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