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