diff --git a/src/main/java/org/apache/commons/math3/geometry/euclidean/twod/hull/AklToussaintHeuristic.java b/src/main/java/org/apache/commons/math3/geometry/euclidean/twod/hull/AklToussaintHeuristic.java new file mode 100644 index 000000000..b540fa146 --- /dev/null +++ b/src/main/java/org/apache/commons/math3/geometry/euclidean/twod/hull/AklToussaintHeuristic.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.math3.geometry.euclidean.twod.hull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.math3.geometry.euclidean.twod.Vector2D; + +/** + * A simple heuristic to improve the performance of convex hull algorithms. + *

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

+ * 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 + * Akl-Toussaint heuristic (Wikipedia) + * @since 3.3 + * @version $Id$ + */ +public final class AklToussaintHeuristic { + + /** Hide utility constructor. */ + private AklToussaintHeuristic() { + } + + /** + * Returns a point set that is reduced by all points for which it is safe to assume + * that they are not part of the convex hull. + * + * @param points the original point set + * @return a reduced point set, useful as input for convex hull algorithms + */ + public static Collection reducePoints(final Collection points) { + + // find the leftmost point + int size = 0; + Vector2D minX = null; + Vector2D maxX = null; + Vector2D minY = null; + Vector2D maxY = null; + for (Vector2D p : points) { + if (minX == null || p.getX() < minX.getX()) { + minX = p; + } + if (maxX == null || p.getX() > maxX.getX()) { + maxX = p; + } + if (minY == null || p.getY() < minY.getY()) { + minY = p; + } + if (maxY == null || p.getY() > maxY.getY()) { + maxY = p; + } + size++; + } + + if (size < 4) { + return points; + } + + final List 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 points; + } + + final List reducedPoints = new ArrayList(quadrilateral); + for (final Vector2D p : points) { + // check all points if they are within the quadrilateral + // in which case they can not be part of the convex hull + if (!insideQuadrilateral(p, quadrilateral)) { + reducedPoints.add(p); + } + } + + return reducedPoints; + } + + /** + * 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 buildQuadrilateral(final Vector2D... points) { + List quadrilateral = new ArrayList(); + for (Vector2D p : points) { + if (!quadrilateral.contains(p)) { + quadrilateral.add(p); + } + } + return quadrilateral; + } + + /** + * 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 static boolean insideQuadrilateral(final Vector2D point, + final List quadrilateralPoints) { + + Vector2D p1 = quadrilateralPoints.get(0); + Vector2D p2 = quadrilateralPoints.get(1); + + if (point.equals(p1) || point.equals(p2)) { + return true; + } + + // get the location of the point relative to the first two vertices + final double last = getLocation(point, p1, p2); + final int size = quadrilateralPoints.size(); + // loop through the rest of the vertices + for (int i = 1; i < size; i++) { + p1 = p2; + p2 = quadrilateralPoints.get((i + 1) == size ? 0 : i + 1); + + if (point.equals(p1) || point.equals(p2)) { + 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 * getLocation(point, p1, p2) < 0) { + return false; + } + } + return true; + } + + /** + * Get the location of a point with regard to the given line. + *

+ * Note: this method does the same as {@link Line#getOffset(Vector)} but is + * faster, thus preferred for this heuristic. + * + * @param point the point to check + * @param linePoint1 the first point of the line + * @param linePoint2 the second point of the line + * @return the location of the point with regard to the line + */ + private static double getLocation(final Vector2D point, + final Vector2D linePoint1, + final Vector2D linePoint2) { + return (linePoint2.getX() - linePoint1.getX()) * (point.getY() - linePoint1.getY()) - + (point.getX() - linePoint1.getX()) * (linePoint2.getY() - linePoint1.getY()); + } + +} diff --git a/src/test/java/org/apache/commons/math3/geometry/euclidean/twod/hull/AklToussaintHeuristicTest.java b/src/test/java/org/apache/commons/math3/geometry/euclidean/twod/hull/AklToussaintHeuristicTest.java new file mode 100644 index 000000000..0a9e594b0 --- /dev/null +++ b/src/test/java/org/apache/commons/math3/geometry/euclidean/twod/hull/AklToussaintHeuristicTest.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.math3.geometry.euclidean.twod.hull; + +import java.util.Collection; + +import org.apache.commons.math3.geometry.euclidean.twod.Vector2D; + +/** + * Test class for AklToussaintHeuristic. + * @version $Id$ + */ +public class AklToussaintHeuristicTest extends ConvexHullGenerator2DAbstractTest { + + protected ConvexHullGenerator2D createConvexHullGenerator() { + return new GrahamScan2D(); + } + + @Override + protected Collection reducePoints(Collection points) { + return AklToussaintHeuristic.reducePoints(points); + } + +} diff --git a/src/test/java/org/apache/commons/math3/geometry/euclidean/twod/hull/ConvexHullGenerator2DAbstractTest.java b/src/test/java/org/apache/commons/math3/geometry/euclidean/twod/hull/ConvexHullGenerator2DAbstractTest.java index 3d4669d82..136cd4faf 100644 --- a/src/test/java/org/apache/commons/math3/geometry/euclidean/twod/hull/ConvexHullGenerator2DAbstractTest.java +++ b/src/test/java/org/apache/commons/math3/geometry/euclidean/twod/hull/ConvexHullGenerator2DAbstractTest.java @@ -45,6 +45,11 @@ public abstract class ConvexHullGenerator2DAbstractTest { protected abstract ConvexHullGenerator2D createConvexHullGenerator(); + protected Collection reducePoints(Collection points) { + // do nothing by default, may be overridden by other tests + return points; + } + @Before public void setUp() { generator = createConvexHullGenerator(); @@ -89,7 +94,7 @@ public abstract class ConvexHullGenerator2DAbstractTest { int size = (int) FastMath.floor(random.nextDouble() * 96.0 + 4.0); List points = createRandomPoints(size); - ConvexHull2D hull = generator.generate(points); + ConvexHull2D hull = generator.generate(reducePoints(points)); checkConvexHull(points, hull); } }