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