From 6a2460f897d9b9bd0dee8c6ecb64c1bb2074c7ab Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Sun, 1 Dec 2024 17:01:49 -0500 Subject: [PATCH] smoother --- .../planetiler/geo/MidpointSmoother.java | 75 ++++++++++++++ .../planetiler/geo/MidpointSmootherTest.java | 99 +++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 planetiler-core/src/main/java/com/onthegomap/planetiler/geo/MidpointSmoother.java create mode 100644 planetiler-core/src/test/java/com/onthegomap/planetiler/geo/MidpointSmootherTest.java 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 new file mode 100644 index 0000000000..6df9759233 --- /dev/null +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/MidpointSmoother.java @@ -0,0 +1,75 @@ +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; + +public class MidpointSmoother extends GeometryTransformer implements GeometryPipeline { + + private final double[] points; + private int iters = 1; + private boolean includeLineEndpoints = true; + + public MidpointSmoother(double[] points) { + this.points = points; + } + + @Override + public Geometry apply(Geometry input) { + return transform(input); + } + + public MidpointSmoother setIters(int iters) { + this.iters = iters; + return this; + } + + public MidpointSmoother includeLineEndpoints(boolean includeLineEndpoints) { + this.includeLineEndpoints = includeLineEndpoints; + return this; + } + + @Override + protected CoordinateSequence transformCoordinates(CoordinateSequence coords, Geometry parent) { + boolean area = parent instanceof LinearRing; + if (coords.size() <= 2) { + return coords.copy(); + } + for (int iter = 0; iter < iters; iter++) { + MutableCoordinateSequence result = new MutableCoordinateSequence(); + boolean skipFirstAndLastInterpolated = !area && includeLineEndpoints && points.length > 1; + if (!area && includeLineEndpoints) { + result.addPoint(coords.getX(0), coords.getY(0)); + } + int last = coords.size() - 1; + double x2 = coords.getX(0); + double y2 = coords.getY(0); + 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; + for (int j = 0; j < points.length; j++) { + if (skipFirstAndLastInterpolated) { + if ((i == 0 && j == 0) || (i == last - 1 && j == points.length - 1)) { + continue; + } + } + double value = points[j]; + result.addPoint(x1 + dx * value, y1 + dy * value); + } + } + if (area) { + result.closeRing(); + } else if (includeLineEndpoints) { + result.addPoint(coords.getX(last), coords.getY(last)); + } + coords = result; + } + return coords; + } +} 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 new file mode 100644 index 0000000000..52c6bc513b --- /dev/null +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/MidpointSmootherTest.java @@ -0,0 +1,99 @@ +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 MidpointSmootherTest { + + @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 0, 10 5, 10 10)", + "LINESTRING(0 0, 10 0, 10 10, 0 10); LINESTRING(0 0, 5 0, 10 5, 5 10, 0 10)", + "POLYGON((0 0, 10 0, 10 10, 0 0)); POLYGON((5 0, 10 5, 5 5, 5 0))", + "POLYGON((0 0, 10 0, 10 10, 0 10, 0 0)); POLYGON((5 0, 10 5, 5 10, 0 5, 5 0))", + }, delimiter = ';') + void testMidpointSmooth(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.5}).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, 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 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(5 0, 10 5)", + "LINESTRING(0 0, 10 0, 10 10, 0 10); LINESTRING(5 0, 10 5, 5 10)", + }, delimiter = ';') + void testDontIncludeEndpointsMidpoint(String inWKT, String outWKT) throws ParseException { + var reader = new WKTReader(); + assertEquals( + TestUtils.round(reader.read(outWKT)), + TestUtils.round( + new MidpointSmoother(new double[]{0.5}).setIters(1).includeLineEndpoints(false).apply(reader.read(inWKT))) + ); + } + + @ParameterizedTest + @CsvSource(value = { + "LINESTRING(0 0, 10 10); LINESTRING(0 0, 10 10)", + "LINESTRING(0 0, 10 0, 10 10); LINESTRING(2 0, 8 0, 10 2, 10 8)", + "LINESTRING(0 0, 10 0, 10 10, 0 10); LINESTRING(2 0, 8 0, 10 2, 10 8, 8 10, 2 10)", + }, delimiter = ';') + void testDontIncludeEndpointsDualMidpoint(String inWKT, String outWKT) throws ParseException { + var reader = new WKTReader(); + assertEquals( + TestUtils.round(reader.read(outWKT)), + TestUtils.round( + new MidpointSmoother(new double[]{0.2, 0.8}).setIters(1).includeLineEndpoints(false).apply(reader.read(inWKT))) + ); + } +}