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   
8   import org.djutils.draw.InvalidProjectionException;
9   import org.djutils.draw.point.Point3d;
10  import org.djutils.exceptions.Throw;
11  
12  /**
13   * Closed PolyLine3d. The actual closing point (which is the same as the starting point) is NOT included in the super
14   * PolyLine3d. The constructors automatically remove the last point if it is a at the same location as the first point.
15   * <p>
16   * Copyright (c) 2020-2025 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
17   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
18   * </p>
19   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
20   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
21   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
22   */
23  public class Polygon3d extends PolyLine3d
24  {
25      /** */
26      private static final long serialVersionUID = 20209999L;
27  
28      /**
29       * Construct a new Polygon3d.
30       * @param x the x coordinates of the points
31       * @param y the y coordinates of the points
32       * @param z the z coordinates of the points
33       * @throws IllegalArgumentException when any two successive points are equal, or when there are too few points
34       */
35      public Polygon3d(final double[] x, final double[] y, final double[] z)
36      {
37          this(NO_FILTER, x, y, z);
38      }
39  
40      /**
41       * Construct a new Polygon3d.
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       * @param z the z coordinates of the points
46       * @throws IllegalArgumentException when any two successive points are equal, or when there are too few points
47       */
48      public Polygon3d(final double epsilon, final double[] x, final double[] y, final double[] z)
49      {
50          super(epsilon, fixClosingPoint(Throw.whenNull(x, "x"), Throw.whenNull(y, "y"), Throw.whenNull(z, "z")),
51                  fixClosingPoint(y, x, z), fixClosingPoint(z, x, y));
52      }
53  
54      /**
55       * Ensure that the last elements in three arrays are is not equal to the first. Remove the last element if necessary.
56       * @param a the a array
57       * @param b the b array
58       * @param c the c array
59       * @return the <code>a</code> array (possibly a copy with the last element removed)
60       */
61      static double[] fixClosingPoint(final double[] a, final double[] b, final double[] c)
62      {
63          if (a.length > 1 && b.length == a.length && c.length == a.length && a[0] == a[a.length - 1] && b[0] == b[a.length - 1]
64                  && c[0] == c[c.length - 1])
65          {
66              return Arrays.copyOf(a, a.length - 1);
67          }
68          return a;
69      }
70  
71      /**
72       * Construct a new Polygon3d.
73       * @param points array of Point3d objects.
74       * @throws NullPointerException when <code>points</code> is <code>null</code>
75       * @throws IllegalArgumentException when <code>points</code> is too short, or contains successive duplicate points
76       */
77      public Polygon3d(final Point3d[] points)
78      {
79          this(NO_FILTER, points);
80      }
81  
82      /**
83       * Construct a new Polygon3d.
84       * @param epsilon minimum distance between points to be considered different (these will <b>not</b> be filtered out)
85       * @param points array of Point3d objects.
86       * @throws NullPointerException when <code>points</code> is <code>null</code>
87       * @throws IllegalArgumentException when <code>points</code> is too short, or contains successive duplicate points
88       */
89      public Polygon3d(final double epsilon, final Point3d[] points)
90      {
91          this(epsilon, PolyLine3d.makeArray(Throw.whenNull(points, "points"), p -> p.x), PolyLine3d.makeArray(points, p -> p.y),
92                  PolyLine3d.makeArray(points, p -> p.z));
93      }
94  
95      /**
96       * Construct a new Polygon3d.
97       * @param point1 the first point of the new Polygon3d
98       * @param point2 the second point of the new Polygon3d
99       * @param otherPoints all remaining points of the new Polygon3d (may be null)
100      * @throws NullPointerException when <code>point1</code> or <code>point2</code> is <code>null</code>, or contains a
101      *             <code>null</code> value
102      * @throws IllegalArgumentException when <code>point1</code> is equal to the last point of <code>otherPoints</code>, or any
103      *             two successive points are equal
104      */
105     public Polygon3d(final Point3d point1, final Point3d point2, final Point3d... otherPoints)
106     {
107         this(NO_FILTER, point1, point2, otherPoints);
108     }
109 
110     /**
111      * Construct a new Polygon3d.
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 Polygon3d
114      * @param point2 the second point of the new Polygon3d
115      * @param otherPoints all remaining points of the new Polygon3d (may be null)
116      * @throws NullPointerException when <code>point1</code> or <code>point2</code> is <code>null</code>, or contains a
117      *             <code>null</code> value
118      * @throws IllegalArgumentException when <code>point1</code> is equal to the last point of <code>otherPoints</code>, or any
119      *             two successive points are equal
120      */
121     public Polygon3d(final double epsilon, final Point3d point1, final Point3d point2, final Point3d... 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 Polygon3d
130      * @param otherPoints the remaining points of a new Polygon3d (may be null)
131      * @return <code>otherPoints</code> (possibly a copy thereof with the last entry removed)
132      */
133     private static Point3d[] fixClosingPoint(final Point3d point1, final Point3d[] otherPoints)
134     {
135         if (otherPoints == null || otherPoints.length == 0)
136         {
137             return otherPoints;
138         }
139         Point3d[] result = otherPoints;
140         Point3d 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 Polygon3d from a list of Point3d objects.
153      * @param points the list of points
154      * @throws NullPointerException when <code>points</code> is <code>null</code>
155      * @throws IllegalArgumentException when <code>points</code> is too short, or the last two points are at the same location
156      */
157     public Polygon3d(final List<Point3d> points)
158     {
159         this(NO_FILTER, points);
160     }
161 
162     /**
163      * Construct a new Polygon3d from a list of Point3d 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>
167      * @throws IllegalArgumentException when <code>points</code> is too short, or the last two points are at the same location
168      */
169     public Polygon3d(final double epsilon, final List<Point3d> 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 removed;
177      *            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<Point3d> fixClosingPoint(final boolean doNotModifyList, final List<Point3d> points)
185     {
186         Throw.when(points.size() < 2, IllegalArgumentException.class, "Need at least two points");
187         Point3d firstPoint = points.get(0);
188         Point3d lastPoint = points.get(points.size() - 1);
189         List<Point3d> result = points;
190         if (firstPoint.x == lastPoint.x && firstPoint.y == lastPoint.y && firstPoint.z == lastPoint.z)
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 && firstPoint.z == lastPoint.z,
207                 IllegalArgumentException.class, "Before last point and last point are at same location");
208         return result;
209     }
210 
211     /**
212      * Construct a new Polygon3d from an iterator that yields Point3d.
213      * @param iterator the iterator
214      */
215     public Polygon3d(final Iterator<Point3d> iterator)
216     {
217         this(NO_FILTER, iterator);
218     }
219 
220     /**
221      * Construct a new Polygon3d from an iterator that yields Point3d.
222      * @param epsilon minimum distance between points to be considered different (these will <b>not</b> be filtered out)
223      * @param iterator the iterator
224      */
225     public Polygon3d(final double epsilon, final Iterator<Point3d> iterator)
226     {
227         this(epsilon, fixClosingPoint(false, iteratorToList(Throw.whenNull(iterator, "iterator"))));
228     }
229 
230     /**
231      * Construct a new Polygon3d from an existing one. This constructor is primarily intended for use in extending classes.
232      * @param polygon the existing Polygon3d
233      * @throws NullPointerException when <code>polygon</code> is <code>null</code>
234      */
235     public Polygon3d(final Polygon3d polygon)
236     {
237         super(polygon);
238     }
239 
240     @Override
241     public double getLength()
242     {
243         // Length a polygon is computed by taking the length of the PolyLine and adding the length of the closing segment
244         return super.getLength()
245                 + Math.hypot(Math.hypot(getX(size() - 1) - getX(0), getY(size() - 1) - getY(0)), getZ(size() - 1) - getZ(0));
246     }
247 
248     @Override
249     public LineSegment3d getSegment(final int index)
250     {
251         if (index < size() - 1)
252         {
253             return super.getSegment(index);
254         }
255         Throw.when(index != size() - 1, IndexOutOfBoundsException.class, "index must be in range [0, .size() - 1] (got %d)",
256                 index);
257         return new LineSegment3d(getX(index), getY(index), getZ(index), getX(0), getY(0), getZ(0));
258     }
259 
260     @Override
261     public Polygon2d project() throws InvalidProjectionException
262     {
263         double[] projectedX = new double[this.size()];
264         double[] projectedY = new double[this.size()];
265 
266         int nextIndex = 0;
267         for (int i = 0; i < this.size(); i++)
268         {
269             if (i > 0 && getX(i) == getX(i - 1) && getY(i) == getY(i - 1))
270             {
271                 continue;
272             }
273             projectedX[nextIndex] = getX(i);
274             projectedY[nextIndex] = getY(i);
275             nextIndex++;
276         }
277         Throw.when(nextIndex < 2, InvalidProjectionException.class,
278                 "projection yielded too few points to construct a valid Polygon2d");
279         if (nextIndex < projectedX.length)
280         {
281             return new Polygon2d(Arrays.copyOf(projectedX, nextIndex), Arrays.copyOf(projectedY, nextIndex));
282         }
283         return new Polygon2d(projectedX, projectedY);
284     }
285 
286     @Override
287     public Polygon3d reverse()
288     {
289         return new Polygon3d(super.reverse().iterator());
290     }
291 
292     @Override
293     public final String toString()
294     {
295         return toString("%f", false);
296     }
297 
298     @Override
299     public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
300     {
301         StringBuilder result = new StringBuilder();
302         if (!doNotIncludeClassName)
303         {
304             result.append("Polygon3d ");
305         }
306         result.append("[super=");
307         result.append(super.toString(doubleFormat, false));
308         result.append("]");
309         return result.toString();
310     }
311 
312 }