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-2023 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<Point2d>; 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<Point2d>; the list of points
141 * @return List<Point2d>; 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<Point2d>; 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 < 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<Point2d>; 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 < 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 }