diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java index 3a08cdde4e..607491a6d1 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java @@ -749,7 +749,7 @@ public SimplifyMethod getSimplifyMethodAtZoom(int zoom) { * features. This function gets run instead of simplification, so should include any simplification if you want * that. */ - public Feature setGeometryPipeline(GeometryPipeline pipeline) { + public Feature transformScaledGeometry(GeometryPipeline pipeline) { this.defaultGeometryPipeline = pipeline; return this; } @@ -759,7 +759,7 @@ public Feature setGeometryPipeline(GeometryPipeline pipeline) { * vector tile features. These functions get run instead of simplification, so should include any simplification if * you want that. */ - public Feature setGeometryPipelineByZoom(ZoomFunction overrides) { + public Feature transformScaledGeometryByZoom(ZoomFunction overrides) { this.geometryPipelineByZoom = overrides; return this; } @@ -768,7 +768,7 @@ public Feature setGeometryPipelineByZoom(ZoomFunction override * Returns the geometry transform function to apply to scaled geometries at {@code zoom}, or null to not update them * at all. */ - public GeometryPipeline getGeometryPipelineAtZoom(int zoom) { + public GeometryPipeline getScaledGeometryTransformAtZoom(int zoom) { return ZoomFunction.applyOrElse(geometryPipelineByZoom, zoom, defaultGeometryPipeline); } 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 67d3086415..ec79a1d274 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 @@ -4,6 +4,12 @@ import com.onthegomap.planetiler.util.ZoomFunction; import org.locationtech.jts.geom.Geometry; +/** + * A function that transforms a {@link Geometry}. + *

+ * This can be chained and used in {@link FeatureCollector.Feature#transformScaledGeometry(GeometryPipeline)} to + * transform geometries before slicing them into tiles. + */ @FunctionalInterface public interface GeometryPipeline extends ZoomFunction { GeometryPipeline NOOP = g -> g; @@ -16,6 +22,7 @@ default GeometryPipeline apply(int zoom) { return this; } + /** Returns a function equivalent to {@code other(this(geom))}. */ default GeometryPipeline andThen(GeometryPipeline other) { return input -> other.apply(apply(input)); } @@ -94,4 +101,20 @@ static VWSimplifier simplifyVW(double tolerance) { static DouglasPeuckerSimplifier simplifyDP(double tolerance) { return new DouglasPeuckerSimplifier(tolerance); } + + /** + * Returns a pipeline that smoothes 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(); + } + + /** + * Returns a pipeline that smoothes 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); + } } 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 6df9759233..ed36b86caf 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 @@ -5,29 +5,45 @@ import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.util.GeometryTransformer; +/** + * Smoothes 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. + */ 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 int iters = 1; - private boolean includeLineEndpoints = true; public MidpointSmoother(double[] points) { this.points = points; } - @Override - public Geometry apply(Geometry input) { - return transform(input); + /** + * 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 midpoint smoothing. */ + public static MidpointSmoother midpoint() { + return new MidpointSmoother(MIDPOINT); } + /** Sets the number of times that smoothing runs. */ public MidpointSmoother setIters(int iters) { this.iters = iters; return this; } - public MidpointSmoother includeLineEndpoints(boolean includeLineEndpoints) { - this.includeLineEndpoints = includeLineEndpoints; - return this; + @Override + public Geometry apply(Geometry input) { + return transform(input); } @Override @@ -38,8 +54,8 @@ protected CoordinateSequence transformCoordinates(CoordinateSequence coords, Geo } for (int iter = 0; iter < iters; iter++) { MutableCoordinateSequence result = new MutableCoordinateSequence(); - boolean skipFirstAndLastInterpolated = !area && includeLineEndpoints && points.length > 1; - if (!area && includeLineEndpoints) { + boolean skipFirstAndLastInterpolated = !area && points.length > 1; + if (!area) { result.addPoint(coords.getX(0), coords.getY(0)); } int last = coords.size() - 1; @@ -54,18 +70,17 @@ protected CoordinateSequence transformCoordinates(CoordinateSequence coords, Geo 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; - } + if (skipFirstAndLastInterpolated && ((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) { + } else { result.addPoint(coords.getX(last), coords.getY(last)); } coords = result; diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/render/FeatureRenderer.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/render/FeatureRenderer.java index 71bc2dbd1d..451b01afe9 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/render/FeatureRenderer.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/render/FeatureRenderer.java @@ -100,7 +100,7 @@ public void accept(FeatureCollector.Feature feature) { private void accept(int zoom, Geometry geom, Map attrs, FeatureCollector.Feature feature) { double scale = 1 << zoom; geom = AffineTransformation.scaleInstance(scale, scale).transform(geom); - GeometryPipeline pipeline = feature.getGeometryPipelineAtZoom(zoom); + GeometryPipeline pipeline = feature.getScaledGeometryTransformAtZoom(zoom); if (pipeline != null) { geom = pipeline.apply(geom); } else if (!(geom instanceof Puntal)) { 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 52c6bc513b..21f7b5f4c1 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 @@ -65,35 +65,4 @@ void testMultiPassSmooth(String inWKT, String outWKT) throws ParseException { 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))) - ); - } } diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/render/FeatureRendererTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/render/FeatureRendererTest.java index 5016da7bea..834991f27e 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/render/FeatureRendererTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/render/FeatureRendererTest.java @@ -1510,7 +1510,7 @@ void testGeometryPipeline() { 0.5 + Z14_WIDTH / 2, 0.5 + Z14_WIDTH / 2, 0.5 + Z14_WIDTH / 2 + Z14_PX * 10, 0.5 + Z14_WIDTH / 2 ) - ).setGeometryPipeline(Geometry::getCentroid).setAttr("k", "v"); + ).transformScaledGeometry(Geometry::getCentroid).setAttr("k", "v"); assertEquals( Set.of( List.of(newPoint(128 + 5, 128), Map.of("k", "v")) @@ -1526,7 +1526,7 @@ void testGeometryPipelineSimplify() { 0.5 + Z14_WIDTH / 2, 0.5 + Z14_WIDTH / 2, 0.5 + Z14_WIDTH / 2 + Z14_PX * 10, 0.5 + Z14_WIDTH / 2 ) - ).setGeometryPipeline( + ).transformScaledGeometry( GeometryPipeline.simplifyVW(1).setWeight(0.9) .andThen(GeometryPipeline.simplifyDP(1)) ).setAttr("k", "v");