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