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       * Construct a new Polygon3d.
27       * @param x the x coordinates of the points
28       * @param y the y coordinates of the points
29       * @param z the z coordinates of the points
30       * @throws IllegalArgumentException when any two successive points are equal, or when there are too few points
31       */
32      public Polygon3d(final double[] x, final double[] y, final double[] z)
33      {
34          this(NO_FILTER, x, y, z);
35      }
36  
37      /**
38       * Construct a new Polygon3d.
39       * @param epsilon minimum distance between points to be considered different (these will <b>not</b> be filtered out)
40       * @param x the x coordinates of the points
41       * @param y the y coordinates of the points
42       * @param z the z coordinates of the points
43       * @throws IllegalArgumentException when any two successive points are equal, or when there are too few points
44       */
45      public Polygon3d(final double epsilon, final double[] x, final double[] y, final double[] z)
46      {
47          super(epsilon, fixClosingPoint(Throw.whenNull(x, "x"), Throw.whenNull(y, "y"), Throw.whenNull(z, "z")),
48                  fixClosingPoint(y, x, z), fixClosingPoint(z, x, y));
49      }
50  
51      /**
52       * Ensure that the last elements in three arrays are is not equal to the first. Remove the last element if necessary.
53       * @param a the a array
54       * @param b the b array
55       * @param c the c array
56       * @return the <code>a</code> array (possibly a copy with the last element removed)
57       */
58      static double[] fixClosingPoint(final double[] a, final double[] b, final double[] c)
59      {
60          if (a.length > 1 && b.length == a.length && c.length == a.length && a[0] == a[a.length - 1] && b[0] == b[a.length - 1]
61                  && c[0] == c[c.length - 1])
62          {
63              return Arrays.copyOf(a, a.length - 1);
64          }
65          return a;
66      }
67  
68      /**
69       * Construct a new Polygon3d.
70       * @param points array of Point3d objects.
71       * @throws NullPointerException when <code>points</code> is <code>null</code>
72       * @throws IllegalArgumentException when <code>points</code> is too short, or contains successive duplicate points
73       */
74      public Polygon3d(final Point3d[] points)
75      {
76          this(NO_FILTER, points);
77      }
78  
79      /**
80       * Construct a new Polygon3d.
81       * @param epsilon minimum distance between points to be considered different (these will <b>not</b> be filtered out)
82       * @param points array of Point3d objects.
83       * @throws NullPointerException when <code>points</code> is <code>null</code>
84       * @throws IllegalArgumentException when <code>points</code> is too short, or contains successive duplicate points
85       */
86      public Polygon3d(final double epsilon, final Point3d[] points)
87      {
88          this(epsilon, PolyLine3d.makeArray(Throw.whenNull(points, "points"), p -> p.x), PolyLine3d.makeArray(points, p -> p.y),
89                  PolyLine3d.makeArray(points, p -> p.z));
90      }
91  
92      /**
93       * Construct a new Polygon3d.
94       * @param point1 the first point of the new Polygon3d
95       * @param point2 the second point of the new Polygon3d
96       * @param otherPoints all remaining points of the new Polygon3d (may be null)
97       * @throws NullPointerException when <code>point1</code> or <code>point2</code> is <code>null</code>, or contains a
98       *             <code>null</code> value
99       * @throws IllegalArgumentException when <code>point1</code> is equal to the last point of <code>otherPoints</code>, or any
100      *             two successive points are equal
101      */
102     public Polygon3d(final Point3d point1, final Point3d point2, final Point3d... otherPoints)
103     {
104         this(NO_FILTER, point1, point2, otherPoints);
105     }
106 
107     /**
108      * Construct a new Polygon3d.
109      * @param epsilon minimum distance between points to be considered different (these will <b>not</b> be filtered out)
110      * @param point1 the first point of the new Polygon3d
111      * @param point2 the second point of the new Polygon3d
112      * @param otherPoints all remaining points of the new Polygon3d (may be null)
113      * @throws NullPointerException when <code>point1</code> or <code>point2</code> is <code>null</code>, or contains a
114      *             <code>null</code> value
115      * @throws IllegalArgumentException when <code>point1</code> is equal to the last point of <code>otherPoints</code>, or any
116      *             two successive points are equal
117      */
118     public Polygon3d(final double epsilon, final Point3d point1, final Point3d point2, final Point3d... otherPoints)
119     {
120         super(epsilon, Throw.whenNull(point1, "point1"), Throw.whenNull(point2, "point2"),
121                 fixClosingPoint(point1, otherPoints));
122     }
123 
124     /**
125      * Ensure that the last point of otherPoints is not equal to point1. Remove the last point if necessary.
126      * @param point1 the first point of a new Polygon3d
127      * @param otherPoints the remaining points of a new Polygon3d (may be null)
128      * @return <code>otherPoints</code> (possibly a copy thereof with the last entry removed)
129      */
130     private static Point3d[] fixClosingPoint(final Point3d point1, final Point3d[] otherPoints)
131     {
132         if (otherPoints == null || otherPoints.length == 0)
133         {
134             return otherPoints;
135         }
136         Point3d[] result = otherPoints;
137         Point3d lastPoint = result[result.length - 1];
138         if (point1.x == lastPoint.x && point1.y == lastPoint.y)
139         {
140             result = Arrays.copyOf(otherPoints, result.length - 1);
141             lastPoint = result[result.length - 1];
142         }
143         Throw.when(point1.x == lastPoint.x && point1.y == lastPoint.y, IllegalArgumentException.class,
144                 "Before last point and last point are at same location");
145         return result;
146     }
147 
148     /**
149      * Construct a new Polygon3d from a list of Point3d objects.
150      * @param points the list of points
151      * @throws NullPointerException when <code>points</code> is <code>null</code>
152      * @throws IllegalArgumentException when <code>points</code> is too short, or the last two points are at the same location
153      */
154     public Polygon3d(final List<Point3d> points)
155     {
156         this(NO_FILTER, points);
157     }
158 
159     /**
160      * Construct a new Polygon3d from a list of Point3d objects.
161      * @param epsilon minimum distance between points to be considered different (these will <b>not</b> be filtered out)
162      * @param points the list of points
163      * @throws NullPointerException when <code>points</code> is <code>null</code>
164      * @throws IllegalArgumentException when <code>points</code> is too short, or the last two points are at the same location
165      */
166     public Polygon3d(final double epsilon, final List<Point3d> points)
167     {
168         super(epsilon, fixClosingPoint(true, Throw.whenNull(points, "points")));
169     }
170 
171     /**
172      * Ensure that the last point in the list is different from the first point by possibly removing the last point.
173      * @param doNotModifyList if<code>true</code>; the list of points will not be modified (if the last point is to be removed;
174      *            the entire list up to the last point is duplicated)
175      * @param points the list of points
176      * @return the fixed list
177      * @throws NullPointerException when <code>points</code> is <code>null</code>
178      * @throws IllegalArgumentException when the (resulting) list is too short, or the before last and last point of points have
179      *             the same coordinates
180      */
181     private static List<Point3d> fixClosingPoint(final boolean doNotModifyList, final List<Point3d> points)
182     {
183         Throw.when(points.size() < 2, IllegalArgumentException.class, "Need at least two points");
184         Point3d firstPoint = points.get(0);
185         Point3d lastPoint = points.get(points.size() - 1);
186         List<Point3d> result = points;
187         if (firstPoint.x == lastPoint.x && firstPoint.y == lastPoint.y && firstPoint.z == lastPoint.z)
188         {
189             if (doNotModifyList)
190             {
191                 result = new ArrayList<>(points.size() - 1);
192                 for (int i = 0; i < points.size() - 1; i++)
193                 {
194                     result.add(points.get(i));
195                 }
196             }
197             else
198             {
199                 result.remove(points.size() - 1);
200             }
201             lastPoint = result.get(result.size() - 1);
202         }
203         Throw.when(firstPoint.x == lastPoint.x && firstPoint.y == lastPoint.y && firstPoint.z == lastPoint.z,
204                 IllegalArgumentException.class, "Before last point and last point are at same location");
205         return result;
206     }
207 
208     /**
209      * Construct a new Polygon3d from an iterator that yields Point3d.
210      * @param iterator the iterator
211      */
212     public Polygon3d(final Iterator<Point3d> iterator)
213     {
214         this(NO_FILTER, iterator);
215     }
216 
217     /**
218      * Construct a new Polygon3d from an iterator that yields Point3d.
219      * @param epsilon minimum distance between points to be considered different (these will <b>not</b> be filtered out)
220      * @param iterator the iterator
221      */
222     public Polygon3d(final double epsilon, final Iterator<Point3d> iterator)
223     {
224         this(epsilon, fixClosingPoint(false, iteratorToList(Throw.whenNull(iterator, "iterator"))));
225     }
226 
227     /**
228      * Construct a new Polygon3d from an existing one. This constructor is primarily intended for use in extending classes.
229      * @param polygon the existing Polygon3d
230      * @throws NullPointerException when <code>polygon</code> is <code>null</code>
231      */
232     public Polygon3d(final Polygon3d polygon)
233     {
234         super(polygon);
235     }
236 
237     @Override
238     public double getLength()
239     {
240         // Length a polygon is computed by taking the length of the PolyLine and adding the length of the closing segment
241         return super.getLength()
242                 + Math.hypot(Math.hypot(getX(size() - 1) - getX(0), getY(size() - 1) - getY(0)), getZ(size() - 1) - getZ(0));
243     }
244 
245     @Override
246     public LineSegment3d getSegment(final int index)
247     {
248         if (index < size() - 1)
249         {
250             return super.getSegment(index);
251         }
252         Throw.when(index != size() - 1, IndexOutOfBoundsException.class, "index must be in range [0, .size() - 1] (got %d)",
253                 index);
254         return new LineSegment3d(getX(index), getY(index), getZ(index), getX(0), getY(0), getZ(0));
255     }
256 
257     @Override
258     public Polygon2d project() throws InvalidProjectionException
259     {
260         double[] projectedX = new double[this.size()];
261         double[] projectedY = new double[this.size()];
262 
263         int nextIndex = 0;
264         for (int i = 0; i < this.size(); i++)
265         {
266             if (i > 0 && getX(i) == getX(i - 1) && getY(i) == getY(i - 1))
267             {
268                 continue;
269             }
270             projectedX[nextIndex] = getX(i);
271             projectedY[nextIndex] = getY(i);
272             nextIndex++;
273         }
274         Throw.when(nextIndex < 2, InvalidProjectionException.class,
275                 "projection yielded too few points to construct a valid Polygon2d");
276         if (nextIndex < projectedX.length)
277         {
278             return new Polygon2d(Arrays.copyOf(projectedX, nextIndex), Arrays.copyOf(projectedY, nextIndex));
279         }
280         return new Polygon2d(projectedX, projectedY);
281     }
282 
283     @Override
284     public Polygon3d reverse()
285     {
286         return new Polygon3d(super.reverse().iterator());
287     }
288 
289     @Override
290     public final String toString()
291     {
292         return toString("%f", false);
293     }
294 
295     @Override
296     public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
297     {
298         StringBuilder result = new StringBuilder();
299         if (!doNotIncludeClassName)
300         {
301             result.append("Polygon3d ");
302         }
303         result.append("[super=");
304         result.append(super.toString(doubleFormat, false));
305         result.append("]");
306         return result.toString();
307     }
308 
309 }