View Javadoc
1   package org.djutils.draw.line;
2   
3   import java.awt.geom.Path2D;
4   import java.awt.geom.PathIterator;
5   import java.util.ArrayList;
6   import java.util.Arrays;
7   import java.util.Iterator;
8   import java.util.List;
9   import java.util.Locale;
10  import java.util.function.Function;
11  
12  import org.djutils.draw.DrawRuntimeException;
13  import org.djutils.draw.Drawable2d;
14  import org.djutils.draw.Space2d;
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 Line 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, Space2d, 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 Line2d. */
47      private final Bounds2d bounds;
48  
49      /**
50       * Construct a new Line2d 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 Line2d 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 Line2d 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 Line2d from an iterator that yields Point2d objects.
171      * @param iterator Iterator&lt;Point2d&gt;; iterator that will provide all points that constitute the new Line2d
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 Line2d from a List&lt;Point2d&gt;.
182      * @param pointList List&lt;Point2d&gt;; the list of points to construct this Line2d 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 Line2d (closed shape) from a Path2D.
193      * @param path Path2D; the Path2D to construct this Line2d 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     /** {@inheritDoc} */
248     @Override
249     public PolyLine2d instantiate(final List<Point2d> pointList) throws NullPointerException, DrawRuntimeException
250     {
251         return new PolyLine2d(pointList);
252     }
253 
254     /** {@inheritDoc} */
255     @Override
256     public int size()
257     {
258         return this.x.length;
259     }
260 
261     /** {@inheritDoc} */
262     @Override
263     public final Point2d get(final int i) throws IndexOutOfBoundsException
264     {
265         return new Point2d(this.x[i], this.y[i]);
266     }
267 
268     /** {@inheritDoc} */
269     @Override
270     public final double getX(final int i) throws IndexOutOfBoundsException
271     {
272         return this.x[i];
273     }
274 
275     /** {@inheritDoc} */
276     @Override
277     public final double getY(final int i) throws IndexOutOfBoundsException
278     {
279         return this.y[i];
280     }
281 
282     /** {@inheritDoc} */
283     @Override
284     public LineSegment2d getSegment(final int index)
285     {
286         Throw.when(index < 0 || index >= this.x.length - 1, DrawRuntimeException.class, "index must be in range 0..size() - 1");
287         return new LineSegment2d(this.x[index], this.y[index], this.x[index + 1], this.y[index + 1]);
288     }
289 
290     /** {@inheritDoc} */
291     @Override
292     public final double lengthAtIndex(final int index)
293     {
294         return this.lengthIndexedLine[index];
295     }
296 
297     /** {@inheritDoc} */
298     @Override
299     public double getLength()
300     {
301         return this.length;
302     }
303 
304     /** {@inheritDoc} */
305     @Override
306     public Iterator<Point2d> getPoints()
307     {
308         return new Iterator<Point2d>()
309         {
310             private int nextIndex = 0;
311 
312             /** {@inheritDoc} */
313             @Override
314             public boolean hasNext()
315             {
316                 return this.nextIndex < size();
317             }
318 
319             /** {@inheritDoc} */
320             @Override
321             public Point2d next()
322             {
323                 return get(this.nextIndex++);
324             }
325         };
326     }
327 
328     /** {@inheritDoc} */
329     @Override
330     public Bounds2d getBounds()
331     {
332         return this.bounds;
333     }
334 
335     /** {@inheritDoc} */
336     @Override
337     public final PolyLine2d noiseFilteredLine(final double noiseLevel)
338     {
339         if (this.size() <= 2)
340         {
341             return this; // Except for some cached fields; an Line2d is immutable; so safe to return
342         }
343         Point2d prevPoint = null;
344         List<Point2d> list = new ArrayList<>();
345         for (int index = 0; index < this.size(); index++)
346         {
347             Point2d currentPoint = get(index);
348             if (null != prevPoint && prevPoint.distance(currentPoint) < noiseLevel)
349             {
350                 if (index == this.size() - 1)
351                 {
352                     if (list.size() > 1)
353                     {
354                         // Replace the last point of the result by the last point of this Line2d
355                         list.set(list.size() - 1, currentPoint);
356                     }
357                     else
358                     {
359                         // Append the last point of this even though it is close to the first point than the noise value to
360                         // comply with the requirement that first and last point of this are ALWAYS included in the result.
361                         list.add(currentPoint);
362                     }
363                 }
364                 continue; // Do not replace prevPoint by currentPoint
365             }
366             list.add(currentPoint);
367             prevPoint = currentPoint;
368         }
369         if (list.size() == this.x.length)
370         {
371             return this;
372         }
373         if (list.size() == 2 && list.get(0).equals(list.get(1)))
374         {
375             // Insert point 1 of this; it MUST be different from point 0; so we don't have to test for anything.
376             list.add(1, get(1));
377         }
378         try
379         {
380             return new PolyLine2d(list);
381         }
382         catch (DrawRuntimeException exception)
383         {
384             // Cannot happen
385             CategoryLogger.always().error(exception);
386             throw new Error(exception);
387         }
388     }
389 
390     /**
391      * Concatenate several Line2d instances.
392      * @param lines PolyLine2d...; Line2d... one or more Line2d. The last point of the first &lt;strong&gt;must&lt;/strong&gt;
393      *            match the first of the second, etc.
394      * @return Line2d
395      * @throws DrawRuntimeException if zero lines are given, or when there is a gap between consecutive lines
396      */
397     public static PolyLine2d concatenate(final PolyLine2d... lines) throws DrawRuntimeException
398     {
399         return concatenate(0.0, lines);
400     }
401 
402     /**
403      * Concatenate two Line2d instances. This method is separate for efficiency reasons.
404      * @param tolerance double; the tolerance between the end point of a line and the first point of the next line
405      * @param line1 PolyLine2d; first line
406      * @param line2 PolyLine2d; second line
407      * @return Line2d; the concatenation of the two lines
408      * @throws DrawRuntimeException if zero lines are given, or when there is a gap between consecutive lines
409      */
410     public static PolyLine2d concatenate(final PolyLine2d.html#PolyLine2d">PolyLine2djxr_keyword">double tolerance, final PolyLine2d.html#PolyLine2d">PolyLine2d line1, final PolyLine2d line2)
411             throws DrawRuntimeException
412     {
413         if (line1.getLast().distance(line2.getFirst()) > tolerance)
414         {
415             throw new DrawRuntimeException("Lines are not connected: " + line1.getLast() + " to " + line2.getFirst()
416                     + " distance is " + line1.getLast().distance(line2.getFirst()) + " > " + tolerance);
417         }
418         int size = line1.size() + line2.size() - 1;
419         Point2dhtml#Point2d">Point2d[] points = new Point2d[size];
420         int nextIndex = 0;
421         for (int j = 0; j < line1.size(); j++)
422         {
423             points[nextIndex++] = line1.get(j);
424         }
425         for (int j = 1; j < line2.size(); j++)
426         {
427             points[nextIndex++] = line2.get(j);
428         }
429         return new PolyLine2d(points);
430     }
431 
432     /**
433      * Concatenate several Line2d instances.
434      * @param tolerance double; the tolerance between the end point of a line and the first point of the next line
435      * @param lines PolyLine2d...; Line2d... one or more Line2d. The last point of the first &lt;strong&gt;must&lt;/strong&gt;
436      *            match the first of the second, etc.
437      * @return Line2d; the concatenation of the lines
438      * @throws DrawRuntimeException if zero lines are given, or when there is a gap between consecutive lines
439      */
440     public static PolyLine2d concatenate(final double tolerance, final PolyLine2d... lines) throws DrawRuntimeException
441     {
442         if (0 == lines.length)
443         {
444             throw new DrawRuntimeException("Empty argument list");
445         }
446         else if (1 == lines.length)
447         {
448             return lines[0];
449         }
450         int size = lines[0].size();
451         for (int i = 1; i < lines.length; i++)
452         {
453             if (lines[i - 1].getLast().distance(lines[i].getFirst()) > tolerance)
454             {
455                 throw new DrawRuntimeException(
456                         "Lines are not connected: " + lines[i - 1].getLast() + " to " + lines[i].getFirst() + " distance is "
457                                 + lines[i - 1].getLast().distance(lines[i].getFirst()) + " > " + tolerance);
458             }
459             size += lines[i].size() - 1;
460         }
461         Point2dhtml#Point2d">Point2d[] points = new Point2d[size];
462         int nextIndex = 0;
463         for (int i = 0; i < lines.length; i++)
464         {
465             PolyLine2d line = lines[i];
466             for (int j = 0 == i ? 0 : 1; j < line.size(); j++)
467             {
468                 points[nextIndex++] = line.get(j);
469             }
470         }
471         return new PolyLine2d(points);
472     }
473 
474     /**
475      * Create a new PolyLine2d, filtering out repeating successive points.
476      * @param points Point2d...; the coordinates of the line as Point2d
477      * @return the line
478      * @throws DrawRuntimeException when number of points &lt; 2
479      */
480     public static PolyLine2d createAndCleanPolyLine2d(final Point2d... points) throws DrawRuntimeException
481     {
482         if (points.length < 2)
483         {
484             throw new DrawRuntimeException(
485                     "Degenerate PolyLine2d; has " + points.length + " point" + (points.length != 1 ? "s" : ""));
486         }
487         return createAndCleanPolyLine2d(new ArrayList<>(Arrays.asList(points)));
488     }
489 
490     /**
491      * Create a new PolyLine2d, while filtering out repeating successive points.
492      * @param pointList List&lt;Point2d&gt;; list of the coordinates of the line as Point2d; any duplicate points in this list
493      *            are removed (this method may modify the provided list)
494      * @return Line2d; the line
495      * @throws DrawRuntimeException when number of non-equal points &lt; 2
496      */
497     public static PolyLine2d createAndCleanPolyLine2d(final List<Point2d> pointList) throws DrawRuntimeException
498     {
499         // TODO rewrite to avoid modifying the input list.
500         // clean successive equal points
501         int i = 1;
502         while (i < pointList.size())
503         {
504             if (pointList.get(i - 1).equals(pointList.get(i)))
505             {
506                 pointList.remove(i);
507             }
508             else
509             {
510                 i++;
511             }
512         }
513         return new PolyLine2d(pointList);
514     }
515 
516     /** {@inheritDoc} */
517     @Override
518     public final Ray2d getLocationExtended(final double position)
519     {
520         if (position >= 0.0 && position <= getLength())
521         {
522             try
523             {
524                 return getLocation(position);
525             }
526             catch (DrawRuntimeException exception)
527             {
528                 // cannot happen
529             }
530         }
531 
532         // position before start point -- extrapolate using direction from first point to second point of this Line2d
533         if (position < 0.0)
534         {
535             double fraction = position / (this.lengthIndexedLine[1] - this.lengthIndexedLine[0]);
536             return new Ray2d(this.x[0] + fraction * (this.x[1] - this.x[0]), this.y[0] + fraction * (this.y[1] - this.y[0]),
537                     this.x[1], this.y[1]);
538         }
539 
540         // position beyond end point -- extrapolate using the direction from the before last point to the last point of this
541         // Line2d
542         int n1 = this.x.length - 1; // index of last point
543         int n2 = this.x.length - 2; // index of before last point
544         double len = position - getLength();
545         double fraction = len / (this.lengthIndexedLine[n1] - this.lengthIndexedLine[n2]);
546         while (Double.isInfinite(fraction))
547         {
548             // Overflow occurred; move n2 back another point; if possible
549             if (--n2 < 0)
550             {
551                 CategoryLogger.always().error("lengthIndexedLine of {} is invalid", this);
552                 return new Ray2d(this.x[n1], this.y[n1], 0.0); // Bogus direction
553             }
554             fraction = len / (this.lengthIndexedLine[n1] - this.lengthIndexedLine[n2]);
555         }
556         return new Ray2d(this.x[n1] + fraction * (this.x[n1] - this.x[n2]), this.y[n1] + fraction * (this.y[n1] - this.y[n2]),
557                 Math.atan2(this.y[n1] - this.y[n2], this.x[n1] - this.x[n2]));
558     }
559 
560     /** {@inheritDoc} */
561     @Override
562     public final Ray2d getLocation(final double position) throws DrawRuntimeException
563     {
564         Throw.when(Double.isNaN(position), DrawRuntimeException.class, "position may not be NaN");
565         Throw.when(position < 0.0 || position > getLength(), DrawRuntimeException.class,
566                 "getLocation for line: position < 0.0 or > line length. Position = " + position + "; length = " + getLength());
567         // handle special cases: position == 0.0, or position == length
568         if (position == 0.0)
569         {
570             return new Ray2d(this.x[0], this.y[0], this.x[1], this.y[1]);
571         }
572         if (position == getLength())
573         {
574             return new Ray2d(this.x[this.x.length - 1], this.y[this.x.length - 1],
575                     2 * this.x[this.x.length - 1] - this.x[this.x.length - 2],
576                     2 * this.y[this.x.length - 1] - this.y[this.x.length - 2]);
577         }
578         // find the index of the line segment, use binary search
579         int index = find(position);
580         double remainder = position - this.lengthIndexedLine[index];
581         double fraction = remainder / (this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index]);
582         // if (fraction >= 1.0 && index < this.x.length - 1)
583         // {
584         // // Rounding problem; move to the next segment.
585         // index++;
586         // remainder = position - this.lengthIndexedLine[index];
587         // fraction = remainder / (this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index]);
588         // }
589         return new Ray2d(this.x[index] + fraction * (this.x[index + 1] - this.x[index]),
590                 this.y[index] + fraction * (this.y[index + 1] - this.y[index]), 2 * this.x[index + 1] - this.x[index],
591                 2 * this.y[index + 1] - this.y[index]);
592     }
593 
594     /**
595      * Perform the orthogonal projection operation.
596      * @param point Point2d; the point to project
597      * @param limitHandling Boolean; if Null; results outside the interval 0.0 .. 1.0 are replaced by NaN, if false, results
598      *            outside that interval are returned as is; if true results outside the interval are truncated to the interval
599      *            and therefore not truly orthogonal
600      * @return double; the fractional position on this PolyLine that is closest to point, or NaN
601      */
602     private double projectOrthogonalFractional(final Point2d point, final Boolean limitHandling)
603     {
604         Throw.whenNull(point, "point may not be null");
605         double bestDistance = Double.POSITIVE_INFINITY;
606         double result = Double.NaN;
607         double bestDistanceExtended = Double.POSITIVE_INFINITY;
608         for (int index = 1; index < this.size(); index++)
609         {
610             double fraction = point.fractionalPositionOnLine(this.x[index - 1], this.y[index - 1], this.x[index], this.y[index],
611                     false, false);
612             double distance = Math.hypot(point.x - (this.x[index - 1] + fraction * (this.x[index] - this.x[index - 1])),
613                     point.y - (this.y[index - 1] + fraction * (this.y[index] - this.y[index - 1])));
614             if (distance < bestDistanceExtended && (fraction >= 0.0 && fraction <= 1.0 || (fraction < 0.0 && index == 1)
615                     || fraction > 1.0 && index == this.size() - 1))
616             {
617                 bestDistanceExtended = distance;
618             }
619             if (distance < bestDistance && (fraction >= 0.0 || index == 1 && limitHandling != null && !limitHandling)
620                     && (fraction <= 1.0 || index == this.size() - 1 && limitHandling != null && !limitHandling))
621             {
622                 bestDistance = distance;
623                 result = lengthAtIndex(index - 1) + fraction * (lengthAtIndex(index) - lengthAtIndex(index - 1));
624             }
625             else if (fraction < 0.0 && limitHandling != null && limitHandling)
626             {
627                 distance = Math.hypot(point.x - this.x[index - 1], point.y - this.y[index - 1]);
628                 if (distance < bestDistance)
629                 {
630                     bestDistance = distance;
631                     result = lengthAtIndex(index - 1);
632                 }
633             }
634             else if (index == this.size() - 1 && limitHandling != null && limitHandling)
635             {
636                 distance = Math.hypot(point.x - this.x[index], point.y - this.y[index]);
637                 if (distance < bestDistance)
638                 {
639                     bestDistance = distance;
640                     result = lengthAtIndex(index);
641                 }
642             }
643         }
644         if (bestDistance > bestDistanceExtended && (limitHandling == null || !limitHandling))
645         {
646             return Double.NaN;
647         }
648         return result / getLength();
649     }
650 
651     /** {@inheritDoc} */
652     @Override
653     public Point2dPoint2d closestPointOnPolyLine(final Point2d point)
654     {
655         try
656         {
657             return getLocation(projectOrthogonalFractional(point, true) * getLength());
658         }
659         catch (DrawRuntimeException e)
660         {
661             // Cannot happen
662             e.printStackTrace();
663             return null;
664         }
665     }
666 
667     /**
668      * Perform the project orthogonal operation.
669      * @param point Point2d; the point to project
670      * @param limitHandling Boolean; if Null; results outside this PolyLin2de are replaced by Null, if false, results outside
671      *            that interval are returned as is; if true results outside this PolyLine2d are truncated to the first or last
672      *            point of this PolyLine2d and therefore not truly orthogonal
673      * @return Point2d; the orthogonal projection of point on this PolyLine2d
674      */
675     private Point2dt2d">Point2d projectOrthogonal(final Point2d point, final Boolean limitHandling)
676     {
677         Throw.whenNull(point, "point may not be null");
678         double fraction = projectOrthogonalFractional(point, limitHandling);
679         if (Double.isNaN(fraction))
680         {
681             return null;
682         }
683         return getLocationExtended(fraction * getLength());
684     }
685 
686     /** {@inheritDoc} */
687     @Override
688     public Point2dt2d">Point2d projectOrthogonal(final Point2d point) throws NullPointerException
689     {
690         return projectOrthogonal(point, null);
691     }
692 
693     /** {@inheritDoc} */
694     @Override
695     public Point2dnt2d projectOrthogonalExtended(final Point2d point) throws NullPointerException
696     {
697         return projectOrthogonal(point, false);
698     }
699 
700     /** {@inheritDoc} */
701     @Override
702     public final double projectOrthogonalFractional(final Point2d point) throws NullPointerException
703     {
704         return projectOrthogonalFractional(point, null);
705     }
706 
707     /** {@inheritDoc} */
708     @Override
709     public double projectOrthogonalFractionalExtended(final Point2d point) throws NullPointerException
710     {
711         return projectOrthogonalFractional(point, false);
712     }
713 
714     /** {@inheritDoc} */
715     @Override
716     public PolyLine2d extract(final double start, final double end) throws DrawRuntimeException
717     {
718         if (Double.isNaN(start) || Double.isNaN(end) || start < 0 || start >= end || end > getLength())
719         {
720             throw new DrawRuntimeException(
721                     "Bad interval (" + start + ".." + end + "; length of this Line2d is " + this.getLength() + ")");
722         }
723         double cumulativeLength = 0;
724         double nextCumulativeLength = 0;
725         double segmentLength = 0;
726         int index = 0;
727         List<Point2d> pointList = new ArrayList<>();
728         while (start > cumulativeLength)
729         {
730             Point2d fromPoint = get(index);
731             index++;
732             Point2d toPoint = get(index);
733             segmentLength = fromPoint.distance(toPoint);
734             cumulativeLength = nextCumulativeLength;
735             nextCumulativeLength = cumulativeLength + segmentLength;
736             if (nextCumulativeLength >= start)
737             {
738                 break;
739             }
740         }
741         if (start == nextCumulativeLength)
742         {
743             pointList.add(get(index));
744         }
745         else
746         {
747             pointList.add(get(index - 1).interpolate(get(index), (start - cumulativeLength) / segmentLength));
748             if (end > nextCumulativeLength)
749             {
750                 pointList.add(get(index));
751             }
752         }
753         while (end > nextCumulativeLength)
754         {
755             Point2d fromPoint = get(index);
756             index++;
757             if (index >= size())
758             {
759                 break; // rounding error
760             }
761             Point2d toPoint = get(index);
762             segmentLength = fromPoint.distance(toPoint);
763             cumulativeLength = nextCumulativeLength;
764             nextCumulativeLength = cumulativeLength + segmentLength;
765             if (nextCumulativeLength >= end)
766             {
767                 break;
768             }
769             pointList.add(toPoint);
770         }
771         if (end == nextCumulativeLength)
772         {
773             pointList.add(get(index));
774         }
775         else if (index < this.x.length)
776         {
777             Point2d point = get(index - 1).interpolate(get(index), (end - cumulativeLength) / segmentLength);
778             // can be the same due to rounding
779             if (!point.equals(pointList.get(pointList.size() - 1)))
780             {
781                 pointList.add(point);
782             }
783         }
784         // else rounding error
785         try
786         {
787             return instantiate(pointList);
788         }
789         catch (DrawRuntimeException exception)
790         {
791             CategoryLogger.always().error(exception, "interval " + start + ".." + end + " too short");
792             throw new DrawRuntimeException("interval " + start + ".." + end + "too short");
793         }
794     }
795 
796     /** {@inheritDoc} */
797     @Override
798     public PolyLine2d truncate(final double position) throws DrawRuntimeException
799     {
800         if (position <= 0.0 || position > getLength())
801         {
802             throw new DrawRuntimeException("truncate for line: position <= 0.0 or > line length. Position = " + position
803                     + ". Length = " + getLength() + " m.");
804         }
805 
806         // handle special case: position == length
807         if (position == getLength())
808         {
809             return this;
810         }
811 
812         // find the index of the line segment
813         int index = find(position);
814         double remainder = position - lengthAtIndex(index);
815         double fraction = remainder / (lengthAtIndex(index + 1) - lengthAtIndex(index));
816         Point2d p1 = get(index);
817         Point2d lastPoint;
818         if (0.0 == fraction)
819         {
820             lastPoint = p1;
821         }
822         else
823         {
824             Point2d p2 = get(index + 1);
825             lastPoint = p1.interpolate(p2, fraction);
826             index++;
827         }
828         double[] truncatedX = new double[index + 1];
829         double[] truncatedY = new double[index + 1];
830         for (int i = 0; i < index; i++)
831         {
832             truncatedX[i] = this.x[i];
833             truncatedY[i] = this.y[i];
834         }
835         truncatedX[index] = lastPoint.x;
836         truncatedY[index] = lastPoint.y;
837         return new PolyLine2d(truncatedX, truncatedY);
838     }
839 
840     /** {@inheritDoc} */
841     @Override
842     @SuppressWarnings("checkstyle:methodlength")
843     public PolyLine2d offsetLine(final double offset, final double circlePrecision, final double offsetMinimumFilterValue,
844             final double offsetMaximumFilterValue, final double offsetFilterRatio, final double minimumOffset)
845             throws IllegalArgumentException
846     {
847         Throw.when(Double.isNaN(offset), IllegalArgumentException.class, "Offset may not be NaN");
848         Throw.when(Double.isNaN(circlePrecision) || circlePrecision <= 0, IllegalArgumentException.class,
849                 "bad circlePrecision");
850         Throw.when(Double.isNaN(offsetMinimumFilterValue) || offsetMinimumFilterValue <= 0, IllegalArgumentException.class,
851                 "bad offsetMinimumFilterValue");
852         Throw.when(Double.isNaN(offsetMaximumFilterValue) || offsetMaximumFilterValue <= 0, IllegalArgumentException.class,
853                 "bad offsetMaximumFilterValue");
854         Throw.when(Double.isNaN(offsetFilterRatio) || offsetFilterRatio <= 0, IllegalArgumentException.class,
855                 "bad offsetFilterRatio");
856         Throw.when(Double.isNaN(minimumOffset) || minimumOffset <= 0, IllegalArgumentException.class, "bad minimumOffset");
857         Throw.when(offsetMinimumFilterValue >= offsetMaximumFilterValue, IllegalArgumentException.class,
858                 "bad offset filter values; minimum must be less than maximum");
859         double bufferOffset = Math.abs(offset);
860         if (bufferOffset < minimumOffset)
861         {
862             return this;
863         }
864 
865         PolyLine2d filteredReferenceLine = noiseFilteredLine(
866                 Math.max(offsetMinimumFilterValue, Math.min(bufferOffset / offsetFilterRatio, offsetMaximumFilterValue)));
867         List<Point2d> tempPoints = new ArrayList<>();
868         // Make good use of the fact that PolyLine3d cannot have consecutive duplicate points and has > 1 points
869         Point2d prevPoint = filteredReferenceLine.get(0);
870         Double prevAngle = null;
871         for (int index = 0; index < filteredReferenceLine.size() - 1; index++)
872         {
873             Point2d nextPoint = filteredReferenceLine.get(index + 1);
874             double angle = Math.atan2(nextPoint.y - prevPoint.y, nextPoint.x - prevPoint.x);
875             Point2dl#Point2d">Point2d segmentFrom = new Point2d(prevPoint.x - Math.sin(angle) * offset, prevPoint.y + Math.cos(angle) * offset);
876             Point2dtml#Point2d">Point2d segmentTo = new Point2d(nextPoint.x - Math.sin(angle) * offset, nextPoint.y + Math.cos(angle) * offset);
877             boolean addSegment = true;
878             if (index > 0)
879             {
880                 double deltaAngle = angle - prevAngle;
881                 if (Math.abs(deltaAngle) > Math.PI)
882                 {
883                     deltaAngle -= Math.signum(deltaAngle) * 2 * Math.PI;
884                 }
885                 if (deltaAngle * offset <= 0)
886                 {
887                     // Outside of curve of reference line
888                     // Approximate an arc using straight segments.
889                     // Determine how many segments are needed.
890                     int numSegments = 1;
891                     if (Math.abs(deltaAngle) > Math.PI / 2)
892                     {
893                         numSegments = 2;
894                     }
895                     while (true)
896                     {
897                         double maxError = bufferOffset * (1 - Math.abs(Math.cos(deltaAngle / numSegments / 2)));
898                         if (maxError < circlePrecision)
899                         {
900                             break; // required precision reached
901                         }
902                         numSegments *= 2;
903                     }
904                     Point2d prevArcPoint = tempPoints.get(tempPoints.size() - 1);
905                     // Generate the intermediate points
906                     for (int additionalPoint = 1; additionalPoint < numSegments; additionalPoint++)
907                     {
908                         double intermediateAngle =
909                                 (additionalPoint * angle + (numSegments - additionalPoint) * prevAngle) / numSegments;
910                         if (prevAngle * angle < 0 && Math.abs(prevAngle) > Math.PI / 2 && Math.abs(angle) > Math.PI / 2)
911                         {
912                             intermediateAngle += Math.PI;
913                         }
914                         Point2dt2d">Point2d intermediatePoint = new Point2d(prevPoint.x - Math.sin(intermediateAngle) * offset,
915                                 prevPoint.y + Math.cos(intermediateAngle) * offset);
916                         // Find any intersection points of the new segment and all previous segments
917                         Point2d prevSegFrom = null;
918                         int stopAt = tempPoints.size();
919                         for (int i = 0; i < stopAt; i++)
920                         {
921                             Point2d prevSegTo = tempPoints.get(i);
922                             if (null != prevSegFrom)
923                             {
924                                 Point2d prevSegIntersection = Point2d.intersectionOfLineSegments(prevArcPoint,
925                                         intermediatePoint, prevSegFrom, prevSegTo);
926                                 if (null != prevSegIntersection && prevSegIntersection.distance(prevArcPoint) > circlePrecision
927                                         && prevSegIntersection.distance(prevSegFrom) > circlePrecision
928                                         && prevSegIntersection.distance(prevSegTo) > circlePrecision)
929                                 {
930                                     tempPoints.add(prevSegIntersection);
931                                     // System.out.println(new OTSLine3D(tempPoints).toPlot());
932                                 }
933                             }
934                             prevSegFrom = prevSegTo;
935                         }
936                         Point2d nextSegmentIntersection =
937                                 Point2d.intersectionOfLineSegments(prevSegFrom, intermediatePoint, segmentFrom, segmentTo);
938                         if (null != nextSegmentIntersection)
939                         {
940                             tempPoints.add(nextSegmentIntersection);
941                             // System.out.println(new OTSLine3D(tempPoints).toPlot());
942                         }
943                         tempPoints.add(intermediatePoint);
944                         // System.out.println(new OTSLine3D(tempPoints).toPlot());
945                         prevArcPoint = intermediatePoint;
946                     }
947                 }
948                 // Inside of curve of reference line.
949                 // Add the intersection point of each previous segment and the next segment
950                 Point2d pPoint = null;
951                 int currentSize = tempPoints.size(); // PK DO NOT use the "dynamic" limit
952                 for (int i = 0; i < currentSize /* tempPoints.size() */; i++)
953                 {
954                     Point2d p = tempPoints.get(i);
955                     if (null != pPoint)
956                     {
957                         double pAngle = Math.atan2(p.y - pPoint.y, p.x - pPoint.x);
958                         double angleDifference = angle - pAngle;
959                         if (Math.abs(angleDifference) > Math.PI)
960                         {
961                             angleDifference -= Math.signum(angleDifference) * 2 * Math.PI;
962                         }
963                         if (Math.abs(angleDifference) > 0)// 0.01)
964                         {
965                             Point2d intersection = Point2d.intersectionOfLineSegments(pPoint, p, segmentFrom, segmentTo);
966                             if (null != intersection)
967                             {
968                                 if (tempPoints.size() - 1 == i)
969                                 {
970                                     tempPoints.remove(tempPoints.size() - 1);
971                                     segmentFrom = intersection;
972                                 }
973                                 else
974                                 {
975                                     tempPoints.add(intersection);
976                                 }
977                             }
978                         }
979                         else
980                         {
981                             // This is where things went very wrong in the TestGeometry demo.
982                             if (i == tempPoints.size() - 1)
983                             {
984                                 tempPoints.remove(tempPoints.size() - 1);
985                                 segmentFrom = tempPoints.get(tempPoints.size() - 1);
986                                 tempPoints.remove(tempPoints.size() - 1);
987                             }
988                         }
989                     }
990                     pPoint = p;
991                 }
992             }
993             if (addSegment)
994             {
995                 tempPoints.add(segmentFrom);
996                 tempPoints.add(segmentTo);
997                 prevPoint = nextPoint;
998                 prevAngle = angle;
999             }
1000         }
1001         // Remove points that are closer than the specified offset
1002         for (int index = 1; index < tempPoints.size() - 1; index++)
1003         {
1004             Point2d checkPoint = tempPoints.get(index);
1005             prevPoint = null;
1006             boolean tooClose = false;
1007             boolean somewhereAtCorrectDistance = false;
1008             for (int i = 0; i < filteredReferenceLine.size(); i++)
1009             {
1010                 Point2d p = filteredReferenceLine.get(i);
1011                 if (null != prevPoint)
1012                 {
1013                     Point2d closestPoint = checkPoint.closestPointOnSegment(prevPoint, p);
1014                     double distance = closestPoint.distance(checkPoint);
1015                     if (distance < bufferOffset - circlePrecision)
1016                     {
1017                         tooClose = true;
1018                         break;
1019                     }
1020                     else if (distance < bufferOffset + minimumOffset)
1021                     {
1022                         somewhereAtCorrectDistance = true;
1023                     }
1024                 }
1025                 prevPoint = p;
1026             }
1027             if (tooClose || !somewhereAtCorrectDistance)
1028             {
1029                 tempPoints.remove(index);
1030                 index--;
1031             }
1032         }
1033         try
1034         {
1035             return PolyLine2d.createAndCleanPolyLine2d(tempPoints);
1036         }
1037         catch (DrawRuntimeException exception)
1038         {
1039             exception.printStackTrace();
1040         }
1041         return null;
1042     }
1043 
1044     /** {@inheritDoc} */
1045     @Override
1046     public PolyLine2d offsetLine(final double offsetAtStart, final double offsetAtEnd, final double circlePrecision,
1047             final double offsetMinimumFilterValue, final double offsetMaximumFilterValue, final double offsetFilterRatio,
1048             final double minimumOffset) throws IllegalArgumentException, DrawRuntimeException
1049     {
1050         if (offsetAtStart == offsetAtEnd)
1051         {
1052             return offsetLine(offsetAtStart, circlePrecision, offsetMinimumFilterValue, offsetMaximumFilterValue,
1053                     offsetFilterRatio, minimumOffset);
1054         }
1055         PolyLine2d atStart = offsetLine(offsetAtStart, circlePrecision, offsetMinimumFilterValue, offsetMaximumFilterValue,
1056                 offsetFilterRatio, minimumOffset);
1057         PolyLine2d atEnd = offsetLine(offsetAtEnd, circlePrecision, offsetMinimumFilterValue, offsetMaximumFilterValue,
1058                 offsetFilterRatio, minimumOffset);
1059         return atStart.transitionLine(atEnd, new TransitionFunction()
1060         {
1061             @Override
1062             public double function(final double fraction)
1063             {
1064                 return fraction;
1065             }
1066         });
1067     }
1068 
1069     /** {@inheritDoc} */
1070     @Override
1071     public PolyLine2dlyLine2d">PolyLine2d transitionLine(final PolyLine2d endLine, final TransitionFunction transition) throws DrawRuntimeException
1072     {
1073         Throw.whenNull(endLine, "endLine may not be null");
1074         Throw.whenNull(transition, "transition may not be null");
1075         List<Point2d> pointList = new ArrayList<>();
1076         int indexInStart = 0;
1077         int indexInEnd = 0;
1078         while (indexInStart < this.size() && indexInEnd < endLine.size())
1079         {
1080             double fractionInStart = lengthAtIndex(indexInStart) / getLength();
1081             double fractionInEnd = endLine.lengthAtIndex(indexInEnd) / endLine.getLength();
1082             if (fractionInStart < fractionInEnd)
1083             {
1084                 pointList.add(get(indexInStart).interpolate(endLine.getLocation(fractionInStart * endLine.getLength()),
1085                         transition.function(fractionInStart)));
1086                 indexInStart++;
1087             }
1088             else if (fractionInStart > fractionInEnd)
1089             {
1090                 pointList.add(this.getLocation(fractionInEnd * getLength()).interpolate(endLine.get(indexInEnd),
1091                         transition.function(fractionInEnd)));
1092                 indexInEnd++;
1093             }
1094             else
1095             {
1096                 pointList.add(this.get(indexInStart).interpolate(endLine.getLocation(fractionInEnd * endLine.getLength()),
1097                         transition.function(fractionInStart)));
1098                 indexInStart++;
1099                 indexInEnd++;
1100             }
1101         }
1102         return createAndCleanPolyLine2d(pointList);
1103     }
1104 
1105     /**
1106      * Find a location on this PolyLine2d that is a reasonable projection of a Ray on this line. The result (if not NaN) lies on
1107      * a line perpendicular to the direction of the Ray and on some segment of this PolyLine. This method attempts to give
1108      * continuous results for continuous changes of the Ray that must be projected. There are cases where this is simply
1109      * impossible, or the optimal result is ambiguous. In these cases this method will return something that is hopefully good
1110      * enough.
1111      * @param ray Ray2d; the Ray
1112      * @return double; length along this PolyLine (some value between 0 and the length of this PolyLine) where ray projects, or
1113      *         NaN if there is no solution
1114      * @throws NullPointerException when ray is null
1115      */
1116     public double projectRay(final Ray2d ray) throws NullPointerException
1117     {
1118         Throw.whenNull(ray, "ray may not be null");
1119         double bestDistance = Double.POSITIVE_INFINITY;
1120         double positionAtBestDistance = Double.NaN;
1121         // Point2d prevPoint = null;
1122         // Define the line that is perpendicular to directedPoint, passing through directedPoint
1123         double perpendicularX = ray.x - Math.sin(ray.phi);
1124         double perpendicularY = ray.y + Math.cos(ray.phi);
1125         for (int index = 1; index < this.x.length; index++)
1126         {
1127             Point2d intersection = Point2d.intersectionOfLines(ray.x, ray.y, perpendicularX, perpendicularY, false, false,
1128                     this.x[index - 1], this.y[index - 1], this.x[index], this.y[index], true, true);
1129             if (intersection != null) // Intersection is on the segment
1130             {
1131                 double thisDistance = intersection.distance(ray);
1132                 if (thisDistance < bestDistance)
1133                 {
1134                     double distanceToPrevPoint =
1135                             Math.hypot(intersection.x - this.x[index - 1], intersection.y - this.y[index - 1]);
1136                     positionAtBestDistance = lengthAtIndex(index - 1) + distanceToPrevPoint;
1137                     bestDistance = thisDistance;
1138                 }
1139             }
1140         }
1141         return positionAtBestDistance;
1142     }
1143 
1144     /** {@inheritDoc} */
1145     @Override
1146     public String toString()
1147     {
1148         return toString("%f", false);
1149     }
1150 
1151     /** {@inheritDoc} */
1152     @Override
1153     public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
1154     {
1155         StringBuilder result = new StringBuilder();
1156         if (!doNotIncludeClassName)
1157         {
1158             result.append("PolyLine2d ");
1159         }
1160         String format = String.format("%%sx=%1$s, y=%1$s", doubleFormat);
1161         for (int index = 0; index < this.x.length; index++)
1162         {
1163             result.append(String.format(Locale.US, format, index == 0 ? "[" : ", ", this.x[index], this.y[index]));
1164         }
1165         result.append("]");
1166         return result.toString();
1167     }
1168 
1169     /** {@inheritDoc} */
1170     @Override
1171     public String toExcel()
1172     {
1173         StringBuffer s = new StringBuffer();
1174         for (int i = 0; i < this.x.length; i++)
1175         {
1176             s.append(this.x[i] + "\t" + this.y[i] + "\n");
1177         }
1178         return s.toString();
1179     }
1180 
1181     /**
1182      * Convert this PolyLine3D to Peter's plot format.
1183      * @return Peter's format plot output
1184      */
1185     public String toPlot()
1186     {
1187         StringBuffer result = new StringBuffer();
1188         for (int i = 0; i < this.x.length; i++)
1189         {
1190             result.append(String.format(Locale.US, "%s%.3f,%.3f", 0 == result.length() ? "M" : " L", this.x[i], this.y[i]));
1191         }
1192         result.append("\n");
1193         return result.toString();
1194     }
1195 
1196     /** {@inheritDoc} */
1197     @Override
1198     public int hashCode()
1199     {
1200         final int prime = 31;
1201         int result = 1;
1202         result = prime * result + Arrays.hashCode(this.x);
1203         result = prime * result + Arrays.hashCode(this.y);
1204         return result;
1205     }
1206 
1207     /** {@inheritDoc} */
1208     @SuppressWarnings("checkstyle:needbraces")
1209     @Override
1210     public boolean equals(final Object obj)
1211     {
1212         if (this == obj)
1213             return true;
1214         if (obj == null)
1215             return false;
1216         if (getClass() != obj.getClass())
1217             return false;
1218         PolyLine2d../../../org/djutils/draw/line/PolyLine2d.html#PolyLine2d">PolyLine2d other = (PolyLine2d) obj;
1219         if (!Arrays.equals(this.x, other.x))
1220             return false;
1221         if (!Arrays.equals(this.y, other.y))
1222             return false;
1223         return true;
1224     }
1225 
1226 }