1 package org.djutils.draw.line;
2
3 import java.util.ArrayList;
4 import java.util.Arrays;
5 import java.util.Iterator;
6 import java.util.List;
7 import java.util.Locale;
8
9 import org.djutils.draw.DrawRuntimeException;
10 import org.djutils.draw.point.Point2d;
11 import org.djutils.exceptions.Throw;
12
13 /**
14 * Polygon2d.java. Closed PolyLine2d. The actual closing point (which is the same as the starting point) is NOT included in the
15 * super PolyLine2d. The constructors automatically remove the last point if it is a at the same location as the first point.
16 * <p>
17 * Copyright (c) 2020-2021 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
18 * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
19 * </p>
20 * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
21 * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
22 */
23 public class Polygon2d extends PolyLine2d
24 {
25 /** */
26 private static final long serialVersionUID = 20209999L;
27
28 /**
29 * Construct a new Polygon2d.
30 * @param x double[]; the x coordinates of the points
31 * @param y double[]; the y coordinates of the points
32 * @throws DrawRuntimeException when any two successive points are equal, or when there are too few points
33 */
34 public Polygon2d(final double[] x, final double[] y) throws DrawRuntimeException
35 {
36 super(fixClosingPointX(Throw.whenNull(x, "x may not be null"), Throw.whenNull(y, "y may not be null")),
37 fixClosingPointY(x, y));
38 }
39
40 /**
41 * Ensure that the last point is not equal to the first. Remove the last point if necessary.
42 * @param x double[]; the x coordinates of the points
43 * @param y double[]; the y coordinates of the points
44 * @return double[]; the x coordinates of the points (possibly a copy with the last element removed)
45 */
46 private static double[] fixClosingPointX(final double[] x, final double[] y)
47 {
48 if (x.length > 1 && y.length == x.length && x[0] == x[x.length - 1] && y[0] == y[x.length - 1])
49 {
50 return Arrays.copyOf(x, x.length - 1);
51 }
52 return x;
53 }
54
55 /**
56 * Ensure that the last point is not equal to the first. Remove the last point if necessary.
57 * @param x double[]; the x coordinates of the points
58 * @param y double[]; the y coordinates of the points
59 * @return double[]; the y coordinates of the points (possibly a copy with the last element removed)
60 */
61 private static double[] fixClosingPointY(final double[] x, final double[] y)
62 {
63 if (x.length > 1 && y.length == x.length && x[0] == x[x.length - 1] && y[0] == y[x.length - 1])
64 {
65 return Arrays.copyOf(y, x.length - 1);
66 }
67 return y;
68 }
69
70 /**
71 * Construct a new Polygon2d.
72 * @param points Point2d[]; array of Point2d objects.
73 * @throws NullPointerException when points is null
74 * @throws DrawRuntimeException when points is too short, or contains successive duplicate points
75 */
76 public Polygon2d(final Point2d[] points) throws NullPointerException, DrawRuntimeException
77 {
78 this(PolyLine2d.makeArray(points, p -> p.x), PolyLine2d.makeArray(points, p -> p.y));
79 }
80
81 /**
82 * Construct a new Polygon2d.
83 * @param point1 Point2d; the first point of the new Polygon2d
84 * @param point2 Point2d; the second point of the new Polygon2d
85 * @param otherPoints Point2d[]; all remaining points of the new Polygon2d (may be null)
86 * @throws NullPointerException when point1 or point2 is null
87 * @throws DrawRuntimeException when point1 is equal to the last point of otherPoints, or any two successive points are
88 * equal
89 */
90 public Polygon2d(final Point2d.html#Point2d">Point2d point1, final Point2d point2, final Point2d... otherPoints)
91 throws NullPointerException, DrawRuntimeException
92 {
93 super(Throw.whenNull(point1, "point1 may not be null"), Throw.whenNull(point2, "point2 may not be null"),
94 fixClosingPoint(point1, otherPoints));
95 }
96
97 /**
98 * Ensure that the last point of otherPoints is not equal to point1. Remove the last point if necessary.
99 * @param point1 Point2d; the first point of a new Polygon2d
100 * @param otherPoints Point2d[]; the remaining points of a new Polygon2d (may be null)
101 * @return Point2d[]; otherPoints (possibly a copy thereof with the last entry removed)
102 */
103 private static Point2d.html#Point2d">Point2dt2d">Point2d[] fixClosingPoint(final Point2d.html#Point2d">Point2d point1, final Point2d[] otherPoints)
104 {
105 if (otherPoints == null || otherPoints.length == 0)
106 {
107 return otherPoints;
108 }
109 Point2d[] result = otherPoints;
110 Point2d lastPoint = result[result.length - 1];
111 if (point1.x == lastPoint.x && point1.y == lastPoint.y)
112 {
113 result = Arrays.copyOf(otherPoints, result.length - 1);
114 lastPoint = result[result.length - 1];
115 }
116 Throw.when(point1.x == lastPoint.x && point1.y == lastPoint.y, DrawRuntimeException.class,
117 "Before last point and last point are at same location");
118 return result;
119 }
120
121 /**
122 * Construct a new Polygon2d from a list of Point2d objects.
123 * @param points List<Point2d>; the list of points
124 * @throws NullPointerException when points is null
125 * @throws DrawRuntimeException when points is too short, or the last two points are at the same location
126 */
127 public Polygon2d(final List<Point2d> points) throws NullPointerException, DrawRuntimeException
128 {
129 super(fixClosingPoint(true, Throw.whenNull(points, "points may not be null")));
130
131 }
132
133 /**
134 * Ensure that the last point in the list is different from the first point by possibly removing the last point.
135 * @param doNotModifyList boolean; if true; the list of points will not be modified (if the last point is to be removed; the
136 * entire list up to the last point is duplicated)
137 * @param points List<Point2d>; the list of points
138 * @return List<Point2d>; the fixed list
139 * @throws DrawRuntimeException when the (resulting) list is too short, or the before last and last point of points have the
140 * same coordinates
141 */
142 private static List<Point2d> fixClosingPoint(final boolean doNotModifyList, final List<Point2d> points)
143 throws DrawRuntimeException
144 {
145 Throw.when(points.size() < 2, DrawRuntimeException.class, "Need at least two points");
146 Point2d firstPoint = points.get(0);
147 Point2d lastPoint = points.get(points.size() - 1);
148 List<Point2d> result = points;
149 if (firstPoint.x == lastPoint.x && firstPoint.y == lastPoint.y)
150 {
151 if (doNotModifyList)
152 {
153 result = new ArrayList<>(points.size() - 1);
154 for (int i = 0; i < points.size() - 1; i++)
155 {
156 result.add(points.get(i));
157 }
158 }
159 else
160 {
161 result.remove(points.size() - 1);
162 }
163 lastPoint = result.get(result.size() - 1);
164 }
165 Throw.when(firstPoint.x == lastPoint.x && firstPoint.y == lastPoint.y, DrawRuntimeException.class,
166 "Before last point and last point are at same location");
167 return result;
168 }
169
170 /**
171 * Construct a new Polygon2d from an iterator that yields Point2d.
172 * @param iterator Iterator<Point2d>; the iterator
173 */
174 public Polygon2d(final Iterator<Point2d> iterator)
175 {
176 this(fixClosingPoint(false, iteratorToList(Throw.whenNull(iterator, "iterator cannot be null"))));
177 }
178
179 /**
180 * Create a new Polygon2d, optionally filtering out repeating successive points.
181 * @param filterDuplicates boolean; if true; filter out successive repeated points; otherwise do not filter
182 * @param points Point2d...; the coordinates of the polygon as Point2d
183 * @throws DrawRuntimeException when number of points < 2
184 */
185 public Polygon2d(final boolean filterDuplicates, final Point2d... points) throws DrawRuntimeException
186 {
187 this(PolyLine2d.cleanPoints(filterDuplicates, Arrays.stream(points).iterator()));
188 }
189
190 /**
191 * Create a new Polygon2d, optionally filtering out repeating successive points.
192 * @param filterDuplicates boolean; if true; filter out successive repeated points; otherwise do not filter
193 * @param pointList List<Point2d>; list of the coordinates of the line as Point3d; any duplicate points in this list
194 * are removed (this method may modify the provided list)
195 * @throws DrawRuntimeException when number of non-equal points < 2
196 */
197 public Polygon2d(final boolean filterDuplicates, final List<Point2d> pointList) throws DrawRuntimeException
198 {
199 this(PolyLine2d.cleanPoints(filterDuplicates, pointList.iterator()));
200 }
201
202 /**
203 * Determine if this Polygon2d is convex. Returns bogus result for self-intersecting polygons. Derived from
204 * http://paulbourke.net/geometry/polygonmesh/source2.c
205 * @return boolean; true if this Polygon2d is convex; false if this Polygon2d is concave
206 */
207 public final boolean isConvex()
208 {
209 int flag = 0;
210 for (int i = 0; i < size(); i++)
211 {
212 int j = (i + 1) % size();
213 int k = (j + 1) % size();
214 double z = (getX(j) - getX(i)) * (getY(k) - getY(j)) - (getY(j) - getY(i)) * (getX(k) - getX(j));
215 if (z < 0)
216 {
217 flag |= 1;
218 }
219 else if (z > 0)
220 {
221 flag |= 2;
222 }
223 if (flag == 3)
224 {
225 return false;
226 }
227 }
228 return flag != 0;
229 }
230
231 /**
232 * Determine if a point is inside this Polygon. Returns bogus results for self-intersecting polygons.
233 * @param point Point2d; the point
234 * @return boolean; true if the point is inside this polygon, false if the point is outside this polygon. Results are
235 * ill-defined for points on the edges of this Polygon.
236 */
237 public boolean contains(final Point2d point)
238 {
239 return contains(point.x, point.y);
240 }
241
242 /**
243 * Determine if a point is inside this Polygon. Returns bogus results for self-intersecting polygons. Derived from
244 * http://paulbourke.net/geometry/polygonmesh/
245 * @param x double; the x-coordinate of the point
246 * @param y double; the y-coordinate of the point
247 * @return boolean; true if the point is inside this polygon, false if the point is outside this polygon. Results are
248 * ill-defined for points on the edges of this Polygon.
249 */
250 public boolean contains(final double x, final double y)
251 {
252 if (!getBounds().contains(x, y))
253 {
254 return false;
255 }
256 int counter = 0;
257 // Unlike Paul Bourke, we initialize prevPoint to the last point of the polygon (so we never have to wrap around)
258 double prevPointX = getX(size() - 1);
259 double prevPointY = getY(size() - 1);
260 for (int i = 0; i < size(); i++)
261 {
262 double curPointX = getX(i);
263 double curPointY = getY(i);
264 // Combined 4 if statements into one; I trust that the java compiler will short-circuit this nicely
265 if (y > Math.min(prevPointY, curPointY) && y <= Math.max(prevPointY, curPointY)
266 && x <= Math.max(prevPointX, curPointX) && prevPointY != curPointY)
267 {
268 double xIntersection = (y - prevPointY) * (curPointX - prevPointX) / (curPointY - prevPointY) + prevPointX;
269 if (prevPointX == curPointX || x <= xIntersection)
270 {
271 counter++;
272 }
273 }
274 prevPointX = curPointX;
275 prevPointY = curPointY;
276 }
277 return counter % 2 != 0;
278 }
279
280 /**
281 * Compute the surface of this Polygon2d. Sign of the result reflects the winding-ness of this this Polygon2d. If this
282 * Polygon2d self-intersects, the result is bogus.
283 * @return double; the surface of this Polygon2d
284 */
285 public double surface()
286 {
287 // TODO scale all coordinates back to something local to reduce rounding errors
288 double result = 0;
289 // We initialize previous point to the last point of this Polygon2d to avoid wrapping problems
290 double prevX = getX(size() - 1);
291 double prevY = getY(size() - 1);
292 for (int i = 0; i < size(); i++)
293 {
294 double thisX = getX(i);
295 double thisY = getY(i);
296 result += prevX * thisY - thisX * prevY;
297 prevX = thisX;
298 prevY = thisY;
299 }
300 return result / 2;
301 }
302
303 /** {@inheritDoc} */
304 @Override
305 public double getLength()
306 {
307 // Length a polygon is computed by taking the length of the PolyLine and adding the length of the closing segment
308 return super.getLength() + Math.hypot(getX(size() - 1) - getX(0), getY(size() - 1) - getY(0));
309 }
310
311 /** {@inheritDoc} */
312 @Override
313 public LineSegment2d getSegment(final int index)
314 {
315 if (index < size() - 1)
316 {
317 return super.getSegment(index);
318 }
319 Throw.when(index != size() - 1, DrawRuntimeException.class, "index must be in range 0..size() - 1");
320 return new LineSegment2d(getX(index), getY(index), getX(0), getY(0));
321 }
322
323 /** {@inheritDoc} */
324 @Override
325 public Polygon2d reverse()
326 {
327 return new Polygon2d(super.reverse().getPoints());
328 }
329
330 /** {@inheritDoc} */
331 @Override
332 public final String toExcel()
333 {
334 StringBuffer s = new StringBuffer();
335 for (int i = 0; i < size(); i++)
336 {
337 s.append(getX(i) + "\t" + getY(i) + "\n");
338 }
339 s.append(getX(0) + "\t" + getY(0) + "\n");
340 return s.toString();
341 }
342
343 /** {@inheritDoc} */
344 @Override
345 public final String toPlot()
346 {
347 StringBuffer result = new StringBuffer();
348 for (int i = 0; i < size(); i++)
349 {
350 result.append(String.format(Locale.US, "%s%.3f,%.3f", 0 == result.length() ? "M" : " L", getX(i), getY(i)));
351 }
352 result.append(String.format(Locale.US, " L%.3f,%.3f", getX(0), getY(0)));
353 result.append("\n");
354 return result.toString();
355 }
356
357 /** {@inheritDoc} */
358 @Override
359 public final String toString()
360 {
361 return toString("%f", false);
362 }
363
364 /** {@inheritDoc} */
365 @Override
366 public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
367 {
368 StringBuilder result = new StringBuilder();
369 if (!doNotIncludeClassName)
370 {
371 result.append("Polygon2d ");
372 }
373 result.append("[super=");
374 result.append(super.toString(doubleFormat, false));
375 result.append("]");
376 return result.toString();
377 }
378
379 }