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