diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/DouglasPeuckerSimplifier.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/DouglasPeuckerSimplifier.java index ac76603a5e..60ab75c6a8 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/DouglasPeuckerSimplifier.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/DouglasPeuckerSimplifier.java @@ -11,6 +11,8 @@ */ package com.onthegomap.planetiler.geo; +import java.util.ArrayList; +import java.util.List; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateSequence; import org.locationtech.jts.geom.Geometry; @@ -45,6 +47,22 @@ public static Geometry simplify(Geometry geom, double distanceTolerance) { return (new DPTransformer(distanceTolerance)).transform(geom); } + /** + * Returns a copy of {@code coords}, simplified using Douglas Peucker Algorithm. + * + * @param coords the coordinate list to simplify + * @param distanceTolerance the threshold below which we discard points + * @param area true if this is a polygon to retain at least 4 points to avoid collapse + * @return the simplified coordinate list + */ + public static List simplify(List coords, double distanceTolerance, boolean area) { + if (coords.isEmpty()) { + return List.of(); + } + + return (new DPTransformer(distanceTolerance)).transformCoordinateList(coords, area); + } + private static class DPTransformer extends GeometryTransformer { private final double sqTolerance; @@ -84,6 +102,42 @@ private static double getSqSegDist(double px, double py, double p1x, double p1y, return dx * dx + dy * dy; } + private void subsimplify(List in, List out, int first, int last, int numForcedPoints) { + // numForcePoints lets us keep some points even if they are below simplification threshold + boolean force = numForcedPoints > 0; + double maxSqDist = force ? -1 : sqTolerance; + int index = -1; + Coordinate p1 = in.get(first); + Coordinate p2 = in.get(last); + double p1x = p1.x; + double p1y = p1.y; + double p2x = p2.x; + double p2y = p2.y; + + int i = first + 1; + Coordinate furthest = null; + for (Coordinate coord : in.subList(first + 1, last)) { + double sqDist = getSqSegDist(coord.x, coord.y, p1x, p1y, p2x, p2y); + + if (sqDist > maxSqDist) { + index = i; + furthest = coord; + maxSqDist = sqDist; + } + i++; + } + + if (force || maxSqDist > sqTolerance) { + if (index - first > 1) { + subsimplify(in, out, first, index, numForcedPoints - 1); + } + out.add(furthest); + if (last - index > 1) { + subsimplify(in, out, index, last, numForcedPoints - 2); + } + } + } + private void subsimplify(CoordinateSequence in, MutableCoordinateSequence out, int first, int last, int numForcedPoints) { // numForcePoints lets us keep some points even if they are below simplification threshold @@ -117,6 +171,20 @@ private void subsimplify(CoordinateSequence in, MutableCoordinateSequence out, i } } + protected List transformCoordinateList(List coords, boolean area) { + if (coords.isEmpty()) { + return coords; + } + // make sure we include the first and last points even if they are closer than the simplification threshold + List result = new ArrayList<>(); + result.add(coords.getFirst()); + // for polygons, additionally keep at least 2 intermediate points even if they are below simplification threshold + // to avoid collapse. + subsimplify(coords, result, 0, coords.size() - 1, area ? 2 : 0); + result.add(coords.getLast()); + return result; + } + @Override protected CoordinateSequence transformCoordinates(CoordinateSequence coords, Geometry parent) { boolean area = parent instanceof LinearRing; diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/LoopLineMerger.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/LoopLineMerger.java index 26599983a0..a3e8ef2128 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/LoopLineMerger.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/LoopLineMerger.java @@ -199,34 +199,10 @@ private void removeShortEdges() { } private void simplify() { - Map mainEdgeToLineString = new HashMap<>(); for (var node : output) { for (var edge : node.getEdges()) { if (edge.main) { - mainEdgeToLineString.put(edge, factory.createLineString(edge.coordinates.toArray(Coordinate[]::new))); - } - } - } - for (var mainEdge : mainEdgeToLineString.keySet()) { - var line = mainEdgeToLineString.get(mainEdge); - if (line.getNumPoints() <= 2) { - continue; - } - Geometry simplified = DouglasPeuckerSimplifier.simplify(line, tolerance); - if (simplified instanceof LineString simpleLineString) { - mainEdgeToLineString.put(mainEdge, simpleLineString); - } else { - // TODO handle error - // LOGGER.warn("line string merge simplify emitted {}", simplified.getGeometryType()); - } - } - for (var node : output) { - for (var edge : node.getEdges()) { - if (edge.main) { - edge.setCoordinates(List.of(mainEdgeToLineString.get(edge).getCoordinates())); - } - else { - edge.setCoordinates(List.of(mainEdgeToLineString.get(edge.reversed).getCoordinates()).reversed()); + edge.simplify(); } } } @@ -460,15 +436,22 @@ public void setCoordinates(List coordinates) { } double angleTo(Edge other) { - assert from.equals(other.from); + assert from.equals(other.from); assert coordinates.size() >= 2; double angle = Angle.angle(coordinates.get(0), coordinates.get(1)); double angleOther = Angle.angle(other.coordinates.get(0), other.coordinates.get(1)); - + return Math.abs(Angle.normalize(angle - angleOther)); } + public void simplify() { + coordinates = DouglasPeuckerSimplifier.simplify(coordinates, tolerance, false); + if (reversed != null) { + reversed.coordinates = coordinates.reversed(); + } + } + @Override public String toString() { return "Edge{" + from.id + "->" + to.id + (main ? "" : "(R)") + ": [" + coordinates.getFirst() + ".." + diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/DouglasPeuckerSimplifierTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/DouglasPeuckerSimplifierTest.java index e2a18899c9..857a00bc04 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/DouglasPeuckerSimplifierTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/DouglasPeuckerSimplifierTest.java @@ -4,9 +4,13 @@ import static com.onthegomap.planetiler.TestUtils.newLineString; import static com.onthegomap.planetiler.TestUtils.newPolygon; import static com.onthegomap.planetiler.TestUtils.rectangle; +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.List; import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.Polygonal; import org.locationtech.jts.geom.util.AffineTransformation; class DouglasPeuckerSimplifierTest { @@ -16,10 +20,18 @@ class DouglasPeuckerSimplifierTest { private void testSimplify(Geometry in, Geometry expected, double amount) { for (int rotation : rotations) { var rotate = AffineTransformation.rotationInstance(Math.PI * rotation / 180); + var expRot = rotate.transform(expected); + var inRot = rotate.transform(in); assertSameNormalizedFeature( - rotate.transform(expected), - DouglasPeuckerSimplifier.simplify(rotate.transform(in), amount) + expRot, + DouglasPeuckerSimplifier.simplify(inRot, amount) ); + + // ensure the List version also works... + List inList = List.of(inRot.getCoordinates()); + List expList = List.of(expRot.getCoordinates()); + List actual = DouglasPeuckerSimplifier.simplify(inList, amount, in instanceof Polygonal); + assertEquals(expList, actual); } } @@ -65,8 +77,8 @@ void testPolygonLeaveAPoint() { rectangle(0, 10), newPolygon( 0, 0, - 10, 10, 10, 0, + 10, 10, 0, 0 ), 20