From f9a7dfc20d2c9eba5d5ace4103f3ff9c1a5561e6 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 20 Dec 2021 10:05:05 -0800 Subject: [PATCH 01/21] Initial commit Signed-off-by: Martin Davis --- .../function/ConstructionFunctions.java | 16 + .../org/locationtech/jts/geom/Triangle.java | 25 +- .../locationtech/jts/hull/ConcaveHull.java | 352 ++++++++++++++++++ .../locationtech/jts/triangulate/tri/Tri.java | 37 ++ .../jts/hull/ConcaveHullTest.java | 48 +++ 5 files changed, 477 insertions(+), 1 deletion(-) create mode 100644 modules/core/src/main/java/org/locationtech/jts/hull/ConcaveHull.java create mode 100644 modules/core/src/test/java/org/locationtech/jts/hull/ConcaveHullTest.java diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java index 13df15dc05..76ac2a02bc 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java @@ -21,6 +21,7 @@ import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.OctagonalEnvelope; +import org.locationtech.jts.hull.ConcaveHull; import org.locationtech.jtstest.geomfunction.Metadata; public class ConstructionFunctions { @@ -130,4 +131,19 @@ public static Geometry circleByRadiusLine(Geometry radiusLine, return radiusLine.getFactory().createPolygon(circlePts); } + public static Geometry concaveHullByLen(Geometry geom, + @Metadata(title="Max edge length") + double maxLen) { + return ConcaveHull.concaveHullByLength(geom, maxLen); + } + + public static Geometry concaveHullByArea(Geometry geom, + @Metadata(title="Max area ratio") + double minAreaPct) { + return ConcaveHull.concaveHullByArea(geom, minAreaPct); + } + + public static double concaveHullLenGuess(Geometry geom) { + return ConcaveHull.uniformEdgeLength(geom); + } } diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/Triangle.java b/modules/core/src/main/java/org/locationtech/jts/geom/Triangle.java index 8cdaa1d16f..8188a166b7 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geom/Triangle.java +++ b/modules/core/src/main/java/org/locationtech/jts/geom/Triangle.java @@ -290,6 +290,19 @@ public static Coordinate centroid(Coordinate a, Coordinate b, Coordinate c) return new Coordinate(x, y); } + /** + * Compute the length of the perimeter of a triangle + * + * @param a a vertex of the triangle + * @param b a vertex of the triangle + * @param c a vertex of the triangle + * @return the length of the triangle perimeter + */ + public static double length(Coordinate a, Coordinate b, Coordinate c) + { + return a.distance(b) + b.distance(c) + c.distance(a); + } + /** * Computes the length of the longest side of a triangle * @@ -430,7 +443,7 @@ public static double area3D(Coordinate a, Coordinate b, Coordinate c) return area3D; } - + /** * Computes the Z-value (elevation) of an XY point on a three-dimensional * plane defined by a triangle whose vertices have Z-values. The defining @@ -563,6 +576,16 @@ public Coordinate centroid() return centroid(p0, p1, p2); } + /** + * Computes the length of the perimeter of this triangle. + * + * @return the length of the perimeter + */ + public double length() + { + return length(p0, p1, p2); + } + /** * Computes the length of the longest side of this triangle * diff --git a/modules/core/src/main/java/org/locationtech/jts/hull/ConcaveHull.java b/modules/core/src/main/java/org/locationtech/jts/hull/ConcaveHull.java new file mode 100644 index 0000000000..63d4a98449 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/hull/ConcaveHull.java @@ -0,0 +1,352 @@ +/* + * Copyright (c) 2021 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.hull; + +import java.util.ArrayList; +import java.util.List; +import java.util.PriorityQueue; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.operation.overlayng.CoverageUnion; +import org.locationtech.jts.triangulate.DelaunayTriangulationBuilder; +import org.locationtech.jts.triangulate.quadedge.QuadEdge; +import org.locationtech.jts.triangulate.quadedge.QuadEdgeSubdivision; +import org.locationtech.jts.triangulate.quadedge.TriangleVisitor; +import org.locationtech.jts.triangulate.tri.Tri; +import org.locationtech.jts.triangulate.tri.TriangulationBuilder; + +/** + * Constructs a concave hull of a set of points. + * The hull is constructed by eroding the Delaunay Triangulation of the points + * until specified target criteria are reached. + * The target criteria are: + * + * Usually only a single criteria will be specified, or both may be provided. + * In addition, the computed hull is always a single connected Polygon with no holes. + * This may cause the target criteria to not hold in the result hull. + * + * @author mdavis + * + */ +public class ConcaveHull +{ + /** + * Computes the approximate edge length of + * a uniform square grid having the same number of + * points as a geometry and the same area as its convex hull. + * This value can be used to determine a suitable length threshold value + * for computing a concave hull. + * A value from 2 to 4 times the uniform grid length + * seems to produce reasonable results. + * + * @param geom a geometry + * @return the approximate uniform grid length + */ + public static double uniformEdgeLength(Geometry geom) { + double areaCH = geom.convexHull().getArea(); + int numPts = geom.getNumPoints(); + return Math.sqrt(areaCH / numPts); + } + + public static Geometry concaveHullByLength(Geometry geom, double maxLength) { + ConcaveHull hull = new ConcaveHull(geom); + hull.setMaximumEdgeLength(maxLength); + return hull.getHull(); + } + + public static Geometry concaveHullByArea(Geometry geom, double areaRatio) { + ConcaveHull hull = new ConcaveHull(geom); + hull.setMaximumAreaRatio(areaRatio); + return hull.getHull(); + } + + private Geometry inputGeometry; + private double maxEdgeLength = 0.0; + private double maxAreaRatio = 0.0; + private GeometryFactory geomFactory; + + + public ConcaveHull(Geometry geom) { + this.inputGeometry = geom; + this.geomFactory = geom.getFactory(); + } + + /** + * Sets the target maximum edge length for the concave hull. + * A value of 0.0 produces a concave hull of minimum area + * that is still connected. + * The {@link #uniformEdgeLength(Geometry)} may be used as + * the basis for estimating an appropriate target maximum edge length. + * + * @param edgeLength a non-negative length + * + * @see #uniformEdgeLength(Geometry) + */ + public void setMaximumEdgeLength(double edgeLength) { + if (edgeLength < 0) + throw new IllegalArgumentException("Edge length must be non-negative"); + this.maxEdgeLength = edgeLength; + } + + /** + * Sets the target maximum concave hull area as a ratio of the convex hull area. + * A value of 1.0 produces the convex hull + * (unless a maximum edge length is also specified). + * A value of 0.0 produces a concave hull with the smallest area + * that is still connected. + * + * @param areaRatio a ratio value between 0 and 1 + */ + public void setMaximumAreaRatio(double areaRatio) { + if (areaRatio < 0 || areaRatio > 1) + throw new IllegalArgumentException("Area ratio must be in range [0,1]"); + this.maxAreaRatio = areaRatio; + } + + /** + * Gets the computed concave hull. + * + * @return the concave hull + */ + public Geometry getHull() { + List triList = createDelaunayTriangulation(inputGeometry); + computeHull(triList); + Geometry hull = toPolygon(triList, geomFactory); + return hull; + } + + private void computeHull(List triList) { + //-- used if area is the threshold criteria + double areaConvex = area(triList); + double areaConcave = areaConvex; + + PriorityQueue queue = new PriorityQueue(); + for (Tri tri : triList) { + if (isBorder(tri)) + addTri(tri, queue); + } + // erode non-connecting tris in order of decreasing size + while (! queue.isEmpty()) { + if (isBelowAreaThreshold(areaConcave, areaConvex)) + break; + + OrderedTri candidate = queue.poll(); + Tri tri = candidate.getTri(); + + if (isBelowLengthThreshold(tri)) + break; + + if (isRemovable(tri)) { + //-- the non-null adjacents are now on the border + Tri adj0 = tri.getAdjacent(0); + Tri adj1 = tri.getAdjacent(1); + Tri adj2 = tri.getAdjacent(2); + //-- remove tri to ensure adjacents are on border when added + tri.remove(); + triList.remove(tri); + areaConcave -= tri.getArea(); + + addTri(adj0, queue); + addTri(adj1, queue); + addTri(adj2, queue); + } + } + } + + private boolean isBelowAreaThreshold(double areaConcave, double areaConvex) { + return areaConcave / areaConvex <= maxAreaRatio; + } + + private boolean isBelowLengthThreshold(Tri tri) { + return lengthOfBorder(tri) < maxEdgeLength; + } + + private static double area(List triList) { + double area = 0; + for (Tri tri : triList) { + area += tri.getArea(); + } + return area; + } + + /** + * Adds a Tri to the queue. + * Only add tris with a single border edge. + * The ordering size is the length of the border edge. + * + * @param tri the Tri to add + * @param queue the priority queue + */ + private void addTri(Tri tri, PriorityQueue queue) { + if (tri == null) return; + if (tri.numAdjacent() != 2) return; + queue.add(new OrderedTri(tri, lengthOfBorder(tri))); + } + + /** + * Tests whether a Tri can be removed while preserving + * the connectivity of the hull. + * + * @param tri the Tri to test + * @return true if the Tri can be removed + */ + private static boolean isRemovable(Tri tri) { + int numAdj = tri.numAdjacent(); + /** + * Tri must have exactly 2 adjacent tris. + * If it it has only 0 or 1 adjacent then removal would remove a vertex. + * If it has 3 adjacent then it is not on border. + */ + if (numAdj != 2) return false; + /** + * The tri cannot be removed if it is connecting, because + * this would create more than one result polygon. + */ + return ! isConnecting(tri); + } + + private static boolean isConnecting(Tri tri) { + int adj2Index = adjacent2VertexIndex(tri); + boolean isInterior = isInteriorVertex(tri, adj2Index); + return ! isInterior; + } + + /** + * A vertex of a triangle is interior if it + * is fully surrounded by triangles. + * + * @param tri a tri containing the vertex + * @param index the vertex index + * @return true if the vertex is interior + */ + private static boolean isInteriorVertex(Tri triStart, int index) { + Tri curr = triStart; + int currIndex = index; + do { + Tri adj = curr.getAdjacent(currIndex); + if (adj == null) return false; + int adjIndex = adj.getIndex(curr); + curr = adj; + currIndex = Tri.next(adjIndex); + } + while (curr != triStart); + return true; + } + + private static int adjacent2VertexIndex(Tri tri) { + if (tri.hasAdjacent(0) && tri.hasAdjacent(1)) return 1; + if (tri.hasAdjacent(1) && tri.hasAdjacent(2)) return 2; + if (tri.hasAdjacent(2) && tri.hasAdjacent(0)) return 0; + return -1; + } + + private static boolean isBorder(Tri tri) { + return tri.getAdjacent(0) == null + || tri.getAdjacent(1) == null + || tri.getAdjacent(2) == null; + } + + private static double lengthOfBorder(Tri tri) { + double len = 0.0; + for (int i = 0; i < 3; i++) { + if (! tri.hasAdjacent(i)) { + len += tri.getCoordinate(i).distance(tri.getCoordinate(Tri.next(i))); + } + } + return len; + } + + private static class OrderedTri implements Comparable { + private Tri tri; + private double size; + + public OrderedTri(Tri tri, double size) { + this.tri = tri; + this.size = size; + } + + public double getSize() { + return size; + } + + public Tri getTri() { + return tri; + } + + /** + * To sort the PriorityQueue with larger sizes at the head, + * smaller sizes must compare as greater than larger sizes. + * (i.e. the normal numeric comparison is reversed) + */ + @Override + public int compareTo(OrderedTri o) { + return -Double.compare(size, o.size); + } + } + + private List createDelaunayTriangulation(Geometry geom) { + //TODO: implement a DT on Tris directly + DelaunayTriangulationBuilder dt = new DelaunayTriangulationBuilder(); + dt.setSites(geom); + QuadEdgeSubdivision subdiv = dt.getSubdivision(); + List triList = toTris(subdiv); + return triList; + } + + private static Geometry toPolygon(List triList, GeometryFactory geomFactory) { + //TODO: make this more efficient by tracing boundary + List polys = new ArrayList(); + for (Tri tri : triList) { + Polygon poly = tri.toPolygon(geomFactory); + polys.add(poly); + } + return CoverageUnion.union(geomFactory.buildGeometry(polys)); + } + + private static List toTris(QuadEdgeSubdivision subdiv) { + TriVisitor visitor = new TriVisitor(); + subdiv.visitTriangles(visitor, false); + List triList = visitor.getTriangles(); + TriangulationBuilder.build(triList); + return triList; + } + + private static class TriVisitor implements TriangleVisitor { + private List triList = new ArrayList(); + + public TriVisitor() { + } + + public void visit(QuadEdge[] triEdges) { + Coordinate p0 = triEdges[0].orig().getCoordinate(); + Coordinate p1 = triEdges[1].orig().getCoordinate(); + Coordinate p2 = triEdges[2].orig().getCoordinate(); + //TODO: check for valid triangles only? + Tri tri = new Tri(p0, p1, p2); + triList.add(tri); + } + + public List getTriangles() { + return triList; + } + } + + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java b/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java index 337905e815..72a65040b3 100755 --- a/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java +++ b/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java @@ -19,6 +19,7 @@ import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.Triangle; import org.locationtech.jts.io.WKTWriter; import org.locationtech.jts.util.Assert; @@ -246,6 +247,24 @@ private void replace(Tri triOld, Tri triNew) { } } + /** + * Removes this triangle from a triangulation. + * All adjacent references and the references to this + * Tri in the adjacent Tris are set to null Date: Mon, 20 Dec 2021 13:50:36 -0800 Subject: [PATCH 02/21] Javadoc Signed-off-by: Martin Davis --- .../org/locationtech/jts/hull/ConcaveHull.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/modules/core/src/main/java/org/locationtech/jts/hull/ConcaveHull.java b/modules/core/src/main/java/org/locationtech/jts/hull/ConcaveHull.java index 63d4a98449..ec0170dcf4 100644 --- a/modules/core/src/main/java/org/locationtech/jts/hull/ConcaveHull.java +++ b/modules/core/src/main/java/org/locationtech/jts/hull/ConcaveHull.java @@ -65,12 +65,28 @@ public static double uniformEdgeLength(Geometry geom) { return Math.sqrt(areaCH / numPts); } + /** + * Computes the concave hull of the vertices in a geometry + * using the target criteria of maximum edge length. + * + * @param geom the input geometry + * @param maxLength the target maximum edge length + * @return the concave hull + */ public static Geometry concaveHullByLength(Geometry geom, double maxLength) { ConcaveHull hull = new ConcaveHull(geom); hull.setMaximumEdgeLength(maxLength); return hull.getHull(); } + /** + * Computes the concave hull of the vertices in a geometry + * using the target criteria of maximum area ratio. + * + * @param geom the input geometry + * @param areaRatio the target maximum area ratio + * @return the concave hull + */ public static Geometry concaveHullByArea(Geometry geom, double areaRatio) { ConcaveHull hull = new ConcaveHull(geom); hull.setMaximumAreaRatio(areaRatio); From 37c12da9755f942659dccf1e409b34fbd6f0f809 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 20 Dec 2021 13:52:27 -0800 Subject: [PATCH 03/21] Move to algorithm/hull Signed-off-by: Martin Davis --- .../locationtech/jtstest/function/ConstructionFunctions.java | 2 +- .../org/locationtech/jts/{ => algorithm}/hull/ConcaveHull.java | 2 +- .../locationtech/jts/{ => algorithm}/hull/ConcaveHullTest.java | 2 +- .../test/java/org/locationtech/jts/hull/ConcaveHullTest.java | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) rename modules/core/src/main/java/org/locationtech/jts/{ => algorithm}/hull/ConcaveHull.java (99%) rename modules/core/src/test/java/org/locationtech/jts/{ => algorithm}/hull/ConcaveHullTest.java (97%) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java index 76ac2a02bc..c2de973a1f 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java @@ -16,12 +16,12 @@ import org.locationtech.jts.algorithm.MinimumDiameter; import org.locationtech.jts.algorithm.construct.LargestEmptyCircle; import org.locationtech.jts.algorithm.construct.MaximumInscribedCircle; +import org.locationtech.jts.algorithm.hull.ConcaveHull; import org.locationtech.jts.densify.Densifier; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.OctagonalEnvelope; -import org.locationtech.jts.hull.ConcaveHull; import org.locationtech.jtstest.geomfunction.Metadata; public class ConstructionFunctions { diff --git a/modules/core/src/main/java/org/locationtech/jts/hull/ConcaveHull.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java similarity index 99% rename from modules/core/src/main/java/org/locationtech/jts/hull/ConcaveHull.java rename to modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java index ec0170dcf4..e925aea02b 100644 --- a/modules/core/src/main/java/org/locationtech/jts/hull/ConcaveHull.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java @@ -9,7 +9,7 @@ * * http://www.eclipse.org/org/documents/edl-v10.php. */ -package org.locationtech.jts.hull; +package org.locationtech.jts.algorithm.hull; import java.util.ArrayList; import java.util.List; diff --git a/modules/core/src/test/java/org/locationtech/jts/hull/ConcaveHullTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java similarity index 97% rename from modules/core/src/test/java/org/locationtech/jts/hull/ConcaveHullTest.java rename to modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java index 52fcd20bb4..42cbfb6006 100644 --- a/modules/core/src/test/java/org/locationtech/jts/hull/ConcaveHullTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java @@ -9,7 +9,7 @@ * * http://www.eclipse.org/org/documents/edl-v10.php. */ -package org.locationtech.jts.hull; +package org.locationtech.jts.algorithm.hull; import org.locationtech.jts.geom.Geometry; diff --git a/modules/lab/src/test/java/org/locationtech/jts/hull/ConcaveHullTest.java b/modules/lab/src/test/java/org/locationtech/jts/hull/ConcaveHullTest.java index 3c8adfbb0e..b5bf756720 100644 --- a/modules/lab/src/test/java/org/locationtech/jts/hull/ConcaveHullTest.java +++ b/modules/lab/src/test/java/org/locationtech/jts/hull/ConcaveHullTest.java @@ -13,6 +13,8 @@ import junit.textui.TestRunner; +import org.locationtech.jts.algorithm.hull.ConcaveHull; +import org.locationtech.jts.algorithm.hull.ConcaveHullTest; import org.locationtech.jts.geom.Geometry; import test.jts.GeometryTestCase; From 051e6a1cca930c8b98f9c0e663e573dcd164e34f Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 20 Dec 2021 13:56:35 -0800 Subject: [PATCH 04/21] Add Area tests Signed-off-by: Martin Davis --- .../jts/algorithm/hull/ConcaveHullTest.java | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java index 42cbfb6006..950b1d7f80 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java @@ -29,20 +29,51 @@ public void testLengthSimple() { 70, "POLYGON ((30 70, 70 70, 90 10, 50 60, 10 10, 30 70))" ); } - public void testLengthZeroConnected() { + public void testLengthZero() { checkHullByLength("MULTIPOINT ((10 10), (90 10), (70 70), (50 60), (50 90), (40 70), (30 30))", 0, "POLYGON ((10 10, 40 70, 50 90, 70 70, 90 10, 50 60, 30 30, 10 10))" ); } + public void testLengthConvex() { + checkHullByLength("MULTIPOINT ((10 10), (90 10), (70 70), (50 60), (50 90), (40 70), (30 30))", + 100, "POLYGON ((10 10, 40 70, 50 90, 70 70, 90 10, 10 10))" ); + } + public void testLengthCShape() { checkHullByLength("MULTIPOINT ((70 80), (80 90), (90 70), (50 80), (30 70), (20 40), (30 20), (50 10), (90 20), (40 50), (40 30), (41 67))", 50, "POLYGON ((30 70, 50 80, 80 90, 90 70, 70 80, 40 50, 40 30, 90 20, 50 10, 30 20, 20 40, 30 70))" ); } + //------------------------------------------------ + + public void testAreaSimple() { + checkHullByArea("MULTIPOINT ((10 10), (90 10), (30 70), (70 70), (50 60))", + .5, "POLYGON ((30 70, 70 70, 90 10, 50 60, 10 10, 30 70))" ); + } + + public void testAreaZero() { + checkHullByArea("MULTIPOINT ((10 10), (90 10), (70 70), (50 60), (50 90), (40 70), (30 30))", + 0, "POLYGON ((10 10, 40 70, 50 90, 70 70, 90 10, 50 60, 30 30, 10 10))" ); + } + + public void testAreaConvex() { + checkHullByArea("MULTIPOINT ((10 10), (90 10), (70 70), (50 60), (50 90), (40 70), (30 30))", + 1, "POLYGON ((10 10, 40 70, 50 90, 70 70, 90 10, 10 10))" ); + } + + //========================================================================== + private void checkHullByLength(String wkt, double threshold, String wktExpected) { Geometry geom = read(wkt); Geometry actual = ConcaveHull.concaveHullByLength(geom, threshold); Geometry expected = read(wktExpected); checkEqual(expected, actual); } + + private void checkHullByArea(String wkt, double threshold, String wktExpected) { + Geometry geom = read(wkt); + Geometry actual = ConcaveHull.concaveHullByArea(geom, threshold); + Geometry expected = read(wktExpected); + checkEqual(expected, actual); + } } From 2f3202e3376e75975871550c3b7ede0b8bdc929c Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 20 Dec 2021 14:02:18 -0800 Subject: [PATCH 05/21] Remove obsolete Lab code Signed-off-by: Martin Davis --- .../locationtech/jts/hull/ConcaveHull.java | 53 ------------------- .../jts/hull/ConcaveHullTest.java | 47 ---------------- 2 files changed, 100 deletions(-) delete mode 100644 modules/lab/src/main/java/org/locationtech/jts/hull/ConcaveHull.java delete mode 100644 modules/lab/src/test/java/org/locationtech/jts/hull/ConcaveHullTest.java diff --git a/modules/lab/src/main/java/org/locationtech/jts/hull/ConcaveHull.java b/modules/lab/src/main/java/org/locationtech/jts/hull/ConcaveHull.java deleted file mode 100644 index da239803b6..0000000000 --- a/modules/lab/src/main/java/org/locationtech/jts/hull/ConcaveHull.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) 2016 Vivid Solutions. - * - * 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.hull; - -import java.util.List; - -import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.triangulate.DelaunayTriangulationBuilder; -import org.locationtech.jts.triangulate.quadedge.QuadEdgeSubdivision; -import org.locationtech.jts.triangulate.quadedge.QuadEdgeTriangle; - -public class ConcaveHull { - - private Geometry geom; - private double tolerance; - - public ConcaveHull(Geometry geom, double tolerance) { - this.geom = geom; - this.tolerance = tolerance; - } - - public Geometry getResult() { - QuadEdgeSubdivision subdiv = buildDelaunay(); - List tris = extractTriangles(subdiv); - Geometry hull = computeHull(tris); - return hull; - } - - private List extractTriangles(QuadEdgeSubdivision subdiv) { - List qeTris = QuadEdgeTriangle.createOn(subdiv); - return qeTris; - } - - private Geometry computeHull(List tris) { - return null; - - } - - private QuadEdgeSubdivision buildDelaunay() { - DelaunayTriangulationBuilder builder = new DelaunayTriangulationBuilder(); - builder.setSites(geom); - return builder.getSubdivision(); - } -} diff --git a/modules/lab/src/test/java/org/locationtech/jts/hull/ConcaveHullTest.java b/modules/lab/src/test/java/org/locationtech/jts/hull/ConcaveHullTest.java deleted file mode 100644 index b5bf756720..0000000000 --- a/modules/lab/src/test/java/org/locationtech/jts/hull/ConcaveHullTest.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2016 Vivid Solutions. - * - * 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.hull; - -import junit.textui.TestRunner; - -import org.locationtech.jts.algorithm.hull.ConcaveHull; -import org.locationtech.jts.algorithm.hull.ConcaveHullTest; -import org.locationtech.jts.geom.Geometry; - -import test.jts.GeometryTestCase; - -public class ConcaveHullTest extends GeometryTestCase { - - public static void main(String args[]) { - TestRunner.run(ConcaveHullTest.class); - } - - public ConcaveHullTest(String name) { - super(name); - } - - public void testSimple() { - checkHull( - "POLYGON ((100 200, 200 180, 300 200, 200 190, 100 200))", - 150, - "POLYGON ((100 200, 200 180, 300 200, 200 190, 100 200))" - ); - } - - private void checkHull(String inputWKT, double tolerance, String expectedWKT) { - Geometry input = read(inputWKT); - Geometry expected = read(expectedWKT); - ConcaveHull hull = new ConcaveHull(input, tolerance); - Geometry actual = hull.getResult(); - //checkEqual(expected, actual); - } -} From 1112869ae3713a756bfe8739ad1655232549ff6f Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 20 Dec 2021 15:41:35 -0800 Subject: [PATCH 06/21] Switch to using Tri subclass Signed-off-by: Martin Davis --- .../jts/algorithm/hull/ConcaveHull.java | 90 +++++++++---------- .../locationtech/jts/triangulate/tri/Tri.java | 14 +++ .../triangulate/tri/TriangulationBuilder.java | 4 +- 3 files changed, 60 insertions(+), 48 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java index e925aea02b..fa2e411d74 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java @@ -142,19 +142,19 @@ public void setMaximumAreaRatio(double areaRatio) { * @return the concave hull */ public Geometry getHull() { - List triList = createDelaunayTriangulation(inputGeometry); + List triList = createDelaunayTriangulation(inputGeometry); computeHull(triList); - Geometry hull = toPolygon(triList, geomFactory); + Geometry hull = toPolygonal(triList, geomFactory); return hull; } - private void computeHull(List triList) { + private void computeHull(List triList) { //-- used if area is the threshold criteria - double areaConvex = area(triList); + double areaConvex = Tri.area(triList); double areaConcave = areaConvex; - PriorityQueue queue = new PriorityQueue(); - for (Tri tri : triList) { + PriorityQueue queue = new PriorityQueue(); + for (HullTri tri : triList) { if (isBorder(tri)) addTri(tri, queue); } @@ -163,17 +163,16 @@ private void computeHull(List triList) { if (isBelowAreaThreshold(areaConcave, areaConvex)) break; - OrderedTri candidate = queue.poll(); - Tri tri = candidate.getTri(); + HullTri tri = queue.poll(); if (isBelowLengthThreshold(tri)) break; if (isRemovable(tri)) { //-- the non-null adjacents are now on the border - Tri adj0 = tri.getAdjacent(0); - Tri adj1 = tri.getAdjacent(1); - Tri adj2 = tri.getAdjacent(2); + HullTri adj0 = (HullTri) tri.getAdjacent(0); + HullTri adj1 = (HullTri) tri.getAdjacent(1); + HullTri adj2 = (HullTri) tri.getAdjacent(2); //-- remove tri to ensure adjacents are on border when added tri.remove(); triList.remove(tri); @@ -194,14 +193,6 @@ private boolean isBelowLengthThreshold(Tri tri) { return lengthOfBorder(tri) < maxEdgeLength; } - private static double area(List triList) { - double area = 0; - for (Tri tri : triList) { - area += tri.getArea(); - } - return area; - } - /** * Adds a Tri to the queue. * Only add tris with a single border edge. @@ -210,10 +201,10 @@ private static double area(List triList) { * @param tri the Tri to add * @param queue the priority queue */ - private void addTri(Tri tri, PriorityQueue queue) { + private void addTri(HullTri tri, PriorityQueue queue) { if (tri == null) return; if (tri.numAdjacent() != 2) return; - queue.add(new OrderedTri(tri, lengthOfBorder(tri))); + queue.add(tri); } /** @@ -289,45 +280,53 @@ private static double lengthOfBorder(Tri tri) { return len; } - private static class OrderedTri implements Comparable { - private Tri tri; + private static class HullTri extends Tri + implements Comparable + { private double size; - public OrderedTri(Tri tri, double size) { - this.tri = tri; - this.size = size; + public HullTri(Coordinate p0, Coordinate p1, Coordinate p2) { + super(p0, p1, p2); + this.size = lengthOfBorder(this); } public double getSize() { return size; } - public Tri getTri() { - return tri; - } - /** - * To sort the PriorityQueue with larger sizes at the head, + * PriorityQueues sort in ascending order. + * To sort with the largest at the head, * smaller sizes must compare as greater than larger sizes. * (i.e. the normal numeric comparison is reversed) */ @Override - public int compareTo(OrderedTri o) { + public int compareTo(HullTri o) { return -Double.compare(size, o.size); } + + private static double lengthOfBorder(Tri tri) { + double len = 0.0; + for (int i = 0; i < 3; i++) { + if (! tri.hasAdjacent(i)) { + len += tri.getCoordinate(i).distance(tri.getCoordinate(Tri.next(i))); + } + } + return len; + } } - private List createDelaunayTriangulation(Geometry geom) { - //TODO: implement a DT on Tris directly + private static List createDelaunayTriangulation(Geometry geom) { + //TODO: implement a DT on Tris directly? DelaunayTriangulationBuilder dt = new DelaunayTriangulationBuilder(); dt.setSites(geom); QuadEdgeSubdivision subdiv = dt.getSubdivision(); - List triList = toTris(subdiv); + List triList = toTris(subdiv); return triList; } - private static Geometry toPolygon(List triList, GeometryFactory geomFactory) { - //TODO: make this more efficient by tracing boundary + private static Geometry toPolygonal(List triList, GeometryFactory geomFactory) { + //TODO: make this more efficient by tracing border List polys = new ArrayList(); for (Tri tri : triList) { Polygon poly = tri.toPolygon(geomFactory); @@ -336,30 +335,29 @@ private static Geometry toPolygon(List triList, GeometryFactory geomFactory return CoverageUnion.union(geomFactory.buildGeometry(polys)); } - private static List toTris(QuadEdgeSubdivision subdiv) { - TriVisitor visitor = new TriVisitor(); + private static List toTris(QuadEdgeSubdivision subdiv) { + HullTriVisitor visitor = new HullTriVisitor(); subdiv.visitTriangles(visitor, false); - List triList = visitor.getTriangles(); + List triList = visitor.getTriangles(); TriangulationBuilder.build(triList); return triList; } - private static class TriVisitor implements TriangleVisitor { - private List triList = new ArrayList(); + private static class HullTriVisitor implements TriangleVisitor { + private List triList = new ArrayList(); - public TriVisitor() { + public HullTriVisitor() { } public void visit(QuadEdge[] triEdges) { Coordinate p0 = triEdges[0].orig().getCoordinate(); Coordinate p1 = triEdges[1].orig().getCoordinate(); Coordinate p2 = triEdges[2].orig().getCoordinate(); - //TODO: check for valid triangles only? - Tri tri = new Tri(p0, p1, p2); + HullTri tri = new HullTri(p0, p1, p2); triList.add(tri); } - public List getTriangles() { + public List getTriangles() { return triList; } } diff --git a/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java b/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java index 72a65040b3..0aa2866a8a 100755 --- a/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java +++ b/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java @@ -50,6 +50,20 @@ public static Geometry toGeometry(List triList, GeometryFactory geomFact) { return geomFact.createGeometryCollection(geoms); } + /** + * Computes the area of a set of Tris. + * + * @param triList a set of Tris + * @return the total area of the triangles + */ + public static double area(List triList) { + double area = 0; + for (Tri tri : triList) { + area += tri.getArea(); + } + return area; + } + /** * Validates a list of Tris. * diff --git a/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/TriangulationBuilder.java b/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/TriangulationBuilder.java index a4828fc9d4..5a9fd381fb 100755 --- a/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/TriangulationBuilder.java +++ b/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/TriangulationBuilder.java @@ -30,7 +30,7 @@ public class TriangulationBuilder { * * @param triList the list of Tris */ - public static void build(List triList) { + public static void build(List triList) { new TriangulationBuilder(triList); } @@ -41,7 +41,7 @@ public static void build(List triList) { * * @param triList the list of Tris */ - private TriangulationBuilder(List triList) { + private TriangulationBuilder(List triList) { triMap = new HashMap(); for (Tri tri : triList) { add(tri); From 3f66f118df6b13677f3927c83d3ea5b90e36d6d4 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 21 Dec 2021 10:34:24 -0800 Subject: [PATCH 07/21] Add support for holes Signed-off-by: Martin Davis --- .../function/ConstructionFunctions.java | 6 + .../jts/algorithm/hull/ConcaveHull.java | 234 ++++++++++++++---- .../locationtech/jts/triangulate/tri/Tri.java | 12 +- .../jts/algorithm/hull/ConcaveHullTest.java | 19 ++ 4 files changed, 220 insertions(+), 51 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java index c2de973a1f..97808ff306 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java @@ -137,6 +137,12 @@ public static Geometry concaveHullByLen(Geometry geom, return ConcaveHull.concaveHullByLength(geom, maxLen); } + public static Geometry concaveHullByLenWithHoles(Geometry geom, + @Metadata(title="Max edge length") + double maxLen) { + return ConcaveHull.concaveHullByLength(geom, maxLen, true); + } + public static Geometry concaveHullByArea(Geometry geom, @Metadata(title="Max area ratio") double minAreaPct) { diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java index fa2e411d74..068b0593a1 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java @@ -11,7 +11,9 @@ */ package org.locationtech.jts.algorithm.hull; +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Deque; import java.util.List; import java.util.PriorityQueue; @@ -19,6 +21,7 @@ import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.Triangle; import org.locationtech.jts.operation.overlayng.CoverageUnion; import org.locationtech.jts.triangulate.DelaunayTriangulationBuilder; import org.locationtech.jts.triangulate.quadedge.QuadEdge; @@ -38,9 +41,14 @@ *
  • Maximum Area Ratio - the ratio of the concave hull area to the convex hull area * will be no larger than this value * - * Usually only a single criteria will be specified, or both may be provided. - * In addition, the computed hull is always a single connected Polygon with no holes. - * This may cause the target criteria to not hold in the result hull. + * Usually only a single criteria is specified, but both may be provided. + *

    + * The computed hull is always a single connected Polygon. + * This constraint may cause the concave hull to not fully meet the target criteria. + *

    + * Optionally the concave hull can be allowed to contain holes. + * Note that this may be substantially slower than not permitting holes, + * and it can produce results of low quality. * * @author mdavis * @@ -74,8 +82,13 @@ public static double uniformEdgeLength(Geometry geom) { * @return the concave hull */ public static Geometry concaveHullByLength(Geometry geom, double maxLength) { + return concaveHullByLength(geom, maxLength, false); + } + + public static Geometry concaveHullByLength(Geometry geom, double maxLength, boolean isHolesAllowed) { ConcaveHull hull = new ConcaveHull(geom); hull.setMaximumEdgeLength(maxLength); + hull.setHolesAllowed(isHolesAllowed); return hull.getHull(); } @@ -96,6 +109,7 @@ public static Geometry concaveHullByArea(Geometry geom, double areaRatio) { private Geometry inputGeometry; private double maxEdgeLength = 0.0; private double maxAreaRatio = 0.0; + private boolean isHolesAllowed = false; private GeometryFactory geomFactory; @@ -136,6 +150,15 @@ public void setMaximumAreaRatio(double areaRatio) { this.maxAreaRatio = areaRatio; } + /** + * Sets whether holes are allowed in the concave hull polygon. + * + * @param isHolesAllowed true if holes are allowed in the result + */ + public void setHolesAllowed(boolean isHolesAllowed) { + this.isHolesAllowed = isHolesAllowed; + } + /** * Gets the computed concave hull. * @@ -153,12 +176,8 @@ private void computeHull(List triList) { double areaConvex = Tri.area(triList); double areaConcave = areaConvex; - PriorityQueue queue = new PriorityQueue(); - for (HullTri tri : triList) { - if (isBorder(tri)) - addTri(tri, queue); - } - // erode non-connecting tris in order of decreasing size + PriorityQueue queue = initQueue(triList); + // remove tris in order of decreasing size (edge length) while (! queue.isEmpty()) { if (isBelowAreaThreshold(areaConcave, areaConvex)) break; @@ -168,29 +187,39 @@ private void computeHull(List triList) { if (isBelowLengthThreshold(tri)) break; - if (isRemovable(tri)) { + if (isRemovable(tri, triList)) { //-- the non-null adjacents are now on the border HullTri adj0 = (HullTri) tri.getAdjacent(0); HullTri adj1 = (HullTri) tri.getAdjacent(1); HullTri adj2 = (HullTri) tri.getAdjacent(2); - //-- remove tri to ensure adjacents are on border when added + + //-- remove tri tri.remove(); triList.remove(tri); areaConcave -= tri.getArea(); - addTri(adj0, queue); - addTri(adj1, queue); - addTri(adj2, queue); + //-- if holes not allowed, add new border adjacents to queue + if (! isHolesAllowed) { + addBorderTri(adj0, queue); + addBorderTri(adj1, queue); + addBorderTri(adj2, queue); + } } } } - private boolean isBelowAreaThreshold(double areaConcave, double areaConvex) { - return areaConcave / areaConvex <= maxAreaRatio; - } - - private boolean isBelowLengthThreshold(Tri tri) { - return lengthOfBorder(tri) < maxEdgeLength; + private PriorityQueue initQueue(List triList) { + PriorityQueue queue = new PriorityQueue(); + for (HullTri tri : triList) { + if (! isHolesAllowed) { + //-- add only border triangles which could be eroded + // (if tri has only 1 adjacent it can't be removed because that would isolate a vertex) + if (tri.numAdjacent() != 2) + continue; + } + queue.add(tri); + } + return queue; } /** @@ -201,20 +230,46 @@ private boolean isBelowLengthThreshold(Tri tri) { * @param tri the Tri to add * @param queue the priority queue */ - private void addTri(HullTri tri, PriorityQueue queue) { + private void addBorderTri(HullTri tri, PriorityQueue queue) { if (tri == null) return; if (tri.numAdjacent() != 2) return; queue.add(tri); } + + private boolean isBelowAreaThreshold(double areaConcave, double areaConvex) { + return areaConcave / areaConvex <= maxAreaRatio; + } + + private boolean isBelowLengthThreshold(HullTri tri) { + double len = 0; + if (isHolesAllowed) { + len = tri.lengthOfLongestEdge(); + } + else { + len = tri.lengthOfBorder(); + } + return len < maxEdgeLength; + } /** * Tests whether a Tri can be removed while preserving * the connectivity of the hull. * * @param tri the Tri to test + * @param triList * @return true if the Tri can be removed */ - private static boolean isRemovable(Tri tri) { + private boolean isRemovable(HullTri tri, List triList) { + if (isHolesAllowed) { + /** + * Don't remove if that would separate a single vertex + */ + if (hasVertexSingleAdjacent(tri, triList)) + return false; + return HullTri.isConnected(triList, tri); + } + + //-- compute removable for no holes allowed int numAdj = tri.numAdjacent(); /** * Tri must have exactly 2 adjacent tris. @@ -229,6 +284,42 @@ private static boolean isRemovable(Tri tri) { return ! isConnecting(tri); } + private static boolean hasVertexSingleAdjacent(HullTri tri, List triList) { + for (int i = 0; i < 3; i++) { + if (degree(tri.getCoordinate(i), triList) <= 1) + return true; + } + return false; + } + + /** + * The degree of a Tri vertex is the number of tris containing it. + * This must be done by searching the entire subdivision, + * since the containing tris may not be adjacent or edge-connected. + * + * @param v the vertex coordinate + * @param triList the tri subdivision + * @return the degree of the vertex + */ + private static int degree(Coordinate v, List triList) { + int degree = 0; + for (HullTri tri : triList) { + for (int i = 0; i < 3; i++) { + if (v.equals2D(tri.getCoordinate(i))) + degree++; + } + } + return degree; + } + + /** + * Tests if a tri is the only one connecting its 2 adjacents. + * Assumes that the tri is on the border of the tri subdivision + * and that the subdivision does not contain holes + * + * @param tri the tri to test + * @return true if the tri is the only connection + */ private static boolean isConnecting(Tri tri) { int adj2Index = adjacent2VertexIndex(tri); boolean isInterior = isInteriorVertex(tri, adj2Index); @@ -263,37 +354,51 @@ private static int adjacent2VertexIndex(Tri tri) { if (tri.hasAdjacent(2) && tri.hasAdjacent(0)) return 0; return -1; } - - private static boolean isBorder(Tri tri) { - return tri.getAdjacent(0) == null - || tri.getAdjacent(1) == null - || tri.getAdjacent(2) == null; - } - - private static double lengthOfBorder(Tri tri) { - double len = 0.0; - for (int i = 0; i < 3; i++) { - if (! tri.hasAdjacent(i)) { - len += tri.getCoordinate(i).distance(tri.getCoordinate(Tri.next(i))); - } - } - return len; - } private static class HullTri extends Tri implements Comparable { private double size; + private boolean isMarked = false; public HullTri(Coordinate p0, Coordinate p1, Coordinate p2) { super(p0, p1, p2); - this.size = lengthOfBorder(this); + //this.size = lengthOfBorder(this); + this.size = Triangle.longestSideLength(p0, p1, p2); } public double getSize() { return size; } + + public boolean isMarked() { + return isMarked; + } + + public void setMarked(boolean isMarked) { + this.isMarked = isMarked; + } + public boolean isBorder() { + return getAdjacent(0) == null + || getAdjacent(1) == null + || getAdjacent(2) == null; + } + + public double lengthOfLongestEdge() { + return Triangle.longestSideLength(p0, p1, p2); + } + + private double lengthOfBorder() { + double len = 0.0; + for (int i = 0; i < 3; i++) { + if (! hasAdjacent(i)) { + len += getCoordinate(i).distance(getCoordinate(Tri.next(i))); + } + } + return len; + } + /** * PriorityQueues sort in ascending order. * To sort with the largest at the head, @@ -305,14 +410,53 @@ public int compareTo(HullTri o) { return -Double.compare(size, o.size); } - private static double lengthOfBorder(Tri tri) { - double len = 0.0; - for (int i = 0; i < 3; i++) { - if (! tri.hasAdjacent(i)) { - len += tri.getCoordinate(i).distance(tri.getCoordinate(Tri.next(i))); + public static boolean isConnected(List triList, HullTri exceptTri) { + if (triList.size() == 0) return false; + clearMarks(triList); + HullTri triStart = findTri(triList, exceptTri); + if (triStart == null) return false; + markConnected(triStart, exceptTri); + exceptTri.setMarked(true); + return isAllMarked(triList); + } + + public static void clearMarks(List triList) { + for (HullTri tri : triList) { + tri.setMarked(false); + } + } + + public static HullTri findTri(List triList, Tri exceptTri) { + for (HullTri tri : triList) { + if (tri != exceptTri) return tri; + } + return null; + } + + public static boolean isAllMarked(List triList) { + for (HullTri tri : triList) { + if (! tri.isMarked()) + return false; + } + return true; + } + + public static void markConnected(HullTri triStart, Tri exceptTri) { + Deque queue = new ArrayDeque(); + queue.add(triStart); + while (! queue.isEmpty()) { + HullTri tri = queue.pop(); + tri.setMarked(true); + for (int i = 0; i < 3; i++) { + HullTri adj = (HullTri) tri.getAdjacent(i); + //-- don't connect thru this tri + if (adj == exceptTri) + continue; + if (adj != null && ! adj.isMarked() ) { + queue.add(adj); + } } } - return len; } } diff --git a/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java b/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java index 0aa2866a8a..e2af1e2d12 100755 --- a/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java +++ b/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java @@ -99,17 +99,17 @@ public static Tri create(Coordinate[] pts) { return new Tri(pts[0], pts[1], pts[2]); } - private Coordinate p0; - private Coordinate p1; - private Coordinate p2; + protected Coordinate p0; + protected Coordinate p1; + protected Coordinate p2; /** * triN is the adjacent triangle across the edge pN - pNN. * pNN is the next vertex CW from pN. */ - private Tri tri0; - private Tri tri1; - private Tri tri2; + protected Tri tri0; + protected Tri tri1; + protected Tri tri2; /** * Creates a triangle with the given vertices. diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java index 950b1d7f80..35d8474d77 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java @@ -46,6 +46,18 @@ public void testLengthCShape() { //------------------------------------------------ + public void testLengthHolesCircle() { + checkHullWithHolesByLength("MULTIPOINT ((90 20), (80 10), (45 5), (10 20), (20 10), (21 30), (40 20), (11 60), (20 70), (20 90), (40 80), (70 80), (80 60), (90 70), (80 90), (56 95), (95 45), (80 40), (70 20), (15 45), (5 40), (40 96), (60 15))", + 40, "POLYGON ((20 90, 40 96, 56 95, 80 90, 90 70, 95 45, 90 20, 80 10, 45 5, 20 10, 10 20, 5 40, 11 60, 20 90), (20 70, 15 45, 40 20, 70 20, 80 40, 80 60, 70 80, 40 80, 20 70))" ); + } + + public void testLengthHolesCircle0() { + checkHullWithHolesByLength("MULTIPOINT ((90 20), (80 10), (45 5), (10 20), (20 10), (21 30), (40 20), (11 60), (20 70), (20 90), (40 80), (70 80), (80 60), (90 70), (80 90), (56 95), (95 45), (80 40), (70 20), (15 45), (5 40), (40 96), (60 15))", + 0, "POLYGON ((20 90, 40 96, 56 95, 80 90, 90 70, 95 45, 90 20, 80 10, 60 15, 45 5, 20 10, 10 20, 5 40, 11 60, 15 45, 21 30, 40 20, 70 20, 80 40, 80 60, 70 80, 40 80, 20 70, 20 90))" ); + } + + //------------------------------------------------ + public void testAreaSimple() { checkHullByArea("MULTIPOINT ((10 10), (90 10), (30 70), (70 70), (50 60))", .5, "POLYGON ((30 70, 70 70, 90 10, 50 60, 10 10, 30 70))" ); @@ -70,6 +82,13 @@ private void checkHullByLength(String wkt, double threshold, String wktExpected) checkEqual(expected, actual); } + private void checkHullWithHolesByLength(String wkt, double threshold, String wktExpected) { + Geometry geom = read(wkt); + Geometry actual = ConcaveHull.concaveHullByLength(geom, threshold, true); + Geometry expected = read(wktExpected); + checkEqual(expected, actual); + } + private void checkHullByArea(String wkt, double threshold, String wktExpected) { Geometry geom = read(wkt); Geometry actual = ConcaveHull.concaveHullByArea(geom, threshold); From 4ed542d9288d5609af9be3c3e8ba9b5253b0d71e Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 21 Dec 2021 14:04:11 -0800 Subject: [PATCH 08/21] Fix issue with HullTri size for border tris Signed-off-by: Martin Davis --- .../jts/algorithm/hull/ConcaveHull.java | 14 ++++++++++++-- .../jts/algorithm/hull/ConcaveHullTest.java | 5 +++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java index 068b0593a1..8c3a25355f 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java @@ -216,6 +216,7 @@ private PriorityQueue initQueue(List triList) { // (if tri has only 1 adjacent it can't be removed because that would isolate a vertex) if (tri.numAdjacent() != 2) continue; + tri.setSizeToBorder(); } queue.add(tri); } @@ -233,6 +234,7 @@ private PriorityQueue initQueue(List triList) { private void addBorderTri(HullTri tri, PriorityQueue queue) { if (tri == null) return; if (tri.numAdjacent() != 2) return; + tri.setSizeToBorder(); queue.add(tri); } @@ -363,14 +365,22 @@ private static class HullTri extends Tri public HullTri(Coordinate p0, Coordinate p1, Coordinate p2) { super(p0, p1, p2); - //this.size = lengthOfBorder(this); - this.size = Triangle.longestSideLength(p0, p1, p2); + this.size = lengthOfLongestEdge(); } public double getSize() { return size; } + /** + * Sets the size to be the length of the border edges. + * This is used when constructing hull without holes, + * by erosion from the subdivision border. + */ + public void setSizeToBorder() { + size = lengthOfBorder(); + } + public boolean isMarked() { return isMarked; } diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java index 35d8474d77..c84a322901 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java @@ -44,6 +44,11 @@ public void testLengthCShape() { 50, "POLYGON ((30 70, 50 80, 80 90, 90 70, 70 80, 40 50, 40 30, 90 20, 50 10, 30 20, 20 40, 30 70))" ); } + public void testLengthSShape() { + checkHullByLength("MULTIPOINT ((0 81), (65 86), (70 71), (80 59), (92 49), (107 44), (122 41), (137 40), (152 41), (167 42), (182 47), (195 55), (203 68), (201 83), (188 92), (173 97), (158 100), (143 103), (128 106), (113 109), (98 112), (83 115), (68 120), (53 125), (40 133), (28 143), (18 155), (13 170), (12 185), (16 200), (26 213), (38 223), (51 231), (66 236), (81 240), (96 243), (111 245), (126 245), (141 245), (156 245), (171 244), (186 241), (201 238), (216 233), (229 225), (242 216), (252 204), (259 190), (262 175), (194 171), (189 186), (178 197), (164 203), (149 205), (134 206), (119 205), (104 203), (89 198), (77 188), (80 173), (93 165), (108 160), (123 157), (138 154), (153 151), (168 149), (183 146), (198 142), (213 138), (227 132), (241 126), (253 116), (263 104), (269 90), (271 75), (270 60), (264 46), (254 34), (243 23), (229 16), (215 10), (200 6), (185 3), (170 1), (155 0), (139 0), (123 0), (108 1), (93 3), (78 5), (63 10), (49 16), (35 23), (23 33), (13 45), (6 59), (16 82), (32 83), (48 84), (245 174), (228 173), (211 172), (131 128), (63 148), (222 207), (127 230), (154 131), (240 82), (72 220), (210 32), (90 22), (206 208), (57 202), (195 117), (55 166), (246 55), (201 101), (224 73), (211 192), (42 176), (152 228), (172 113), (24 61), (76 33), (92 216), (46 69), (118 138), (169 23), (213 118), (221 56), (44 192), (118 22), (224 40), (56 57), (192 32), (179 220), (34 44), (145 18), (239 194), (40 155), (92 136), (231 106), (40 207), (108 228), (256 81), (28 185), (54 33), (74 205), (172 132), (221 93), (249 96), (69 47), (78 146), (155 115), (202 223))", + 20, "POLYGON ((16 200, 26 213, 38 223, 51 231, 66 236, 81 240, 96 243, 111 245, 126 245, 141 245, 156 245, 171 244, 186 241, 201 238, 216 233, 229 225, 242 216, 252 204, 259 190, 262 175, 245 174, 228 173, 211 172, 194 171, 189 186, 178 197, 164 203, 149 205, 134 206, 119 205, 104 203, 89 198, 77 188, 80 173, 93 165, 108 160, 123 157, 138 154, 153 151, 168 149, 183 146, 198 142, 213 138, 227 132, 241 126, 253 116, 263 104, 269 90, 271 75, 270 60, 264 46, 254 34, 243 23, 229 16, 215 10, 200 6, 185 3, 170 1, 155 0, 139 0, 123 0, 108 1, 93 3, 78 5, 63 10, 49 16, 35 23, 23 33, 13 45, 6 59, 0 81, 16 82, 32 83, 48 84, 65 86, 70 71, 80 59, 92 49, 107 44, 122 41, 137 40, 152 41, 167 42, 182 47, 195 55, 203 68, 201 83, 188 92, 173 97, 158 100, 143 103, 128 106, 113 109, 98 112, 83 115, 68 120, 53 125, 40 133, 28 143, 18 155, 13 170, 12 185, 16 200))" ); + } + //------------------------------------------------ public void testLengthHolesCircle() { From a988adb3b8f49a4a1bf90930b1f599517be91c1c Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 22 Dec 2021 10:45:45 -0800 Subject: [PATCH 09/21] Add border tracing to extract result Signed-off-by: Martin Davis --- .../jts/algorithm/hull/ConcaveHull.java | 159 +++++++++++++++--- .../jts/algorithm/hull/ConcaveHullTest.java | 5 + 2 files changed, 143 insertions(+), 21 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java index 8c3a25355f..95decf1033 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java @@ -18,6 +18,7 @@ import java.util.PriorityQueue; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateList; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Polygon; @@ -29,6 +30,7 @@ import org.locationtech.jts.triangulate.quadedge.TriangleVisitor; import org.locationtech.jts.triangulate.tri.Tri; import org.locationtech.jts.triangulate.tri.TriangulationBuilder; +import org.locationtech.jts.util.Assert; /** * Constructs a concave hull of a set of points. @@ -167,7 +169,7 @@ public void setHolesAllowed(boolean isHolesAllowed) { public Geometry getHull() { List triList = createDelaunayTriangulation(inputGeometry); computeHull(triList); - Geometry hull = toPolygonal(triList, geomFactory); + Geometry hull = toPolygon(triList, geomFactory); return hull; } @@ -296,11 +298,11 @@ private static boolean hasVertexSingleAdjacent(HullTri tri, List triLis /** * The degree of a Tri vertex is the number of tris containing it. - * This must be done by searching the entire subdivision, + * This must be done by searching the entire triangulation, * since the containing tris may not be adjacent or edge-connected. * - * @param v the vertex coordinate - * @param triList the tri subdivision + * @param v a vertex coordinate + * @param triList a triangulation * @return the degree of the vertex */ private static int degree(Coordinate v, List triList) { @@ -316,8 +318,8 @@ private static int degree(Coordinate v, List triList) { /** * Tests if a tri is the only one connecting its 2 adjacents. - * Assumes that the tri is on the border of the tri subdivision - * and that the subdivision does not contain holes + * Assumes that the tri is on the border of the triangulation + * and that the triangulation does not contain holes * * @param tri the tri to test * @return true if the tri is the only connection @@ -375,7 +377,7 @@ public double getSize() { /** * Sets the size to be the length of the border edges. * This is used when constructing hull without holes, - * by erosion from the subdivision border. + * by erosion from the triangulation border. */ public void setSizeToBorder() { size = lengthOfBorder(); @@ -390,9 +392,48 @@ public void setMarked(boolean isMarked) { } public boolean isBorder() { - return getAdjacent(0) == null - || getAdjacent(1) == null - || getAdjacent(2) == null; + return isBorder(0) || isBorder(1) || isBorder(2); + } + + public boolean isBorder(int index) { + return ! hasAdjacent(index); + } + + public int borderIndex() { + if (isBorder(0)) return 0; + if (isBorder(1)) return 1; + if (isBorder(2)) return 2; + return -1; + } + + /** + * Gets the most CCW border edge index. + * This assumes there is at least one non-border edge. + * + * @return the CCW border edge index + */ + public int borderIndexCCW() { + int index = borderIndex(); + int prevIndex = prev(index); + if (isBorder(prevIndex)) { + return prevIndex; + } + return index; + } + + /** + * Gets the most CW border edge index. + * This assumes there is at least one non-border edge. + * + * @return the CW border edge index + */ + public int borderIndexCW() { + int index = borderIndex(); + int nextIndex = next(index); + if (isBorder(nextIndex)) { + return nextIndex; + } + return index; } public double lengthOfLongestEdge() { @@ -409,6 +450,22 @@ private double lengthOfBorder() { return len; } + public HullTri nextBorderTri() { + HullTri tri = this; + //-- start at first non-border edge CW + int index = next(borderIndexCW()); + //-- scan CCW around vertex for next border tri + do { + HullTri adjTri = (HullTri) tri.getAdjacent(index); + if (adjTri == this) + throw new IllegalStateException("No outgoing border edge found"); + index = next(adjTri.getIndex(tri)); + tri = adjTri; + } + while (! tri.isBorder(index)); + return (tri); + } + /** * PriorityQueues sort in ascending order. * To sort with the largest at the head, @@ -468,6 +525,7 @@ public static void markConnected(HullTri triStart, Tri exceptTri) { } } } + } private static List createDelaunayTriangulation(Geometry geom) { @@ -478,16 +536,6 @@ private static List createDelaunayTriangulation(Geometry geom) { List triList = toTris(subdiv); return triList; } - - private static Geometry toPolygonal(List triList, GeometryFactory geomFactory) { - //TODO: make this more efficient by tracing border - List polys = new ArrayList(); - for (Tri tri : triList) { - Polygon poly = tri.toPolygon(geomFactory); - polys.add(poly); - } - return CoverageUnion.union(geomFactory.buildGeometry(polys)); - } private static List toTris(QuadEdgeSubdivision subdiv) { HullTriVisitor visitor = new HullTriVisitor(); @@ -507,7 +555,13 @@ public void visit(QuadEdge[] triEdges) { Coordinate p0 = triEdges[0].orig().getCoordinate(); Coordinate p1 = triEdges[1].orig().getCoordinate(); Coordinate p2 = triEdges[2].orig().getCoordinate(); - HullTri tri = new HullTri(p0, p1, p2); + HullTri tri; + if (Triangle.isCCW(p0, p1, p2)) { + tri = new HullTri(p0, p2, p1); + } + else { + tri = new HullTri(p0, p1, p2); + } triList.add(tri); } @@ -516,5 +570,68 @@ public List getTriangles() { } } + private Geometry toPolygon(List triList, GeometryFactory geomFactory) { + if (! isHolesAllowed) { + return extractPolygon(triList, geomFactory); + } + //-- in case holes are present use union (slower but handles holes) + return union(triList, geomFactory); + } + + private Geometry extractPolygon(List triList, GeometryFactory geomFactory) { + if (triList.size() == 1) { + Tri tri = triList.get(0); + return tri.toPolygon(geomFactory); + } + Coordinate[] pts = traceBorder(triList); + return geomFactory.createPolygon(pts); + } + private static Geometry union(List triList, GeometryFactory geomFactory) { + List polys = new ArrayList(); + for (Tri tri : triList) { + Polygon poly = tri.toPolygon(geomFactory); + polys.add(poly); + } + return CoverageUnion.union(geomFactory.buildGeometry(polys)); + } + + /** + * Extracts the coordinates along the border of a triangulation, + * by tracing CW around the border triangles. + * Assumption: there are at least 2 tris, they are connected, + * and there are no holes. + * So each tri has at least one non-border edge, and there is only one border. + * + * @param triList the triangulation + * @return the border of the triangulation + */ + private static Coordinate[] traceBorder(List triList) { + HullTri triStart = findBorderTri(triList); + CoordinateList coordList = new CoordinateList(); + HullTri tri = triStart; + do { + int borderIndex = tri.borderIndexCCW(); + //-- add border vertex + coordList.add(tri.getCoordinate(borderIndex), false); + int nextIndex = Tri.next(borderIndex); + //-- if next edge is also border, add it and move to next + if (tri.isBorder(nextIndex)) { + coordList.add(tri.getCoordinate(nextIndex), false); + borderIndex = nextIndex; + } + //-- find next border tri CCW around non-border edge + tri = tri.nextBorderTri(); + } while (tri != triStart); + coordList.closeRing(); + return coordList.toCoordinateArray(); + } + + public static HullTri findBorderTri(List triList) { + for (HullTri tri : triList) { + if (tri.isBorder()) return tri; + } + Assert.shouldNeverReachHere("No border triangles found"); + return null; + } } diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java index c84a322901..52beb5dd6e 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java @@ -24,6 +24,11 @@ public static void main(String args[]) { public ConcaveHullTest(String name) { super(name); } + public void testLength3() { + checkHullByLength("MULTIPOINT ((10 10), (90 10), (30 70))", + 70, "POLYGON ((10 10, 30 70, 90 10, 10 10))" ); + } + public void testLengthSimple() { checkHullByLength("MULTIPOINT ((10 10), (90 10), (30 70), (70 70), (50 60))", 70, "POLYGON ((30 70, 70 70, 90 10, 50 60, 10 10, 30 70))" ); From 6de2ca545edd0a5c1a673fb685a2f3bfc5573ce6 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 22 Dec 2021 15:40:37 -0800 Subject: [PATCH 10/21] Add edgeLengthFactor target Handle degenerate cases Signed-off-by: Martin Davis --- .../function/ConstructionFunctions.java | 8 +- .../jts/algorithm/hull/ConcaveHull.java | 101 ++++++++++++++++-- .../jts/algorithm/hull/ConcaveHullTest.java | 43 +++++++- 3 files changed, 139 insertions(+), 13 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java index 97808ff306..e78db2a5b0 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java @@ -137,6 +137,12 @@ public static Geometry concaveHullByLen(Geometry geom, return ConcaveHull.concaveHullByLength(geom, maxLen); } + public static Geometry concaveHullByLenRatio(Geometry geom, + @Metadata(title="Max edge length ratio") + double maxLen) { + return ConcaveHull.concaveHullByLengthFactor(geom, maxLen); + } + public static Geometry concaveHullByLenWithHoles(Geometry geom, @Metadata(title="Max edge length") double maxLen) { @@ -150,6 +156,6 @@ public static Geometry concaveHullByArea(Geometry geom, } public static double concaveHullLenGuess(Geometry geom) { - return ConcaveHull.uniformEdgeLength(geom); + return ConcaveHull.uniformGridEdgeLength(geom); } } diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java index 95decf1033..a0bddedfaa 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java @@ -39,20 +39,24 @@ * The target criteria are: *

      *
    • Maximum Edge Length - the length of the longest edge of the hull will be no larger - * than this value + * than this value. + *
    • Maximum Edge Length Factor - this determines the Maximum Edge Length + * as a fraction of the length difference between the longest and shortest edges in the Delaunay Triangulation. + * It is a way of normalizing the Maximum Edge Length to make it scale-independent. *
    • Maximum Area Ratio - the ratio of the concave hull area to the convex hull area - * will be no larger than this value + * will be no larger than this value. *
    * Usually only a single criteria is specified, but both may be provided. *

    - * The computed hull is always a single connected Polygon. - * This constraint may cause the concave hull to not fully meet the target criteria. + * The computed hull is always a single connected {@link Polygon} + * (unless it is degenerate, in which case it will be a {@link Point} or a {@link LineString}). + * This constraint may cause the concave hull to fail to meet the target criteria. *

    * Optionally the concave hull can be allowed to contain holes. - * Note that this may be substantially slower than not permitting holes, + * Note that this may result in substantially slower computation, * and it can produce results of low quality. * - * @author mdavis + * @author Martin Davis * */ public class ConcaveHull @@ -69,7 +73,7 @@ public class ConcaveHull * @param geom a geometry * @return the approximate uniform grid length */ - public static double uniformEdgeLength(Geometry geom) { + public static double uniformGridEdgeLength(Geometry geom) { double areaCH = geom.convexHull().getArea(); int numPts = geom.getNumPoints(); return Math.sqrt(areaCH / numPts); @@ -87,6 +91,33 @@ public static Geometry concaveHullByLength(Geometry geom, double maxLength) { return concaveHullByLength(geom, maxLength, false); } + /** + * Computes the concave hull of the vertices in a geometry + * using the target criteria of maximum edge length factor. + * The edge length factor is a fraction of the length delta + * between the longest and shortest edges + * in the Delaunay Triangulation of the input points. + * + * @param geom the input geometry + * @param lengthFactor the target edge length factor + * @return the concave hull + */ + public static Geometry concaveHullByLengthFactor(Geometry geom, double lengthFactor) { + ConcaveHull hull = new ConcaveHull(geom); + hull.setMaximumEdgeLengthFactor(lengthFactor); + return hull.getHull(); + } + + /** + * Computes the concave hull of the vertices in a geometry + * using the target criteria of maximum edge length, + * and optionally allowing holes. + * + * @param geom the input geometry + * @param maxLength the target maximum edge length + * @param isHolesAllowed whether holes are allowed in the result + * @return the concave hull + */ public static Geometry concaveHullByLength(Geometry geom, double maxLength, boolean isHolesAllowed) { ConcaveHull hull = new ConcaveHull(geom); hull.setMaximumEdgeLength(maxLength); @@ -110,11 +141,17 @@ public static Geometry concaveHullByArea(Geometry geom, double areaRatio) { private Geometry inputGeometry; private double maxEdgeLength = 0.0; + private double maxEdgeLengthFactor = -1; private double maxAreaRatio = 0.0; private boolean isHolesAllowed = false; private GeometryFactory geomFactory; + /** + * Creates a new instance for a given geometry. + * + * @param geom the input geometry + */ public ConcaveHull(Geometry geom) { this.inputGeometry = geom; this.geomFactory = geom.getFactory(); @@ -124,17 +161,35 @@ public ConcaveHull(Geometry geom) { * Sets the target maximum edge length for the concave hull. * A value of 0.0 produces a concave hull of minimum area * that is still connected. - * The {@link #uniformEdgeLength(Geometry)} may be used as + * The {@link #uniformGridEdgeLength(Geometry)} may be used as * the basis for estimating an appropriate target maximum edge length. * * @param edgeLength a non-negative length * - * @see #uniformEdgeLength(Geometry) + * @see #uniformGridEdgeLength(Geometry) */ public void setMaximumEdgeLength(double edgeLength) { if (edgeLength < 0) throw new IllegalArgumentException("Edge length must be non-negative"); this.maxEdgeLength = edgeLength; + maxEdgeLengthFactor = -1; + } + + /** + * Sets the target maximum edge length factor for the concave hull. + * The edge length factor is a fraction of the length delta + * between the longest and shortest edges + * in the Delaunay Triangulation of the input points. + * A value of 1.0 produces the convex hull. + * A value of 0.0 produces a concave hull of minimum area + * that is still connected. + * + * @param edgeLengthFactor a length factor value between 0 and 1 + */ + public void setMaximumEdgeLengthFactor(double edgeLengthFactor) { + if (edgeLengthFactor < 0 || edgeLengthFactor > 1) + throw new IllegalArgumentException("Edge length ratio must be in range [0,1]e"); + this.maxEdgeLengthFactor = edgeLengthFactor; } /** @@ -167,12 +222,39 @@ public void setHolesAllowed(boolean isHolesAllowed) { * @return the concave hull */ public Geometry getHull() { + if (inputGeometry.isEmpty()) { + return geomFactory.createPolygon(); + } List triList = createDelaunayTriangulation(inputGeometry); + if (maxEdgeLengthFactor >= 0) { + maxEdgeLength = computeTargetEdgeLength(triList, maxEdgeLengthFactor); + } + if (triList.isEmpty()) + return inputGeometry.convexHull(); computeHull(triList); Geometry hull = toPolygon(triList, geomFactory); return hull; } + private static double computeTargetEdgeLength(List triList, + double edgeLengthFactor) { + if (edgeLengthFactor == 0) return 0; + double maxEdgeLen = -1; + double minEdgeLen = -1; + for (Tri tri : triList) { + for (int i = 0; i < 3; i++) { + double len = tri.getCoordinate(i).distance(tri.getCoordinate(Tri.next(i))); + if (len > maxEdgeLen) + maxEdgeLen = len; + if (minEdgeLen < 0 || len < minEdgeLen) + minEdgeLen = len; + } + } + //-- ensure all edges are included + if (edgeLengthFactor == 1) return 2 * maxEdgeLen; + return edgeLengthFactor * (maxEdgeLen - minEdgeLen) + minEdgeLen; + } + private void computeHull(List triList) { //-- used if area is the threshold criteria double areaConvex = Tri.area(triList); @@ -525,7 +607,6 @@ public static void markConnected(HullTri triStart, Tri exceptTri) { } } } - } private static List createDelaunayTriangulation(Geometry geom) { diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java index 52beb5dd6e..1d1695c2c1 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java @@ -24,12 +24,27 @@ public static void main(String args[]) { public ConcaveHullTest(String name) { super(name); } - public void testLength3() { + public void testLengthEmpty() { + checkHullByLength("MULTIPOINT EMPTY", + 70, "POLYGON EMPTY" ); + } + + public void testLengthPoint() { + checkHullByLength("MULTIPOINT ((10 10), (10 10))", + 70, "POINT (10 10)" ); + } + + public void testLengthCollinear() { + checkHullByLength("LINESTRING (10 10, 20 20, 30 30))", + 70, "LINESTRING (10 10, 30 30)" ); + } + + public void testLengthTriangle() { checkHullByLength("MULTIPOINT ((10 10), (90 10), (30 70))", 70, "POLYGON ((10 10, 30 70, 90 10, 10 10))" ); } - public void testLengthSimple() { + public void testLengthChevron() { checkHullByLength("MULTIPOINT ((10 10), (90 10), (30 70), (70 70), (50 60))", 70, "POLYGON ((30 70, 70 70, 90 10, 50 60, 10 10, 30 70))" ); } @@ -54,6 +69,23 @@ public void testLengthSShape() { 20, "POLYGON ((16 200, 26 213, 38 223, 51 231, 66 236, 81 240, 96 243, 111 245, 126 245, 141 245, 156 245, 171 244, 186 241, 201 238, 216 233, 229 225, 242 216, 252 204, 259 190, 262 175, 245 174, 228 173, 211 172, 194 171, 189 186, 178 197, 164 203, 149 205, 134 206, 119 205, 104 203, 89 198, 77 188, 80 173, 93 165, 108 160, 123 157, 138 154, 153 151, 168 149, 183 146, 198 142, 213 138, 227 132, 241 126, 253 116, 263 104, 269 90, 271 75, 270 60, 264 46, 254 34, 243 23, 229 16, 215 10, 200 6, 185 3, 170 1, 155 0, 139 0, 123 0, 108 1, 93 3, 78 5, 63 10, 49 16, 35 23, 23 33, 13 45, 6 59, 0 81, 16 82, 32 83, 48 84, 65 86, 70 71, 80 59, 92 49, 107 44, 122 41, 137 40, 152 41, 167 42, 182 47, 195 55, 203 68, 201 83, 188 92, 173 97, 158 100, 143 103, 128 106, 113 109, 98 112, 83 115, 68 120, 53 125, 40 133, 28 143, 18 155, 13 170, 12 185, 16 200))" ); } + //------------------------------------------------ + + public void testLengthFactorZero() { + checkHullByLengthFactor("MULTIPOINT ((10 90), (10 10), (90 10), (90 90), (40 40), (60 30), (30 70), (40 60), (60 50), (60 72), (47 66), (90 60))", + 0, "POLYGON ((30 70, 10 90, 60 72, 90 90, 90 60, 90 10, 60 30, 10 10, 40 40, 60 50, 47 66, 40 60, 30 70))" ); + } + + public void testLengthFactorP5() { + checkHullByLengthFactor("MULTIPOINT ((10 90), (10 10), (90 10), (90 90), (40 40), (60 30), (30 70), (40 60), (60 50), (60 72), (47 66), (90 60))", + 0.5, "POLYGON ((30 70, 10 90, 60 72, 90 90, 90 60, 90 10, 60 30, 10 10, 40 40, 30 70))" ); + } + + public void testLengthFactorOne() { + checkHullByLengthFactor("MULTIPOINT ((10 90), (10 10), (90 10), (90 90), (40 40), (60 30), (30 70), (40 60), (60 50), (60 72), (47 66), (90 60))", + 1, "POLYGON ((10 10, 10 90, 90 90, 90 60, 90 10, 10 10))" ); + } + //------------------------------------------------ public void testLengthHolesCircle() { @@ -85,6 +117,13 @@ public void testAreaConvex() { //========================================================================== + private void checkHullByLengthFactor(String wkt, double threshold, String wktExpected) { + Geometry geom = read(wkt); + Geometry actual = ConcaveHull.concaveHullByLengthFactor(geom, threshold); + Geometry expected = read(wktExpected); + checkEqual(expected, actual); + } + private void checkHullByLength(String wkt, double threshold, String wktExpected) { Geometry geom = read(wkt); Geometry actual = ConcaveHull.concaveHullByLength(geom, threshold); From 9d013d23e3a0ecd62fe330e63d37eca4a3a0869f Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 22 Dec 2021 16:59:52 -0800 Subject: [PATCH 11/21] Add TestBuilder concavity function Signed-off-by: Martin Davis --- .../jtstest/function/ConstructionFunctions.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java index e78db2a5b0..7e6844336a 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java @@ -158,4 +158,17 @@ public static Geometry concaveHullByArea(Geometry geom, public static double concaveHullLenGuess(Geometry geom) { return ConcaveHull.uniformGridEdgeLength(geom); } + + /** + * A concavity measure defined in terms of perimeter length. + * As defined by Park & Oh, 2012 + * + * @param geom + * @return + */ + public static double concavity(Geometry geom) { + double convexLen = geom.convexHull().getLength(); + return (geom.getLength() - convexLen) / convexLen; + } + } From 8d177cb5e3c144376d0a7617f54ab5d51a2ab5b0 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 22 Dec 2021 22:37:55 -0800 Subject: [PATCH 12/21] Update TestBuilder functions Signed-off-by: Martin Davis --- .../jtstest/function/ConstructionFunctions.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java index 7e6844336a..1f94f53ce6 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java @@ -132,25 +132,25 @@ public static Geometry circleByRadiusLine(Geometry radiusLine, } public static Geometry concaveHullByLen(Geometry geom, - @Metadata(title="Max edge length") + @Metadata(title="Length") double maxLen) { return ConcaveHull.concaveHullByLength(geom, maxLen); } - public static Geometry concaveHullByLenRatio(Geometry geom, - @Metadata(title="Max edge length ratio") + public static Geometry concaveHullByLenFactor(Geometry geom, + @Metadata(title="Length factor") double maxLen) { return ConcaveHull.concaveHullByLengthFactor(geom, maxLen); } public static Geometry concaveHullByLenWithHoles(Geometry geom, - @Metadata(title="Max edge length") + @Metadata(title="Length") double maxLen) { return ConcaveHull.concaveHullByLength(geom, maxLen, true); } public static Geometry concaveHullByArea(Geometry geom, - @Metadata(title="Max area ratio") + @Metadata(title="Area ratio") double minAreaPct) { return ConcaveHull.concaveHullByArea(geom, minAreaPct); } From 0b3fe9511c9788ceae534e1f9d7f9f2e1ecc5402 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 23 Dec 2021 08:13:20 -0800 Subject: [PATCH 13/21] Javadoc Signed-off-by: Martin Davis --- .../jts/algorithm/hull/ConcaveHull.java | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java index a0bddedfaa..898cecf49f 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java @@ -34,8 +34,9 @@ /** * Constructs a concave hull of a set of points. - * The hull is constructed by eroding the Delaunay Triangulation of the points - * until specified target criteria are reached. + * The hull is constructed by eroding the longest edges + * of the Delaunay Triangulation of the points + * until certain target criteria are reached. * The target criteria are: *

      *
    • Maximum Edge Length - the length of the longest edge of the hull will be no larger @@ -47,6 +48,9 @@ * will be no larger than this value. *
    * Usually only a single criteria is specified, but both may be provided. + * The preferred criteria is the Maximum Edge Length Factor, since it is + * scale-independent, and local (so that no assumption needs to be made about the + * total amount of concavity present). *

    * The computed hull is always a single connected {@link Polygon} * (unless it is degenerate, in which case it will be a {@link Point} or a {@link LineString}). @@ -94,7 +98,7 @@ public static Geometry concaveHullByLength(Geometry geom, double maxLength) { /** * Computes the concave hull of the vertices in a geometry * using the target criteria of maximum edge length factor. - * The edge length factor is a fraction of the length delta + * The edge length factor is a fraction of the length difference * between the longest and shortest edges * in the Delaunay Triangulation of the input points. * @@ -159,9 +163,15 @@ public ConcaveHull(Geometry geom) { /** * Sets the target maximum edge length for the concave hull. - * A value of 0.0 produces a concave hull of minimum area + * The length value must be zero or greater. + *

      + *
    • The value 0.0 produces the concave hull of smallest area * that is still connected. - * The {@link #uniformGridEdgeLength(Geometry)} may be used as + *
    • Larger values produce less concave results. + * A value equal or greater than the longest Delaunay Triangulation edge length + * produces the convex hull. + *
    + * The {@link #uniformGridEdgeLength(Geometry)} value may be used as * the basis for estimating an appropriate target maximum edge length. * * @param edgeLength a non-negative length @@ -177,12 +187,15 @@ public void setMaximumEdgeLength(double edgeLength) { /** * Sets the target maximum edge length factor for the concave hull. - * The edge length factor is a fraction of the length delta + * The edge length factor is a fraction of the length difference * between the longest and shortest edges - * in the Delaunay Triangulation of the input points. - * A value of 1.0 produces the convex hull. - * A value of 0.0 produces a concave hull of minimum area + * in the Delaunay Triangulation of the input points. + * It is a value in the range 0 to 1. + *
      + *
    • The value 0.0 produces a concave hull of minimum area * that is still connected. + *
    • The value 1.0 produces the convex hull. + *
        * * @param edgeLengthFactor a length factor value between 0 and 1 */ @@ -194,10 +207,13 @@ public void setMaximumEdgeLengthFactor(double edgeLengthFactor) { /** * Sets the target maximum concave hull area as a ratio of the convex hull area. - * A value of 1.0 produces the convex hull - * (unless a maximum edge length is also specified). - * A value of 0.0 produces a concave hull with the smallest area + * It is a value in the range 0 to 1. + *
          + *
        • The value 0.0 produces a concave hull with the smallest area * that is still connected. + *
        • The value 1.0 produces the convex hull + * (unless a maximum edge length is also specified). + *
        * * @param areaRatio a ratio value between 0 and 1 */ From b97a4eb37116815669910fce7946df6a0d0a6c84 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 23 Dec 2021 09:46:46 -0800 Subject: [PATCH 14/21] Javadoc Signed-off-by: Martin Davis --- .../jts/algorithm/hull/ConcaveHull.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java index 898cecf49f..3312dc46bb 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java @@ -39,11 +39,12 @@ * until certain target criteria are reached. * The target criteria are: *
          - *
        • Maximum Edge Length - the length of the longest edge of the hull will be no larger + *
        • Maximum Edge Length - the length of the longest edge of the hull is no larger * than this value. - *
        • Maximum Edge Length Factor - this determines the Maximum Edge Length - * as a fraction of the length difference between the longest and shortest edges in the Delaunay Triangulation. - * It is a way of normalizing the Maximum Edge Length to make it scale-independent. + *
        • Maximum Edge Length Factor - determine the Maximum Edge Length + * as a fraction of the difference between the longest and shortest edge lengths + * in the Delaunay Triangulation. + * This normalizes the Maximum Edge Length to be scale-independent. *
        • Maximum Area Ratio - the ratio of the concave hull area to the convex hull area * will be no larger than this value. *
        @@ -51,6 +52,10 @@ * The preferred criteria is the Maximum Edge Length Factor, since it is * scale-independent, and local (so that no assumption needs to be made about the * total amount of concavity present). + * Other length criteria can be used by setting the Maximum Edge Length. + * For example, use a length relative to the longest edge length + * in the Minimum Spanning Tree of the point set. + * Or, use a length derived from the {@link #uniformGridEdgeLength(Geometry)} value. *

        * The computed hull is always a single connected {@link Polygon} * (unless it is degenerate, in which case it will be a {@link Point} or a {@link LineString}). @@ -187,8 +192,8 @@ public void setMaximumEdgeLength(double edgeLength) { /** * Sets the target maximum edge length factor for the concave hull. - * The edge length factor is a fraction of the length difference - * between the longest and shortest edges + * The edge length factor is a fraction of the difference + * between the longest and shortest edge lengths * in the Delaunay Triangulation of the input points. * It is a value in the range 0 to 1. *

          From 76f77555c1494d517ff0ef4fd39bcab434b68a3a Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 23 Dec 2021 11:57:53 -0800 Subject: [PATCH 15/21] Javadoc Signed-off-by: Martin Davis --- .../java/org/locationtech/jts/algorithm/hull/ConcaveHull.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java index 3312dc46bb..38c56f6e7d 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java @@ -34,7 +34,7 @@ /** * Constructs a concave hull of a set of points. - * The hull is constructed by eroding the longest edges + * The hull is constructed by removing the longest outer edges * of the Delaunay Triangulation of the points * until certain target criteria are reached. * The target criteria are: From bccc2be97cfc8cc224eb72942c80bcd5c9447409 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 3 Jan 2022 12:32:56 -0800 Subject: [PATCH 16/21] Add ConcaveHull unit test for XYZ Signed-off-by: Martin Davis --- .../jts/algorithm/hull/ConcaveHullTest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java index 1d1695c2c1..6d8f05ba03 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java @@ -86,6 +86,11 @@ public void testLengthFactorOne() { 1, "POLYGON ((10 10, 10 90, 90 90, 90 60, 90 10, 10 10))" ); } + public void testLengthFactorXYZChevronP5() { + checkHullByLengthFactorXYZ("MULTIPOINT Z ((10 10 1), (90 10 2), (30 70 3), (70 70 4), (50 60 5))", + 0.5, "POLYGON Z ((30 70 3, 70 70 4, 90 10 2, 50 60 5, 10 10 1, 30 70 3))" ); + } + //------------------------------------------------ public void testLengthHolesCircle() { @@ -124,6 +129,13 @@ private void checkHullByLengthFactor(String wkt, double threshold, String wktExp checkEqual(expected, actual); } + private void checkHullByLengthFactorXYZ(String wkt, double threshold, String wktExpected) { + Geometry geom = read(wkt); + Geometry actual = ConcaveHull.concaveHullByLengthFactor(geom, threshold); + Geometry expected = read(wktExpected); + checkEqualXYZ(expected, actual); + } + private void checkHullByLength(String wkt, double threshold, String wktExpected) { Geometry geom = read(wkt); Geometry actual = ConcaveHull.concaveHullByLength(geom, threshold); From 0d904817905b4b18260fdedf52a97a18d362ef23 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 3 Jan 2022 12:38:25 -0800 Subject: [PATCH 17/21] Add output coordinate copying Signed-off-by: Martin Davis --- .../java/org/locationtech/jts/algorithm/hull/ConcaveHull.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java index 38c56f6e7d..0f28e12aad 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java @@ -715,11 +715,11 @@ private static Coordinate[] traceBorder(List triList) { do { int borderIndex = tri.borderIndexCCW(); //-- add border vertex - coordList.add(tri.getCoordinate(borderIndex), false); + coordList.add(tri.getCoordinate(borderIndex).copy(), false); int nextIndex = Tri.next(borderIndex); //-- if next edge is also border, add it and move to next if (tri.isBorder(nextIndex)) { - coordList.add(tri.getCoordinate(nextIndex), false); + coordList.add(tri.getCoordinate(nextIndex).copy(), false); borderIndex = nextIndex; } //-- find next border tri CCW around non-border edge From 96001c878779f592831b0283233ade967b779748 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 3 Jan 2022 18:14:32 -0800 Subject: [PATCH 18/21] Add queue sorting by largest area; add function for length factor with holes Signed-off-by: Martin Davis --- .../jts/algorithm/hull/ConcaveHull.java | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java index 0f28e12aad..991ac27c7c 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java @@ -100,6 +100,23 @@ public static Geometry concaveHullByLength(Geometry geom, double maxLength) { return concaveHullByLength(geom, maxLength, false); } + /** + * Computes the concave hull of the vertices in a geometry + * using the target criteria of maximum edge length, + * and optionally allowing holes. + * + * @param geom the input geometry + * @param maxLength the target maximum edge length + * @param isHolesAllowed whether holes are allowed in the result + * @return the concave hull + */ + public static Geometry concaveHullByLength(Geometry geom, double maxLength, boolean isHolesAllowed) { + ConcaveHull hull = new ConcaveHull(geom); + hull.setMaximumEdgeLength(maxLength); + hull.setHolesAllowed(isHolesAllowed); + return hull.getHull(); + } + /** * Computes the concave hull of the vertices in a geometry * using the target criteria of maximum edge length factor. @@ -112,24 +129,25 @@ public static Geometry concaveHullByLength(Geometry geom, double maxLength) { * @return the concave hull */ public static Geometry concaveHullByLengthFactor(Geometry geom, double lengthFactor) { - ConcaveHull hull = new ConcaveHull(geom); - hull.setMaximumEdgeLengthFactor(lengthFactor); - return hull.getHull(); + return concaveHullByLengthFactor(geom, lengthFactor, false); } /** * Computes the concave hull of the vertices in a geometry - * using the target criteria of maximum edge length, + * using the target criteria of maximum edge length factor, * and optionally allowing holes. + * The edge length factor is a fraction of the length difference + * between the longest and shortest edges + * in the Delaunay Triangulation of the input points. * * @param geom the input geometry * @param maxLength the target maximum edge length * @param isHolesAllowed whether holes are allowed in the result * @return the concave hull */ - public static Geometry concaveHullByLength(Geometry geom, double maxLength, boolean isHolesAllowed) { + public static Geometry concaveHullByLengthFactor(Geometry geom, double lengthFactor, boolean isHolesAllowed) { ConcaveHull hull = new ConcaveHull(geom); - hull.setMaximumEdgeLength(maxLength); + hull.setMaximumEdgeLengthFactor(lengthFactor); hull.setHolesAllowed(isHolesAllowed); return hull.getHull(); } @@ -573,10 +591,22 @@ public HullTri nextBorderTri() { * PriorityQueues sort in ascending order. * To sort with the largest at the head, * smaller sizes must compare as greater than larger sizes. - * (i.e. the normal numeric comparison is reversed) + * (i.e. the normal numeric comparison is reversed). + * If the sizes are identical (which should be an infrequent case), + * the areas are compared, with larger areas sorting before smaller. + * (The rationale is that larger areas indicate an area of lower point density, + * which is more likely to be in the exterior of the computed shape.) + * This improves the determinism of the queue ordering. */ @Override public int compareTo(HullTri o) { + /** + * If size is identical compare areas to ensure a (more) deterministic ordering. + * Larger areas sort before smaller ones. + */ + if (size == o.size) { + return -Double.compare(this.getArea(), o.getArea()); + } return -Double.compare(size, o.size); } From 16264de762950d0d3531976d898c68d6bb336005 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 3 Jan 2022 18:14:49 -0800 Subject: [PATCH 19/21] Add TestBuilder functions Signed-off-by: Martin Davis --- .../jtstest/function/ConstructionFunctions.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java index 1f94f53ce6..ce486cc532 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java @@ -137,16 +137,22 @@ public static Geometry concaveHullByLen(Geometry geom, return ConcaveHull.concaveHullByLength(geom, maxLen); } + public static Geometry concaveHullWithHolesByLen(Geometry geom, + @Metadata(title="Length") + double maxLen) { + return ConcaveHull.concaveHullByLength(geom, maxLen, true); + } + public static Geometry concaveHullByLenFactor(Geometry geom, @Metadata(title="Length factor") double maxLen) { return ConcaveHull.concaveHullByLengthFactor(geom, maxLen); } - public static Geometry concaveHullByLenWithHoles(Geometry geom, + public static Geometry concaveHullWithHolesByLenFactor(Geometry geom, @Metadata(title="Length") double maxLen) { - return ConcaveHull.concaveHullByLength(geom, maxLen, true); + return ConcaveHull.concaveHullByLengthFactor(geom, maxLen, true); } public static Geometry concaveHullByArea(Geometry geom, From 6e322c48ea704592ecded29a41d873a6e205e583 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 4 Jan 2022 08:18:20 -0800 Subject: [PATCH 20/21] Javadoc, concaveness function Signed-off-by: Martin Davis --- .../function/ConstructionFunctions.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java index ce486cc532..ee2f2c9ee3 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java @@ -166,13 +166,21 @@ public static double concaveHullLenGuess(Geometry geom) { } /** - * A concavity measure defined in terms of perimeter length. - * As defined by Park & Oh, 2012 + * A concaveness measure defined in terms of the perimeter length + * relative to the convex hull perimeter. + *
          +   * C = ( P(geom) - P(CH) ) / P(CH)
          +   * 
          + * Concaveness values are >= 0. + * A convex polygon has C = 0. + * A higher concaveness indicates a more concave polygon. + *

          + * Originally defined by Park & Oh, 2012. * - * @param geom - * @return + * @param geom a polygonal geometry + * @return the concaveness measure of the geometry */ - public static double concavity(Geometry geom) { + public static double concaveness(Geometry geom) { double convexLen = geom.convexHull().getLength(); return (geom.getLength() - convexLen) / convexLen; } From d52c76c2e7a3e19a44f5107d0032444b3a7781dc Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 4 Jan 2022 08:56:38 -0800 Subject: [PATCH 21/21] Fix TestBuilder function metadata Signed-off-by: Martin Davis --- .../locationtech/jtstest/function/ConstructionFunctions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java index ee2f2c9ee3..e3301c8258 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java @@ -150,7 +150,7 @@ public static Geometry concaveHullByLenFactor(Geometry geom, } public static Geometry concaveHullWithHolesByLenFactor(Geometry geom, - @Metadata(title="Length") + @Metadata(title="Length factor") double maxLen) { return ConcaveHull.concaveHullByLengthFactor(geom, maxLen, true); }