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 }