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.DrawRuntimeException;
9   import org.djutils.draw.point.Point3d;
10  import org.djutils.exceptions.Throw;
11  
12  /**
13   * Polygon3d.java. Closed PolyLine3d. The actual closing point (which is the same as the starting point) is NOT included in the
14   * super 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-2024 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://www.tudelft.nl/pknoppers">Peter Knoppers</a>
21   */
22  public class Polygon3d extends PolyLine3d
23  {
24      /** */
25      private static final long serialVersionUID = 20209999L;
26  
27      /**
28       * Construct a new Polygon3d.
29       * @param x double[]; the x coordinates of the points
30       * @param y double[]; the y coordinates of the points
31       * @param z double[]; the z coordinates of the points
32       * @throws DrawRuntimeException when any two successive points are equal, or when there are too few points
33       */
34      public Polygon3d(final double[] x, final double[] y, final double[] z) throws DrawRuntimeException
35      {
36          super(fixClosingPointX(Throw.whenNull(x, "x may not be null"), Throw.whenNull(y, "y may not be null"),
37                  Throw.whenNull(z, "z may not be null")), fixClosingPointY(x, y, z), fixClosingPointZ(x, y, z));
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       * @param z double[]; the z coordinates of the points
45       * @return double[]; the y coordinates of the points (possibly a copy with the last element removed)
46       */
47      static double[] fixClosingPointX(final double[] x, final double[] y, final double[] z)
48      {
49          if (x.length > 1 && y.length == x.length && z.length == x.length && x[0] == x[x.length - 1] && y[0] == y[x.length - 1]
50                  && z[0] == z[z.length - 1])
51          {
52              return Arrays.copyOf(x, x.length - 1);
53          }
54          return x;
55      }
56  
57      /**
58       * Ensure that the last point is not equal to the first. Remove the last point if necessary.
59       * @param x double[]; the x coordinates of the points
60       * @param y double[]; the y coordinates of the points
61       * @param z double[]; the z coordinates of the points
62       * @return double[]; the y coordinates of the points (possibly a copy with the last element removed)
63       */
64      static double[] fixClosingPointY(final double[] x, final double[] y, final double[] z)
65      {
66          if (x.length > 1 && y.length == x.length && z.length == x.length && x[0] == x[x.length - 1] && y[0] == y[x.length - 1]
67                  && z[0] == z[z.length - 1])
68          {
69              return Arrays.copyOf(y, x.length - 1);
70          }
71          return y;
72      }
73  
74      /**
75       * Ensure that the last point is not equal to the first. Remove the last point if necessary.
76       * @param x double[]; the x coordinates of the points
77       * @param y double[]; the y coordinates of the points
78       * @param z double[]; the z coordinates of the points
79       * @return double[]; the y coordinates of the points (possibly a copy with the last element removed)
80       */
81      static double[] fixClosingPointZ(final double[] x, final double[] y, final double[] z)
82      {
83          if (x.length > 1 && y.length == x.length && z.length == x.length && x[0] == x[x.length - 1] && y[0] == y[x.length - 1]
84                  && z[0] == z[z.length - 1])
85          {
86              return Arrays.copyOf(z, x.length - 1);
87          }
88          return z;
89      }
90  
91      /**
92       * Construct a new Polygon3d.
93       * @param points Point3d[]; array of Point3d objects.
94       * @throws NullPointerException when points is null
95       * @throws DrawRuntimeException when points is too short, or contains successive duplicate points
96       */
97      public Polygon3d(final Point3d[] points) throws NullPointerException, DrawRuntimeException
98      {
99          this(PolyLine3d.makeArray(Throw.whenNull(points, "points may not be null"), p -> p.x),
100                 PolyLine3d.makeArray(points, p -> p.y), PolyLine3d.makeArray(points, p -> p.z));
101     }
102 
103     /**
104      * Construct a new Polygon3d.
105      * @param point1 Point3d; the first point of the new Polygon3d
106      * @param point2 Point3d; the second point of the new Polygon3d
107      * @param otherPoints Point3d[]; all remaining points of the new Polygon3d (may be null)
108      * @throws NullPointerException when point1 or point2 is null
109      * @throws DrawRuntimeException when point1 is equal to the last point of otherPoints, or any two successive points are
110      *             equal
111      */
112     public Polygon3d(final Point3d point1, final Point3d point2, final Point3d... otherPoints)
113             throws NullPointerException, DrawRuntimeException
114     {
115         super(Throw.whenNull(point1, "point1 may not be null"), Throw.whenNull(point2, "point2 may not be null"),
116                 fixClosingPoint(point1, otherPoints));
117     }
118 
119     /**
120      * Ensure that the last point of otherPoints is not equal to point1. Remove the last point if necessary.
121      * @param point1 Point3d; the first point of a new Polygon3d
122      * @param otherPoints Point3d[]; the remaining points of a new Polygon3d (may be null)
123      * @return Point3d[]; otherPoints (possibly a copy thereof with the last entry removed)
124      */
125     private static Point3d[] fixClosingPoint(final Point3d point1, final Point3d[] otherPoints)
126     {
127         if (otherPoints == null || otherPoints.length == 0)
128         {
129             return otherPoints;
130         }
131         Point3d[] result = otherPoints;
132         Point3d lastPoint = result[result.length - 1];
133         if (point1.x == lastPoint.x && point1.y == lastPoint.y)
134         {
135             result = Arrays.copyOf(otherPoints, result.length - 1);
136             lastPoint = result[result.length - 1];
137         }
138         Throw.when(point1.x == lastPoint.x && point1.y == lastPoint.y, DrawRuntimeException.class,
139                 "Before last point and last point are at same location");
140         return result;
141     }
142 
143     /**
144      * Construct a new Polygon3d from a list of Point3d objects.
145      * @param points List&lt;Point3d&gt;; the list of points
146      * @throws NullPointerException when points is null
147      * @throws DrawRuntimeException when points is too short, or the last two points are at the same location
148      */
149     public Polygon3d(final List<Point3d> points) throws NullPointerException, DrawRuntimeException
150     {
151         super(fixClosingPoint(true, Throw.whenNull(points, "points may not be null")));
152 
153     }
154 
155     /**
156      * Ensure that the last point in the list is different from the first point by possibly removing the last point.
157      * @param doNotModifyList boolean; if true; the list of points will not be modified (if the last point is to be removed; the
158      *            entire list up to the last point is duplicated)
159      * @param points List&lt;Point3d&gt;; the list of points
160      * @return List&lt;Point3d&gt;; the fixed list
161      * @throws DrawRuntimeException when the (resulting) list is too short, or the before last and last point of points have the
162      *             same coordinates
163      */
164     private static List<Point3d> fixClosingPoint(final boolean doNotModifyList, final List<Point3d> points)
165             throws DrawRuntimeException
166     {
167         Throw.when(points.size() < 2, DrawRuntimeException.class, "Need at least two points");
168         Point3d firstPoint = points.get(0);
169         Point3d lastPoint = points.get(points.size() - 1);
170         List<Point3d> result = points;
171         if (firstPoint.x == lastPoint.x && firstPoint.y == lastPoint.y && firstPoint.z == lastPoint.z)
172         {
173             if (doNotModifyList)
174             {
175                 result = new ArrayList<>(points.size() - 1);
176                 for (int i = 0; i < points.size() - 1; i++)
177                 {
178                     result.add(points.get(i));
179                 }
180             }
181             else
182             {
183                 result.remove(points.size() - 1);
184             }
185             lastPoint = result.get(result.size() - 1);
186         }
187         Throw.when(firstPoint.x == lastPoint.x && firstPoint.y == lastPoint.y && firstPoint.z == lastPoint.z,
188                 DrawRuntimeException.class, "Before last point and last point are at same location");
189         return result;
190     }
191 
192     /**
193      * Construct a new Polygon3d from an iterator that yields Point3d.
194      * @param iterator Iterator&lt;Point3d&gt;; the iterator
195      */
196     public Polygon3d(final Iterator<Point3d> iterator)
197     {
198         this(fixClosingPoint(false, iteratorToList(Throw.whenNull(iterator, "iterator cannot be null"))));
199     }
200 
201     /**
202      * Create a new Polygon3d, optionally filtering out repeating successive points.
203      * @param filterDuplicates boolean; if true; filter out successive repeated points; otherwise do not filter
204      * @param points Point3d...; the coordinates of the polygon as Point3d
205      * @throws DrawRuntimeException when number of points &lt; 2
206      */
207     public Polygon3d(final boolean filterDuplicates, final Point3d... points) throws DrawRuntimeException
208     {
209         this(PolyLine3d.cleanPoints(filterDuplicates, Arrays.stream(points).iterator()));
210     }
211 
212     /**
213      * Create a new Polygon3d, optionally filtering out repeating successive points.
214      * @param filterDuplicates boolean; if true; filter out successive repeated points; otherwise do not filter
215      * @param pointList List&lt;Point3d&gt;; list of the coordinates of the line as Point3d; any duplicate points in this list
216      *            are removed (this method may modify the provided list)
217      * @throws DrawRuntimeException when number of non-equal points &lt; 2
218      */
219     public Polygon3d(final boolean filterDuplicates, final List<Point3d> pointList) throws DrawRuntimeException
220     {
221         this(PolyLine3d.cleanPoints(filterDuplicates, pointList.iterator()));
222     }
223 
224     /** {@inheritDoc} */
225     @Override
226     public double getLength()
227     {
228         // Length a polygon is computed by taking the length of the PolyLine and adding the length of the closing segment
229         return super.getLength()
230                 + Math.hypot(Math.hypot(getX(size() - 1) - getX(0), getY(size() - 1) - getY(0)), getZ(size() - 1) - getZ(0));
231     }
232 
233     /** {@inheritDoc} */
234     @Override
235     public LineSegment3d getSegment(final int index)
236     {
237         if (index < size() - 1)
238         {
239             return super.getSegment(index);
240         }
241         Throw.when(index != size() - 1, DrawRuntimeException.class, "index must be in range 0..size() - 1");
242         return new LineSegment3d(getX(index), getY(index), getZ(index), getX(0), getY(0), getZ(0));
243     }
244 
245     /** {@inheritDoc} */
246     @Override
247     public Polygon2d project() throws DrawRuntimeException
248     {
249         double[] projectedX = new double[this.size()];
250         double[] projectedY = new double[this.size()];
251 
252         int nextIndex = 0;
253         for (int i = 0; i < this.size(); i++)
254         {
255             if (i > 0 && getX(i) == getX(i - 1) && getY(i) == getY(i - 1))
256             {
257                 continue;
258             }
259             projectedX[nextIndex] = getX(i);
260             projectedY[nextIndex] = getY(i);
261             nextIndex++;
262         }
263         if (nextIndex < projectedX.length)
264         {
265             return new Polygon2d(Arrays.copyOf(projectedX, nextIndex), Arrays.copyOf(projectedY, nextIndex));
266         }
267         return new Polygon2d(projectedX, projectedY);
268     }
269 
270     /** {@inheritDoc} */
271     @Override
272     public Polygon3d reverse()
273     {
274         return new Polygon3d(super.reverse().getPoints());
275     }
276 
277     /** {@inheritDoc} */
278     @Override
279     public final String toExcel()
280     {
281         StringBuffer s = new StringBuffer();
282         for (int i = 0; i < size(); i++)
283         {
284             s.append(getX(i) + "\t" + getY(i) + "\t" + getZ(i) + "\n");
285         }
286         s.append(getX(0) + "\t" + getY(0) + "\t" + getZ(0) + "\n");
287         return s.toString();
288     }
289 
290     /** {@inheritDoc} */
291     @Override
292     public final String toString()
293     {
294         return toString("%f", false);
295     }
296 
297     /** {@inheritDoc} */
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 }