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  
11  import org.djutils.draw.DrawException;
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>
30  {
31      /** */
32      private static final long serialVersionUID = 20200911L;
33  
34      /** The points of the line. */
35      private final Point2d[] points;
36  
37      /** The cumulative length of the line at point 'i'. */
38      private final double[] lengthIndexedLine;
39  
40      /** The length. */
41      private final double length;
42  
43      /** Bounding rectangle of this Line2d. */
44      private final Bounds2d bounds;
45  
46      /**
47       * Construct a new Line2D from an array of Point2d.
48       * @param copyNeeded boolean; if true; a deep copy of the points array is stored instead of the provided array
49       * @param points Point2d[]; the array of points to construct this Line2d from.
50       * @throws NullPointerException when iterator is null
51       * @throws DrawRuntimeException when the provided points do not constitute a valid line (too few points or identical
52       *             adjacent points)
53       */
54      private PolyLine2d(final boolean copyNeeded, final Point2d[] points) throws NullPointerException, DrawRuntimeException
55      {
56          Throw.whenNull(points, "points may not be null");
57          Throw.when(points.length < 2, DrawRuntimeException.class, "Need at least two points");
58          this.points = copyNeeded ? Arrays.copyOf(points, points.length) : points;
59          Point2d prevPoint = points[0];
60          double minX = prevPoint.x;
61          double minY = prevPoint.y;
62          double maxX = prevPoint.x;
63          double maxY = prevPoint.y;
64          this.lengthIndexedLine = new double[points.length];
65          this.lengthIndexedLine[0] = 0.0;
66          for (int i = 1; i < points.length; i++)
67          {
68              Point2d point = points[i];
69              minX = Math.min(minX, point.x);
70              minY = Math.min(minY, point.y);
71              maxX = Math.max(maxX, point.x);
72              maxY = Math.max(maxY, point.y);
73              if (prevPoint.x == point.x && prevPoint.y == point.y)
74              {
75                  throw new DrawRuntimeException("Degenerate Line2d; point " + (i - 1) + " has the same x and y as point " + i);
76              }
77              this.lengthIndexedLine[i] = this.lengthIndexedLine[i - 1] + prevPoint.distance(point);
78              prevPoint = point;
79          }
80          this.length = this.lengthIndexedLine[this.lengthIndexedLine.length - 1];
81          this.bounds = new Bounds2d(minX, maxX, minY, maxY);
82      }
83  
84      /**
85       * Construct a new PolyLine2d from an array of Point2d.
86       * @param point1 Point2d; starting point of the PolyLine2d
87       * @param point2 Point2d; second point of the PolyLine2d
88       * @param otherPoints Point2d...; additional points of the PolyLine2d
89       * @throws NullPointerException when iterator is null
90       * @throws DrawRuntimeException when the provided points do not constitute a valid line (too few points or identical
91       *             adjacent points)
92       */
93      public PolyLine2d(final Point2d.html#Point2d">Point2d point1, final Point2d point2, final Point2d... otherPoints)
94              throws NullPointerException, DrawRuntimeException
95      {
96          this(false, spliceArray(point1, point2, otherPoints));
97      }
98  
99      /**
100      * Construct an array of Point2d from two points plus an array of Point2d.
101      * @param point1 Point2d; the first point (ends up at index 0 of the result)
102      * @param point2 Point2d; the second point (ends up at index 1 of the result)
103      * @param otherPoints Point2d...; may be null, may be empty. If non empty, the elements in otherPoints end up at index 2 and
104      *            up in the result
105      * @return Point2d[]; the combined array
106      */
107     private static Point2d.html#Point2d">Point2dPoint2d">Point2d[] spliceArray(final Point2d.html#Point2d">Point2d point1, final Point2d point2, final Point2d... otherPoints)
108     {
109         Point2dhtml#Point2d">Point2d[] result = new Point2d[2 + (otherPoints == null ? 0 : otherPoints.length)];
110         result[0] = point1;
111         result[1] = point2;
112         if (otherPoints != null)
113         {
114             for (int i = 0; i < otherPoints.length; i++)
115             {
116                 result[i + 2] = otherPoints[i];
117             }
118         }
119         return result;
120     }
121 
122     /**
123      * Construct a new PolyLine2d from an array of Point2d.
124      * @param points Point2d[]; points of the PolyLine2d
125      * @throws NullPointerException when iterator is null
126      * @throws DrawRuntimeException when the provided points do not constitute a valid line (too few points or identical
127      *             adjacent points)
128      */
129     public PolyLine2d(final Point2d[] points) throws NullPointerException, DrawRuntimeException
130     {
131         this(true, checkLengthIsTwoOrMore(Throw.whenNull(points, "points may not be null")));
132     }
133 
134     /**
135      * Check that the length of an array of Point2d is at least two.
136      * @param points Point2d[]; the array of points to check
137      * @return Point2d[]; points
138      * @throws DrawRuntimeException when the length of points is less than two
139      */
140     private static Point2dint2d[] checkLengthIsTwoOrMore(final Point2d[] points) throws DrawRuntimeException
141     {
142         Throw.when(points.length < 2, DrawRuntimeException.class, "Need at least two points");
143         return points;
144     }
145 
146     /**
147      * Construct a new Line2d and initialize its length indexed line, bounds, centroid and length.
148      * @param iterator Iterator&lt;Point2d&gt;; iterator that will provide all points that constitute the new Line2d
149      * @throws NullPointerException when iterator is null
150      * @throws DrawException when the iterator provides too few points, or some adjacent identical points)
151      */
152     public PolyLine2d(final Iterator<Point2d> iterator) throws NullPointerException, DrawException
153     {
154         this(iteratorToList(Throw.whenNull(iterator, "iterator cannot be null")));
155     }
156 
157     /**
158      * Construct a new Line2d from a List&lt;Point2d&gt;.
159      * @param pointList List&lt;Point2d&gt;; the list of points to construct this Line2d from.
160      * @throws DrawRuntimeException when the provided points do not constitute a valid line (too few points or identical
161      *             adjacent points)
162      */
163     public PolyLine2d(final List<Point2d> pointList) throws DrawRuntimeException
164     {
165         this(false, pointList.toArray(new Point2d[pointList.size()]));
166     }
167 
168     /** {@inheritDoc} */
169     @Override
170     public PolyLine2d instantiate(final List<Point2d> pointList) throws NullPointerException, DrawRuntimeException
171     {
172         return new PolyLine2d(pointList);
173     }
174 
175     /**
176      * Construct a new Line2d (closed shape) from a Path2D.
177      * @param path Path2D; the Path2D to construct this Line2d from.
178      * @throws DrawException when the provided points do not constitute a valid line (too few points or identical adjacent
179      *             points)
180      */
181     public PolyLine2d(final Path2D path) throws DrawException
182     {
183         this(false, path2DtoArray(path));
184     }
185 
186     /**
187      * Convert a path2D to a Point2d[] array to construct the line.
188      * @param path Path2D; the path to convert
189      * @return Point2d[]; an array of points based on MOVETO and LINETO elements of the Path2D
190      * @throws DrawException when the pathIterator of the path returns an unsupported command
191      */
192     private static Point2d[] path2DtoArray(final Path2D path) throws DrawException
193     {
194         List<Point2d> result = new ArrayList<>();
195         for (PathIterator pi = path.getPathIterator(null); !pi.isDone(); pi.next())
196         {
197             double[] p = new double[6];
198             int segType = pi.currentSegment(p);
199             if (segType == PathIterator.SEG_MOVETO || segType == PathIterator.SEG_LINETO)
200             {
201                 result.add(new Point2d(p[0], p[1]));
202             }
203             else if (segType == PathIterator.SEG_CLOSE)
204             {
205                 if (!result.get(0).equals(result.get(result.size() - 1)))
206                 {
207                     result.add(result.get(0));
208                 }
209                 break;
210             }
211             else
212             {
213                 throw new DrawException("path2DtoArray only handles SEG_MOVETO, SEG_LINETO and SEG_CLOSE");
214             }
215         }
216         return result.toArray(new Point2d[result.size() - 1]);
217     }
218 
219     /**
220      * Build a list from the Point2d objects that an iterator provides.
221      * @param iterator Iterator&lt;Point2d&gt;; the iterator that will provide the points
222      * @return List&lt;Point2d&gt;; a list of the points provided by the iterator
223      */
224     private static List<Point2d> iteratorToList(final Iterator<Point2d> iterator)
225     {
226         List<Point2d> result = new ArrayList<>();
227         iterator.forEachRemaining(result::add);
228         return result;
229     }
230 
231     /** {@inheritDoc} */
232     @Override
233     public int size()
234     {
235         return this.points.length;
236     }
237 
238     /** {@inheritDoc} */
239     @Override
240     public final Point2d get(final int i) throws IndexOutOfBoundsException
241     {
242         return this.points[i];
243     }
244 
245     /** {@inheritDoc} */
246     @Override
247     public final double lengthAtIndex(final int index)
248     {
249         return this.lengthIndexedLine[index];
250     }
251 
252     /** {@inheritDoc} */
253     @Override
254     public final double getLength()
255     {
256         return this.length;
257     }
258 
259     /** {@inheritDoc} */
260     @Override
261     public Iterator<Point2d> getPoints()
262     {
263         return Arrays.stream(this.points).iterator();
264     }
265 
266     /** {@inheritDoc} */
267     @Override
268     public Bounds2d getBounds()
269     {
270         return this.bounds;
271     }
272 
273     /**
274      * Construct a new Line2d that is equal to this line except for segments that are shorter than the <cite>noiseLevel</cite>.
275      * The result is guaranteed to start with the first point of this line and end with the last point of this line.
276      * @param noiseLevel double; the minimum segment length that is <b>not</b> removed
277      * @return Line2d; the filtered line
278      */
279     public final PolyLine2d noiseFilteredLine(final double noiseLevel)
280     {
281         if (this.size() <= 2)
282         {
283             return this; // Except for some cached fields; an Line2d is immutable; so safe to return
284         }
285         Point2d prevPoint = null;
286         List<Point2d> list = null;
287         for (int index = 0; index < this.size(); index++)
288         {
289             Point2d currentPoint = this.points[index];
290             if (null != prevPoint && prevPoint.distance(currentPoint) < noiseLevel)
291             {
292                 if (null == list)
293                 {
294                     // Found something to filter; copy this up to (and including) prevPoint
295                     list = new ArrayList<>();
296                     for (int i = 0; i < index; i++)
297                     {
298                         list.add(this.points[i]);
299                     }
300                 }
301                 if (index == this.size() - 1)
302                 {
303                     if (list.size() > 1)
304                     {
305                         // Replace the last point of the result by the last point of this Line2d
306                         list.set(list.size() - 1, currentPoint);
307                     }
308                     else
309                     {
310                         // Append the last point of this even though it is close to the first point than the noise value to
311                         // comply with the requirement that first and last point of this are ALWAYS included in the result.
312                         list.add(currentPoint);
313                     }
314                 }
315                 continue; // Do not replace prevPoint by currentPoint
316             }
317             else if (null != list)
318             {
319                 list.add(currentPoint);
320             }
321             prevPoint = currentPoint;
322         }
323         if (null == list)
324         {
325             return this;
326         }
327         if (list.size() == 2 && list.get(0).equals(list.get(1)))
328         {
329             // Insert point 1 of this; it MUST be different from point 0; so we don't have to test for anything.
330             list.add(1, this.points[1]);
331         }
332         try
333         {
334             return new PolyLine2d(list);
335         }
336         catch (DrawRuntimeException exception)
337         {
338             // Cannot happen
339             CategoryLogger.always().error(exception);
340             throw new Error(exception);
341         }
342     }
343 
344     /**
345      * Concatenate several Line2d instances.
346      * @param lines PolyLine2d...; Line2d... one or more Line2d. The last point of the first &lt;strong&gt;must&lt;/strong&gt;
347      *            match the first of the second, etc.
348      * @return Line2d
349      * @throws DrawException if zero lines are given, or when there is a gap between consecutive lines
350      */
351     public static PolyLine2d concatenate(final PolyLine2d... lines) throws DrawException
352     {
353         return concatenate(0.0, lines);
354     }
355 
356     /**
357      * Concatenate two Line2d instances. This method is separate for efficiency reasons.
358      * @param tolerance double; the tolerance between the end point of a line and the first point of the next line
359      * @param line1 PolyLine2d; first line
360      * @param line2 PolyLine2d; second line
361      * @return Line2d; the concatenation of the two lines
362      * @throws DrawException if zero lines are given, or when there is a gap between consecutive lines
363      */
364     public static PolyLine2d concatenate(final PolyLine2d.html#PolyLine2d">PolyLine2djxr_keyword">double tolerance, final PolyLine2d.html#PolyLine2d">PolyLine2d line1, final PolyLine2d line2)
365             throws DrawException
366     {
367         if (line1.getLast().distance(line2.getFirst()) > tolerance)
368         {
369             throw new DrawException("Lines are not connected: " + line1.getLast() + " to " + line2.getFirst() + " distance is "
370                     + line1.getLast().distance(line2.getFirst()) + " > " + tolerance);
371         }
372         int size = line1.size() + line2.size() - 1;
373         Point2dhtml#Point2d">Point2d[] points = new Point2d[size];
374         int nextIndex = 0;
375         for (int j = 0; j < line1.size(); j++)
376         {
377             points[nextIndex++] = line1.get(j);
378         }
379         for (int j = 1; j < line2.size(); j++)
380         {
381             points[nextIndex++] = line2.get(j);
382         }
383         return new PolyLine2d(false, points);
384     }
385 
386     /**
387      * Concatenate several Line2d instances.
388      * @param tolerance double; the tolerance between the end point of a line and the first point of the next line
389      * @param lines PolyLine2d...; Line2d... one or more Line2d. The last point of the first &lt;strong&gt;must&lt;/strong&gt;
390      *            match the first of the second, etc.
391      * @return Line2d; the concatenation of the lines
392      * @throws DrawException if zero lines are given, or when there is a gap between consecutive lines
393      */
394     public static PolyLine2d concatenate(final double tolerance, final PolyLine2d... lines) throws DrawException
395     {
396         if (0 == lines.length)
397         {
398             throw new DrawException("Empty argument list");
399         }
400         else if (1 == lines.length)
401         {
402             return lines[0];
403         }
404         int size = lines[0].size();
405         for (int i = 1; i < lines.length; i++)
406         {
407             if (lines[i - 1].getLast().distance(lines[i].getFirst()) > tolerance)
408             {
409                 throw new DrawException("Lines are not connected: " + lines[i - 1].getLast() + " to " + lines[i].getFirst()
410                         + " distance is " + lines[i - 1].getLast().distance(lines[i].getFirst()) + " > " + tolerance);
411             }
412             size += lines[i].size() - 1;
413         }
414         Point2dhtml#Point2d">Point2d[] points = new Point2d[size];
415         int nextIndex = 0;
416         for (int i = 0; i < lines.length; i++)
417         {
418             PolyLine2d line = lines[i];
419             for (int j = 0 == i ? 0 : 1; j < line.size(); j++)
420             {
421                 points[nextIndex++] = line.get(j);
422             }
423         }
424         return new PolyLine2d(false, points);
425     }
426 
427     /**
428      * Create a new Line2d, filtering out repeating successive points.
429      * @param points Point2d...; the coordinates of the line as Point2d
430      * @return the line
431      * @throws DrawException when number of points &lt; 2
432      */
433     public static PolyLine2d createAndCleanPolyLine2d(final Point2d... points) throws DrawException
434     {
435         if (points.length < 2)
436         {
437             throw new DrawException("Degenerate Line2d; has " + points.length + " point" + (points.length != 1 ? "s" : ""));
438         }
439         return createAndCleanPolyLine2d(new ArrayList<>(Arrays.asList(points)));
440     }
441 
442     /**
443      * Create an Line2d, while filtering out repeating successive points.
444      * @param pointList List&lt;Point2d&gt;; list of the coordinates of the line as Point2d; any duplicate points in this list
445      *            are removed (this method may modify the provided list)
446      * @return Line2d; the line
447      * @throws DrawException when number of non-equal points &lt; 2
448      */
449     public static PolyLine2d createAndCleanPolyLine2d(final List<Point2d> pointList) throws DrawException
450     {
451         // TODO avoid modifying the input list.
452         // clean successive equal points
453         int i = 1;
454         while (i < pointList.size())
455         {
456             if (pointList.get(i - 1).equals(pointList.get(i)))
457             {
458                 pointList.remove(i);
459             }
460             else
461             {
462                 i++;
463             }
464         }
465         return new PolyLine2d(pointList);
466     }
467 
468     /** {@inheritDoc} */
469     @Override
470     public final Ray2d getLocationExtended(final double position)
471     {
472         if (position >= 0.0 && position <= getLength())
473         {
474             try
475             {
476                 return getLocation(position);
477             }
478             catch (DrawException exception)
479             {
480                 // cannot happen
481             }
482         }
483 
484         // position before start point -- extrapolate using direction from first point to second point of this Line2d
485         if (position < 0.0)
486         {
487             double len = position;
488             double fraction = len / (this.lengthIndexedLine[1] - this.lengthIndexedLine[0]);
489             Point2d p1 = this.points[0];
490             Point2d p2 = this.points[1];
491             return new Ray2d(p1.x + fraction * (p2.x - p1.x), p1.y + fraction * (p2.y - p1.y), p2);
492         }
493 
494         // position beyond end point -- extrapolate using the direction from the before last point to the last point of this
495         // Line2d
496         int n1 = this.lengthIndexedLine.length - 1;
497         int n2 = this.lengthIndexedLine.length - 2;
498         double len = position - getLength();
499         double fraction = len / (this.lengthIndexedLine[n1] - this.lengthIndexedLine[n2]);
500         while (Double.isInfinite(fraction))
501         {
502             if (--n2 < 0)
503             {
504                 CategoryLogger.always().error("lengthIndexedLine of {} is invalid", this);
505                 Point2d p = this.points[n1];
506                 return new Ray2d(p.x, p.y, 0.0); // Bogus direction
507             }
508             fraction = len / (this.lengthIndexedLine[n1] - this.lengthIndexedLine[n2]);
509         }
510         Point2d p1 = this.points[n2];
511         Point2d p2 = this.points[n1];
512         return new Ray2d(p2.x + fraction * (p2.x - p1.x), p2.y + fraction * (p2.y - p1.y), p1.directionTo(p2));
513     }
514 
515     /** {@inheritDoc} */
516     @Override
517     public final Ray2d getLocation(final double position) throws DrawException
518     {
519         if (position < 0.0 || position > getLength())
520         {
521             throw new DrawException("getLocation for line: position < 0.0 or > line length. Position = " + position
522                     + "; length = " + getLength());
523         }
524 
525         // handle special cases: position == 0.0, or position == length
526         if (position == 0.0)
527         {
528             Point2d p1 = this.points[0];
529             Point2d p2 = this.points[1];
530             return new Ray2d(p1.x, p1.y, p2);
531         }
532         if (position == getLength())
533         {
534             Point2d p1 = this.points[this.points.length - 2];
535             Point2d p2 = this.points[this.points.length - 1];
536             return new Ray2d(p2.x, p2.y, p1.directionTo(p2));
537         }
538 
539         // find the index of the line segment, use binary search
540         int index = find(position);
541         double remainder = position - this.lengthIndexedLine[index];
542         double fraction = remainder / (this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index]);
543         Point2d p1 = this.points[index];
544         Point2d p2 = this.points[index + 1];
545         return new Ray2d(p1.x + fraction * (p2.x - p1.x), p1.y + fraction * (p2.y - p1.y), p1.directionTo(p2));
546     }
547 
548     /**
549      * Returns the fractional position along this line of the orthogonal projection of a point on this line. If the point is not
550      * orthogonal to the closest line segment, the nearest point is selected.
551      * @param point Point2d; the point to project
552      * @return fractional position along this line of the orthogonal projection on this line of a point
553      */
554     public final double projectOrthogonal(final Point2d point)
555     {
556         return projectOrthogonal(point.x, point.y);
557     }
558 
559     /**
560      * Returns the fractional position along this line of the orthogonal projection of point (x, y) on this line. If the point
561      * is not orthogonal to the closest line segment, the nearest point is selected.
562      * @param x double; x-coordinate of point to project
563      * @param y double; y-coordinate of point to project
564      * @return fractional position along this line of the orthogonal projection on this line of a point
565      */
566     public final double projectOrthogonal(final double x, final double y)
567     {
568         // prepare
569         double minDistance = Double.POSITIVE_INFINITY;
570         double minSegmentFraction = 0;
571         int minSegment = -1;
572 
573         // code based on Line2D.ptSegDistSq(...)
574         for (int i = 0; i < size() - 1; i++)
575         {
576             double dx = this.points[i + 1].x - this.points[i].x;
577             double dy = this.points[i + 1].y - this.points[i].y;
578             // vector relative to (x(i), y(i))
579             double px = x - this.points[i].x;
580             double py = y - this.points[i].y;
581             // dot product
582             double dot1 = px * dx + py * dy;
583             double f;
584             double distance;
585             if (dot1 > 0)
586             {
587                 // vector relative to (x(i+1), y(i+1))
588                 px = dx - px;
589                 py = dy - py;
590                 // dot product
591                 double dot2 = px * dx + py * dy;
592                 if (dot2 > 0)
593                 {
594                     // projection on line segment
595                     double len2 = dx * dx + dy * dy;
596                     double proj = dot2 * dot2 / len2;
597                     f = dot1 / len2;
598                     distance = px * px + py * py - proj;
599                 }
600                 else
601                 {
602                     // dot<=0 projection 'after' line segment
603                     f = 1;
604                     distance = px * px + py * py;
605                 }
606             }
607             else
608             {
609                 // dot<=0 projection 'before' line segment
610                 f = 0;
611                 distance = px * px + py * py;
612             }
613             // check if closer than previous
614             if (distance < minDistance)
615             {
616                 minDistance = distance;
617                 minSegmentFraction = f;
618                 minSegment = i;
619             }
620         }
621 
622         // return
623         double segLen = this.lengthIndexedLine[minSegment + 1] - this.lengthIndexedLine[minSegment];
624         return (this.lengthIndexedLine[minSegment] + segLen * minSegmentFraction) / getLength();
625     }
626 
627     /** {@inheritDoc} */
628     @Override
629     public PolyLine2d extract(final double start, final double end) throws DrawException
630     {
631         if (Double.isNaN(start) || Double.isNaN(end) || start < 0 || start >= end || end > getLength())
632         {
633             throw new DrawException(
634                     "Bad interval (" + start + ".." + end + "; length of this Line2d is " + this.getLength() + ")");
635         }
636         double cumulativeLength = 0;
637         double nextCumulativeLength = 0;
638         double segmentLength = 0;
639         int index = 0;
640         List<Point2d> pointList = new ArrayList<>();
641         while (start > cumulativeLength)
642         {
643             Point2d fromPoint = get(index);
644             index++;
645             Point2d toPoint = get(index);
646             segmentLength = fromPoint.distance(toPoint);
647             cumulativeLength = nextCumulativeLength;
648             nextCumulativeLength = cumulativeLength + segmentLength;
649             if (nextCumulativeLength >= start)
650             {
651                 break;
652             }
653         }
654         if (start == nextCumulativeLength)
655         {
656             pointList.add(get(index));
657         }
658         else
659         {
660             pointList.add(get(index - 1).interpolate(get(index), (start - cumulativeLength) / segmentLength));
661             if (end > nextCumulativeLength)
662             {
663                 pointList.add(get(index));
664             }
665         }
666         while (end > nextCumulativeLength)
667         {
668             Point2d fromPoint = get(index);
669             index++;
670             if (index >= size())
671             {
672                 break; // rounding error
673             }
674             Point2d toPoint = get(index);
675             segmentLength = fromPoint.distance(toPoint);
676             cumulativeLength = nextCumulativeLength;
677             nextCumulativeLength = cumulativeLength + segmentLength;
678             if (nextCumulativeLength >= end)
679             {
680                 break;
681             }
682             pointList.add(toPoint);
683         }
684         if (end == nextCumulativeLength)
685         {
686             pointList.add(get(index));
687         }
688         else
689         {
690             Point2d point = get(index - 1).interpolate(get(index), (end - cumulativeLength) / segmentLength);
691             // can be the same due to rounding
692             if (!point.equals(pointList.get(pointList.size() - 1)))
693             {
694                 pointList.add(point);
695             }
696         }
697         try
698         {
699             return instantiate(pointList);
700         }
701         catch (DrawRuntimeException exception)
702         {
703             CategoryLogger.always().error(exception, "interval " + start + ".." + end + " too short");
704             throw new DrawException("interval " + start + ".." + end + "too short");
705         }
706     }
707 
708     /** {@inheritDoc} */
709     @Override
710     public PolyLine2d truncate(final double position) throws DrawException
711     {
712         if (position <= 0.0 || position > getLength())
713         {
714             throw new DrawException("truncate for line: position <= 0.0 or > line length. Position = " + position
715                     + ". Length = " + getLength() + " m.");
716         }
717 
718         // handle special case: position == length
719         if (position == getLength())
720         {
721             return this;
722         }
723 
724         // find the index of the line segment
725         int index = find(position);
726         double remainder = position - lengthAtIndex(index);
727         double fraction = remainder / (lengthAtIndex(index + 1) - lengthAtIndex(index));
728         Point2d p1 = get(index);
729         Point2d lastPoint;
730         if (0.0 == fraction)
731         {
732             index--;
733             lastPoint = p1;
734         }
735         else
736         {
737             Point2d p2 = get(index + 1);
738             lastPoint = p1.interpolate(p2, fraction);
739 
740         }
741         // FIXME: Cannot create a P[]; will have to do it with a List<P>
742         List<Point2d> coords = new ArrayList<>(index + 2);
743         for (int i = 0; i <= index; i++)
744         {
745             coords.add(get(i));
746         }
747         coords.add(lastPoint);
748         return instantiate(coords);
749     }
750 
751     /** Default precision of approximation of arcs in the offsetLine method. */
752     public static final double DEFAULT_CIRCLE_PRECISION = 0.001;
753 
754     /** By default, noise in the reference line of the offsetLine method less than this value is always filtered. */
755     public static final double DEFAULT_OFFSET_MINIMUM_FILTER_VALUE = 0.001;
756 
757     /** By default, noise in the reference line of the offsetLineMethod greater than this value is never filtered. */
758     public static final double DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE = 0.1;
759 
760     /**
761      * By default, noise in the reference line of the offsetLineMethod less than <cite>offset / offsetFilterRatio</cite> is
762      * filtered except when the resulting value exceeds <cite>offsetMaximumFilterValue</cite>.
763      */
764     public static final double DEFAULT_OFFSET_FILTER_RATIO = 10;
765 
766     /** By default, the offsetLineMethod uses this offset precision. */
767     public static final double DEFAULT_OFFSET_PRECISION = 0.00001;
768 
769     /**
770      * Construct an offset line. This is similar to what geographical specialists call buffering, except that this method only
771      * construct a new line on one side of the reference line and does not add half disks around the end points. This method
772      * tries to strike a delicate balance between generating too few and too many points to approximate arcs. Noise in
773      * <cite>this</cite> (the reference line) can cause major artifacts in the offset line. This method calls the underlying
774      * method with default values for circlePrecision (<cite>DEFAULT_OFFSET</cite>), offsetMinimumFilterValue
775      * (<cite>DEFAULT_OFFSET_MINIMUM_FILTER_VALUE</cite>), offsetMaximumFilterValue
776      * (<cite>DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE</cite>), offsetFilterRatio (<cite>DEFAULT_OFFSET_FILTER_RATIO</cite>),
777      * minimumOffset (<cite>DEFAULT_OFFSET_PRECISION</cite>).
778      * @param offset double; the offset; positive values indicate left of the reference line, negative values indicate right of
779      *            the reference line
780      * @return PolyLine2d; a line at the specified offset from the reference line
781      */
782     public PolyLine2d offsetLine(final double offset)
783     {
784         return offsetLine(offset, DEFAULT_CIRCLE_PRECISION, DEFAULT_OFFSET_MINIMUM_FILTER_VALUE,
785                 DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE, DEFAULT_OFFSET_FILTER_RATIO, DEFAULT_OFFSET_PRECISION);
786     }
787 
788     /**
789      * Construct an offset line. This is similar to what geographical specialists call buffering, except that this method only
790      * construct a new line on one side of the reference line and does not add half disks around the end points. This method
791      * tries to strike a delicate balance between generating too few and too many points to approximate arcs. Noise in
792      * <cite>this</cite> (the reference line) can cause major artifacts in the offset line.
793      * @param offset double; the offset; positive values indicate left of the reference line, negative values indicate right of
794      *            the reference line
795      * @param circlePrecision double; precision of approximation of arcs; the line segments that are used to approximate an arc
796      *            will not deviate from the exact arc by more than this value
797      * @param offsetMinimumFilterValue double; noise in the reference line less than this value is always filtered
798      * @param offsetMaximumFilterValue double; noise in the reference line greater than this value is never filtered
799      * @param offsetFilterRatio double; noise in the reference line less than <cite>offset / offsetFilterRatio</cite> is
800      *            filtered except when the resulting value exceeds <cite>offsetMaximumFilterValue</cite>
801      * @param minimumOffset double; an offset value less than this value is treated as 0.0
802      * @return PolyLine2d; a line at the specified offset from the reference line
803      * @throws IllegalArgumentException when offset is NaN, or circlePrecision, offsetMinimumFilterValue,
804      *             offsetMaximumfilterValue, offsetFilterRatio, or minimumOffset is not positive, or NaN, or
805      *             offsetMinimumFilterValue &gt;= offsetMaximumFilterValue
806      */
807     @SuppressWarnings("checkstyle:methodlength")
808     public PolyLine2d offsetLine(final double offset, final double circlePrecision, final double offsetMinimumFilterValue,
809             final double offsetMaximumFilterValue, final double offsetFilterRatio, final double minimumOffset)
810             throws IllegalArgumentException
811     {
812         Throw.when(Double.isNaN(offset), IllegalArgumentException.class, "Offset may not be NaN");
813         Throw.when(Double.isNaN(circlePrecision) || circlePrecision <= 0, IllegalArgumentException.class,
814                 "bad circlePrecision");
815         Throw.when(Double.isNaN(offsetMinimumFilterValue) || offsetMinimumFilterValue <= 0, IllegalArgumentException.class,
816                 "bad offsetMinimumFilterValue");
817         Throw.when(Double.isNaN(offsetMaximumFilterValue) || offsetMaximumFilterValue <= 0, IllegalArgumentException.class,
818                 "bad offsetMaximumFilterValue");
819         Throw.when(Double.isNaN(offsetFilterRatio) || offsetFilterRatio <= 0, IllegalArgumentException.class,
820                 "bad offsetFilterRatio");
821         Throw.when(Double.isNaN(minimumOffset) || minimumOffset <= 0, IllegalArgumentException.class, "bad minimumOffset");
822         Throw.when(offsetMinimumFilterValue >= offsetMaximumFilterValue, IllegalArgumentException.class,
823                 "bad offset filter values; minimum must be less than maximum");
824         double bufferOffset = Math.abs(offset);
825         if (bufferOffset < minimumOffset)
826         {
827             return this;
828         }
829 
830         PolyLine2d filteredReferenceLine = noiseFilteredLine(
831                 Math.max(offsetMinimumFilterValue, Math.min(bufferOffset / offsetFilterRatio, offsetMaximumFilterValue)));
832         List<Point2d> tempPoints = new ArrayList<>();
833         // Make good use of the fact that an OTSLine3D cannot have consecutive duplicate points and has > 1 points
834         Point2d prevPoint = filteredReferenceLine.get(0);
835         Double prevAngle = null;
836         for (int index = 0; index < filteredReferenceLine.size() - 1; index++)
837         {
838             Point2d nextPoint = filteredReferenceLine.get(index + 1);
839             double angle = Math.atan2(nextPoint.y - prevPoint.y, nextPoint.x - prevPoint.x);
840             Point2dl#Point2d">Point2d segmentFrom = new Point2d(prevPoint.x - Math.sin(angle) * offset, prevPoint.y + Math.cos(angle) * offset);
841             Point2dtml#Point2d">Point2d segmentTo = new Point2d(nextPoint.x - Math.sin(angle) * offset, nextPoint.y + Math.cos(angle) * offset);
842             boolean addSegment = true;
843             if (index > 0)
844             {
845                 double deltaAngle = angle - prevAngle;
846                 if (Math.abs(deltaAngle) > Math.PI)
847                 {
848                     deltaAngle -= Math.signum(deltaAngle) * 2 * Math.PI;
849                 }
850                 if (deltaAngle * offset <= 0)
851                 {
852                     // Outside of curve of reference line
853                     // Approximate an arc using straight segments.
854                     // Determine how many segments are needed.
855                     int numSegments = 1;
856                     if (Math.abs(deltaAngle) > Math.PI / 2)
857                     {
858                         numSegments = 2;
859                     }
860                     while (true)
861                     {
862                         double maxError = bufferOffset * (1 - Math.abs(Math.cos(deltaAngle / numSegments / 2)));
863                         if (maxError < circlePrecision)
864                         {
865                             break; // required precision reached
866                         }
867                         numSegments *= 2;
868                     }
869                     Point2d prevArcPoint = tempPoints.get(tempPoints.size() - 1);
870                     // Generate the intermediate points
871                     for (int additionalPoint = 1; additionalPoint < numSegments; additionalPoint++)
872                     {
873                         double intermediateAngle =
874                                 (additionalPoint * angle + (numSegments - additionalPoint) * prevAngle) / numSegments;
875                         if (prevAngle * angle < 0 && Math.abs(prevAngle) > Math.PI / 2 && Math.abs(angle) > Math.PI / 2)
876                         {
877                             intermediateAngle += Math.PI;
878                         }
879                         Point2dt2d">Point2d intermediatePoint = new Point2d(prevPoint.x - Math.sin(intermediateAngle) * offset,
880                                 prevPoint.y + Math.cos(intermediateAngle) * offset);
881                         // Find any intersection points of the new segment and all previous segments
882                         Point2d prevSegFrom = null;
883                         int stopAt = tempPoints.size();
884                         for (int i = 0; i < stopAt; i++)
885                         {
886                             Point2d prevSegTo = tempPoints.get(i);
887                             if (null != prevSegFrom)
888                             {
889                                 Point2d prevSegIntersection = Point2d.intersectionOfLineSegments(prevArcPoint,
890                                         intermediatePoint, prevSegFrom, prevSegTo);
891                                 if (null != prevSegIntersection && prevSegIntersection.distance(prevArcPoint) > circlePrecision
892                                         && prevSegIntersection.distance(prevSegFrom) > circlePrecision
893                                         && prevSegIntersection.distance(prevSegTo) > circlePrecision)
894                                 {
895                                     tempPoints.add(prevSegIntersection);
896                                     // System.out.println(new OTSLine3D(tempPoints).toPlot());
897                                 }
898                             }
899                             prevSegFrom = prevSegTo;
900                         }
901                         Point2d nextSegmentIntersection =
902                                 Point2d.intersectionOfLineSegments(prevSegFrom, intermediatePoint, segmentFrom, segmentTo);
903                         if (null != nextSegmentIntersection)
904                         {
905                             tempPoints.add(nextSegmentIntersection);
906                             // System.out.println(new OTSLine3D(tempPoints).toPlot());
907                         }
908                         tempPoints.add(intermediatePoint);
909                         // System.out.println(new OTSLine3D(tempPoints).toPlot());
910                         prevArcPoint = intermediatePoint;
911                     }
912                 }
913                 // Inside of curve of reference line.
914                 // Add the intersection point of each previous segment and the next segment
915                 Point2d pPoint = null;
916                 int currentSize = tempPoints.size(); // PK DO NOT use the "dynamic" limit
917                 for (int i = 0; i < currentSize /* tempPoints.size() */; i++)
918                 {
919                     Point2d p = tempPoints.get(i);
920                     if (null != pPoint)
921                     {
922                         double pAngle = Math.atan2(p.y - pPoint.y, p.x - pPoint.x);
923                         double angleDifference = angle - pAngle;
924                         if (Math.abs(angleDifference) > Math.PI)
925                         {
926                             angleDifference -= Math.signum(angleDifference) * 2 * Math.PI;
927                         }
928                         if (Math.abs(angleDifference) > 0)// 0.01)
929                         {
930                             Point2d intersection = Point2d.intersectionOfLineSegments(pPoint, p, segmentFrom, segmentTo);
931                             if (null != intersection)
932                             {
933                                 if (tempPoints.size() - 1 == i)
934                                 {
935                                     tempPoints.remove(tempPoints.size() - 1);
936                                     segmentFrom = intersection;
937                                 }
938                                 else
939                                 {
940                                     tempPoints.add(intersection);
941                                 }
942                             }
943                         }
944                         else
945                         {
946                             // This is where things went very wrong in the TestGeometry demo.
947                             if (i == tempPoints.size() - 1)
948                             {
949                                 tempPoints.remove(tempPoints.size() - 1);
950                                 segmentFrom = tempPoints.get(tempPoints.size() - 1);
951                                 tempPoints.remove(tempPoints.size() - 1);
952                             }
953                         }
954                     }
955                     pPoint = p;
956                 }
957             }
958             if (addSegment)
959             {
960                 tempPoints.add(segmentFrom);
961                 tempPoints.add(segmentTo);
962                 prevPoint = nextPoint;
963                 prevAngle = angle;
964             }
965         }
966         // Remove points that are closer than the specified offset
967         for (int index = 1; index < tempPoints.size() - 1; index++)
968         {
969             Point2d checkPoint = tempPoints.get(index);
970             prevPoint = null;
971             boolean tooClose = false;
972             boolean somewhereAtCorrectDistance = false;
973             for (int i = 0; i < filteredReferenceLine.size(); i++)
974             {
975                 Point2d p = filteredReferenceLine.get(i);
976                 if (null != prevPoint)
977                 {
978                     Point2d closestPoint = checkPoint.closestPointOnSegment(prevPoint, p);
979                     double distance = closestPoint.distance(checkPoint);
980                     if (distance < bufferOffset - circlePrecision)
981                     {
982                         tooClose = true;
983                         break;
984                     }
985                     else if (distance < bufferOffset + minimumOffset)
986                     {
987                         somewhereAtCorrectDistance = true;
988                     }
989                 }
990                 prevPoint = p;
991             }
992             if (tooClose || !somewhereAtCorrectDistance)
993             {
994                 tempPoints.remove(index);
995                 index--;
996             }
997         }
998         try
999         {
1000             return PolyLine2d.createAndCleanPolyLine2d(tempPoints);
1001         }
1002         catch (DrawException exception)
1003         {
1004             exception.printStackTrace();
1005         }
1006         return null;
1007     }
1008 
1009     /**
1010      * Find a location on this PolyLine2d that is a reasonable projection of a DirectedPoint on this line. The result (if not
1011      * NaN) lies on a line perpendicular to the direction of the DirectedPoint and on some segment of this PolyLine. This method
1012      * attempts to give continuous results for continuous changes of the DirectedPoint that must be projected. There are cases
1013      * where this is simply impossible, or the optimal result is ambiguous. In these cases this method will return something
1014      * that is hopefully good enough.
1015      * @param orientedPoint Ray2d; the DirectedPoint
1016      * @return double; length along this PolyLine (some value between 0 and the length of this PolyLine) where directedPoint
1017      *         projects, or NaN if there is no solution
1018      * @throws NullPointerException when directedPoint is null
1019      */
1020     public double projectRay(final Ray2d orientedPoint) throws NullPointerException
1021     {
1022         Throw.whenNull(orientedPoint, "directedPoint may not be null");
1023         double bestDistance = Double.POSITIVE_INFINITY;
1024         double positionAtBestDistance = Double.NaN;
1025         Point2d prevPoint = null;
1026         // Define the line that is perpendicular to directedPoint, passing through directedPoint
1027         Point2d perpendicular = orientedPoint.translate(-Math.sin(orientedPoint.phi), Math.cos(orientedPoint.phi));
1028         double cumulativeDistance = 0;
1029         for (Point2d nextPoint : this.points)
1030         {
1031             if (null != prevPoint)
1032             {
1033                 double segmentLength = prevPoint.distance(nextPoint);
1034                 Point2d intersection = Point2d.intersectionOfLines(orientedPoint, perpendicular, prevPoint, nextPoint);
1035                 double distanceToPrevPoint = intersection.distance(prevPoint);
1036                 if (distanceToPrevPoint <= segmentLength && intersection.distance(nextPoint) <= segmentLength)
1037                 {
1038                     // Intersection is on the segment
1039                     double thisDistance = intersection.distance(orientedPoint);
1040                     if (thisDistance < bestDistance)
1041                     {
1042                         positionAtBestDistance = cumulativeDistance + distanceToPrevPoint;
1043                         bestDistance = thisDistance;
1044                     }
1045                 }
1046                 cumulativeDistance += segmentLength;
1047             }
1048             prevPoint = nextPoint;
1049         }
1050         return positionAtBestDistance;
1051     }
1052 
1053     /** {@inheritDoc} */
1054     @Override
1055     public String toString()
1056     {
1057         return "PolyLine2d [points=" + Arrays.toString(this.points) + "]";
1058     }
1059 
1060     /**
1061      * Convert this PolyLine2d to something that MS-Excel can plot.
1062      * @return excel XY plottable output
1063      */
1064     public final String toExcel()
1065     {
1066         StringBuffer s = new StringBuffer();
1067         for (Point2d p : this.points)
1068         {
1069             s.append(p.x + "\t" + p.y + "\n");
1070         }
1071         return s.toString();
1072     }
1073 
1074     /**
1075      * Convert this PolyLine3D to Peter's plot format.
1076      * @return Peter's format plot output
1077      */
1078     public final String toPlot()
1079     {
1080         StringBuffer result = new StringBuffer();
1081         for (Point2d p : this.points)
1082         {
1083             result.append(String.format(Locale.US, "%s%.3f,%.3f", 0 == result.length() ? "M" : " L", p.x, p.y));
1084         }
1085         result.append("\n");
1086         return result.toString();
1087     }
1088 
1089     /** {@inheritDoc} */
1090     @Override
1091     public int hashCode()
1092     {
1093         final int prime = 31;
1094         int result = 1;
1095         result = prime * result + Arrays.hashCode(this.points);
1096         return result;
1097     }
1098 
1099     /** {@inheritDoc} */
1100     @SuppressWarnings({"checkstyle:designforextension", "checkstyle:needbraces"})
1101     @Override
1102     public boolean equals(final Object obj)
1103     {
1104         if (this == obj)
1105             return true;
1106         if (obj == null)
1107             return false;
1108         if (getClass() != obj.getClass())
1109             return false;
1110         PolyLine2d../../../org/djutils/draw/line/PolyLine2d.html#PolyLine2d">PolyLine2d other = (PolyLine2d) obj;
1111         if (!Arrays.equals(this.points, other.points))
1112             return false;
1113         return true;
1114     }
1115 
1116 }