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