View Javadoc
1   package org.djutils.draw.line;
2   
3   import java.util.ArrayList;
4   import java.util.Collection;
5   import java.util.Collections;
6   import java.util.Comparator;
7   import java.util.Iterator;
8   import java.util.List;
9   
10  import org.djutils.draw.DrawRuntimeException;
11  import org.djutils.draw.Drawable2d;
12  import org.djutils.draw.bounds.Bounds2d;
13  import org.djutils.draw.point.Point2d;
14  import org.djutils.exceptions.Throw;
15  
16  /**
17   * ConvexHull.java. Compute the convex hull of a collection of Point2d or Drawable2d. All implementations here return a
18   * Polygon2d object. If the convex hull of the input would be a single point, the implementations will throw a
19   * DrawRuntimeException because a single point does not make a valid Polygon2d.
20   * <p>
21   * Copyright (c) 2020-2025 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
22   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
23   * </p>
24   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
25   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
26   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
27   */
28  public final class ConvexHull
29  {
30      /**
31       * Do not instantiate.
32       */
33      private ConvexHull()
34      {
35          // Do not instantiate
36      }
37  
38      /**
39       * Compute the convex hull of a collection of Point2d objects.
40       * @param iterator iterator that shall return all the points for which the convex hull is to be
41       *            computed
42       * @return the convex hull of the points
43       */
44      public static Polygon2d convexHull(final Iterator<Point2d> iterator)
45      {
46          List<Point2d> list = new ArrayList<>();
47          iterator.forEachRemaining(list::add);
48          return convexHullAlshamrani(list);
49      }
50  
51      /**
52       * Compute the convex hull of one or more Drawable2d objects.
53       * @param drawable2d the Drawable2d objects
54       * @return the convex hull of the Drawable2d objects
55       * @throws NullPointerException when any of the drawable2d object is <code>null</code>
56       * @throws IllegalArgumentException when zero arguments are provided
57       */
58      public static Polygon2d convexHull(final Drawable2d... drawable2d)
59      {
60          return convexHull(Bounds2d.pointsOf(drawable2d));
61      }
62  
63      /**
64       * Construct a Bounds2d for a Collection of Drawable2d objects.
65       * @param drawableCollection the collection
66       * @return the convex hull of the Drawable2d objects
67       * @throws NullPointerException when the <code>drawableCollection</code> is <code>null</code>, or contains a
68       *             <code>null</code> value
69       * @throws IllegalArgumentException when the <code>drawableCollection</code> is empty
70       */
71      public static Polygon2d convexHull(final Collection<Drawable2d> drawableCollection)
72      {
73          Throw.whenNull(drawableCollection, "drawableCollection");
74          Throw.when(drawableCollection.isEmpty(), DrawRuntimeException.class, "drawableCollection may not be empty");
75          return convexHull(Bounds2d.pointsOf(drawableCollection));
76      }
77  
78      /**
79       * Compute the convex hull of a list of Point2d objects. The input list will not be modified.
80       * @param list the list of Point2d objects
81       * @return the convex hull of the points
82       */
83      public static Polygon2d convexHull(final List<Point2d> list)
84      {
85          return convexHullAlshamrani(list);
86      }
87  
88      /**
89       * Return whether moving from a through b to c, the turn at b is counter-clockwise.
90       * @param a point a
91       * @param b point b
92       * @param c point c
93       * @return <code>true</code> if the turn at b is counter clockwise; <code>false</code> if there is not turn; or it
94       *         is clockwise
95       */
96      private static boolean ccw(final Point2d a, final Point2d b, final Point2d c)
97      {
98          return ((b.x - a.x) * (c.y - a.y)) > ((b.y - a.y) * (c.x - a.x));
99      }
100 
101     /**
102      * Repeatedly remove the last point if not counter clockwise with new point; then add the new point. If the new point is
103      * equal to the last point in the list; do nothing.
104      * @param list the list of points
105      * @param newPoint the point that will be added.
106      */
107     private static void cleanAndAppend(final List<Point2d> list, final Point2d newPoint)
108     {
109         Point2d last = list.get(list.size() - 1);
110         if (last.x == newPoint.x && last.y == newPoint.y)
111         {
112             return;
113         }
114         while (list.size() >= 2 && !ccw(list.get(list.size() - 2), list.get(list.size() - 1), newPoint))
115         {
116             list.remove(list.size() - 1);
117         }
118         list.add(newPoint);
119     }
120 
121     /**
122      * Implementation of the convex hull algorithm by Reham Alshamrani c.s.; see
123      * <a href="https://www.sciencedirect.com/science/article/pii/S1877050920304750">A Preprocessing Technique for Fast Convex
124      * Hull Computation</a>.
125      * @param list list of the points (will not be modified)
126      * @return the convex hull of the points
127      * @throws NullPointerException when the <code>list</code> is <code>null</code>
128      * @throws IllegalArgumentException when the <code>list</code> contains too few points
129      */
130     public static Polygon2d convexHullAlshamrani(final List<Point2d> list)
131     {
132         // Find the four extreme points
133         Throw.whenNull(list, "list");
134         Throw.when(list.size() < 1, IllegalArgumentException.class, "Too few points in list");
135         Point2d minX = list.get(0); // Initialize to the first point in list to avoid checking for null on each iteration
136         Point2d minY = list.get(0);
137         Point2d maxX = list.get(0);
138         Point2d maxY = list.get(0);
139         for (Point2d point : list)
140         {
141             if (minX.x > point.x || minX.x == point.x && minX.y > point.y)
142             {
143                 minX = point;
144             }
145             if (minY.y > point.y || minY.y == point.y && minY.x < point.x)
146             {
147                 minY = point;
148             }
149             if (maxX.x < point.x || maxX.x == point.x && maxX.y > point.y)
150             {
151                 maxX = point;
152             }
153             if (maxY.y < point.y || maxY.y == point.y && maxY.x < point.x)
154             {
155                 maxY = point;
156             }
157         }
158         // Filter and group the points into priority queues that order by x value (tie breaker is y value)
159         // Alshamrani does not show how he tests that a point is outside each edge of the four extreme points. We use ccw.
160         // Alshamrani poorly documents the ordering method for the four queues when the primary component values are the same.
161         // Testing has shown that sorting a filled ArrayList is faster than putting the same elements one by one in a TreeSet.
162         Comparator<Point2d> forwardComparator = new Comparator<Point2d>()
163         {
164             @Override
165             public int compare(final Point2d point1, final Point2d point2)
166             {
167                 if (point1.x == point2.x)
168                 {
169                     return (int) Math.signum(point1.y - point2.y);
170                 }
171                 return (int) Math.signum(point1.x - point2.x);
172             }
173         };
174         Comparator<Point2d> reverseComparator = new Comparator<Point2d>()
175         {
176             @Override
177             public int compare(final Point2d point1, final Point2d point2)
178             {
179                 if (point1.x == point2.x)
180                 {
181                     return (int) Math.signum(point1.y - point2.y);
182                 }
183                 return (int) Math.signum(point2.x - point1.x);
184             }
185         };
186         List<Point2d> lowerLeft = new ArrayList<>();
187         List<Point2d> lowerRight = new ArrayList<>();
188         List<Point2d> upperRight = new ArrayList<>();
189         List<Point2d> upperLeft = new ArrayList<>();
190 
191         for (Point2d point : list)
192         {
193             if (point.x <= minY.x && point.y <= minX.y)
194             {
195                 if (ccw(minX, point, minY))
196                 {
197                     lowerLeft.add(point);
198                 }
199             }
200             else if (point.x >= minY.x && point.y <= maxX.y)
201             {
202                 if (ccw(minY, point, maxX))
203                 {
204                     lowerRight.add(point);
205                 }
206             }
207             else if (point.x >= maxY.x && point.y >= maxX.y)
208             {
209                 if (ccw(maxX, point, maxY))
210                 {
211                     upperRight.add(point);
212                 }
213             }
214             else if (point.x <= maxY.x && point.y >= minX.y)
215             {
216                 if (ccw(maxY, point, minX))
217                 {
218                     upperLeft.add(point);
219                 }
220             }
221         }
222         Collections.sort(lowerLeft, forwardComparator);
223         Collections.sort(lowerRight, forwardComparator);
224         Collections.sort(upperRight, reverseComparator);
225         Collections.sort(upperLeft, reverseComparator);
226         // Construct the convex hull
227         List<Point2d> result = new ArrayList<>();
228         result.add(minX);
229         for (Point2d point : lowerLeft)
230         {
231             cleanAndAppend(result, point);
232         }
233         cleanAndAppend(result, minY);
234         for (Point2d point : lowerRight)
235         {
236             cleanAndAppend(result, point);
237         }
238         cleanAndAppend(result, maxX);
239         for (Point2d point : upperRight)
240         {
241             cleanAndAppend(result, point);
242         }
243         cleanAndAppend(result, maxY);
244         for (Point2d point : upperLeft)
245         {
246             cleanAndAppend(result, point);
247         }
248         return new Polygon2d(result);
249     }
250 
251     /**
252      * Implementation of Andrew's Monotone Chain convex hull algorithm. This implementation (sorts) modifies the provided list
253      * of points!
254      * @param list list of the points (will be modified)
255      * @return the convex hull of the points
256      * @throws NullPointerException when the <code>list</code> is <code>null</code>
257      * @throws IllegalArgumentException when the <code>list</code> contains too few points
258      */
259     public static Polygon2d convexHullMonotone(final List<Point2d> list)
260     {
261         Collections.sort(list, new Comparator<Point2d>()
262         {
263             @Override
264             public int compare(final Point2d point1, final Point2d point2)
265             {
266                 if (point1.x == point2.x)
267                 {
268                     return (int) Math.signum(point1.y - point2.y);
269                 }
270                 return (int) Math.signum(point1.x - point2.x);
271             }
272         });
273         // That sort operation was O(N log (N)); the remainder is O(N)
274         List<Point2d> result = new ArrayList<>();
275         // Lower hull
276         for (Point2d p : list)
277         {
278             while (result.size() >= 2 && !ccw(result.get(result.size() - 2), result.get(result.size() - 1), p))
279             {
280                 result.remove(result.size() - 1);
281             }
282             result.add(p);
283         }
284         // Upper hull
285         int lowLimit = result.size() + 1;
286         for (int i = list.size() - 1; i >= 0; i--)
287         {
288             Point2d p = list.get(i);
289             while (result.size() >= lowLimit && !ccw(result.get(result.size() - 2), result.get(result.size() - 1), p))
290             {
291                 result.remove(result.size() - 1);
292             }
293             result.add(p);
294         }
295         if (result.size() > 0)
296         {
297             result.remove(result.size() - 1);
298         }
299         // else; zero points; the constructor of the Polygon2d will throw an IllegalArgumentException
300         return new Polygon2d(result);
301     }
302 
303 }