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