From 214aa71f79259581506b08a8d3f34295c34ef740 Mon Sep 17 00:00:00 2001 From: Kevin Smith Date: Tue, 12 May 2026 20:20:02 -0700 Subject: [PATCH] Add SegmentInterpolator extension point for Densifier. Implement FractalSegmentInterpolator for naturalistic random fractal densification. Signed-off-by: Kevin Smith --- .../locationtech/jts/densify/Densifier.java | 79 +++++++++++++------ .../densify/FractalSegmentInterpolator.java | 54 +++++++++++++ .../jts/densify/SegmentInterpolator.java | 39 +++++++++ .../densify/SteppingSegmentInterpolator.java | 72 +++++++++++++++++ .../StraightSteppingSegmentInterpolator.java | 33 ++++++++ ...traightSubdividingSegmentInterpolator.java | 32 ++++++++ .../SubdividingSegmentInterpolator.java | 78 ++++++++++++++++++ .../jts/densify/DensifierTest.java | 29 +++++++ 8 files changed, 391 insertions(+), 25 deletions(-) create mode 100644 modules/core/src/main/java/org/locationtech/jts/densify/FractalSegmentInterpolator.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/densify/SegmentInterpolator.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/densify/SteppingSegmentInterpolator.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/densify/StraightSteppingSegmentInterpolator.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/densify/StraightSubdividingSegmentInterpolator.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/densify/SubdividingSegmentInterpolator.java diff --git a/modules/core/src/main/java/org/locationtech/jts/densify/Densifier.java b/modules/core/src/main/java/org/locationtech/jts/densify/Densifier.java index 47b91759cf..9bc98160f8 100644 --- a/modules/core/src/main/java/org/locationtech/jts/densify/Densifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/densify/Densifier.java @@ -11,6 +11,8 @@ */ package org.locationtech.jts.densify; +import java.util.Iterator; + import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateList; import org.locationtech.jts.geom.CoordinateSequence; @@ -56,6 +58,22 @@ public static Geometry densify(Geometry geom, double distanceTolerance) { return densifier.getResultGeometry(); } + /** + * Densifies a geometry using a given distance tolerance, and respecting the + * input geometry's {@link PrecisionModel}. + * + * @param geom the geometry to densify + * @param distanceTolerance the distance tolerance to densify + * @param interpolator The segment interpolator to use + * @return the densified geometry + */ + public static Geometry densify(Geometry geom, double distanceTolerance, SegmentInterpolator interpolator) { + Densifier densifier = new Densifier(geom); + densifier.setInterpolator(interpolator); + densifier.setDistanceTolerance(distanceTolerance); + return densifier.getResultGeometry(); + } + /** * Densifies a list of coordinates. * @@ -63,8 +81,8 @@ public static Geometry densify(Geometry geom, double distanceTolerance) { * @param distanceTolerance the densify tolerance * @return the densified coordinate sequence */ - private static Coordinate[] densifyPoints(Coordinate[] pts, - double distanceTolerance, PrecisionModel precModel) { + private static Coordinate[] densifyPoints(Coordinate[] pts, double distanceTolerance, PrecisionModel precModel, + SegmentInterpolator interpolator) { LineSegment seg = new LineSegment(); CoordinateList coordList = new CoordinateList(); for (int i = 0; i < pts.length - 1; i++) { @@ -78,15 +96,10 @@ private static Coordinate[] densifyPoints(Coordinate[] pts, continue; // densify the segment - int densifiedSegCount = (int) Math.ceil(len / distanceTolerance); - double densifiedSegLen = len / densifiedSegCount; - for (int j = 1; j < densifiedSegCount; j++) { - double segFract = (j * densifiedSegLen) / len; - Coordinate p = seg.pointAlong(segFract); - if(!Double.isNaN(seg.p0.z) && !Double.isNaN(seg.p1.z)) { - p.setZ(seg.p0.z + segFract * (seg.p1.z - seg.p0.z)); - } - precModel.makePrecise(p); + Iterator it = interpolator.densifySegment(seg, pts, i, distanceTolerance); + while (it.hasNext()) { + Coordinate p = it.next(); + precModel.makePrecise(p); coordList.add(p, false); } } @@ -100,6 +113,8 @@ private static Coordinate[] densifyPoints(Coordinate[] pts, private double distanceTolerance; + private SegmentInterpolator interpolator = new StraightSteppingSegmentInterpolator(); + /** * Indicates whether areas should be topologically validated. */ @@ -136,30 +151,44 @@ public void setDistanceTolerance(double distanceTolerance) { public void setValidate(boolean isValidated) { this.isValidated = isValidated; } - + + /** + * Sets the interpolator used to generate new points for each segment + * + * @param interpolator the interpolator to use + */ + public void setInterpolator(SegmentInterpolator interpolator) { + this.interpolator = interpolator; + } + /** * Gets the densified geometry. * * @return the densified geometry */ public Geometry getResultGeometry() { - return (new DensifyTransformer(distanceTolerance, isValidated)).transform(inputGeom); + return (new DensifyTransformer(distanceTolerance, isValidated, interpolator)).transform(inputGeom); } static class DensifyTransformer extends GeometryTransformer { - double distanceTolerance; - private boolean isValidated; - - DensifyTransformer(double distanceTolerance, boolean isValidated) { - this.distanceTolerance = distanceTolerance; - this.isValidated = isValidated; - } - - protected CoordinateSequence transformCoordinates( - CoordinateSequence coords, Geometry parent) { + double distanceTolerance; + private boolean isValidated; + SegmentInterpolator interpolator; + + DensifyTransformer(double distanceTolerance, boolean isValidated) { + this(distanceTolerance, isValidated, new StraightSteppingSegmentInterpolator()); + } + + DensifyTransformer(double distanceTolerance, boolean isValidated, SegmentInterpolator interpolator) { + this.distanceTolerance = distanceTolerance; + this.isValidated = isValidated; + this.interpolator = interpolator; + } + + protected CoordinateSequence transformCoordinates(CoordinateSequence coords, Geometry parent) { Coordinate[] inputPts = coords.toCoordinateArray(); - Coordinate[] newPts = Densifier - .densifyPoints(inputPts, distanceTolerance, parent.getPrecisionModel()); + Coordinate[] newPts = Densifier.densifyPoints(inputPts, distanceTolerance, parent.getPrecisionModel(), + interpolator); // prevent creation of invalid linestrings if (parent instanceof LineString && newPts.length == 1) { newPts = new Coordinate[0]; diff --git a/modules/core/src/main/java/org/locationtech/jts/densify/FractalSegmentInterpolator.java b/modules/core/src/main/java/org/locationtech/jts/densify/FractalSegmentInterpolator.java new file mode 100644 index 0000000000..c9e6175f6d --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/densify/FractalSegmentInterpolator.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026 Kevin Smith. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ + +package org.locationtech.jts.densify; + +import java.util.Random; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineSegment; + +/** + * Produces points by dividing the segment in two, offsetting perpendicularly by + * a random amount proportional to the length of the segment and repeating until + * the threshold is met. + *

+ * The result is a random fractal shape resembling a natural coastline or river. + * + * @author Kevin Smith + */ +public class FractalSegmentInterpolator extends SubdividingSegmentInterpolator { + + private static final double MIDWAY = 0.5; + + /** + * Create a FractalSegmentInterpolator + * + * @param proportion Maximum offset as a proportion of the segment length + * @param rand Random number generator to use + */ + public FractalSegmentInterpolator(double proportion, Random rand) { + this.rand = rand; + this.proportion = proportion; + } + + private final Random rand; + private final double proportion; + + @Override + public Coordinate midpoint(LineSegment seg) { + Coordinate p = seg.pointAlongOffset(MIDWAY, seg.getLength() * (rand.nextDouble() * 2 - 1) * proportion); + fillZ(seg, p); + return p; + } + +} \ No newline at end of file diff --git a/modules/core/src/main/java/org/locationtech/jts/densify/SegmentInterpolator.java b/modules/core/src/main/java/org/locationtech/jts/densify/SegmentInterpolator.java new file mode 100644 index 0000000000..0b1f2bd977 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/densify/SegmentInterpolator.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026 Kevin Smith. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ + +package org.locationtech.jts.densify; + +import java.util.Iterator; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineSegment; + +/** + * A scheme used to generate intermediate points + * + * @author Kevin Smith + */ +@FunctionalInterface +public interface SegmentInterpolator { + /** + * Generate the additional points along the given segment of a coordinate array + * + * @param seg The current segment being interpolated + * @param pts The full sequence of original points + * @param i The index of the starting element of the current + * segment in the full sequence + * @param distanceTolerance The maximum length allowable between consecutive + * result points + * @return iterator over the intermediate points to divide the given segment + */ + Iterator densifySegment(LineSegment seg, Coordinate[] pts, int i, double distanceTolerance); +} \ No newline at end of file diff --git a/modules/core/src/main/java/org/locationtech/jts/densify/SteppingSegmentInterpolator.java b/modules/core/src/main/java/org/locationtech/jts/densify/SteppingSegmentInterpolator.java new file mode 100644 index 0000000000..e2d1408573 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/densify/SteppingSegmentInterpolator.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2016 Vivid Solutions. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ + +package org.locationtech.jts.densify; + +import java.util.Iterator; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineSegment; + +/** + * Interpolator which generates points using a function of segFract, where + * segFract is evenly stepped along the length of the segment. + * + * @author Kevin Smith + */ +public abstract class SteppingSegmentInterpolator implements SegmentInterpolator { + @Override + public Iterator densifySegment(LineSegment seg, Coordinate[] pts, int i, double distanceTolerance) { + return new Iterator() { + int j = 1; + double len = seg.getLength(); + int densifiedSegCount = (int) Math.ceil(len / distanceTolerance); + double densifiedSegLen = len / densifiedSegCount; + + @Override + public boolean hasNext() { + return j < densifiedSegCount; + } + + @Override + public Coordinate next() { + double segFract = (j * densifiedSegLen) / len; + j++; + return pointAlong(seg, segFract); + } + + }; + + } + + /** + * Generate a point a given proportion of the way along a segment. + * @param seg A segment to interpolate along + * @param segFract A value between 0 and 1 representing a position along the + * segment + * @return A point along the segment + */ + public abstract Coordinate pointAlong(LineSegment seg, double segFract); + + /** + * Fill in the z value of a point along a segment if the both endpoints have z set. + * + * @param seg + * @param segFract + * @param p + */ + protected final void fillZ(LineSegment seg, double segFract, Coordinate p) { + if (!Double.isNaN(seg.p0.z) && !Double.isNaN(seg.p1.z)) { + p.setZ(seg.p0.z + segFract * (seg.p1.z - seg.p0.z)); + } + } +} \ No newline at end of file diff --git a/modules/core/src/main/java/org/locationtech/jts/densify/StraightSteppingSegmentInterpolator.java b/modules/core/src/main/java/org/locationtech/jts/densify/StraightSteppingSegmentInterpolator.java new file mode 100644 index 0000000000..aabdf8550b --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/densify/StraightSteppingSegmentInterpolator.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026 Kevin Smith. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ + +package org.locationtech.jts.densify; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineSegment; + +/** + * Produces a minimal set of intermediate points evenly spaced along the + * segment. + * + * @author Kevin Smith + */ +public class StraightSteppingSegmentInterpolator extends SteppingSegmentInterpolator { + + @Override + public Coordinate pointAlong(LineSegment seg, double segFract) { + Coordinate p = seg.pointAlong(segFract); + fillZ(seg, segFract, p); + return p; + } + +} \ No newline at end of file diff --git a/modules/core/src/main/java/org/locationtech/jts/densify/StraightSubdividingSegmentInterpolator.java b/modules/core/src/main/java/org/locationtech/jts/densify/StraightSubdividingSegmentInterpolator.java new file mode 100644 index 0000000000..733ea3ad69 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/densify/StraightSubdividingSegmentInterpolator.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026 Kevin Smith. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ + +package org.locationtech.jts.densify; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineSegment; + +/** + * Produces points dividing the segment into a power of two in a straight line. + * + * @author Kevin Smith + */ +public class StraightSubdividingSegmentInterpolator extends SubdividingSegmentInterpolator { + + @Override + public Coordinate midpoint(LineSegment seg) { + Coordinate p = seg.midPoint(); + fillZ(seg, p); + return p; + } + +} \ No newline at end of file diff --git a/modules/core/src/main/java/org/locationtech/jts/densify/SubdividingSegmentInterpolator.java b/modules/core/src/main/java/org/locationtech/jts/densify/SubdividingSegmentInterpolator.java new file mode 100644 index 0000000000..1af05b96fe --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/densify/SubdividingSegmentInterpolator.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026 Kevin Smith. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ + +package org.locationtech.jts.densify; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineSegment; + +/** + * Interpolator which generates points by dividing the segment in two and then + * repeating recursively until all the segments are under the threshold. + * + * @author Kevin Smith + */ +public abstract class SubdividingSegmentInterpolator implements SegmentInterpolator { + @Override + public Iterator densifySegment(LineSegment seg, Coordinate[] pts, int i, double distanceTolerance) { + final Deque stack = new ArrayDeque(); + stack.push(seg); + + return new Iterator() { + + @Override + public boolean hasNext() { + return stack.size() > 1 || stack.peek().getLength() > distanceTolerance; + } + + @Override + public Coordinate next() { + + LineSegment currentSeg = stack.pop(); + + while (currentSeg.getLength() > distanceTolerance) { + Coordinate midpoint = midpoint(currentSeg); + stack.push(new LineSegment(midpoint, currentSeg.p1)); + currentSeg.p1 = midpoint; + } + + return currentSeg.p1; + } + + }; + } + + /** + * Generate a point midway along a segment + * + * @param seg + * @return + */ + public abstract Coordinate midpoint(LineSegment seg); + + /** + * Fill in the z value of the midpoint of a segment if the both endpoints have z + * set. + * + * @param seg + * @param p + */ + protected final void fillZ(LineSegment seg, Coordinate p) { + if (!Double.isNaN(seg.p0.z) && !Double.isNaN(seg.p1.z)) { + p.setZ((seg.p0.z + seg.p1.z) / 2.0); + } + } +} \ No newline at end of file diff --git a/modules/core/src/test/java/org/locationtech/jts/densify/DensifierTest.java b/modules/core/src/test/java/org/locationtech/jts/densify/DensifierTest.java index dbf8a7770a..05322689ad 100644 --- a/modules/core/src/test/java/org/locationtech/jts/densify/DensifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/densify/DensifierTest.java @@ -11,6 +11,8 @@ */ package org.locationtech.jts.densify; +import java.util.Random; + import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateXY; import org.locationtech.jts.geom.Geometry; @@ -94,13 +96,38 @@ public void testDimension3d() { assertEquals(3, line.getCoordinateSequence().getDimension()); } + public void testStepping() { + checkDensifyWithInterpolator("LINESTRING (0 0, 0 24)", 10, new StraightSteppingSegmentInterpolator(), + "LINESTRING (0 0, 0 8, 0 16, 0 24)"); + } + + public void testSubdividing() { + checkDensifyWithInterpolator("LINESTRING (0 0, 0 24)", 10, new StraightSubdividingSegmentInterpolator(), + "LINESTRING (0 0, 0 6, 0 12, 0 18, 0 24)"); + } + public void testFractal() { + final double segment = 10; + Geometry geom = read("LINESTRING (0 0, 0 24)"); + Geometry actual = Densifier.densify(geom, (double) segment, new FractalSegmentInterpolator(0.5, new Random())); + assertTrue("Densified geometry has length less than original", actual.getLength() > geom.getLength()); + assertTrue("Densified geometry has length longer than expected after random fractalization", + actual.getLength() < geom.getLength() * Math.pow(2, geom.getLength() / segment / 2.0)); + } + private void checkDensify(String wkt, double distanceTolerance, String wktExpected) { Geometry geom = read(wkt); Geometry expected = read(wktExpected); Geometry actual = Densifier.densify(geom, distanceTolerance); checkEqual(expected, actual, TOLERANCE); } + + private void checkDensifyWithInterpolator(String wkt, double distanceTolerance, SegmentInterpolator interpolator, String wktExpected) { + Geometry geom = read(wkt); + Geometry expected = read(wktExpected); + Geometry actual = Densifier.densify(geom, distanceTolerance, interpolator); + checkEqual(expected, actual, TOLERANCE); + } private void checkDensifyXYZ(String wkt, double distanceTolerance, String wktExpected) { Geometry geom = read(wkt); @@ -108,6 +135,7 @@ private void checkDensifyXYZ(String wkt, double distanceTolerance, String wktExp Geometry actual = Densifier.densify(geom, distanceTolerance); checkEqualXYZ(expected, actual); } + /** * Note: it's hard to construct a geometry which would actually be invalid when densified. * This test just checks that the code path executes. @@ -126,4 +154,5 @@ private void checkDensifyNoValidate(String wkt, double distanceTolerance, String checkEqual(expected, actual, TOLERANCE); } + }