This is an automated email from the ASF dual-hosted git repository. mattjuntunen pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-geometry.git
commit 519c19fed40eeb94d6664d6e5eedf0563a2d8b48 Author: Andreas Goß <andreas.goss1...@gmail.com> AuthorDate: Thu Jul 20 21:25:03 2023 +0200 GEOMETRY-144 Add Builder to ConvexHull2D which uses the logic of AklToussainHeuristic, AbstractVonvexHullGenerator2D and MonotoneChain. --- .../geometry/euclidean/twod/hull/ConvexHull2D.java | 364 ++++++++++++++++++++- 1 file changed, 363 insertions(+), 1 deletion(-) diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/ConvexHull2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/ConvexHull2D.java index a3a7b9ec..099abd29 100644 --- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/ConvexHull2D.java +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/ConvexHull2D.java @@ -14,15 +14,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.commons.geometry.hull.euclidean.twod; + +package org.apache.commons.geometry.euclidean.twod.hull; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.List; import org.apache.commons.geometry.core.ConvexHull; +import org.apache.commons.geometry.euclidean.EuclideanCollections; import org.apache.commons.geometry.euclidean.twod.ConvexArea; +import org.apache.commons.geometry.euclidean.twod.Lines; import org.apache.commons.geometry.euclidean.twod.Vector2D; import org.apache.commons.geometry.euclidean.twod.path.LinePath; import org.apache.commons.numbers.core.Precision; @@ -104,4 +108,362 @@ public final class ConvexHull2D implements ConvexHull<Vector2D> { .appendVertices(vertices) .build(closeLoop); } + + /** Class used to build convex hulls. The builder is based on the Akl-Toussaint + * heuristic to construct the hull. The heuristic is based on the idea of a + * convex quadrilateral, which is formed by four points with the lowest and + * highest x / y coordinates. Any point that lies inside this quadrilateral can + * not be part of the convex hull and can thus be safely discarded before + * generating the convex hull itself. + * <p> + * The complexity of the operation is O(n), and may greatly improve the time it + * takes to construct the convex hull afterwards, depending on the point + * distribution. + * + * @see <a href= + * "http://en.wikipedia.org/wiki/Convex_hull_algorithms#Akl-Toussaint_heuristic"> + * Akl-Toussaint heuristic (Wikipedia)</a> + */ + public static final class Builder { + + /** Corner of triangle with minimal x coordinate. */ + private Vector2D minX; + + /** Corner of triangle with maximal x coordinate. */ + private Vector2D maxX; + + /** Corner of triangle with minimal y coordinate. */ + private Vector2D minY; + + /** Corner of triangle with maximal y coordinate. */ + private Vector2D maxY; + + /** Collection of all remaining candidates for a convex hull. */ + private final Collection<Vector2D> candidates; + + /** A precision context for comparing points. */ + private final Precision.DoubleEquivalence precision; + + /** Indicates if collinear points on the hull shall be present in the output. + * If {@code false}, only the extreme points are added to the hull. + */ + private final boolean includeCollinearPoints; + + /**Return a {@link Builder} instance configured with the given precision + * context. The precision context is used when comparing points. + * + * @param builderPrecision precision context to use when building a convex + * hull from raw vertices; may be null if raw + * vertices are not used. + * @param includeCollinearPoints whether collinear points shall be added as hull + * vertices + */ + public Builder(final boolean includeCollinearPoints, final Precision.DoubleEquivalence builderPrecision) { + this.precision = builderPrecision; + this.includeCollinearPoints = includeCollinearPoints; + candidates = EuclideanCollections.pointSet2D(builderPrecision); + } + + /** Appends the given point to a collection of possible hull points, if and only + * if the given point is outside of a constructed quadrilateral of extreme properties. + * + * @param point a given point. + * @return this instance. + */ + public Builder append(Vector2D point) { + + //Checks if the given point supersedes one of the corners. + checkCorners(point); + + //Only proceed if the quadrilateral is complete. + if (candidates.size() < 4) { + return this; + } + + final List<Vector2D> quadrilateral = buildQuadrilateral(minY, maxX, maxY, minX); + // if the quadrilateral is not well formed, e.g. only 2 points, do not attempt to reduce + if (quadrilateral.size() < 3) { + return this; + } + + // check all points if they are within the quadrilateral + // in which case they can not be part of the convex hull + if (!insideQuadrilateral(point, quadrilateral)) { + candidates.add(point); + } + + return this; + } + + /** Appends the given points to a collection of possible hull points, if and only + * if the given points are outside of a constructed quadrilateral of extreme + * properties. + * + * @param points a given collection of points. + * @throws NullPointerException if points is {@code null}. + * @return this instance. + */ + public Builder append(Collection<Vector2D> points) { + points.forEach(this::append); + return this; + } + + /** + * Build a convex hull from the set appended points. + * + * @return the convex hull + * @throws IllegalStateException if generator fails to generate a convex hull for + * the given set of input points + */ + public ConvexHull2D build() { + Collection<Vector2D> hullVertices; + if (candidates.size() < 2) { + hullVertices = candidates; + } else { + hullVertices = findHullVertices(candidates); + } + + if (!isConvex(hullVertices)) { + throw new IllegalStateException("Convex hull algorithm failed to generate solution"); + } + + return new ConvexHull2D(hullVertices, precision); + } + + /** Build the convex quadrilateral with the found corner points (with min/max x/y + * coordinates). + * + * @param points the respective points with min/max x/y coordinate + * @return the quadrilateral + */ + private static List<Vector2D> buildQuadrilateral(final Vector2D... points) { + final List<Vector2D> quadrilateral = new ArrayList<>(); + for (final Vector2D p : points) { + if (!quadrilateral.contains(p)) { + quadrilateral.add(p); + } + } + return quadrilateral; + } + + /** Checks if the given point supersedes one of the corners. If it does the old + * corner is removed and the point added to the collection of points. + * + * @param point a given point. + */ + private void checkCorners(Vector2D point) { + if (minX == null || point.getX() < minX.getX()) { + minX = point; + candidates.add(point); + } + if (maxX == null || point.getX() > maxX.getX()) { + maxX = point; + candidates.add(point); + } + if (minY == null || point.getY() < minY.getY()) { + minY = point; + candidates.add(point); + } + if (maxY == null || point.getY() > maxY.getY()) { + maxY = point; + candidates.add(point); + } + } + + /** Checks if the given point is located within the convex quadrilateral. + * @param point the point to check + * @param quadrilateralPoints the convex quadrilateral, represented by 4 points + * @return {@code true} if the point is inside the quadrilateral, {@code false} otherwise + */ + private boolean insideQuadrilateral(final Vector2D point, + final List<? extends Vector2D> quadrilateralPoints) { + + Vector2D v1 = quadrilateralPoints.get(0); + Vector2D v2 = quadrilateralPoints.get(1); + + if (point.equals(v1) || point.equals(v2)) { + return true; + } + + // get the location of the point relative to the first two vertices + final double last = signedAreaPoints(v1, v2, point); + + // If the area is zero then this means the given point is on a boundary line. + // and must be included as collinear point. + if (precision.eq(last, 0.0) && includeCollinearPoints) { + return false; + } + + final int size = quadrilateralPoints.size(); + // loop through the rest of the vertices + for (int i = 1; i < size; i++) { + v1 = v2; + v2 = quadrilateralPoints.get((i + 1) == size ? 0 : i + 1); + + if (point.equals(v2)) { + return true; + } + + // do side of line test: multiply the last location with this location + // if they are the same sign then the operation will yield a positive result + // -x * -y = +xy, x * y = +xy, -x * y = -xy, x * -y = -xy + if (last * signedAreaPoints(v1, v2, point) < 0) { + return false; + } + } + return true; + } + + /** Compute the signed area of the parallelogram formed by vectors between the given points. The first + * vector points from {@code p0} to {@code p1} and the second from {@code p0} to {@code p3}. + * @param p0 first point + * @param p1 second point + * @param p2 third point + * @return signed area of parallelogram formed by vectors between the given points + */ + private static double signedAreaPoints(final Vector2D p0, final Vector2D p1, final Vector2D p2) { + return p0.vectorTo(p1).signedArea(p0.vectorTo(p2)); + } + + /** + * Find the convex hull vertices from the set of input points. + * @param points the set of input points + * @return the convex hull vertices in CCW winding + */ + private Collection<Vector2D> findHullVertices(final Collection<Vector2D> points) { + + final List<Vector2D> pointsSortedByXAxis = new ArrayList<>(points); + + // sort the points in increasing order on the x-axis + pointsSortedByXAxis.sort((o1, o2) -> { + // need to take the tolerance value into account, otherwise collinear points + // will not be handled correctly when building the upper/lower hull + final int cmp = precision.compare(o1.getX(), o2.getX()); + if (cmp == 0) { + return precision.compare(o1.getY(), o2.getY()); + } else { + return cmp; + } + }); + + // build lower hull + final List<Vector2D> lowerHull = new ArrayList<>(); + for (final Vector2D p : pointsSortedByXAxis) { + updateHull(p, lowerHull); + } + + // build upper hull + final List<Vector2D> upperHull = new ArrayList<>(); + for (int idx = pointsSortedByXAxis.size() - 1; idx >= 0; idx--) { + final Vector2D p = pointsSortedByXAxis.get(idx); + updateHull(p, upperHull); + } + + // concatenate the lower and upper hulls + // the last point of each list is omitted as it is repeated at the beginning of the other list + final List<Vector2D> hullVertices = new ArrayList<>(lowerHull.size() + upperHull.size() - 2); + for (int idx = 0; idx < lowerHull.size() - 1; idx++) { + hullVertices.add(lowerHull.get(idx)); + } + for (int idx = 0; idx < upperHull.size() - 1; idx++) { + hullVertices.add(upperHull.get(idx)); + } + + // special case: if the lower and upper hull may contain only 1 point if all are identical + if (hullVertices.isEmpty() && !lowerHull.isEmpty()) { + hullVertices.add(lowerHull.get(0)); + } + + return hullVertices; + } + + /** + * Update the partial hull with the current point. + * + * @param point the current point + * @param hull the partial hull + */ + private void updateHull(final Vector2D point, final List<Vector2D> hull) { + if (hull.size() == 1) { + // ensure that we do not add an identical point + final Vector2D p1 = hull.get(0); + if (p1.eq(point, precision)) { + return; + } + } + + while (hull.size() >= 2) { + final int size = hull.size(); + final Vector2D p1 = hull.get(size - 2); + final Vector2D p2 = hull.get(size - 1); + + final double offset = Lines.fromPoints(p1, p2, precision).offset(point); + if (precision.eqZero(offset)) { + // the point is collinear to the line (p1, p2) + + final double distanceToCurrent = p1.distance(point); + if (precision.eqZero(distanceToCurrent) || precision.eqZero(p2.distance(point))) { + // the point is assumed to be identical to either p1 or p2 + return; + } + + final double distanceToLast = p1.distance(p2); + if (includeCollinearPoints) { + final int index = distanceToCurrent < distanceToLast ? size - 1 : size; + hull.add(index, point); + } else { + if (distanceToCurrent > distanceToLast) { + hull.remove(size - 1); + hull.add(point); + } + } + return; + } else if (offset > 0) { + hull.remove(size - 1); + } else { + break; + } + } + hull.add(point); + } + + /** Return true if the given vertices define a convex hull. + * @param vertices the hull vertices + * @return {@code true} if the vertices form a convex hull, {@code false} otherwise + */ + private boolean isConvex(final Collection<Vector2D> vertices) { + final int size = vertices.size(); + + if (size < 3) { + // 1 or 2 points always define a convex set + return true; + } + + final Iterator<Vector2D> it = vertices.iterator(); + + Vector2D p1 = it.next(); + Vector2D p2 = it.next(); + Vector2D p3; + + Vector2D v1; + Vector2D v2; + + while (it.hasNext()) { + p3 = it.next(); + + v1 = p1.vectorTo(p2); + v2 = p2.vectorTo(p3); + + // negative signed areas mean a clockwise winding + if (precision.compare(v1.signedArea(v2), 0.0) < 0) { + return false; + } + + p1 = p2; + p2 = p3; + } + + return true; + } + } }