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