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