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