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 d7a9b075be..f0671d84ec 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java @@ -227,6 +227,7 @@ public final class Feature { private ZoomFunction pixelTolerance = null; private String numPointsAttr = null; + private boolean removeHolesBelowMinSize = false; private Feature(String layer, Geometry geom, long id) { this.layer = layer; @@ -731,5 +732,14 @@ public String toString() { ", attrs=" + attrs + '}'; } + + public Feature setRemoveHolesBelowMinSize(boolean b) { + this.removeHolesBelowMinSize = b; + return this; + } + + public boolean getRemoveHolesBelowMinSize() { + return this.removeHolesBelowMinSize; + } } } diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureMerge.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureMerge.java index 6b9967d619..afbcfaee20 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureMerge.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureMerge.java @@ -25,6 +25,10 @@ import org.locationtech.jts.geom.GeometryCollection; import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.MultiLineString; +import org.locationtech.jts.geom.MultiPoint; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.geom.Polygonal; import org.locationtech.jts.index.strtree.STRtree; @@ -87,6 +91,77 @@ public static List mergeLineStrings(List return mergeLineStrings(features, minLength, tolerance, buffer, false); } + public static List mergeMultiPoint(List features) { + return mergeGeometries( + features, + GeometryType.POINT, + Point.class, + MultiPoint.class, + GeoUtils::combinePoints + ); + } + + public static List mergeMultiPolygon(List features) { + return mergeGeometries( + features, + GeometryType.POLYGON, + Polygon.class, + MultiPolygon.class, + GeoUtils::combinePolygons + ); + } + + public static List mergeMultiLineString(List features) { + return mergeGeometries( + features, + GeometryType.LINE, + LineString.class, + MultiLineString.class, + GeoUtils::combineLineStrings + ); + } + + private static List mergeGeometries( + List features, + GeometryType geometryType, + Class singleClass, + Class multiClass, + Function, Geometry> combine + ) { + List result = new ArrayList<>(features.size()); + var groupedByAttrs = groupByAttrs(features, result, geometryType); + for (List groupedFeatures : groupedByAttrs) { + VectorTile.Feature feature1 = groupedFeatures.get(0); + if (groupedFeatures.size() == 1) { + result.add(feature1); + } else { + List geoms = new ArrayList<>(); + for (var feature : groupedFeatures) { + try { + // TODO can we avoid decoding/encoding? + var geom = feature.geometry().decode(); + if (singleClass.isInstance(geom)) { + geoms.add(singleClass.cast(geom)); + } else if (multiClass.isInstance(geom)) { + var mp = multiClass.cast(geom); + for (int i = 0; i < mp.getNumGeometries(); i++) { + geoms.add(singleClass.cast(mp.getGeometryN(i))); + } + } else if (LOGGER.isWarnEnabled()) { + LOGGER.warn("Unexpected geometry type in merge({}): {}", + geometryType.name().toLowerCase(), + geom.getClass()); + } + } catch (GeometryException e) { + e.log("Error merging merging into a multi" + geometryType.name().toLowerCase() + ": " + feature); + } + } + result.add(feature1.copyWithNewGeometry(combine.apply(geoms))); + } + } + return result; + } + /** * Merges linestrings with the same attributes as {@link #mergeLineStrings(List, Function, double, double, boolean)} * except sets {@code resimplify=false} by default. 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..99ef2b8f59 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,7 @@ */ package com.onthegomap.planetiler.geo; +import org.locationtech.jts.algorithm.Area; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateSequence; import org.locationtech.jts.geom.Geometry; @@ -45,12 +46,26 @@ public static Geometry simplify(Geometry geom, double distanceTolerance) { return (new DPTransformer(distanceTolerance)).transform(geom); } + public static Geometry simplify(Geometry geom, double distanceTolerance, double minHoleSize) { + if (geom.isEmpty()) { + return geom.copy(); + } + + return (new DPTransformer(distanceTolerance, minHoleSize)).transform(geom); + } + private static class DPTransformer extends GeometryTransformer { private final double sqTolerance; + private final double minHoleSize; private DPTransformer(double distanceTolerance) { + this(distanceTolerance, 0); + } + + private DPTransformer(double distanceTolerance, double minHoleSize) { this.sqTolerance = distanceTolerance * distanceTolerance; + this.minHoleSize = minHoleSize; } /** @@ -142,7 +157,8 @@ protected Geometry transformPolygon(Polygon geom, Geometry parent) { protected Geometry transformLinearRing(LinearRing geom, Geometry parent) { boolean removeDegenerateRings = parent instanceof Polygon; Geometry simpResult = super.transformLinearRing(geom, parent); - if (removeDegenerateRings && !(simpResult instanceof LinearRing)) { + if (removeDegenerateRings && (!(simpResult instanceof LinearRing ring) || + (minHoleSize > 0 && Area.ofRing(ring.getCoordinateSequence()) <= minHoleSize))) { return null; } return simpResult; diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/FeatureMergeTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/FeatureMergeTest.java index b080abdc0b..50dca14d99 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/FeatureMergeTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/FeatureMergeTest.java @@ -10,13 +10,22 @@ import com.onthegomap.planetiler.geo.GeometryException; import com.onthegomap.planetiler.mbtiles.Mbtiles; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.UnaryOperator; +import java.util.stream.IntStream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -684,4 +693,95 @@ void mergeMultiPolygonExcludeSmallInnerRings() throws GeometryException { ) ); } + + @Test + void mergeMultipoints() throws GeometryException { + testMultigeometryMerger( + i -> newPoint(i, 2 * i), + items -> newMultiPoint(items.toArray(Point[]::new)), + rectangle(0, 1), + FeatureMerge::mergeMultiPoint + ); + } + + @Test + void mergeMultipolygons() throws GeometryException { + testMultigeometryMerger( + i -> rectangle(i, i + 1), + items -> newMultiPolygon(items.toArray(Polygon[]::new)), + newPoint(0, 0), + FeatureMerge::mergeMultiPolygon + ); + } + + @Test + void mergeMultiline() throws GeometryException { + testMultigeometryMerger( + i -> newLineString(i, i + 1, i + 2, i + 3), + items -> newMultiLineString(items.toArray(LineString[]::new)), + newPoint(0, 0), + FeatureMerge::mergeMultiLineString + ); + } + + + public static void main(String[] args) { + List features = new ArrayList<>(); + for (int i = 0; i < 1_000; i++) { + double[] points = IntStream.range(0, 100).mapToDouble(Double::valueOf).toArray(); + var lineString = newLineString(points); + features.add(new VectorTile.Feature("layer", i, VectorTile.encodeGeometry(lineString), Map.of("a", 1))); + } + for (int j = 0; j < 10; j++) { + long start = System.currentTimeMillis(); + for (int i = 0; i < 1_000; i++) { + FeatureMerge.mergeMultiLineString(features); + } + System.err.println(System.currentTimeMillis() - start); + } + } + + void testMultigeometryMerger( + IntFunction generateGeometry, + Function, M> combineJTS, + Geometry otherGeometry, + UnaryOperator> merge + ) throws GeometryException { + var geom1 = generateGeometry.apply(1); + var geom2 = generateGeometry.apply(2); + var geom3 = generateGeometry.apply(3); + var geom4 = generateGeometry.apply(4); + + assertTopologicallyEquivalentFeatures( + List.of(), + merge.apply(List.of()) + ); + + assertTopologicallyEquivalentFeatures( + List.of( + feature(1, geom1, Map.of("a", 1)) + ), + merge.apply( + List.of( + feature(1, geom1, Map.of("a", 1)) + ) + ) + ); + + assertTopologicallyEquivalentFeatures( + List.of( + feature(4, otherGeometry, Map.of("a", 1)), + feature(1, combineJTS.apply(List.of(geom1, geom2, geom3)), Map.of("a", 1)), + feature(3, geom4, Map.of("a", 2)) + ), + merge.apply( + List.of( + feature(1, geom1, Map.of("a", 1)), + feature(2, combineJTS.apply(List.of(geom2, geom3)), Map.of("a", 1)), + feature(3, geom4, Map.of("a", 2)), + feature(4, otherGeometry, Map.of("a", 1)) + ) + ) + ); + } }