View Javadoc
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&lt;Point2d&gt;; 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&lt;Point2d&gt;; the list of points
138      * @return List&lt;Point2d&gt;; 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&lt;Point2d&gt;; 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 &lt; 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&lt;Point2d&gt;; 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 &lt; 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 }