View Javadoc
1   package org.opentrafficsim.core.geometry;
2   
3   import java.awt.geom.Line2D;
4   import java.awt.geom.Path2D;
5   import java.awt.geom.PathIterator;
6   import java.awt.geom.Point2D;
7   import java.io.Serializable;
8   import java.util.ArrayList;
9   import java.util.Arrays;
10  import java.util.List;
11  import java.util.Locale;
12  
13  import javax.media.j3d.Bounds;
14  import javax.vecmath.Point3d;
15  
16  import org.djunits.unit.DirectionUnit;
17  import org.djunits.value.vdouble.scalar.Direction;
18  import org.djunits.value.vdouble.scalar.Length;
19  import org.djutils.exceptions.Throw;
20  import org.locationtech.jts.geom.Coordinate;
21  import org.locationtech.jts.geom.CoordinateSequence;
22  import org.locationtech.jts.geom.Envelope;
23  import org.locationtech.jts.geom.Geometry;
24  import org.locationtech.jts.geom.GeometryFactory;
25  import org.locationtech.jts.geom.LineString;
26  import org.locationtech.jts.linearref.LengthIndexedLine;
27  
28  import nl.tudelft.simulation.dsol.animation.Locatable;
29  import nl.tudelft.simulation.dsol.logger.SimLogger;
30  import nl.tudelft.simulation.language.d3.BoundingBox;
31  import nl.tudelft.simulation.language.d3.DirectedPoint;
32  
33  /**
34   * Line with OTSPoint3D points, a cached length indexed line, a cached length, and a cached centroid (all calculated on first
35   * use).
36   * <p>
37   * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
38   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
39   * <p>
40   * $LastChangedDate: 2015-07-16 10:20:53 +0200 (Thu, 16 Jul 2015) $, @version $Revision: 1124 $, by $Author: pknoppers $,
41   * initial version Jul 22, 2015 <br>
42   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
43   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
44   * @author <a href="http://www.citg.tudelft.nl">Guus Tamminga</a>
45   * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
46   */
47  public class OTSLine3D implements Locatable, Serializable
48  {
49      /** */
50      private static final long serialVersionUID = 20150722L;
51  
52      /** The points of the line. */
53      private OTSPoint3D[] points;
54  
55      /** The cumulative length of the line at point 'i'. */
56      private double[] lengthIndexedLine = null;
57  
58      /** The cached length; will be calculated at time of construction. */
59      private Length length;
60  
61      /** The cached centroid; will be calculated when needed for the first time. */
62      private OTSPoint3D centroid = null;
63  
64      /** The cached bounds; will be calculated when needed for the first time. */
65      private Bounds bounds = null;
66  
67      /** The cached helper points for fractional projection; will be calculated when needed for the first time. */
68      private OTSPoint3D[] fractionalHelperCenters = null;
69  
70      /** The cached helper directions for fractional projection; will be calculated when needed for the first time. */
71      private Point2D.Double[] fractionalHelperDirections = null;
72  
73      /** Intersection of unit offset lines of first two segments. */
74      private OTSPoint3D firstOffsetIntersection;
75  
76      /** Intersection of unit offset lines of last two segments. */
77      private OTSPoint3D lastOffsetIntersection;
78  
79      /** Precision for fractional projection algorithm. */
80      private static final double FRAC_PROJ_PRECISION = 2e-5 /* PK too fine 1e-6 */;
81  
82      /** Radius at each vertex. */
83      private Length[] vertexRadii;
84  
85      /** Bounding of this OTSLine3D. */
86      private Envelope envelope;
87  
88      /**
89       * Construct a new OTSLine3D.
90       * @param points OTSPoint3D...; the array of points to construct this OTSLine3D from.
91       * @throws OTSGeometryException when the provided points do not constitute a valid line (too few points or identical
92       *             adjacent points)
93       */
94      public OTSLine3D(final OTSPoint3D... points) throws OTSGeometryException
95      {
96          init(points);
97      }
98  
99      /**
100      * Construct a new OTSLine3D, and immediately make the length-indexed line.
101      * @param pts OTSPoint3D...; the array of points to construct this OTSLine3D from.
102      * @throws OTSGeometryException when the provided points do not constitute a valid line (too few points or identical
103      *             adjacent points)
104      */
105     private void init(final OTSPoint3D... pts) throws OTSGeometryException
106     {
107         if (pts.length < 2)
108         {
109             throw new OTSGeometryException("Degenerate OTSLine3D; has " + pts.length + " point" + (pts.length != 1 ? "s" : ""));
110         }
111         this.lengthIndexedLine = new double[pts.length];
112         this.lengthIndexedLine[0] = 0.0;
113         for (int i = 1; i < pts.length; i++)
114         {
115             if (pts[i - 1].x == pts[i].x && pts[i - 1].y == pts[i].y && pts[i - 1].z == pts[i].z)
116             {
117                 throw new OTSGeometryException(
118                         "Degenerate OTSLine3D; point " + (i - 1) + " has the same x, y and z as point " + i);
119             }
120             this.lengthIndexedLine[i] = this.lengthIndexedLine[i - 1] + pts[i - 1].distanceSI(pts[i]);
121         }
122         this.points = pts;
123         this.length = Length.instantiateSI(this.lengthIndexedLine[this.lengthIndexedLine.length - 1]);
124     }
125 
126     /** Which offsetLine method to use... */
127     public enum OffsetMethod
128     {
129         /** Via JTS buffer. */
130         JTS,
131 
132         /** Peter Knoppers. */
133         PK;
134     };
135 
136     /** Which offset line method to use... */
137     public static final OffsetMethod OFFSETMETHOD = OffsetMethod.PK;
138 
139     /**
140      * Construct parallel line.<br>
141      * TODO Let the Z-component of the result follow the Z-values of the reference line.
142      * @param offset double; offset distance from the reference line; positive is LEFT, negative is RIGHT
143      * @return OTSLine3D; the line that has the specified offset from the reference line
144      */
145     public final OTSLine3D offsetLine(final double offset)
146     {
147         try
148         {
149             switch (OFFSETMETHOD)
150             {
151                 case PK:
152                     return OTSOffsetLinePK.offsetLine(this, offset);
153 
154                 case JTS:
155                     return OTSBufferingJTS.offsetGeometryOLD(this, offset);
156 
157                 default:
158                     return null;
159             }
160         }
161         catch (OTSGeometryException exception)
162         {
163             SimLogger.always().error(exception);
164             return null;
165         }
166     }
167 
168     /**
169      * Construct a line that is equal to this line except for segments that are shorter than the <cite>noiseLevel</cite>. The
170      * result is guaranteed to start with the first point of this line and end with the last point of this line.
171      * @param noiseLevel double; the minimum segment length that is <b>not</b> removed
172      * @return OTSLine3D; the filtered line
173      */
174     public final OTSLine3D noiseFilteredLine(final double noiseLevel)
175     {
176         if (this.size() <= 2)
177         {
178             return this; // Except for some cached fields; an OTSLine3D is immutable; so safe to return
179         }
180         OTSPoint3D prevPoint = null;
181         List<OTSPoint3D> list = null;
182         for (int index = 0; index < this.size(); index++)
183         {
184             OTSPoint3D currentPoint = this.points[index];
185             if (null != prevPoint && prevPoint.distanceSI(currentPoint) < noiseLevel)
186             {
187                 if (null == list)
188                 {
189                     // Found something to filter; copy this up to (and including) prevPoint
190                     list = new ArrayList<>();
191                     for (int i = 0; i < index; i++)
192                     {
193                         list.add(this.points[i]);
194                     }
195                 }
196                 if (index == this.size() - 1)
197                 {
198                     if (list.size() > 1)
199                     {
200                         // Replace the last point of the result by the last point of this OTSLine3D
201                         list.set(list.size() - 1, currentPoint);
202                     }
203                     else
204                     {
205                         // Append the last point of this even though it is close to the first point than the noise value to
206                         // comply with the requirement that first and last point of this are ALWAYS included in the result.
207                         list.add(currentPoint);
208                     }
209                 }
210                 continue; // Do not replace prevPoint by currentPoint
211             }
212             else if (null != list)
213             {
214                 list.add(currentPoint);
215             }
216             prevPoint = currentPoint;
217         }
218         if (null == list)
219         {
220             return this;
221         }
222         if (list.size() == 2 && list.get(0).equals(list.get(1)))
223         {
224             SimLogger.always().debug("Fixing up degenerate noiseFilteredLine by inserting an intermediate point");
225             // Find something to insert along the way
226             for (int index = 1; index < this.size() - 1; index++)
227             {
228                 if (!this.points[index].equals(list.get(0)))
229                 {
230                     list.add(1, this.points[index]);
231                     break;
232                 }
233             }
234         }
235         try
236         {
237             return new OTSLine3D(list);
238         }
239         catch (OTSGeometryException exception)
240         {
241             SimLogger.always().error(exception);
242             throw new Error(exception);
243         }
244     }
245 
246     /**
247      * Clean up a list of points that describe a polyLine by removing points that lie within epsilon distance of a more
248      * straightened version of the line. <br>
249      * TODO Test this code (currently untested).
250      * @param epsilon double; maximal deviation
251      * @param useHorizontalDistance boolean; if true; the horizontal distance is used; if false; the 3D distance is used
252      * @return OTSLine3D; a new OTSLine3D containing all the remaining points
253      */
254     public final OTSLine3D noiseFilterRamerDouglasPeuker(final double epsilon, final boolean useHorizontalDistance)
255     {
256         // TODO rename this filter to noiseFilterRamerDouglasPeucker (with a c in Peucker).
257         try
258         {
259             // Apply the Ramer-Douglas-Peucker algorithm to the buffered points.
260             // Adapted from https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
261             double maxDeviation = 0;
262             int splitIndex = -1;
263             int pointCount = size();
264             OTSLine3DOTSLine3D.html#OTSLine3D">OTSLine3D straight = new OTSLine3D(get(0), get(pointCount - 1));
265             // Find the point with largest deviation from the straight line from start point to end point
266             for (int i = 1; i < pointCount - 1; i++)
267             {
268                 OTSPoint3D point = get(i);
269                 OTSPoint3D closest =
270                         useHorizontalDistance ? point.closestPointOnLine2D(straight) : point.closestPointOnLine(straight);
271                 double deviation = useHorizontalDistance ? closest.horizontalDistanceSI(point) : closest.distanceSI(point);
272                 if (deviation > maxDeviation)
273                 {
274                     splitIndex = i;
275                     maxDeviation = deviation;
276                 }
277             }
278             if (maxDeviation <= epsilon)
279             {
280                 // All intermediate points can be dropped. Return a new list containing only the first and last point.
281                 return straight;
282             }
283             // The largest deviation is larger than epsilon.
284             // Split the polyLine at the point with the maximum deviation. Process each sub list recursively and concatenate the
285             // results
286             OTSLine3Dry/OTSLine3D.html#OTSLine3D">OTSLine3D first = new OTSLine3D(Arrays.copyOfRange(this.points, 0, splitIndex + 1))
287                     .noiseFilterRamerDouglasPeuker(epsilon, useHorizontalDistance);
288             OTSLine3Dy/OTSLine3D.html#OTSLine3D">OTSLine3D second = new OTSLine3D(Arrays.copyOfRange(this.points, splitIndex, this.points.length))
289                     .noiseFilterRamerDouglasPeuker(epsilon, useHorizontalDistance);
290             return concatenate(epsilon, first, second);
291         }
292         catch (OTSGeometryException exception)
293         {
294             SimLogger.always().error(exception); // Peter thinks this cannot happen ...
295             return null;
296         }
297     }
298 
299     /**
300      * Create a line at linearly varying offset from this line. The offset may change linearly from its initial value at the
301      * start of the reference line to its final offset value at the end of the reference line.
302      * @param offsetAtStart double; offset at the start of the reference line (positive value is Left, negative value is Right)
303      * @param offsetAtEnd double; offset at the end of the reference line (positive value is Left, negative value is Right)
304      * @return Geometry; the Geometry of the line at linearly changing offset of the reference line
305      * @throws OTSGeometryException when this method fails to create the offset line
306      */
307     public final OTSLine3D offsetLine(final double offsetAtStart, final double offsetAtEnd) throws OTSGeometryException
308     {
309         // SimLogger.trace(Cat.CORE, OTSGeometry.printCoordinates("#referenceLine: \nc1,0,0\n# offset at start is "
310         // + offsetAtStart + " at end is " + offsetAtEnd + "\n#", referenceLine, "\n "));
311 
312         OTSLine3D offsetLineAtStart = offsetLine(offsetAtStart);
313         if (offsetAtStart == offsetAtEnd)
314         {
315             return offsetLineAtStart; // offset does not change
316         }
317         // SimLogger.trace(Cat.CORE, OTSGeometry.printCoordinates("#offset line at start: \nc0,0,0\n#",
318         // offsetLineAtStart, "\n "));
319         OTSLine3D offsetLineAtEnd = offsetLine(offsetAtEnd);
320         // SimLogger.trace(Cat.CORE, OTSGeometry.printCoordinates("#offset line at end: \nc0.7,0.7,0.7\n#",
321         // offsetLineAtEnd, "\n "));
322         Geometry startGeometry = offsetLineAtStart.getLineString();
323         Geometry endGeometry = offsetLineAtEnd.getLineString();
324         LengthIndexedLine first = new LengthIndexedLine(startGeometry);
325         double firstLength = startGeometry.getLength();
326         LengthIndexedLine second = new LengthIndexedLine(endGeometry);
327         double secondLength = endGeometry.getLength();
328         ArrayList<Coordinate> out = new ArrayList<>();
329         Coordinate[] firstCoordinates = startGeometry.getCoordinates();
330         Coordinate[] secondCoordinates = endGeometry.getCoordinates();
331         int firstIndex = 0;
332         int secondIndex = 0;
333         Coordinate prevCoordinate = null;
334         final double tooClose = 0.05; // 5 cm
335         while (firstIndex < firstCoordinates.length && secondIndex < secondCoordinates.length)
336         {
337             double firstRatio = firstIndex < firstCoordinates.length ? first.indexOf(firstCoordinates[firstIndex]) / firstLength
338                     : Double.MAX_VALUE;
339             double secondRatio = secondIndex < secondCoordinates.length
340                     ? second.indexOf(secondCoordinates[secondIndex]) / secondLength : Double.MAX_VALUE;
341             double ratio;
342             if (firstRatio < secondRatio)
343             {
344                 ratio = firstRatio;
345                 firstIndex++;
346             }
347             else
348             {
349                 ratio = secondRatio;
350                 secondIndex++;
351             }
352             Coordinate firstCoordinate = first.extractPoint(ratio * firstLength);
353             Coordinate secondCoordinate = second.extractPoint(ratio * secondLength);
354             Coordinate resultCoordinate = new Coordinate((1 - ratio) * firstCoordinate.x + ratio * secondCoordinate.x,
355                     (1 - ratio) * firstCoordinate.y + ratio * secondCoordinate.y);
356             if (null == prevCoordinate || resultCoordinate.distance(prevCoordinate) > tooClose)
357             {
358                 out.add(resultCoordinate);
359                 prevCoordinate = resultCoordinate;
360             }
361         }
362         Coordinate[] resultCoordinates = new Coordinate[out.size()];
363         for (int index = 0; index < out.size(); index++)
364         {
365             resultCoordinates[index] = out.get(index);
366         }
367         return new OTSLine3D(resultCoordinates);
368     }
369 
370     /**
371      * Create a line at linearly varying offset from this line. The offset may change linearly from its initial value at the
372      * start of the reference line via a number of intermediate offsets at intermediate positions to its final offset value at
373      * the end of the reference line.
374      * @param relativeFractions double[]; positional fractions for which the offsets have to be generated
375      * @param offsets double[]; offsets at the relative positions (positive value is Left, negative value is Right)
376      * @return Geometry; the Geometry of the line at linearly changing offset of the reference line
377      * @throws OTSGeometryException when this method fails to create the offset line
378      */
379     public final OTSLine3D offsetLine(final double[] relativeFractions, final double[] offsets) throws OTSGeometryException
380     {
381         OTSLine3Dine3D.html#OTSLine3D">OTSLine3D[] offsetLine = new OTSLine3D[relativeFractions.length];
382         for (int i = 0; i < offsets.length; i++)
383         {
384             offsetLine[i] = offsetLine(offsets[i]);
385             // SimLogger.trace(Cat.CORE, offsetLine[i].toExcel() + "\n");
386         }
387 
388         ArrayList<Coordinate> out = new ArrayList<>();
389         Coordinate prevCoordinate = null;
390         final double tooClose = 0.05; // 5 cm
391         for (int i = 0; i < offsets.length - 1; i++)
392         {
393             Geometry startGeometry =
394                     offsetLine[i].extractFractional(relativeFractions[i], relativeFractions[i + 1]).getLineString();
395             Geometry endGeometry =
396                     offsetLine[i + 1].extractFractional(relativeFractions[i], relativeFractions[i + 1]).getLineString();
397             LengthIndexedLine first = new LengthIndexedLine(startGeometry);
398             double firstLength = startGeometry.getLength();
399             LengthIndexedLine second = new LengthIndexedLine(endGeometry);
400             double secondLength = endGeometry.getLength();
401             Coordinate[] firstCoordinates = startGeometry.getCoordinates();
402             Coordinate[] secondCoordinates = endGeometry.getCoordinates();
403             int firstIndex = 0;
404             int secondIndex = 0;
405             while (firstIndex < firstCoordinates.length && secondIndex < secondCoordinates.length)
406             {
407                 double firstRatio = firstIndex < firstCoordinates.length
408                         ? first.indexOf(firstCoordinates[firstIndex]) / firstLength : Double.MAX_VALUE;
409                 double secondRatio = secondIndex < secondCoordinates.length
410                         ? second.indexOf(secondCoordinates[secondIndex]) / secondLength : Double.MAX_VALUE;
411                 double ratio;
412                 if (firstRatio < secondRatio)
413                 {
414                     ratio = firstRatio;
415                     firstIndex++;
416                 }
417                 else
418                 {
419                     ratio = secondRatio;
420                     secondIndex++;
421                 }
422                 Coordinate firstCoordinate = first.extractPoint(ratio * firstLength);
423                 Coordinate secondCoordinate = second.extractPoint(ratio * secondLength);
424                 Coordinate resultCoordinate = new Coordinate((1 - ratio) * firstCoordinate.x + ratio * secondCoordinate.x,
425                         (1 - ratio) * firstCoordinate.y + ratio * secondCoordinate.y);
426                 if (null == prevCoordinate || resultCoordinate.distance(prevCoordinate) > tooClose)
427                 {
428                     out.add(resultCoordinate);
429                     prevCoordinate = resultCoordinate;
430                 }
431             }
432         }
433 
434         Coordinate[] resultCoordinates = new Coordinate[out.size()];
435         for (int index = 0; index < out.size(); index++)
436         {
437             resultCoordinates[index] = out.get(index);
438         }
439         return new OTSLine3D(resultCoordinates);
440     }
441 
442     /**
443      * Concatenate several OTSLine3D instances.
444      * @param lines OTSLine3D...; OTSLine3D... one or more OTSLine3D. The last point of the first
445      *            &lt;strong&gt;must&lt;/strong&gt; match the first of the second, etc.
446      * @return OTSLine3D
447      * @throws OTSGeometryException if zero lines are given, or when there is a gap between consecutive lines
448      */
449     public static OTSLine3D concatenate(final OTSLine3D... lines) throws OTSGeometryException
450     {
451         return concatenate(0.0, lines);
452     }
453 
454     /**
455      * Concatenate two OTSLine3D instances. This method is separate for efficiency reasons.
456      * @param toleranceSI double; the tolerance between the end point of a line and the first point of the next line
457      * @param line1 OTSLine3D; first line
458      * @param line2 OTSLine3D; second line
459      * @return OTSLine3D
460      * @throws OTSGeometryException if zero lines are given, or when there is a gap between consecutive lines
461      */
462     public static OTSLine3D concatenate(finalOTSLine3Dy/OTSLine3D.html#OTSLine3D">OTSLine3Dclass="jxr_keyword">double toleranceSI, final OTSLine3Dy/OTSLine3D.html#OTSLine3D">OTSLine3D line1, final OTSLine3D line2)
463             throws OTSGeometryException
464     {
465         if (line1.getLast().distance(line2.getFirst()).si > toleranceSI)
466         {
467             throw new OTSGeometryException("Lines are not connected: " + line1.getLast() + " to " + line2.getFirst()
468                     + " distance is " + line1.getLast().distance(line2.getFirst()).si + " > " + toleranceSI);
469         }
470         int size = line1.size() + line2.size() - 1;
471         OTSPoint3DTSPoint3D.html#OTSPoint3D">OTSPoint3D[] points = new OTSPoint3D[size];
472         int nextIndex = 0;
473         for (int j = 0; j < line1.size(); j++)
474         {
475             points[nextIndex++] = line1.get(j);
476         }
477         for (int j = 1; j < line2.size(); j++)
478         {
479             points[nextIndex++] = line2.get(j);
480         }
481         return new OTSLine3D(points);
482     }
483 
484     /**
485      * Concatenate several OTSLine3D instances.
486      * @param toleranceSI double; the tolerance between the end point of a line and the first point of the next line
487      * @param lines OTSLine3D...; OTSLine3D... one or more OTSLine3D. The last point of the first
488      *            &lt;strong&gt;must&lt;/strong&gt; match the first of the second, etc.
489      * @return OTSLine3D
490      * @throws OTSGeometryException if zero lines are given, or when there is a gap between consecutive lines
491      */
492     public static OTSLine3D concatenate(final double toleranceSI, final OTSLine3D... lines) throws OTSGeometryException
493     {
494         // SimLogger.trace(Cat.CORE, "Concatenating " + lines.length + " lines.");
495         if (0 == lines.length)
496         {
497             throw new OTSGeometryException("Empty argument list");
498         }
499         else if (1 == lines.length)
500         {
501             return lines[0];
502         }
503         int size = lines[0].size();
504         for (int i = 1; i < lines.length; i++)
505         {
506             if (lines[i - 1].getLast().distance(lines[i].getFirst()).si > toleranceSI)
507             {
508                 throw new OTSGeometryException(
509                         "Lines are not connected: " + lines[i - 1].getLast() + " to " + lines[i].getFirst() + " distance is "
510                                 + lines[i - 1].getLast().distance(lines[i].getFirst()).si + " > " + toleranceSI);
511             }
512             size += lines[i].size() - 1;
513         }
514         OTSPoint3DTSPoint3D.html#OTSPoint3D">OTSPoint3D[] points = new OTSPoint3D[size];
515         int nextIndex = 0;
516         for (int i = 0; i < lines.length; i++)
517         {
518             OTSLine3D line = lines[i];
519             for (int j = 0 == i ? 0 : 1; j < line.size(); j++)
520             {
521                 points[nextIndex++] = line.get(j);
522             }
523         }
524         return new OTSLine3D(points);
525     }
526 
527     /**
528      * Construct a new OTSLine3D with all points of this OTSLine3D in reverse order.
529      * @return OTSLine3D; the new OTSLine3D
530      */
531     public final OTSLine3D reverse()
532     {
533         OTSPoint3Dt3D.html#OTSPoint3D">OTSPoint3D[] resultPoints = new OTSPoint3D[size()];
534         int nextIndex = size();
535         for (OTSPoint3D p : getPoints())
536         {
537             resultPoints[--nextIndex] = p;
538         }
539         try
540         {
541             return new OTSLine3D(resultPoints);
542         }
543         catch (OTSGeometryException exception)
544         {
545             // Cannot happen
546             throw new RuntimeException(exception);
547         }
548     }
549 
550     /**
551      * Construct a new OTSLine3D covering the indicated fraction of this OTSLine3D.
552      * @param start double; starting point, valid range [0..<cite>end</cite>)
553      * @param end double; ending point, valid range (<cite>start</cite>..1]
554      * @return OTSLine3D; the new OTSLine3D
555      * @throws OTSGeometryException when start &gt;= end, or start &lt; 0, or end &gt; 1
556      */
557     public final OTSLine3D extractFractional(final double start, final double end) throws OTSGeometryException
558     {
559         if (start < 0 || start >= end || end > 1)
560         {
561             throw new OTSGeometryException(
562                     "Bad interval (start=" + start + ", end=" + end + ", this is " + this.toString() + ")");
563         }
564         return extract(start * this.length.si, end * this.length.si);
565     }
566 
567     /**
568      * Create a new OTSLine3D that covers a sub-section of this OTSLine3D.
569      * @param start Length; the length along this OTSLine3D where the sub-section starts, valid range [0..<cite>end</cite>)
570      * @param end Length; length along this OTSLine3D where the sub-section ends, valid range
571      *            (<cite>start</cite>..<cite>length</cite> (length is the length of this OTSLine3D)
572      * @return OTSLine3D; the selected sub-section
573      * @throws OTSGeometryException when start &gt;= end, or start &lt; 0, or end &gt; length
574      */
575     public final OTSLine3D extract(final Length start, final Length end) throws OTSGeometryException
576     {
577         return extract(start.si, end.si);
578     }
579 
580     /**
581      * Create a new OTSLine3D that covers a sub-section of this OTSLine3D.
582      * @param start double; length along this OTSLine3D where the sub-section starts, valid range [0..<cite>end</cite>)
583      * @param end double; length along this OTSLine3D where the sub-section ends, valid range
584      *            (<cite>start</cite>..<cite>length</cite> (length is the length of this OTSLine3D)
585      * @return OTSLine3D; the selected sub-section
586      * @throws OTSGeometryException when start &gt;= end, or start &lt; 0, or end &gt; length
587      */
588     public final OTSLine3D extract(final double start, final double end) throws OTSGeometryException
589     {
590         if (Double.isNaN(start) || Double.isNaN(end) || start < 0 || start >= end || end > getLengthSI())
591         {
592             throw new OTSGeometryException(
593                     "Bad interval (" + start + ".." + end + "; length of this OTSLine3D is " + this.getLengthSI() + ")");
594         }
595         double cumulativeLength = 0;
596         double nextCumulativeLength = 0;
597         double segmentLength = 0;
598         int index = 0;
599         List<OTSPoint3D> pointList = new ArrayList<>();
600         // SimLogger.trace(Cat.CORE, "interval " + start + ".." + end);
601         while (start > cumulativeLength)
602         {
603             OTSPoint3D fromPoint = this.points[index];
604             index++;
605             OTSPoint3D toPoint = this.points[index];
606             segmentLength = fromPoint.distanceSI(toPoint);
607             cumulativeLength = nextCumulativeLength;
608             nextCumulativeLength = cumulativeLength + segmentLength;
609             if (nextCumulativeLength >= start)
610             {
611                 break;
612             }
613         }
614         if (start == nextCumulativeLength)
615         {
616             pointList.add(this.points[index]);
617         }
618         else
619         {
620             pointList.add(OTSPoint3D.interpolate((start - cumulativeLength) / segmentLength, this.points[index - 1],
621                     this.points[index]));
622             if (end > nextCumulativeLength)
623             {
624                 pointList.add(this.points[index]);
625             }
626         }
627         while (end > nextCumulativeLength)
628         {
629             OTSPoint3D fromPoint = this.points[index];
630             index++;
631             if (index >= this.points.length)
632             {
633                 break; // rounding error
634             }
635             OTSPoint3D toPoint = this.points[index];
636             segmentLength = fromPoint.distanceSI(toPoint);
637             cumulativeLength = nextCumulativeLength;
638             nextCumulativeLength = cumulativeLength + segmentLength;
639             if (nextCumulativeLength >= end)
640             {
641                 break;
642             }
643             pointList.add(toPoint);
644         }
645         if (end == nextCumulativeLength)
646         {
647             pointList.add(this.points[index]);
648         }
649         else
650         {
651             OTSPoint3D point = OTSPoint3D.interpolate((end - cumulativeLength) / segmentLength, this.points[index - 1],
652                     this.points[index]);
653             // can be the same due to rounding
654             if (!point.equals(pointList.get(pointList.size() - 1)))
655             {
656                 pointList.add(point);
657             }
658         }
659         try
660         {
661             return new OTSLine3D(pointList);
662         }
663         catch (OTSGeometryException exception)
664         {
665             SimLogger.always().error(exception, "interval " + start + ".." + end + " too short");
666             throw new OTSGeometryException("interval " + start + ".." + end + "too short");
667         }
668     }
669 
670     /**
671      * Build an array of OTSPoint3D from an array of Coordinate.
672      * @param coordinates Coordinate[]; the coordinates
673      * @return OTSPoint3D[]
674      */
675     private static OTSPoint3D[] coordinatesToOTSPoint3D(final Coordinate[] coordinates)
676     {
677         OTSPoint3DTSPoint3D.html#OTSPoint3D">OTSPoint3D[] result = new OTSPoint3D[coordinates.length];
678         for (int i = 0; i < coordinates.length; i++)
679         {
680             result[i] = new OTSPoint3D(coordinates[i]);
681         }
682         return result;
683     }
684 
685     /**
686      * Create an OTSLine3D, while cleaning repeating successive points.
687      * @param points OTSPoint3D...; the coordinates of the line as OTSPoint3D
688      * @return the line
689      * @throws OTSGeometryException when number of points &lt; 2
690      */
691     public static OTSLine3D createAndCleanOTSLine3D(final OTSPoint3D... points) throws OTSGeometryException
692     {
693         if (points.length < 2)
694         {
695             throw new OTSGeometryException(
696                     "Degenerate OTSLine3D; has " + points.length + " point" + (points.length != 1 ? "s" : ""));
697         }
698         return createAndCleanOTSLine3D(new ArrayList<>(Arrays.asList(points)));
699     }
700 
701     /**
702      * Create an OTSLine3D, while cleaning repeating successive points.
703      * @param pointList List&lt;OTSPoint3D&gt;; list of the coordinates of the line as OTSPoint3D; any duplicate points in this
704      *            list are removed (this method may modify the provided list)
705      * @return OTSLine3D; the line
706      * @throws OTSGeometryException when number of non-equal points &lt; 2
707      */
708     public static OTSLine3D createAndCleanOTSLine3D(final List<OTSPoint3D> pointList) throws OTSGeometryException
709     {
710         // clean successive equal points
711         int i = 1;
712         while (i < pointList.size())
713         {
714             if (pointList.get(i - 1).equals(pointList.get(i)))
715             {
716                 pointList.remove(i);
717             }
718             else
719             {
720                 i++;
721             }
722         }
723         return new OTSLine3D(pointList);
724     }
725 
726     /**
727      * Construct a new OTSLine3D from an array of Coordinate.
728      * @param coordinates Coordinate[]; the array of coordinates to construct this OTSLine3D from
729      * @throws OTSGeometryException when the provided points do not constitute a valid line (too few points or identical
730      *             adjacent points)
731      */
732     public OTSLine3D(final Coordinate[] coordinates) throws OTSGeometryException
733     {
734         this(coordinatesToOTSPoint3D(coordinates));
735     }
736 
737     /**
738      * Construct a new OTSLine3D from a LineString.
739      * @param lineString LineString; the lineString to construct this OTSLine3D from.
740      * @throws OTSGeometryException when the provided LineString does not constitute a valid line (too few points or identical
741      *             adjacent points)
742      */
743     public OTSLine3D(final LineString lineString) throws OTSGeometryException
744     {
745         this(lineString.getCoordinates());
746     }
747 
748     /**
749      * Construct a new OTSLine3D from a Geometry.
750      * @param geometry Geometry; the geometry to construct this OTSLine3D from
751      * @throws OTSGeometryException when the provided Geometry do not constitute a valid line (too few points or identical
752      *             adjacent points)
753      */
754     public OTSLine3D(final Geometry geometry) throws OTSGeometryException
755     {
756         this(geometry.getCoordinates());
757     }
758 
759     /**
760      * Construct a new OTSLine3D from a List&lt;OTSPoint3D&gt;.
761      * @param pointList List&lt;OTSPoint3D&gt;; the list of points to construct this OTSLine3D from.
762      * @throws OTSGeometryException when the provided points do not constitute a valid line (too few points or identical
763      *             adjacent points)
764      */
765     public OTSLine3D(final List<OTSPoint3D> pointList) throws OTSGeometryException
766     {
767         this(pointList.toArray(new OTSPoint3D[pointList.size()]));
768     }
769 
770     /**
771      * Construct a new OTSShape (closed shape) from a Path2D.
772      * @param path Path2D; the Path2D to construct this OTSLine3D from.
773      * @throws OTSGeometryException when the provided points do not constitute a valid line (too few points or identical
774      *             adjacent points)
775      */
776     public OTSLine3D(final Path2D path) throws OTSGeometryException
777     {
778         List<OTSPoint3D> pl = new ArrayList<>();
779         for (PathIterator pi = path.getPathIterator(null); !pi.isDone(); pi.next())
780         {
781             double[] p = new double[6];
782             int segType = pi.currentSegment(p);
783             if (segType == PathIterator.SEG_MOVETO || segType == PathIterator.SEG_LINETO)
784             {
785                 pl.add(new OTSPoint3D(p[0], p[1]));
786             }
787             else if (segType == PathIterator.SEG_CLOSE)
788             {
789                 if (!pl.get(0).equals(pl.get(pl.size() - 1)))
790                 {
791                     pl.add(new OTSPoint3D(pl.get(0).x, pl.get(0).y));
792                 }
793                 break;
794             }
795         }
796         init(pl.toArray(new OTSPoint3D[pl.size() - 1]));
797     }
798 
799     /**
800      * Construct a Coordinate array and fill it with the points of this OTSLine3D.
801      * @return an array of Coordinates corresponding to this OTSLine
802      */
803     public final Coordinate[] getCoordinates()
804     {
805         Coordinate[] result = new Coordinate[size()];
806         for (int i = 0; i < size(); i++)
807         {
808             result[i] = this.points[i].getCoordinate();
809         }
810         return result;
811     }
812 
813     /**
814      * Construct a LineString from this OTSLine3D.
815      * @return a LineString corresponding to this OTSLine
816      */
817     public final LineString getLineString()
818     {
819         GeometryFactory factory = new GeometryFactory();
820         Coordinate[] coordinates = getCoordinates();
821         CoordinateSequence cs = factory.getCoordinateSequenceFactory().create(coordinates);
822         return new LineString(cs, factory);
823     }
824 
825     /**
826      * Return the number of points in this OTSLine3D.
827      * @return the number of points on the line
828      */
829     public final int size()
830     {
831         return this.points.length;
832     }
833 
834     /**
835      * Return the first point of this OTSLine3D.
836      * @return the first point on the line
837      */
838     public final OTSPoint3D getFirst()
839     {
840         return this.points[0];
841     }
842 
843     /**
844      * Return the last point of this OTSLine3D.
845      * @return the last point on the line
846      */
847     public final OTSPoint3D getLast()
848     {
849         return this.points[size() - 1];
850     }
851 
852     /**
853      * Return one point of this OTSLine3D.
854      * @param i int; the index of the point to retrieve
855      * @return OTSPoint3d; the i-th point of the line
856      * @throws OTSGeometryException when i &lt; 0 or i &gt; the number of points
857      */
858     public final OTSPoint3D get(final int i) throws OTSGeometryException
859     {
860         if (i < 0 || i > size() - 1)
861         {
862             throw new OTSGeometryException("OTSLine3D.get(i=" + i + "); i<0 or i>=size(), which is " + size());
863         }
864         return this.points[i];
865     }
866 
867     /**
868      * Return the length of this OTSLine3D as a double value in SI units. (Assumes that the coordinates of the points
869      * constituting this line are expressed in meters.)
870      * @return the length of the line in SI units
871      */
872     public final double getLengthSI()
873     {
874         return this.length.si;
875     }
876 
877     /**
878      * Return the length of this OTSLine3D in meters. (Assuming that the coordinates of the points constituting this line are
879      * expressed in meters.)
880      * @return the length of the line
881      */
882     public final Length getLength()
883     {
884         return this.length;
885     }
886 
887     /**
888      * Return an array of OTSPoint3D that represents this OTSLine3D. <strong>Do not modify the result.</strong>
889      * @return the points of this line
890      */
891     public final OTSPoint3D[] getPoints()
892     {
893         return this.points;
894     }
895 
896     /**
897      * Make the length indexed line if it does not exist yet, and cache it.
898      */
899     private void makeLengthIndexedLine()
900     {
901         if (this.lengthIndexedLine == null)
902         {
903             this.lengthIndexedLine = new double[this.points.length];
904             this.lengthIndexedLine[0] = 0.0;
905             for (int i = 1; i < this.points.length; i++)
906             {
907                 this.lengthIndexedLine[i] = this.lengthIndexedLine[i - 1] + this.points[i - 1].distanceSI(this.points[i]);
908             }
909         }
910     }
911 
912     /**
913      * Get the location at a position on the line, with its direction. Position can be below 0 or more than the line length. In
914      * that case, the position will be extrapolated in the direction of the line at its start or end.
915      * @param position Length; the position on the line for which to calculate the point on, before, of after the line
916      * @return a directed point
917      */
918     public final DirectedPoint getLocationExtended(final Length position)
919     {
920         return getLocationExtendedSI(position.getSI());
921     }
922 
923     /**
924      * Get the location at a position on the line, with its direction. Position can be below 0 or more than the line length. In
925      * that case, the position will be extrapolated in the direction of the line at its start or end.
926      * @param positionSI double; the position on the line for which to calculate the point on, before, of after the line, in SI
927      *            units
928      * @return a directed point
929      */
930     public final DirectedPoint getLocationExtendedSI(final double positionSI)
931     {
932         makeLengthIndexedLine();
933         if (positionSI >= 0.0 && positionSI <= getLengthSI())
934         {
935             try
936             {
937                 return getLocationSI(positionSI);
938             }
939             catch (OTSGeometryException exception)
940             {
941                 // cannot happen
942             }
943         }
944 
945         // position before start point -- extrapolate
946         if (positionSI < 0.0)
947         {
948             double len = positionSI;
949             double fraction = len / (this.lengthIndexedLine[1] - this.lengthIndexedLine[0]);
950             OTSPoint3D p1 = this.points[0];
951             OTSPoint3D p2 = this.points[1];
952             return new DirectedPoint(p1.x + fraction * (p2.x - p1.x), p1.y + fraction * (p2.y - p1.y),
953                     p1.z + fraction * (p2.z - p1.z), 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
954         }
955 
956         // position beyond end point -- extrapolate
957         int n1 = this.lengthIndexedLine.length - 1;
958         int n2 = this.lengthIndexedLine.length - 2;
959         double len = positionSI - getLengthSI();
960         double fraction = len / (this.lengthIndexedLine[n1] - this.lengthIndexedLine[n2]);
961         OTSPoint3D p1 = this.points[n2];
962         OTSPoint3D p2 = this.points[n1];
963         return new DirectedPoint(p2.x + fraction * (p2.x - p1.x), p2.y + fraction * (p2.y - p1.y),
964                 p2.z + fraction * (p2.z - p1.z), 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
965     }
966 
967     /**
968      * Get the location at a fraction of the line, with its direction. Fraction should be between 0.0 and 1.0.
969      * @param fraction double; the fraction for which to calculate the point on the line
970      * @return a directed point
971      * @throws OTSGeometryException when fraction less than 0.0 or more than 1.0.
972      */
973     public final DirectedPoint getLocationFraction(final double fraction) throws OTSGeometryException
974     {
975         if (fraction < 0.0 || fraction > 1.0)
976         {
977             throw new OTSGeometryException("getLocationFraction for line: fraction < 0.0 or > 1.0. fraction = " + fraction);
978         }
979         return getLocationSI(fraction * getLengthSI());
980     }
981 
982     /**
983      * Get the location at a fraction of the line, with its direction. Fraction should be between 0.0 and 1.0.
984      * @param fraction double; the fraction for which to calculate the point on the line
985      * @param tolerance double; the delta from 0.0 and 1.0 that will be forgiven
986      * @return a directed point
987      * @throws OTSGeometryException when fraction less than 0.0 or more than 1.0.
988      */
989     public final DirectedPoint getLocationFraction(final double fraction, final double tolerance) throws OTSGeometryException
990     {
991         if (fraction < -tolerance || fraction > 1.0 + tolerance)
992         {
993             throw new OTSGeometryException(
994                     "getLocationFraction for line: fraction < 0.0 - tolerance or > 1.0 + tolerance; fraction = " + fraction);
995         }
996         double f = fraction < 0 ? 0.0 : fraction > 1.0 ? 1.0 : fraction;
997         return getLocationSI(f * getLengthSI());
998     }
999 
1000     /**
1001      * Get the location at a fraction of the line (or outside the line), with its direction.
1002      * @param fraction double; the fraction for which to calculate the point on the line
1003      * @return a directed point
1004      */
1005     public final DirectedPoint getLocationFractionExtended(final double fraction)
1006     {
1007         return getLocationExtendedSI(fraction * getLengthSI());
1008     }
1009 
1010     /**
1011      * Get the location at a position on the line, with its direction. Position should be between 0.0 and line length.
1012      * @param position Length; the position on the line for which to calculate the point on the line
1013      * @return a directed point
1014      * @throws OTSGeometryException when position less than 0.0 or more than line length.
1015      */
1016     public final DirectedPoint getLocation(final Length position) throws OTSGeometryException
1017     {
1018         return getLocationSI(position.getSI());
1019     }
1020 
1021     /**
1022      * Binary search for a position on the line.
1023      * @param pos double; the position to look for.
1024      * @return the index below the position; the position is between points[index] and points[index+1]
1025      * @throws OTSGeometryException when index could not be found
1026      */
1027     private int find(final double pos) throws OTSGeometryException
1028     {
1029         if (pos == 0)
1030         {
1031             return 0;
1032         }
1033 
1034         int lo = 0;
1035         int hi = this.lengthIndexedLine.length - 1;
1036         while (lo <= hi)
1037         {
1038             if (hi == lo)
1039             {
1040                 return lo;
1041             }
1042             int mid = lo + (hi - lo) / 2;
1043             if (pos < this.lengthIndexedLine[mid])
1044             {
1045                 hi = mid - 1;
1046             }
1047             else if (pos > this.lengthIndexedLine[mid + 1])
1048             {
1049                 lo = mid + 1;
1050             }
1051             else
1052             {
1053                 return mid;
1054             }
1055         }
1056         throw new OTSGeometryException(
1057                 "Could not find position " + pos + " on line with length indexes: " + Arrays.toString(this.lengthIndexedLine));
1058     }
1059 
1060     /**
1061      * Get the location at a position on the line, with its direction. Position should be between 0.0 and line length.
1062      * @param positionSI double; the position on the line for which to calculate the point on the line
1063      * @return a directed point
1064      * @throws OTSGeometryException when position less than 0.0 or more than line length.
1065      */
1066     public final DirectedPoint getLocationSI(final double positionSI) throws OTSGeometryException
1067     {
1068         makeLengthIndexedLine();
1069         if (positionSI < 0.0 || positionSI > getLengthSI())
1070         {
1071             throw new OTSGeometryException("getLocationSI for line: position < 0.0 or > line length. Position = " + positionSI
1072                     + " m. Length = " + getLengthSI() + " m.");
1073         }
1074 
1075         // handle special cases: position == 0.0, or position == length
1076         if (positionSI == 0.0)
1077         {
1078             OTSPoint3D p1 = this.points[0];
1079             OTSPoint3D p2 = this.points[1];
1080             return new DirectedPoint(p1.x, p1.y, p1.z, 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
1081         }
1082         if (positionSI == getLengthSI())
1083         {
1084             OTSPoint3D p1 = this.points[this.points.length - 2];
1085             OTSPoint3D p2 = this.points[this.points.length - 1];
1086             return new DirectedPoint(p2.x, p2.y, p2.z, 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
1087         }
1088 
1089         // find the index of the line segment, use binary search
1090         int index = find(positionSI);
1091         double remainder = positionSI - this.lengthIndexedLine[index];
1092         double fraction = remainder / (this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index]);
1093         OTSPoint3D p1 = this.points[index];
1094         OTSPoint3D p2 = this.points[index + 1];
1095         return new DirectedPoint(p1.x + fraction * (p2.x - p1.x), p1.y + fraction * (p2.y - p1.y),
1096                 p1.z + fraction * (p2.z - p1.z), 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
1097     }
1098 
1099     /**
1100      * Truncate a line at the given length (less than the length of the line, and larger than zero) and return a new line.
1101      * @param lengthSI double; the location where to truncate the line
1102      * @return a new OTSLine3D truncated at the exact position where line.getLength() == lengthSI
1103      * @throws OTSGeometryException when position less than 0.0 or more than line length.
1104      */
1105     public final OTSLine3D truncate(final double lengthSI) throws OTSGeometryException
1106     {
1107         makeLengthIndexedLine();
1108         if (lengthSI <= 0.0 || lengthSI > getLengthSI())
1109         {
1110             throw new OTSGeometryException("truncate for line: position <= 0.0 or > line length. Position = " + lengthSI
1111                     + " m. Length = " + getLengthSI() + " m.");
1112         }
1113 
1114         // handle special case: position == length
1115         if (lengthSI == getLengthSI())
1116         {
1117             return new OTSLine3D(getPoints());
1118         }
1119 
1120         // find the index of the line segment
1121         int index = find(lengthSI);
1122         double remainder = lengthSI - this.lengthIndexedLine[index];
1123         double fraction = remainder / (this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index]);
1124         OTSPoint3D p1 = this.points[index];
1125         OTSPoint3D lastPoint;
1126         if (0.0 == fraction)
1127         {
1128             index--;
1129             lastPoint = p1;
1130         }
1131         else
1132         {
1133             OTSPoint3D p2 = this.points[index + 1];
1134             lastPoint = new OTSPoint3D(p1.x + fraction * (p2.x - p1.x), p1.y + fraction * (p2.y - p1.y),
1135                     p1.z + fraction * (p2.z - p1.z));
1136 
1137         }
1138         OTSPoint3DTSPoint3D.html#OTSPoint3D">OTSPoint3D[] coords = new OTSPoint3D[index + 2];
1139         for (int i = 0; i <= index; i++)
1140         {
1141             coords[i] = this.points[i];
1142         }
1143         coords[index + 1] = lastPoint;
1144         return new OTSLine3D(coords);
1145     }
1146 
1147     /*-
1148      * TODO finish this method if it is needed; remove otherwise.
1149      * Calculate the first point on this line that intersects somewhere with the provided line, or NaN if no intersection was
1150      * found.
1151      * @param line the line to test the intersection with
1152      * @return the fraction of the first intersection point
1153      *
1154     public final double firstIntersectionFraction(final OTSLine3D line)
1155     {
1156         List<Line2D.Double> segs = new ArrayList<>();
1157         for (int j = 1; j < line.getPoints().length; j++)
1158         {
1159             Line2D.Double seg =
1160                 new Line2D.Double(this.points[j - 1].x, this.points[j - 1].y, this.points[j].x, this.points[j].y);
1161             segs.add(seg);
1162     
1163         }
1164         for (int i = 1; i < this.points.length; i++)
1165         {
1166             Line2D.Double thisSeg =
1167                 new Line2D.Double(this.points[i - 1].x, this.points[i - 1].y, this.points[i].x, this.points[i].y);
1168             for (Line2D.Double seg : segs)
1169             {
1170                 if (thisSeg.intersectsLine(seg))
1171                 {
1172                     // Point2D.Double intersectionPoint = thisSeg.
1173                     
1174                 }
1175             }
1176         }
1177         return Double.NaN;
1178     }
1179      */
1180 
1181     /**
1182      * Returns the fractional position along this line of the orthogonal projection of point (x, y) on this line. If the point
1183      * is not orthogonal to the closest line segment, the nearest point is selected.
1184      * @param x double; x-coordinate of point to project
1185      * @param y double; y-coordinate of point to project
1186      * @return fractional position along this line of the orthogonal projection on this line of a point
1187      */
1188     public final double projectOrthogonal(final double x, final double y)
1189     {
1190 
1191         // prepare
1192         makeLengthIndexedLine();
1193         double minDistance = Double.POSITIVE_INFINITY;
1194         double minSegmentFraction = 0;
1195         int minSegment = -1;
1196 
1197         // code based on Line2D.ptSegDistSq(...)
1198         for (int i = 0; i < size() - 1; i++)
1199         {
1200             double dx = this.points[i + 1].x - this.points[i].x;
1201             double dy = this.points[i + 1].y - this.points[i].y;
1202             // vector relative to (x(i), y(i))
1203             double px = x - this.points[i].x;
1204             double py = y - this.points[i].y;
1205             // dot product
1206             double dot1 = px * dx + py * dy;
1207             double f;
1208             double distance;
1209             if (dot1 > 0)
1210             {
1211                 // vector relative to (x(i+1), y(i+1))
1212                 px = dx - px;
1213                 py = dy - py;
1214                 // dot product
1215                 double dot2 = px * dx + py * dy;
1216                 if (dot2 > 0)
1217                 {
1218                     // projection on line segment
1219                     double len2 = dx * dx + dy * dy;
1220                     double proj = dot2 * dot2 / len2;
1221                     f = dot1 / len2;
1222                     distance = px * px + py * py - proj;
1223                 }
1224                 else
1225                 {
1226                     // dot<=0 projection 'after' line segment
1227                     f = 1;
1228                     distance = px * px + py * py;
1229                 }
1230             }
1231             else
1232             {
1233                 // dot<=0 projection 'before' line segment
1234                 f = 0;
1235                 distance = px * px + py * py;
1236             }
1237             // check if closer than previous
1238             if (distance < minDistance)
1239             {
1240                 minDistance = distance;
1241                 minSegmentFraction = f;
1242                 minSegment = i;
1243             }
1244         }
1245 
1246         // return
1247         double segLen = this.lengthIndexedLine[minSegment + 1] - this.lengthIndexedLine[minSegment];
1248         return (this.lengthIndexedLine[minSegment] + segLen * minSegmentFraction) / getLengthSI();
1249 
1250     }
1251 
1252     /**
1253      * Returns the fractional projection of a point to a line. The projection works by taking slices in space per line segment
1254      * as shown below. A point is always projected to the nearest segment, but not necessarily to the closest point on that
1255      * segment. The slices in space are analogous to a Voronoi diagram, but for the line segments instead of points. If
1256      * fractional projection fails, the orthogonal projection is returned.<br>
1257      * <br>
1258      * The point 'A' is projected to point 'B' on the 3rd segment of line 'C-D'. The line from 'A' to 'B' extends towards point
1259      * 'E', which is the intersection of lines 'E-F' and 'E-G'. Line 'E-F' cuts the first bend of the 3rd segment (at point 'H')
1260      * in half, while the line 'E-G' cuts the second bend of the 3rd segment (at point 'I') in half.
1261      * 
1262      * <pre>
1263      *            ____________________________     G                   .
1264      * .         |                            |    .                 .
1265      *   .       |  . . . .  helper lines     |    .               .
1266      *     .     |  _.._.._  projection line  |   I.             .
1267      *       .   |____________________________|  _.'._         .       L
1268      *        F.                              _.'  .  '-.    .
1269      *          ..                       B _.'     .     '-.
1270      *           . .                    _.\        .     .  D
1271      *            .  .               _.'   :       .   .
1272      *     J       .   .          _.'      \       . .
1273      *             ..    .     _.'          :      .                M
1274      *            .  .     ..-'             \      .
1275      *           .    .    /H.               A     .
1276      *          .      .  /    .                   .
1277      *        C _________/       .                 .
1278      *        .          .         .               .
1279      *   K   .            .          .             .
1280      *      .              .           .           .
1281      *     .                .            .         .           N
1282      *    .                  .             .       .
1283      *   .                    .              .     .
1284      *  .                      .               .   .
1285      * .                        .                . .
1286      *                           .                 .E
1287      *                            .                  .
1288      *                             .                   .
1289      *                              .                    .
1290      * </pre>
1291      * 
1292      * Fractional projection may fail in three cases.
1293      * <ol>
1294      * <li>Numerical difficulties at slight bend, orthogonal projection returns the correct point.</li>
1295      * <li>Fractional projection is possible only to segments that aren't the nearest segment(s).</li>
1296      * <li>Fractional projection is possible for no segment.</li>
1297      * </ol>
1298      * In the latter two cases the projection is undefined and a orthogonal projection is returned if
1299      * {@code orthoFallback = true}, or {@code NaN} if {@code orthoFallback = false}.
1300      * @param start Direction; direction in first point
1301      * @param end Direction; direction in last point
1302      * @param x double; x-coordinate of point to project
1303      * @param y double; y-coordinate of point to project
1304      * @param fallback FractionalFallback; fallback method for when fractional projection fails
1305      * @return fractional position along this line of the fractional projection on that line of a point
1306      */
1307     public final double projectFractional(final Direction start, final Direction end, final double x, final double y,
1308             final FractionalFallback fallback)
1309     {
1310 
1311         // prepare
1312         makeLengthIndexedLine();
1313         double minDistance = Double.POSITIVE_INFINITY;
1314         double minSegmentFraction = 0;
1315         int minSegment = -1;
1316         OTSPoint3Dy/OTSPoint3D.html#OTSPoint3D">OTSPoint3D point = new OTSPoint3D(x, y);
1317 
1318         // determine helpers (centers and directions)
1319         determineFractionalHelpers(start, end);
1320 
1321         // get distance of point to each segment
1322         double[] d = new double[this.points.length - 1];
1323         double minD = Double.POSITIVE_INFINITY;
1324         for (int i = 0; i < this.points.length - 1; i++)
1325         {
1326             d[i] = Line2D.ptSegDist(this.points[i].x, this.points[i].y, this.points[i + 1].x, this.points[i + 1].y, x, y);
1327             minD = d[i] < minD ? d[i] : minD;
1328         }
1329 
1330         // loop over segments for projection
1331         double distance;
1332         for (int i = 0; i < this.points.length - 1; i++)
1333         {
1334             // skip if not the closest segment, note that often two segments are equally close in their shared end point
1335             if (d[i] > minD + FRAC_PROJ_PRECISION)
1336             {
1337                 continue;
1338             }
1339             OTSPoint3D center = this.fractionalHelperCenters[i];
1340             OTSPoint3D p;
1341             if (center != null)
1342             {
1343                 // get intersection of line "center - (x, y)" and the segment
1344                 p = OTSPoint3D.intersectionOfLines(center, point, this.points[i], this.points[i + 1]);
1345                 if (p == null || (x < center.x + FRAC_PROJ_PRECISION && center.x + FRAC_PROJ_PRECISION < p.x)
1346                         || (x > center.x - FRAC_PROJ_PRECISION && center.x - FRAC_PROJ_PRECISION > p.x)
1347                         || (y < center.y + FRAC_PROJ_PRECISION && center.y + FRAC_PROJ_PRECISION < p.y)
1348                         || (y > center.y - FRAC_PROJ_PRECISION && center.y - FRAC_PROJ_PRECISION > p.y))
1349                 {
1350                     // projected point may not be 'beyond' segment center (i.e. center may not be between (x, y) and (p.x, p.y)
1351                     continue;
1352                 }
1353             }
1354             else
1355             {
1356                 // parallel helper lines, project along direction
1357                 OTSPoint3D offsetPoint =
1358                         new OTSPoint3D(x + this.fractionalHelperDirections[i].x, y + this.fractionalHelperDirections[i].y);
1359                 p = OTSPoint3D.intersectionOfLines(point, offsetPoint, this.points[i], this.points[i + 1]);
1360             }
1361             double segLength = this.points[i].distance(this.points[i + 1]).si + FRAC_PROJ_PRECISION;
1362             if (p == null || this.points[i].distance(p).si > segLength || this.points[i + 1].distance(p).si > segLength)
1363             {
1364                 // intersection must be on the segment
1365                 // in case of p == null, the length of the fractional helper direction falls away due to precision
1366                 continue;
1367             }
1368             // distance from (x, y) to intersection on segment
1369             double dx = x - p.x;
1370             double dy = y - p.y;
1371             distance = Math.sqrt(dx * dx + dy * dy);
1372             // distance from start of segment to point on segment
1373             if (distance < minDistance)
1374             {
1375                 dx = p.x - this.points[i].x;
1376                 dy = p.y - this.points[i].y;
1377                 double dFrac = Math.sqrt(dx * dx + dy * dy);
1378                 // fraction to point on segment
1379                 minDistance = distance;
1380                 minSegmentFraction = dFrac / (this.lengthIndexedLine[i + 1] - this.lengthIndexedLine[i]);
1381                 minSegment = i;
1382             }
1383         }
1384 
1385         // return
1386         if (minSegment == -1)
1387 
1388         {
1389             /*
1390              * If fractional projection fails (x, y) is either outside of the applicable area for fractional projection, or is
1391              * inside an area where numerical difficulties arise (i.e. far away outside of very slight bend which is considered
1392              * parallel).
1393              */
1394             // SimLogger.info(Cat.CORE, "projectFractional failed to project " + point + " on " + this
1395             // + "; using fallback approach");
1396             return fallback.getFraction(this, x, y);
1397         }
1398 
1399         double segLen = this.lengthIndexedLine[minSegment + 1] - this.lengthIndexedLine[minSegment];
1400         return (this.lengthIndexedLine[minSegment] + segLen * minSegmentFraction) / getLengthSI();
1401 
1402     }
1403 
1404     /**
1405      * Fallback method for when fractional projection fails as the point is beyond the line or from numerical limitations.
1406      * <p>
1407      * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
1408      * <br>
1409      * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
1410      * <p>
1411      * @version $Revision$, $LastChangedDate$, by $Author$, initial version 18 apr. 2018 <br>
1412      * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
1413      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
1414      * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
1415      */
1416     public enum FractionalFallback
1417     {
1418         /** Orthogonal projection. */
1419         ORTHOGONAL
1420         {
1421             @Override
1422             double getFraction(final OTSLine3D line, final double x, final double y)
1423             {
1424                 return line.projectOrthogonal(x, y);
1425             }
1426         },
1427 
1428         /** Distance to nearest end point. */
1429         ENDPOINT
1430         {
1431             @Override
1432             double getFraction(final OTSLine3D line, final double x, final double y)
1433             {
1434                 OTSPoint3Dy/OTSPoint3D.html#OTSPoint3D">OTSPoint3D point = new OTSPoint3D(x, y);
1435                 double dStart = point.distanceSI(line.getFirst());
1436                 double dEnd = point.distanceSI(line.getLast());
1437                 if (dStart < dEnd)
1438                 {
1439                     return -dStart / line.getLengthSI();
1440                 }
1441                 else
1442                 {
1443                     return (dEnd + line.getLengthSI()) / line.getLengthSI();
1444                 }
1445             }
1446         },
1447 
1448         /** NaN value. */
1449         NaN
1450         {
1451             @Override
1452             double getFraction(final OTSLine3D line, final double x, final double y)
1453             {
1454                 return Double.NaN;
1455             }
1456         };
1457 
1458         /**
1459          * Returns fraction for when fractional projection fails as the point is beyond the line or from numerical limitations.
1460          * @param line OTSLine3D; line
1461          * @param x double; x coordinate of point
1462          * @param y double; y coordinate of point
1463          * @return double; fraction for when fractional projection fails
1464          */
1465         abstract double getFraction(OTSLine3D line, double x, double y);
1466 
1467     }
1468 
1469     /**
1470      * Determines all helpers (points and/or directions) for fractional projection and stores fixed information in properties
1471      * while returning the first and last center points (.
1472      * @param start Direction; direction in first point
1473      * @param end Direction; direction in last point
1474      */
1475     private void determineFractionalHelpers(final Direction start, final Direction end)
1476     {
1477 
1478         final int n = this.points.length - 1;
1479 
1480         // calculate fixed helpers if not done yet
1481         if (this.fractionalHelperCenters == null)
1482         {
1483             this.fractionalHelperCenters = new OTSPoint3D[n];
1484             this.fractionalHelperDirections = new Point2D.Double[n];
1485             if (this.points.length > 2)
1486             {
1487                 // intersection of parallel lines of first and second segment
1488                 OTSLine3D prevOfsSeg = unitOffsetSegment(0);
1489                 OTSLine3D nextOfsSeg = unitOffsetSegment(1);
1490                 OTSPoint3D parStartPoint;
1491                 try
1492                 {
1493                     parStartPoint = OTSPoint3D.intersectionOfLines(prevOfsSeg.get(0), prevOfsSeg.get(1), nextOfsSeg.get(0),
1494                             nextOfsSeg.get(1));
1495                     if (parStartPoint == null || prevOfsSeg.get(1).distanceSI(nextOfsSeg.get(0)) < Math
1496                             .min(prevOfsSeg.get(1).distanceSI(parStartPoint), nextOfsSeg.get(0).distanceSI(parStartPoint)))
1497                     {
1498                         parStartPoint = new OTSPoint3D((prevOfsSeg.get(1).x + nextOfsSeg.get(0).x) / 2,
1499                                 (prevOfsSeg.get(1).y + nextOfsSeg.get(0).y) / 2);
1500                     }
1501                 }
1502                 catch (OTSGeometryException oge)
1503                 {
1504                     // cannot happen as only the first and second point (which are always present) are requested
1505                     throw new RuntimeException(oge);
1506                 }
1507                 // remember the intersection of the first two unit offset segments
1508                 this.firstOffsetIntersection = parStartPoint;
1509                 // loop segments
1510                 for (int i = 1; i < this.points.length - 2; i++)
1511                 {
1512                     prevOfsSeg = nextOfsSeg;
1513                     nextOfsSeg = unitOffsetSegment(i + 1);
1514                     OTSPoint3D parEndPoint;
1515                     try
1516                     {
1517                         parEndPoint = OTSPoint3D.intersectionOfLines(prevOfsSeg.get(0), prevOfsSeg.get(1), nextOfsSeg.get(0),
1518                                 nextOfsSeg.get(1));
1519                         if (parEndPoint == null || prevOfsSeg.get(1).distanceSI(nextOfsSeg.get(0)) < Math
1520                                 .min(prevOfsSeg.get(1).distanceSI(parEndPoint), nextOfsSeg.get(0).distanceSI(parEndPoint)))
1521                         {
1522                             parEndPoint = new OTSPoint3D((prevOfsSeg.get(1).x + nextOfsSeg.get(0).x) / 2,
1523                                     (prevOfsSeg.get(1).y + nextOfsSeg.get(0).y) / 2);
1524                         }
1525                     }
1526                     catch (OTSGeometryException oge)
1527                     {
1528                         // cannot happen as only the first and second point (which are always present) are requested
1529                         throw new RuntimeException(oge);
1530                     }
1531                     // center = intersections of helper lines
1532                     this.fractionalHelperCenters[i] =
1533                             OTSPoint3D.intersectionOfLines(this.points[i], parStartPoint, this.points[i + 1], parEndPoint);
1534                     if (this.fractionalHelperCenters[i] == null)
1535                     {
1536                         // parallel helper lines, parallel segments or /\/ cause parallel helper lines, use direction
1537                         this.fractionalHelperDirections[i] =
1538                                 new Point2D.Double(parStartPoint.x - this.points[i].x, parStartPoint.y - this.points[i].y);
1539                     }
1540                     parStartPoint = parEndPoint;
1541                 }
1542                 // remember the intersection of the last two unit offset segments
1543                 this.lastOffsetIntersection = parStartPoint;
1544             }
1545         }
1546 
1547         // use directions at start and end to get unit offset points to the left at a distance of 1
1548         double ang = (start == null ? Math.atan2(this.points[1].y - this.points[0].y, this.points[1].x - this.points[0].x)
1549                 : start.getInUnit(DirectionUnit.DEFAULT)) + Math.PI / 2; // start.si + Math.PI / 2;
1550         OTSPoint3Detry/OTSPoint3D.html#OTSPoint3D">OTSPoint3D p1 = new OTSPoint3D(this.points[0].x + Math.cos(ang), this.points[0].y + Math.sin(ang));
1551         ang = (end == null ? Math.atan2(this.points[n].y - this.points[n - 1].y, this.points[n].x - this.points[n - 1].x)
1552                 : end.getInUnit(DirectionUnit.DEFAULT)) + Math.PI / 2; // end.si + Math.PI / 2;
1553         OTSPoint3Detry/OTSPoint3D.html#OTSPoint3D">OTSPoint3D p2 = new OTSPoint3D(this.points[n].x + Math.cos(ang), this.points[n].y + Math.sin(ang));
1554 
1555         // calculate first and last center (i.e. intersection of unit offset segments), which depend on inputs 'start' and 'end'
1556         if (this.points.length > 2)
1557         {
1558             this.fractionalHelperCenters[0] =
1559                     OTSPoint3D.intersectionOfLines(this.points[0], p1, this.points[1], this.firstOffsetIntersection);
1560             this.fractionalHelperCenters[n - 1] =
1561                     OTSPoint3D.intersectionOfLines(this.points[n - 1], this.lastOffsetIntersection, this.points[n], p2);
1562             if (this.fractionalHelperCenters[n - 1] == null)
1563             {
1564                 // parallel helper lines, use direction for projection
1565                 this.fractionalHelperDirections[n - 1] = new Point2D.Double(p2.x - this.points[n].x, p2.y - this.points[n].y);
1566             }
1567         }
1568         else
1569         {
1570             // only a single segment
1571             this.fractionalHelperCenters[0] = OTSPoint3D.intersectionOfLines(this.points[0], p1, this.points[1], p2);
1572         }
1573         if (this.fractionalHelperCenters[0] == null)
1574         {
1575             // parallel helper lines, use direction for projection
1576             this.fractionalHelperDirections[0] = new Point2D.Double(p1.x - this.points[0].x, p1.y - this.points[0].y);
1577         }
1578 
1579     }
1580 
1581     /**
1582      * Helper method for fractional projection which returns an offset line to the left of a segment at a distance of 1.
1583      * @param segment int; segment number
1584      * @return parallel line to the left of a segment at a distance of 1
1585      */
1586     private OTSLine3D unitOffsetSegment(final int segment)
1587     {
1588 
1589         // double angle = Math.atan2(this.points[segment + 1].y - this.points[segment].y,
1590         // this.points[segment + 1].x - this.points[segment].x) + Math.PI / 2;
1591         // while (angle > Math.PI)
1592         // {
1593         // angle -= Math.PI;
1594         // }
1595         // while (angle < -Math.PI)
1596         // {
1597         // angle += Math.PI;
1598         // }
1599         // OTSPoint3D from = new OTSPoint3D(this.points[segment].x + Math.cos(angle), this.points[segment].y + Math.sin(angle));
1600         // OTSPoint3D to =
1601         // new OTSPoint3D(this.points[segment + 1].x + Math.cos(angle), this.points[segment + 1].y + Math.sin(angle));
1602         // try
1603         // {
1604         // return new OTSLine3D(from, to);
1605         // }
1606         // catch (OTSGeometryException oge)
1607         // {
1608         // // cannot happen as points are from this OTSLine3D which performed the same checks and 2 points are given
1609         // throw new RuntimeException(oge);
1610         // }
1611         OTSPoint3Dry/OTSPoint3D.html#OTSPoint3D">OTSPoint3D from = new OTSPoint3D(this.points[segment].x, this.points[segment].y);
1612         OTSPoint3Detry/OTSPoint3D.html#OTSPoint3D">OTSPoint3D to = new OTSPoint3D(this.points[segment + 1].x, this.points[segment + 1].y);
1613         try
1614         {
1615             OTSLine3Dtry/OTSLine3D.html#OTSLine3D">OTSLine3D line = new OTSLine3D(from, to);
1616             return line.offsetLine(1.0);
1617         }
1618         catch (OTSGeometryException oge)
1619         {
1620             // Cannot happen as points are from this OTSLine3D which performed the same checks and 2 points are given
1621             throw new RuntimeException(oge);
1622         }
1623     }
1624 
1625     /**
1626      * Returns the directional radius of the line at a given fraction. Negative values reflect right-hand curvature in the
1627      * design-line direction. The radius is taken as the minimum of the radii at the vertices before and after the given
1628      * fraction. The radius at a vertex is calculated as the radius of a circle that is equidistant from both edges connected to
1629      * the vertex. The circle center is on a line perpendicular to the shortest edge, crossing through the middle of the
1630      * shortest edge.
1631      * @param fraction double; fraction along the line, between 0.0 and 1.0 (both inclusive)
1632      * @return Length; radius
1633      * @throws OTSGeometryException fraction out of bounds
1634      */
1635     public Length getRadius(final double fraction) throws OTSGeometryException
1636     {
1637         Throw.when(fraction < 0.0 || fraction > 1.0, OTSGeometryException.class, "Fraction %f is out of bounds [0.0 ... 1.0]",
1638                 fraction);
1639         if (this.vertexRadii == null)
1640         {
1641             this.vertexRadii = new Length[size() - 1];
1642         }
1643         int index = find(fraction * getLength().si);
1644         if (index > 0 && this.vertexRadii[index] == null)
1645         {
1646             this.vertexRadii[index] = getVertexRadius(index);
1647         }
1648         if (index < size() - 2 && this.vertexRadii[index + 1] == null)
1649         {
1650             this.vertexRadii[index + 1] = getVertexRadius(index + 1);
1651         }
1652         if (index == 0)
1653         {
1654             return this.vertexRadii[1];
1655         }
1656         if (index == size() - 2)
1657         {
1658             return this.vertexRadii[size() - 2];
1659         }
1660         return Math.abs(this.vertexRadii[index].si) < Math.abs(this.vertexRadii[index + 1].si) ? this.vertexRadii[index]
1661                 : this.vertexRadii[index + 1];
1662     }
1663 
1664     /**
1665      * Calculates the directional radius at a vertex. Negative values reflect right-hand curvature in the design-line direction.
1666      * The radius at a vertex is calculated as the radius of a circle that is equidistant from both edges connected to the
1667      * vertex. The circle center is on a line perpendicular to the shortest edge, crossing through the middle of the shortest
1668      * edge.
1669      * @param index int; index of the vertex in range [1 ... size() - 2]
1670      * @return Length; radius at the vertex
1671      * @throws OTSGeometryException if the index is out of bounds
1672      */
1673     public Length getVertexRadius(final int index) throws OTSGeometryException
1674     {
1675         Throw.when(index < 1 || index > size() - 2, OTSGeometryException.class, "Index %d is out of bounds [1 ... size() - 2].",
1676                 index);
1677         makeLengthIndexedLine();
1678         determineFractionalHelpers(null, null);
1679         double length1 = this.lengthIndexedLine[index] - this.lengthIndexedLine[index - 1];
1680         double length2 = this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index];
1681         int shortIndex = length1 < length2 ? index : index + 1;
1682         // center of shortest edge
1683         OTSPoint3Detry/OTSPoint3D.html#OTSPoint3D">OTSPoint3D p1 = new OTSPoint3D(.5 * (this.points[shortIndex - 1].x + this.points[shortIndex].x),
1684                 .5 * (this.points[shortIndex - 1].y + this.points[shortIndex].y),
1685                 .5 * (this.points[shortIndex - 1].z + this.points[shortIndex].z));
1686         // perpendicular to shortest edge, line crossing p1
1687         OTSPoint3Detry/OTSPoint3D.html#OTSPoint3D">OTSPoint3D p2 = new OTSPoint3D(p1.x + (this.points[shortIndex].y - this.points[shortIndex - 1].y),
1688                 p1.y - (this.points[shortIndex].x - this.points[shortIndex - 1].x), p1.z);
1689         // vertex
1690         OTSPoint3D p3 = this.points[index];
1691         // point on line that splits angle between edges at vertex 50-50
1692         OTSPoint3D p4 = this.fractionalHelperCenters[index];
1693         if (p4 == null)
1694         {
1695             // parallel helper lines
1696             p4 = new OTSPoint3D(p3.x + this.fractionalHelperDirections[index].x,
1697                     p3.y + this.fractionalHelperDirections[index].y);
1698         }
1699         OTSPoint3D intersection = OTSPoint3D.intersectionOfLines(p1, p2, p3, p4);
1700         if (null == intersection)
1701         {
1702             return Length.instantiateSI(Double.NaN);
1703         }
1704         // determine left or right
1705         double refLength = length1 < length2 ? length1 : length2;
1706         Length radius = intersection.distance(p1);
1707         Length i2p2 = intersection.distance(p2);
1708         if (radius.si < i2p2.si && i2p2.si > refLength)
1709         {
1710             // left as p1 is closer than p2 (which was placed to the right) and not on the perpendicular line
1711             return radius;
1712         }
1713         // right as not left
1714         return radius.neg();
1715     }
1716 
1717     /**
1718      * Returns the length fraction at the vertex.
1719      * @param index int; index of vertex [0 ... size() - 1]
1720      * @return double; length fraction at the vertex
1721      * @throws OTSGeometryException if the index is out of bounds
1722      */
1723     public double getVertexFraction(final int index) throws OTSGeometryException
1724     {
1725         Throw.when(index < 0 || index > size() - 1, OTSGeometryException.class, "Index %d is out of bounds [0 %d].", index,
1726                 size() - 1);
1727         makeLengthIndexedLine();
1728         return this.lengthIndexedLine[index] / getLengthSI();
1729     }
1730 
1731     /**
1732      * Calculate the centroid of this line, and the bounds, and cache for later use.
1733      */
1734     private void calcCentroidBounds()
1735     {
1736         double minX = Double.POSITIVE_INFINITY;
1737         double minY = Double.POSITIVE_INFINITY;
1738         double minZ = Double.POSITIVE_INFINITY;
1739         double maxX = Double.NEGATIVE_INFINITY;
1740         double maxY = Double.NEGATIVE_INFINITY;
1741         double maxZ = Double.NEGATIVE_INFINITY;
1742         for (OTSPoint3D p : this.points)
1743         {
1744             minX = Math.min(minX, p.x);
1745             minY = Math.min(minY, p.y);
1746             minZ = Math.min(minZ, p.z);
1747             maxX = Math.max(maxX, p.x);
1748             maxY = Math.max(maxY, p.y);
1749             maxZ = Math.max(maxZ, p.z);
1750         }
1751         this.centroid = new OTSPoint3D((maxX + minX) / 2, (maxY + minY) / 2, (maxZ + minZ) / 2);
1752         double deltaX = maxX - minX;
1753         double deltaY = maxY - minY;
1754         double deltaZ = maxZ - minZ;
1755         this.bounds = new BoundingBox(new Point3d(-deltaX / 2.0, -deltaY / 2.0, -deltaZ / 2.0),
1756                 new Point3d(deltaX / 2, deltaY / 2, deltaZ / 2));
1757         this.envelope = new Envelope(minX, maxX, minY, maxY);
1758     }
1759 
1760     /**
1761      * Retrieve the centroid of this OTSLine3D.
1762      * @return OTSPoint3D; the centroid of this OTSLine3D
1763      */
1764     public final OTSPoint3D getCentroid()
1765     {
1766         if (this.centroid == null)
1767         {
1768             calcCentroidBounds();
1769         }
1770         return this.centroid;
1771     }
1772 
1773     /**
1774      * Get the bounding rectangle of this OTSLine3D.
1775      * @return Rectangle2D; the bounding rectangle of this OTSLine3D
1776      */
1777     public final Envelope getEnvelope()
1778     {
1779         if (this.envelope == null)
1780         {
1781             calcCentroidBounds();
1782         }
1783         return this.envelope;
1784     }
1785 
1786     /** {@inheritDoc} */
1787     @Override
1788     @SuppressWarnings("checkstyle:designforextension")
1789     public DirectedPoint getLocation()
1790     {
1791         if (this.centroid == null)
1792         {
1793             calcCentroidBounds();
1794         }
1795         return this.centroid.getDirectedPoint();
1796     }
1797 
1798     /** {@inheritDoc} */
1799     @Override
1800     @SuppressWarnings("checkstyle:designforextension")
1801     public Bounds getBounds()
1802     {
1803         if (this.bounds == null)
1804         {
1805             calcCentroidBounds();
1806         }
1807         return this.bounds;
1808     }
1809 
1810     /** {@inheritDoc} */
1811     @Override
1812     @SuppressWarnings("checkstyle:designforextension")
1813     public String toString()
1814     {
1815         return Arrays.toString(this.points);
1816     }
1817 
1818     /** {@inheritDoc} */
1819     @Override
1820     @SuppressWarnings("checkstyle:designforextension")
1821     public int hashCode()
1822     {
1823         final int prime = 31;
1824         int result = 1;
1825         result = prime * result + Arrays.hashCode(this.points);
1826         return result;
1827     }
1828 
1829     /** {@inheritDoc} */
1830     @Override
1831     @SuppressWarnings({"checkstyle:designforextension", "checkstyle:needbraces"})
1832     public boolean equals(final Object obj)
1833     {
1834         if (this == obj)
1835             return true;
1836         if (obj == null)
1837             return false;
1838         if (getClass() != obj.getClass())
1839             return false;
1840         OTSLine3D/../../../org/opentrafficsim/core/geometry/OTSLine3D.html#OTSLine3D">OTSLine3D other = (OTSLine3D) obj;
1841         if (!Arrays.equals(this.points, other.points))
1842             return false;
1843         return true;
1844     }
1845 
1846     /**
1847      * Convert the 2D projection of this OTSLine3D to something that MS-Excel can plot.
1848      * @return excel XY plottable output
1849      */
1850     public final String toExcel()
1851     {
1852         StringBuffer s = new StringBuffer();
1853         for (OTSPoint3D p : this.points)
1854         {
1855             s.append(p.x + "\t" + p.y + "\n");
1856         }
1857         return s.toString();
1858     }
1859 
1860     /**
1861      * Convert the 2D projection of this OTSLine3D to Peter's plot format.
1862      * @return Peter's format plot output
1863      */
1864     public final String toPlot()
1865     {
1866         StringBuffer result = new StringBuffer();
1867         for (OTSPoint3D p : this.points)
1868         {
1869             result.append(String.format(Locale.US, "%s%.3f,%.3f", 0 == result.length() ? "M" : " L", p.x, p.y));
1870         }
1871         result.append("\n");
1872         return result.toString();
1873     }
1874 
1875     /**
1876      * Test/development code for the fractional helper stuff.
1877      * @param args String[]; the command line arguments (not used)
1878      * @throws OTSGeometryException in case of error
1879      */
1880     public static void main(final String[] args) throws OTSGeometryException
1881     {
1882 
1883         /*
1884          * OTSLine3D line = new OTSLine3D(new OTSPoint3D(-263.811, -86.551, 1.180), new OTSPoint3D(-262.945, -84.450, 1.180),
1885          * new OTSPoint3D(-261.966, -82.074, 1.180), new OTSPoint3D(-260.890, -79.464, 1.198), new OTSPoint3D(-259.909, -76.955,
1886          * 1.198), new OTSPoint3D(-258.911, -74.400, 1.198), new OTSPoint3D(-257.830, -71.633, 1.234));
1887          * System.out.println(line.toExcel()); double[] relativeFractions = new double[] { 0.0, 0.19827228089475762,
1888          * 0.30549496392494213, 0.5824753163948581, 0.6815307752261827, 0.7903990449840241, 0.8942375145295614, 1.0 }; double[]
1889          * offsets = new double[] { 2.9779999256134, 4.6029999256134, 3.886839156071996, 2.3664845198627207, 1.7858981925396709,
1890          * 1.472348149010167, 2.0416709053157285, 2.798692100483229 }; System.out.println(line.offsetLine(relativeFractions,
1891          * offsets).toExcel());
1892          */
1893 
1894         List<OTSPoint3D> list = new ArrayList<>();
1895         boolean laneOn933 = true;
1896         if (!laneOn933)
1897         {
1898             double x = 0;
1899             double y = 0;
1900             double dx = 0.000001;
1901             double dy = 0.05;
1902             double ddx = 1.5;
1903             for (int i = 0; i < 32; i++)
1904             {
1905                 list.add(new OTSPoint3D(x, y));
1906                 x += dx;
1907                 dx *= ddx;
1908                 y += dy;
1909             }
1910         }
1911         else
1912         {
1913             String lineStr = "@0   426333.939, 4581372.345@" + "1   426333.92109750526, 4581372.491581111@"
1914                     + "2   426333.9016207722, 4581372.6364820665@" + "3   426333.8806181711, 4581372.7797264075@"
1915                     + "4   426333.8581377007, 4581372.921337651@" + "5   426333.8342269785, 4581373.061339286@"
1916                     + "6   426333.80893323367, 4581373.199754763@" + "7   426333.78230329906, 4581373.336607476@"
1917                     + "8   426333.75438360614, 4581373.471920755@" + "9   426333.7252201801, 4581373.605717849@"
1918                     + "10  426333.69485863775, 4581373.738021923@" + "11  426333.6633441839, 4581373.868856039@"
1919                     + "12  426333.6307216125, 4581373.998243135@" + "13  426333.5970353065, 4581374.1262060385@"
1920                     + "14  426333.56232923956, 4581374.252767426@" + "15  426333.54571270826, 4581374.331102062@"
1921                     + "16  426333.53121128445, 4581374.399777128@" + "17  426333.51761287224, 4581374.46141805@"
1922                     + "18  426333.5035609495, 4581374.524905452@" + "19  426333.4885681211, 4581374.590110448@"
1923                     + "20  426333.4750534529, 4581374.648530791@" + "21  426333.4586325006, 4581374.71720738@"
1924                     + "22  426333.44573716016, 4581374.770680802@" + "23  426333.4278589452, 4581374.84273674@"
1925                     + "24  426333.41565935884, 4581374.891382747@" + "25  426333.39629928104, 4581374.966726161@"
1926                     + "26  426333.3640042249, 4581375.089202983@" + "27  426333.3310233974, 4581375.210194213@"
1927                     + "28  426333.2974053264, 4581375.329726505@" + "29  426333.26319745823, 4581375.44782613@"
1928                     + "30  426333.2284461768, 4581375.564518943@" + "31  426333.1931968143, 4581375.679830365@"
1929                     + "32  426333.15749366966, 4581375.793785359@" + "33  426333.12138002727, 4581375.9064084105@"
1930                     + "34  426333.0848981781, 4581376.017723508@" + "35  426333.0526068902, 4581376.127395174@"
1931                     + "36  426333.0222216131, 4581376.235573194@" + "37  426333.00835773064, 4581376.284013769@"
1932                     + "38  426332.9916265083, 4581376.342442355@" + "39  426332.9771780217, 4581376.392075247@"
1933                     + "40  426332.96085931134, 4581376.448026933@" + "41  426332.9448449097, 4581376.5021694945@"
1934                     + "42  426332.9299564511, 4581376.552350422@" + "43  426332.9123899684, 4581376.610862428@"
1935                     + "44  426332.87985284685, 4581376.718179138@" + "45  426332.8472718188, 4581376.824143872@"
1936                     + "46  426332.81468381727, 4581376.92878003@" + "47  426332.78212446393, 4581377.032110168@"
1937                     + "48  426332.7496281178, 4581377.134155947@" + "49  426332.71722788643, 4581377.234938197@"
1938                     + "50  426332.68495568086, 4581377.3344768565@" + "51  426332.6528422234, 4581377.432791035@"
1939                     + "52  426332.6209170973, 4581377.529898969@" + "53  426332.59026768577, 4581377.622609458@"
1940                     + "54  426332.5618311538, 4581377.708242513@" + "55  426332.5292456913, 4581377.813700842@"
1941                     + "56  426332.5007497582, 4581377.905735847@" + "57  426332.4725916431, 4581377.996633883@"
1942                     + "58  426332.4447947076, 4581378.086409748@" + "59  426332.41739884845, 4581378.175020202@"
1943                     + "60  426332.3904224847, 4581378.262486783@" + "61  426332.37513187295, 4581378.312218361@"
1944                     + "62  426332.3474726438, 4581378.402429141@" + "63  426332.3203478011, 4581378.491354613@"
1945                     + "64  426332.2937555201, 4581378.579078223@" + "65  426332.26771504263, 4581378.665610338@"
1946                     + "66  426332.24224462465, 4581378.750960108@" + "67  426332.21736132156, 4581378.835136287@"
1947                     + "68  426332.1930813682, 4581378.918146061@" + "69  426332.1694196611, 4581378.999996922@"
1948                     + "70  426332.1468078785, 4581379.079234334@" + "71  426332.1253935003, 4581379.155326921@"
1949                     + "72  426332.10456227185, 4581379.230438552@" + "73  426332.08413377195, 4581379.301777359@"
1950                     + "74  426332.0575671712, 4581379.393246921@" + "75  426332.037751917, 4581379.463051603@"
1951                     + "76  426332.01541074895, 4581379.543672992@" + "77  426331.9954696024, 4581379.617241848@"
1952                     + "78  426331.9764488572, 4581379.689794578@" + "79  426331.9581173997, 4581379.761214821@"
1953                     + "80  426331.9407607595, 4581379.831643043@" + "81  426331.92459788476, 4581379.898797621@"
1954                     + "82  426331.89349001576, 4581380.036207511@" + "83  426331.8662295119, 4581380.167554456@"
1955                     + "84  426331.84239882755, 4581380.294825263@" + "85  426331.8220095046, 4581380.41813201@"
1956                     + "86  426331.80506772455, 4581380.537631294@" + "87  426331.79158302536, 4581380.653536015@"
1957                     + "88  426331.78158027114, 4581380.766126917@" + "89  426331.7754554946, 4581380.838605414@"
1958                     + "90  426331.76793314604, 4581380.909291444@" + "91  426331.7605002508, 4581381.016285149@"
1959                     + "92  426331.75725734304, 4581381.119549306@" + "93  426331.75814653496, 4581381.219559045@"
1960                     + "94  426331.76316353114, 4581381.316908372@" + "95  426331.7723867522, 4581381.412305131@"
1961                     + "96  426331.7860053539, 4581381.506554079@" + "97  426331.80434182915, 4581381.600527881@"
1962                     + "98  426331.82733581704, 4581381.692992337@" + "99  426331.8531803791, 4581381.777938947@"
1963                     + "100 426331.884024255, 4581381.864352291@" + "101 426331.92063241004, 4581381.953224321@"
1964                     + "102 426331.96390912175, 4581382.045434713@" + "103 426331.9901409878, 4581382.095566823@"
1965                     + "104 426332.0148562894, 4581382.141714169@" + "105 426332.05172826024, 4581382.204388889@"
1966                     + "106 426332.12722889386, 4581382.323121141@" + "107 426332.1628785428, 4581382.375872464@"
1967                     + "108 426332.22007742553, 4581382.462661629@" + "109 426332.26023980865, 4581382.523784153@"
1968                     + "110 426332.3033344728, 4581382.586422447@" + "111 426332.34946240357, 4581382.650580184@"
1969                     + "112 426332.3987196004, 4581382.716255575@" + "113 426332.4511967281, 4581382.783441929@"
1970                     + "114 426332.50697922776, 4581382.852128648@" + "115 426332.56614731904, 4581382.922301916@"
1971                     + "116 426332.628776037, 4581382.993945288@" + "117 426332.6949354622, 4581383.067040358@"
1972                     + "118 426332.76469110255, 4581383.141567508@" + "119 426332.8381037568, 4581383.217505949@"
1973                     + "120 426332.91523022414, 4581383.294834619@" + "121 426332.9961233405, 4581383.373532268@"
1974                     + "122 426333.0808322224, 4581383.453577724@" + "123 426333.1693585424, 4581383.534909724@"
1975                     + "124 426333.26164044754, 4581383.61741792@" + "125 426333.3650128907, 4581383.707446191@";
1976             int fromIndex = 0;
1977             while (true)
1978             {
1979                 int at1 = lineStr.indexOf('@', fromIndex);
1980                 fromIndex = at1 + 1;
1981                 int at2 = lineStr.indexOf('@', fromIndex);
1982                 if (at2 < 0)
1983                 {
1984                     break;
1985                 }
1986                 fromIndex = at2;
1987 
1988                 String subStr = lineStr.substring(at1 + 5, at2);
1989                 int comma = subStr.indexOf(',');
1990                 double x = Double.valueOf(subStr.substring(0, comma));
1991                 double y = Double.valueOf(subStr.substring(comma + 1));
1992 
1993                 list.add(new OTSPoint3D(x, y, 0.0));
1994 
1995             }
1996         }
1997         OTSLine3Dtry/OTSLine3D.html#OTSLine3D">OTSLine3D line = new OTSLine3D(list);
1998 
1999         line.projectFractional(null, null, 1.0, 0.5, FractionalFallback.NaN); // creates fractional helper points
2000 
2001         // create line of fractional helper points, give NaN points for null values
2002         OTSPoint3D[] array = line.fractionalHelperCenters;
2003         for (int i = 0; i < array.length; i++)
2004         {
2005             if (array[i] == null)
2006             {
2007                 array[i] = new OTSPoint3D(Double.NaN, Double.NaN);
2008             }
2009         }
2010         OTSLine3D/OTSLine3D.html#OTSLine3D">OTSLine3D helpers = new OTSLine3D(line.fractionalHelperCenters);
2011 
2012         // create Matlab compatible strings of lines
2013         StringBuilder str = new StringBuilder();
2014         str.append("line = [");
2015         String sep = "";
2016         for (OTSPoint3D p : line.getPoints())
2017         {
2018             str.append(String.format(Locale.US, "%s %.8f, %.8f", sep, p.x, p.y));
2019             sep = ",";
2020         }
2021         str.append("];\n");
2022 
2023         str.append("helpers = [");
2024         sep = "";
2025         for (OTSPoint3D p : helpers.getPoints())
2026         {
2027             str.append(String.format(Locale.US, "%s %.8f, %.8f", sep, p.x, p.y));
2028             sep = ",";
2029         }
2030         str.append("];\n");
2031 
2032         System.out.print(str);
2033     }
2034 
2035 }