From e482476d9bb5a76e46c798c1039bb5cf36c75d1c Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Wed, 4 Dec 2024 05:47:21 -0500 Subject: [PATCH] split midpoint/dual midpoint --- .../planetiler/geo/DualMidpointSmoother.java | 218 ++++++++++++++++++ .../planetiler/geo/GeometryPipeline.java | 24 +- .../planetiler/geo/MidpointSmoother.java | 211 +---------------- .../geo/DualMidpointSmootherTest.java | 119 ++++++++++ .../planetiler/geo/MidpointSmootherTest.java | 100 +------- 5 files changed, 368 insertions(+), 304 deletions(-) create mode 100644 planetiler-core/src/main/java/com/onthegomap/planetiler/geo/DualMidpointSmoother.java create mode 100644 planetiler-core/src/test/java/com/onthegomap/planetiler/geo/DualMidpointSmootherTest.java diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/DualMidpointSmoother.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/DualMidpointSmoother.java new file mode 100644 index 0000000000..6026cb445b --- /dev/null +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/DualMidpointSmoother.java @@ -0,0 +1,218 @@ +package com.onthegomap.planetiler.geo; + +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.util.GeometryTransformer; + +/** + * Smooths an input geometry by interpolating 2 points at certain ratios along each edge and repeating for a set number + * of iterations. This can be thought of as slicing off of each vertex until the segments are so short it appears round. + *

+ * When the points are {@code [0.25, 0.75]} this is equivalent to + * Chaikin Smoothing. + */ +public class DualMidpointSmoother extends GeometryTransformer implements GeometryPipeline { + + private final double a; + private final double b; + private int iters = 1; + private double minSquaredVertexTolerance = 0; + private double minVertexArea = 0; + private double maxArea = 0; + private double maxSquaredOffset = 0; + + public DualMidpointSmoother(double a, double b) { + this.a = a; + this.b = b; + } + + /** + * Returns a new smoother that does Chaikin + * Smoothing {@code iters} times on the input line. + */ + public static DualMidpointSmoother chaikin(int iters) { + return new DualMidpointSmoother(0.25, 0.75).setIters(iters); + } + + /** + * Returns a new smoother that does Chaikin + * Smoothing but instead of stopping after a fixed number of iterations, it stops when the added points would get + * dropped with {@link DouglasPeuckerSimplifier Douglas-Peucker simplification} with {@code tolerance} threshold. + */ + public static DualMidpointSmoother chaikinToTolerance(double tolerance) { + return chaikin(10).setMinVertexTolerance(tolerance); + } + + /** + * Returns a new smoother that does Chaikin + * Smoothing but instead of stopping after a fixed number of iterations, it stops when the added points would get + * dropped with {@link VWSimplifier Visvalingam-Whyatt simplification} with {@code minArea} threshold. + */ + public static DualMidpointSmoother chaikinToMinArea(double minArea) { + return chaikin(10).setMinVertexArea(minArea); + } + + /** + * Sets the min distance between a vertex and the line between its neighbors below which a vertex will not be + * squashed. + *

+ * If all points are below this threshold for an entire iteration then smoothing will stop before the maximum number + * of iterations is reached. + */ + public DualMidpointSmoother setMinVertexTolerance(double minVertexTolerance) { + this.minSquaredVertexTolerance = minVertexTolerance * minVertexTolerance; + return this; + } + + /** + * Sets the min area of the triangle created by a point and its 2 neighbors below which a vertex will not be squashed. + *

+ * If all points are below this threshold for an entire iteration then smoothing will stop before the maximum number + * of iterations is reached. + */ + public DualMidpointSmoother setMinVertexArea(double minVertexArea) { + this.minVertexArea = minVertexArea; + return this; + } + + /** + * Sets a limit on the maximum area that can be removed when removing a vertex. When the area is larger than this, the + * new points are moved closer to the vertex in order to bring the removed area down to this threshold. + *

+ * This prevents smoothing 2 long adjacent edges from introducing a large deviation from the original shape. + */ + public DualMidpointSmoother setMaxArea(double maxArea) { + this.maxArea = maxArea; + return this; + } + + /** + * Sets a limit on the maximum distance from the original shape that can be introduced by smoothing. When the error + * introduced by squashing a vertex will be above this threshold, the new points are moved closer to the vertex in + * order to bring the distance down to this threshold. + *

+ * This prevents smoothing 2 long adjacent edges from introducing a large deviation from the original shape. + */ + public DualMidpointSmoother setMaxOffset(double maxOffset) { + this.maxSquaredOffset = maxOffset * maxOffset; + return this; + } + + /** Sets the number of times that smoothing runs. */ + public DualMidpointSmoother setIters(int iters) { + this.iters = iters; + return this; + } + + @Override + public Geometry apply(Geometry input) { + return transform(input); + } + + @Override + protected CoordinateSequence transformCoordinates(CoordinateSequence coords, Geometry parent) { + boolean area = parent instanceof LinearRing; + if (coords.size() <= 2) { + return coords.copy(); + } + boolean checkVertices = minSquaredVertexTolerance > 0 || minVertexArea > 0 || maxSquaredOffset > 0 || maxArea > 0; + for (int iter = 0; iter < iters; iter++) { + MutableCoordinateSequence result = new MutableCoordinateSequence(); + if (!area) { + result.addPoint(coords.getX(0), coords.getY(0)); + } + int last = coords.size() - 1; + double x2 = coords.getX(0); + double y2 = coords.getY(0); + double nextA = a; + boolean skippedLastVertex = false; + double x1, y1; + for (int i = 0; i < last; i++) { + x1 = x2; + y1 = y2; + x2 = coords.getX(i + 1); + y2 = coords.getY(i + 1); + double dx = x2 - x1; + double dy = y2 - y1; + + if ((area || i > 0) && !skippedLastVertex) { + result.addPoint(x1 + dx * nextA, y1 + dy * nextA); + } + nextA = a; + + if (area || i < last - 1) { + double nextB = b; + if (checkVertices) { + int next = i < last - 1 ? (i + 2) : 1; + double x3 = coords.getX(next); + double y3 = coords.getY(next); + if (skipVertex(x1, y1, x2, y2, x3, y3)) { + result.addPoint(x2, y2); + skippedLastVertex = true; + continue; + } + skippedLastVertex = false; + + // check the amount of error introduced by removing this vertex (either by offset or area) + // and if it is too large, then move nextA/nextB ratios closer to the vertex to limit the error + if (maxArea > 0 || maxSquaredOffset > 0) { + double magA = Math.hypot(x2 - x1, y2 - y1); + double magB = Math.hypot(x3 - x2, y3 - y2); + double den = magA * magB; + double aDist = magA * (1 - b); + double bDist = magB * a; + double maxDistSquared = Double.POSITIVE_INFINITY; + if (maxArea > 0) { + double sin = den <= 0 ? 0 : Math.abs(((x1 - x2) * (y3 - y2)) - ((y1 - y2) * (x3 - x2))) / den; + maxDistSquared = 2 * maxArea / sin; + } + if (maxSquaredOffset > 0) { + double cos = den <= 0 ? 0 : Math.clamp(((x1 - x2) * (x3 - x2) + (y1 - y2) * (y3 - y2)) / den, -1, 1); + maxDistSquared = Math.min(maxDistSquared, 2 * maxSquaredOffset / (1 + cos)); + } + double maxDist = Double.NaN; + if (aDist * aDist > maxDistSquared) { + nextB = 1 - (maxDist = Math.sqrt(maxDistSquared)) / magA; + } + if (bDist * bDist > maxDistSquared) { + if (Double.isNaN(maxDist)) { + maxDist = Math.sqrt(maxDistSquared); + } + nextA = maxDist / magA; + } + } + } + + result.addPoint(x1 + dx * nextB, y1 + dy * nextB); + } + } + if (area) { + if (skippedLastVertex) { + result.setX(0, result.getX(result.size() - 1)); + result.setY(0, result.getY(result.size() - 1)); + } else { + if (nextA != a) { + result.setX(0, (coords.getX(1) - coords.getX(0)) * nextA); + result.setY(0, (coords.getY(1) - coords.getY(0)) * nextA); + } + result.closeRing(); + } + } else { + result.addPoint(coords.getX(last), coords.getY(last)); + } + // early exit case: if no vertices were collapsed then we are done smoothing + if (coords.size() == result.size()) { + return result; + } + coords = result; + } + return coords; + } + + private boolean skipVertex(double x1, double y1, double x2, double y2, double x3, double y3) { + return (minVertexArea > 0 && VWSimplifier.triangleArea(x1, y1, x2, y2, x3, y3) < minVertexArea) || + (minSquaredVertexTolerance > 0 && + DouglasPeuckerSimplifier.getSqSegDist(x2, y2, x1, y1, x3, y3) < minSquaredVertexTolerance); + } +} diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeometryPipeline.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeometryPipeline.java index 6b12a3a695..f485458fd2 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeometryPipeline.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeometryPipeline.java @@ -106,23 +106,31 @@ static DouglasPeuckerSimplifier simplifyDP(double tolerance) { * Returns a pipeline that smooths an input geometry by joining the midpoint of each edge of lines or polygons in the * order in which they are encountered. */ - static MidpointSmoother smoothMidpoint() { - return MidpointSmoother.midpoint(); + static MidpointSmoother smoothMidpoint(int iters) { + return new MidpointSmoother().setIters(iters); } /** * Returns a pipeline that smooths an input geometry by slicing off each corner {@code iters} times until you get a * sufficiently smooth curve. */ - static MidpointSmoother smoothChaikin(int iters) { - return MidpointSmoother.chaikin(iters); + static DualMidpointSmoother smoothChaikin(int iters) { + return DualMidpointSmoother.chaikin(iters); } - static MidpointSmoother smoothChaikinToTolerance(double tolerance) { - return MidpointSmoother.chaikinToTolerance(tolerance); + /** + * Returns a new smoother that slices off each corner, except instead of stopping at a fixed number of iterations, it + * stops when the distance between a vertex and its 2 adjacent neighbors is below {@code tolerance}. + */ + static DualMidpointSmoother smoothChaikinToTolerance(double tolerance) { + return DualMidpointSmoother.chaikinToTolerance(tolerance); } - static MidpointSmoother smoothChaikinToMinArea(double minArea) { - return MidpointSmoother.chaikinToMinArea(minArea); + /** + * Returns a new smoother that slices off each corner, except instead of stopping at a fixed number of iterations, it + * stops when the area formed between a vertex and its 2 adjacent neighbors is below {@code minArea}. + */ + static DualMidpointSmoother smoothChaikinToMinArea(double minArea) { + return DualMidpointSmoother.chaikinToMinArea(minArea); } } diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/MidpointSmoother.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/MidpointSmoother.java index 6a970c3010..4f6580c56c 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/MidpointSmoother.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/MidpointSmoother.java @@ -1,111 +1,27 @@ package com.onthegomap.planetiler.geo; -import java.util.Arrays; import org.locationtech.jts.geom.CoordinateSequence; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.util.GeometryTransformer; /** - * Smooths an input geometry by interpolating points along each edge and repeating for a set number of iterations. - *

- * When the points array is {@code [0.5]} this means midpoint smoothing, and when it is {@code [0.25, 0.75]} it means - * Chaikin Smoothing. + * Smooths an input geometry by iteratively joining the midpoint of each edge of lines or polygons in the order in which + * they are encountered. + * + * @see github.com/wipfli/midpoint-smoothing. */ public class MidpointSmoother extends GeometryTransformer implements GeometryPipeline { - private static final double[] CHAIKIN = new double[]{0.25, 0.75}; - private static final double[] MIDPOINT = new double[]{0.5}; - private final double[] points; + private final double ratio; private int iters = 1; - private double minSquaredVertexTolerance = 0; - private double minVertexArea = 0; - private double maxArea = 0; - private double maxSquaredOffset = 0; - public MidpointSmoother(double[] points) { - if (points.length < 1 || points.length > 2) { - throw new IllegalArgumentException("Smoothing only works with 1 or 2 points along each edge, got %s".formatted( - Arrays.toString(points))); - } - this.points = points; - } - - /** - * Returns a new smoother that does Chaikin - * Smoothing {@code iters} times on the input line. - */ - public static MidpointSmoother chaikin(int iters) { - return new MidpointSmoother(CHAIKIN).setIters(iters); - } - - /** - * Returns a new smoother that does Chaikin - * Smoothing until the added points would get dropped with {@link DouglasPeuckerSimplifier Douglas-Peucker - * simplification} with {@code tolerance} threshold. - */ - public static MidpointSmoother chaikinToTolerance(double tolerance) { - return new MidpointSmoother(CHAIKIN).setIters(10).setMinVertexTolerance(tolerance); - } - - /** - * Returns a new smoother that does Chaikin - * Smoothing until the added points would get dropped with {@link VWSimplifier Visvalingam-Whyatt simplification} - * with {@code minArea} threshold. - */ - public static MidpointSmoother chaikinToMinArea(double minArea) { - return new MidpointSmoother(CHAIKIN).setIters(10).setMinVertexArea(minArea); + public MidpointSmoother(double ratio) { + this.ratio = ratio; } - /** - * Sets the min distance between a vertex and the line between its neighbors below which a vertex will not be - * squashed. - *

- * If all points are below this threshold for an entire iteration then smoothing will stop before the maximum number - * of iterations is reached. - */ - public MidpointSmoother setMinVertexTolerance(double minVertexTolerance) { - this.minSquaredVertexTolerance = minVertexTolerance * minVertexTolerance; - return this; - } - - /** - * Sets the min area of the triangle created by a point and its 2 neighbors below which a vertex will not be squashed. - *

- * If all points are below this threshold for an entire iteration then smoothing will stop before the maximum number - * of iterations is reached. - */ - public MidpointSmoother setMinVertexArea(double minVertexArea) { - this.minVertexArea = minVertexArea; - return this; - } - - /** - * Sets a limit on the maximum area that can be removed when removing a vertex. When the area is larger than this, the - * new points are moved closer to the vertex in order to bring the removed area to this threshold. - *

- * This prevents smoothing 2 long adjacent edges from introducing a large deviation from the original shape. - */ - public MidpointSmoother setMaxArea(double maxArea) { - this.maxArea = maxArea; - return this; - } - - /** - * Sets a limit on the maximum distance from the original shape that can be introduced by smoothing. When the error - * introduced by squashing a vertex will be above this threshold, the new points are moved closer to the vertex in - * order to bring the removed area to this threshold. - *

- * This prevents smoothing 2 long adjacent edges from introducing a large deviation from the original shape. - */ - public MidpointSmoother setMaxOffset(double maxOffset) { - this.maxSquaredOffset = maxOffset * maxOffset; - return this; - } - - /** Returns a new smoother that connects the points halfway along each edge. */ - public static MidpointSmoother midpoint() { - return new MidpointSmoother(MIDPOINT); + public MidpointSmoother() { + this(0.5); } /** Sets the number of times that smoothing runs. */ @@ -125,15 +41,6 @@ protected CoordinateSequence transformCoordinates(CoordinateSequence coords, Geo if (coords.size() <= 2) { return coords.copy(); } - if (points.length == 1) { - return singlePointSmooth(coords, area, points[0]); - } else if (points.length >= 2) { - return dualPointSmooth(coords, area, points[0], points[1]); - } - return coords.copy(); - } - - private CoordinateSequence singlePointSmooth(CoordinateSequence coords, boolean area, double ratio) { for (int iter = 0; iter < iters; iter++) { MutableCoordinateSequence result = new MutableCoordinateSequence(); if (!area) { @@ -159,104 +66,4 @@ private CoordinateSequence singlePointSmooth(CoordinateSequence coords, boolean } return coords; } - - private CoordinateSequence dualPointSmooth(CoordinateSequence coords, boolean area, double a, double b) { - boolean checkVertices = minSquaredVertexTolerance > 0 || minVertexArea > 0 || maxSquaredOffset > 0 || maxArea > 0; - for (int iter = 0; iter < iters; iter++) { - MutableCoordinateSequence result = new MutableCoordinateSequence(); - if (!area) { - result.addPoint(coords.getX(0), coords.getY(0)); - } - int last = coords.size() - 1; - double x2 = coords.getX(0); - double y2 = coords.getY(0); - double nextA = a; - boolean skippedLastVertex = false; - double x1, y1; - for (int i = 0; i < last; i++) { - x1 = x2; - y1 = y2; - x2 = coords.getX(i + 1); - y2 = coords.getY(i + 1); - double dx = x2 - x1; - double dy = y2 - y1; - - if ((area || i > 0) && !skippedLastVertex) { - result.addPoint(x1 + dx * nextA, y1 + dy * nextA); - } - nextA = a; - - - if (area || i < last - 1) { - double nextB = b; - if (checkVertices) { - int next = i < last - 1 ? (i + 2) : 1; - double x3 = coords.getX(next); - double y3 = coords.getY(next); - if (skipVertex(x1, y1, x2, y2, x3, y3)) { - result.addPoint(x2, y2); - skippedLastVertex = true; - continue; - } - skippedLastVertex = false; - - if (maxArea > 0 || maxSquaredOffset > 0) { - double magA = Math.hypot(x2 - x1, y2 - y1); - double magB = Math.hypot(x3 - x2, y3 - y2); - double den = magA * magB; - double aDist = magA * (1 - b); - double bDist = magB * a; - double maxDistSquared = Double.POSITIVE_INFINITY; - if (maxArea > 0) { - double sin = den <= 0 ? 0 : Math.abs(((x1 - x2) * (y3 - y2)) - ((y1 - y2) * (x3 - x2))) / den; - maxDistSquared = 2 * maxArea / sin; - } - if (maxSquaredOffset > 0) { - double cos = den <= 0 ? 0 : Math.clamp(((x1 - x2) * (x3 - x2) + (y1 - y2) * (y3 - y2)) / den, -1, 1); - maxDistSquared = Math.min(maxDistSquared, 2 * maxSquaredOffset / (1 + cos)); - } - double maxDist = Double.NaN; - if (aDist * aDist > maxDistSquared) { - nextB = 1 - (maxDist = Math.sqrt(maxDistSquared)) / magA; - } - if (bDist * bDist > maxDistSquared) { - if (Double.isNaN(maxDist)) { - maxDist = Math.sqrt(maxDistSquared); - } - nextA = maxDist / magA; - } - } - } - - result.addPoint(x1 + dx * nextB, y1 + dy * nextB); - } - } - if (area) { - if (skippedLastVertex) { - result.setX(0, result.getX(result.size() - 1)); - result.setY(0, result.getY(result.size() - 1)); - } else { - if (nextA != a) { - result.setX(0, (coords.getX(1) - coords.getX(0)) * nextA); - result.setY(0, (coords.getY(1) - coords.getY(0)) * nextA); - } - result.closeRing(); - } - } else { - result.addPoint(coords.getX(last), coords.getY(last)); - } - // early exit case: if no vertices were collapsed then we are done smoothing - if (coords.size() == result.size()) { - return result; - } - coords = result; - } - return coords; - } - - private boolean skipVertex(double x1, double y1, double x2, double y2, double x3, double y3) { - return (minVertexArea > 0 && VWSimplifier.triangleArea(x1, y1, x2, y2, x3, y3) < minVertexArea) || - (minSquaredVertexTolerance > 0 && - DouglasPeuckerSimplifier.getSqSegDist(x2, y2, x1, y1, x3, y3) < minSquaredVertexTolerance); - } } diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/DualMidpointSmootherTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/DualMidpointSmootherTest.java new file mode 100644 index 0000000000..493bc408b0 --- /dev/null +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/DualMidpointSmootherTest.java @@ -0,0 +1,119 @@ +package com.onthegomap.planetiler.geo; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.onthegomap.planetiler.TestUtils; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.io.ParseException; +import org.locationtech.jts.io.WKTReader; + +class DualDualMidpointSmootherTest { + + @ParameterizedTest + @CsvSource(value = { + "POINT(1 1); POINT(1 1)", + "LINESTRING(0 0, 10 10); LINESTRING(0 0, 10 10)", + "LINESTRING(0 0, 10 0, 10 10); LINESTRING(0 0, 8 0, 10 2, 10 10)", + "LINESTRING(0 0, 10 0, 10 10, 0 10); LINESTRING(0 0, 8 0, 10 2, 10 8, 8 10, 0 10)", + "POLYGON((0 0, 10 0, 10 10, 0 0)); POLYGON((2 0, 8 0, 10 2, 10 8, 8 8, 2 2, 2 0))", + "POLYGON((0 0, 10 0, 10 10, 0 10, 0 0)); POLYGON((2 0, 8 0, 10 2, 10 8, 8 10, 2 10, 0 8, 0 2, 2 0))", + }, delimiter = ';') + void testDualMidpointSmooth(String inWKT, String outWKT) throws ParseException { + var reader = new WKTReader(); + Geometry in = reader.read(inWKT); + Geometry out = reader.read(outWKT); + assertEquals( + TestUtils.round(out), + TestUtils.round(new DualMidpointSmoother(0.2, 0.8).setIters(1).apply(in)) + ); + } + + @ParameterizedTest + @CsvSource(value = { + "POINT(1 1); POINT(1 1)", + "LINESTRING(0 0, 10 10); LINESTRING(0 0, 10 10)", + "LINESTRING(0 0, 10 0, 10 10); LINESTRING(0 0, 6.4 0, 8.4 0.4, 9.6 1.6, 10 3.6, 10 10)", + "POLYGON((0 0, 10 0, 10 10, 0 0)); POLYGON((3.2 0, 6.8 0, 8.4 0.4, 9.6 1.6, 10 3.2, 10 6.8, 9.6 8, 8.4 8, 6.8 6.8, 3.2 3.2, 2 1.6, 2 0.4, 3.2 0))", + }, delimiter = ';') + void testMultiPassSmooth(String inWKT, String outWKT) throws ParseException { + var reader = new WKTReader(); + Geometry in = reader.read(inWKT); + Geometry out = reader.read(outWKT); + assertEquals( + TestUtils.round(out), + TestUtils.round(new DualMidpointSmoother(0.2, 0.8).setIters(2).apply(in)) + ); + } + + @ParameterizedTest + @CsvSource(value = { + "POINT(1 1); POINT(1 1)", + "LINESTRING(0 0, 10 10); LINESTRING(0 0, 10 10)", + "LINESTRING(0 0, 10 0, 10 10); LINESTRING (0 0, 6.4 0, 8 0.32, 8.64 0.64, 9.36 1.36, 9.68 2, 10 3.6, 10 10)", + "POLYGON((0 0, 10 0, 10 10, 0 0)); POLYGON ((3.2 0, 6.8 0, 8.4 0.4, 9.6 1.6, 10 3.2, 10 6.8, 9.68 7.76, 9.36 8, 8.4 8, 6.8 6.8, 3.2 3.2, 2 1.6, 2 0.64, 2.24 0.32, 3.2 0))", + }, delimiter = ';') + void testSmoothToTolerance(String inWKT, String outWKT) throws ParseException { + var reader = new WKTReader(); + Geometry in = reader.read(inWKT); + Geometry out = reader.read(outWKT); + assertEquals( + TestUtils.round(out), + TestUtils.round(new DualMidpointSmoother(0.2, 0.8).setIters(200).setMinVertexTolerance(0.5).apply(in)) + ); + } + + @ParameterizedTest + @CsvSource(value = { + "POINT(1 1); POINT(1 1)", + "LINESTRING(0 0, 10 10); LINESTRING(0 0, 10 10)", + "LINESTRING(0 0, 10 0, 10 10); LINESTRING (0 0, 5.12 0, 6.8 0.08, 8 0.32, 8.64 0.64, 9.36 1.36, 9.68 2, 9.92 3.2, 10 4.88, 10 10)", + "POLYGON((0 0, 10 0, 10 10, 0 0)); POLYGON ((3.92 0, 6.08 0, 7.12 0.08, 8.08 0.32, 8.64 0.64, 9.36 1.36, 9.68 1.92, 9.92 2.88, 10 3.92, 10 6.08, 9.92 7.04, 9.68 7.76, 9.36 8, 8.64 8, 8.08 7.76, 7.12 7.04, 6.08 6.08, 3.92 3.92, 2.96 2.88, 2.24 1.92, 2 1.36, 2 0.64, 2.24 0.32, 2.96 0.08, 3.92 0))", + }, delimiter = ';') + void testSmoothToMinArea(String inWKT, String outWKT) throws ParseException { + var reader = new WKTReader(); + Geometry in = reader.read(inWKT); + Geometry out = reader.read(outWKT); + assertEquals( + TestUtils.round(out), + TestUtils.round(new DualMidpointSmoother(0.2, 0.8).setIters(200).setMinVertexArea(0.5).apply(in)) + ); + } + + @ParameterizedTest + @CsvSource(value = { + "LINESTRING(0 0, 10 10); LINESTRING(0 0, 10 10)", + "LINESTRING(0 0, 10 0, 10 10); LINESTRING (0 0, 9 0, 10 1, 10 10)", + "LINESTRING(0 0, 10 0, 20 0); LINESTRING (0 0, 7.5 0, 12.5 0, 20 0)", + "LINESTRING(0 0, 10 0, 0 0); LINESTRING (0 0, 7.5 0, 0 0)", + "POLYGON((0 0, 10 0, 10 10, 0 10, 0 0)); POLYGON ((1 0, 9 0, 10 1, 10 9, 9 10, 1 10, 0 9, 0 1, 1 0))", + }, delimiter = ';') + void testSmoothWithMaxArea(String inWKT, String outWKT) throws ParseException { + var reader = new WKTReader(); + Geometry in = reader.read(inWKT); + Geometry out = reader.read(outWKT); + assertEquals( + TestUtils.round(out), + TestUtils.round(DualMidpointSmoother.chaikin(1).setMaxArea(0.5).apply(in)) + ); + } + + @ParameterizedTest + @CsvSource(value = { + "LINESTRING(0 0, 10 10); LINESTRING(0 0, 10 10)", + "LINESTRING(0 0, 10 0, 10 10); LINESTRING (0 0, 9 0, 10 1, 10 10)", + "LINESTRING(0 0, 10 0, 20 0); LINESTRING (0 0, 7.5 0, 12.5 0, 20 0)", + "LINESTRING(0 0, 10 0, 0 0); LINESTRING (0 0, 9.29289 0, 0 0)", + "POLYGON((0 0, 10 0, 10 10, 0 10, 0 0)); POLYGON ((1 0, 9 0, 10 1, 10 9, 9 10, 1 10, 0 9, 0 1, 1 0))", + }, delimiter = ';') + void testSmoothWithMaxOffset(String inWKT, String outWKT) throws ParseException { + var reader = new WKTReader(); + Geometry in = reader.read(inWKT); + Geometry out = reader.read(outWKT); + assertEquals( + TestUtils.round(out), + TestUtils.round(DualMidpointSmoother.chaikin(1).setMaxOffset(Math.sqrt(0.5)).apply(in)) + ); + } +} diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/MidpointSmootherTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/MidpointSmootherTest.java index a905f52bbd..a7271c680c 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/MidpointSmootherTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/MidpointSmootherTest.java @@ -26,7 +26,7 @@ void testMidpointSmooth(String inWKT, String outWKT) throws ParseException { Geometry out = reader.read(outWKT); assertEquals( TestUtils.round(out), - TestUtils.round(new MidpointSmoother(new double[]{0.5}).setIters(1).apply(in)) + TestUtils.round(new MidpointSmoother().setIters(1).apply(in)) ); } @@ -34,105 +34,17 @@ void testMidpointSmooth(String inWKT, String outWKT) throws ParseException { @CsvSource(value = { "POINT(1 1); POINT(1 1)", "LINESTRING(0 0, 10 10); LINESTRING(0 0, 10 10)", - "LINESTRING(0 0, 10 0, 10 10); LINESTRING(0 0, 8 0, 10 2, 10 10)", - "LINESTRING(0 0, 10 0, 10 10, 0 10); LINESTRING(0 0, 8 0, 10 2, 10 8, 8 10, 0 10)", - "POLYGON((0 0, 10 0, 10 10, 0 0)); POLYGON((2 0, 8 0, 10 2, 10 8, 8 8, 2 2, 2 0))", - "POLYGON((0 0, 10 0, 10 10, 0 10, 0 0)); POLYGON((2 0, 8 0, 10 2, 10 8, 8 10, 2 10, 0 8, 0 2, 2 0))", + "LINESTRING(0 0, 10 0, 10 10); LINESTRING(0 0, 2.5 0, 7.5 2.5, 10 7.5, 10 10)", + "POLYGON((0 0, 10 0, 10 10, 0 0)); POLYGON ((7.5 2.5, 7.5 5, 5 2.5, 7.5 2.5))", + "POLYGON((0 0, 10 0, 10 10, 0 10, 0 0)); POLYGON ((7.5 2.5, 7.5 7.5, 2.5 7.5, 2.5 2.5, 7.5 2.5))", }, delimiter = ';') - void testDualMidpointSmooth(String inWKT, String outWKT) throws ParseException { + void testMidpointSmoothTwice(String inWKT, String outWKT) throws ParseException { var reader = new WKTReader(); Geometry in = reader.read(inWKT); Geometry out = reader.read(outWKT); assertEquals( TestUtils.round(out), - TestUtils.round(new MidpointSmoother(new double[]{0.2, 0.8}).setIters(1).apply(in)) - ); - } - - @ParameterizedTest - @CsvSource(value = { - "POINT(1 1); POINT(1 1)", - "LINESTRING(0 0, 10 10); LINESTRING(0 0, 10 10)", - "LINESTRING(0 0, 10 0, 10 10); LINESTRING(0 0, 6.4 0, 8.4 0.4, 9.6 1.6, 10 3.6, 10 10)", - "POLYGON((0 0, 10 0, 10 10, 0 0)); POLYGON((3.2 0, 6.8 0, 8.4 0.4, 9.6 1.6, 10 3.2, 10 6.8, 9.6 8, 8.4 8, 6.8 6.8, 3.2 3.2, 2 1.6, 2 0.4, 3.2 0))", - }, delimiter = ';') - void testMultiPassSmooth(String inWKT, String outWKT) throws ParseException { - var reader = new WKTReader(); - Geometry in = reader.read(inWKT); - Geometry out = reader.read(outWKT); - assertEquals( - TestUtils.round(out), - TestUtils.round(new MidpointSmoother(new double[]{0.2, 0.8}).setIters(2).apply(in)) - ); - } - - @ParameterizedTest - @CsvSource(value = { - "POINT(1 1); POINT(1 1)", - "LINESTRING(0 0, 10 10); LINESTRING(0 0, 10 10)", - "LINESTRING(0 0, 10 0, 10 10); LINESTRING (0 0, 6.4 0, 8 0.32, 8.64 0.64, 9.36 1.36, 9.68 2, 10 3.6, 10 10)", - "POLYGON((0 0, 10 0, 10 10, 0 0)); POLYGON ((3.2 0, 6.8 0, 8.4 0.4, 9.6 1.6, 10 3.2, 10 6.8, 9.68 7.76, 9.36 8, 8.4 8, 6.8 6.8, 3.2 3.2, 2 1.6, 2 0.64, 2.24 0.32, 3.2 0))", - }, delimiter = ';') - void testSmoothToTolerance(String inWKT, String outWKT) throws ParseException { - var reader = new WKTReader(); - Geometry in = reader.read(inWKT); - Geometry out = reader.read(outWKT); - assertEquals( - TestUtils.round(out), - TestUtils.round(new MidpointSmoother(new double[]{0.2, 0.8}).setIters(200).setMinVertexTolerance(0.5).apply(in)) - ); - } - - @ParameterizedTest - @CsvSource(value = { - "POINT(1 1); POINT(1 1)", - "LINESTRING(0 0, 10 10); LINESTRING(0 0, 10 10)", - "LINESTRING(0 0, 10 0, 10 10); LINESTRING (0 0, 5.12 0, 6.8 0.08, 8 0.32, 8.64 0.64, 9.36 1.36, 9.68 2, 9.92 3.2, 10 4.88, 10 10)", - "POLYGON((0 0, 10 0, 10 10, 0 0)); POLYGON ((3.92 0, 6.08 0, 7.12 0.08, 8.08 0.32, 8.64 0.64, 9.36 1.36, 9.68 1.92, 9.92 2.88, 10 3.92, 10 6.08, 9.92 7.04, 9.68 7.76, 9.36 8, 8.64 8, 8.08 7.76, 7.12 7.04, 6.08 6.08, 3.92 3.92, 2.96 2.88, 2.24 1.92, 2 1.36, 2 0.64, 2.24 0.32, 2.96 0.08, 3.92 0))", - }, delimiter = ';') - void testSmoothToMinArea(String inWKT, String outWKT) throws ParseException { - var reader = new WKTReader(); - Geometry in = reader.read(inWKT); - Geometry out = reader.read(outWKT); - assertEquals( - TestUtils.round(out), - TestUtils.round(new MidpointSmoother(new double[]{0.2, 0.8}).setIters(200).setMinVertexArea(0.5).apply(in)) - ); - } - - @ParameterizedTest - @CsvSource(value = { - "LINESTRING(0 0, 10 10); LINESTRING(0 0, 10 10)", - "LINESTRING(0 0, 10 0, 10 10); LINESTRING (0 0, 9 0, 10 1, 10 10)", - "LINESTRING(0 0, 10 0, 20 0); LINESTRING (0 0, 7.5 0, 12.5 0, 20 0)", - "LINESTRING(0 0, 10 0, 0 0); LINESTRING (0 0, 7.5 0, 0 0)", - "POLYGON((0 0, 10 0, 10 10, 0 10, 0 0)); POLYGON ((1 0, 9 0, 10 1, 10 9, 9 10, 1 10, 0 9, 0 1, 1 0))", - }, delimiter = ';') - void testSmoothWithMaxArea(String inWKT, String outWKT) throws ParseException { - var reader = new WKTReader(); - Geometry in = reader.read(inWKT); - Geometry out = reader.read(outWKT); - assertEquals( - TestUtils.round(out), - TestUtils.round(MidpointSmoother.chaikin(1).setMaxArea(0.5).apply(in)) - ); - } - - @ParameterizedTest - @CsvSource(value = { - "LINESTRING(0 0, 10 10); LINESTRING(0 0, 10 10)", - "LINESTRING(0 0, 10 0, 10 10); LINESTRING (0 0, 9 0, 10 1, 10 10)", - "LINESTRING(0 0, 10 0, 20 0); LINESTRING (0 0, 7.5 0, 12.5 0, 20 0)", - "LINESTRING(0 0, 10 0, 0 0); LINESTRING (0 0, 9.29289 0, 0 0)", - "POLYGON((0 0, 10 0, 10 10, 0 10, 0 0)); POLYGON ((1 0, 9 0, 10 1, 10 9, 9 10, 1 10, 0 9, 0 1, 1 0))", - }, delimiter = ';') - void testSmoothWithMaxOffset(String inWKT, String outWKT) throws ParseException { - var reader = new WKTReader(); - Geometry in = reader.read(inWKT); - Geometry out = reader.read(outWKT); - assertEquals( - TestUtils.round(out), - TestUtils.round(MidpointSmoother.chaikin(1).setMaxOffset(Math.sqrt(0.5)).apply(in)) + TestUtils.round(new MidpointSmoother().setIters(2).apply(in)) ); } }