Skip to content

Commit

Permalink
Add per-element tolerances
Browse files Browse the repository at this point in the history
  • Loading branch information
dr-jts committed Jun 19, 2024
1 parent f86e508 commit f684feb
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/
package org.locationtech.jtstest.function;

import java.util.Arrays;
import java.util.List;

import org.locationtech.jts.coverage.CoverageGapFinder;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -157,6 +159,9 @@ public boolean isOuter() {
}

public void setPrimary(boolean isPrimary) {
//-- preserve primary status if set
if (this.isPrimary)
return;
this.isPrimary = isPrimary;
}

Expand Down Expand Up @@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -74,31 +74,14 @@ public List<CoverageEdge> 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<CoverageEdge> selectEdges(int ringCount) {
List<CoverageEdge> result = new ArrayList<CoverageEdge>();
for (CoverageEdge edge : edges) {
if (edge.getRingCount() == ringCount) {
result.add(edge);
}
}
return result;
}

private void build() {
Set<Coordinate> nodes = findMultiRingNodes(coverage);
Set<LineSegment> boundarySegs = CoverageBoundarySegmentFinder.findBoundarySegments(coverage);
nodes.addAll(findBoundaryNodes(boundarySegs));
HashMap<LineSegment, CoverageEdge> uniqueEdgeMap = new HashMap<LineSegment, CoverageEdge>();
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);
Expand All @@ -112,15 +95,15 @@ 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);
//-- skip empty holes. Missing rings are copied in result
if (hole.isEmpty())
continue;
//-- holes are never primary
addRingEdges(hole, false, nodes, boundarySegs, uniqueEdgeMap);
addRingEdges(i, hole, false, nodes, boundarySegs, uniqueEdgeMap);
}
}
}
Expand All @@ -142,10 +125,10 @@ private int findLargestPolygonIndex(Geometry geom) {
return indexLargest;
}

private void addRingEdges(LinearRing ring, boolean isPrimary, Set<Coordinate> nodes, Set<LineSegment> boundarySegs,
private void addRingEdges(int index, LinearRing ring, boolean isPrimary, Set<Coordinate> nodes, Set<LineSegment> boundarySegs,
HashMap<LineSegment, CoverageEdge> uniqueEdgeMap) {
addBoundaryInnerNodes(ring, boundarySegs, nodes);
List<CoverageEdge> ringEdges = extractRingEdges(ring, isPrimary, uniqueEdgeMap, nodes);
List<CoverageEdge> ringEdges = extractRingEdges(index, ring, isPrimary, uniqueEdgeMap, nodes);
if (ringEdges != null)
ringEdgesMap.put(ring, ringEdges);
}
Expand Down Expand Up @@ -176,14 +159,15 @@ private void addBoundaryInnerNodes(LinearRing ring, Set<LineSegment> 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)
* @param uniqueEdgeMap
* @param nodes
* @return null if the ring has too few distinct vertices
*/
private List<CoverageEdge> extractRingEdges(LinearRing ring,
private List<CoverageEdge> extractRingEdges(int index, LinearRing ring,
boolean isPrimary, HashMap<LineSegment, CoverageEdge> uniqueEdgeMap,
Set<Coordinate> nodes) {
// System.out.println(ring);
Expand All @@ -198,20 +182,21 @@ private List<CoverageEdge> 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);
//-- a single-node ring is only retained if specified
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;
Expand All @@ -226,19 +211,18 @@ private List<CoverageEdge> 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<LineSegment, CoverageEdge> uniqueEdgeMap) {
private CoverageEdge createEdge(Coordinate[] ring, int start, int end, int index, boolean isPrimary, HashMap<LineSegment, CoverageEdge> 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) {
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<CoverageEdge> covEdges = covRings.getEdges();
TPVWSimplifier.Edge[] edges = createEdges(covEdges, tolerances);
return simplify(covRings, covEdges, edges);
}

private Edge[] createEdges(List<CoverageEdge> 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<CoverageEdge> covEdges = covRings.getEdges();
TPVWSimplifier.Edge[] edges = createEdges(covEdges, toleranceInner, toleranceOuter);
return simplify(covRings, covEdges, edges);
}

private Geometry[] simplify(CoverageRingEdges covRings, List<CoverageEdge> covEdges, TPVWSimplifier.Edge[] edges) {
CornerArea cornerArea = new CornerArea(smoothWeight);
TPVWSimplifier.simplify(edges, cornerArea, removableSizeFactor);
setCoordinates(covEdges, edges);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))"
Expand All @@ -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<CoverageEdge> edges = covEdges.selectEdges(ringCount);
MultiLineString edgeLines = toArray(edges, geom.getFactory());
Geometry expected = read(wktExpected);
checkEqual(expected, edgeLines);
}

private MultiLineString toArray(List<CoverageEdge> edges, GeometryFactory geomFactory) {
LineString[] lines = new LineString[edges.size()];
for (int i = 0; i < edges.size(); i++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))" )
);
}

//=================================


Expand All @@ -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);
Expand Down

0 comments on commit f684feb

Please sign in to comment.