View Javadoc
1   package org.djutils.draw.line;
2   
3   import java.awt.geom.Path2D;
4   import java.awt.geom.PathIterator;
5   import java.util.ArrayList;
6   import java.util.Arrays;
7   import java.util.Iterator;
8   import java.util.List;
9   import java.util.Locale;
10  import java.util.NoSuchElementException;
11  import java.util.function.Function;
12  
13  import org.djutils.draw.DrawRuntimeException;
14  import org.djutils.draw.Drawable2d;
15  import org.djutils.draw.bounds.Bounds2d;
16  import org.djutils.draw.point.Point2d;
17  import org.djutils.exceptions.Throw;
18  import org.djutils.logger.CategoryLogger;
19  
20  /**
21   * Implementation of PolyLine for 2D space.
22   * <p>
23   * Copyright (c) 2020-2021 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
24   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
25   * </p>
26   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
27   * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
28   */
29  public class PolyLine2d implements Drawable2d, PolyLine<PolyLine2d, Point2d, Ray2d, LineSegment2d>
30  {
31      /** */
32      private static final long serialVersionUID = 20200911L;
33  
34      /** X-coordinates of the points. */
35      private final double[] x;
36  
37      /** Y-coordinates of the points. */
38      private final double[] y;
39  
40      /** The cumulative length of the line at point 'i'. */
41      private final double[] lengthIndexedLine;
42  
43      /** The length. */
44      private final double length;
45  
46      /** Bounding rectangle of this PolyLine2d. */
47      private final Bounds2d bounds;
48  
49      /** Heading at start point (only needed for degenerate PolyLine2d). */
50      private final double startHeading;
51  
52      /**
53       * Construct a new PolyLine2d from an array of double x values and an array of double y values.
54       * @param copyNeeded boolean; if true; a deep copy of the points array is stored instead of the provided array
55       * @param x double[]; the x-coordinates of the points
56       * @param y double[]; the y-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      PolyLine2d(final boolean copyNeeded, final double[] x, final double[] y) throws NullPointerException, DrawRuntimeException
62      {
63          Throw.whenNull(x, "x array may not be null");
64          Throw.whenNull(y, "y array may not be null");
65          Throw.when(x.length != y.length, DrawRuntimeException.class, "x and y arrays must have same length");
66          Throw.when(x.length < 2, DrawRuntimeException.class, "Need at least two points");
67          this.x = copyNeeded ? Arrays.copyOf(x, x.length) : x;
68          this.y = copyNeeded ? Arrays.copyOf(y, y.length) : y;
69          double minX = x[0];
70          double minY = y[0];
71          double maxX = x[0];
72          double maxY = y[0];
73          this.lengthIndexedLine = new double[x.length];
74          this.lengthIndexedLine[0] = 0.0;
75          for (int i = 1; i < x.length; i++)
76          {
77              minX = Math.min(minX, x[i]);
78              minY = Math.min(minY, y[i]);
79              maxX = Math.max(maxX, x[i]);
80              maxY = Math.max(maxY, y[i]);
81              if (x[i - 1] == x[i] && y[i - 1] == y[i])
82              {
83                  throw new DrawRuntimeException(
84                          "Degenerate PolyLine2d; point " + (i - 1) + " has the same x and y as point " + i);
85              }
86              this.lengthIndexedLine[i] = this.lengthIndexedLine[i - 1] + Math.hypot(x[i] - x[i - 1], y[i] - y[i - 1]);
87          }
88          this.length = this.lengthIndexedLine[this.lengthIndexedLine.length - 1];
89          this.bounds = new Bounds2d(minX, maxX, minY, maxY);
90          this.startHeading = Double.NaN;
91      }
92  
93      /**
94       * Construct a degenerate PolyLine2d (consisting of only one point).
95       * @param x double; the x-coordinate
96       * @param y double; the y-coordinate
97       * @param heading double; the heading in radians
98       * @throws DrawRuntimeException when x, y, or heading is NaN, or heading is infinite
99       */
100     public PolyLine2d(final double x, final double y, final double heading) throws DrawRuntimeException
101     {
102         Throw.when(Double.isNaN(x), DrawRuntimeException.class, "x may not be NaN");
103         Throw.when(Double.isNaN(y), DrawRuntimeException.class, "y may not be NaN");
104         Throw.when(Double.isNaN(heading), DrawRuntimeException.class, "heading may not be NaN");
105         Throw.when(Double.isInfinite(heading), DrawRuntimeException.class, "heading must be finite");
106         this.x = new double[] { x };
107         this.y = new double[] { y };
108         this.startHeading = heading;
109         this.length = 0;
110         this.bounds = new Bounds2d(x, x, y, y);
111         this.lengthIndexedLine = new double[] { 0.0 };
112     }
113 
114     /**
115      * Construct a degenerate PolyLine2d (consisting of only one point).
116      * @param p Point2d; the point of the degenerate PolyLine2d
117      * @param heading double; the heading in radians
118      * @throws NullPointerException when p is null
119      * @throws DrawRuntimeException when heading is infinite
120      */
121     public PolyLine2d(final Point2d p, final double heading) throws NullPointerException, DrawRuntimeException
122     {
123         this(Throw.whenNull(p, "p may not be null").x, p.y, heading);
124     }
125 
126     /**
127      * Construct a degenerate PolyLine2d (consisting of only one point).
128      * @param r Ray2d; point and heading of the degenerate PolyLine2d
129      * @throws NullPointerException when p is null
130      * @throws DrawRuntimeException when heading is infinite
131      */
132     public PolyLine2d(final Ray2d r) throws NullPointerException, DrawRuntimeException
133     {
134         this(Throw.whenNull(r, "r may not be NaN").x, r.y, r.phi);
135     }
136 
137     /**
138      * Construct a new PolyLine2d from an array of Point2d. This constructor makes a deep copy of the parameters.
139      * @param x double[]; the x-coordinates of the points
140      * @param y double[]; the y-coordinates of the points
141      * @throws NullPointerException when iterator is null
142      * @throws DrawRuntimeException when the provided points do not constitute a valid line (too few points or identical
143      *             adjacent points)
144      */
145     public PolyLine2d(final double[] x, final double[] y) throws NullPointerException, DrawRuntimeException
146     {
147         this(true, x, y);
148     }
149 
150     /**
151      * Construct a new PolyLine2d from an array of Point2d.
152      * @param points Point2d[]; the array of points to construct this PolyLine2d from.
153      * @throws NullPointerException when the array is null
154      * @throws DrawRuntimeException when the provided points do not constitute a valid line (too few points or identical
155      *             adjacent points)
156      */
157     public PolyLine2d(final Point2d[] points) throws NullPointerException, DrawRuntimeException
158     {
159         this(false, makeArray(Throw.whenNull(points, "points may not be null"), p -> p.x), makeArray(points, p -> p.y));
160     }
161 
162     /**
163      * Make an array of double an fill it with the appropriate coordinate of points.
164      * @param points Point2d[]; array of points
165      * @param getter Function&lt;Point2d, Double&gt;; function that obtains the intended coordinate
166      * @return double[]; array of double filled with the requested coordinate values
167      */
168     protected static double[] makeArray(final Point2d[] points, final Function<Point2d, Double> getter)
169     {
170         double[] array = new double[points.length];
171         for (int index = 0; index < points.length; index++)
172         {
173             array[index] = getter.apply(points[index]);
174         }
175         return array;
176     }
177 
178     /**
179      * Construct a new PolyLine2d from an array of Point2d.
180      * @param point1 Point2d; starting point of the PolyLine2d
181      * @param point2 Point2d; second point of the PolyLine2d
182      * @param otherPoints Point2d...; additional points of the PolyLine2d (may be null)
183      * @throws NullPointerException when iterator is null
184      * @throws DrawRuntimeException when the provided points do not constitute a valid line (too few points or identical
185      *             adjacent points)
186      */
187     public PolyLine2d(final Point2d.html#Point2d">Point2d point1, final Point2d point2, final Point2d... otherPoints)
188             throws NullPointerException, DrawRuntimeException
189     {
190         this(spliceArray(Throw.whenNull(point1, "point1 may not be null"), Throw.whenNull(point2, "point2 may not be null"),
191                 otherPoints));
192     }
193 
194     /**
195      * Construct an array of Point2d from two points plus an array of Point2d.
196      * @param point1 Point2d; the first point (ends up at index 0 of the result)
197      * @param point2 Point2d; the second point (ends up at index 1 of the result)
198      * @param otherPoints Point2d...; may be null, may be empty. If non empty, the elements in otherPoints end up at index 2 and
199      *            up in the result
200      * @return Point2d[]; the combined array
201      */
202     private static Point2d.html#Point2d">Point2dPoint2d">Point2d[] spliceArray(final Point2d.html#Point2d">Point2d point1, final Point2d point2, final Point2d... otherPoints)
203     {
204         Point2dhtml#Point2d">Point2d[] result = new Point2d[2 + (otherPoints == null ? 0 : otherPoints.length)];
205         result[0] = point1;
206         result[1] = point2;
207         if (otherPoints != null)
208         {
209             for (int i = 0; i < otherPoints.length; i++)
210             {
211                 result[i + 2] = otherPoints[i];
212             }
213         }
214         return result;
215     }
216 
217     /**
218      * Construct a new PolyLine2d from an iterator that yields Point2d objects.
219      * @param iterator Iterator&lt;Point2d&gt;; iterator that will provide all points that constitute the new PolyLine2d
220      * @throws NullPointerException when iterator is null
221      * @throws DrawRuntimeException when the iterator provides too few points, or some adjacent identical points)
222      */
223     public PolyLine2d(final Iterator<Point2d> iterator) throws NullPointerException, DrawRuntimeException
224     {
225         this(iteratorToList(Throw.whenNull(iterator, "iterator cannot be null")));
226     }
227 
228     /**
229      * Construct a new PolyLine2d from a List&lt;Point2d&gt;.
230      * @param pointList List&lt;Point2d&gt;; the list of points to construct this PolyLine2d from.
231      * @throws DrawRuntimeException when the provided points do not constitute a valid line (too few points or identical
232      *             adjacent points)
233      * @throws NullPointerException when pointList is null
234      */
235     public PolyLine2d(final List<Point2d> pointList) throws DrawRuntimeException, NullPointerException
236     {
237         this(pointList.toArray(new Point2d[Throw.whenNull(pointList, "pointList may not be null").size()]));
238     }
239 
240     /**
241      * Construct a new PolyLine2d (closed shape) from a Path2D.
242      * @param path Path2D; the Path2D to construct this PolyLine2d from.
243      * @throws DrawRuntimeException when the provided points do not constitute a valid line (too few points or identical
244      *             adjacent points)
245      * @throws NullPointerException when path is null
246      */
247     public PolyLine2d(final Path2D path) throws DrawRuntimeException, NullPointerException
248     {
249         this(path2DtoArray(Throw.whenNull(path, "path may not be null")));
250     }
251 
252     /**
253      * Convert a path2D to a Point2d[] array to construct the line.
254      * @param path Path2D; the path to convert
255      * @return Point2d[]; an array of points based on MOVETO and LINETO elements of the Path2D
256      * @throws DrawRuntimeException when the pathIterator of the path returns an unsupported command
257      */
258     private static Point2d[] path2DtoArray(final Path2D path) throws DrawRuntimeException
259     {
260         List<Point2d> result = new ArrayList<>();
261         for (PathIterator pi = path.getPathIterator(null); !pi.isDone(); pi.next())
262         {
263             double[] p = new double[6];
264             int segType = pi.currentSegment(p);
265             if (segType == PathIterator.SEG_MOVETO || segType == PathIterator.SEG_LINETO)
266             {
267                 result.add(new Point2d(p[0], p[1]));
268             }
269             else if (segType == PathIterator.SEG_CLOSE)
270             {
271                 if (!result.get(0).equals(result.get(result.size() - 1)))
272                 {
273                     result.add(result.get(0));
274                 }
275                 break;
276             }
277             else
278             {
279                 throw new DrawRuntimeException("path2DtoArray only handles SEG_MOVETO, SEG_LINETO and SEG_CLOSE");
280             }
281         }
282         return result.toArray(new Point2d[result.size() - 1]);
283     }
284 
285     /**
286      * Build a list from the Point2d objects that an iterator provides.
287      * @param iterator Iterator&lt;Point2d&gt;; the iterator that will provide the points
288      * @return List&lt;Point2d&gt;; a list of the points provided by the iterator
289      */
290     protected static List<Point2d> iteratorToList(final Iterator<Point2d> iterator)
291     {
292         List<Point2d> result = new ArrayList<>();
293         iterator.forEachRemaining(result::add);
294         return result;
295     }
296 
297     /**
298      * Create a new PolyLine2d, optionally filtering out repeating successive points.
299      * @param filterDuplicates boolean; if true; filter out successive repeated points; otherwise do not filter
300      * @param points Point2d...; the coordinates of the line as Point2d
301      * @throws DrawRuntimeException when number of points &lt; 2
302      */
303     public PolyLine2d(final boolean filterDuplicates, final Point2d... points) throws DrawRuntimeException
304     {
305         this(PolyLine2d.cleanPoints(filterDuplicates, Arrays.stream(points).iterator()));
306     }
307 
308     /**
309      * Create a new PolyLine2d, optionally filtering out repeating successive points.
310      * @param filterDuplicates boolean; if true; filter out successive repeated points; otherwise do not filter
311      * @param pointList List&lt;Point2d&gt;; list of the coordinates of the line as Point3d; any duplicate points in this list
312      *            are removed (this method may modify the provided list)
313      * @throws DrawRuntimeException when number of non-equal points &lt; 2
314      */
315     public PolyLine2d(final boolean filterDuplicates, final List<Point2d> pointList) throws DrawRuntimeException
316     {
317         this(PolyLine2d.cleanPoints(filterDuplicates, pointList.iterator()));
318     }
319 
320     /**
321      * Return an iterator that optionally skips identical successive points.
322      * @param filter boolean; if true; filter out itentical successive points; if false; do not filter
323      * @param iterator Iterator&lt;Point2d&gt;; iterator that generates points, potentially with successive duplicates
324      * @return Iterator&lt;Point2d&gt;; iterator that skips identical successive points
325      */
326     static Iterator<Point2d> cleanPoints(final boolean filter, final Iterator<Point2d> iterator)
327     {
328         Throw.whenNull(iterator, "Iterator may not be null");
329         Throw.when(!iterator.hasNext(), DrawRuntimeException.class, "Iterator has no points to return");
330         if (!filter)
331         {
332             return iterator;
333         }
334         return new Iterator<Point2d>()
335         {
336             private Point2d currentPoint = iterator.next();
337 
338             @Override
339             public boolean hasNext()
340             {
341                 return this.currentPoint != null;
342             }
343 
344             @Override
345             public Point2d next()
346             {
347                 Throw.when(this.currentPoint == null, NoSuchElementException.class, "Out of input");
348                 Point2d result = this.currentPoint;
349                 this.currentPoint = null;
350                 while (iterator.hasNext())
351                 {
352                     this.currentPoint = iterator.next();
353                     if (result.x != this.currentPoint.x || result.y != this.currentPoint.y)
354                     {
355                         break;
356                     }
357                     this.currentPoint = null;
358                 }
359                 return result;
360             }
361         };
362     }
363 
364     /** {@inheritDoc} */
365     @Override
366     public PolyLine2d instantiate(final List<Point2d> pointList) throws NullPointerException, DrawRuntimeException
367     {
368         return new PolyLine2d(pointList);
369     }
370 
371     /** {@inheritDoc} */
372     @Override
373     public int size()
374     {
375         return this.x.length;
376     }
377 
378     /** {@inheritDoc} */
379     @Override
380     public final Point2d get(final int i) throws IndexOutOfBoundsException
381     {
382         return new Point2d(this.x[i], this.y[i]);
383     }
384 
385     /** {@inheritDoc} */
386     @Override
387     public final double getX(final int i) throws IndexOutOfBoundsException
388     {
389         return this.x[i];
390     }
391 
392     /** {@inheritDoc} */
393     @Override
394     public final double getY(final int i) throws IndexOutOfBoundsException
395     {
396         return this.y[i];
397     }
398 
399     /** {@inheritDoc} */
400     @Override
401     public LineSegment2d getSegment(final int index)
402     {
403         Throw.when(index < 0 || index >= this.x.length - 1, DrawRuntimeException.class, "index must be in range 0..size() - 1");
404         return new LineSegment2d(this.x[index], this.y[index], this.x[index + 1], this.y[index + 1]);
405     }
406 
407     /** {@inheritDoc} */
408     @Override
409     public final double lengthAtIndex(final int index)
410     {
411         return this.lengthIndexedLine[index];
412     }
413 
414     /** {@inheritDoc} */
415     @Override
416     public double getLength()
417     {
418         return this.length;
419     }
420 
421     /** {@inheritDoc} */
422     @Override
423     public Iterator<Point2d> getPoints()
424     {
425         return new Iterator<Point2d>()
426         {
427             private int nextIndex = 0;
428 
429             /** {@inheritDoc} */
430             @Override
431             public boolean hasNext()
432             {
433                 return this.nextIndex < size();
434             }
435 
436             /** {@inheritDoc} */
437             @Override
438             public Point2d next()
439             {
440                 return get(this.nextIndex++);
441             }
442         };
443     }
444 
445     /** {@inheritDoc} */
446     @Override
447     public Bounds2d getBounds()
448     {
449         return this.bounds;
450     }
451 
452     /** {@inheritDoc} */
453     @Override
454     public final PolyLine2d noiseFilteredLine(final double noiseLevel)
455     {
456         if (this.size() <= 2)
457         {
458             return this; // Except for some cached fields; a PolyLine2d is immutable; so safe to return
459         }
460         Point2d prevPoint = null;
461         List<Point2d> list = new ArrayList<>();
462         for (int index = 0; index < this.size(); index++)
463         {
464             Point2d currentPoint = get(index);
465             if (null != prevPoint && prevPoint.distance(currentPoint) < noiseLevel)
466             {
467                 if (index == this.size() - 1)
468                 {
469                     if (list.size() > 1)
470                     {
471                         // Replace the last point of the result by the last point of this PolyLine2d
472                         list.set(list.size() - 1, currentPoint);
473                     }
474                     else
475                     {
476                         // Append the last point of this even though it is close to the first point than the noise value to
477                         // comply with the requirement that first and last point of this are ALWAYS included in the result.
478                         list.add(currentPoint);
479                     }
480                 }
481                 continue; // Do not replace prevPoint by currentPoint
482             }
483             list.add(currentPoint);
484             prevPoint = currentPoint;
485         }
486         if (list.size() == this.x.length)
487         {
488             return this;
489         }
490         if (list.size() == 2 && list.get(0).equals(list.get(1)))
491         {
492             // Insert point 1 of this; it MUST be different from point 0; so we don't have to test for anything.
493             list.add(1, get(1));
494         }
495         try
496         {
497             return new PolyLine2d(list);
498         }
499         catch (DrawRuntimeException exception)
500         {
501             // Cannot happen
502             CategoryLogger.always().error(exception);
503             throw new Error(exception);
504         }
505     }
506 
507     /**
508      * Concatenate several PolyLine2d instances.
509      * @param lines PolyLine2d...; One or more PolyLine2d objects. The last point of the first &lt;strong&gt;must&lt;/strong&gt;
510      *            match the first of the second, etc.
511      * @return PolyLine2d
512      * @throws DrawRuntimeException if zero lines are given, or when there is a gap between consecutive lines
513      */
514     public static PolyLine2d concatenate(final PolyLine2d... lines) throws DrawRuntimeException
515     {
516         return concatenate(0.0, lines);
517     }
518 
519     /**
520      * Concatenate two PolyLine2d instances. This method is separate for efficiency reasons.
521      * @param tolerance double; the tolerance between the end point of a line and the first point of the next line
522      * @param line1 PolyLine2d; first line
523      * @param line2 PolyLine2d; second line
524      * @return PolyLine2d; the concatenation of the two lines
525      * @throws DrawRuntimeException if zero lines are given, or when there is a gap between consecutive lines
526      */
527     public static PolyLine2d concatenate(final PolyLine2d.html#PolyLine2d">PolyLine2djxr_keyword">double tolerance, final PolyLine2d.html#PolyLine2d">PolyLine2d line1, final PolyLine2d line2)
528             throws DrawRuntimeException
529     {
530         if (line1.getLast().distance(line2.getFirst()) > tolerance)
531         {
532             throw new DrawRuntimeException("Lines are not connected: " + line1.getLast() + " to " + line2.getFirst()
533                     + " distance is " + line1.getLast().distance(line2.getFirst()) + " > " + tolerance);
534         }
535         int size = line1.size() + line2.size() - 1;
536         Point2dhtml#Point2d">Point2d[] points = new Point2d[size];
537         int nextIndex = 0;
538         for (int j = 0; j < line1.size(); j++)
539         {
540             points[nextIndex++] = line1.get(j);
541         }
542         for (int j = 1; j < line2.size(); j++)
543         {
544             points[nextIndex++] = line2.get(j);
545         }
546         return new PolyLine2d(points);
547     }
548 
549     /**
550      * Concatenate several PolyLine2d instances.
551      * @param tolerance double; the tolerance between the end point of a line and the first point of the next line
552      * @param lines PolyLine2d...; one or more PolyLine2d objects. The last point of the first &lt;strong&gt;must&lt;/strong&gt;
553      *            match the first of the second, etc.
554      * @return PolyLine2d; the concatenation of the lines
555      * @throws DrawRuntimeException if zero lines are given, or when there is a gap between consecutive lines
556      */
557     public static PolyLine2d concatenate(final double tolerance, final PolyLine2d... lines) throws DrawRuntimeException
558     {
559         if (0 == lines.length)
560         {
561             throw new DrawRuntimeException("Empty argument list");
562         }
563         else if (1 == lines.length)
564         {
565             return lines[0];
566         }
567         int size = lines[0].size();
568         for (int i = 1; i < lines.length; i++)
569         {
570             if (lines[i - 1].getLast().distance(lines[i].getFirst()) > tolerance)
571             {
572                 throw new DrawRuntimeException(
573                         "Lines are not connected: " + lines[i - 1].getLast() + " to " + lines[i].getFirst() + " distance is "
574                                 + lines[i - 1].getLast().distance(lines[i].getFirst()) + " > " + tolerance);
575             }
576             size += lines[i].size() - 1;
577         }
578         Point2dhtml#Point2d">Point2d[] points = new Point2d[size];
579         int nextIndex = 0;
580         for (int i = 0; i < lines.length; i++)
581         {
582             PolyLine2d line = lines[i];
583             for (int j = 0 == i ? 0 : 1; j < line.size(); j++)
584             {
585                 points[nextIndex++] = line.get(j);
586             }
587         }
588         return new PolyLine2d(points);
589     }
590 
591     /** {@inheritDoc} */
592     @Override
593     public final Ray2d getLocationExtended(final double position)
594     {
595         if (position >= 0.0 && position <= getLength())
596         {
597             try
598             {
599                 return getLocation(position);
600             }
601             catch (DrawRuntimeException exception)
602             {
603                 // cannot happen
604             }
605         }
606 
607         // position before start point -- extrapolate using direction from first point to second point of this PolyLine2d
608         if (position < 0.0)
609         {
610             double fraction = position / (this.lengthIndexedLine[1] - this.lengthIndexedLine[0]);
611             return new Ray2d(this.x[0] + fraction * (this.x[1] - this.x[0]), this.y[0] + fraction * (this.y[1] - this.y[0]),
612                     this.x[1], this.y[1]);
613         }
614 
615         // position beyond end point -- extrapolate using the direction from the before last point to the last point of this
616         // PolyLine2d
617         int n1 = this.x.length - 1; // index of last point
618         int n2 = this.x.length - 2; // index of before last point
619         double len = position - getLength();
620         double fraction = len / (this.lengthIndexedLine[n1] - this.lengthIndexedLine[n2]);
621         while (Double.isInfinite(fraction))
622         {
623             // Overflow occurred; move n2 back another point; if possible
624             if (--n2 < 0)
625             {
626                 CategoryLogger.always().error("lengthIndexedLine of {} is invalid", this);
627                 return new Ray2d(this.x[n1], this.y[n1], 0.0); // Bogus direction
628             }
629             fraction = len / (this.lengthIndexedLine[n1] - this.lengthIndexedLine[n2]);
630         }
631         return new Ray2d(this.x[n1] + fraction * (this.x[n1] - this.x[n2]), this.y[n1] + fraction * (this.y[n1] - this.y[n2]),
632                 Math.atan2(this.y[n1] - this.y[n2], this.x[n1] - this.x[n2]));
633     }
634 
635     /** {@inheritDoc} */
636     @Override
637     public final Ray2d getLocation(final double position) throws DrawRuntimeException
638     {
639         Throw.when(Double.isNaN(position), DrawRuntimeException.class, "position may not be NaN");
640         Throw.when(position < 0.0 || position > getLength(), DrawRuntimeException.class,
641                 "getLocation for line: position < 0.0 or > line length. Position = " + position + "; length = " + getLength());
642         // handle special cases: position == 0.0, or position == length
643         if (position == 0.0)
644         {
645             if (this.lengthIndexedLine.length == 1) // Extra special case; degenerate PolyLine2d
646             {
647                 return new Ray2d(this.x[0], this.y[0], this.startHeading);
648             }
649             return new Ray2d(this.x[0], this.y[0], this.x[1], this.y[1]);
650         }
651         if (position == getLength())
652         {
653             return new Ray2d(this.x[this.x.length - 1], this.y[this.x.length - 1],
654                     2 * this.x[this.x.length - 1] - this.x[this.x.length - 2],
655                     2 * this.y[this.x.length - 1] - this.y[this.x.length - 2]);
656         }
657         // find the index of the line segment, use binary search
658         int index = find(position);
659         double remainder = position - this.lengthIndexedLine[index];
660         double fraction = remainder / (this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index]);
661         // if (fraction >= 1.0 && index < this.x.length - 1)
662         // {
663         // // Rounding problem; move to the next segment.
664         // index++;
665         // remainder = position - this.lengthIndexedLine[index];
666         // fraction = remainder / (this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index]);
667         // }
668         return new Ray2d(this.x[index] + fraction * (this.x[index + 1] - this.x[index]),
669                 this.y[index] + fraction * (this.y[index + 1] - this.y[index]), 2 * this.x[index + 1] - this.x[index],
670                 2 * this.y[index + 1] - this.y[index]);
671     }
672 
673     /**
674      * Perform the orthogonal projection operation.
675      * @param point Point2d; the point to project
676      * @param limitHandling Boolean; if Null; results outside the interval 0.0 .. 1.0 are replaced by NaN, if false, results
677      *            outside that interval are returned as is; if true results outside the interval are truncated to the interval
678      *            and therefore not truly orthogonal
679      * @return double; the fractional position on this PolyLine that is closest to point, or NaN
680      */
681     private double projectOrthogonalFractional(final Point2d point, final Boolean limitHandling)
682     {
683         Throw.whenNull(point, "point may not be null");
684         double result = Double.NaN;
685         if (this.lengthIndexedLine.length == 1)
686         {
687             // This is a degenerate PolyLine2d
688             if (null != limitHandling && limitHandling)
689             {
690                 return 0.0;
691             }
692             result = getLocation(0.0).projectOrthogonalFractionalExtended(point);
693             if (null == limitHandling)
694             {
695                 return result == 0.0 ? 0.0 : Double.NaN;
696             }
697             // limitHanling is false
698             if (result == 0.0)
699             {
700                 return 0.0;
701             }
702             return result > 0 ? Double.POSITIVE_INFINITY : Double.NEGATIVE_INFINITY;
703         }
704         double bestDistance = Double.POSITIVE_INFINITY;
705         double bestDistanceExtended = Double.POSITIVE_INFINITY;
706         for (int index = 1; index < this.size(); index++)
707         {
708             double fraction = point.fractionalPositionOnLine(this.x[index - 1], this.y[index - 1], this.x[index], this.y[index],
709                     false, false);
710             double distance = Math.hypot(point.x - (this.x[index - 1] + fraction * (this.x[index] - this.x[index - 1])),
711                     point.y - (this.y[index - 1] + fraction * (this.y[index] - this.y[index - 1])));
712             if (distance < bestDistanceExtended && (fraction >= 0.0 && fraction <= 1.0 || (fraction < 0.0 && index == 1)
713                     || fraction > 1.0 && index == this.size() - 1))
714             {
715                 bestDistanceExtended = distance;
716             }
717             if (distance < bestDistance && (fraction >= 0.0 || index == 1 && limitHandling != null && !limitHandling)
718                     && (fraction <= 1.0 || index == this.size() - 1 && limitHandling != null && !limitHandling))
719             {
720                 bestDistance = distance;
721                 result = lengthAtIndex(index - 1) + fraction * (lengthAtIndex(index) - lengthAtIndex(index - 1));
722             }
723             else if (fraction < 0.0 && limitHandling != null && limitHandling)
724             {
725                 distance = Math.hypot(point.x - this.x[index - 1], point.y - this.y[index - 1]);
726                 if (distance < bestDistance)
727                 {
728                     bestDistance = distance;
729                     result = lengthAtIndex(index - 1);
730                 }
731             }
732             else if (index == this.size() - 1 && limitHandling != null && limitHandling)
733             {
734                 distance = Math.hypot(point.x - this.x[index], point.y - this.y[index]);
735                 if (distance < bestDistance)
736                 {
737                     bestDistance = distance;
738                     result = lengthAtIndex(index);
739                 }
740             }
741         }
742         if (bestDistance > bestDistanceExtended && (limitHandling == null || !limitHandling))
743         {
744             return Double.NaN;
745         }
746         return result / getLength();
747     }
748 
749     /** {@inheritDoc} */
750     @Override
751     public Point2dPoint2d closestPointOnPolyLine(final Point2d point)
752     {
753         try
754         {
755             return getLocation(projectOrthogonalFractional(point, true) * getLength());
756         }
757         catch (DrawRuntimeException e)
758         {
759             // Cannot happen
760             e.printStackTrace();
761             return null;
762         }
763     }
764 
765     /**
766      * Perform the project orthogonal operation.
767      * @param point Point2d; the point to project
768      * @param limitHandling Boolean; if Null; results outside this PolyLin2de are replaced by Null, if false, results outside
769      *            that interval are returned as is; if true results outside this PolyLine2d are truncated to the first or last
770      *            point of this PolyLine2d and therefore not truly orthogonal
771      * @return Point2d; the orthogonal projection of point on this PolyLine2d
772      */
773     private Point2dt2d">Point2d projectOrthogonal(final Point2d point, final Boolean limitHandling)
774     {
775         Throw.whenNull(point, "point may not be null");
776         if (this.lengthIndexedLine.length == 1) // Handle degenerate case
777         {
778             // limitHandling == true is not handled because it cannot happen
779             Point2d result = this.getLocation(0.0).projectOrthogonalExtended(point);
780             if (null == limitHandling)
781             {
782                 return result.x != this.x[0] || result.y != this.y[0] ? null : get(0);
783             }
784             // limitHandling is false
785             return result;
786         }
787         double fraction = projectOrthogonalFractional(point, limitHandling);
788         if (Double.isNaN(fraction))
789         {
790             return null;
791         }
792         return getLocationExtended(fraction * getLength());
793     }
794 
795     /** {@inheritDoc} */
796     @Override
797     public Point2dt2d">Point2d projectOrthogonal(final Point2d point) throws NullPointerException
798     {
799         return projectOrthogonal(point, null);
800     }
801 
802     /** {@inheritDoc} */
803     @Override
804     public Point2dnt2d projectOrthogonalExtended(final Point2d point) throws NullPointerException
805     {
806         return projectOrthogonal(point, false);
807     }
808 
809     /** {@inheritDoc} */
810     @Override
811     public final double projectOrthogonalFractional(final Point2d point) throws NullPointerException
812     {
813         return projectOrthogonalFractional(point, null);
814     }
815 
816     /** {@inheritDoc} */
817     @Override
818     public double projectOrthogonalFractionalExtended(final Point2d point) throws NullPointerException
819     {
820         return projectOrthogonalFractional(point, false);
821     }
822 
823     /** {@inheritDoc} */
824     @Override
825     public PolyLine2d extract(final double start, final double end) throws DrawRuntimeException
826     {
827         if (Double.isNaN(start) || Double.isNaN(end) || start < 0 || start >= end || end > getLength())
828         {
829             throw new DrawRuntimeException(
830                     "Bad interval (" + start + ".." + end + "; length of this PolyLine2d is " + this.getLength() + ")");
831         }
832         double cumulativeLength = 0;
833         double nextCumulativeLength = 0;
834         double segmentLength = 0;
835         int index = 0;
836         List<Point2d> pointList = new ArrayList<>();
837         while (start > cumulativeLength)
838         {
839             Point2d fromPoint = get(index);
840             index++;
841             Point2d toPoint = get(index);
842             segmentLength = fromPoint.distance(toPoint);
843             cumulativeLength = nextCumulativeLength;
844             nextCumulativeLength = cumulativeLength + segmentLength;
845             if (nextCumulativeLength >= start)
846             {
847                 break;
848             }
849         }
850         if (start == nextCumulativeLength)
851         {
852             pointList.add(get(index));
853         }
854         else
855         {
856             pointList.add(get(index - 1).interpolate(get(index), (start - cumulativeLength) / segmentLength));
857             if (end > nextCumulativeLength)
858             {
859                 pointList.add(get(index));
860             }
861         }
862         while (end > nextCumulativeLength)
863         {
864             Point2d fromPoint = get(index);
865             index++;
866             if (index >= size())
867             {
868                 break; // rounding error
869             }
870             Point2d toPoint = get(index);
871             segmentLength = fromPoint.distance(toPoint);
872             cumulativeLength = nextCumulativeLength;
873             nextCumulativeLength = cumulativeLength + segmentLength;
874             if (nextCumulativeLength >= end)
875             {
876                 break;
877             }
878             pointList.add(toPoint);
879         }
880         if (end == nextCumulativeLength)
881         {
882             pointList.add(get(index));
883         }
884         else if (index < this.x.length)
885         {
886             Point2d point = get(index - 1).interpolate(get(index), (end - cumulativeLength) / segmentLength);
887             // can be the same due to rounding
888             if (!point.equals(pointList.get(pointList.size() - 1)))
889             {
890                 pointList.add(point);
891             }
892         }
893         // else rounding error
894         try
895         {
896             return instantiate(pointList);
897         }
898         catch (DrawRuntimeException exception)
899         {
900             CategoryLogger.always().error(exception, "interval " + start + ".." + end + " too short");
901             throw new DrawRuntimeException("interval " + start + ".." + end + "too short");
902         }
903     }
904 
905     /** {@inheritDoc} */
906     @Override
907     public PolyLine2d truncate(final double position) throws DrawRuntimeException
908     {
909         if (position <= 0.0 || position > getLength())
910         {
911             throw new DrawRuntimeException("truncate for line: position <= 0.0 or > line length. Position = " + position
912                     + ". Length = " + getLength() + " m.");
913         }
914 
915         // handle special case: position == length
916         if (position == getLength())
917         {
918             return this;
919         }
920 
921         // find the index of the line segment
922         int index = find(position);
923         double remainder = position - lengthAtIndex(index);
924         double fraction = remainder / (lengthAtIndex(index + 1) - lengthAtIndex(index));
925         Point2d p1 = get(index);
926         Point2d lastPoint;
927         if (0.0 == fraction)
928         {
929             lastPoint = p1;
930         }
931         else
932         {
933             Point2d p2 = get(index + 1);
934             lastPoint = p1.interpolate(p2, fraction);
935             index++;
936         }
937         double[] truncatedX = new double[index + 1];
938         double[] truncatedY = new double[index + 1];
939         for (int i = 0; i < index; i++)
940         {
941             truncatedX[i] = this.x[i];
942             truncatedY[i] = this.y[i];
943         }
944         truncatedX[index] = lastPoint.x;
945         truncatedY[index] = lastPoint.y;
946         return new PolyLine2d(truncatedX, truncatedY);
947     }
948 
949     /** {@inheritDoc} */
950     @Override
951     @SuppressWarnings("checkstyle:methodlength")
952     public PolyLine2d offsetLine(final double offset, final double circlePrecision, final double offsetMinimumFilterValue,
953             final double offsetMaximumFilterValue, final double offsetFilterRatio, final double minimumOffset)
954             throws IllegalArgumentException
955     {
956         Throw.when(Double.isNaN(offset), IllegalArgumentException.class, "Offset may not be NaN");
957         Throw.when(Double.isNaN(circlePrecision) || circlePrecision <= 0, IllegalArgumentException.class,
958                 "bad circlePrecision");
959         Throw.when(Double.isNaN(offsetMinimumFilterValue) || offsetMinimumFilterValue <= 0, IllegalArgumentException.class,
960                 "bad offsetMinimumFilterValue");
961         Throw.when(Double.isNaN(offsetMaximumFilterValue) || offsetMaximumFilterValue <= 0, IllegalArgumentException.class,
962                 "bad offsetMaximumFilterValue");
963         Throw.when(Double.isNaN(offsetFilterRatio) || offsetFilterRatio <= 0, IllegalArgumentException.class,
964                 "bad offsetFilterRatio");
965         Throw.when(Double.isNaN(minimumOffset) || minimumOffset <= 0, IllegalArgumentException.class, "bad minimumOffset");
966         Throw.when(offsetMinimumFilterValue >= offsetMaximumFilterValue, IllegalArgumentException.class,
967                 "bad offset filter values; minimum must be less than maximum");
968         double bufferOffset = Math.abs(offset);
969         if (bufferOffset < minimumOffset)
970         {
971             return this;
972         }
973 
974         PolyLine2d filteredReferenceLine = noiseFilteredLine(
975                 Math.max(offsetMinimumFilterValue, Math.min(bufferOffset / offsetFilterRatio, offsetMaximumFilterValue)));
976         List<Point2d> tempPoints = new ArrayList<>();
977         // Make good use of the fact that PolyLine3d cannot have consecutive duplicate points and has > 1 points
978         Point2d prevPoint = filteredReferenceLine.get(0);
979         Double prevAngle = null;
980         for (int index = 0; index < filteredReferenceLine.size() - 1; index++)
981         {
982             Point2d nextPoint = filteredReferenceLine.get(index + 1);
983             double angle = Math.atan2(nextPoint.y - prevPoint.y, nextPoint.x - prevPoint.x);
984             Point2dl#Point2d">Point2d segmentFrom = new Point2d(prevPoint.x - Math.sin(angle) * offset, prevPoint.y + Math.cos(angle) * offset);
985             Point2dtml#Point2d">Point2d segmentTo = new Point2d(nextPoint.x - Math.sin(angle) * offset, nextPoint.y + Math.cos(angle) * offset);
986             boolean addSegment = true;
987             if (index > 0)
988             {
989                 double deltaAngle = angle - prevAngle;
990                 if (Math.abs(deltaAngle) > Math.PI)
991                 {
992                     deltaAngle -= Math.signum(deltaAngle) * 2 * Math.PI;
993                 }
994                 if (deltaAngle * offset <= 0)
995                 {
996                     // Outside of curve of reference line
997                     // Approximate an arc using straight segments.
998                     // Determine how many segments are needed.
999                     int numSegments = 1;
1000                     if (Math.abs(deltaAngle) > Math.PI / 2)
1001                     {
1002                         numSegments = 2;
1003                     }
1004                     while (true)
1005                     {
1006                         double maxError = bufferOffset * (1 - Math.abs(Math.cos(deltaAngle / numSegments / 2)));
1007                         if (maxError < circlePrecision)
1008                         {
1009                             break; // required precision reached
1010                         }
1011                         numSegments *= 2;
1012                     }
1013                     Point2d prevArcPoint = tempPoints.get(tempPoints.size() - 1);
1014                     // Generate the intermediate points
1015                     for (int additionalPoint = 1; additionalPoint < numSegments; additionalPoint++)
1016                     {
1017                         double intermediateAngle =
1018                                 (additionalPoint * angle + (numSegments - additionalPoint) * prevAngle) / numSegments;
1019                         if (prevAngle * angle < 0 && Math.abs(prevAngle) > Math.PI / 2 && Math.abs(angle) > Math.PI / 2)
1020                         {
1021                             intermediateAngle += Math.PI;
1022                         }
1023                         Point2dt2d">Point2d intermediatePoint = new Point2d(prevPoint.x - Math.sin(intermediateAngle) * offset,
1024                                 prevPoint.y + Math.cos(intermediateAngle) * offset);
1025                         // Find any intersection points of the new segment and all previous segments
1026                         Point2d prevSegFrom = null;
1027                         int stopAt = tempPoints.size();
1028                         for (int i = 0; i < stopAt; i++)
1029                         {
1030                             Point2d prevSegTo = tempPoints.get(i);
1031                             if (null != prevSegFrom)
1032                             {
1033                                 Point2d prevSegIntersection = Point2d.intersectionOfLineSegments(prevArcPoint,
1034                                         intermediatePoint, prevSegFrom, prevSegTo);
1035                                 if (null != prevSegIntersection && prevSegIntersection.distance(prevArcPoint) > circlePrecision
1036                                         && prevSegIntersection.distance(prevSegFrom) > circlePrecision
1037                                         && prevSegIntersection.distance(prevSegTo) > circlePrecision)
1038                                 {
1039                                     tempPoints.add(prevSegIntersection);
1040                                     // System.out.println(new OTSLine3D(tempPoints).toPlot());
1041                                 }
1042                             }
1043                             prevSegFrom = prevSegTo;
1044                         }
1045                         Point2d nextSegmentIntersection =
1046                                 Point2d.intersectionOfLineSegments(prevSegFrom, intermediatePoint, segmentFrom, segmentTo);
1047                         if (null != nextSegmentIntersection)
1048                         {
1049                             tempPoints.add(nextSegmentIntersection);
1050                             // System.out.println(new OTSLine3D(tempPoints).toPlot());
1051                         }
1052                         tempPoints.add(intermediatePoint);
1053                         // System.out.println(new OTSLine3D(tempPoints).toPlot());
1054                         prevArcPoint = intermediatePoint;
1055                     }
1056                 }
1057                 // Inside of curve of reference line.
1058                 // Add the intersection point of each previous segment and the next segment
1059                 Point2d pPoint = null;
1060                 int currentSize = tempPoints.size(); // PK DO NOT use the "dynamic" limit
1061                 for (int i = 0; i < currentSize /* tempPoints.size() */; i++)
1062                 {
1063                     Point2d p = tempPoints.get(i);
1064                     if (null != pPoint)
1065                     {
1066                         double pAngle = Math.atan2(p.y - pPoint.y, p.x - pPoint.x);
1067                         double angleDifference = angle - pAngle;
1068                         if (Math.abs(angleDifference) > Math.PI)
1069                         {
1070                             angleDifference -= Math.signum(angleDifference) * 2 * Math.PI;
1071                         }
1072                         if (Math.abs(angleDifference) > 0)// 0.01)
1073                         {
1074                             Point2d intersection = Point2d.intersectionOfLineSegments(pPoint, p, segmentFrom, segmentTo);
1075                             if (null != intersection)
1076                             {
1077                                 if (tempPoints.size() - 1 == i)
1078                                 {
1079                                     tempPoints.remove(tempPoints.size() - 1);
1080                                     segmentFrom = intersection;
1081                                 }
1082                                 else
1083                                 {
1084                                     tempPoints.add(intersection);
1085                                 }
1086                             }
1087                         }
1088                         else
1089                         {
1090                             // This is where things went very wrong in the TestGeometry demo.
1091                             if (i == tempPoints.size() - 1)
1092                             {
1093                                 tempPoints.remove(tempPoints.size() - 1);
1094                                 segmentFrom = tempPoints.get(tempPoints.size() - 1);
1095                                 tempPoints.remove(tempPoints.size() - 1);
1096                             }
1097                         }
1098                     }
1099                     pPoint = p;
1100                 }
1101             }
1102             if (addSegment)
1103             {
1104                 tempPoints.add(segmentFrom);
1105                 tempPoints.add(segmentTo);
1106                 prevPoint = nextPoint;
1107                 prevAngle = angle;
1108             }
1109         }
1110         // Remove points that are closer than the specified offset
1111         for (int index = 1; index < tempPoints.size() - 1; index++)
1112         {
1113             Point2d checkPoint = tempPoints.get(index);
1114             prevPoint = null;
1115             boolean tooClose = false;
1116             boolean somewhereAtCorrectDistance = false;
1117             for (int i = 0; i < filteredReferenceLine.size(); i++)
1118             {
1119                 Point2d p = filteredReferenceLine.get(i);
1120                 if (null != prevPoint)
1121                 {
1122                     Point2d closestPoint = checkPoint.closestPointOnSegment(prevPoint, p);
1123                     double distance = closestPoint.distance(checkPoint);
1124                     if (distance < bufferOffset - circlePrecision)
1125                     {
1126                         tooClose = true;
1127                         break;
1128                     }
1129                     else if (distance < bufferOffset + minimumOffset)
1130                     {
1131                         somewhereAtCorrectDistance = true;
1132                     }
1133                 }
1134                 prevPoint = p;
1135             }
1136             if (tooClose || !somewhereAtCorrectDistance)
1137             {
1138                 tempPoints.remove(index);
1139                 index--;
1140             }
1141         }
1142         try
1143         {
1144             return new PolyLine2d(true, tempPoints);
1145         }
1146         catch (DrawRuntimeException exception)
1147         {
1148             exception.printStackTrace();
1149         }
1150         return null;
1151     }
1152 
1153     /** {@inheritDoc} */
1154     @Override
1155     public PolyLine2d offsetLine(final double offsetAtStart, final double offsetAtEnd, final double circlePrecision,
1156             final double offsetMinimumFilterValue, final double offsetMaximumFilterValue, final double offsetFilterRatio,
1157             final double minimumOffset) throws IllegalArgumentException, DrawRuntimeException
1158     {
1159         if (offsetAtStart == offsetAtEnd)
1160         {
1161             return offsetLine(offsetAtStart, circlePrecision, offsetMinimumFilterValue, offsetMaximumFilterValue,
1162                     offsetFilterRatio, minimumOffset);
1163         }
1164         PolyLine2d atStart = offsetLine(offsetAtStart, circlePrecision, offsetMinimumFilterValue, offsetMaximumFilterValue,
1165                 offsetFilterRatio, minimumOffset);
1166         PolyLine2d atEnd = offsetLine(offsetAtEnd, circlePrecision, offsetMinimumFilterValue, offsetMaximumFilterValue,
1167                 offsetFilterRatio, minimumOffset);
1168         return atStart.transitionLine(atEnd, new TransitionFunction()
1169         {
1170             @Override
1171             public double function(final double fraction)
1172             {
1173                 return fraction;
1174             }
1175         });
1176     }
1177 
1178     /** {@inheritDoc} */
1179     @Override
1180     public PolyLine2dlyLine2d">PolyLine2d transitionLine(final PolyLine2d endLine, final TransitionFunction transition) throws DrawRuntimeException
1181     {
1182         Throw.whenNull(endLine, "endLine may not be null");
1183         Throw.whenNull(transition, "transition may not be null");
1184         List<Point2d> pointList = new ArrayList<>();
1185         int indexInStart = 0;
1186         int indexInEnd = 0;
1187         while (indexInStart < this.size() && indexInEnd < endLine.size())
1188         {
1189             double fractionInStart = lengthAtIndex(indexInStart) / getLength();
1190             double fractionInEnd = endLine.lengthAtIndex(indexInEnd) / endLine.getLength();
1191             if (fractionInStart < fractionInEnd)
1192             {
1193                 pointList.add(get(indexInStart).interpolate(endLine.getLocation(fractionInStart * endLine.getLength()),
1194                         transition.function(fractionInStart)));
1195                 indexInStart++;
1196             }
1197             else if (fractionInStart > fractionInEnd)
1198             {
1199                 pointList.add(this.getLocation(fractionInEnd * getLength()).interpolate(endLine.get(indexInEnd),
1200                         transition.function(fractionInEnd)));
1201                 indexInEnd++;
1202             }
1203             else
1204             {
1205                 pointList.add(this.get(indexInStart).interpolate(endLine.getLocation(fractionInEnd * endLine.getLength()),
1206                         transition.function(fractionInStart)));
1207                 indexInStart++;
1208                 indexInEnd++;
1209             }
1210         }
1211         return new PolyLine2d(true, pointList);
1212     }
1213 
1214     /**
1215      * Find a location on this PolyLine2d that is a reasonable projection of a Ray on this line. The result (if not NaN) lies on
1216      * a line perpendicular to the direction of the Ray and on some segment of this PolyLine. This method attempts to give
1217      * continuous results for continuous changes of the Ray that must be projected. There are cases where this is simply
1218      * impossible, or the optimal result is ambiguous. In these cases this method will return something that is hopefully good
1219      * enough.
1220      * @param ray Ray2d; the Ray
1221      * @return double; length along this PolyLine (some value between 0 and the length of this PolyLine) where ray projects, or
1222      *         NaN if there is no solution
1223      * @throws NullPointerException when ray is null
1224      */
1225     public double projectRay(final Ray2d ray) throws NullPointerException
1226     {
1227         Throw.whenNull(ray, "ray may not be null");
1228         double bestDistance = Double.POSITIVE_INFINITY;
1229         double positionAtBestDistance = Double.NaN;
1230         // Point2d prevPoint = null;
1231         // Define the line that is perpendicular to directedPoint, passing through directedPoint
1232         double perpendicularX = ray.x - Math.sin(ray.phi);
1233         double perpendicularY = ray.y + Math.cos(ray.phi);
1234         for (int index = 1; index < this.x.length; index++)
1235         {
1236             Point2d intersection = Point2d.intersectionOfLines(ray.x, ray.y, perpendicularX, perpendicularY, false, false,
1237                     this.x[index - 1], this.y[index - 1], this.x[index], this.y[index], true, true);
1238             if (intersection != null) // Intersection is on the segment
1239             {
1240                 double thisDistance = intersection.distance(ray);
1241                 if (thisDistance < bestDistance)
1242                 {
1243                     double distanceToPrevPoint =
1244                             Math.hypot(intersection.x - this.x[index - 1], intersection.y - this.y[index - 1]);
1245                     positionAtBestDistance = lengthAtIndex(index - 1) + distanceToPrevPoint;
1246                     bestDistance = thisDistance;
1247                 }
1248             }
1249         }
1250         return positionAtBestDistance;
1251     }
1252 
1253     /** {@inheritDoc} */
1254     @Override
1255     public String toString()
1256     {
1257         return toString("%f", false);
1258     }
1259 
1260     /** {@inheritDoc} */
1261     @Override
1262     public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
1263     {
1264         StringBuilder result = new StringBuilder();
1265         if (!doNotIncludeClassName)
1266         {
1267             result.append("PolyLine2d ");
1268         }
1269         String format = String.format("%%sx=%1$s, y=%1$s", doubleFormat);
1270         for (int index = 0; index < this.x.length; index++)
1271         {
1272             result.append(String.format(Locale.US, format, index == 0 ? "[" : ", ", this.x[index], this.y[index]));
1273         }
1274         if (this.lengthIndexedLine.length == 1)
1275         {
1276             format = String.format(", startHeading=%1$s", doubleFormat);
1277             result.append(String.format(Locale.US, format, this.startHeading));
1278         }
1279         result.append("]");
1280         return result.toString();
1281     }
1282 
1283     /** {@inheritDoc} */
1284     @Override
1285     public String toExcel()
1286     {
1287         StringBuffer s = new StringBuffer();
1288         for (int i = 0; i < this.x.length; i++)
1289         {
1290             s.append(this.x[i] + "\t" + this.y[i] + "\n");
1291         }
1292         return s.toString();
1293     }
1294 
1295     /**
1296      * Convert this PolyLine3D to Peter's plot format.
1297      * @return Peter's format plot output
1298      */
1299     public String toPlot()
1300     {
1301         StringBuffer result = new StringBuffer();
1302         for (int i = 0; i < this.x.length; i++)
1303         {
1304             result.append(String.format(Locale.US, "%s%.3f,%.3f", 0 == result.length() ? "M" : " L", this.x[i], this.y[i]));
1305         }
1306         result.append("\n");
1307         return result.toString();
1308     }
1309 
1310     /** {@inheritDoc} */
1311     @Override
1312     public int hashCode()
1313     {
1314         final int prime = 31;
1315         int result = 1;
1316         long temp;
1317         temp = Double.doubleToLongBits(this.startHeading);
1318         result = prime * result + (int) (temp ^ (temp >>> 32));
1319         result = prime * result + Arrays.hashCode(this.x);
1320         result = prime * result + Arrays.hashCode(this.y);
1321         return result;
1322     }
1323 
1324     /** {@inheritDoc} */
1325     @SuppressWarnings("checkstyle:needbraces")
1326     @Override
1327     public boolean equals(final Object obj)
1328     {
1329         if (this == obj)
1330             return true;
1331         if (obj == null)
1332             return false;
1333         if (getClass() != obj.getClass())
1334             return false;
1335         PolyLine2d../../../org/djutils/draw/line/PolyLine2d.html#PolyLine2d">PolyLine2d other = (PolyLine2d) obj;
1336         if (Double.doubleToLongBits(this.startHeading) != Double.doubleToLongBits(other.startHeading))
1337             return false;
1338         if (!Arrays.equals(this.x, other.x))
1339             return false;
1340         if (!Arrays.equals(this.y, other.y))
1341             return false;
1342         return true;
1343     }
1344 
1345 }