From d33f08388c8406f1cdaf5568c8c534554a1b6b58 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 4 Jan 2022 09:50:04 -0800 Subject: [PATCH] ConcaveHull for Points (#823) * ConcaveHull for Points, with Length, Length Factor and Area Ratio target criteria Signed-off-by: Martin Davis --- .../function/ConstructionFunctions.java | 55 ++ .../jts/algorithm/hull/ConcaveHull.java | 769 ++++++++++++++++++ .../org/locationtech/jts/geom/Triangle.java | 25 +- .../locationtech/jts/triangulate/tri/Tri.java | 63 +- .../triangulate/tri/TriangulationBuilder.java | 4 +- .../jts/algorithm/hull/ConcaveHullTest.java | 159 ++++ .../locationtech/jts/hull/ConcaveHull.java | 53 -- .../jts/hull/ConcaveHullTest.java | 45 - 8 files changed, 1066 insertions(+), 107 deletions(-) create mode 100644 modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java create mode 100644 modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java 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/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java index 13df15dc05..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 @@ -16,6 +16,7 @@ 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; @@ -130,4 +131,58 @@ public static Geometry circleByRadiusLine(Geometry radiusLine, return radiusLine.getFactory().createPolygon(circlePts); } + public static Geometry concaveHullByLen(Geometry geom, + @Metadata(title="Length") + double maxLen) { + 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 concaveHullWithHolesByLenFactor(Geometry geom, + @Metadata(title="Length factor") + double maxLen) { + return ConcaveHull.concaveHullByLengthFactor(geom, maxLen, true); + } + + public static Geometry concaveHullByArea(Geometry geom, + @Metadata(title="Area ratio") + double minAreaPct) { + return ConcaveHull.concaveHullByArea(geom, minAreaPct); + } + + public static double concaveHullLenGuess(Geometry geom) { + return ConcaveHull.uniformGridEdgeLength(geom); + } + + /** + * 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 a polygonal geometry + * @return the concaveness measure of the geometry + */ + public static double concaveness(Geometry geom) { + double convexLen = geom.convexHull().getLength(); + return (geom.getLength() - convexLen) / convexLen; + } + } 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 new file mode 100644 index 0000000000..991ac27c7c --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java @@ -0,0 +1,769 @@ +/* + * 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.algorithm.hull; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +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; +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; +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; +import org.locationtech.jts.util.Assert; + +/** + * Constructs a concave hull of a set of points. + * 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: + *

+ * 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). + * 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}). + * 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 result in substantially slower computation, + * and it can produce results of low quality. + * + * @author Martin Davis + * + */ +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 uniformGridEdgeLength(Geometry geom) { + double areaCH = geom.convexHull().getArea(); + int numPts = geom.getNumPoints(); + 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) { + 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. + * 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 lengthFactor the target edge length factor + * @return the concave hull + */ + public static Geometry concaveHullByLengthFactor(Geometry geom, double lengthFactor) { + return concaveHullByLengthFactor(geom, lengthFactor, false); + } + + /** + * Computes the concave hull of the vertices in a geometry + * 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 concaveHullByLengthFactor(Geometry geom, double lengthFactor, boolean isHolesAllowed) { + ConcaveHull hull = new ConcaveHull(geom); + hull.setMaximumEdgeLengthFactor(lengthFactor); + hull.setHolesAllowed(isHolesAllowed); + 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); + return hull.getHull(); + } + + 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(); + } + + /** + * Sets the target maximum edge length for the concave hull. + * The length value must be zero or greater. + *

+ * 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 + * + * @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 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. + *