View Javadoc
1   package org.djutils.draw.line;
2   
3   import java.awt.geom.Path2D;
4   import java.awt.geom.Path2D.Double;
5   import java.util.ArrayList;
6   import java.util.Arrays;
7   import java.util.Iterator;
8   import java.util.List;
9   
10  import org.djutils.draw.bounds.Bounds2d;
11  import org.djutils.draw.point.Point2d;
12  import org.djutils.exceptions.Throw;
13  
14  /**
15   * Closed PolyLine2d. The actual closing point (which is the same as the starting point) is NOT included in the super
16   * PolyLine2d. The constructors automatically remove the last point if it is a at the same location as the first point.
17   * <p>
18   * Copyright (c) 2020-2025 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
19   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
20   * </p>
21   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
22   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
23   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
24   */
25  public class Polygon2d extends PolyLine2d
26  {
27      /** */
28      private static final long serialVersionUID = 20209999L;
29  
30      /**
31       * Construct a new Polygon2d.
32       * @param x the x coordinates of the points
33       * @param y the y coordinates of the points
34       * @throws NullPointerException when <code>x</code>, or <code>y</code> is <code>null</code>
35       * @throws IllegalArgumentException when any two successive points are equal, or when there are too few points, or when the
36       *             lengths of the coordinate arrays are not equal
37       */
38      public Polygon2d(final double[] x, final double[] y)
39      {
40          this(NO_FILTER, x, y);
41      }
42  
43      /**
44       * Construct a new Polygon2d.
45       * @param epsilon minimum distance between points to be considered different (these will <b>not</b> be filtered out)
46       * @param x the x coordinates of the points
47       * @param y the y coordinates of the points
48       * @throws NullPointerException when <code>x</code>, or <code>y</code> is <code>null</code>
49       * @throws IllegalArgumentException when any two successive points are equal, or when there are too few points, or when the
50       *             lengths of the coordinate arrays are not equal
51       */
52      public Polygon2d(final double epsilon, final double[] x, final double[] y)
53      {
54          super(epsilon, fixClosingPoint(Throw.whenNull(x, "x"), Throw.whenNull(y, "y")), fixClosingPoint(y, x));
55      }
56  
57      /**
58       * Ensure that the last pair of values in two arrays are not equal to the first pair. Remove the last pair if necessary.
59       * @param a the a array
60       * @param b the b array
61       * @return the <code>a</code> array (possibly a copy with the last element removed)
62       */
63      private static double[] fixClosingPoint(final double[] a, final double[] b)
64      {
65          if (a.length > 1 && b.length == a.length && a[0] == a[a.length - 1] && b[0] == b[a.length - 1])
66          {
67              return Arrays.copyOf(a, a.length - 1);
68          }
69          return a;
70      }
71  
72      /**
73       * Construct a new Polygon2d.
74       * @param points array of Point2d objects.
75       * @throws NullPointerException when <code>points</code> is <code>null</code>
76       * @throws IllegalArgumentException when <code>points</code> is too short, or contains successive duplicate points
77       */
78      public Polygon2d(final Point2d[] points)
79      {
80          this(NO_FILTER, points);
81      }
82  
83      /**
84       * Construct a new Polygon2d.
85       * @param epsilon minimum distance between points to be considered different (these will <b>not</b> be filtered out)
86       * @param points array of Point2d objects.
87       * @throws NullPointerException when <code>points</code> is <code>null</code>
88       * @throws IllegalArgumentException when <code>points</code> is too short, or contains successive duplicate points
89       */
90      public Polygon2d(final double epsilon, final Point2d[] points)
91      {
92          this(epsilon, PolyLine2d.makeArray(points, p -> p.x), PolyLine2d.makeArray(points, p -> p.y));
93      }
94  
95      /**
96       * Construct a new Polygon2d.
97       * @param point1 the first point of the new Polygon2d
98       * @param point2 the second point of the new Polygon2d
99       * @param otherPoints all remaining points of the new Polygon2d (may be <code>null</code>)
100      * @throws NullPointerException when <code>point1</code> or <code>point2</code> is <code>null</code>, or
101      *             <code>otherPoints</code> contains a <code>null</code> value
102      * @throws IllegalArgumentException when <code>point1</code> is equal to the last entry of <code>otherPoints</code>, or any
103      *             two successive points are equal
104      */
105     public Polygon2d(final Point2d point1, final Point2d point2, final Point2d... otherPoints)
106     {
107         this(NO_FILTER, point1, point2, otherPoints);
108     }
109 
110     /**
111      * Construct a new Polygon2d.
112      * @param epsilon minimum distance between points to be considered different (these will <b>not</b> be filtered out)
113      * @param point1 the first point of the new Polygon2d
114      * @param point2 the second point of the new Polygon2d
115      * @param otherPoints all remaining points of the new Polygon2d (may be <code>null</code>)
116      * @throws NullPointerException when <code>point1</code> or <code>point2</code> is <code>null</code>, or
117      *             <code>otherPoints</code> contains a <code>null</code> value
118      * @throws IllegalArgumentException when <code>point1</code> is equal to the last entry of <code>otherPoints</code>, or any
119      *             two successive points are equal
120      */
121     public Polygon2d(final double epsilon, final Point2d point1, final Point2d point2, final Point2d... otherPoints)
122     {
123         super(epsilon, Throw.whenNull(point1, "point1"), Throw.whenNull(point2, "point2"),
124                 fixClosingPoint(point1, otherPoints));
125     }
126 
127     /**
128      * Ensure that the last point of otherPoints is not equal to point1. Remove the last point if necessary.
129      * @param point1 the first point of a new Polygon2d
130      * @param otherPoints the remaining points of a new Polygon2d (may be <code>null</code>)
131      * @return <code>otherPoints</code> (possibly a copy thereof with the last entry removed)
132      */
133     private static Point2d[] fixClosingPoint(final Point2d point1, final Point2d[] otherPoints)
134     {
135         if (otherPoints == null || otherPoints.length == 0)
136         {
137             return otherPoints;
138         }
139         Point2d[] result = otherPoints;
140         Point2d lastPoint = result[result.length - 1];
141         if (point1.x == lastPoint.x && point1.y == lastPoint.y)
142         {
143             result = Arrays.copyOf(otherPoints, result.length - 1);
144             lastPoint = result[result.length - 1];
145         }
146         Throw.when(point1.x == lastPoint.x && point1.y == lastPoint.y, IllegalArgumentException.class,
147                 "Before last point and last point are at same location");
148         return result;
149     }
150 
151     /**
152      * Construct a new Polygon2d from a list of Point2d objects.
153      * @param points the list of points
154      * @throws NullPointerException when <code>points</code> is <code>null</code>, or contains a <code>null</code> value
155      * @throws IllegalArgumentException when <code>points</code> is too short, or the last two points are at the same location
156      */
157     public Polygon2d(final List<Point2d> points)
158     {
159         this(NO_FILTER, points);
160     }
161 
162     /**
163      * Construct a new Polygon2d from a list of Point2d objects.
164      * @param epsilon minimum distance between points to be considered different (these will <b>not</b> be filtered out)
165      * @param points the list of points
166      * @throws NullPointerException when <code>points</code> is <code>null</code>, or contains a <code>null</code> value
167      * @throws IllegalArgumentException when <code>points</code> is too short, or the last two points are at the same location
168      */
169     public Polygon2d(final double epsilon, final List<Point2d> points)
170     {
171         super(epsilon, fixClosingPoint(true, Throw.whenNull(points, "points")));
172     }
173 
174     /**
175      * Ensure that the last point in the list is different from the first point by possibly removing the last point.
176      * @param doNotModifyList if <code>true</code>; the list of points will not be modified (if the last point is to be
177      *            removed; the entire list up to the last point is duplicated)
178      * @param points the list of points
179      * @return the fixed list
180      * @throws NullPointerException when <code>points</code> is <code>null</code>
181      * @throws IllegalArgumentException when the (resulting) list is too short, or the before last and last point of points have
182      *             the same coordinates
183      */
184     private static List<Point2d> fixClosingPoint(final boolean doNotModifyList, final List<Point2d> points)
185     {
186         Throw.when(points.size() < 2, IllegalArgumentException.class, "Need at least two points");
187         Point2d firstPoint = points.get(0);
188         Point2d lastPoint = points.get(points.size() - 1);
189         List<Point2d> result = points;
190         if (firstPoint.x == lastPoint.x && firstPoint.y == lastPoint.y)
191         {
192             if (doNotModifyList)
193             {
194                 result = new ArrayList<>(points.size() - 1);
195                 for (int i = 0; i < points.size() - 1; i++)
196                 {
197                     result.add(points.get(i));
198                 }
199             }
200             else
201             {
202                 result.remove(points.size() - 1);
203             }
204             lastPoint = result.get(result.size() - 1);
205         }
206         Throw.when(firstPoint.x == lastPoint.x && firstPoint.y == lastPoint.y, IllegalArgumentException.class,
207                 "Before last point and last point are at same location");
208         return result;
209     }
210 
211     /**
212      * Construct a new Polygon2d from an iterator that yields Point2d.
213      * @param iterator the iterator
214      * @throws NullPointerException when <code>iterator</code> is <code>null</code>, or the iterator returns a <code>null</code>
215      *             value
216      * @throws IllegalArgumentException when the <code>iterator</code> yields too few points, or the before last and last point
217      *             have the same coordinates
218      */
219     public Polygon2d(final Iterator<Point2d> iterator)
220     {
221         this(NO_FILTER, iterator);
222     }
223 
224     /**
225      * Construct a new Polygon2d from an iterator that yields Point2d.
226      * @param epsilon minimum distance between points to be considered different (these will <b>not</b> be filtered out)
227      * @param iterator the iterator
228      * @throws NullPointerException when <code>iterator</code> is <code>null</code>, or the iterator returns a <code>null</code>
229      *             value
230      * @throws IllegalArgumentException when the <code>iterator</code> yields too few points, or the before last and last point
231      *             have the same coordinates
232      */
233     public Polygon2d(final double epsilon, final Iterator<Point2d> iterator)
234     {
235         this(epsilon, fixClosingPoint(false, iteratorToList(Throw.whenNull(iterator, "iterator"))));
236     }
237 
238     /**
239      * Construct a new Polygon2d from an existing one. This constructor is primarily intended for use in extending classes.
240      * @param polygon the existing Polygon2d
241      * @throws NullPointerException when <code>polygon</code> is <code>null</code>
242      */
243     public Polygon2d(final Polygon2d polygon)
244     {
245         super(polygon);
246     }
247 
248     /**
249      * Determine if this Polygon is convex. Returns bogus result for self-intersecting polygons. Derived from
250      * <a href="http://paulbourke.net/geometry/polygonmesh/source2.c">Convex by Paul Bourke</a>
251      * @return <code>true</code> if this <code>Polygon2d</code> is convex; <code>false</code> if this
252      *         <code>Polygon2d</code> is concave
253      */
254     public final boolean isConvex()
255     {
256         int flag = 0;
257         for (int i = 0; i < size(); i++)
258         {
259             int j = (i + 1) % size();
260             int k = (j + 1) % size();
261             double z = (getX(j) - getX(i)) * (getY(k) - getY(j)) - (getY(j) - getY(i)) * (getX(k) - getX(j));
262             if (z < 0)
263             {
264                 flag |= 1;
265             }
266             else if (z > 0)
267             {
268                 flag |= 2;
269             }
270             if (flag == 3)
271             {
272                 return false;
273             }
274         }
275         return flag != 0;
276     }
277 
278     /**
279      * Determine if a point is inside this Polygon. Returns bogus results for self-intersecting polygons.
280      * @param point the point
281      * @return <code>true</code> if the point is inside this <code>Polygon2d</code>, <code>false</code> if the point is
282      *         outside this <code>Polygon2d</code>. Results are ill-defined for points on the edges of this
283      *         <code>Polygon2d</code>.
284      */
285     public boolean contains(final Point2d point)
286     {
287         return contains(point.x, point.y);
288     }
289 
290     /**
291      * Determine if a point is inside this Polygon. Returns bogus results for self-intersecting polygons. Derived from
292      * <a href="http://paulbourke.net/geometry/polygonmesh/">Polygons and meshes by Paul Bourke</a>
293      * @param x the x-coordinate of the point
294      * @param y the y-coordinate of the point
295      * @return <code>true</code> if the point is inside this <code>Polygon2d</code>, <code>false</code> if the point is
296      *         outside this <code>Polygon2d</code>. Results are ill-defined for points on the edges of this
297      *         <code>Polygon2d</code>.
298      */
299     public boolean contains(final double x, final double y)
300     {
301         if (!getBounds().contains(x, y))
302         {
303             return false;
304         }
305         int counter = 0;
306         // Unlike Paul Bourke, we initialize prevPoint to the last point of the polygon (so we never have to wrap around)
307         double prevPointX = getX(size() - 1);
308         double prevPointY = getY(size() - 1);
309         for (int i = 0; i < size(); i++)
310         {
311             double curPointX = getX(i);
312             double curPointY = getY(i);
313             // Combined 4 if statements into one; I trust that the java compiler will short-circuit this nicely
314             if (y > Math.min(prevPointY, curPointY) && y <= Math.max(prevPointY, curPointY)
315                     && x <= Math.max(prevPointX, curPointX) && prevPointY != curPointY)
316             {
317                 double xIntersection = (y - prevPointY) * (curPointX - prevPointX) / (curPointY - prevPointY) + prevPointX;
318                 if (prevPointX == curPointX || x <= xIntersection)
319                 {
320                     counter++;
321                 }
322             }
323             prevPointX = curPointX;
324             prevPointY = curPointY;
325         }
326         return counter % 2 != 0;
327     }
328 
329     /**
330      * Determine if this Polygon completely contains a Bounds2d object. If this Polygon self-intersects, the results is bogus.
331      * @param bounds the Bounds2d object
332      * @return <code>true</code> if the <code>Bounds2d</code> object is completely contained in this
333      *         <code>Polygon2d</code>; <code>false</code> if any part (or all) of the Bounds2d object is outside this
334      *         <code>Polygon2d</code>. If the Bounds2d object touches this <code>Polygon2d</code> the results are ill-defined.
335      */
336     public boolean contains(final Bounds2d bounds)
337     {
338         // Step 1: quick check to see if the bounds intersect
339         if (getBounds().disjoint(bounds))
340         {
341             return false;
342         }
343         // This is a quick hack
344         return toPath2D().contains(bounds.getMinX(), bounds.getMinY(), bounds.getDeltaX(), bounds.getDeltaY());
345     }
346 
347     /**
348      * Determine if this Polygon2d intersects another Polygon2d.
349      * @param other the other Polygon2d
350      * @return <code>true</code> if the polygons intersect; <code>false</code> if the polygons are disjunct.
351      *         Ill-defined if the polygons touch.
352      */
353     public boolean intersects(final Polygon2d other)
354     {
355         // step 1: quick check to see if the bounds intersect
356         if (!getBounds().intersects(other.getBounds()))
357         {
358             return false;
359         }
360 
361         // step 2: quick check to see if any of the points of this polygon lies inside the other polygon
362         for (Iterator<Point2d> iterator = iterator(); iterator.hasNext();)
363         {
364             if (other.contains(iterator.next()))
365             {
366                 return true;
367             }
368         }
369 
370         // step 3: quick check to see if any of the points of the other polygon lies inside this polygon
371         for (Iterator<Point2d> iterator = other.iterator(); iterator.hasNext();)
372         {
373             if (contains(iterator.next()))
374             {
375                 return true;
376             }
377         }
378 
379         // step 4: see if any of the lines of shape 1 and shape 2 intersect (expensive!)
380         for (int i = 0; i < this.size(); i++)
381         {
382             LineSegment2d ourSegment = getSegment(i);
383             for (int j = 0; j < other.size(); j++)
384             {
385                 LineSegment2d otherSegment = other.getSegment(j);
386                 Point2d intersection = Point2d.intersectionOfLineSegments(ourSegment, otherSegment);
387                 if (intersection != null)
388                 {
389                     double p1x = ourSegment.startX, p1y = ourSegment.startY;
390                     double d1x = ourSegment.endX - p1x, d1y = ourSegment.endY - p1y;
391                     double p2x = otherSegment.startX, p2y = otherSegment.startY;
392                     double d2x = otherSegment.endX - p2x, d2y = otherSegment.endY - p2y;
393 
394                     double det = d2x * d1y - d2y * d1x;
395                     if (det != 0)
396                     {
397                         double z = (d2x * (p2y - p1y) + d2y * (p1x - p2x)) / det;
398                         if (Math.abs(z) < 10.0 * Math.ulp(1.0) || Math.abs(z - 1.0) > 10.0 * Math.ulp(1.0))
399                         {
400                             return true; // intersection at end point
401                         }
402                     }
403                 }
404             }
405         }
406         return false;
407     }
408 
409     /**
410      * Compute the surface of this Polygon2d. Sign of the result reflects the winding-ness of this this Polygon2d. If this
411      * Polygon2d self-intersects, the result is bogus.
412      * @return the surface of this <code>Polygon2d</code>
413      */
414     public double surface()
415     {
416         // Translate all coordinates to the median of the bounding box to minimize rounding errors
417         Point2d midPoint = getBounds().midPoint();
418         double result = 0;
419         // We initialize previous point to the last point of this Polygon2d to avoid wrapping problems
420         double prevX = getX(size() - 1) - midPoint.x;
421         double prevY = getY(size() - 1) - midPoint.y;
422         for (int i = 0; i < size(); i++)
423         {
424             double thisX = getX(i) - midPoint.x;
425             double thisY = getY(i) - midPoint.y;
426             result += prevX * thisY - thisX * prevY;
427             prevX = thisX;
428             prevY = thisY;
429         }
430         return result / 2;
431     }
432 
433     @Override
434     public double getLength()
435     {
436         // Length of a polygon is computed by taking the length of the PolyLine and adding the length of the closing segment
437         return super.getLength() + Math.hypot(getX(size() - 1) - getX(0), getY(size() - 1) - getY(0));
438     }
439 
440     @Override
441     public LineSegment2d getSegment(final int index)
442     {
443         if (index < size() - 1)
444         {
445             return super.getSegment(index);
446         }
447         Throw.when(index != size() - 1, IndexOutOfBoundsException.class, "index must be in range [0..size() - 1] (got %d)",
448                 index);
449         return new LineSegment2d(getX(index), getY(index), getX(0), getY(0));
450     }
451 
452     @Override
453     public Polygon2d reverse()
454     {
455         return new Polygon2d(super.reverse().iterator());
456     }
457 
458     /**
459      * Construct a Path2D from this PolyLine2d. The result is NOT cached (in the current implementation).
460      * @return newly construct Path2D consisting solely of straight segments.
461      */
462     @Override
463     public Path2D toPath2D()
464     {
465         Path2D.Double result = (Double) super.toPath2D();
466         result.closePath();
467         return result;
468     }
469 
470     @Override
471     public final String toString()
472     {
473         return toString("%f", false);
474     }
475 
476     @Override
477     public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
478     {
479         StringBuilder result = new StringBuilder();
480         if (!doNotIncludeClassName)
481         {
482             result.append("Polygon2d ");
483         }
484         result.append("[super=");
485         result.append(super.toString(doubleFormat, false));
486         result.append("]");
487         return result.toString();
488     }
489 
490 }