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 (!getAbsoluteBounds().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 (getAbsoluteBounds().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 (!getAbsoluteBounds().intersects(other.getAbsoluteBounds()))
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 = getAbsoluteBounds().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 }