From 210beb7e4bc991fbd29bc4fb779452beafb89b63 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Fri, 5 Jul 2024 10:04:47 -0700 Subject: [PATCH] Improve CoverageSimplifier with ring removal, smoothing, inner/outer and per-feature tolerances (#1060) --- .../jtstest/function/CoverageFunctions.java | 75 +++++- .../org/locationtech/jts/coverage/Corner.java | 11 +- .../locationtech/jts/coverage/CornerArea.java | 62 +++++ .../jts/coverage/CoverageEdge.java | 83 +++++-- .../jts/coverage/CoverageRingEdges.java | 152 ++++++++---- .../jts/coverage/CoverageSimplifier.java | 228 +++++++++++++---- .../jts/coverage/TPVWSimplifier.java | 234 ++++++++++-------- .../jts/coverage/CoverageRingEdgesTest.java | 26 -- .../jts/coverage/CoverageSimplifierTest.java | 135 ++++++++-- .../jts/coverage/TPVWSimplifierTest.java | 72 ++++-- 10 files changed, 768 insertions(+), 310 deletions(-) create mode 100644 modules/core/src/main/java/org/locationtech/jts/coverage/CornerArea.java 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 74f52f6a1b..0e847718f2 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; @@ -62,20 +63,82 @@ public static Geometry union(Geometry coverage) { public static Geometry simplify(Geometry coverage, double tolerance) { Geometry[] cov = toGeometryArray(coverage); Geometry[] result = CoverageSimplifier.simplify(cov, tolerance); - return FunctionsUtil.buildGeometry(result); + return coverage.getFactory().createGeometryCollection(result); + } + + @Metadata(description="Simplify a coverage with a smoothness weight") + public static Geometry simplifySharp(Geometry coverage, + @Metadata(title="Distance tol") + double tolerance, + @Metadata(title="Weight") + double weight) { + Geometry[] cov = toGeometryArray(coverage); + CoverageSimplifier simplifier = new CoverageSimplifier(cov); + simplifier.setSmoothWeight(weight); + Geometry[] result = simplifier.simplify(tolerance); + return coverage.getFactory().createGeometryCollection(result); + } + + @Metadata(description="Simplify a coverage with a ring removal size factor") + public static Geometry simplifyRemoveRings(Geometry coverage, + @Metadata(title="Distance tol") + double tolerance, + @Metadata(title="Removal Size Factor") + double factor) { + Geometry[] cov = toGeometryArray(coverage); + CoverageSimplifier simplifier = new CoverageSimplifier(cov); + simplifier.setRemovableRingSizeFactor(factor); + Geometry[] result = simplifier.simplify(tolerance); + return coverage.getFactory().createGeometryCollection(result); } @Metadata(description="Simplify inner edges of a coverage") - public static Geometry simplifyinner(Geometry coverage, double tolerance) { + public static Geometry simplifyInner(Geometry coverage, double tolerance) { Geometry[] cov = toGeometryArray(coverage); Geometry[] result = CoverageSimplifier.simplifyInner(cov, tolerance); + return coverage.getFactory().createGeometryCollection(result); + } + + @Metadata(description="Simplify outer edges of a coverage") + public static Geometry simplifyOuter(Geometry coverage, double tolerance) { + Geometry[] cov = toGeometryArray(coverage); + Geometry[] result = CoverageSimplifier.simplifyOuter(cov, tolerance); + return coverage.getFactory().createGeometryCollection(result); + } + + @Metadata(description="Simplify inner and outer edges of a coverage differently") + public static Geometry simplifyInOut(Geometry coverage, + @Metadata(title="Inner Distance tol") + double toleranceInner, + @Metadata(title="Outer Distance tol") + double toleranceOuter) { + Geometry[] cov = toGeometryArray(coverage); + CoverageSimplifier simplifier = new CoverageSimplifier(cov); + Geometry[] result = simplifier.simplify(toleranceInner, toleranceOuter); + return coverage.getFactory().createGeometryCollection(result); + } + + @Metadata(description="Simplify a coverage with per-geometry tolerances") + public static Geometry simplifyTolerances(Geometry coverage, + @Metadata(title="Tolerances (comma-sep)") + String tolerancesCSV) { + Geometry[] cov = toGeometryArray(coverage); + double[] tolerances = tolerances(tolerancesCSV, cov.length); + Geometry[] result = CoverageSimplifier.simplify(cov, tolerances); return FunctionsUtil.buildGeometry(result); } - static Geometry extractPolygons(Geometry geom) { - List components = PolygonExtracter.getPolygons(geom); - Geometry result = geom.getFactory().buildGeometry(components); - return 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); } private static Geometry[] toGeometryArray(Geometry geom) { diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/Corner.java b/modules/core/src/main/java/org/locationtech/jts/coverage/Corner.java index 1f57fa10d5..e46889e862 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/Corner.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/Corner.java @@ -25,12 +25,12 @@ class Corner implements Comparable { private int next; private double area; - public Corner(LinkedLine edge, int i) { + public Corner(LinkedLine edge, int i, double area) { this.edge = edge; this.index = i; this.prev = edge.prev(i); this.next = edge.next(i); - this.area = area(edge, i); + this.area = area; } public boolean isVertex(int index) { @@ -58,13 +58,6 @@ public Coordinate prev() { public Coordinate next() { return edge.getCoordinate(next); } - - private static double area(LinkedLine edge, int index) { - Coordinate pp = edge.prevCoordinate(index); - Coordinate p = edge.getCoordinate(index); - Coordinate pn = edge.nextCoordinate(index); - return Triangle.area(pp, p, pn); - } /** * Orders corners by increasing area. diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CornerArea.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CornerArea.java new file mode 100644 index 0000000000..8338625727 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CornerArea.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.coverage; + +import org.locationtech.jts.algorithm.Angle; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Triangle; +import org.locationtech.jts.math.MathUtil; + +/** + * Computes the effective area of corners, + * taking into account the smoothing weight. + * + *

FUTURE WORK

+ * + * Support computing geodetic area + * + * @author Martin Davis + * + */ +class CornerArea { + public static final double DEFAULT_SMOOTH_WEIGHT = 0.0; + + private double smoothWeight = DEFAULT_SMOOTH_WEIGHT; + + public CornerArea() { + } + + /** + * Creates a new corner area computer. + * + * @param smoothWeight the weight for smoothing corners. In range [0..1]. + */ + public CornerArea(double smoothWeight) { + this.smoothWeight = smoothWeight; + } + + public double area(Coordinate pp, Coordinate p, Coordinate pn) { + + double area = Triangle.area(pp, p, pn); + double ang = angleNorm(pp, p, pn); + //-- rescale to [-1 .. 1], with 1 being narrow and -1 being flat + double angBias = 1.0 - 2.0 * ang; + //-- reduce area for narrower corners, to make them more likely to be removed + double areaWeighted = (1 - smoothWeight * angBias) * area; + return areaWeighted; + } + + private static double angleNorm(Coordinate pp, Coordinate p, Coordinate pn) { + double angNorm = Angle.angleBetween(pp, p, pn) / 2 / Math.PI; + return MathUtil.clamp(angNorm, 0, 1); + } +} 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 2208200b29..74bd2993b1 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 @@ -11,47 +11,38 @@ */ package org.locationtech.jts.coverage; -import java.util.List; - import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateArrays; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LineSegment; import org.locationtech.jts.geom.LineString; -import org.locationtech.jts.geom.MultiLineString; import org.locationtech.jts.io.WKTWriter; /** * An edge of a polygonal coverage formed from all or a section of a polygon ring. - * An edge may be a free ring, which is a ring which has not node points - * (i.e. does not touch any other rings in the parent coverage). + * An edge may be a free ring, which is a ring which has no node points + * (i.e. does not share a vertex with any other rings in the parent coverage). * * @author mdavis * */ class CoverageEdge { - public static CoverageEdge createEdge(Coordinate[] ring) { + public static final int RING_COUNT_INNER = 2; + public static final int RING_COUNT_OUTER = 1; + + public static CoverageEdge createEdge(Coordinate[] ring, boolean isPrimary) { Coordinate[] pts = extractEdgePoints(ring, 0, ring.length - 1); - CoverageEdge edge = new CoverageEdge(pts, true); + CoverageEdge edge = new CoverageEdge(pts, isPrimary, true); return edge; } - public static CoverageEdge createEdge(Coordinate[] ring, int start, int end) { + public static CoverageEdge createEdge(Coordinate[] ring, int start, int end, boolean isPrimary) { Coordinate[] pts = extractEdgePoints(ring, start, end); - CoverageEdge edge = new CoverageEdge(pts, false); + CoverageEdge edge = new CoverageEdge(pts, isPrimary, false); return edge; } - static MultiLineString createLines(List edges, GeometryFactory geomFactory) { - LineString lines[] = new LineString[edges.size()]; - for (int i = 0; i < edges.size(); i++) { - CoverageEdge edge = edges.get(i); - lines[i] = edge.toLineString(geomFactory); - } - MultiLineString mls = geomFactory.createMultiLineString(lines); - return mls; - } - private static Coordinate[] extractEdgePoints(Coordinate[] ring, int start, int end) { int size = start < end ? end - start + 1 @@ -136,12 +127,16 @@ else if (i > pts.length - 1) { private Coordinate[] pts; 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 isFreeRing) { + public CoverageEdge(Coordinate[] pts, boolean isPrimary, boolean isFreeRing) { this.pts = pts; + this.isPrimary = isPrimary; this.isFreeRing = isFreeRing; } - + public void incRingCount() { ringCount++; } @@ -150,9 +145,30 @@ public int getRingCount() { return ringCount; } + public boolean isInner() { + return ringCount == RING_COUNT_INNER; + } + + public boolean isOuter() { + return ringCount == RING_COUNT_OUTER; + } + + public void setPrimary(boolean isPrimary) { + //-- preserve primary status if set + if (this.isPrimary) + return; + this.isPrimary = isPrimary; + } + + public boolean isRemovableRing() { + boolean isRing = CoordinateArrays.isRing(pts); + return isRing && ! isPrimary; + } + /** * Returns whether this edge is a free ring; - * i.e. one with no constrained nodes. + * i.e. one that does not have nodes + * which are anchored because they occur in another ring. * * @return true if this is a free ring */ @@ -184,5 +200,28 @@ 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; + } + + public boolean hasAdjacentIndex(int index) { + if (index == 0) + return adjacentIndex0 >= 0; + return adjacentIndex1 >= 0; + } } 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 ba5f852219..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 @@ -24,6 +24,7 @@ import org.locationtech.jts.geom.CoordinateList; import org.locationtech.jts.geom.CoordinateSequence; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LineSegment; import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.MultiPolygon; @@ -73,28 +74,15 @@ public List getEdges() { return edges; } - /** - * Selects the edges with a given ring count (which can be 1 or 2). - * - * @param ringCount the edge ring count to select (1 or 2) - * @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); @@ -102,25 +90,45 @@ private void build() { if (poly.isEmpty()) continue; + //-- largest polygon is the primary one, which is never removed + boolean isPrimary = ipoly == indexLargest; + //-- extract shell LinearRing shell = poly.getExteriorRing(); - addRingEdges(shell, 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); - //-- skip empty rings. Missing rings are copied in result + //-- skip empty holes. Missing rings are copied in result if (hole.isEmpty()) continue; - addRingEdges(hole, nodes, boundarySegs, uniqueEdgeMap); + //-- holes are never primary + addRingEdges(i, hole, false, nodes, boundarySegs, uniqueEdgeMap); } } } } - private void addRingEdges(LinearRing ring, Set nodes, Set boundarySegs, + private int findLargestPolygonIndex(Geometry geom) { + if (geom instanceof Polygon) + return 0; + int indexLargest = -1; + double areaLargest = -1; + for (int ipoly = 0; ipoly < geom.getNumGeometries(); ipoly++) { + Polygon poly = (Polygon) geom.getGeometryN(ipoly); + double area = poly.getArea(); + if (area > areaLargest) { + areaLargest = area; + indexLargest = ipoly; + } + } + return indexLargest; + } + + private void addRingEdges(int index, LinearRing ring, boolean isPrimary, Set nodes, Set boundarySegs, HashMap uniqueEdgeMap) { addBoundaryInnerNodes(ring, boundarySegs, nodes); - List ringEdges = extractRingEdges(ring, uniqueEdgeMap, nodes); + List ringEdges = extractRingEdges(index, ring, isPrimary, uniqueEdgeMap, nodes); if (ringEdges != null) ringEdgesMap.put(ring, ringEdges); } @@ -149,8 +157,18 @@ private void addBoundaryInnerNodes(LinearRing ring, Set boundarySeg } } - private List extractRingEdges(LinearRing ring, - HashMap uniqueEdgeMap, + /** + * Extracts the {@link CoverageEdge}s for a ring. + * @param index + * + * @param ring + * @param isRetained true if the ring is retained (must not be removed) + * @param uniqueEdgeMap + * @param nodes + * @return null if the ring has too few distinct vertices + */ + private List extractRingEdges(int index, LinearRing ring, + boolean isPrimary, HashMap uniqueEdgeMap, Set nodes) { // System.out.println(ring); List ringEdges = new ArrayList(); @@ -164,15 +182,21 @@ 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, 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); - CoverageEdge edge = createEdge(pts, start, end, uniqueEdgeMap); + //-- a single-node ring is only retained if specified + if (end == start) { + isEdgePrimary = isPrimary; + } + CoverageEdge edge = createEdge(pts, start, end, index, isEdgePrimary, uniqueEdgeMap); // System.out.println(ringEdges.size() + " : " + edge); ringEdges.add(edge); start = end; @@ -180,33 +204,37 @@ private List extractRingEdges(LinearRing ring, } return ringEdges; } - - private CoverageEdge createEdge(Coordinate[] ring, HashMap uniqueEdgeMap) { - CoverageEdge edge; - LineSegment edgeKey = CoverageEdge.key(ring); - if (uniqueEdgeMap.containsKey(edgeKey)) { - edge = uniqueEdgeMap.get(edgeKey); - } - else { - edge = CoverageEdge.createEdge(ring); - uniqueEdgeMap.put(edgeKey, edge); - edges.add(edge); - } - edge.incRingCount(); - return edge; - } - private CoverageEdge createEdge(Coordinate[] ring, int start, int end, HashMap uniqueEdgeMap) { + /** + * Creates or updates an edge for the given ring or ring section. + * + * @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, 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); + //-- update shared attributes + edge.setPrimary(isPrimary); } else { - edge = CoverageEdge.createEdge(ring, start, end); + if (start < 0) { + edge = CoverageEdge.createEdge(ring, isPrimary); + } + else { + edge = CoverageEdge.createEdge(ring, start, end, isPrimary); + } uniqueEdgeMap.put(edgeKey, edge); edges.add(edge); } + edge.addIndex(index); edge.incRingCount(); return edge; } @@ -299,24 +327,43 @@ private Geometry buildPolygonal(Geometry geom) { } private Geometry buildMultiPolygon(MultiPolygon geom) { - Polygon[] polys = new Polygon[geom.getNumGeometries()]; - for (int i = 0; i < polys.length; i++) { - polys[i] = buildPolygon((Polygon) geom.getGeometryN(i)); + List polyList = new ArrayList(); + for (int i = 0; i < geom.getNumGeometries(); i++) { + Polygon poly = buildPolygon((Polygon) geom.getGeometryN(i)); + if (poly != null) { + polyList.add(poly); + } + } + if (polyList.size() == 1) { + return polyList.get(0); } + Polygon[] polys = GeometryFactory.toPolygonArray(polyList); return geom.getFactory().createMultiPolygon(polys); } + /** + * + * @param polygon + * @return null if the polygon has been removed + */ private Polygon buildPolygon(Polygon polygon) { LinearRing shell = buildRing(polygon.getExteriorRing()); - + if (shell == null) { + return null; + } if (polygon.getNumInteriorRing() == 0) { return polygon.getFactory().createPolygon(shell); } - LinearRing holes[] = new LinearRing[polygon.getNumInteriorRing()]; - for (int i = 0; i < holes.length; i++) { + List holeList = new ArrayList(); + for (int i = 0; i < polygon.getNumInteriorRing(); i++) { LinearRing hole = polygon.getInteriorRingN(i); - holes[i] = buildRing(hole); + LinearRing newHole = buildRing(hole); + if (newHole != null) { + holeList.add(newHole); + } } + //LinearRing holes[] = new LinearRing[polygon.getNumInteriorRing()]; + LinearRing holes[] = GeometryFactory.toLinearRingArray(holeList); return polygon.getFactory().createPolygon(shell, holes); } @@ -326,6 +373,11 @@ private LinearRing buildRing(LinearRing ring) { if (ringEdges == null) return (LinearRing) ring.copy(); + boolean isRemoved = ringEdges.size() == 1 + && ringEdges.get(0).getCoordinates().length == 0; + if (isRemoved) + return null; + CoordinateList ptsList = new CoordinateList(); for (int i = 0; i < ringEdges.size(); i++) { Coordinate lastPt = ptsList.size() > 0 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 6a0134e814..3ad9b16381 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 @@ -11,12 +11,10 @@ */ package org.locationtech.jts.coverage; -import java.util.BitSet; import java.util.List; +import org.locationtech.jts.coverage.TPVWSimplifier.Edge; import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.GeometryFactory; -import org.locationtech.jts.geom.MultiLineString; /** * Simplifies the boundaries of the polygons in a polygonal coverage @@ -35,18 +33,34 @@ *

* The simplified result coverage has the following characteristics: *

    - *
  • It has the same number and types of polygonal geometries as the input + *
  • It has the same number of polygonal geometries as the input + *
  • If the input is a valid coverage, then so is the result *
  • Node points (inner vertices shared by three or more polygons, * or boundary vertices shared by two or more) are not changed - *
  • If the input is a valid coverage, then so is the result + *
  • Polygons maintain their line-adjacency (edges are never removed) + *
  • Rings are simplified to a minimum of 4 vertices, to better preserve their shape + *
  • Rings smaller than the area tolerance are removed where possible. + * This applies to both holes and "islands" (multipolygon elements + * which are disjoint or touch another polygon at a single vertex). + * At least one polygon is retained for each input geometry + * (the one with largest area). *
- * This class also supports inner simplification, which simplifies + * This class supports simplification using different distance tolerances + * for inner and outer edges of the coverage (including no simplfication + * using a tolerance of 0.0). + * This allows, for example, inner simplification, which simplifies * only edges of the coverage which are adjacent to two polygons. * This allows partial simplification of a coverage, since a simplified * subset of a coverage still matches the remainder of the coverage. *

+ * The class allows specifying a separate tolerance for each element of the input coverage. + *

* The input coverage should be valid according to {@link CoverageValidator}. - * Invalid coverages may still be simplified, but the result will still be invalid. + * Invalid coverages may be simplified, but the result will likely still be invalid. + * + *

FUTURE WORK

+ * + * Support geodetic data by computing true geodetic area, and accepting tolerances in metres. * * @author Martin Davis */ @@ -58,13 +72,29 @@ public class CoverageSimplifier { * * @param coverage a set of polygonal geometries forming a coverage * @param tolerance the simplification tolerance - * @return the simplified polygons + * @return the simplified coverage polygons */ public static Geometry[] simplify(Geometry[] coverage, double tolerance) { CoverageSimplifier simplifier = new CoverageSimplifier(coverage); return simplifier.simplify(tolerance); } + /** + * Simplifies the boundaries of a set of polygonal geometries forming a coverage, + * preserving the coverage topology, using a separate tolerance + * for each element of the coverage. + * Coverage edges are simplified using the lowest tolerance of each adjacent + * element. + * + * @param coverage a set of polygonal geometries forming a coverage + * @param tolerance the simplification tolerances (one per input element) + * @return the simplified coverage polygons + */ + 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. @@ -72,15 +102,30 @@ public static Geometry[] simplify(Geometry[] coverage, double tolerance) { * * @param coverage a set of polygonal geometries forming a coverage * @param tolerance the simplification tolerance - * @return the simplified polygons + * @return the simplified coverage polygons */ public static Geometry[] simplifyInner(Geometry[] coverage, double tolerance) { CoverageSimplifier simplifier = new CoverageSimplifier(coverage); - return simplifier.simplifyInner(tolerance); + return simplifier.simplify(tolerance, 0); } - private Geometry[] input; - private GeometryFactory geomFactory; + /** + * Simplifies the outer boundaries of a set of polygonal geometries forming a coverage, + * preserving the coverage topology. + * Edges in the interior of the coverage are left unchanged. + * + * @param coverage a set of polygonal geometries forming a coverage + * @param tolerance the simplification tolerance + * @return the simplified polygons + */ + public static Geometry[] simplifyOuter(Geometry[] coverage, double tolerance) { + CoverageSimplifier simplifier = new CoverageSimplifier(coverage); + return simplifier.simplify(0, tolerance); + } + + private Geometry[] coverage; + private double smoothWeight = CornerArea.DEFAULT_SMOOTH_WEIGHT; + private double removableSizeFactor = 1.0; /** * Create a new coverage simplifier instance. @@ -88,63 +133,148 @@ public static Geometry[] simplifyInner(Geometry[] coverage, double tolerance) { * @param coverage a set of polygonal geometries forming a coverage */ public CoverageSimplifier(Geometry[] coverage) { - input = coverage; - geomFactory = coverage[0].getFactory(); + this.coverage = coverage; } /** - * Computes the simplified coverage, preserving the coverage topology. + * Sets the factor applied to the area tolerance to determine + * if small rings should be removed. + * Larger values cause more rings to be removed. + * A value of 0 prevents rings from being removed. * - * @param tolerance the simplification tolerance - * @return the simplified polygons + * @param removableSizeFactor the factor to determine ring size to remove */ - public Geometry[] simplify(double tolerance) { - CoverageRingEdges cov = CoverageRingEdges.create(input); - simplifyEdges(cov.getEdges(), null, tolerance); - Geometry[] result = cov.buildCoverage(); - return result; + public void setRemovableRingSizeFactor(double removableSizeFactor) { + double factor = removableSizeFactor; + if (factor < 0.0) + factor = 0.0; + this.removableSizeFactor = factor; } /** - * Computes the inner-boundary simplified coverage, - * preserving the coverage topology, - * and leaving outer boundary edges unchanged. + * Sets the weight influencing how smooth the simplification should be. + * The weight must be between 0 and 1. + * Larger values increase the smoothness of the simplified edges. * - * @param tolerance the simplification tolerance - * @return the simplified polygons + * @param smoothWeight a value between 0 and 1 */ - public Geometry[] simplifyInner(double tolerance) { - CoverageRingEdges cov = CoverageRingEdges.create(input); - List innerEdges = cov.selectEdges(2); - List outerEdges = cov.selectEdges(1); - MultiLineString constraintEdges = CoverageEdge.createLines(outerEdges, geomFactory); + public void setSmoothWeight(double smoothWeight) { + if (smoothWeight < 0.0 || smoothWeight > 1.0) + throw new IllegalArgumentException("smoothWeight must be in range [0 - 1]"); + this.smoothWeight = smoothWeight; + } + + /** + * Computes the simplified coverage using a single distance tolerance, + * preserving the coverage topology. + * + * @param tolerance the simplification distance tolerance + * @return the simplified coverage polygons + */ + public Geometry[] simplify(double tolerance) { + return simplifyEdges(tolerance, tolerance); + } - simplifyEdges(innerEdges, constraintEdges, tolerance); - Geometry[] result = cov.buildCoverage(); - return result; + /** + * Computes the simplified coverage using separate distance tolerances + * for inner and outer edges, + * preserving the coverage topology. + * + * @param toleranceInner the distance tolerance for inner edges + * @param toleranceOuter the distance tolerance for outer edges + * @return the simplified coverage polygons + */ + public Geometry[] simplify(double toleranceInner, double toleranceOuter) { + return simplifyEdges(toleranceInner, toleranceOuter); + } + + /** + * Computes the simplified coverage using separate distance tolerances + * for each coverage element, + * preserving the coverage topology. + * + * @param tolerances the distance tolerances for the coverage elements + * @return the simplified coverage polygons + */ + public Geometry[] simplify(double[] tolerances) { + if (tolerances.length != coverage.length) + throw new IllegalArgumentException("number of tolerances does not match number of 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 void simplifyEdges(List edges, MultiLineString constraints, double tolerance) { - MultiLineString lines = CoverageEdge.createLines(edges, geomFactory); - BitSet freeRings = getFreeRings(edges); - MultiLineString linesSimp = TPVWSimplifier.simplify(lines, freeRings, constraints, tolerance); - //Assert: mlsSimp.getNumGeometries = edges.length + private double computeTolerance(CoverageEdge covEdge, double[] tolerances) { + int index0 = covEdge.getAdjacentIndex(0); + // assert: index0 >= 0 + double tolerance = tolerances[index0]; - setCoordinates(edges, linesSimp); + if (covEdge.hasAdjacentIndex(1)) { + int index1 = covEdge.getAdjacentIndex(1); + double tol1 = tolerances[index1]; + //-- use lowest tolerance for edge + 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); + Geometry[] result = covRings.buildCoverage(); + return result; } - private void setCoordinates(List edges, MultiLineString lines) { - for (int i = 0; i < edges.size(); i++) { - edges.get(i).setCoordinates(lines.getGeometryN(i).getCoordinates()); + private static TPVWSimplifier.Edge[] createEdges(List covEdges, double toleranceInner, double toleranceOuter) { + 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, toleranceInner, toleranceOuter); + edges[i] = createEdge(covEdge, tol); } + return edges; } - private BitSet getFreeRings(List edges) { - BitSet freeRings = new BitSet(edges.size()); - for (int i = 0 ; i < edges.size() ; i++) { - freeRings.set(i, edges.get(i).isFreeRing()); + private static Edge createEdge(CoverageEdge covEdge, double tol) { + return new TPVWSimplifier.Edge(covEdge.getCoordinates(), tol, + covEdge.isFreeRing(), covEdge.isRemovableRing()); + } + + private static double computeTolerance(CoverageEdge covEdge, double toleranceInner, double toleranceOuter) { + return covEdge.isInner() ? toleranceInner : toleranceOuter; + } + + private void setCoordinates(List covEdges, Edge[] edges) { + for (int i = 0; i < covEdges.size(); i++) { + Edge edge = edges[i]; + if (edge.getTolerance() > 0) { + covEdges.get(i).setCoordinates(edges[i].getCoordinates()); + } } - return freeRings; } } diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java index 64f37913d6..dc73247f28 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java @@ -11,17 +11,13 @@ */ package org.locationtech.jts.coverage; -import java.util.ArrayList; -import java.util.BitSet; import java.util.List; import java.util.PriorityQueue; +import org.locationtech.jts.algorithm.Area; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateArrays; import org.locationtech.jts.geom.Envelope; -import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.GeometryFactory; -import org.locationtech.jts.geom.LineString; -import org.locationtech.jts.geom.MultiLineString; import org.locationtech.jts.index.VertexSequencePackedRtree; import org.locationtech.jts.index.strtree.STRtree; import org.locationtech.jts.simplify.LinkedLine; @@ -33,6 +29,8 @@ * in the original input. * Line and ring endpoints are preserved, except for rings * which are flagged as "free". + * Rings which are smaller than the tolerance area + * may be removed entirely, as long as they are flagged as removable. *

* The amount of simplification is determined by a tolerance value, * which is a non-zero quantity. @@ -45,19 +43,6 @@ * */ class TPVWSimplifier { - - /** - * Simplifies a set of lines, preserving the topology of the lines. - * - * @param lines the lines to simplify - * @param distanceTolerance the simplification tolerance - * @return the simplified lines - */ - public static MultiLineString simplify(MultiLineString lines, double distanceTolerance) { - TPVWSimplifier simp = new TPVWSimplifier(lines, distanceTolerance); - MultiLineString result = (MultiLineString) simp.simplify(); - return result; - } /** * Simplifies a set of lines, preserving the topology of the lines between @@ -72,104 +57,118 @@ public static MultiLineString simplify(MultiLineString lines, double distanceTol * @param distanceTolerance the simplification tolerance * @return the simplified lines */ - public static MultiLineString simplify(MultiLineString lines, BitSet freeRings, - MultiLineString constraintLines, double distanceTolerance) { - TPVWSimplifier simp = new TPVWSimplifier(lines, distanceTolerance); - simp.setFreeRingIndices(freeRings); - simp.setConstraints(constraintLines); - MultiLineString result = (MultiLineString) simp.simplify(); - return result; + public static void simplify(Edge[] edges, + CornerArea cornerArea, + double removableSizeFactor) { + TPVWSimplifier simp = new TPVWSimplifier(edges); + simp.setCornerArea(cornerArea); + simp.setRemovableRingSizeFactor(removableSizeFactor); + simp.simplify(); } - private MultiLineString inputLines; - private BitSet isFreeRing; - private double areaTolerance; - private GeometryFactory geomFactory; - private MultiLineString constraintLines = null; - - private TPVWSimplifier(MultiLineString lines, double distanceTolerance) { - this.inputLines = lines; - this.areaTolerance = distanceTolerance * distanceTolerance; - geomFactory = inputLines.getFactory(); + private CornerArea cornerArea; + private double removableSizeFactor = 1.0; + private Edge[] edges; + + public TPVWSimplifier(Edge[] edges) { + this.edges = edges; } - private void setConstraints(MultiLineString constraints) { - this.constraintLines = constraints; + public void setRemovableRingSizeFactor(double removableSizeFactor) { + this.removableSizeFactor = removableSizeFactor; } - - public void setFreeRingIndices(BitSet isFreeRing) { - //Assert: bit set has same size as number of lines. - this.isFreeRing = isFreeRing; + + public void setCornerArea(CornerArea cornerArea) { + this.cornerArea = cornerArea; } - - private Geometry simplify() { - List edges = createEdges(inputLines, this.isFreeRing); - List constraintEdges = createEdges(constraintLines, null); - + + private void simplify() { EdgeIndex edgeIndex = new EdgeIndex(); - edgeIndex.add(edges); - edgeIndex.add(constraintEdges); + add(edges, edgeIndex); - LineString[] result = new LineString[edges.size()]; - for (int i = 0 ; i < edges.size(); i++) { - Edge edge = edges.get(i); - Coordinate[] ptsSimp = edge.simplify(edgeIndex); - result[i] = geomFactory.createLineString(ptsSimp); + for (int i = 0 ; i < edges.length; i++) { + Edge edge = edges[i]; + edge.simplify(cornerArea, edgeIndex); } - return geomFactory.createMultiLineString(result); } - private List createEdges(MultiLineString lines, BitSet isFreeRing) { - List edges = new ArrayList(); - if (lines == null) - return edges; - for (int i = 0 ; i < lines.getNumGeometries(); i++) { - LineString line = (LineString) lines.getGeometryN(i); - boolean isFree = isFreeRing == null ? false : isFreeRing.get(i); - edges.add(new Edge(line, isFree, areaTolerance)); + private void add(Edge[] edges, EdgeIndex edgeIndex) { + for (Edge edge : edges) { + //-- don't include removed edges in index + edge.updateRemoved(removableSizeFactor); + if (! edge.isRemoved()) { + //-- avoid fluffing up removed edges + edge.init(); + edgeIndex.add(edge); + } } - return edges; } - private static class Edge { - private double areaTolerance; + public static class Edge { + private static final int MIN_EDGE_SIZE = 2; + private static final int MIN_RING_SIZE = 4; + private LinkedLine linkedLine; - private int minEdgeSize; private boolean isFreeRing; - private int nbPts; - + private int nPts; + private Coordinate[] pts; private VertexSequencePackedRtree vertexIndex; private Envelope envelope; + private boolean isRemoved = false; + private boolean isRemovable; + private double distanceTolerance = 0.0; /** * Creates a new edge. * The endpoints of the edge are preserved during simplification, * unless it is a ring and the {@Link #isFreeRing} flag is set. * - * @param inputLine the line or ring + * @param pts the line or ring + * @param distanceTolerance * @param isFreeRing whether a ring endpoint can be removed - * @param areaTolerance the simplification tolerance + * @param isFreeRing + * @param isRemovable */ - Edge(LineString inputLine, boolean isFreeRing, double areaTolerance) { - this.areaTolerance = areaTolerance; + Edge(Coordinate[] pts, double distanceTolerance, boolean isFreeRing, boolean isRemovable) { + this.envelope = CoordinateArrays.envelope(pts); + this.pts = pts; + this.nPts = pts.length; this.isFreeRing = isFreeRing; - this.envelope = inputLine.getEnvelopeInternal(); - Coordinate[] pts = inputLine.getCoordinates(); - this.nbPts = pts.length; - linkedLine = new LinkedLine(pts); - minEdgeSize = linkedLine.isRing() ? 3 : 2; - - vertexIndex = new VertexSequencePackedRtree(pts); - //-- remove ring duplicate final vertex - if (linkedLine.isRing()) { - vertexIndex.remove(pts.length-1); - } + this.isRemovable = isRemovable; + this.distanceTolerance = distanceTolerance; } + public void updateRemoved(double removableSizeFactor) { + if (! isRemovable) + return; + double areaTolerance = distanceTolerance * distanceTolerance; + isRemoved = CoordinateArrays.isRing(pts) + && Area.ofRing(pts) < removableSizeFactor * areaTolerance; + } + + public void init() { + linkedLine = new LinkedLine(pts); + } + + public double getTolerance() { + return distanceTolerance; + } + + public boolean isRemoved() { + return isRemoved; + } + private Coordinate getCoordinate(int index) { - return linkedLine.getCoordinate(index); + return pts[index]; } + public Coordinate[] getCoordinates() { + if (isRemoved) { + return new Coordinate[0]; + } + return linkedLine.getCoordinates(); + } + public Envelope getEnvelope() { return envelope; } @@ -178,8 +177,18 @@ public int size() { return linkedLine.size(); } - private Coordinate[] simplify(EdgeIndex edgeIndex) { - PriorityQueue cornerQueue = createQueue(); + public void simplify(CornerArea cornerArea, EdgeIndex edgeIndex) { + if (isRemoved) { + return; + } + //-- don't simplify + if (distanceTolerance <= 0.0) + return; + + double areaTolerance = distanceTolerance * distanceTolerance; + int minEdgeSize = linkedLine.isRing() ? MIN_RING_SIZE : MIN_EDGE_SIZE; + + PriorityQueue cornerQueue = createQueue(areaTolerance, cornerArea); while (! cornerQueue.isEmpty() && size() > minEdgeSize) { Corner corner = cornerQueue.poll(); @@ -191,31 +200,39 @@ && size() > minEdgeSize) { if (corner.getArea() > areaTolerance) break; if (isRemovable(corner, edgeIndex) ) { - removeCorner(corner, cornerQueue); + removeCorner(corner, areaTolerance, cornerArea, cornerQueue); } } - return linkedLine.getCoordinates(); } - private PriorityQueue createQueue() { + private PriorityQueue createQueue(double areaTolerance, CornerArea cornerArea) { PriorityQueue cornerQueue = new PriorityQueue(); int minIndex = (linkedLine.isRing() && isFreeRing) ? 0 : 1; - int maxIndex = nbPts - 1; + int maxIndex = nPts - 1; for (int i = minIndex; i < maxIndex; i++) { - addCorner(i, cornerQueue); + addCorner(i, areaTolerance, cornerArea, cornerQueue); } return cornerQueue; } - private void addCorner(int i, PriorityQueue cornerQueue) { - if (isFreeRing || (i != 0 && i != nbPts-1)) { - Corner corner = new Corner(linkedLine, i); - if (corner.getArea() <= areaTolerance) { + private void addCorner(int i, double areaTolerance, CornerArea cornerArea, PriorityQueue cornerQueue) { + //-- add if this vertex can be a corner + if (isFreeRing || (i != 0 && i != nPts - 1)) { + double area = area(i, cornerArea); + if (area <= areaTolerance) { + Corner corner = new Corner(linkedLine, i, area); cornerQueue.add(corner); } } } + private double area(int index, CornerArea cornerArea) { + Coordinate pp = linkedLine.prevCoordinate(index); + Coordinate p = linkedLine.getCoordinate(index); + Coordinate pn = linkedLine.nextCoordinate(index); + return cornerArea.area(pp, p, pn); + } + private boolean isRemovable(Corner corner, EdgeIndex edgeIndex) { Envelope cornerEnv = corner.envelope(); //-- check nearby lines for violating intersections @@ -260,7 +277,18 @@ private boolean hasIntersectingVertex(Corner corner, Envelope cornerEnv, return false; } + private void initIndex() { + vertexIndex = new VertexSequencePackedRtree(pts); + //-- remove ring duplicate final vertex + if (CoordinateArrays.isRing(pts)) { + vertexIndex.remove(pts.length-1); + } + } + private int[] query(Envelope cornerEnv) { + if (vertexIndex == null) { + initIndex(); + } return vertexIndex.query(cornerEnv); } @@ -271,9 +299,11 @@ private int[] query(Envelope cornerEnv) { * (if they are non-convex and thus removable). * * @param corner the corner to remove + * @param cornerArea + * @param areaTolerance * @param cornerQueue the corner queue */ - private void removeCorner(Corner corner, PriorityQueue cornerQueue) { + private void removeCorner(Corner corner, double areaTolerance, CornerArea cornerArea, PriorityQueue cornerQueue) { int index = corner.getIndex(); int prev = linkedLine.prev(index); int next = linkedLine.next(index); @@ -281,8 +311,8 @@ private void removeCorner(Corner corner, PriorityQueue cornerQueue) { vertexIndex.remove(index); //-- potentially add the new corners created - addCorner(prev, cornerQueue); - addCorner(next, cornerQueue); + addCorner(prev, areaTolerance, cornerArea, cornerQueue); + addCorner(next, areaTolerance, cornerArea, cornerQueue); } public String toString() { @@ -294,12 +324,6 @@ private static class EdgeIndex { STRtree index = new STRtree(); - public void add(List edges) { - for (Edge edge : edges) { - add(edge); - } - } - public void add(Edge edge) { index.insert(edge.getEnvelope(), edge); } 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 a5822e956d..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 @@ -58,10 +58,10 @@ public void testNoopMulti() { public void testRepeatedPointRemoved() { checkResult(readArray( - "POLYGON ((5 9, 6.5 6.5, 9 5, 5 5, 5 5, 5 9))" ), + "POLYGON ((2 9, 7 6, 9 1, 2 1, 2 1, 3 6, 2 9))" ), 2, readArray( - "POLYGON ((5 5, 5 9, 9 5, 5 5))" ) + "POLYGON ((2 1, 2 9, 7 6, 9 1, 2 1))" ) ); } @@ -118,10 +118,10 @@ public void testMultiPolygons() { public void testSingleRingNoCollapse() { checkResult(readArray( - "POLYGON ((10 50, 60 90, 70 50, 60 10, 10 50))" ), + "POLYGON ((10 50, 50 90, 60 90, 70 50, 60 10, 10 50))" ), 100000, readArray( - "POLYGON ((10 50, 60 90, 60 10, 10 50))" ) + "POLYGON ((10 50, 60 90, 70 50, 60 10, 10 50))" ) ); } @@ -146,8 +146,8 @@ public void testFilledHole() { "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (50 20, 20 30, 20 80, 60 50, 80 20, 50 20))" ), 28, readArray( - "POLYGON ((20 30, 20 80, 80 20, 20 30))", - "POLYGON ((10 10, 10 90, 90 90, 90 10, 10 10), (20 30, 80 20, 20 80, 20 30))" ) + "POLYGON ((20 30, 20 80, 60 50, 80 20, 20 30))", + "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (20 30, 20 80, 60 50, 80 20, 20 30))" ) ); } @@ -158,26 +158,26 @@ public void testTouchingHoles() { "POLYGON (( 12 6, 12 7, 13 7, 13 9, 14 9, 14 6, 12 6 ))"), 1.0, readArray( - "POLYGON ((0 0, 0 11, 19 11, 19 0, 0 0), (4 5, 12 5, 12 6, 10 6, 9 9, 6 8, 6 6, 4 5), (12 6, 14 6, 14 9, 12 6))", + "POLYGON ((0 0, 0 11, 19 11, 19 0, 0 0), (4 5, 12 5, 12 6, 10 6, 9 9, 6 8, 6 6, 4 5), (12 6, 14 6, 14 9, 13 7, 12 6))", "POLYGON ((4 5, 6 6, 6 8, 9 9, 10 6, 12 6, 12 5, 4 5))", - "POLYGON ((12 6, 14 9, 14 6, 12 6))" ) + " POLYGON ((12 6, 14 6, 14 9, 13 7, 12 6))" ) ); } - public void testHoleTouchingShell() { + public void testInnerHoleTouchingShell() { checkResultInner(readArray( - "POLYGON ((200 300, 300 300, 300 100, 100 100, 100 300, 200 300), (170 220, 170 160, 200 140, 200 250, 170 220), (170 250, 200 250, 200 300, 170 250))", - "POLYGON ((170 220, 200 250, 200 140, 170 160, 170 220))", - "POLYGON ((170 250, 200 300, 200 250, 170 250))"), - 100.0, + "POLYGON ((200 300, 300 300, 300 100, 100 100, 100 300, 200 300), (170 220, 170 160, 200 140, 210 200, 200 250, 170 220), (170 250, 200 250, 200 300, 180 280, 170 250))", + "POLYGON ((170 220, 200 250, 210 200, 200 140, 170 160, 170 220))", + "POLYGON ((170 250, 180 280, 200 300, 200 250, 170 250))" ), + 70.0, readArray( - "POLYGON ((100 100, 100 300, 200 300, 300 300, 300 100, 100 100), (170 160, 200 140, 200 250, 170 160), (170 250, 200 250, 200 300, 170 250))", - "POLYGON ((170 160, 200 250, 200 140, 170 160))", - "POLYGON ((200 250, 200 300, 170 250, 200 250))" ) + "POLYGON ((100 100, 100 300, 200 300, 300 300, 300 100, 100 100), (170 160, 200 140, 200 250, 170 220, 170 160), (170 250, 200 250, 200 300, 170 250))", + "POLYGON ((170 160, 170 220, 200 250, 200 140, 170 160))", + "POLYGON ((170 250, 200 300, 200 250, 170 250))" ) ); } - public void testHolesTouchingHolesAndShellInner() { + public void testInnerHolesTouchingHolesAndShell() { checkResultInner(readArray( "POLYGON (( 8 5, 9 4, 9 2, 1 2, 1 4, 2 4, 2 5, 1 5, 1 8, 9 8, 9 6, 8 5 ), ( 8 5, 7 6, 6 6, 6 4, 7 4, 8 5 ), ( 7 6, 8 6, 7 7, 7 6 ), ( 6 6, 6 7, 5 6, 6 6 ), ( 6 4, 5 4, 6 3, 6 4 ), ( 7 4, 7 3, 8 4, 7 4 ))"), 4.0, @@ -191,11 +191,11 @@ public void testHolesTouchingHolesAndShell() { "POLYGON (( 8 5, 9 4, 9 2, 1 2, 1 4, 2 4, 2 5, 1 5, 1 8, 9 8, 9 6, 8 5 ), ( 8 5, 7 6, 6 6, 6 4, 7 4, 8 5 ), ( 7 6, 8 6, 7 7, 7 6 ), ( 6 6, 6 7, 5 6, 6 6 ), ( 6 4, 5 4, 6 3, 6 4 ), ( 7 4, 7 3, 8 4, 7 4 ))"), 4.0, readArray( - "POLYGON (( 1 2, 1 8, 9 8, 8 5, 9 2, 1 2 ), ( 5 4, 6 3, 6 4, 5 4 ), ( 5 6, 6 6, 6 7, 5 6 ), ( 6 4, 7 4, 8 5, 7 6, 6 6, 6 4 ), ( 7 3, 8 4, 7 4, 7 3 ), ( 7 6, 8 6, 7 7, 7 6 ))") + "POLYGON ((8 5, 9 2, 1 2, 1 8, 9 8, 8 5), (8 5, 7 6, 6 6, 6 4, 7 4, 8 5))") ); } - public void testMultiPolygonWithTouchingShellsInner() { + public void testInnerMultiPolygonWithTouchingShells() { checkResultInner( readArray( "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 )))"), @@ -208,14 +208,14 @@ public void testMultiPolygonWithTouchingShellsInner() { public void testMultiPolygonWithTouchingShells() { checkResult( readArray( - "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 )))"), + "MULTIPOLYGON (((1 6, 1 7, 2 7, 2 6, 1 6)), ((0 7, 0 8, 1 8, 1.2 7.5, 1 7, 0 7)), ((0 5, 0 6, 1 6, 1.2 5.5, 1 5, 0 5)))"), 1.0, readArray( - "MULTIPOLYGON (((0 5, 0 6, 1 6, 0 5)), ((0 8, 1 8, 1 7, 0 8)), ((1 6, 1 7, 2 7, 2 6, 1 6)), ((2 5, 2 6, 3 5, 2 5)), ((2 7, 3 8, 3 7, 2 7)))") + "MULTIPOLYGON (((0 5, 0 6, 1 6, 1 5, 0 5)), ((0 7, 0 8, 1 8, 1 7, 0 7)), ((1 6, 1 7, 2 6, 1 6)))") ); } - public void testTouchingShellsInner() { + public void testInnerTouchingShells() { checkResultInner(readArray( "POLYGON ((0 0, 0 5, 5 6, 10 5, 10 0, 0 0))", "POLYGON ((0 10, 5 6, 10 10, 0 10))"), @@ -235,7 +235,7 @@ public void testShellSimplificationAtStartingNode() { ); } - public void testSimplifyInnerAtStartingNode() { + public void testInnerAtStartingNode() { checkResultInner(readArray( "POLYGON (( 0 5, 0 9, 6 9, 6 2, 1 2, 0 5 ), ( 1 5, 2 3, 5 3, 5 7, 1 7, 1 5 ))", "POLYGON (( 1 5, 1 7, 5 7, 5 3, 2 3, 1 5 ))"), @@ -246,7 +246,7 @@ public void testSimplifyInnerAtStartingNode() { ); } - public void testSimplifyAllAtStartingNode() { + public void testAtStartingNode() { checkResult(readArray( "POLYGON (( 0 5, 0 9, 6 9, 6 2, 1 2, 0 5 ), ( 1 5, 2 3, 5 3, 5 7, 1 7, 1 5 ))", "POLYGON (( 1 5, 1 7, 5 7, 5 3, 2 3, 1 5 ))"), @@ -306,6 +306,81 @@ public void testEmptyHole() { ); } + //============== Test with removed rings ======================= + + // A Polygon with a small hole containing another Polygon - small Polygon is primary so is not removed + public void testPolygonInHoleNotRemoved() { + checkResult(readArray( + "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9), (2 2, 2 3, 3 3, 2 2))", + "POLYGON ((2 2, 2 3, 3 3, 2 2))" + ), + 1, + readArray( + "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9), (2 2, 2 3, 3 3, 2 2))", + "POLYGON ((2 2, 2 3, 3 3, 2 2))" ) + ); + } + + public void testMultiPolygonWithSmallPartRemoved() { + checkResult(readArray( + "MULTIPOLYGON (((11 9, 15 9, 15 5, 11 5, 11 9)), ((11 2, 12 2, 11 1, 11 2)))", + "POLYGON ((15 9, 18 9, 19 4, 14 1, 15 5, 15 9))" + ), + 1, + readArray( + "POLYGON ((11 5, 11 9, 15 9, 15 5, 11 5))", + "POLYGON ((15 9, 18 9, 19 4, 14 1, 15 5, 15 9))" ) + ); + } + + public void testMultiPolygonWithTouchingSmallPartsRemoved() { + checkResult(readArray( + "MULTIPOLYGON (((1 5, 5 5, 5 1, 1 1, 1 5)), ((6 3, 7 2, 6 2, 6 3)), ((8 2, 7 2, 8 3, 8 2)))" + ), + 1, + readArray( + "POLYGON ((1 5, 5 5, 5 1, 1 1, 1 5)))" ) + ); + } + + public void testMultiPolygonHolesSmallPartRemoved() { + checkResult(readArray( + "POLYGON ((0 9, 9 9, 9 0, 0 0, 0 9), (2 5, 2 4, 3 5, 2 5), (4 5, 4 3, 6 3, 6 5, 4 5))", + "MULTIPOLYGON (((2 5, 3 5, 2 4, 2 5)), ((4 5, 6 5, 6 3, 4 3, 4 5)))" + ), + 1, + readArray( + "POLYGON ((0 9, 9 9, 9 0, 0 0, 0 9), (4 5, 4 3, 6 3, 6 5, 4 5))", + "POLYGON ((4 5, 6 5, 6 3, 4 3, 4 5))" ) + ); + } + + public void testMultiPolygonHolesSmallPart() { + checkResultRemovalSize(readArray( + "POLYGON ((0 9, 9 9, 9 0, 0 0, 0 9), (2 5, 2 4, 3 5, 2 5), (4 5, 4 3, 6 3, 6 5, 4 5))", + "MULTIPOLYGON (((2 5, 3 5, 2 4, 2 5)), ((4 5, 6 5, 6 3, 4 3, 4 5)))" + ), + 1, 0, + readArray( + "POLYGON ((0 9, 9 9, 9 0, 0 0, 0 9), (2 5, 2 4, 3 5, 2 5), (4 5, 4 3, 6 3, 6 5, 4 5))", + "MULTIPOLYGON (((2 5, 3 5, 2 4, 2 5)), ((4 5, 6 5, 6 3, 4 3, 4 5)))" ) + ); + } + + 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))" ) + ); + } + //================================= @@ -314,11 +389,23 @@ 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); } + private void checkResultRemovalSize(Geometry[] input, double tolerance, double removalFactor, Geometry[] expected) { + CoverageSimplifier simplifier = new CoverageSimplifier(input); + simplifier.setRemovableRingSizeFactor(removalFactor); + Geometry[] actual = simplifier.simplify(tolerance); + checkEqual(expected, actual); + } + private void checkResultInner(Geometry[] input, double tolerance, Geometry[] expected) { Geometry[] actual = CoverageSimplifier.simplifyInner(input, tolerance); checkEqual(expected, actual); diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java index 58bb97f6ec..52d9bccc2e 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java @@ -11,9 +11,14 @@ */ package org.locationtech.jts.coverage; -import java.util.BitSet; +import java.util.ArrayList; +import java.util.List; +import org.locationtech.jts.coverage.TPVWSimplifier.Edge; +import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.MultiLineString; import junit.textui.TestRunner; @@ -47,10 +52,10 @@ public void testFreeRing() { } public void testNoFreeRing() { - checkSimplify("MULTILINESTRING ((1 9, 9 9, 9 1), (1 9, 1 1, 9 1), (5 5, 4 8, 2 8, 2 2, 4 2, 5 5), (5 5, 6 8, 8 8, 8 2, 6 2, 5 5))", + checkSimplify("MULTILINESTRING ((1 19, 19 19, 19 1), (1 19, 1 1, 19 1), (10 10, 9 18, 2 18, 2 2, 7 6, 10 10), (10 10, 11 18, 18 18, 18 2, 13 6, 10 10))", new int[] { }, 2, - "MULTILINESTRING ((1 9, 1 1, 9 1), (1 9, 9 9, 9 1), (5 5, 2 2, 2 8, 5 5), (5 5, 8 2, 8 8, 5 5))"); + "MULTILINESTRING ((1 19, 1 1, 19 1), (1 19, 19 19, 19 1), (10 10, 2 2, 2 18, 9 18, 10 10), (10 10, 11 18, 18 18, 18 2, 10 10))"); } public void testConstraint() { @@ -58,20 +63,15 @@ public void testConstraint() { new int[] { }, "MULTILINESTRING ((1 9, 9 9, 6 5, 9 1), (1 9, 1 1, 9 1))", 1, - "MULTILINESTRING ((6 8, 2 8, 2 2, 6 2, 5.9 5, 6 8))"); + "MULTILINESTRING ((1 9, 1 1, 9 1), (1 9, 9 9, 6 5, 9 1), (6 8, 2 8, 2 2, 6 2, 5.9 5, 6 8))"); } private void checkNoop(String wkt, double tolerance) { - MultiLineString geom = (MultiLineString) read(wkt); - Geometry actual = TPVWSimplifier.simplify(geom, tolerance); - checkEqual(geom, actual); + checkSimplify(wkt, null, null, tolerance, wkt); } private void checkSimplify(String wkt, double tolerance, String wktExpected) { - MultiLineString geom = (MultiLineString) read(wkt); - Geometry actual = TPVWSimplifier.simplify(geom, tolerance); - Geometry expected = read(wktExpected); - checkEqual(expected, actual); + checkSimplify(wkt, null, null, tolerance, wktExpected); } private void checkSimplify(String wkt, int[] freeRingIndex, @@ -82,16 +82,50 @@ private void checkSimplify(String wkt, int[] freeRingIndex, private void checkSimplify(String wkt, int[] freeRingIndex, String wktConstraints, double tolerance, String wktExpected) { - MultiLineString lines = (MultiLineString) read(wkt); - BitSet freeRings = new BitSet(); - for (int index : freeRingIndex) { - freeRings.set(index); - } - MultiLineString constraints = wktConstraints == null ? null - : (MultiLineString) read(wktConstraints); - Geometry actual = TPVWSimplifier.simplify(lines, freeRings, constraints, tolerance); + TPVWSimplifier.Edge[] edges = createEdges(wkt, freeRingIndex, wktConstraints, tolerance); + CornerArea cornerArea = new CornerArea(); + TPVWSimplifier.simplify(edges, cornerArea, 1.0); + Geometry expected = read(wktExpected); + MultiLineString actual = createResult(edges, expected.getFactory()); checkEqual(expected, actual); } + private TPVWSimplifier.Edge[] createEdges(String wkt, int[] freeRingIndex, String wktConstraints, double tolerance) { + List edgeList = new ArrayList(); + addEdges(wkt, freeRingIndex, tolerance, edgeList); + if (wktConstraints != null) { + addEdges(wktConstraints, null, 0.0, edgeList); + } + TPVWSimplifier.Edge[] edges = edgeList.toArray(new TPVWSimplifier.Edge[0]); + return edges; + } + + private void addEdges(String wkt, int[] freeRings, double tolerance, List edges) { + MultiLineString lines = (MultiLineString) read(wkt); + for (int i = 0; i < lines.getNumGeometries(); i++) { + LineString line = (LineString) lines.getGeometryN(i); + boolean isRemovable = false; + boolean isFreeRing = freeRings == null ? false : hasIndex(freeRings, i); + Edge edge = new Edge(line.getCoordinates(), tolerance, isFreeRing, isRemovable); + edges.add(edge); + } + } + + private boolean hasIndex(int[] freeRings, int i) { + for (int fr : freeRings) { + if (fr == i) + return true; + } + return false; + } + + private static MultiLineString createResult(Edge[] edges, GeometryFactory geomFactory) { + LineString[] result = new LineString[edges.length]; + for (int i = 0; i < edges.length; i++) { + Coordinate[] pts = edges[i].getCoordinates(); + result[i] = geomFactory.createLineString(pts); + } + return geomFactory.createMultiLineString(result); + } }