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