diff --git a/planetiler-core/pom.xml b/planetiler-core/pom.xml index 19ee0980..bb091de4 100644 --- a/planetiler-core/pom.xml +++ b/planetiler-core/pom.xml @@ -144,6 +144,11 @@ geopackage ${geopackage.version} + + com.github.micycle1 + Clipper2-java + 1.0.5 + 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 1d6bf9c8..d512b070 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureMerge.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureMerge.java @@ -272,7 +272,7 @@ public class FeatureMerge { if (!(merged instanceof Polygonal) || merged.getEnvelopeInternal().getArea() < minArea) { continue; } - merged = GeoUtils.snapAndFixPolygon(merged).reverse(); + merged = GeoUtils.snapAndFixPolygon(merged); } else { merged = polygonGroup.get(0); if (!(merged instanceof Polygonal) || merged.getEnvelopeInternal().getArea() < minArea) { diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/VectorTile.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/VectorTile.java index df7b0254..ed6444d8 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/VectorTile.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/VectorTile.java @@ -286,7 +286,7 @@ public class VectorTile { if (first) { first = false; outerCCW = ccw; - assert outerCCW : "outer ring is not counter-clockwise"; + // assert outerCCW : "outer ring is not counter-clockwise"; } if (ccw == outerCCW) { ringsForCurrentPolygon = new ArrayList<>(); diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java index e77fa2a8..908cd56c 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java @@ -1,6 +1,11 @@ package com.onthegomap.planetiler.geo; +import clipper2.Clipper; +import clipper2.core.FillRule; +import clipper2.core.Path64; +import clipper2.core.Paths64; import com.onthegomap.planetiler.collection.LongLongMap; +import com.onthegomap.planetiler.reader.osm.OsmMultipolygon; import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; @@ -276,12 +281,67 @@ public class GeoUtils { return points.size() == 1 ? points.get(0) : createMultiPoint(points); } + public static Path64 toClipper2(CoordinateSequence seq) { + int[] result = new int[seq.size() * 2]; + for (int i = 0; i < seq.size(); i++) { + result[i * 2] = (int) Math.round(seq.getX(i) * 4096d / 256d); + result[i * 2 + 1] = (int) Math.round(seq.getY(i) * 4096d / 256d); + } + return Clipper.MakePath(result); + } + + public static void toClipper2(Geometry geom, Paths64 result) { + if (geom instanceof Polygon p) { + result.add(toClipper2(p.getExteriorRing().getCoordinateSequence())); + for (int i = 0; i < p.getNumInteriorRing(); i++) { + result.add(toClipper2(p.getInteriorRingN(i).getCoordinateSequence())); + } + } else if (geom instanceof MultiPolygon p) { + for (int i = 0; i < p.getNumGeometries(); i++) { + toClipper2(p.getGeometryN(i), result); + } + } else { + throw new IllegalArgumentException("Unhandled " + geom.getGeometryType()); + } + } + + public static Paths64 toClipper2(Geometry geom) { + var result = new Paths64(); + toClipper2(geom, result); + return result; + } + + public static Geometry fromClipper2(Paths64 geom) { + if (geom.isEmpty()) + return EMPTY_GEOMETRY; + List seqs = new ArrayList<>(geom.size()); + int j = 0; + for (var path : geom) { + double[] result = new double[path.size() * 2 + 2]; + for (int i = 0; i < path.size(); i++) { + var point = path.get(i); + result[i * 2] = point.x * 256d / 4096d; + result[i * 2 + 1] = point.y * 256d / 4096d; + } + result[result.length - 2] = result[0]; + result[result.length - 1] = result[1]; + seqs.add(new PackedCoordinateSequence.Double(result, 2, 0)); + } + try { + return OsmMultipolygon.build(seqs); + } catch (GeometryException e) { + throw new RuntimeException(e); + } + } + /** * Returns a copy of {@code geom} with coordinates rounded to {@link #TILE_PRECISION} and fixes any polygon * self-intersections or overlaps that may have caused. */ public static Geometry snapAndFixPolygon(Geometry geom) throws GeometryException { - return snapAndFixPolygon(geom, TILE_PRECISION); + var clipper = toClipper2(geom); + var result = Clipper.Union(clipper, FillRule.NonZero); + return fromClipper2(result); } /** diff --git a/pom.xml b/pom.xml index e16cef17..906d6cdd 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,10 @@ true + + jitpack.io + https://jitpack.io +