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