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 }