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  
12  import javax.media.j3d.Bounds;
13  
14  import org.djunits.unit.LengthUnit;
15  import org.djunits.value.vdouble.scalar.Direction;
16  import org.djunits.value.vdouble.scalar.Length;
17  
18  import com.vividsolutions.jts.geom.Coordinate;
19  import com.vividsolutions.jts.geom.CoordinateSequence;
20  import com.vividsolutions.jts.geom.Envelope;
21  import com.vividsolutions.jts.geom.Geometry;
22  import com.vividsolutions.jts.geom.GeometryFactory;
23  import com.vividsolutions.jts.geom.LineString;
24  import com.vividsolutions.jts.linearref.LengthIndexedLine;
25  
26  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
27  import nl.tudelft.simulation.dsol.animation.Locatable;
28  import nl.tudelft.simulation.language.d3.BoundingBox;
29  import nl.tudelft.simulation.language.d3.DirectedPoint;
30  
31  /**
32   * Line with OTSPoint3D points, a cached length indexed line, a cahced length, and a cached centroid (all calculated on first
33   * use).
34   * <p>
35   * Copyright (c) 2013-2016 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
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 when needed for the first time. */
57      private double length = Double.NaN;
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;
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 = 1e-6;
79  
80      /** Bounding of this OTSLine3D. */
81      private Envelope envelope;
82  
83      /**
84       * Construct a new OTSLine3D.
85       * @param points 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 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     }
119 
120     /** Which offsetLine method to use... */
121     public enum OffsetMethod
122     {
123         /** Via JTS buffer. */
124         JTS,
125 
126         /** Peter Knoppers. */
127         PK;
128     };
129 
130     /** Which offset line method to use... */
131     public static final OffsetMethod OFFSETMETHOD = OffsetMethod.PK;
132 
133     /**
134      * Construct parallel line.<br>
135      * TODO Let the Z-component of the result follow the Z-values of the reference line.
136      * @param offset double; offset distance from the reference line; positive is LEFT, negative is RIGHT
137      * @return OTSLine3D; the line that has the specified offset from the reference line
138      */
139     public final OTSLine3D offsetLine(final double offset)
140     {
141         try
142         {
143             switch (OFFSETMETHOD)
144             {
145                 case PK:
146                     return OTSOffsetLinePK.offsetLine(this, offset);
147 
148                 case JTS:
149                     return OTSBufferingJTS.offsetGeometryOLD(this, offset);
150 
151                 default:
152                     return null;
153             }
154         }
155         catch (OTSGeometryException exception)
156         {
157             exception.printStackTrace();
158             return null;
159         }
160     }
161 
162     /**
163      * Construct a line that is equal to this line except for segments that are shorter than the <cite>noiseLevel</cite>. The
164      * result is guaranteed to start with the first point of this line and end with the last point of this line.
165      * @param noiseLevel double; the minimum segment length that is <b>not</b> removed
166      * @return OTSLine3D; the filtered line
167      */
168     public final OTSLine3D noiseFilteredLine(final double noiseLevel)
169     {
170         if (this.size() <= 2)
171         {
172             return this; // Except for some cached fields; an OTSLine3D is immutable; so safe to return
173         }
174         OTSPoint3D prevPoint = null;
175         List<OTSPoint3D> list = null;
176         for (int index = 0; index < this.size(); index++)
177         {
178             OTSPoint3D currentPoint = this.points[index];
179             if (null != prevPoint && prevPoint.distanceSI(currentPoint) < noiseLevel)
180             {
181                 if (null == list)
182                 {
183                     // Found something to filter; copy this up to (and including) prevPoint
184                     list = new ArrayList<OTSPoint3D>();
185                     for (int i = 0; i < index; i++)
186                     {
187                         list.add(this.points[i]);
188                     }
189                 }
190                 if (index == this.size() - 1)
191                 {
192                     if (list.size() > 1)
193                     {
194                         // Replace the last point of the result by the last point of this OTSLine3D
195                         list.set(list.size() - 1, currentPoint);
196                     }
197                     else
198                     {
199                         // Append the last point of this even though it is close to the first point than the noise value to
200                         // comply with the requirement that first and last point of this are ALWAYS included in the result.
201                         list.add(currentPoint);
202                     }
203                 }
204                 continue; // Do not replace prevPoint by currentPoint
205             }
206             else if (null != list)
207             {
208                 list.add(currentPoint);
209             }
210             prevPoint = currentPoint;
211         }
212         if (null == list)
213         {
214             return this;
215         }
216         try
217         {
218             return new OTSLine3D(list);
219         }
220         catch (OTSGeometryException exception)
221         {
222             System.err.println("CANNOT HAPPEN");
223             exception.printStackTrace();
224             throw new Error(exception);
225         }
226     }
227 
228     /**
229      * Clean up a list of points that describe a polyLine by removing points that lie within epsilon distance of a more
230      * straightened version of the line. <br>
231      * TODO Test this code (currently untested).
232      * @param epsilon double; maximal deviation
233      * @param useHorizontalDistance boolean; if true; the horizontal distance is used; if false; the 3D distance is used
234      * @return OTSLine3D; a new OTSLine3D containing all the remaining points
235      */
236     public final OTSLine3D noiseFilterRamerDouglasPeuker(final double epsilon, final boolean useHorizontalDistance)
237     {
238         try
239         {
240             // Apply the Ramer-Douglas-Peucker algorithm to the buffered points.
241             // Adapted from https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
242             double maxDeviation = 0;
243             int splitIndex = -1;
244             int pointCount = size();
245             OTSLine3D straight = new OTSLine3D(get(0), get(pointCount - 1));
246             // Find the point with largest deviation from the straight line from start point to end point
247             for (int i = 1; i < pointCount - 1; i++)
248             {
249                 OTSPoint3D point = get(i);
250                 OTSPoint3D closest =
251                         useHorizontalDistance ? point.closestPointOnLine2D(straight) : point.closestPointOnLine(straight);
252                 double deviation = useHorizontalDistance ? closest.horizontalDistanceSI(point) : closest.distanceSI(point);
253                 if (deviation > maxDeviation)
254                 {
255                     splitIndex = i;
256                     maxDeviation = deviation;
257                 }
258             }
259             if (maxDeviation <= epsilon)
260             {
261                 // All intermediate points can be dropped. Return a new list containing only the first and last point.
262                 return straight;
263             }
264             // The largest deviation is larger than epsilon.
265             // Split the polyLine at the point with the maximum deviation. Process each sub list recursively and concatenate the
266             // results
267             OTSLine3D first = new OTSLine3D(Arrays.copyOfRange(this.points, 0, splitIndex + 1))
268                     .noiseFilterRamerDouglasPeuker(epsilon, useHorizontalDistance);
269             OTSLine3D second = new OTSLine3D(Arrays.copyOfRange(this.points, splitIndex, this.points.length))
270                     .noiseFilterRamerDouglasPeuker(epsilon, useHorizontalDistance);
271             return concatenate(epsilon, first, second);
272         }
273         catch (OTSGeometryException exception)
274         {
275             exception.printStackTrace(); // Peter thinks this cannot happen ...
276             return null;
277         }
278     }
279 
280     /**
281      * Create a line at linearly varying offset from this line. The offset may change linearly from its initial value at the
282      * start of the reference line to its final offset value at the end of the reference line.
283      * @param offsetAtStart double; offset at the start of the reference line (positive value is Left, negative value is Right)
284      * @param offsetAtEnd double; offset at the end of the reference line (positive value is Left, negative value is Right)
285      * @return Geometry; the Geometry of the line at linearly changing offset of the reference line
286      * @throws OTSGeometryException when this method fails to create the offset line
287      */
288     public final OTSLine3D offsetLine(final double offsetAtStart, final double offsetAtEnd) throws OTSGeometryException
289     {
290         // System.out.println(OTSGeometry.printCoordinates("#referenceLine: \nc1,0,0\n# offset at start is " + offsetAtStart
291         // + " at end is " + offsetAtEnd + "\n#", referenceLine, "\n "));
292 
293         OTSLine3D offsetLineAtStart = offsetLine(offsetAtStart);
294         if (offsetAtStart == offsetAtEnd)
295         {
296             return offsetLineAtStart; // offset does not change
297         }
298         // System.out.println(OTSGeometry.printCoordinates("#offset line at start: \nc0,0,0\n#", offsetLineAtStart, "\n "));
299         OTSLine3D offsetLineAtEnd = offsetLine(offsetAtEnd);
300         // System.out.println(OTSGeometry.printCoordinates("#offset line at end: \nc0.7,0.7,0.7\n#", offsetLineAtEnd, "\n "));
301         Geometry startGeometry = offsetLineAtStart.getLineString();
302         Geometry endGeometry = offsetLineAtEnd.getLineString();
303         LengthIndexedLine first = new LengthIndexedLine(startGeometry);
304         double firstLength = startGeometry.getLength();
305         LengthIndexedLine second = new LengthIndexedLine(endGeometry);
306         double secondLength = endGeometry.getLength();
307         ArrayList<Coordinate> out = new ArrayList<Coordinate>();
308         Coordinate[] firstCoordinates = startGeometry.getCoordinates();
309         Coordinate[] secondCoordinates = endGeometry.getCoordinates();
310         int firstIndex = 0;
311         int secondIndex = 0;
312         Coordinate prevCoordinate = null;
313         final double tooClose = 0.05; // 5 cm
314         while (firstIndex < firstCoordinates.length && secondIndex < secondCoordinates.length)
315         {
316             double firstRatio = firstIndex < firstCoordinates.length ? first.indexOf(firstCoordinates[firstIndex]) / firstLength
317                     : Double.MAX_VALUE;
318             double secondRatio = secondIndex < secondCoordinates.length
319                     ? second.indexOf(secondCoordinates[secondIndex]) / secondLength : Double.MAX_VALUE;
320             double ratio;
321             if (firstRatio < secondRatio)
322             {
323                 ratio = firstRatio;
324                 firstIndex++;
325             }
326             else
327             {
328                 ratio = secondRatio;
329                 secondIndex++;
330             }
331             Coordinate firstCoordinate = first.extractPoint(ratio * firstLength);
332             Coordinate secondCoordinate = second.extractPoint(ratio * secondLength);
333             Coordinate resultCoordinate = new Coordinate((1 - ratio) * firstCoordinate.x + ratio * secondCoordinate.x,
334                     (1 - ratio) * firstCoordinate.y + ratio * secondCoordinate.y);
335             if (null == prevCoordinate || resultCoordinate.distance(prevCoordinate) > tooClose)
336             {
337                 out.add(resultCoordinate);
338                 prevCoordinate = resultCoordinate;
339             }
340         }
341         Coordinate[] resultCoordinates = new Coordinate[out.size()];
342         for (int index = 0; index < out.size(); index++)
343         {
344             resultCoordinates[index] = out.get(index);
345         }
346         return new OTSLine3D(resultCoordinates);
347     }
348 
349     /**
350      * Create a line at linearly varying offset from this line. The offset may change linearly from its initial value at the
351      * start of the reference line via a number of intermediate offsets at intermediate positions to its final offset value at
352      * the end of the reference line.
353      * @param relativeFractions double[]; positional fractions for which the offsets have to be generated
354      * @param offsets double[]; offsets at the relative positions (positive value is Left, negative value is Right)
355      * @return Geometry; the Geometry of the line at linearly changing offset of the reference line
356      * @throws OTSGeometryException when this method fails to create the offset line
357      */
358     public final OTSLine3D offsetLine(final double[] relativeFractions, final double[] offsets) throws OTSGeometryException
359     {
360         OTSLine3D[] offsetLine = new OTSLine3D[relativeFractions.length];
361         for (int i = 0; i < offsets.length; i++)
362         {
363             offsetLine[i] = offsetLine(offsets[i]);
364             // System.out.println(offsetLine[i].toExcel());
365             // System.out.println();
366         }
367 
368         ArrayList<Coordinate> out = new ArrayList<Coordinate>();
369         Coordinate prevCoordinate = null;
370         final double tooClose = 0.05; // 5 cm
371         for (int i = 0; i < offsets.length - 1; i++)
372         {
373             Geometry startGeometry =
374                     offsetLine[i].extractFractional(relativeFractions[i], relativeFractions[i + 1]).getLineString();
375             Geometry endGeometry =
376                     offsetLine[i + 1].extractFractional(relativeFractions[i], relativeFractions[i + 1]).getLineString();
377             LengthIndexedLine first = new LengthIndexedLine(startGeometry);
378             double firstLength = startGeometry.getLength();
379             LengthIndexedLine second = new LengthIndexedLine(endGeometry);
380             double secondLength = endGeometry.getLength();
381             Coordinate[] firstCoordinates = startGeometry.getCoordinates();
382             Coordinate[] secondCoordinates = endGeometry.getCoordinates();
383             int firstIndex = 0;
384             int secondIndex = 0;
385             while (firstIndex < firstCoordinates.length && secondIndex < secondCoordinates.length)
386             {
387                 double firstRatio = firstIndex < firstCoordinates.length
388                         ? first.indexOf(firstCoordinates[firstIndex]) / firstLength : Double.MAX_VALUE;
389                 double secondRatio = secondIndex < secondCoordinates.length
390                         ? second.indexOf(secondCoordinates[secondIndex]) / secondLength : Double.MAX_VALUE;
391                 double ratio;
392                 if (firstRatio < secondRatio)
393                 {
394                     ratio = firstRatio;
395                     firstIndex++;
396                 }
397                 else
398                 {
399                     ratio = secondRatio;
400                     secondIndex++;
401                 }
402                 Coordinate firstCoordinate = first.extractPoint(ratio * firstLength);
403                 Coordinate secondCoordinate = second.extractPoint(ratio * secondLength);
404                 Coordinate resultCoordinate = new Coordinate((1 - ratio) * firstCoordinate.x + ratio * secondCoordinate.x,
405                         (1 - ratio) * firstCoordinate.y + ratio * secondCoordinate.y);
406                 if (null == prevCoordinate || resultCoordinate.distance(prevCoordinate) > tooClose)
407                 {
408                     out.add(resultCoordinate);
409                     prevCoordinate = resultCoordinate;
410                 }
411             }
412         }
413 
414         Coordinate[] resultCoordinates = new Coordinate[out.size()];
415         for (int index = 0; index < out.size(); index++)
416         {
417             resultCoordinates[index] = out.get(index);
418         }
419         return new OTSLine3D(resultCoordinates);
420     }
421 
422     /**
423      * Concatenate several OTSLine3D instances.
424      * @param lines OTSLine3D... one or more OTSLine3D. The last point of the first <strong>must</strong> match the first of the
425      *            second, etc.
426      * @return OTSLine3D
427      * @throws OTSGeometryException if zero lines are given, or when there is a gap between consecutive lines
428      */
429     public static OTSLine3D concatenate(final OTSLine3D... lines) throws OTSGeometryException
430     {
431         return concatenate(0.0, lines);
432     }
433 
434     /**
435      * Concatenate two OTSLine3D instances. This method is seperate for efficiency reasons.
436      * @param toleranceSI the tolerance between the end point of a line and the first point of the next line
437      * @param line1 OTSLine3D; first line
438      * @param line2 OTSLine3D; second line
439      * @return OTSLine3D
440      * @throws OTSGeometryException if zero lines are given, or when there is a gap between consecutive lines
441      */
442     public static OTSLine3D concatenate(final double toleranceSI, final OTSLine3D line1, final OTSLine3D line2)
443             throws OTSGeometryException
444     {
445         if (line1.getLast().distance(line2.getFirst()).si > toleranceSI)
446         {
447             throw new OTSGeometryException("Lines are not connected: " + line1.getLast() + " to " + line2.getFirst()
448                     + " distance is " + line1.getLast().distance(line2.getFirst()).si + " > " + toleranceSI);
449         }
450         int size = line1.size() + line2.size() - 1;
451         OTSPoint3D[] points = new OTSPoint3D[size];
452         int nextIndex = 0;
453         for (int j = 0; j < line1.size(); j++)
454         {
455             points[nextIndex++] = line1.get(j);
456         }
457         for (int j = 1; j < line2.size(); j++)
458         {
459             points[nextIndex++] = line2.get(j);
460         }
461         return new OTSLine3D(points);
462     }
463 
464     /**
465      * Concatenate several OTSLine3D instances.
466      * @param toleranceSI the tolerance between the end point of a line and the first point of the next line
467      * @param lines OTSLine3D... one or more OTSLine3D. The last point of the first <strong>must</strong> match the first of the
468      *            second, etc.
469      * @return OTSLine3D
470      * @throws OTSGeometryException if zero lines are given, or when there is a gap between consecutive lines
471      */
472     public static OTSLine3D concatenate(final double toleranceSI, final OTSLine3D... lines) throws OTSGeometryException
473     {
474         System.out.println("Concatenating " + lines.length + " lines.");
475         if (0 == lines.length)
476         {
477             throw new OTSGeometryException("Empty argument list");
478         }
479         else if (1 == lines.length)
480         {
481             return lines[0];
482         }
483         int size = lines[0].size();
484         for (int i = 1; i < lines.length; i++)
485         {
486             if (lines[i - 1].getLast().distance(lines[i].getFirst()).si > toleranceSI)
487             {
488                 throw new OTSGeometryException(
489                         "Lines are not connected: " + lines[i - 1].getLast() + " to " + lines[i].getFirst() + " distance is "
490                                 + lines[i - 1].getLast().distance(lines[i].getFirst()).si + " > " + toleranceSI);
491             }
492             size += lines[i].size() - 1;
493         }
494         OTSPoint3D[] points = new OTSPoint3D[size];
495         int nextIndex = 0;
496         for (int i = 0; i < lines.length; i++)
497         {
498             OTSLine3D line = lines[i];
499             for (int j = 0 == i ? 0 : 1; j < line.size(); j++)
500             {
501                 points[nextIndex++] = line.get(j);
502             }
503         }
504         return new OTSLine3D(points);
505     }
506 
507     /**
508      * Construct a new OTSLine3D with all points of this OTSLine3D in reverse order.
509      * @return OTSLine3D; the new OTSLine3D
510      */
511     public final OTSLine3D reverse()
512     {
513         OTSPoint3D[] resultPoints = new OTSPoint3D[size()];
514         int nextIndex = size();
515         for (OTSPoint3D p : getPoints())
516         {
517             resultPoints[--nextIndex] = p;
518         }
519         try
520         {
521             return new OTSLine3D(resultPoints);
522         }
523         catch (OTSGeometryException exception)
524         {
525             // Cannot happen
526             throw new RuntimeException(exception);
527         }
528     }
529 
530     /**
531      * Construct a new OTSLine3D covering the indicated fraction of this OTSLine3D.
532      * @param start double; starting point, valid range [0..<cite>end</cite>)
533      * @param end double; ending point, valid range (<cite>start</cite>..1]
534      * @return OTSLine3D; the new OTSLine3D
535      * @throws OTSGeometryException when start &gt;= end, or start &lt; 0, or end &gt; 1
536      */
537     public final OTSLine3D extractFractional(final double start, final double end) throws OTSGeometryException
538     {
539         if (start < 0 || start >= end || end > 1)
540         {
541             throw new OTSGeometryException("Bad interval");
542         }
543         getLength(); // computes and sets the length field
544         return extract(start * this.length, end * this.length);
545     }
546 
547     /**
548      * Create a new OTSLine3D that covers a sub-section of this OTSLine3D.
549      * @param start Length; the length along this OTSLine3D where the sub-section starts, valid range [0..<cite>end</cite>)
550      * @param end Length; length along this OTSLine3D where the sub-section ends, valid range
551      *            (<cite>start</cite>..<cite>length</cite> (length is the length of this OTSLine3D)
552      * @return OTSLine3D; the selected sub-section
553      * @throws OTSGeometryException when start &gt;= end, or start &lt; 0, or end &gt; length
554      */
555     public final OTSLine3D extract(final Length start, final Length end) throws OTSGeometryException
556     {
557         return extract(start.si, end.si);
558     }
559 
560     /**
561      * Create a new OTSLine3D that covers a sub-section of this OTSLine3D.
562      * @param start double; length along this OTSLine3D where the sub-section starts, valid range [0..<cite>end</cite>)
563      * @param end double; length along this OTSLine3D where the sub-section ends, valid range
564      *            (<cite>start</cite>..<cite>length</cite> (length is the length of this OTSLine3D)
565      * @return OTSLine3D; the selected sub-section
566      * @throws OTSGeometryException when start &gt;= end, or start &lt; 0, or end &gt; length
567      */
568     @SuppressFBWarnings("FE_FLOATING_POINT_EQUALITY")
569     public final OTSLine3D extract(final double start, final double end) throws OTSGeometryException
570     {
571         if (Double.isNaN(start) || Double.isNaN(end) || start < 0 || start >= end || end > getLengthSI())
572         {
573             throw new OTSGeometryException(
574                     "Bad interval (" + start + ".." + end + "; length of this OTSLine3D is " + this.getLengthSI() + ")");
575         }
576         double cumulativeLength = 0;
577         double nextCumulativeLength = 0;
578         double segmentLength = 0;
579         int index = 0;
580         List<OTSPoint3D> pointList = new ArrayList<>();
581         // System.err.println("interval " + start + ".." + end);
582         while (start > cumulativeLength)
583         {
584             OTSPoint3D fromPoint = this.points[index];
585             index++;
586             OTSPoint3D toPoint = this.points[index];
587             segmentLength = fromPoint.distanceSI(toPoint);
588             cumulativeLength = nextCumulativeLength;
589             nextCumulativeLength = cumulativeLength + segmentLength;
590             if (nextCumulativeLength >= start)
591             {
592                 break;
593             }
594         }
595         if (start == nextCumulativeLength)
596         {
597             pointList.add(this.points[index]);
598         }
599         else
600         {
601             pointList.add(OTSPoint3D.interpolate((start - cumulativeLength) / segmentLength, this.points[index - 1],
602                     this.points[index]));
603             if (end > nextCumulativeLength)
604             {
605                 pointList.add(this.points[index]);
606             }
607         }
608         while (end > nextCumulativeLength)
609         {
610             OTSPoint3D fromPoint = this.points[index];
611             index++;
612             if (index >= this.points.length)
613             {
614                 break; // rounding error
615             }
616             OTSPoint3D toPoint = this.points[index];
617             segmentLength = fromPoint.distanceSI(toPoint);
618             cumulativeLength = nextCumulativeLength;
619             nextCumulativeLength = cumulativeLength + segmentLength;
620             if (nextCumulativeLength >= end)
621             {
622                 break;
623             }
624             pointList.add(toPoint);
625         }
626         if (end == nextCumulativeLength)
627         {
628             pointList.add(this.points[index]);
629         }
630         else
631         {
632             pointList.add(OTSPoint3D.interpolate((end - cumulativeLength) / segmentLength, this.points[index - 1],
633                     this.points[index]));
634         }
635         try
636         {
637             return new OTSLine3D(pointList);
638         }
639         catch (OTSGeometryException exception)
640         {
641             System.err.println("interval " + start + ".." + end + "too short");
642             throw new OTSGeometryException("interval " + start + ".." + end + "too short");
643         }
644     }
645 
646     /**
647      * Build an array of OTSPoint3D from an array of Coordinate.
648      * @param coordinates Coordinate[]; the coordinates
649      * @return OTSPoint3D[]
650      */
651     private static OTSPoint3D[] coordinatesToOTSPoint3D(final Coordinate[] coordinates)
652     {
653         OTSPoint3D[] result = new OTSPoint3D[coordinates.length];
654         for (int i = 0; i < coordinates.length; i++)
655         {
656             result[i] = new OTSPoint3D(coordinates[i]);
657         }
658         return result;
659     }
660 
661     /**
662      * Create an OTSLine3D, while cleaning repeating successive points.
663      * @param points the coordinates of the line as OTSPoint3D
664      * @return the line
665      * @throws OTSGeometryException when number of points &lt; 2
666      */
667     public static OTSLine3D createAndCleanOTSLine3D(final OTSPoint3D... points) throws OTSGeometryException
668     {
669         if (points.length < 2)
670         {
671             throw new OTSGeometryException(
672                     "Degenerate OTSLine3D; has " + points.length + " point" + (points.length != 1 ? "s" : ""));
673         }
674         return createAndCleanOTSLine3D(new ArrayList<>(Arrays.asList(points)));
675     }
676 
677     /**
678      * Create an OTSLine3D, while cleaning repeating successive points.
679      * @param pointList List&lt;OTSPoint3D&gt;; list of the coordinates of the line as OTSPoint3D; any duplicate points in this
680      *            list are removed (this method may modify the provided list)
681      * @return OTSLine3D; the line
682      * @throws OTSGeometryException when number of non-equal points &lt; 2
683      */
684     public static OTSLine3D createAndCleanOTSLine3D(final List<OTSPoint3D> pointList) throws OTSGeometryException
685     {
686         // clean successive equal points
687         int i = 1;
688         while (i < pointList.size())
689         {
690             if (pointList.get(i - 1).equals(pointList.get(i)))
691             {
692                 pointList.remove(i);
693             }
694             else
695             {
696                 i++;
697             }
698         }
699         return new OTSLine3D(pointList);
700     }
701 
702     /**
703      * Construct a new OTSLine3D from an array of Coordinate.
704      * @param coordinates the array of coordinates to construct this OTSLine3D from
705      * @throws OTSGeometryException when the provided points do not constitute a valid line (too few points or identical
706      *             adjacent points)
707      */
708     public OTSLine3D(final Coordinate[] coordinates) throws OTSGeometryException
709     {
710         this(coordinatesToOTSPoint3D(coordinates));
711     }
712 
713     /**
714      * Construct a new OTSLine3D from a LineString.
715      * @param lineString the lineString to construct this OTSLine3D from.
716      * @throws OTSGeometryException when the provided LineString does not constitute a valid line (too few points or identical
717      *             adjacent points)
718      */
719     public OTSLine3D(final LineString lineString) throws OTSGeometryException
720     {
721         this(lineString.getCoordinates());
722     }
723 
724     /**
725      * Construct a new OTSLine3D from a Geometry.
726      * @param geometry the geometry to construct this OTSLine3D from
727      * @throws OTSGeometryException when the provided Geometry do not constitute a valid line (too few points or identical
728      *             adjacent points)
729      */
730     public OTSLine3D(final Geometry geometry) throws OTSGeometryException
731     {
732         this(geometry.getCoordinates());
733     }
734 
735     /**
736      * Construct a new OTSLine3D from a List&lt;OTSPoint3D&gt;.
737      * @param pointList the list of points to construct this OTSLine3D from.
738      * @throws OTSGeometryException when the provided points do not constitute a valid line (too few points or identical
739      *             adjacent points)
740      */
741     public OTSLine3D(final List<OTSPoint3D> pointList) throws OTSGeometryException
742     {
743         this(pointList.toArray(new OTSPoint3D[pointList.size()]));
744     }
745 
746     /**
747      * Construct a new OTSShape (closed shape) from a Path2D.
748      * @param path the Path2D to construct this OTSLine3D from.
749      * @throws OTSGeometryException when the provided points do not constitute a valid line (too few points or identical
750      *             adjacent points)
751      */
752     public OTSLine3D(final Path2D path) throws OTSGeometryException
753     {
754         List<OTSPoint3D> pl = new ArrayList<>();
755         for (PathIterator pi = path.getPathIterator(null); !pi.isDone(); pi.next())
756         {
757             double[] p = new double[6];
758             int segType = pi.currentSegment(p);
759             if (segType == PathIterator.SEG_MOVETO || segType == PathIterator.SEG_LINETO)
760             {
761                 pl.add(new OTSPoint3D(p[0], p[1]));
762             }
763             else if (segType == PathIterator.SEG_CLOSE)
764             {
765                 if (!pl.get(0).equals(pl.get(pl.size() - 1)))
766                 {
767                     pl.add(new OTSPoint3D(pl.get(0).x, pl.get(0).y));
768                 }
769                 break;
770             }
771         }
772         init(pl.toArray(new OTSPoint3D[pl.size() - 1]));
773     }
774 
775     /**
776      * Construct a Coordinate array and fill it with the points of this OTSLine3D.
777      * @return an array of Coordinates corresponding to this OTSLine
778      */
779     public final Coordinate[] getCoordinates()
780     {
781         Coordinate[] result = new Coordinate[size()];
782         for (int i = 0; i < size(); i++)
783         {
784             result[i] = this.points[i].getCoordinate();
785         }
786         return result;
787     }
788 
789     /**
790      * Construct a LineString from this OTSLine3D.
791      * @return a LineString corresponding to this OTSLine
792      */
793     public final LineString getLineString()
794     {
795         GeometryFactory factory = new GeometryFactory();
796         Coordinate[] coordinates = getCoordinates();
797         CoordinateSequence cs = factory.getCoordinateSequenceFactory().create(coordinates);
798         return new LineString(cs, factory);
799     }
800 
801     /**
802      * Return the number of points in this OTSLine3D.
803      * @return the number of points on the line
804      */
805     public final int size()
806     {
807         return this.points.length;
808     }
809 
810     /**
811      * Return the first point of this OTSLine3D.
812      * @return the first point on the line
813      */
814     public final OTSPoint3D getFirst()
815     {
816         return this.points[0];
817     }
818 
819     /**
820      * Return the last point of this OTSLine3D.
821      * @return the last point on the line
822      */
823     public final OTSPoint3D getLast()
824     {
825         return this.points[size() - 1];
826     }
827 
828     /**
829      * Return one point of this OTSLine3D.
830      * @param i int; the index of the point to retrieve
831      * @return OTSPoint3d; the i-th point of the line
832      * @throws OTSGeometryException when i &lt; 0 or i &gt; the number of points
833      */
834     public final OTSPoint3D get(final int i) throws OTSGeometryException
835     {
836         if (i < 0 || i > size() - 1)
837         {
838             throw new OTSGeometryException("OTSLine3D.get(i=" + i + "); i<0 or i>=size(), which is " + size());
839         }
840         return this.points[i];
841     }
842 
843     /**
844      * Return the length of this OTSLine3D as a double value in SI units. (Assumes that the coordinates of the points
845      * constituting this line are expressed in meters.)
846      * @return the length of the line in SI units
847      */
848     public final synchronized double getLengthSI()
849     {
850         if (Double.isNaN(this.length))
851         {
852             this.length = 0.0;
853             for (int i = 0; i < size() - 1; i++)
854             {
855                 this.length += this.points[i].distanceSI(this.points[i + 1]);
856             }
857         }
858         return this.length;
859     }
860 
861     /**
862      * Return the length of this OTSLine3D in meters. (Assuming that the coordinates of the points constituting this line are
863      * expressed in meters.)
864      * @return the length of the line
865      */
866     public final Length getLength()
867     {
868         return new Length(getLengthSI(), LengthUnit.SI);
869     }
870 
871     /**
872      * Return an array of OTSPoint3D that represents this OTSLine3D. <strong>Do not modify the result.</strong>
873      * @return the points of this line
874      */
875     public final OTSPoint3D[] getPoints()
876     {
877         return this.points;
878     }
879 
880     /**
881      * Make the length indexed line if it does not exist yet, and cache it.
882      */
883     private void makeLengthIndexedLine()
884     {
885         if (this.lengthIndexedLine == null)
886         {
887             this.lengthIndexedLine = new double[this.points.length];
888             this.lengthIndexedLine[0] = 0.0;
889             for (int i = 1; i < this.points.length; i++)
890             {
891                 this.lengthIndexedLine[i] = this.lengthIndexedLine[i - 1] + this.points[i - 1].distanceSI(this.points[i]);
892             }
893         }
894     }
895 
896     /**
897      * Get the location at a position on the line, with its direction. Position can be below 0 or more than the line length. In
898      * that case, the position will be extrapolated in the direction of the line at its start or end.
899      * @param position the position on the line for which to calculate the point on, before, of after the line
900      * @return a directed point
901      */
902     public final DirectedPoint getLocationExtended(final Length position)
903     {
904         return getLocationExtendedSI(position.getSI());
905     }
906 
907     /**
908      * Get the location at a position on the line, with its direction. Position can be below 0 or more than the line length. In
909      * that case, the position will be extrapolated in the direction of the line at its start or end.
910      * @param positionSI the position on the line for which to calculate the point on, before, of after the line, in SI units
911      * @return a directed point
912      */
913     public final DirectedPoint getLocationExtendedSI(final double positionSI)
914     {
915         makeLengthIndexedLine();
916         if (positionSI >= 0.0 && positionSI <= getLengthSI())
917         {
918             try
919             {
920                 return getLocationSI(positionSI);
921             }
922             catch (OTSGeometryException exception)
923             {
924                 // cannot happen
925             }
926         }
927 
928         // position before start point -- extrapolate
929         if (positionSI < 0.0)
930         {
931             double len = positionSI;
932             double fraction = len / (this.lengthIndexedLine[1] - this.lengthIndexedLine[0]);
933             OTSPoint3D p1 = this.points[0];
934             OTSPoint3D p2 = this.points[1];
935             return new DirectedPoint(p1.x + fraction * (p2.x - p1.x), p1.y + fraction * (p2.y - p1.y),
936                     p1.z + fraction * (p2.z - p1.z), 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
937         }
938 
939         // position beyond end point -- extrapolate
940         int n1 = this.lengthIndexedLine.length - 1;
941         int n2 = this.lengthIndexedLine.length - 2;
942         double len = positionSI - getLengthSI();
943         double fraction = len / (this.lengthIndexedLine[n1] - this.lengthIndexedLine[n2]);
944         OTSPoint3D p1 = this.points[n2];
945         OTSPoint3D p2 = this.points[n1];
946         return new DirectedPoint(p2.x + fraction * (p2.x - p1.x), p2.y + fraction * (p2.y - p1.y),
947                 p2.z + fraction * (p2.z - p1.z), 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
948     }
949 
950     /**
951      * Get the location at a fraction of the line, with its direction. Fraction should be between 0.0 and 1.0.
952      * @param fraction the fraction for which to calculate the point on the line
953      * @return a directed point
954      * @throws OTSGeometryException when fraction less than 0.0 or more than 1.0.
955      */
956     public final DirectedPoint getLocationFraction(final double fraction) throws OTSGeometryException
957     {
958         if (fraction < 0.0 || fraction > 1.0)
959         {
960             throw new OTSGeometryException("getLocationFraction for line: fraction < 0.0 or > 1.0. fraction = " + fraction);
961         }
962         return getLocationSI(fraction * getLengthSI());
963     }
964 
965     /**
966      * Get the location at a fraction of the line, with its direction. Fraction should be between 0.0 and 1.0.
967      * @param fraction the fraction for which to calculate the point on the line
968      * @param tolerance the delta from 0.0 and 1.0 that will be forgiven
969      * @return a directed point
970      * @throws OTSGeometryException when fraction less than 0.0 or more than 1.0.
971      */
972     public final DirectedPoint getLocationFraction(final double fraction, final double tolerance) throws OTSGeometryException
973     {
974         if (fraction < -tolerance || fraction > 1.0 + tolerance)
975         {
976             throw new OTSGeometryException(
977                     "getLocationFraction for line: fraction < 0.0 - tolerance or > 1.0 + tolerance; fraction = " + fraction);
978         }
979         double f = fraction < 0 ? 0.0 : fraction > 1.0 ? 1.0 : fraction;
980         return getLocationSI(f * getLengthSI());
981     }
982 
983     /**
984      * Get the location at a fraction of the line (or outside the line), with its direction.
985      * @param fraction the fraction for which to calculate the point on the line
986      * @return a directed point
987      */
988     public final DirectedPoint getLocationFractionExtended(final double fraction)
989     {
990         return getLocationExtendedSI(fraction * getLengthSI());
991     }
992 
993     /**
994      * Get the location at a position on the line, with its direction. Position should be between 0.0 and line length.
995      * @param position the position on the line for which to calculate the point on the line
996      * @return a directed point
997      * @throws OTSGeometryException when position less than 0.0 or more than line length.
998      */
999     public final DirectedPoint getLocation(final Length position) throws OTSGeometryException
1000     {
1001         return getLocationSI(position.getSI());
1002     }
1003 
1004     /**
1005      * Binary search for a position on the line.
1006      * @param pos the position to look for.
1007      * @return the index below the position; the position is between points[index] and points[index+1]
1008      * @throws OTSGeometryException when index could not be found
1009      */
1010     private int find(final double pos) throws OTSGeometryException
1011     {
1012         if (pos == 0)
1013         {
1014             return 0;
1015         }
1016 
1017         for (int i = 0; i < this.lengthIndexedLine.length - 2; i++)
1018         {
1019             if (pos > this.lengthIndexedLine[i] && pos <= this.lengthIndexedLine[i + 1])
1020             {
1021                 return i;
1022             }
1023         }
1024 
1025         return this.lengthIndexedLine.length - 2;
1026 
1027         /*- binary variant
1028         int lo = 0;
1029         int hi = this.lengthIndexedLine.length - 1;
1030         while (lo <= hi)
1031         {
1032             if (hi - lo <= 1)
1033             {
1034                 return lo;
1035             }
1036             int mid = lo + (hi - lo) / 2;
1037             if (pos < this.lengthIndexedLine[mid])
1038             {
1039                 hi = mid - 1;
1040             }
1041             else if (pos > this.lengthIndexedLine[mid])
1042             {
1043                 lo = mid + 1;
1044             }
1045         }
1046         throw new OTSGeometryException("Could not find position " + pos + " on line with length indexes: "
1047             + this.lengthIndexedLine);
1048          */
1049     }
1050 
1051     /**
1052      * Get the location at a position on the line, with its direction. Position should be between 0.0 and line length.
1053      * @param positionSI the position on the line for which to calculate the point on the line
1054      * @return a directed point
1055      * @throws OTSGeometryException when position less than 0.0 or more than line length.
1056      */
1057     public final DirectedPoint getLocationSI(final double positionSI) throws OTSGeometryException
1058     {
1059         makeLengthIndexedLine();
1060         if (positionSI < 0.0 || positionSI > getLengthSI())
1061         {
1062             throw new OTSGeometryException("getLocationSI for line: position < 0.0 or > line length. Position = " + positionSI
1063                     + " m. Length = " + getLengthSI() + " m.");
1064         }
1065 
1066         // handle special cases: position == 0.0, or position == length
1067         if (positionSI == 0.0)
1068         {
1069             OTSPoint3D p1 = this.points[0];
1070             OTSPoint3D p2 = this.points[1];
1071             return new DirectedPoint(p1.x, p1.y, p1.z, 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
1072         }
1073         if (positionSI == getLengthSI())
1074         {
1075             OTSPoint3D p1 = this.points[this.points.length - 2];
1076             OTSPoint3D p2 = this.points[this.points.length - 1];
1077             return new DirectedPoint(p2.x, p2.y, p2.z, 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
1078         }
1079 
1080         // find the index of the line segment, use binary search
1081         int index = find(positionSI);
1082         double remainder = positionSI - this.lengthIndexedLine[index];
1083         double fraction = remainder / (this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index]);
1084         OTSPoint3D p1 = this.points[index];
1085         OTSPoint3D p2 = this.points[index + 1];
1086         return new DirectedPoint(p1.x + fraction * (p2.x - p1.x), p1.y + fraction * (p2.y - p1.y),
1087                 p1.z + fraction * (p2.z - p1.z), 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
1088     }
1089 
1090     /**
1091      * Truncate a line at the given length (less than the length of the line, and larger than zero) and return a new line.
1092      * @param lengthSI the location where to truncate the line
1093      * @return a new OTSLine3D truncated at the exact position where line.getLength() == lengthSI
1094      * @throws OTSGeometryException when position less than 0.0 or more than line length.
1095      */
1096     public final OTSLine3D truncate(final double lengthSI) throws OTSGeometryException
1097     {
1098         makeLengthIndexedLine();
1099         if (lengthSI <= 0.0 || lengthSI > getLengthSI())
1100         {
1101             throw new OTSGeometryException("truncate for line: position <= 0.0 or > line length. Position = " + lengthSI
1102                     + " m. Length = " + getLengthSI() + " m.");
1103         }
1104 
1105         // handle special case: position == length
1106         if (lengthSI == getLengthSI())
1107         {
1108             return new OTSLine3D(getPoints());
1109         }
1110 
1111         // find the index of the line segment
1112         int index = find(lengthSI);
1113         double remainder = lengthSI - this.lengthIndexedLine[index];
1114         double fraction = remainder / (this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index]);
1115         OTSPoint3D p1 = this.points[index];
1116         OTSPoint3D p2 = this.points[index + 1];
1117         OTSPoint3D newLastPoint = new OTSPoint3D(p1.x + fraction * (p2.x - p1.x), p1.y + fraction * (p2.y - p1.y),
1118                 p1.z + fraction * (p2.z - p1.z));
1119         OTSPoint3D[] coords = new OTSPoint3D[index + 2];
1120         for (int i = 0; i <= index; i++)
1121         {
1122             coords[i] = this.points[i];
1123         }
1124         coords[index + 1] = newLastPoint;
1125         return new OTSLine3D(coords);
1126     }
1127 
1128     /*-
1129      * TODO finish this method if it is needed; remove otherwise.
1130      * Calculate the first point on this line that intersects somewhere with the provided line, or NaN if no intersection was
1131      * found.
1132      * @param line the line to test the intersection with
1133      * @return the fraction of the first intersection point
1134      *
1135     public final double firstIntersectionFraction(final OTSLine3D line)
1136     {
1137         List<Line2D.Double> segs = new ArrayList<>();
1138         for (int j = 1; j < line.getPoints().length; j++)
1139         {
1140             Line2D.Double seg =
1141                 new Line2D.Double(this.points[j - 1].x, this.points[j - 1].y, this.points[j].x, this.points[j].y);
1142             segs.add(seg);
1143     
1144         }
1145         for (int i = 1; i < this.points.length; i++)
1146         {
1147             Line2D.Double thisSeg =
1148                 new Line2D.Double(this.points[i - 1].x, this.points[i - 1].y, this.points[i].x, this.points[i].y);
1149             for (Line2D.Double seg : segs)
1150             {
1151                 if (thisSeg.intersectsLine(seg))
1152                 {
1153                     // Point2D.Double intersectionPoint = thisSeg.
1154                     
1155                 }
1156             }
1157         }
1158         return Double.NaN;
1159     }
1160      */
1161 
1162     /**
1163      * Returns the fractional position along this line of the orthogonal projection of point (x, y) on this line. If the point
1164      * is not orthogonal to the closest line segment, the nearest point is selected.
1165      * @param x x-coordinate of point to project
1166      * @param y y-coordinate of point to project
1167      * @return fractional position along this line of the orthogonal projection on this line of a point
1168      */
1169     public final double projectOrthogonal(final double x, final double y)
1170     {
1171 
1172         // prepare
1173         makeLengthIndexedLine();
1174         double minDistance = Double.POSITIVE_INFINITY;
1175         double minSegmentFraction = 0;
1176         int minSegment = -1;
1177 
1178         // code based on Line2D.ptSegDistSq(...)
1179         for (int i = 0; i < size() - 1; i++)
1180         {
1181             double dx = this.points[i + 1].x - this.points[i].x;
1182             double dy = this.points[i + 1].y - this.points[i].y;
1183             // vector relative to (x(i), y(i))
1184             double px = x - this.points[i].x;
1185             double py = y - this.points[i].y;
1186             // dot product
1187             double dot1 = px * dx + py * dy;
1188             double f;
1189             double distance;
1190             if (dot1 > 0)
1191             {
1192                 // vector relative to (x(i+1), y(i+1))
1193                 px = dx - px;
1194                 py = dy - py;
1195                 // dot product
1196                 double dot2 = px * dx + py * dy;
1197                 if (dot2 > 0)
1198                 {
1199                     // projection on line segment
1200                     double len2 = dx * dx + dy * dy;
1201                     double proj = dot2 * dot2 / len2;
1202                     f = dot1 / len2;
1203                     distance = px * px + py * py - proj;
1204                 }
1205                 else
1206                 {
1207                     // dot<=0 projection 'after' line segment
1208                     f = 1;
1209                     distance = px * px + py * py;
1210                 }
1211             }
1212             else
1213             {
1214                 // dot<=0 projection 'before' line segment
1215                 f = 0;
1216                 distance = px * px + py * py;
1217             }
1218             // check if closer than previous
1219             if (distance < minDistance)
1220             {
1221                 minDistance = distance;
1222                 minSegmentFraction = f;
1223                 minSegment = i;
1224             }
1225         }
1226 
1227         // return
1228         double segLen = this.lengthIndexedLine[minSegment + 1] - this.lengthIndexedLine[minSegment];
1229         return (this.lengthIndexedLine[minSegment] + segLen * minSegmentFraction) / getLengthSI();
1230 
1231     }
1232 
1233     /**
1234      * Returns the fractional projection of a point to a line. The projection works by taking slices in space per line segment
1235      * as shown below. A point is always projected to the nearest segment, but not necessarily to the closest point on that
1236      * segment. The slices in space are analogous to a Voronoi diagram, but for the line segments instead of points. If
1237      * fractional projection fails, the orthogonal projection is returned.<br>
1238      * <br>
1239      * 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
1240      * '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')
1241      * in half, while the line 'E-G' cuts the second bend of the 3rd segment (at point 'I') in half.
1242      * 
1243      * <pre>
1244      *            ____________________________     G                   .
1245      * .         |                            |    .                 .
1246      *   .       |  . . . .  helper lines     |    .               .
1247      *     .     |  _.._.._  projection line  |   I.             .
1248      *       .   |____________________________|  _.'._         .       L
1249      *        F.                              _.'  .  '-.    .
1250      *          ..                       B _.'     .     '-.
1251      *           . .                    _.\        .     .  D
1252      *            .  .               _.'   :       .   .
1253      *     J       .   .          _.'      \       . .
1254      *             ..    .     _.'          :      .                M
1255      *            .  .     ..-'             \      .
1256      *           .    .    /H.               A     .
1257      *          .      .  /    .                   .
1258      *        C _________/       .                 .
1259      *        .          .         .               .
1260      *   K   .            .          .             .
1261      *      .              .           .           .
1262      *     .                .            .         .           N
1263      *    .                  .             .       .
1264      *   .                    .              .     .
1265      *  .                      .               .   .
1266      * .                        .                . .
1267      *                           .                 .E
1268      *                            .                  .
1269      *                             .                   .
1270      *                              .                    .
1271      * </pre>
1272      * 
1273      * Fractional projection may fail in three cases.
1274      * <ol>
1275      * <li>Numerical difficulties at slight bend, orthogonal projection returns the correct point.</li>
1276      * <li>Fractional projection is possible only to segments that aren't the nearest segment(s).</li>
1277      * <li>Fractional projection is possible for no segment.</li>
1278      * </ol>
1279      * In the latter two cases the projection is undefined and a orthogonal projection is returned.
1280      * @param start direction in first point
1281      * @param end direction in last point
1282      * @param x x-coordinate of point to project
1283      * @param y y-coordinate of point to project
1284      * @return fractional position along this line of the fractional projection on that line of a point
1285      */
1286     public final double projectFractional(final Direction start, final Direction end, final double x, final double y)
1287     {
1288 
1289         // prepare
1290         makeLengthIndexedLine();
1291         double minDistance = Double.POSITIVE_INFINITY;
1292         double minSegmentFraction = 0;
1293         int minSegment = -1;
1294         OTSPoint3D point = new OTSPoint3D(x, y);
1295 
1296         // determine helpers (centers and directions)
1297         determineFractionalHelpers(start, end);
1298 
1299         // get distance of point to each segment
1300         double[] d = new double[this.points.length - 1];
1301         double minD = Double.POSITIVE_INFINITY;
1302         for (int i = 0; i < this.points.length - 1; i++)
1303         {
1304             d[i] = Line2D.ptSegDist(this.points[i].x, this.points[i].y, this.points[i + 1].x, this.points[i + 1].y, x, y);
1305             minD = d[i] < minD ? d[i] : minD;
1306         }
1307 
1308         // loop over segments for projection
1309         double distance;
1310         for (int i = 0; i < this.points.length - 1; i++)
1311         {
1312             // skip if not the closest segment, note that often two segments are equally close in their shared end point
1313             if (d[i] > minD + FRAC_PROJ_PRECISION)
1314             {
1315                 continue;
1316             }
1317             OTSPoint3D center = this.fractionalHelperCenters[i];
1318             OTSPoint3D p;
1319             if (center != null)
1320             {
1321                 // get intersection of line "center - (x, y)" and the segment
1322                 p = OTSPoint3D.intersectionOfLines(center, point, this.points[i], this.points[i + 1]);
1323                 if (p == null || (x < center.x + FRAC_PROJ_PRECISION && center.x + FRAC_PROJ_PRECISION < p.x)
1324                         || (x > center.x - FRAC_PROJ_PRECISION && center.x - FRAC_PROJ_PRECISION > p.x)
1325                         || (y < center.y + FRAC_PROJ_PRECISION && center.y + FRAC_PROJ_PRECISION < p.y)
1326                         || (y > center.y - FRAC_PROJ_PRECISION && center.y - FRAC_PROJ_PRECISION > p.y))
1327                 {
1328                     // projected point may not be 'beyond' segment center (i.e. center may not be between (x, y) and (p.x, p.y)
1329                     continue;
1330                 }
1331             }
1332             else
1333             {
1334                 // parallel helper lines, project along direction
1335                 OTSPoint3D offsetPoint =
1336                         new OTSPoint3D(x + this.fractionalHelperDirections[i].x, y + this.fractionalHelperDirections[i].y);
1337                 p = OTSPoint3D.intersectionOfLines(point, offsetPoint, this.points[i], this.points[i + 1]);
1338             }
1339             if (p == null || p.x < Math.min(this.points[i].x, this.points[i + 1].x) - FRAC_PROJ_PRECISION
1340                     || p.x > Math.max(this.points[i].x, this.points[i + 1].x) + FRAC_PROJ_PRECISION
1341                     || p.y < Math.min(this.points[i].y, this.points[i + 1].y) - FRAC_PROJ_PRECISION
1342                     || p.y > Math.max(this.points[i].y, this.points[i + 1].y) + FRAC_PROJ_PRECISION)
1343             {
1344                 // intersection must be on the segment
1345                 // in case of p == null, the length of the fractional helper direction falls away due to precision
1346                 continue;
1347             }
1348             // distance from (x, y) to intersection on segment
1349             double dx = x - p.x;
1350             double dy = y - p.y;
1351             distance = Math.sqrt(dx * dx + dy * dy);
1352             // distance from start of segment to point on segment
1353             if (distance < minDistance)
1354             {
1355                 dx = p.x - this.points[i].x;
1356                 dy = p.y - this.points[i].y;
1357                 double dFrac = Math.sqrt(dx * dx + dy * dy);
1358                 // fraction to point on segment
1359                 minDistance = distance;
1360                 minSegmentFraction = dFrac / (this.lengthIndexedLine[i + 1] - this.lengthIndexedLine[i]);
1361                 minSegment = i;
1362             }
1363         }
1364 
1365         // return
1366         if (minSegment == -1)
1367         {
1368             /*
1369              * If fractional projection fails (x, y) is either outside of the applicable area for fractional projection, or is
1370              * inside an area where numerical difficulties arise (i.e. far away outside of very slight bend which is considered
1371              * parallel).
1372              */
1373             return projectOrthogonal(x, y);
1374         }
1375         double segLen = this.lengthIndexedLine[minSegment + 1] - this.lengthIndexedLine[minSegment];
1376         return (this.lengthIndexedLine[minSegment] + segLen * minSegmentFraction) / getLengthSI();
1377 
1378     }
1379 
1380     /**
1381      * Determines all helpers (points and/or directions) for fractional projection and stores fixed information in properties
1382      * while returning the first and last center points (.
1383      * @param start direction in first point
1384      * @param end direction in last point
1385      */
1386     private void determineFractionalHelpers(final Direction start, final Direction end)
1387     {
1388 
1389         final int n = this.points.length - 1;
1390 
1391         // calculate fixed helpers if not done yet
1392         if (this.fractionalHelperCenters == null)
1393         {
1394             this.fractionalHelperCenters = new OTSPoint3D[n];
1395             this.fractionalHelperDirections = new Point2D.Double[n];
1396             if (this.points.length > 2)
1397             {
1398                 // intersection of parallel lines of first and second segment
1399                 OTSLine3D prevOfsSeg = unitOffsetSegment(0);
1400                 OTSLine3D nextOfsSeg = unitOffsetSegment(1);
1401                 OTSPoint3D parStartPoint;
1402                 try
1403                 {
1404                     parStartPoint = OTSPoint3D.intersectionOfLines(prevOfsSeg.get(0), prevOfsSeg.get(1), nextOfsSeg.get(0),
1405                             nextOfsSeg.get(1));
1406                     if (parStartPoint == null)
1407                     {
1408                         parStartPoint = new OTSPoint3D((prevOfsSeg.get(1).x + nextOfsSeg.get(0).x) / 2,
1409                                 (prevOfsSeg.get(1).y + nextOfsSeg.get(0).y) / 2);
1410                     }
1411                 }
1412                 catch (OTSGeometryException oge)
1413                 {
1414                     // cannot happen as only the first and second point (which are always present) are requested
1415                     throw new RuntimeException(oge);
1416                 }
1417                 // remember the intersection of the first two unit offset segments
1418                 this.firstOffsetIntersection = parStartPoint;
1419                 // loop segments
1420                 for (int i = 1; i < this.points.length - 2; i++)
1421                 {
1422                     prevOfsSeg = nextOfsSeg;
1423                     nextOfsSeg = unitOffsetSegment(i + 1);
1424                     OTSPoint3D parEndPoint;
1425                     try
1426                     {
1427                         parEndPoint = OTSPoint3D.intersectionOfLines(prevOfsSeg.get(0), prevOfsSeg.get(1), nextOfsSeg.get(0),
1428                                 nextOfsSeg.get(1));
1429                         if (parEndPoint == null)
1430                         {
1431                             parEndPoint = new OTSPoint3D((prevOfsSeg.get(1).x + nextOfsSeg.get(0).x) / 2,
1432                                     (prevOfsSeg.get(1).y + nextOfsSeg.get(0).y) / 2);
1433                         }
1434                     }
1435                     catch (OTSGeometryException oge)
1436                     {
1437                         // cannot happen as only the first and second point (which are always present) are requested
1438                         throw new RuntimeException(oge);
1439                     }
1440                     // center = intersections of helper lines
1441                     this.fractionalHelperCenters[i] =
1442                             OTSPoint3D.intersectionOfLines(this.points[i], parStartPoint, this.points[i + 1], parEndPoint);
1443                     if (this.fractionalHelperCenters[i] == null)
1444                     {
1445                         // parallel helper lines, parallel segments or /\/ cause parallel helper lines, use direction
1446                         this.fractionalHelperDirections[i] =
1447                                 new Point2D.Double(parStartPoint.x - this.points[i].x, parStartPoint.y - this.points[i].y);
1448                     }
1449                     parStartPoint = parEndPoint;
1450                 }
1451                 // remember the intersection of the last two unit offset segments
1452                 this.lastOffsetIntersection = parStartPoint;
1453             }
1454         }
1455 
1456         // use directions at start and end to get unit offset points to the left at a distance of 1
1457         double ang = start.si + Math.PI / 2;
1458         OTSPoint3D p1 = new OTSPoint3D(this.points[0].x + Math.cos(ang), this.points[0].y + Math.sin(ang));
1459         ang = end.si + Math.PI / 2;
1460         OTSPoint3D p2 = new OTSPoint3D(this.points[n].x + Math.cos(ang), this.points[n].y + Math.sin(ang));
1461 
1462         // calculate first and last center (i.e. intersection of unit offset segments), which depend on inputs 'start' and 'end'
1463         if (this.points.length > 2)
1464         {
1465             this.fractionalHelperCenters[0] =
1466                     OTSPoint3D.intersectionOfLines(this.points[0], p1, this.points[1], this.firstOffsetIntersection);
1467             this.fractionalHelperCenters[n - 1] =
1468                     OTSPoint3D.intersectionOfLines(this.points[n - 1], this.lastOffsetIntersection, this.points[n], p2);
1469             if (this.fractionalHelperCenters[n - 1] == null)
1470             {
1471                 // parallel helper lines, use direction for projection
1472                 this.fractionalHelperDirections[n - 1] = new Point2D.Double(p2.x - this.points[n].x, p2.y - this.points[n].y);
1473             }
1474         }
1475         else
1476         {
1477             // only a single segment
1478             this.fractionalHelperCenters[0] = OTSPoint3D.intersectionOfLines(this.points[0], p1, this.points[1], p2);
1479             this.fractionalHelperCenters[n - 1] = null;
1480         }
1481         if (this.fractionalHelperCenters[0] == null)
1482         {
1483             // parallel helper lines, use direction for projection
1484             this.fractionalHelperDirections[0] = new Point2D.Double(p1.x - this.points[0].x, p1.y - this.points[0].y);
1485         }
1486 
1487     }
1488 
1489     /**
1490      * Helper method for fractional projection which returns an offset line to the left of a segment at a distance of 1.
1491      * @param segment segment number
1492      * @return parallel line to the left of a segment at a distance of 1
1493      */
1494     private OTSLine3D unitOffsetSegment(final int segment)
1495     {
1496         OTSPoint3D from = new OTSPoint3D(this.points[segment].x, this.points[segment].y);
1497         OTSPoint3D to = new OTSPoint3D(this.points[segment + 1].x, this.points[segment + 1].y);
1498         try
1499         {
1500             OTSLine3D line = new OTSLine3D(from, to);
1501             return line.offsetLine(1.0);
1502         }
1503         catch (OTSGeometryException oge)
1504         {
1505             // cannot happen as points are from this OTSLine3D which performed the same checks and 2 points are given
1506             throw new RuntimeException(oge);
1507         }
1508     }
1509 
1510     /**
1511      * Calculate the centroid of this line, and the bounds, and cache for later use. Make sure the dx, dy and dz are at least
1512      * 0.5 m wide.
1513      */
1514     private void calcCentroidBounds()
1515     {
1516         double minX = Double.POSITIVE_INFINITY;
1517         double minY = Double.POSITIVE_INFINITY;
1518         double minZ = Double.POSITIVE_INFINITY;
1519         double maxX = Double.NEGATIVE_INFINITY;
1520         double maxY = Double.NEGATIVE_INFINITY;
1521         double maxZ = Double.NEGATIVE_INFINITY;
1522         for (OTSPoint3D p : this.points)
1523         {
1524             minX = Math.min(minX, p.x);
1525             minY = Math.min(minY, p.y);
1526             minZ = Math.min(minZ, p.z);
1527             maxX = Math.max(maxX, p.x);
1528             maxY = Math.max(maxY, p.y);
1529             maxZ = Math.max(maxZ, p.z);
1530         }
1531         this.centroid = new OTSPoint3D((maxX + minX) / 2, (maxY + minY) / 2, (maxZ + minZ) / 2);
1532         double deltaX = Math.max(maxX - minX, 0.5);
1533         double deltaY = Math.max(maxY - minY, 0.5);
1534         double deltaZ = Math.max(maxZ - minZ, 0.5);
1535         this.bounds = new BoundingBox(deltaX, deltaY, deltaZ);
1536         this.envelope = new Envelope(minX, maxX, minY, maxY);
1537     }
1538 
1539     /**
1540      * Retrieve the centroid of this OTSLine3D.
1541      * @return OTSPoint3D; the centroid of this OTSLine3D
1542      */
1543     public final OTSPoint3D getCentroid()
1544     {
1545         if (this.centroid == null)
1546         {
1547             calcCentroidBounds();
1548         }
1549         return this.centroid;
1550     }
1551 
1552     /**
1553      * Get the bounding rectangle of this OTSLine3D.
1554      * @return Rectangle2D; the bounding rectangle of this OTSLine3D
1555      */
1556     public final Envelope getEnvelope()
1557     {
1558         if (this.envelope == null)
1559         {
1560             calcCentroidBounds();
1561         }
1562         return this.envelope;
1563     }
1564 
1565     /** {@inheritDoc} */
1566     @Override
1567     @SuppressWarnings("checkstyle:designforextension")
1568     public DirectedPoint getLocation()
1569     {
1570         if (this.centroid == null)
1571         {
1572             calcCentroidBounds();
1573         }
1574         return this.centroid.getDirectedPoint();
1575     }
1576 
1577     /** {@inheritDoc} */
1578     @Override
1579     @SuppressWarnings("checkstyle:designforextension")
1580     public Bounds getBounds()
1581     {
1582         if (this.bounds == null)
1583         {
1584             calcCentroidBounds();
1585         }
1586         return this.bounds;
1587     }
1588 
1589     /** {@inheritDoc} */
1590     @Override
1591     @SuppressWarnings("checkstyle:designforextension")
1592     public String toString()
1593     {
1594         return Arrays.toString(this.points);
1595     }
1596 
1597     /** {@inheritDoc} */
1598     @Override
1599     @SuppressWarnings("checkstyle:designforextension")
1600     public int hashCode()
1601     {
1602         final int prime = 31;
1603         int result = 1;
1604         result = prime * result + Arrays.hashCode(this.points);
1605         return result;
1606     }
1607 
1608     /** {@inheritDoc} */
1609     @Override
1610     @SuppressWarnings({ "checkstyle:designforextension", "checkstyle:needbraces" })
1611     public boolean equals(final Object obj)
1612     {
1613         if (this == obj)
1614             return true;
1615         if (obj == null)
1616             return false;
1617         if (getClass() != obj.getClass())
1618             return false;
1619         OTSLine3D other = (OTSLine3D) obj;
1620         if (!Arrays.equals(this.points, other.points))
1621             return false;
1622         return true;
1623     }
1624 
1625     /**
1626      * @return excel XY plottable output
1627      */
1628     public final String toExcel()
1629     {
1630         StringBuffer s = new StringBuffer();
1631         for (OTSPoint3D p : this.points)
1632         {
1633             s.append(p.x + "\t" + p.y + "\n");
1634         }
1635         return s.toString();
1636     }
1637 
1638     /**
1639      * @param args String[]; the command line arguments (not used)
1640      * @throws OTSGeometryException in case of error
1641      */
1642     public static void main(final String[] args) throws OTSGeometryException
1643     {
1644         OTSLine3D line = new OTSLine3D(new OTSPoint3D(-263.811, -86.551, 1.180), new OTSPoint3D(-262.945, -84.450, 1.180),
1645                 new OTSPoint3D(-261.966, -82.074, 1.180), new OTSPoint3D(-260.890, -79.464, 1.198),
1646                 new OTSPoint3D(-259.909, -76.955, 1.198), new OTSPoint3D(-258.911, -74.400, 1.198),
1647                 new OTSPoint3D(-257.830, -71.633, 1.234));
1648         System.out.println(line.toExcel());
1649         double[] relativeFractions = new double[] { 0.0, 0.19827228089475762, 0.30549496392494213, 0.5824753163948581,
1650                 0.6815307752261827, 0.7903990449840241, 0.8942375145295614, 1.0 };
1651         double[] offsets = new double[] { 2.9779999256134, 4.6029999256134, 3.886839156071996, 2.3664845198627207,
1652                 1.7858981925396709, 1.472348149010167, 2.0416709053157285, 2.798692100483229 };
1653         System.out.println(line.offsetLine(relativeFractions, offsets).toExcel());
1654     }
1655 }