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