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