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