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