From f684feb0571948707a00fc1d44267848c09a6b95 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 19 Jun 2024 16:13:33 -0700 Subject: [PATCH] Add per-element tolerances --- .../jtstest/function/CoverageFunctions.java | 23 +++++++++ .../jts/coverage/CoverageEdge.java | 23 ++++++++- .../jts/coverage/CoverageRingEdges.java | 47 +++++++------------ .../jts/coverage/CoverageSimplifier.java | 47 +++++++++++++++++++ .../jts/coverage/CoverageRingEdgesTest.java | 26 ---------- .../jts/coverage/CoverageSimplifierTest.java | 19 ++++++++ 6 files changed, 126 insertions(+), 59 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/CoverageFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/CoverageFunctions.java index d0fb7f325e..d214ae153a 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/CoverageFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/CoverageFunctions.java @@ -11,6 +11,7 @@ */ package org.locationtech.jtstest.function; +import java.util.Arrays; import java.util.List; import org.locationtech.jts.coverage.CoverageGapFinder; @@ -117,6 +118,28 @@ public static Geometry simplifyInOut(Geometry coverage, return coverage.getFactory().createGeometryCollection(result); } + @Metadata(description="Simplify a coverage with per-geometry tolerances") + public static Geometry simplifyTolerances(Geometry coverage, String tolerances) { + Geometry[] cov = toGeometryArray(coverage); + double[] toleranceList = tolerances(tolerances, cov.length); + CoverageSimplifier simplifier = new CoverageSimplifier(cov); + Geometry[] result = simplifier.simplify(toleranceList); + return FunctionsUtil.buildGeometry(result); + } + + private static double[] tolerances(String csvList, int len) { + Double[] tolsDouble = toDoubleArray(csvList); + double[] tols = new double[len]; + for (int i = 0; i < tolsDouble.length; i++) { + tols[i] = tolsDouble[i]; + } + return tols; + } + + private static Double[] toDoubleArray(String csvList) { + return Arrays.stream(csvList.split(",")).map(Double::parseDouble).toArray(Double[]::new); + } + static Geometry extractPolygons(Geometry geom) { List components = PolygonExtracter.getPolygons(geom); Geometry result = geom.getFactory().buildGeometry(components); diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java index 2bc15ec011..ec0a02de33 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java @@ -133,6 +133,8 @@ else if (i > pts.length - 1) { private int ringCount = 0; private boolean isFreeRing = true; private boolean isPrimary = true; + private int adjacentIndex0 = -1; + private int adjacentIndex1 = -1; public CoverageEdge(Coordinate[] pts, boolean isPrimary, boolean isFreeRing) { this.pts = pts; @@ -157,6 +159,9 @@ public boolean isOuter() { } public void setPrimary(boolean isPrimary) { + //-- preserve primary status if set + if (this.isPrimary) + return; this.isPrimary = isPrimary; } @@ -200,8 +205,22 @@ public String toString() { return WKTWriter.toLineString(pts); } + public void addIndex(int index) { + //TODO: keep information about which element is L and R? + + // assert: at least one elementIndex is unset (< 0) + if (adjacentIndex0 < 0) { + adjacentIndex0 = index; + } + else { + adjacentIndex1 = index; + } + } - - + public int getAdjacentIndex(int index) { + if (index == 0) + return adjacentIndex0; + return adjacentIndex1; + } } diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java index 672f1bb817..a2b83ddf55 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java @@ -74,31 +74,14 @@ public List getEdges() { return edges; } - /** - * Selects the edges with a given ring count (1 or 2). - * Outer edges have ring count 1. - * Inner edges have ring count 2. - * - * @param ringCount the edge ring count to select (1 for outer or 2 for inner) - * @return the selected edges - */ - public List selectEdges(int ringCount) { - List result = new ArrayList(); - for (CoverageEdge edge : edges) { - if (edge.getRingCount() == ringCount) { - result.add(edge); - } - } - return result; - } - private void build() { Set nodes = findMultiRingNodes(coverage); Set boundarySegs = CoverageBoundarySegmentFinder.findBoundarySegments(coverage); nodes.addAll(findBoundaryNodes(boundarySegs)); HashMap uniqueEdgeMap = new HashMap(); - for (Geometry geom : coverage) { + for (int i = 0; i < coverage.length; i++) { //-- geom is a Polygon or MultiPolygon + Geometry geom = coverage[i]; int indexLargest = findLargestPolygonIndex(geom); for (int ipoly = 0; ipoly < geom.getNumGeometries(); ipoly++) { Polygon poly = (Polygon) geom.getGeometryN(ipoly); @@ -112,7 +95,7 @@ private void build() { //-- extract shell LinearRing shell = poly.getExteriorRing(); - addRingEdges(shell, isPrimary, nodes, boundarySegs, uniqueEdgeMap); + addRingEdges(i, shell, isPrimary, nodes, boundarySegs, uniqueEdgeMap); //-- extract holes for (int ihole = 0; ihole < poly.getNumInteriorRing(); ihole++) { LinearRing hole = poly.getInteriorRingN(ihole); @@ -120,7 +103,7 @@ private void build() { if (hole.isEmpty()) continue; //-- holes are never primary - addRingEdges(hole, false, nodes, boundarySegs, uniqueEdgeMap); + addRingEdges(i, hole, false, nodes, boundarySegs, uniqueEdgeMap); } } } @@ -142,10 +125,10 @@ private int findLargestPolygonIndex(Geometry geom) { return indexLargest; } - private void addRingEdges(LinearRing ring, boolean isPrimary, Set nodes, Set boundarySegs, + private void addRingEdges(int index, LinearRing ring, boolean isPrimary, Set nodes, Set boundarySegs, HashMap uniqueEdgeMap) { addBoundaryInnerNodes(ring, boundarySegs, nodes); - List ringEdges = extractRingEdges(ring, isPrimary, uniqueEdgeMap, nodes); + List ringEdges = extractRingEdges(index, ring, isPrimary, uniqueEdgeMap, nodes); if (ringEdges != null) ringEdgesMap.put(ring, ringEdges); } @@ -176,6 +159,7 @@ private void addBoundaryInnerNodes(LinearRing ring, Set boundarySeg /** * Extracts the {@link CoverageEdge}s for a ring. + * @param index * * @param ring * @param isRetained true if the ring is retained (must not be removed) @@ -183,7 +167,7 @@ private void addBoundaryInnerNodes(LinearRing ring, Set boundarySeg * @param nodes * @return null if the ring has too few distinct vertices */ - private List extractRingEdges(LinearRing ring, + private List extractRingEdges(int index, LinearRing ring, boolean isPrimary, HashMap uniqueEdgeMap, Set nodes) { // System.out.println(ring); @@ -198,12 +182,13 @@ private List extractRingEdges(LinearRing ring, int first = findNextNodeIndex(pts, -1, nodes); if (first < 0) { //-- ring does not contain a node, so edge is entire ring - CoverageEdge edge = createEdge(pts, -1, -1, isPrimary, uniqueEdgeMap); + CoverageEdge edge = createEdge(pts, -1, -1, index, isPrimary, uniqueEdgeMap); ringEdges.add(edge); } else { int start = first; int end = start; + //-- two-node edges are always primary boolean isEdgePrimary = true; do { end = findNextNodeIndex(pts, start, nodes); @@ -211,7 +196,7 @@ private List extractRingEdges(LinearRing ring, if (end == start) { isEdgePrimary = isPrimary; } - CoverageEdge edge = createEdge(pts, start, end, isEdgePrimary, uniqueEdgeMap); + CoverageEdge edge = createEdge(pts, start, end, index, isEdgePrimary, uniqueEdgeMap); // System.out.println(ringEdges.size() + " : " + edge); ringEdges.add(edge); start = end; @@ -226,19 +211,18 @@ private List extractRingEdges(LinearRing ring, * @param ring ring to create edge for * @param start start index of ring section; -1 indicates edge is entire ring * @param end end index of ring section + * @param index * @param isPrimary whether this ring is a primary ring * @param uniqueEdgeMap map of edges * @return the CoverageEdge for the ring or portion of ring */ - private CoverageEdge createEdge(Coordinate[] ring, int start, int end, boolean isPrimary, HashMap uniqueEdgeMap) { + private CoverageEdge createEdge(Coordinate[] ring, int start, int end, int index, boolean isPrimary, HashMap uniqueEdgeMap) { CoverageEdge edge; LineSegment edgeKey = (end == start) ? CoverageEdge.key(ring) : CoverageEdge.key(ring, start, end); if (uniqueEdgeMap.containsKey(edgeKey)) { edge = uniqueEdgeMap.get(edgeKey); - //-- ensure existing edge is retained if this ring is retained - if (isPrimary) { - edge.setPrimary(true); - } + //-- update shared attributes + edge.setPrimary(isPrimary); } else { if (start < 0) { @@ -250,6 +234,7 @@ private CoverageEdge createEdge(Coordinate[] ring, int start, int end, boolean i uniqueEdgeMap.put(edgeKey, edge); edges.add(edge); } + edge.addIndex(index); edge.incRingCount(); return edge; } diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java index 45a6dd5c8a..6f3273508f 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java @@ -70,6 +70,11 @@ public static Geometry[] simplify(Geometry[] coverage, double tolerance) { return simplifier.simplify(tolerance); } + public static Geometry[] simplify(Geometry[] coverage, double[] tolerances) { + CoverageSimplifier simplifier = new CoverageSimplifier(coverage); + return simplifier.simplify(tolerances); + } + /** * Simplifies the inner boundaries of a set of polygonal geometries forming a coverage, * preserving the coverage topology. @@ -135,10 +140,52 @@ public Geometry[] simplify(double toleranceInner, double toleranceOuter) { return simplifyEdges(toleranceInner, toleranceOuter); } + public Geometry[] simplify(double[] tolerances) { + if (tolerances.length != coverage.length) + throw new IllegalArgumentException("must have same number of tolerances as coverage elements"); + return simplifyEdges(tolerances); + } + + private Geometry[] simplifyEdges(double[] tolerances) { + CoverageRingEdges covRings = CoverageRingEdges.create(coverage); + List covEdges = covRings.getEdges(); + TPVWSimplifier.Edge[] edges = createEdges(covEdges, tolerances); + return simplify(covRings, covEdges, edges); + } + + private Edge[] createEdges(List covEdges, double[] tolerances) { + TPVWSimplifier.Edge[] edges = new TPVWSimplifier.Edge[covEdges.size()]; + for (int i = 0; i < covEdges.size(); i++) { + CoverageEdge covEdge = covEdges.get(i); + double tol = computeTolerance(covEdge, tolerances); + edges[i] = createEdge(covEdge, tol); + } + return edges; + } + + private double computeTolerance(CoverageEdge covEdge, double[] tolerances) { + int index0 = covEdge.getAdjacentIndex(0); + // assert: index0 >= 0 + double tolerance = tolerances[index0]; + + int index1 = covEdge.getAdjacentIndex(0); + if (index1 >= 0) { + double tol1 = tolerances[index1]; + //-- minimum tolerance is used + if (tol1 < tolerance) + tolerance = tol1; + } + return tolerance; + } + private Geometry[] simplifyEdges(double toleranceInner, double toleranceOuter) { CoverageRingEdges covRings = CoverageRingEdges.create(coverage); List covEdges = covRings.getEdges(); TPVWSimplifier.Edge[] edges = createEdges(covEdges, toleranceInner, toleranceOuter); + return simplify(covRings, covEdges, edges); + } + + private Geometry[] simplify(CoverageRingEdges covRings, List covEdges, TPVWSimplifier.Edge[] edges) { CornerArea cornerArea = new CornerArea(smoothWeight); TPVWSimplifier.simplify(edges, cornerArea, removableSizeFactor); setCoordinates(covEdges, edges); diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java index 2289e5a5b2..1a70f46c6c 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java @@ -45,22 +45,6 @@ public void testHolesAndFillWithDifferentEndpoints() { "MULTILINESTRING ((0 10, 0 0, 10 0, 10 10, 0 10), (1 1, 1 9, 4 8, 9 9), (1 1, 9 1, 9 9), (1 1, 9 9))"); } - public void testTouchingSquares() { - String wkt = "MULTIPOLYGON (((2 7, 2 8, 3 8, 3 7, 2 7)), ((1 6, 1 7, 2 7, 2 6, 1 6)), ((0 7, 0 8, 1 8, 1 7, 0 7)), ((0 5, 0 6, 1 6, 1 5, 0 5)), ((2 5, 2 6, 3 6, 3 5, 2 5)))"; - checkEdgesSelected(wkt, 1, - "MULTILINESTRING ((1 6, 0 6, 0 5, 1 5, 1 6), (1 6, 1 7), (1 6, 2 6), (1 7, 0 7, 0 8, 1 8, 1 7), (1 7, 2 7), (2 6, 2 5, 3 5, 3 6, 2 6), (2 6, 2 7), (2 7, 2 8, 3 8, 3 7, 2 7))"); - checkEdgesSelected(wkt, 2, - "MULTILINESTRING EMPTY"); - } - - public void testAdjacentSquares() { - String wkt = "GEOMETRYCOLLECTION (POLYGON ((1 3, 2 3, 2 2, 1 2, 1 3)), POLYGON ((3 3, 3 2, 2 2, 2 3, 3 3)), POLYGON ((3 1, 2 1, 2 2, 3 2, 3 1)), POLYGON ((1 1, 1 2, 2 2, 2 1, 1 1)))"; - checkEdgesSelected(wkt, 1, - "MULTILINESTRING ((1 2, 1 1, 2 1), (1 2, 1 3, 2 3), (2 1, 3 1, 3 2), (2 3, 3 3, 3 2))"); - checkEdgesSelected(wkt, 2, - "MULTILINESTRING ((1 2, 2 2), (2 1, 2 2), (2 2, 2 3), (2 2, 3 2))"); - } - public void testMultiPolygons() { checkEdges("GEOMETRYCOLLECTION (MULTIPOLYGON (((5 9, 2.5 7.5, 1 5, 5 5, 5 9)), ((5 5, 9 5, 7.5 2.5, 5 1, 5 5))), MULTIPOLYGON (((5 9, 6.5 6.5, 9 5, 5 5, 5 9)), ((1 5, 5 5, 5 1, 3.5 3.5, 1 5))))", "MULTILINESTRING ((1 5, 2.5 7.5, 5 9), (1 5, 3.5 3.5, 5 1), (1 5, 5 5), (5 1, 5 5), (5 1, 7.5 2.5, 9 5), (5 5, 5 9), (5 5, 9 5), (5 9, 6.5 6.5, 9 5))" @@ -76,16 +60,6 @@ private void checkEdges(String wkt, String wktExpected) { checkEqual(expected, edgeLines); } - private void checkEdgesSelected(String wkt, int ringCount, String wktExpected) { - Geometry geom = read(wkt); - Geometry[] polygons = toArray(geom); - CoverageRingEdges covEdges = CoverageRingEdges.create(polygons); - List edges = covEdges.selectEdges(ringCount); - MultiLineString edgeLines = toArray(edges, geom.getFactory()); - Geometry expected = read(wktExpected); - checkEqual(expected, edgeLines); - } - private MultiLineString toArray(List edges, GeometryFactory geomFactory) { LineString[] lines = new LineString[edges.size()]; for (int i = 0; i < edges.size(); i++) { diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java index 6406615605..c14ab56c7c 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java @@ -367,6 +367,20 @@ public void testMultiPolygonHolesSmallPart() { ); } + public void testTolerances() { + checkResult(readArray( + "POLYGON ((1 19, 6 19, 7 11, 6 1, 1 1, 1 19))", + "POLYGON ((6 19, 12 19, 11 15, 12 1, 6 1, 7 11, 6 19))", + "POLYGON ((12 19, 19 19, 22 10, 19 1, 12 1, 11 15, 12 19))" + ), + new double[] { 0, 3, 6 }, + readArray( + "POLYGON ((6 19, 7 11, 6 1, 1 1, 1 19, 6 19))", + "POLYGON ((6 19, 12 19, 12 1, 6 1, 7 11, 6 19))", + "POLYGON ((12 19, 19 19, 19 1, 12 1, 12 19))" ) + ); + } + //================================= @@ -375,6 +389,11 @@ private void checkNoop(Geometry[] input) { checkEqual(input, actual); } + private void checkResult(Geometry[] input, double[] tolerances, Geometry[] expected) { + Geometry[] actual = CoverageSimplifier.simplify(input, tolerances); + checkEqual(expected, actual); + } + private void checkResult(Geometry[] input, double tolerance, Geometry[] expected) { Geometry[] actual = CoverageSimplifier.simplify(input, tolerance); checkEqual(expected, actual);