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   import java.util.function.Function;
9   
10  import org.djutils.draw.Drawable3d;
11  import org.djutils.draw.InvalidProjectionException;
12  import org.djutils.draw.bounds.Bounds3d;
13  import org.djutils.draw.point.DirectedPoint3d;
14  import org.djutils.draw.point.Point2d;
15  import org.djutils.draw.point.Point3d;
16  import org.djutils.exceptions.Throw;
17  import org.djutils.logger.CategoryLogger;
18  
19  /**
20   * Implementation of PolyLine for 3D space.
21   * <p>
22   * Copyright (c) 2020-2025 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
23   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
24   * </p>
25   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
26   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
27   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
28   */
29  public class PolyLine3d implements Drawable3d, PolyLine<PolyLine3d, Point3d, Ray3d, DirectedPoint3d, LineSegment3d>
30  {
31      /** X-coordinates of the points. */
32      private final double[] x;
33  
34      /** Y-coordinates of the points. */
35      private final double[] y;
36  
37      /** Z-coordinates of the points. */
38      private final double[] z;
39  
40      /** The cumulative length of the line at point 'i'. */
41      private final double[] lengthIndexedLine;
42  
43      /** The length. */
44      private final double length;
45  
46      /** Bounding box of this PolyLine3d. */
47      private final Bounds3d bounds;
48  
49      /** Heading at start point (only needed for degenerate PolyLine3d). */
50      private final double startDirZ;
51  
52      /** Complement of the slope at start point (only needed for degenerate PolyLine3d). */
53      private final double startDirY;
54  
55      /**
56       * Construct a new PolyLine3d from an array of double x values, an array of double y values and an array of double z values.
57       * @param copyNeeded if<code>true</code>; a deep copy of the points array is stored instead of the provided array
58       * @param epsilon minimum distance between points to be considered different (these will <b>not</b> be filtered out)
59       * @param x the x-coordinates of the points
60       * @param y the y-coordinates of the points
61       * @param z the z-coordinates of the points
62       * @throws NullPointerException when <code>iterator</code> is <code>null</code>
63       * @throws IllegalArgumentException when the provided points do not constitute a valid line (too few points or identical
64       *             adjacent points, or or <code>x</code> and <code>y</code> and <code>z</code> differ in length)
65       */
66      private PolyLine3d(final boolean copyNeeded, final double epsilon, final double[] x, final double[] y, final double[] z)
67      {
68          Throw.when(x.length != y.length || x.length != z.length, IllegalArgumentException.class,
69                  "x, y  and z arrays must have same length");
70          double[][] filtered = filterNearDuplicates(epsilon, x, y, z);
71          Throw.when(x.length < 2, IllegalArgumentException.class, "Need at least two points");
72          this.x = copyNeeded && filtered[0].length == x.length ? Arrays.copyOf(x, x.length) : filtered[0];
73          this.y = copyNeeded && filtered[0].length == x.length ? Arrays.copyOf(y, y.length) : filtered[1];
74          this.z = copyNeeded && filtered[0].length == x.length ? Arrays.copyOf(z, z.length) : filtered[2];
75          double minX = this.x[0];
76          double minY = this.y[0];
77          double minZ = this.z[0];
78          double maxX = this.x[0];
79          double maxY = this.y[0];
80          double maxZ = this.z[0];
81          this.lengthIndexedLine = new double[this.x.length];
82          this.lengthIndexedLine[0] = 0.0;
83          for (int i = 1; i < this.x.length; i++)
84          {
85              minX = Math.min(minX, this.x[i]);
86              minY = Math.min(minY, this.y[i]);
87              minZ = Math.min(minZ, this.z[i]);
88              maxX = Math.max(maxX, this.x[i]);
89              maxY = Math.max(maxY, this.y[i]);
90              maxZ = Math.max(maxZ, this.z[i]);
91              if (this.x[i - 1] == this.x[i] && this.y[i - 1] == this.y[i] && (this.z[i - 1] == this.z[i]))
92              {
93                  throw new IllegalArgumentException(
94                          "Degenerate PolyLine2d; point " + (i - 1) + " has the same x, y and z as point " + i);
95              }
96              // There should be a varargs Math.hypot implementation
97              this.lengthIndexedLine[i] = this.lengthIndexedLine[i - 1]
98                      + Math.hypot(Math.hypot(this.x[i] - this.x[i - 1], this.y[i] - this.y[i - 1]), this.z[i] - this.z[i - 1]);
99          }
100         this.length = this.lengthIndexedLine[this.lengthIndexedLine.length - 1];
101         this.bounds = new Bounds3d(minX, maxX, minY, maxY, minZ, maxZ);
102         this.startDirZ = Double.NaN;
103         this.startDirY = Double.NaN;
104     }
105 
106     /**
107      * Construct a degenerate PolyLine3d (consisting of only one point).
108      * @param x the x-coordinate
109      * @param y the y-coordinate
110      * @param z the z-coordinate
111      * @param dirY the angle from the positive Z axis direction in radians
112      * @param dirZ the angle from the positive X axis direction in radians.
113      * @throws ArithmeticException when <code>x</code>, <code>y</code>, <code>z</code>, or <code>dirY</code>, or
114      *             <code>dirZ</code> is <code>NaN</code>
115      * @throws IllegalArgumentException when <code>dirY</code>, or <code>dirZ</code> is infinite
116      */
117     public PolyLine3d(final double x, final double y, final double z, final double dirY, final double dirZ)
118     {
119         Throw.whenNaN(x, "x");
120         Throw.whenNaN(y, "y");
121         Throw.whenNaN(z, "z");
122         Throw.whenNaN(dirY, "dirY");
123         Throw.when(Double.isInfinite(dirY), IllegalArgumentException.class, "dirY must be finite");
124         Throw.whenNaN(dirZ, "dirZ");
125         Throw.when(Double.isInfinite(dirZ), IllegalArgumentException.class, "dirZ must be finite");
126         this.x = new double[] {x};
127         this.y = new double[] {y};
128         this.z = new double[] {z};
129         this.startDirY = dirY;
130         this.startDirZ = dirZ;
131         this.length = 0;
132         this.bounds = new Bounds3d(x, x, y, y, z, z);
133         this.lengthIndexedLine = new double[] {0.0};
134     }
135 
136     /**
137      * Construct a degenerate PolyLine3d (consisting of only one point).
138      * @param p the point of the degenerate PolyLine3d
139      * @param dirY the angle from the positive Z axis direction in radians
140      * @param dirZ the angle from the positive X axis direction in the XY plane in radians.
141      * @throws NullPointerException when <code>p</code> is <code>null</code>
142      * @throws ArithmeticException when <code>dirY</code>, or <code>dirZ</code> is <code>NaN</code>
143      * @throws IllegalArgumentException when <code>dirY</code>, or <code>dirZ</code> is infinite
144      */
145     public PolyLine3d(final Point3d p, final double dirY, final double dirZ)
146     {
147         this(Throw.whenNull(p, "p").x, p.y, p.z, dirY, dirZ);
148     }
149 
150     /**
151      * Construct a degenerate PolyLine3d (consisting of only one point).
152      * @param directedPoint3d point, <code>dirY</code> and <code>dirZ</code> of the degenerate PolyLine3d
153      * @throws NullPointerException when <code>p</code> is <code>null</code>
154      * @throws IllegalArgumentException when <code>dirY</code> or <code>dirZ</code> is infinite (should not be possible)
155      */
156     public PolyLine3d(final DirectedPoint3d directedPoint3d) throws NullPointerException, IllegalArgumentException
157     {
158         this(Throw.whenNull(directedPoint3d, "r").x, directedPoint3d.y, directedPoint3d.z, directedPoint3d.dirY,
159                 directedPoint3d.dirZ);
160     }
161 
162     /**
163      * Construct a new PolyLine3d from an array of Point2d. This constructor makes a deep copy of the parameters.
164      * @param x the x-coordinates of the points
165      * @param y the y-coordinates of the points
166      * @param z the z-coordinates of the points
167      * @throws NullPointerException when <code>x</code>, <code>y</code>, or <code>z</code> is <code>null</code>
168      * @throws IllegalArgumentException when the provided coordinate values do not constitute a valid line (too few points or
169      *             identical adjacent points, or the arrays are not the same length)
170      */
171     public PolyLine3d(final double[] x, final double[] y, final double[] z)
172     {
173         this(NO_FILTER, x, y, z);
174     }
175 
176     /**
177      * Construct a new PolyLine3d from an array of Point2d. This constructor makes a deep copy of the parameters.
178      * @param epsilon minimum distance between points to be considered different (these will <b>not</b> be filtered out)
179      * @param x the x-coordinates of the points
180      * @param y the y-coordinates of the points
181      * @param z the z-coordinates of the points
182      * @throws NullPointerException when <code>x</code>, <code>y</code>, or <code>z</code> is <code>null</code>
183      * @throws IllegalArgumentException when the provided coordinate values do not constitute a valid line (too few points or
184      *             identical adjacent points, or the arrays are not the same length)
185      */
186     public PolyLine3d(final double epsilon, final double[] x, final double[] y, final double[] z)
187     {
188         this(true, epsilon, x, y, z);
189     }
190 
191     /**
192      * Construct a new PolyLine3d from an array of Point3d.
193      * @param points the array of points to construct this PolyLine3d from.
194      * @throws NullPointerException when the <code>points</code> array is <code>null</code>
195      * @throws IllegalArgumentException when the provided points do not constitute a valid line (too few points or identical
196      *             adjacent points)
197      */
198     public PolyLine3d(final Point3d[] points)
199     {
200         this(NO_FILTER, points);
201     }
202 
203     /**
204      * Construct a new PolyLine3d from an array of Point3d.
205      * @param epsilon minimum distance between points to be considered different (these will <b>not</b> be filtered out)
206      * @param points the array of points to construct this PolyLine3d from.
207      * @throws NullPointerException when the <code>points</code> array is <code>null</code>
208      * @throws IllegalArgumentException when the provided points do not constitute a valid line (too few points or identical
209      *             adjacent points)
210      */
211     public PolyLine3d(final double epsilon, final Point3d[] points)
212     {
213         this(false, epsilon, makeArray(Throw.whenNull(points, "points"), p -> p.x), makeArray(points, p -> p.y),
214                 makeArray(points, p -> p.z));
215     }
216 
217     /**
218      * Make an array of double an fill it with the appropriate coordinate of points.
219      * @param points array of points
220      * @param getter function that obtains the intended coordinate
221      * @return array of double values filled with the requested coordinate values
222      */
223     protected static double[] makeArray(final Point3d[] points, final Function<Point3d, Double> getter)
224     {
225         double[] array = new double[points.length];
226         for (int index = 0; index < points.length; index++)
227         {
228             array[index] = getter.apply(points[index]);
229         }
230         return array;
231     }
232 
233     /**
234      * Construct a new PolyLine3d from two or more Point3d arguments.
235      * @param point1 starting point of the PolyLine3d
236      * @param point2 second point of the PolyLine3d
237      * @param otherPoints additional points of the PolyLine3d (may be <code>null</code>, or have zero length)
238      * @throws NullPointerException when <code>point1</code>, or <code>point2</code> is <code>null</code>, or
239      *             <code>otherPoints</code> contains a <code>null</code> value
240      * @throws IllegalArgumentException when the provided points do not constitute a valid line (too few points or identical
241      *             adjacent points)
242      */
243     public PolyLine3d(final Point3d point1, final Point3d point2, final Point3d... otherPoints)
244     {
245         this(NO_FILTER, point1, point2, otherPoints);
246     }
247 
248     /**
249      * Construct a new PolyLine3d from two or more Point3d arguments.
250      * @param epsilon minimum distance between points to be considered different (these will <b>not</b> be filtered out)
251      * @param point1 starting point of the PolyLine3d
252      * @param point2 second point of the PolyLine3d
253      * @param otherPoints additional points of the PolyLine3d (may be <code>null</code>, or have zero length)
254      * @throws NullPointerException when <code>point1</code>, or <code>point2</code> is <code>null</code>, or
255      *             <code>otherPoints</code> contains a <code>null</code> value
256      * @throws IllegalArgumentException when the provided points do not constitute a valid line (too few points or identical
257      *             adjacent points)
258      */
259     public PolyLine3d(final double epsilon, final Point3d point1, final Point3d point2, final Point3d... otherPoints)
260     {
261         this(epsilon, spliceArray(point1, point2, otherPoints));
262     }
263 
264     /**
265      * Construct an array of Point3d from two points plus an array of Point3d.
266      * @param point1 the first point (ends up at index 0 of the result)
267      * @param point2 the second point (ends up at index 1 of the result)
268      * @param otherPoints may be <code>null</code>, may be empty. If non empty, the elements in otherPoints end up at index 2
269      *            and up in the result
270      * @return the combined array
271      * @throws NullPointerException when <code>point1</code> or <code>point2</code> is <code>null</code>
272      */
273     private static Point3d[] spliceArray(final Point3d point1, final Point3d point2, final Point3d... otherPoints)
274     {
275         Point3d[] result = new Point3d[2 + (otherPoints == null ? 0 : otherPoints.length)];
276         result[0] = point1;
277         result[1] = point2;
278         if (otherPoints != null)
279         {
280             for (int i = 0; i < otherPoints.length; i++)
281             {
282                 result[i + 2] = otherPoints[i];
283             }
284         }
285         return result;
286     }
287 
288     /**
289      * Construct a new PolyLine3d from an iterator that yields Point3d objects.
290      * @param iterator iterator that will provide all points that constitute the new PolyLine3d
291      * @throws NullPointerException when <code>iterator</code> is <code>null</code>, or yields a <code>null</code> value
292      * @throws IllegalArgumentException when the <code>iterator</code> provides too few points, or some adjacent identical
293      *             points)
294      */
295     public PolyLine3d(final Iterator<Point3d> iterator)
296     {
297         this(NO_FILTER, iterator);
298     }
299 
300     /**
301      * Construct a new PolyLine3d from an iterator that yields Point3d objects.
302      * @param epsilon minimum distance between points to be considered different (these will <b>not</b> be filtered out)
303      * @param iterator iterator that will provide all points that constitute the new PolyLine3d
304      * @throws NullPointerException when <code>iterator</code> is <code>null</code>, or yields a <code>null</code> value
305      * @throws IllegalArgumentException when the <code>iterator</code> provides too few points, or some adjacent identical
306      *             points)
307      */
308     public PolyLine3d(final double epsilon, final Iterator<Point3d> iterator)
309     {
310         this(epsilon, iteratorToList(Throw.whenNull(iterator, "iterator")));
311     }
312 
313     /**
314      * Construct a new PolyLine3d from a List&lt;Point3d&gt;.
315      * @param pointList the list of points to construct the new PolyLine3d from.
316      * @throws NullPointerException when <code>pointList</code> is <code>null</code>, or contains a <code>null</code> value
317      * @throws IllegalArgumentException when the provided points do not constitute a valid line (too few points or identical
318      *             adjacent points)
319      */
320     public PolyLine3d(final List<Point3d> pointList)
321     {
322         this(NO_FILTER, pointList);
323     }
324 
325     /**
326      * Construct a new PolyLine3d from a List&lt;Point3d&gt;.
327      * @param epsilon minimum distance between points to be considered different (these will <b>not</b> be filtered out)
328      * @param pointList the list of points to construct the new PolyLine3d from.
329      * @throws NullPointerException when <code>pointList</code> is <code>null</code>, or contains a <code>null</code> value
330      * @throws IllegalArgumentException when the provided points do not constitute a valid line (too few points or identical
331      *             adjacent points)
332      */
333     public PolyLine3d(final double epsilon, final List<Point3d> pointList)
334     {
335         this(epsilon, pointList.toArray(new Point3d[Throw.whenNull(pointList, "pointList").size()]));
336     }
337 
338     /**
339      * Construct a new PolyLine3d from an existing one. This constructor is primarily intended for use in extending classes.
340      * @param polyLine the existing PolyLine3d.
341      * @throws NullPointerException when <code>polyLine</code> is <code>null</code>
342      */
343     public PolyLine3d(final PolyLine3d polyLine)
344     {
345         this.x = polyLine.x;
346         this.y = polyLine.y;
347         this.z = polyLine.z;
348         this.lengthIndexedLine = polyLine.lengthIndexedLine;
349         this.length = polyLine.length;
350         this.bounds = polyLine.bounds;
351         this.startDirY = polyLine.startDirY;
352         this.startDirZ = polyLine.startDirZ;
353     }
354 
355     @Override
356     public PolyLine3d instantiate(final double epsilon, final List<Point3d> pointList)
357     {
358         return new PolyLine3d(epsilon, pointList);
359     }
360 
361     /**
362      * Build a list from the Point3d objects that an iterator provides.
363      * @param iterator the iterator that will provide the points
364      * @return a list of the points provided by the iterator
365      * @throws NullPointerException when <code>iterator</code> is <code>null</code>
366      */
367     static List<Point3d> iteratorToList(final Iterator<Point3d> iterator)
368     {
369         List<Point3d> result = new ArrayList<>();
370         iterator.forEachRemaining(result::add);
371         return result;
372     }
373 
374     @Override
375     public int size()
376     {
377         return this.x.length;
378     }
379 
380     @Override
381     public final Point3d get(final int i) throws IndexOutOfBoundsException
382     {
383         return new Point3d(this.x[i], this.y[i], this.z[i]);
384     }
385 
386     @Override
387     public final double getX(final int i) throws IndexOutOfBoundsException
388     {
389         return this.x[i];
390     }
391 
392     @Override
393     public final double getY(final int i) throws IndexOutOfBoundsException
394     {
395         return this.y[i];
396     }
397 
398     /**
399      * Return the z-coordinate of a point of this PolyLine.
400      * @param index the index of the requested z-coordinate
401      * @return the z-coordinate of the requested point of this PolyLine
402      * @throws IndexOutOfBoundsException when <code>index &lt; 0</code>, or <code>index &ge; size()</code>
403      */
404     public final double getZ(final int index)
405     {
406         return this.z[index];
407     }
408 
409     @Override
410     public LineSegment3d getSegment(final int index)
411     {
412         Throw.when(index < 0 || index >= this.x.length - 1, IndexOutOfBoundsException.class,
413                 "index must be in range [0, size() - 1) (got %d)", index);
414         return new LineSegment3d(this.x[index], this.y[index], this.z[index], this.x[index + 1], this.y[index + 1],
415                 this.z[index + 1]);
416     }
417 
418     @Override
419     public final double lengthAtIndex(final int index)
420     {
421         return this.lengthIndexedLine[index];
422     }
423 
424     @Override
425     public double getLength()
426     {
427         return this.length;
428     }
429 
430     @Override
431     public Iterator<Point3d> iterator()
432     {
433         return new Iterator<Point3d>()
434         {
435             private int nextIndex = 0;
436 
437             @Override
438             public boolean hasNext()
439             {
440                 return this.nextIndex < size();
441             }
442 
443             @Override
444             public Point3d next()
445             {
446                 return get(this.nextIndex++);
447             }
448         };
449     }
450 
451     @Override
452     public Bounds3d getAbsoluteBounds()
453     {
454         return this.bounds;
455     }
456 
457     @Override
458     public final PolyLine3d noiseFilteredLine(final double noiseLevel)
459     {
460         if (this.size() <= 2)
461         {
462             return this; // Except for some cached fields; a PolyLine2d is immutable; so safe to return
463         }
464         Point3d prevPoint = null;
465         List<Point3d> list = new ArrayList<>();
466         for (int index = 0; index < this.size(); index++)
467         {
468             Point3d currentPoint = get(index);
469             if (null != prevPoint && prevPoint.distance(currentPoint) < noiseLevel)
470             {
471                 if (index == this.size() - 1)
472                 {
473                     if (list.size() > 1)
474                     {
475                         // Replace the last point of the result by the last point of this PolyLine2d
476                         list.set(list.size() - 1, currentPoint);
477                     }
478                     else
479                     {
480                         // Append the last point of this even though it is close to the first point than the noise value to
481                         // comply with the requirement that first and last point of this are ALWAYS included in the result.
482                         list.add(currentPoint);
483                     }
484                 }
485                 continue; // Do not replace prevPoint by currentPoint
486             }
487             list.add(currentPoint);
488             prevPoint = currentPoint;
489         }
490         if (list.size() == this.x.length)
491         {
492             return this;
493         }
494         if (list.size() == 2 && list.get(0).equals(list.get(1)))
495         {
496             // Insert point 1 of this; it MUST be different from point 0; so we don't have to test for anything.
497             list.add(1, get(1));
498         }
499         return new PolyLine3d(list);
500     }
501 
502     /**
503      * Concatenate several PolyLine3d instances.
504      * @param lines one or more PolyLine3d. The last point of the first &lt;strong&gt;must&lt;/strong&gt; match the first of the
505      *            second, etc.
506      * @return PolyLine3d
507      * @throws NullPointerException when <code>lines</code> is <code>null</code>, or contains a <code>null</code> value
508      * @throws IllegalArgumentException if zero lines are given, or when there is a gap between consecutive lines
509      */
510     public static PolyLine3d concatenate(final PolyLine3d... lines)
511     {
512         return concatenate(0.0, lines);
513     }
514 
515     /**
516      * Concatenate two PolyLine3d instances. This method is separate for efficiency reasons.
517      * @param tolerance the tolerance between the end point of a line and the first point of the next line
518      * @param line1 first line
519      * @param line2 second line
520      * @return the concatenation of the two lines
521      * @throws NullPointerException when <code>line1</code>, or <code>line2</code> is <code>null</code>
522      * @throws IllegalArgumentException when there is a gap between the lines
523      */
524     public static PolyLine3d concatenate(final double tolerance, final PolyLine3d line1, final PolyLine3d line2)
525     {
526         if (line1.getLast().distance(line2.getFirst()) > tolerance)
527         {
528             throw new IllegalArgumentException("Lines are not connected: " + line1.getLast() + " to " + line2.getFirst()
529                     + " distance is " + line1.getLast().distance(line2.getFirst()) + " > " + tolerance);
530         }
531         int size = line1.size() + line2.size() - 1;
532         Point3d[] points = new Point3d[size];
533         int nextIndex = 0;
534         for (int j = 0; j < line1.size(); j++)
535         {
536             points[nextIndex++] = line1.get(j);
537         }
538         for (int j = 1; j < line2.size(); j++)
539         {
540             points[nextIndex++] = line2.get(j);
541         }
542         return new PolyLine3d(points);
543     }
544 
545     /**
546      * Concatenate several PolyLine3d instances.
547      * @param tolerance the tolerance between the end point of a line and the first point of the next line
548      * @param lines one or more PolyLine3d. The last point of the first &lt;strong&gt;must&lt;/strong&gt; match the first of the
549      *            second within the provided tolerance value, etc.
550      * @return the concatenation of the lines
551      * @throws NullPointerException when <code>lines</code> is <code>null</code>, or contains a <code>null</code> value
552      * @throws IllegalArgumentException if zero lines are given, or when there is a gap larger than tolerance between
553      *             consecutive lines
554      */
555     public static PolyLine3d concatenate(final double tolerance, final PolyLine3d... lines)
556     {
557         Throw.when(0 == lines.length, IllegalArgumentException.class, "Empty argument list");
558         if (1 == lines.length)
559         {
560             return lines[0];
561         }
562         int size = lines[0].size();
563         for (int i = 1; i < lines.length; i++)
564         {
565             if (lines[i - 1].getLast().distance(lines[i].getFirst()) > tolerance)
566             {
567                 throw new IllegalArgumentException(
568                         "Lines are not connected: " + lines[i - 1].getLast() + " to " + lines[i].getFirst() + " distance is "
569                                 + lines[i - 1].getLast().distance(lines[i].getFirst()) + " > " + tolerance);
570             }
571             size += lines[i].size() - 1;
572         }
573         Point3d[] points = new Point3d[size];
574         int nextIndex = 0;
575         for (int i = 0; i < lines.length; i++)
576         {
577             PolyLine3d line = lines[i];
578             for (int j = 0 == i ? 0 : 1; j < line.size(); j++)
579             {
580                 points[nextIndex++] = line.get(j);
581             }
582         }
583         return new PolyLine3d(points);
584     }
585 
586     @Override
587     public PolyLine2d project() throws InvalidProjectionException
588     {
589         for (int i = 1; i < size(); i++)
590         {
591             if (this.x[i] != this.x[0] || this.y[i] != this.y[0])
592             {
593                 break;
594             }
595             Throw.when(i == size() - 1, InvalidProjectionException.class, "all points project onto the same point");
596         }
597         return new PolyLine2d(false, 0.0, this.x, this.y);
598     }
599 
600     @Override
601     public final DirectedPoint3d getLocationExtended(final double position)
602     {
603         if (position >= 0.0 && position <= this.length)
604         {
605             return getLocation(position);
606         }
607 
608         // position before start point -- extrapolate using direction from first point to second point of this PolyLine2d
609         if (position < 0.0)
610         {
611             double fraction = position / (this.lengthIndexedLine[1] - this.lengthIndexedLine[0]);
612             return new DirectedPoint3d(this.x[0] + fraction * (this.x[1] - this.x[0]),
613                     this.y[0] + fraction * (this.y[1] - this.y[0]), this.z[0] + fraction * (this.z[1] - this.z[0]), this.x[1],
614                     this.y[1], this.z[1]);
615         }
616 
617         // position beyond end point -- extrapolate using the direction from the before last point to the last point of this
618         // PolyLine2d
619         int n1 = this.x.length - 1; // index of last point
620         int n2 = this.x.length - 2; // index of before last point
621         double len = position - this.length;
622         double fraction = len / (this.lengthIndexedLine[n1] - this.lengthIndexedLine[n2]);
623         while (Double.isInfinite(fraction))
624         {
625             // Overflow occurred; move n2 back another point; if possible
626             if (--n2 < 0)
627             {
628                 CategoryLogger.always().error("lengthIndexedLine of {} is invalid", this);
629                 return new DirectedPoint3d(this.x[n1], this.y[n1], this.z[n1], 0.0, 0.0); // Bogus direction
630             }
631             fraction = len / (this.lengthIndexedLine[n1] - this.lengthIndexedLine[n2]);
632         }
633         return new DirectedPoint3d(this.x[n1] + fraction * (this.x[n1] - this.x[n2]),
634                 this.y[n1] + fraction * (this.y[n1] - this.y[n2]), this.z[n1] + fraction * (this.z[n1] - this.z[n2]),
635                 Math.atan2(Math.hypot(this.x[n1] - this.x[n2], this.y[n1] - this.y[n2]), this.z[n1] - this.z[n2]),
636                 Math.atan2(this.y[n1] - this.y[n2], this.x[n1] - this.x[n2]));
637     }
638 
639     @Override
640     public final DirectedPoint3d getLocation(final double position)
641     {
642         Throw.whenNaN(position, "position");
643         Throw.when(position < 0.0 || position > this.length, IllegalArgumentException.class,
644                 "illegal position (got %f, should be in range [0.0, %f])", position, this.length);
645         // handle special cases: position == 0.0, or position == length
646         if (position == 0.0)
647         {
648             if (this.lengthIndexedLine.length == 1) // Extra special case; degenerate PolyLine2d
649             {
650                 return new Ray3d(this.x[0], this.y[0], this.z[0], this.startDirZ, this.startDirY);
651             }
652             return new DirectedPoint3d(this.x[0], this.y[0], this.z[0], this.x[1], this.y[1], this.z[1]);
653         }
654         if (position == this.length)
655         {
656             return new DirectedPoint3d(this.x[this.x.length - 1], this.y[this.x.length - 1], this.z[this.x.length - 1],
657                     2 * this.x[this.x.length - 1] - this.x[this.x.length - 2],
658                     2 * this.y[this.x.length - 1] - this.y[this.x.length - 2],
659                     2 * this.z[this.x.length - 1] - this.z[this.x.length - 2]);
660         }
661         // find the index of the line segment, use binary search
662         int index = find(position);
663         double remainder = position - this.lengthIndexedLine[index];
664         double fraction = remainder / (this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index]);
665         // if (fraction >= 1.0 && index < this.x.length - 1)
666         // {
667         // // Rounding problem; move to the next segment.
668         // index++;
669         // remainder = position - this.lengthIndexedLine[index];
670         // fraction = remainder / (this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index]);
671         // }
672         return new DirectedPoint3d(this.x[index] + fraction * (this.x[index + 1] - this.x[index]),
673                 this.y[index] + fraction * (this.y[index + 1] - this.y[index]),
674                 this.z[index] + fraction * (this.z[index + 1] - this.z[index]), 2 * this.x[index + 1] - this.x[index],
675                 2 * this.y[index + 1] - this.y[index], 2 * this.z[index + 1] - this.z[index]);
676     }
677 
678     /**
679      * Perform the orthogonal projection operation.
680      * @param point the point to project
681      * @param limitHandling if <code>Null</code>; results outside the interval [0.0, 1.0] are replaced by <code>NaN</code>, if
682      *            <code>false</code>, results outside that interval are returned as is; if <code>true</code> results outside the
683      *            interval are truncated to the interval and therefore not truly orthogonal
684      * @return the fractional position on this <code>PolyLine3d</code> that is closest to point, or <code>NaN</code>
685      * @throws NullPointerException when <code>point</code> is <code>null</code>
686      */
687     private double projectOrthogonalFractional(final Point3d point, final Boolean limitHandling)
688     {
689         Throw.whenNull(point, "point");
690         double result = Double.NaN;
691         if (this.lengthIndexedLine.length == 1)
692         {
693             // This is a degenerate PolyLine2d
694             if (null != limitHandling && limitHandling)
695             {
696                 return 0.0;
697             }
698             result = new Ray3d(getLocation(0.0)).projectOrthogonalFractionalExtended(point);
699             if (null == limitHandling)
700             {
701                 return result == 0.0 ? 0.0 : Double.NaN;
702             }
703             // limitHanling is false
704             if (result == 0.0)
705             {
706                 return 0.0;
707             }
708             return result > 0 ? Double.POSITIVE_INFINITY : Double.NEGATIVE_INFINITY;
709         }
710         double bestDistance = Double.POSITIVE_INFINITY;
711         double bestDistanceExtended = Double.POSITIVE_INFINITY;
712         for (int index = 1; index < this.size(); index++)
713         {
714             double fraction = point.fractionalPositionOnLine(this.x[index - 1], this.y[index - 1], this.z[index - 1],
715                     this.x[index], this.y[index], this.z[index], false, false);
716             double distance = Math.hypot(
717                     Math.hypot(point.x - (this.x[index - 1] + fraction * (this.x[index] - this.x[index - 1])),
718                             point.y - (this.y[index - 1] + fraction * (this.y[index] - this.y[index - 1]))),
719                     point.z - (this.z[index - 1] + fraction * (this.z[index] - this.z[index - 1])));
720             if (distance < bestDistanceExtended && (fraction >= 0.0 && fraction <= 1.0 || (fraction < 0.0 && index == 1)
721                     || fraction > 1.0 && index == this.size() - 1))
722             {
723                 bestDistanceExtended = distance;
724             }
725             if (distance < bestDistance && (fraction >= 0.0 || index == 1 && limitHandling != null && !limitHandling)
726                     && (fraction <= 1.0 || index == this.size() - 1 && limitHandling != null && !limitHandling))
727             {
728                 bestDistance = distance;
729                 result = lengthAtIndex(index - 1) + fraction * (lengthAtIndex(index) - lengthAtIndex(index - 1));
730             }
731             else if (fraction < 0.0 && limitHandling != null && limitHandling)
732             {
733                 distance = Math.hypot(Math.hypot(point.x - this.x[index - 1], point.y - this.y[index - 1]),
734                         point.z - this.z[index - 1]);
735                 if (distance < bestDistance)
736                 {
737                     bestDistance = distance;
738                     result = lengthAtIndex(index - 1);
739                 }
740             }
741             else if (index == this.size() - 1 && limitHandling != null && limitHandling)
742             {
743                 distance = Math.hypot(Math.hypot(point.x - this.x[index], point.y - this.y[index]), point.z - this.z[index]);
744                 if (distance < bestDistance)
745                 {
746                     bestDistance = distance;
747                     result = lengthAtIndex(index);
748                 }
749             }
750         }
751         if (bestDistance > bestDistanceExtended && (limitHandling == null || !limitHandling))
752         {
753             return Double.NaN;
754         }
755         return result / this.length;
756     }
757 
758     @Override
759     public DirectedPoint3d closestPointOnPolyLine(final Point3d point)
760     {
761         return getLocation(projectOrthogonalFractional(point, true) * this.length);
762     }
763 
764     /**
765      * Perform the project orthogonal operation.
766      * @param point the point to project
767      * @param limitHandling if <code>Null</code>; results outside the interval [0.0, 1.0] are replaced by <code>NaN</code>, if
768      *            <code>false</code>, results outside that interval are returned as is; if <code>true</code> results outside the
769      *            interval are truncated to the interval and therefore not truly orthogonal
770      * @return the fractional position on this <code>PolyLine3d</code> that is closest to point, or <code>NaN</code>
771      * @throws NullPointerException when <code>point</code> is <code>null</code>
772      */
773     private Point3d projectOrthogonal(final Point3d point, final Boolean limitHandling)
774     {
775         Throw.whenNull(point, "point");
776         if (this.lengthIndexedLine.length == 1) // Handle degenerate case
777         {
778             // limitHandling == true is not handled because it cannot happen
779             Point3d result = new Ray3d(getLocation(0.0)).projectOrthogonalExtended(point);
780             if (null == limitHandling)
781             {
782                 return result.x != this.x[0] || result.y != this.y[0] || result.z != this.z[0] ? null : get(0);
783             }
784             // limitHandling is false
785             return result;
786         }
787         double fraction = projectOrthogonalFractional(point, limitHandling);
788         if (Double.isNaN(fraction))
789         {
790             return null;
791         }
792         return getLocationExtended(fraction * this.length);
793     }
794 
795     @Override
796     public Point3d projectOrthogonal(final Point3d point)
797     {
798         return projectOrthogonal(point, null);
799     }
800 
801     @Override
802     public Point3d projectOrthogonalExtended(final Point3d point)
803     {
804         return projectOrthogonal(point, false);
805     }
806 
807     @Override
808     public final double projectOrthogonalFractional(final Point3d point)
809     {
810         return projectOrthogonalFractional(point, null);
811     }
812 
813     @Override
814     public double projectOrthogonalFractionalExtended(final Point3d point)
815     {
816         return projectOrthogonalFractional(point, false);
817     }
818 
819     @Override
820     public PolyLine3d extract(final double start, final double end)
821     {
822         Throw.whenNaN(start, "start");
823         Throw.whenNaN(end, "end");
824         Throw.when(start < 0 || start >= end || end > this.length, IllegalArgumentException.class,
825                 "Bad interval (%f...%f; length of this PolyLine3d is %f)", start, end, this.length);
826         double cumulativeLength = 0;
827         double nextCumulativeLength = 0;
828         double segmentLength = 0;
829         int index = 0;
830         List<Point3d> pointList = new ArrayList<>();
831         while (start > cumulativeLength)
832         {
833             Point3d fromPoint = get(index);
834             index++;
835             Point3d toPoint = get(index);
836             segmentLength = fromPoint.distance(toPoint);
837             cumulativeLength = nextCumulativeLength;
838             nextCumulativeLength = cumulativeLength + segmentLength;
839             if (nextCumulativeLength >= start)
840             {
841                 break;
842             }
843         }
844         if (start == nextCumulativeLength)
845         {
846             pointList.add(get(index));
847         }
848         else
849         {
850             pointList.add(get(index - 1).interpolate(get(index), (start - cumulativeLength) / segmentLength));
851             if (end > nextCumulativeLength)
852             {
853                 pointList.add(get(index));
854             }
855         }
856         while (end > nextCumulativeLength)
857         {
858             Point3d fromPoint = get(index);
859             index++;
860             if (index >= size())
861             {
862                 break; // rounding error
863             }
864             Point3d toPoint = get(index);
865             segmentLength = fromPoint.distance(toPoint);
866             cumulativeLength = nextCumulativeLength;
867             nextCumulativeLength = cumulativeLength + segmentLength;
868             if (nextCumulativeLength >= end)
869             {
870                 break;
871             }
872             pointList.add(toPoint);
873         }
874         if (end == nextCumulativeLength)
875         {
876             pointList.add(get(index));
877         }
878         else if (index < this.x.length)
879         {
880             Point3d point = get(index - 1).interpolate(get(index), (end - cumulativeLength) / segmentLength);
881             // can be the same due to rounding
882             if (!point.equals(pointList.get(pointList.size() - 1)))
883             {
884                 pointList.add(point);
885             }
886         }
887         // else: rounding error
888         return instantiate(pointList);
889     }
890 
891     @Override
892     public PolyLine3d offsetLine(final double offset, final double circlePrecision, final double offsetMinimumFilterValue,
893             final double offsetMaximumFilterValue, final double offsetFilterRatio, final double minimumOffset)
894     {
895         return restoreElevation(project().offsetLine(offset, circlePrecision, offsetMinimumFilterValue,
896                 offsetMaximumFilterValue, offsetFilterRatio, minimumOffset));
897     }
898 
899     @Override
900     public PolyLine3d offsetLine(final double[] relativeFractions, final double[] offsets,
901             final double offsetMinimumFilterValue)
902     {
903         return restoreElevation(project().offsetLine(relativeFractions, offsets, offsetMinimumFilterValue));
904     }
905 
906     /**
907      * Approximate the z-values of a PolyLine that was made 2d in order to apply some modification.
908      * @param flat the modified 2d PolyLine
909      * @return the PolyLine with approximately restored z-values
910      */
911     private PolyLine3d restoreElevation(final PolyLine2d flat)
912     {
913         List<Point3d> points = new ArrayList<>();
914         int start = 0;
915         for (int index = 0; index < flat.size(); index++)
916         {
917             Point2d point2d = flat.get(index);
918             // Find the closest point in reference to point2d; starting at start
919             int bestIndex = start;
920             double bestDistance = Double.POSITIVE_INFINITY;
921             Point3d bestInReference = null;
922             for (int i = start; i < size(); i++)
923             {
924                 Point3d pointInReference = get(i);
925                 double distance = point2d.distance(pointInReference.project());
926                 if (distance < bestDistance)
927                 {
928                     bestIndex = i;
929                     bestDistance = distance;
930                     bestInReference = pointInReference;
931                 }
932             }
933             points.add(new Point3d(point2d, bestInReference.z));
934             start = bestIndex;
935         }
936         return new PolyLine3d(points);
937     }
938 
939     @Override
940     public PolyLine3d offsetLine(final double offsetAtStart, final double offsetAtEnd, final double circlePrecision,
941             final double offsetMinimumFilterValue, final double offsetMaximumFilterValue, final double offsetFilterRatio,
942             final double minimumOffset)
943     {
944         if (offsetAtStart == offsetAtEnd)
945         {
946             return offsetLine(offsetAtStart, circlePrecision, offsetMinimumFilterValue, offsetMaximumFilterValue,
947                     offsetFilterRatio, minimumOffset);
948         }
949         PolyLine3d atStart = offsetLine(offsetAtStart, circlePrecision, offsetMinimumFilterValue, offsetMaximumFilterValue,
950                 offsetFilterRatio, minimumOffset);
951         PolyLine3d atEnd = offsetLine(offsetAtEnd, circlePrecision, offsetMinimumFilterValue, offsetMaximumFilterValue,
952                 offsetFilterRatio, minimumOffset);
953         return atStart.transitionLine(atEnd, new TransitionFunction()
954         {
955             @Override
956             public double function(final double fraction)
957             {
958                 return fraction;
959             }
960         });
961     }
962 
963     @Override
964     public PolyLine3d transitionLine(final PolyLine3d endLine, final TransitionFunction transition)
965     {
966         Throw.whenNull(endLine, "endLine");
967         Throw.whenNull(transition, "transition");
968         List<Point3d> pointList = new ArrayList<>();
969         int indexInStart = 0;
970         int indexInEnd = 0;
971         while (indexInStart < this.size() && indexInEnd < endLine.size())
972         {
973             double fractionInStart = lengthAtIndex(indexInStart) / this.length;
974             double fractionInEnd = endLine.lengthAtIndex(indexInEnd) / endLine.length;
975             if (fractionInStart < fractionInEnd)
976             {
977                 pointList.add(get(indexInStart).interpolate(endLine.getLocation(fractionInStart * endLine.length),
978                         transition.function(fractionInStart)));
979                 indexInStart++;
980             }
981             else if (fractionInStart > fractionInEnd)
982             {
983                 pointList.add(this.getLocation(fractionInEnd * this.length).interpolate(endLine.get(indexInEnd),
984                         transition.function(fractionInEnd)));
985                 indexInEnd++;
986             }
987             else
988             {
989                 pointList.add(this.get(indexInStart).interpolate(endLine.getLocation(fractionInEnd * endLine.length),
990                         transition.function(fractionInStart)));
991                 indexInStart++;
992                 indexInEnd++;
993             }
994         }
995         return new PolyLine3d(0.0, pointList);
996     }
997 
998     @Override
999     public PolyLine3d truncate(final double position)
1000     {
1001         Throw.when(position <= 0.0 || position > this.length, IllegalArgumentException.class,
1002                 "illegal position (got %f should be in range (0.0, %f])", position, this.length);
1003         // handle special case: position == length
1004         if (position == this.length)
1005         {
1006             return this;
1007         }
1008 
1009         // find the index of the line segment
1010         int index = find(position);
1011         double remainder = position - lengthAtIndex(index);
1012         double fraction = remainder / (lengthAtIndex(index + 1) - lengthAtIndex(index));
1013         Point3d p1 = get(index);
1014         Point3d lastPoint;
1015         if (0.0 == fraction)
1016         {
1017             lastPoint = p1;
1018         }
1019         else
1020         {
1021             Point3d p2 = get(index + 1);
1022             lastPoint = p1.interpolate(p2, fraction);
1023             index++;
1024         }
1025         double[] truncatedX = new double[index + 1];
1026         double[] truncatedY = new double[index + 1];
1027         double[] truncatedZ = new double[index + 1];
1028         for (int i = 0; i < index; i++)
1029         {
1030             truncatedX[i] = this.x[i];
1031             truncatedY[i] = this.y[i];
1032             truncatedZ[i] = this.z[i];
1033         }
1034         truncatedX[index] = lastPoint.x;
1035         truncatedY[index] = lastPoint.y;
1036         truncatedZ[index] = lastPoint.z;
1037         return new PolyLine3d(truncatedX, truncatedY, truncatedZ);
1038     }
1039 
1040     @Override
1041     public String toString()
1042     {
1043         return toString("%f", false);
1044     }
1045 
1046     @Override
1047     public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
1048     {
1049         StringBuilder result = new StringBuilder();
1050         if (!doNotIncludeClassName)
1051         {
1052             result.append("PolyLine3d ");
1053         }
1054         String format = String.format("%%sx=%1$s, y=%1$s, z=%1$s", doubleFormat);
1055         for (int index = 0; index < this.x.length; index++)
1056         {
1057             result.append(
1058                     String.format(Locale.US, format, index == 0 ? "[" : ", ", this.x[index], this.y[index], this.z[index]));
1059         }
1060         if (this.lengthIndexedLine.length == 1)
1061         {
1062             format = String.format(", startDirY=%1$s, startDirZ=%1$s", doubleFormat);
1063             result.append(String.format(Locale.US, format, this.startDirY, this.startDirZ));
1064         }
1065         result.append("]");
1066         return result.toString();
1067     }
1068 
1069     @Override
1070     public int hashCode()
1071     {
1072         final int prime = 31;
1073         int result = 1;
1074         long temp;
1075         temp = Double.doubleToLongBits(this.startDirZ);
1076         result = prime * result + (int) (temp ^ (temp >>> 32));
1077         temp = Double.doubleToLongBits(this.startDirY);
1078         result = prime * result + (int) (temp ^ (temp >>> 32));
1079         result = prime * result + Arrays.hashCode(this.x);
1080         result = prime * result + Arrays.hashCode(this.y);
1081         result = prime * result + Arrays.hashCode(this.z);
1082         return result;
1083     }
1084 
1085     @SuppressWarnings("checkstyle:needbraces")
1086     @Override
1087     public boolean equals(final Object obj)
1088     {
1089         if (this == obj)
1090             return true;
1091         if (obj == null)
1092             return false;
1093         if (getClass() != obj.getClass())
1094             return false;
1095         PolyLine3d other = (PolyLine3d) obj;
1096         if (Double.doubleToLongBits(this.startDirZ) != Double.doubleToLongBits(other.startDirZ))
1097             return false;
1098         if (Double.doubleToLongBits(this.startDirY) != Double.doubleToLongBits(other.startDirY))
1099             return false;
1100         if (!Arrays.equals(this.x, other.x))
1101             return false;
1102         if (!Arrays.equals(this.y, other.y))
1103             return false;
1104         if (!Arrays.equals(this.z, other.z))
1105             return false;
1106         return true;
1107     }
1108 
1109 }