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