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 0e847718f2..b355f363c7 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 @@ -19,6 +19,7 @@ import org.locationtech.jts.coverage.CoverageSimplifier; import org.locationtech.jts.coverage.CoverageUnion; import org.locationtech.jts.coverage.CoverageValidator; +import org.locationtech.jts.coverage.CoverageEdgeExtractor; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.util.PolygonExtracter; import org.locationtech.jtstest.geomfunction.Metadata; @@ -53,19 +54,25 @@ public static Geometry findGaps(Geometry geom, return CoverageGapFinder.findGaps(toGeometryArray(geom),gapWidth); } + @Metadata(description="Extract edges from a coverage") + public static Geometry extractEdges(Geometry geom) { + Geometry[] edges = CoverageEdgeExtractor.extract(toGeometryArray(geom)); + return FunctionsUtil.buildGeometryCollection(edges, geom.getFactory().createLineString()); + } + @Metadata(description="Fast Union of a coverage") public static Geometry union(Geometry coverage) { Geometry[] cov = toGeometryArray(coverage); return CoverageUnion.union(cov); } - + @Metadata(description="Simplify a coverage") public static Geometry simplify(Geometry coverage, double tolerance) { Geometry[] cov = toGeometryArray(coverage); Geometry[] result = CoverageSimplifier.simplify(cov, tolerance); return coverage.getFactory().createGeometryCollection(result); } - + @Metadata(description="Simplify a coverage with a smoothness weight") public static Geometry simplifySharp(Geometry coverage, @Metadata(title="Distance tol") diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdgeExtractor.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdgeExtractor.java new file mode 100644 index 0000000000..595a8414f7 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdgeExtractor.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2024 Nick Bowsher. + * + * 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.geom.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * Extracts the set of unique coverage edges from a polygonal coverage. + * The coverage edges are returned as an array of linear geometries. + * The input coverage should be valid according to {@link CoverageValidator}. + * + * @author Nick Bowsher + */ +public class CoverageEdgeExtractor { + /** + * Extracts the set of unique coverage edges from a polygonal coverage. + * + * @param coverage an array of polygons forming a coverage + * @return an array of linear geometries representing coverage edges + */ + public static Geometry[] extract(Geometry[] coverage) { + CoverageEdgeExtractor e = new CoverageEdgeExtractor(coverage); + return e.extract(); + } + + /** + * Creates a new coverage edge extractor + * + * @param coverage an array of polygons forming a coverage + */ + public CoverageEdgeExtractor(Geometry[] coverage) { + this.coverage = coverage; + } + + /** + * Extracts the set of unique coverage edges from a polygonal coverage. + * The result is an array of the same size as the amount of coverage edges. + * + * @return an array of linear geometries representing coverage edges with the {@link CoverageEdgeParentRings} set as user data for each coverage edge line geometry + */ + public Geometry[] extract() { + CoverageRingEdges covRings = new CoverageRingEdges(coverage); + GeometryFactory f = new GeometryFactory(new PrecisionModel(1000)); + List lines = new ArrayList(); + for (CoverageEdge edge : covRings.getEdges()) { + LineString line = edge.toLineString(f); + line.setUserData(parentRings(edge, f)); + lines.add(line); + } + return GeometryFactory.toLineStringArray(lines); + } + + /** + * Retrieves the indices of the adjacent coverage polygons and verifies if they are to the left or right of the edge + * + * @param edge a coverage edge + * @param f a geometry factory + * + * @return the edge parent indices from the coverage + */ + private CoverageEdgeParentRings parentRings(CoverageEdge edge, GeometryFactory f){ + int index0 = edge.getAdjacentIndex(0); + int index1 = edge.getAdjacentIndex(1); + + Coordinate start = edge.getStartCoordinate(); + Coordinate end = edge.getCoordinates()[1]; + Coordinate midPoint = LineSegment.midPoint(start, end); + + double dx = end.x - start.x; + double dy = end.y - start.y; + double gridSize = f.getPrecisionModel().gridSize(); + Coordinate leftPoint = new Coordinate(midPoint.x - dy * gridSize, midPoint.y + dx * gridSize); + + if (coverage[index0].contains(f.createPoint(leftPoint))){ + return new CoverageEdgeParentRings(index0, index1); + } + return new CoverageEdgeParentRings(index1, index0); + } + + private Geometry[] coverage; +} diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdgeParentRings.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdgeParentRings.java new file mode 100644 index 0000000000..8d5148ffa9 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdgeParentRings.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 Nick Bowsher. + * + * 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; + +/** +* Contains the adjacent indices of a polygonal coverage according to their relative position +* when compared to a coverage edge. This data structure is utilized by a {@link CoverageEdgeExtractor } +* to set the user data of each extracted coverage edge line geometry. +* +* @author Nick Bowsher +*/ +public class CoverageEdgeParentRings { + /** + * Construct the pair of Parent ring indices for an edge + * @param leftIndex the index of the coverage polygon that lies left of the coverage edge + * @param rightIndex the index of the coverage polygon that lies right of the coverage edge + */ + public CoverageEdgeParentRings(int leftIndex, int rightIndex){ + this.leftParentIndex = leftIndex; + this.rightParentIndex = rightIndex; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof CoverageEdgeParentRings)) { + return false; + } + CoverageEdgeParentRings casted = (CoverageEdgeParentRings) o; + return this.leftParentIndex == casted.leftParentIndex && this.rightParentIndex == casted.rightParentIndex; + } + + public int leftIndex() { + return leftParentIndex; + } + + public int rightIndex() { + return rightParentIndex; + } + private int leftParentIndex = -1; + private int rightParentIndex = -1; +} diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageEdgeExtractorTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageEdgeExtractorTest.java new file mode 100644 index 0000000000..a6330f1462 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageEdgeExtractorTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2024 Nick Bowsher. + * + * 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 junit.textui.TestRunner; +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 test.jts.GeometryTestCase; + +public class CoverageEdgeExtractorTest extends GeometryTestCase { + public static void main(String args[]) { + TestRunner.run(CoverageEdgeExtractorTest.class); + } + + public CoverageEdgeExtractorTest(String name) { + super(name); + } + + public void testTwoAdjacent() { + checkEdges("GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 5, 9 6, 9 1, 1 1)), POLYGON ((1 9, 6 9, 6 5, 1 6, 1 9)))", + "MULTILINESTRING ((1 6, 6 5), (6 5, 9 6, 9 1, 1 1, 1 6), (1 6, 1 9, 6 9, 6 5))", + new CoverageEdgeParentRings[]{ parentRings(1, 0), parentRings(-1, 0), parentRings(-1, 1) } + ); + } + + public void testTwoAdjacentWithFilledHole() { + checkEdges("GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 5, 9 6, 9 1, 1 1), (2 4, 4 4, 4 2, 2 2, 2 4)), POLYGON ((1 9, 6 9, 6 5, 1 6, 1 9)), POLYGON ((4 2, 2 2, 2 4, 4 4, 4 2)))", + "MULTILINESTRING ((1 6, 6 5), (6 5, 9 6, 9 1, 1 1, 1 6), (2 4, 4 4, 4 2, 2 2, 2 4), (1 6, 1 9, 6 9, 6 5))", + new CoverageEdgeParentRings[]{ parentRings(1, 0), parentRings(-1, 0), parentRings(0, 2), parentRings(-1, 1) } + ); + } + + public void testHolesAndFillWithDifferentEndpoints() { + checkEdges("GEOMETRYCOLLECTION (POLYGON ((0 10, 10 10, 10 0, 0 0, 0 10), (1 9, 4 8, 9 9, 9 1, 1 1, 1 9)), POLYGON ((9 9, 1 1, 1 9, 4 8, 9 9)), POLYGON ((1 1, 9 9, 9 1, 1 1)))", + "MULTILINESTRING ((0 10, 10 10, 10 0, 0 0, 0 10), (9 9, 9 1, 1 1), (1 1, 1 9, 4 8, 9 9), (9 9, 1 1))", + new CoverageEdgeParentRings[]{ parentRings(-1, 0), parentRings(0, 2), parentRings(0, 1), parentRings(2, 1)} + ); + } + + 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 ((5 9, 2.5 7.5, 1 5), (1 5, 5 5), (5 5, 5 9), (5 5, 9 5), (9 5, 7.5 2.5, 5 1), (5 1, 5 5), (5 9, 6.5 6.5, 9 5), (5 1, 3.5 3.5, 1 5)))", + new CoverageEdgeParentRings[]{ parentRings(0, -1), parentRings(0, 1), parentRings(0, 1), parentRings(1, 0), + parentRings(-1, 0), parentRings(1, 0), parentRings(-1, 1), parentRings(-1, 1) } + ); + } + + private void checkEdges(String wkt, String wktExpected, CoverageEdgeParentRings[] expectedParentRings) { + Geometry geom = read(wkt); + Geometry[] coverage = toArray(geom); + Geometry[] edges = CoverageEdgeExtractor.extract(coverage); + MultiLineString edgeLines = toArray(edges, geom.getFactory()); + Geometry expected = read(wktExpected); + assertEquals(expected.getNumGeometries(), expectedParentRings.length); + assertEquals(expectedParentRings.length, edges.length); + for (int i = 0; i < edges.length; i++){ + assertEquals(expectedParentRings[i], edges[i].getUserData()); + } + + checkEqual(expected, edgeLines); + } + + private MultiLineString toArray(Geometry[] edges, GeometryFactory geomFactory) { + LineString[] lines = new LineString[edges.length]; + for (int i = 0; i < edges.length; i++) { + lines[i] = geomFactory.createLineString(edges[i].getCoordinates()); + } + return geomFactory.createMultiLineString(lines); + } + + private static Geometry[] toArray(Geometry geom) { + Geometry[] geoms = new Geometry[geom.getNumGeometries()]; + for (int i = 0; i < geom.getNumGeometries(); i++) { + geoms[i] = geom.getGeometryN(i); + } + return geoms; + } + + private static CoverageEdgeParentRings parentRings(int leftIndex, int rightIndex){ + return new CoverageEdgeParentRings(leftIndex,rightIndex); + } +}