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