View Javadoc
1   package org.opentrafficsim.core.geometry;
2   
3   import java.awt.geom.Path2D;
4   import java.awt.geom.PathIterator;
5   import java.awt.geom.Rectangle2D;
6   import java.io.Serializable;
7   import java.util.ArrayList;
8   import java.util.Arrays;
9   import java.util.List;
10  
11  import javax.media.j3d.Bounds;
12  
13  import nl.tudelft.simulation.dsol.animation.LocatableInterface;
14  import nl.tudelft.simulation.language.d3.BoundingBox;
15  import nl.tudelft.simulation.language.d3.DirectedPoint;
16  
17  import org.djunits.unit.LengthUnit;
18  import org.djunits.value.vdouble.scalar.Length;
19  
20  import com.vividsolutions.jts.geom.Coordinate;
21  import com.vividsolutions.jts.geom.CoordinateSequence;
22  import com.vividsolutions.jts.geom.Geometry;
23  import com.vividsolutions.jts.geom.GeometryFactory;
24  import com.vividsolutions.jts.geom.LineString;
25  import com.vividsolutions.jts.linearref.LengthIndexedLine;
26  
27  /**
28   * Line with OTSPoint3D points, a cached length indexed line, a cahced length, and a cached centroid (all calculated on first
29   * use).
30   * <p>
31   * Copyright (c) 2013-2015 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
32   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
33   * <p>
34   * $LastChangedDate: 2015-07-16 10:20:53 +0200 (Thu, 16 Jul 2015) $, @version $Revision: 1124 $, by $Author: pknoppers $,
35   * initial version Jul 22, 2015 <br>
36   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
37   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
38   * @author <a href="http://www.citg.tudelft.nl">Guus Tamminga</a>
39   */
40  public class OTSLine3D implements LocatableInterface, Serializable
41  {
42      /** */
43      private static final long serialVersionUID = 20150722L;
44  
45      /** The points of the line. */
46      private OTSPoint3D[] points;
47  
48      /** The cumulative length of the line at point 'i'. */
49      private double[] lengthIndexedLine = null;
50  
51      /** The cached length; will be calculated when needed for the first time. */
52      private double length = Double.NaN;
53  
54      /** The cached centroid; will be calculated when needed for the first time. */
55      private OTSPoint3D centroid = null;
56  
57      /** The cached bounds; will be calculated when needed for the first time. */
58      private Bounds bounds = null;
59  
60      private java.awt.geom.Rectangle2D.Double boundingRect;
61  
62      /**
63       * Construct a new OTSLine3D.
64       * @param points the array of points to construct this OTSLine3D from.
65       * @throws OTSGeometryException when the provided points do not constitute a valid line (too few points or identical
66       *             adjacent points)
67       */
68      public OTSLine3D(final OTSPoint3D... points) throws OTSGeometryException
69      {
70          init(points);
71      }
72  
73      /**
74       * Construct a new OTSLine3D.
75       * @param pts the array of points to construct this OTSLine3D from.
76       * @throws OTSGeometryException when the provided points do not constitute a valid line (too few points or identical
77       *             adjacent points)
78       */
79      private void init(final OTSPoint3D... pts) throws OTSGeometryException
80      {
81          if (pts.length < 2)
82          {
83              throw new OTSGeometryException("Degenerate OTSLine3D; has " + pts.length + " point"
84                  + (pts.length != 1 ? "s" : ""));
85          }
86          for (int i = 1; i < pts.length; i++)
87          {
88              if (pts[i - 1].x == pts[i].x && pts[i - 1].y == pts[i].y && pts[i - 1].z == pts[i].z)
89              {
90                  throw new OTSGeometryException("Degenerate OTSLine3D; point " + (i - 1)
91                      + " has the same x, y and z as point " + i);
92              }
93          }
94          this.points = pts;
95      }
96  
97      /** Which offsetLine method to use... */
98      public enum OffsetMethod
99      {
100         /** Via JTS buffer. */
101         JTS,
102 
103         /** Peter Knoppers. */
104         PK,
105 
106         /** Alexander Verbraeck. */
107         AV;
108     };
109 
110     /** Which offset line method to use... */
111     public static final OffsetMethod OFFSETMETHOD = OffsetMethod.PK;
112 
113     /**
114      * Construct parallel line.
115      * @param offset double; offset distance from the reference line; positive is LEFT, negative is RIGHT
116      * @return OTSLine3D; the line that has the specified offset from the reference line
117      */
118     public final OTSLine3D offsetLine(final double offset)
119     {
120         try
121         {
122             switch (OFFSETMETHOD)
123             {
124                 case PK:
125                     return OTSOffsetLinePK.offsetLine(this, offset);
126 
127                 case AV:
128                     return OTSBufferingAV.offsetLine(this, offset);
129 
130                 case JTS:
131                     return OTSBufferingJTS.offsetGeometryOLD(this, offset);
132 
133                 default:
134                     return null;
135             }
136         }
137         catch (OTSGeometryException exception)
138         {
139             exception.printStackTrace();
140             return null;
141         }
142     }
143 
144     /**
145      * Construct a line that is equal to this line except for segments that are shorter than the <cite>noiseLevel</cite>. The
146      * result is guaranteed to start with the first point of this line and end with the last point of this line.
147      * @param noiseLevel double; the minimum segment length that is <b>not</b> removed
148      * @return OTSLine3D; the filtered line
149      */
150     public final OTSLine3D noiseFilteredLine(final double noiseLevel)
151     {
152         if (this.size() <= 2)
153         {
154             return this; // Except for some cached fields; an OTSLine3D is immutable; so safe to return
155         }
156         OTSPoint3D prevPoint = null;
157         List<OTSPoint3D> list = null;
158         for (int index = 0; index < this.size(); index++)
159         {
160             OTSPoint3D currentPoint = this.points[index];
161             if (null != prevPoint && prevPoint.distanceSI(currentPoint) < noiseLevel)
162             {
163                 if (null == list)
164                 {
165                     // Found something to filter; copy this up to (and including) prevPoint
166                     list = new ArrayList<OTSPoint3D>();
167                     for (int i = 0; i < index; i++)
168                     {
169                         list.add(this.points[i]);
170                     }
171                 }
172                 if (index == this.size() - 1)
173                 {
174                     if (list.size() > 1)
175                     {
176                         // Replace the last point of the result by the last point of this OTSLine3D
177                         list.set(list.size() - 1, currentPoint);
178                     }
179                     else
180                     {
181                         // Append the last point of this even though it is close to the first point than the noise value to
182                         // comply with the requirement that first and last point of this are ALWAYS included in the result.
183                         list.add(currentPoint);
184                     }
185                 }
186                 continue; // Do not replace prevPoint by currentPoint
187             }
188             else if (null != list)
189             {
190                 list.add(currentPoint);
191             }
192             prevPoint = currentPoint;
193         }
194         if (null == list)
195         {
196             return this;
197         }
198         try
199         {
200             return new OTSLine3D(list);
201         }
202         catch (OTSGeometryException exception)
203         {
204             System.err.println("CANNOT HAPPEN");
205             exception.printStackTrace();
206             throw new Error(exception);
207         }
208     }
209 
210     /**
211      * Create a line at linearly varying offset from this line. The offset may change linearly from its initial value at the
212      * start of the reference line to its final offset value at the end of the reference line.
213      * @param offsetAtStart double; offset at the start of the reference line (positive value is Left, negative value is Right)
214      * @param offsetAtEnd double; offset at the end of the reference line (positive value is Left, negative value is Right)
215      * @return Geometry; the Geometry of the line at linearly changing offset of the reference line
216      * @throws OTSGeometryException when this method fails to create the offset line
217      */
218     public final OTSLine3D offsetLine(final double offsetAtStart, final double offsetAtEnd) throws OTSGeometryException
219     {
220         // System.out.println(OTSGeometry.printCoordinates("#referenceLine: \nc1,0,0\n# offset at start is " + offsetAtStart
221         // + " at end is " + offsetAtEnd + "\n#", referenceLine, "\n   "));
222 
223         OTSLine3D offsetLineAtStart = offsetLine(offsetAtStart);
224         if (offsetAtStart == offsetAtEnd)
225         {
226             return offsetLineAtStart; // offset does not change
227         }
228         // System.out.println(OTSGeometry.printCoordinates("#offset line at start: \nc0,0,0\n#", offsetLineAtStart, "\n   "));
229         OTSLine3D offsetLineAtEnd = offsetLine(offsetAtEnd);
230         // System.out.println(OTSGeometry.printCoordinates("#offset line at end: \nc0.7,0.7,0.7\n#", offsetLineAtEnd, "\n   "));
231         Geometry startGeometry = offsetLineAtStart.getLineString();
232         Geometry endGeometry = offsetLineAtEnd.getLineString();
233         LengthIndexedLine first = new LengthIndexedLine(startGeometry);
234         double firstLength = startGeometry.getLength();
235         LengthIndexedLine second = new LengthIndexedLine(endGeometry);
236         double secondLength = endGeometry.getLength();
237         ArrayList<Coordinate> out = new ArrayList<Coordinate>();
238         Coordinate[] firstCoordinates = startGeometry.getCoordinates();
239         Coordinate[] secondCoordinates = endGeometry.getCoordinates();
240         int firstIndex = 0;
241         int secondIndex = 0;
242         Coordinate prevCoordinate = null;
243         final double tooClose = 0.05; // 5 cm
244         while (firstIndex < firstCoordinates.length && secondIndex < secondCoordinates.length)
245         {
246             double firstRatio =
247                 firstIndex < firstCoordinates.length ? first.indexOf(firstCoordinates[firstIndex]) / firstLength
248                     : Double.MAX_VALUE;
249             double secondRatio =
250                 secondIndex < secondCoordinates.length ? second.indexOf(secondCoordinates[secondIndex]) / secondLength
251                     : Double.MAX_VALUE;
252             double ratio;
253             if (firstRatio < secondRatio)
254             {
255                 ratio = firstRatio;
256                 firstIndex++;
257             }
258             else
259             {
260                 ratio = secondRatio;
261                 secondIndex++;
262             }
263             Coordinate firstCoordinate = first.extractPoint(ratio * firstLength);
264             Coordinate secondCoordinate = second.extractPoint(ratio * secondLength);
265             Coordinate resultCoordinate =
266                 new Coordinate((1 - ratio) * firstCoordinate.x + ratio * secondCoordinate.x, (1 - ratio)
267                     * firstCoordinate.y + ratio * secondCoordinate.y);
268             if (null == prevCoordinate || resultCoordinate.distance(prevCoordinate) > tooClose)
269             {
270                 out.add(resultCoordinate);
271                 prevCoordinate = resultCoordinate;
272             }
273         }
274         Coordinate[] resultCoordinates = new Coordinate[out.size()];
275         for (int index = 0; index < out.size(); index++)
276         {
277             resultCoordinates[index] = out.get(index);
278         }
279         return new OTSLine3D(resultCoordinates);
280     }
281 
282     /**
283      * Create a line at linearly varying offset from this line. The offset may change linearly from its initial value at the
284      * start of the reference line via a number of intermediate offsets at intermediate positions to its final offset value at
285      * the end of the reference line.
286      * @param relativeFractions double[]; positional fractions for which the offsets have to be generated
287      * @param offsets double[]; offsets at the relative positions (positive value is Left, negative value is Right)
288      * @return Geometry; the Geometry of the line at linearly changing offset of the reference line
289      * @throws OTSGeometryException when this method fails to create the offset line
290      */
291     public final OTSLine3D offsetLine(final double[] relativeFractions, final double[] offsets)
292         throws OTSGeometryException
293     {
294         OTSLine3D[] offsetLine = new OTSLine3D[relativeFractions.length];
295         for (int i = 0; i < offsets.length; i++)
296         {
297             offsetLine[i] = offsetLine(offsets[i]);
298             // System.out.println(offsetLine[i].toExcel());
299             // System.out.println();
300         }
301 
302         ArrayList<Coordinate> out = new ArrayList<Coordinate>();
303         Coordinate prevCoordinate = null;
304         final double tooClose = 0.05; // 5 cm
305         for (int i = 0; i < offsets.length - 1; i++)
306         {
307             Geometry startGeometry =
308                 offsetLine[i].extractFractional(relativeFractions[i], relativeFractions[i + 1]).getLineString();
309             Geometry endGeometry =
310                 offsetLine[i + 1].extractFractional(relativeFractions[i], relativeFractions[i + 1]).getLineString();
311             LengthIndexedLine first = new LengthIndexedLine(startGeometry);
312             double firstLength = startGeometry.getLength();
313             LengthIndexedLine second = new LengthIndexedLine(endGeometry);
314             double secondLength = endGeometry.getLength();
315             Coordinate[] firstCoordinates = startGeometry.getCoordinates();
316             Coordinate[] secondCoordinates = endGeometry.getCoordinates();
317             int firstIndex = 0;
318             int secondIndex = 0;
319             while (firstIndex < firstCoordinates.length && secondIndex < secondCoordinates.length)
320             {
321                 double firstRatio =
322                     firstIndex < firstCoordinates.length ? first.indexOf(firstCoordinates[firstIndex]) / firstLength
323                         : Double.MAX_VALUE;
324                 double secondRatio =
325                     secondIndex < secondCoordinates.length ? second.indexOf(secondCoordinates[secondIndex])
326                         / secondLength : Double.MAX_VALUE;
327                 double ratio;
328                 if (firstRatio < secondRatio)
329                 {
330                     ratio = firstRatio;
331                     firstIndex++;
332                 }
333                 else
334                 {
335                     ratio = secondRatio;
336                     secondIndex++;
337                 }
338                 Coordinate firstCoordinate = first.extractPoint(ratio * firstLength);
339                 Coordinate secondCoordinate = second.extractPoint(ratio * secondLength);
340                 Coordinate resultCoordinate =
341                     new Coordinate((1 - ratio) * firstCoordinate.x + ratio * secondCoordinate.x, (1 - ratio)
342                         * firstCoordinate.y + ratio * secondCoordinate.y);
343                 if (null == prevCoordinate || resultCoordinate.distance(prevCoordinate) > tooClose)
344                 {
345                     out.add(resultCoordinate);
346                     prevCoordinate = resultCoordinate;
347                 }
348             }
349         }
350 
351         Coordinate[] resultCoordinates = new Coordinate[out.size()];
352         for (int index = 0; index < out.size(); index++)
353         {
354             resultCoordinates[index] = out.get(index);
355         }
356         return new OTSLine3D(resultCoordinates);
357     }
358 
359     /**
360      * Concatenate several OTSLine3D instances.
361      * @param lines OTSLine3D... one or more OTSLine3D. The last point of the first <strong>must</strong> match the first of the
362      *            second, etc.
363      * @return OTSLine3D
364      * @throws OTSGeometryException if zero lines are given, or when there is a gap between consecutive lines
365      */
366     public static OTSLine3D concatenate(final OTSLine3D... lines) throws OTSGeometryException
367     {
368         return concatenate(0.0, lines);
369     }
370 
371     /**
372      * Concatenate several OTSLine3D instances.
373      * @param toleranceSI the tolerance between the end point of a line and the first point of the next line
374      * @param lines OTSLine3D... one or more OTSLine3D. The last point of the first <strong>must</strong> match the first of the
375      *            second, etc.
376      * @return OTSLine3D
377      * @throws OTSGeometryException if zero lines are given, or when there is a gap between consecutive lines
378      */
379     public static OTSLine3D concatenate(final double toleranceSI, final OTSLine3D... lines) throws OTSGeometryException
380     {
381         if (0 == lines.length)
382         {
383             throw new OTSGeometryException("Empty argument list");
384         }
385         else if (1 == lines.length)
386         {
387             return lines[0];
388         }
389         int size = lines[0].size();
390         for (int i = 1; i < lines.length; i++)
391         {
392             if (lines[i - 1].getLast().distance(lines[i].getFirst()).si > toleranceSI)
393             {
394                 throw new OTSGeometryException("Lines are not connected");
395             }
396             size += lines[i].size() - 1;
397         }
398         OTSPoint3D[] points = new OTSPoint3D[size];
399         int nextIndex = 0;
400         for (int i = 0; i < lines.length; i++)
401         {
402             OTSLine3D line = lines[i];
403             for (int j = 0 == i ? 0 : 1; j < line.size(); j++)
404             {
405                 points[nextIndex++] = line.get(j);
406             }
407         }
408         return new OTSLine3D(points);
409     }
410 
411     /**
412      * Construct a new OTSLine3D with all points of this OTSLine3D in reverse order.
413      * @return OTSLine3D; the new OTSLine3D
414      */
415     public final OTSLine3D reverse()
416     {
417         OTSPoint3D[] resultPoints = new OTSPoint3D[size()];
418         int nextIndex = size();
419         for (OTSPoint3D p : getPoints())
420         {
421             resultPoints[--nextIndex] = p;
422         }
423         try
424         {
425             return new OTSLine3D(resultPoints);
426         }
427         catch (OTSGeometryException exception)
428         {
429             // Cannot happen
430             throw new RuntimeException(exception);
431         }
432     }
433 
434     /**
435      * Construct a new OTSLine3D covering the indicated fraction of this OTSLine3D.
436      * @param start double; starting point, valid range [0..<cite>end</cite>)
437      * @param end double; ending point, valid range (<cite>start</cite>..1]
438      * @return OTSLine3D; the new OTSLine3D
439      * @throws OTSGeometryException when start &gt;= end, or start &lt; 0, or end &gt; 1
440      */
441     public final OTSLine3D extractFractional(final double start, final double end) throws OTSGeometryException
442     {
443         if (start < 0 || start >= end || end > 1)
444         {
445             throw new OTSGeometryException("Bad interval");
446         }
447         getLength(); // computes and sets the length field
448         return extract(start * this.length, end * this.length);
449     }
450 
451     /**
452      * Create a new OTSLine3D that covers a sub-section of this OTSLine3D.
453      * @param start Length.Rel; the length along this OTSLine3D where the sub-section starts, valid range [0..<cite>end</cite>)
454      * @param end Length.Rel; length along this OTSLine3D where the sub-section ends, valid range
455      *            (<cite>start</cite>..<cite>length</cite> (length is the length of this OTSLine3D)
456      * @return OTSLine3D; the selected sub-section
457      * @throws OTSGeometryException when start &gt;= end, or start &lt; 0, or end &gt; length
458      */
459     public final OTSLine3D extract(final Length.Rel start, final Length.Rel end) throws OTSGeometryException
460     {
461         return extract(start.si, end.si);
462     }
463 
464     /**
465      * Create a new OTSLine3D that covers a sub-section of this OTSLine3D.
466      * @param start double; length along this OTSLine3D where the sub-section starts, valid range [0..<cite>end</cite>)
467      * @param end double; length along this OTSLine3D where the sub-section ends, valid range
468      *            (<cite>start</cite>..<cite>length</cite> (length is the length of this OTSLine3D)
469      * @return OTSLine3D; the selected sub-section
470      * @throws OTSGeometryException when start &gt;= end, or start &lt; 0, or end &gt; length
471      */
472     public final OTSLine3D extract(final double start, final double end) throws OTSGeometryException
473     {
474         if (Double.isNaN(start) || Double.isNaN(end) || start < 0 || start >= end || end > getLengthSI())
475         {
476             throw new OTSGeometryException("Bad interval (" + start + ".." + end + "; length of this OTSLine3D is "
477                 + this.getLengthSI() + ")");
478         }
479         double cumulativeLength = 0;
480         double nextCumulativeLength = 0;
481         double segmentLength = 0;
482         int index = 0;
483         List<OTSPoint3D> pointList = new ArrayList<>();
484         // System.err.println("interval " + start + ".." + end);
485         while (start > cumulativeLength)
486         {
487             OTSPoint3D fromPoint = this.points[index];
488             index++;
489             OTSPoint3D toPoint = this.points[index];
490             segmentLength = fromPoint.distanceSI(toPoint);
491             cumulativeLength = nextCumulativeLength;
492             nextCumulativeLength = cumulativeLength + segmentLength;
493             if (nextCumulativeLength >= start)
494             {
495                 break;
496             }
497         }
498         if (start == nextCumulativeLength)
499         {
500             pointList.add(this.points[index]);
501         }
502         else
503         {
504             pointList.add(OTSPoint3D.interpolate((start - cumulativeLength) / segmentLength, this.points[index - 1],
505                 this.points[index]));
506             if (end > nextCumulativeLength)
507             {
508                 pointList.add(this.points[index]);
509             }
510         }
511         while (end > nextCumulativeLength)
512         {
513             OTSPoint3D fromPoint = this.points[index];
514             index++;
515             if (index >= this.points.length)
516             {
517                 break; // rounding error
518             }
519             OTSPoint3D toPoint = this.points[index];
520             segmentLength = fromPoint.distanceSI(toPoint);
521             cumulativeLength = nextCumulativeLength;
522             nextCumulativeLength = cumulativeLength + segmentLength;
523             if (nextCumulativeLength >= end)
524             {
525                 break;
526             }
527             pointList.add(toPoint);
528         }
529         if (end == nextCumulativeLength)
530         {
531             pointList.add(this.points[index]);
532         }
533         else
534         {
535             pointList.add(OTSPoint3D.interpolate((end - cumulativeLength) / segmentLength, this.points[index - 1],
536                 this.points[index]));
537         }
538         try
539         {
540             return new OTSLine3D(pointList);
541         }
542         catch (OTSGeometryException exception)
543         {
544             System.err.println("interval " + start + ".." + end + "too short");
545             throw new OTSGeometryException("interval " + start + ".." + end + "too short");
546         }
547     }
548 
549     /**
550      * Build an array of OTSPoint3D from an array of Coordinate.
551      * @param coordinates Coordinate[]; the coordinates
552      * @return OTSPoint3D[]
553      */
554     private static OTSPoint3D[] coordinatesToOTSPoint3D(final Coordinate[] coordinates)
555     {
556         OTSPoint3D[] result = new OTSPoint3D[coordinates.length];
557         for (int i = 0; i < coordinates.length; i++)
558         {
559             result[i] = new OTSPoint3D(coordinates[i]);
560         }
561         return result;
562     }
563 
564     /**
565      * Create an OTSLine3D, while cleaning repeating successive points.
566      * @param points the coordinates of the line as OTSPoint3D
567      * @return the line
568      * @throws OTSGeometryException when number of points &lt; 2
569      */
570     public static OTSLine3D createAndCleanOTSLine3D(final OTSPoint3D[] points) throws OTSGeometryException
571     {
572         if (points.length < 2)
573         {
574             throw new OTSGeometryException("Degenerate OTSLine3D; has " + points.length + " point"
575                 + (points.length != 1 ? "s" : ""));
576         }
577         return createAndCleanOTSLine3D(new ArrayList<>(Arrays.asList(points)));
578     }
579 
580     /**
581      * Create an OTSLine3D, while cleaning repeating successive points.
582      * @param pointList List&lt;OTSPoint3D&gt;; list of the coordinates of the line as OTSPoint3D; any duplicate points in this
583      *            list are removed (this method may modify the provided list)
584      * @return OTSLine3D; the line
585      * @throws OTSGeometryException when number of non-equal points &lt; 2
586      */
587     public static OTSLine3D createAndCleanOTSLine3D(final List<OTSPoint3D> pointList) throws OTSGeometryException
588     {
589         // clean successive equal points
590         int i = 1;
591         while (i < pointList.size())
592         {
593             if (pointList.get(i - 1).equals(pointList.get(i)))
594             {
595                 pointList.remove(i);
596             }
597             else
598             {
599                 i++;
600             }
601         }
602         return new OTSLine3D(pointList);
603     }
604 
605     /**
606      * Construct a new OTSLine3D from an array of Coordinate.
607      * @param coordinates the array of coordinates to construct this OTSLine3D from
608      * @throws OTSGeometryException when the provided points do not constitute a valid line (too few points or identical
609      *             adjacent points)
610      */
611     public OTSLine3D(final Coordinate[] coordinates) throws OTSGeometryException
612     {
613         this(coordinatesToOTSPoint3D(coordinates));
614     }
615 
616     /**
617      * Construct a new OTSLine3D from a LineString.
618      * @param lineString the lineString to construct this OTSLine3D from.
619      * @throws OTSGeometryException when the provided LineString does not constitute a valid line (too few points or identical
620      *             adjacent points)
621      */
622     public OTSLine3D(final LineString lineString) throws OTSGeometryException
623     {
624         this(lineString.getCoordinates());
625     }
626 
627     /**
628      * Construct a new OTSLine3D from a Geometry.
629      * @param geometry the geometry to construct this OTSLine3D from
630      * @throws OTSGeometryException when the provided Geometry do not constitute a valid line (too few points or identical
631      *             adjacent points)
632      */
633     public OTSLine3D(final Geometry geometry) throws OTSGeometryException
634     {
635         this(geometry.getCoordinates());
636     }
637 
638     /**
639      * Construct a new OTSLine3D from a List&lt;OTSPoint3D&gt;.
640      * @param pointList the list of points to construct this OTSLine3D from.
641      * @throws OTSGeometryException when the provided points do not constitute a valid line (too few points or identical
642      *             adjacent points)
643      */
644     public OTSLine3D(final List<OTSPoint3D> pointList) throws OTSGeometryException
645     {
646         this(pointList.toArray(new OTSPoint3D[pointList.size()]));
647     }
648 
649     /**
650      * Construct a new OTSShape (closed shape) from a Path2D.
651      * @param path the Path2D to construct this OTSLine3D from.
652      * @throws OTSGeometryException when the provided points do not constitute a valid line (too few points or identical
653      *             adjacent points)
654      */
655     public OTSLine3D(final Path2D path) throws OTSGeometryException
656     {
657         PathIterator pi = path.getPathIterator(null);
658         List<OTSPoint3D> pl = new ArrayList<>();
659         double[] p = new double[6];
660         while (!pi.isDone())
661         {
662             pi.next();
663             int segType = pi.currentSegment(p);
664             if (segType == PathIterator.SEG_MOVETO || segType == PathIterator.SEG_LINETO)
665             {
666                 pl.add(new OTSPoint3D(p[0], p[1]));
667             }
668             else if (segType == PathIterator.SEG_CLOSE)
669             {
670                 if (!pl.get(0).equals(pl.get(pl.size() - 1)))
671                 {
672                     pl.add(new OTSPoint3D(pl.get(0).x, pl.get(0).y));
673                 }
674                 break;
675             }
676         }
677         init(pl.toArray(new OTSPoint3D[pl.size() - 1]));
678     }
679 
680     /**
681      * Construct a Coordinate array and fill it with the points of this OTSLine3D.
682      * @return an array of Coordinates corresponding to this OTSLine
683      */
684     public final Coordinate[] getCoordinates()
685     {
686         Coordinate[] result = new Coordinate[size()];
687         for (int i = 0; i < size(); i++)
688         {
689             result[i] = this.points[i].getCoordinate();
690         }
691         return result;
692     }
693 
694     /**
695      * Construct a LineString from this OTSLine3D.
696      * @return a LineString corresponding to this OTSLine
697      */
698     public final LineString getLineString()
699     {
700         GeometryFactory factory = new GeometryFactory();
701         Coordinate[] coordinates = getCoordinates();
702         CoordinateSequence cs = factory.getCoordinateSequenceFactory().create(coordinates);
703         return new LineString(cs, factory);
704     }
705 
706     /**
707      * Return the number of points in this OTSLine3D.
708      * @return the number of points on the line
709      */
710     public final int size()
711     {
712         return this.points.length;
713     }
714 
715     /**
716      * Return the first point of this OTSLine3D.
717      * @return the first point on the line
718      */
719     public final OTSPoint3D getFirst()
720     {
721         return this.points[0];
722     }
723 
724     /**
725      * Return the last point of this OTSLine3D.
726      * @return the last point on the line
727      */
728     public final OTSPoint3D getLast()
729     {
730         return this.points[size() - 1];
731     }
732 
733     /**
734      * Return one point of this OTSLine3D.
735      * @param i int; the index of the point to retrieve
736      * @return OTSPoint3d; the i-th point of the line
737      * @throws OTSGeometryException when i &lt; 0 or i &gt; the number of points
738      */
739     public final OTSPoint3D get(final int i) throws OTSGeometryException
740     {
741         if (i < 0 || i > size() - 1)
742         {
743             throw new OTSGeometryException("OTSLine3D.get(i=" + i + "); i<0 or i>=size(), which is " + size());
744         }
745         return this.points[i];
746     }
747 
748     /**
749      * Return the length of this OTSLine3D as a double value in SI units. (Assumes that the coordinates of the points
750      * constituting this line are expressed in meters.)
751      * @return the length of the line in SI units
752      */
753     public final synchronized double getLengthSI()
754     {
755         if (Double.isNaN(this.length))
756         {
757             this.length = 0.0;
758             for (int i = 0; i < size() - 1; i++)
759             {
760                 this.length += this.points[i].distanceSI(this.points[i + 1]);
761             }
762         }
763         return this.length;
764     }
765 
766     /**
767      * Return the length of this OTSLine3D in meters. (Assuming that the coordinates of the points constituting this line are
768      * expressed in meters.)
769      * @return the length of the line
770      */
771     public final Length.Rel getLength()
772     {
773         return new Length.Rel(getLengthSI(), LengthUnit.SI);
774     }
775 
776     /**
777      * Return an array of OTSPoint3D that represents this OTSLine3D. <strong>Do not modify the result.</strong>
778      * @return the points of this line
779      */
780     public final OTSPoint3D[] getPoints()
781     {
782         return this.points;
783     }
784 
785     /**
786      * Make the length indexed line if it does not exist yet, and cache it.
787      */
788     private void makeLengthIndexedLine()
789     {
790         if (this.lengthIndexedLine == null)
791         {
792             this.lengthIndexedLine = new double[this.points.length];
793             this.lengthIndexedLine[0] = 0.0;
794             for (int i = 1; i < this.points.length; i++)
795             {
796                 this.lengthIndexedLine[i] =
797                     this.lengthIndexedLine[i - 1] + this.points[i - 1].distanceSI(this.points[i]);
798             }
799         }
800     }
801 
802     /**
803      * Get the location at a position on the line, with its direction. Position can be below 0 or more than the line length. In
804      * that case, the position will be extrapolated in the direction of the line at its start or end.
805      * @param position the position on the line for which to calculate the point on, before, of after the line
806      * @return a directed point
807      */
808     public final DirectedPoint getLocationExtended(final Length.Rel position)
809     {
810         return getLocationExtendedSI(position.getSI());
811     }
812 
813     /**
814      * Get the location at a position on the line, with its direction. Position can be below 0 or more than the line length. In
815      * that case, the position will be extrapolated in the direction of the line at its start or end.
816      * @param positionSI the position on the line for which to calculate the point on, before, of after the line, in SI units
817      * @return a directed point
818      */
819     public final DirectedPoint getLocationExtendedSI(final double positionSI)
820     {
821         makeLengthIndexedLine();
822         if (positionSI >= 0.0 && positionSI <= getLengthSI())
823         {
824             try
825             {
826                 return getLocationSI(positionSI);
827             }
828             catch (OTSGeometryException exception)
829             {
830                 // cannot happen
831             }
832         }
833 
834         // position before start point -- extrapolate
835         if (positionSI < 0.0)
836         {
837             double len = positionSI;
838             double fraction = len / (this.lengthIndexedLine[1] - this.lengthIndexedLine[0]);
839             OTSPoint3D p1 = this.points[0];
840             OTSPoint3D p2 = this.points[1];
841             return new DirectedPoint(p1.x + fraction * (p2.x - p1.x), p1.y + fraction * (p2.y - p1.y), p1.z + fraction
842                 * (p2.z - p1.z), 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
843         }
844 
845         // position beyond end point -- extrapolate
846         int n1 = this.lengthIndexedLine.length - 1;
847         int n2 = this.lengthIndexedLine.length - 2;
848         double len = positionSI - getLengthSI();
849         double fraction = len / (this.lengthIndexedLine[n1] - this.lengthIndexedLine[n2]);
850         OTSPoint3D p1 = this.points[n2];
851         OTSPoint3D p2 = this.points[n1];
852         return new DirectedPoint(p2.x + fraction * (p2.x - p1.x), p2.y + fraction * (p2.y - p1.y), p2.z + fraction
853             * (p2.z - p1.z), 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
854     }
855 
856     /**
857      * Get the location at a fraction of the line, with its direction. Fraction should be between 0.0 and 1.0.
858      * @param fraction the fraction for which to calculate the point on the line
859      * @return a directed point
860      * @throws OTSGeometryException when fraction less than 0.0 or more than 1.0.
861      */
862     public final DirectedPoint getLocationFraction(final double fraction) throws OTSGeometryException
863     {
864         if (fraction < 0.0 || fraction > 1.0)
865         {
866             throw new OTSGeometryException("getLocationFraction for line: fraction < 0.0 or > 1.0. fraction = "
867                 + fraction);
868         }
869         return getLocationSI(fraction * getLengthSI());
870     }
871 
872     /**
873      * Get the location at a fraction of the line, with its direction. Fraction should be between 0.0 and 1.0.
874      * @param fraction the fraction for which to calculate the point on the line
875      * @param tolerance the delta from 0.0 and 1.0 that will be forgiven
876      * @return a directed point
877      * @throws OTSGeometryException when fraction less than 0.0 or more than 1.0.
878      */
879     public final DirectedPoint getLocationFraction(final double fraction, final double tolerance)
880         throws OTSGeometryException
881     {
882         if (fraction < -tolerance || fraction > 1.0 + tolerance)
883         {
884             throw new OTSGeometryException(
885                 "getLocationFraction for line: fraction < 0.0 - tolerance or > 1.0 + tolerance; fraction = " + fraction);
886         }
887         double f = fraction < 0 ? 0.0 : fraction > 1.0 ? 1.0 : fraction;
888         return getLocationSI(f * getLengthSI());
889     }
890 
891     /**
892      * Get the location at a fraction of the line (or outside the line), with its direction.
893      * @param fraction the fraction for which to calculate the point on the line
894      * @return a directed point
895      */
896     public final DirectedPoint getLocationFractionExtended(final double fraction)
897     {
898         return getLocationExtendedSI(fraction * getLengthSI());
899     }
900 
901     /**
902      * Get the location at a position on the line, with its direction. Position should be between 0.0 and line length.
903      * @param position the position on the line for which to calculate the point on the line
904      * @return a directed point
905      * @throws OTSGeometryException when position less than 0.0 or more than line length.
906      */
907     public final DirectedPoint getLocation(final Length.Rel position) throws OTSGeometryException
908     {
909         return getLocationSI(position.getSI());
910     }
911 
912     /**
913      * Binary search for a position on the line.
914      * @param pos the position to look for.
915      * @return the index below the position; the position is between points[index] and points[index+1]
916      * @throws OTSGeometryException when index could not be found
917      */
918     private int find(final double pos) throws OTSGeometryException
919     {
920         if (pos == 0)
921         {
922             return 0;
923         }
924 
925         for (int i = 0; i < this.lengthIndexedLine.length - 2; i++)
926         {
927             if (pos > this.lengthIndexedLine[i] && pos <= this.lengthIndexedLine[i + 1])
928             {
929                 return i;
930             }
931         }
932 
933         return this.lengthIndexedLine.length - 2;
934 
935         /*- binary variant
936         int lo = 0;
937         int hi = this.lengthIndexedLine.length - 1;
938         while (lo <= hi)
939         {
940             if (hi - lo <= 1)
941             {
942                 return lo;
943             }
944             int mid = lo + (hi - lo) / 2;
945             if (pos < this.lengthIndexedLine[mid])
946             {
947                 hi = mid - 1;
948             }
949             else if (pos > this.lengthIndexedLine[mid])
950             {
951                 lo = mid + 1;
952             }
953         }
954         throw new OTSGeometryException("Could not find position " + pos + " on line with length indexes: "
955             + this.lengthIndexedLine);
956          */
957     }
958 
959     /**
960      * Get the location at a position on the line, with its direction. Position should be between 0.0 and line length.
961      * @param positionSI the position on the line for which to calculate the point on the line
962      * @return a directed point
963      * @throws OTSGeometryException when position less than 0.0 or more than line length.
964      */
965     public final DirectedPoint getLocationSI(final double positionSI) throws OTSGeometryException
966     {
967         makeLengthIndexedLine();
968         if (positionSI < 0.0 || positionSI > getLengthSI())
969         {
970             throw new OTSGeometryException("getLocationSI for line: position < 0.0 or > line length. Position = "
971                 + positionSI + " m. Length = " + getLengthSI() + " m.");
972         }
973 
974         // handle special cases: position == 0.0, or position == length
975         if (positionSI == 0.0)
976         {
977             OTSPoint3D p1 = this.points[0];
978             OTSPoint3D p2 = this.points[1];
979             return new DirectedPoint(p1.x, p1.y, p1.z, 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
980         }
981         if (positionSI == getLengthSI())
982         {
983             OTSPoint3D p1 = this.points[this.points.length - 2];
984             OTSPoint3D p2 = this.points[this.points.length - 1];
985             return new DirectedPoint(p2.x, p2.y, p2.z, 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
986         }
987 
988         // find the index of the line segment, use binary search
989         int index = find(positionSI);
990         double remainder = positionSI - this.lengthIndexedLine[index];
991         double fraction = remainder / (this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index]);
992         OTSPoint3D p1 = this.points[index];
993         OTSPoint3D p2 = this.points[index + 1];
994         return new DirectedPoint(p1.x + fraction * (p2.x - p1.x), p1.y + fraction * (p2.y - p1.y), p1.z + fraction
995             * (p2.z - p1.z), 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
996     }
997 
998     /**
999      * Truncate a line at the given length (less than the length of the line, and larger than zero) and return a new line.
1000      * @param lengthSI the location where to truncate the line
1001      * @return a new OTSLine3D truncated at the exact position where line.getLength() == lengthSI
1002      * @throws OTSGeometryException when position less than 0.0 or more than line length.
1003      */
1004     public final OTSLine3D truncate(final double lengthSI) throws OTSGeometryException
1005     {
1006         makeLengthIndexedLine();
1007         if (lengthSI <= 0.0 || lengthSI > getLengthSI())
1008         {
1009             throw new OTSGeometryException("truncate for line: position <= 0.0 or > line length. Position = "
1010                 + lengthSI + " m. Length = " + getLengthSI() + " m.");
1011         }
1012 
1013         // handle special case: position == length
1014         if (lengthSI == getLengthSI())
1015         {
1016             return new OTSLine3D(getPoints());
1017         }
1018 
1019         // find the index of the line segment
1020         int index = find(lengthSI);
1021         double remainder = lengthSI - this.lengthIndexedLine[index];
1022         double fraction = remainder / (this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index]);
1023         OTSPoint3D p1 = this.points[index];
1024         OTSPoint3D p2 = this.points[index + 1];
1025         OTSPoint3D newLastPoint =
1026             new OTSPoint3D(p1.x + fraction * (p2.x - p1.x), p1.y + fraction * (p2.y - p1.y), p1.z + fraction
1027                 * (p2.z - p1.z));
1028         OTSPoint3D[] coords = new OTSPoint3D[index + 2];
1029         for (int i = 0; i <= index; i++)
1030         {
1031             coords[i] = this.points[i];
1032         }
1033         coords[index + 1] = newLastPoint;
1034         return new OTSLine3D(coords);
1035     }
1036 
1037     /*-
1038      * TODO finish this method if it is needed; remove otherwise.
1039      * Calculate the first point on this line that intersects somewhere with the provided line, or NaN if no intersection was
1040      * found.
1041      * @param line the line to test the intersection with
1042      * @return the fraction of the first intersection point
1043      *
1044     public final double firstIntersectionFraction(final OTSLine3D line)
1045     {
1046         List<Line2D.Double> segs = new ArrayList<>();
1047         for (int j = 1; j < line.getPoints().length; j++)
1048         {
1049             Line2D.Double seg =
1050                 new Line2D.Double(this.points[j - 1].x, this.points[j - 1].y, this.points[j].x, this.points[j].y);
1051             segs.add(seg);
1052 
1053         }
1054         for (int i = 1; i < this.points.length; i++)
1055         {
1056             Line2D.Double thisSeg =
1057                 new Line2D.Double(this.points[i - 1].x, this.points[i - 1].y, this.points[i].x, this.points[i].y);
1058             for (Line2D.Double seg : segs)
1059             {
1060                 if (thisSeg.intersectsLine(seg))
1061                 {
1062                     // Point2D.Double intersectionPoint = thisSeg.
1063                     
1064                 }
1065             }
1066         }
1067         return Double.NaN;
1068     }
1069      */
1070 
1071     /**
1072      * Calculate the centroid of this line, and the bounds, and cache for later use. Make sure the dx, dy and dz are at least
1073      * 0.5 m wide.
1074      */
1075     private void calcCentroidBounds()
1076     {
1077         double minX = Double.POSITIVE_INFINITY;
1078         double minY = Double.POSITIVE_INFINITY;
1079         double minZ = Double.POSITIVE_INFINITY;
1080         double maxX = Double.NEGATIVE_INFINITY;
1081         double maxY = Double.NEGATIVE_INFINITY;
1082         double maxZ = Double.NEGATIVE_INFINITY;
1083         for (OTSPoint3D p : this.points)
1084         {
1085             minX = Math.min(minX, p.x);
1086             minY = Math.min(minY, p.y);
1087             minZ = Math.min(minZ, p.z);
1088             maxX = Math.max(maxX, p.x);
1089             maxY = Math.max(maxY, p.y);
1090             maxZ = Math.max(maxZ, p.z);
1091         }
1092         this.centroid = new OTSPoint3D((maxX + minX) / 2, (maxY + minY) / 2, (maxZ + minZ) / 2);
1093         double deltaX = Math.max(maxX - minX, 0.5);
1094         double deltaY = Math.max(maxY - minY, 0.5);
1095         double deltaZ = Math.max(maxZ - minZ, 0.5);
1096         this.bounds = new BoundingBox(deltaX, deltaY, deltaZ);
1097         this.boundingRect = new Rectangle2D.Double(minX, minY, deltaX, deltaY);
1098     }
1099 
1100     /**
1101      * Retrieve the centroid of this OTSLine3D.
1102      * @return OTSPoint3D; the centroid of this OTSLine3D
1103      */
1104     public final OTSPoint3D getCentroid()
1105     {
1106         if (this.centroid == null)
1107         {
1108             calcCentroidBounds();
1109         }
1110         return this.centroid;
1111     }
1112 
1113     /**
1114      * Get the bounding rectangle of this OTSLine3D.
1115      * @return Rectangle2D; the bounding rectangle of this OTSLine3D
1116      */
1117     public final Rectangle2D getBoundingRectangle()
1118     {
1119         if (this.boundingRect == null)
1120         {
1121             calcCentroidBounds();
1122         }
1123         return this.boundingRect;
1124     }
1125 
1126     /** {@inheritDoc} */
1127     @Override
1128     @SuppressWarnings("checkstyle:designforextension")
1129     public DirectedPoint getLocation()
1130     {
1131         if (this.centroid == null)
1132         {
1133             calcCentroidBounds();
1134         }
1135         return this.centroid.getDirectedPoint();
1136     }
1137 
1138     /** {@inheritDoc} */
1139     @Override
1140     @SuppressWarnings("checkstyle:designforextension")
1141     public Bounds getBounds()
1142     {
1143         if (this.bounds == null)
1144         {
1145             calcCentroidBounds();
1146         }
1147         return this.bounds;
1148     }
1149 
1150     /** {@inheritDoc} */
1151     @Override
1152     @SuppressWarnings("checkstyle:designforextension")
1153     public String toString()
1154     {
1155         return Arrays.toString(this.points);
1156     }
1157 
1158     /** {@inheritDoc} */
1159     @Override
1160     @SuppressWarnings("checkstyle:designforextension")
1161     public int hashCode()
1162     {
1163         final int prime = 31;
1164         int result = 1;
1165         result = prime * result + Arrays.hashCode(this.points);
1166         return result;
1167     }
1168 
1169     /** {@inheritDoc} */
1170     @Override
1171     @SuppressWarnings({"checkstyle:designforextension", "checkstyle:needbraces"})
1172     public boolean equals(final Object obj)
1173     {
1174         if (this == obj)
1175             return true;
1176         if (obj == null)
1177             return false;
1178         if (getClass() != obj.getClass())
1179             return false;
1180         OTSLine3D other = (OTSLine3D) obj;
1181         if (!Arrays.equals(this.points, other.points))
1182             return false;
1183         return true;
1184     }
1185 
1186     /**
1187      * @return excel XY plottable output
1188      */
1189     public final String toExcel()
1190     {
1191         StringBuffer s = new StringBuffer();
1192         for (OTSPoint3D p : this.points)
1193         {
1194             s.append(p.x + "\t" + p.y + "\n");
1195         }
1196         return s.toString();
1197     }
1198 
1199     /**
1200      * @param args String[]; the command line arguments (not used)
1201      * @throws OTSGeometryException in case of error
1202      */
1203     public static void main(final String[] args) throws OTSGeometryException
1204     {
1205         OTSLine3D line =
1206             new OTSLine3D(new OTSPoint3D(-263.811, -86.551, 1.180), new OTSPoint3D(-262.945, -84.450, 1.180),
1207                 new OTSPoint3D(-261.966, -82.074, 1.180), new OTSPoint3D(-260.890, -79.464, 1.198), new OTSPoint3D(
1208                     -259.909, -76.955, 1.198), new OTSPoint3D(-258.911, -74.400, 1.198), new OTSPoint3D(-257.830,
1209                     -71.633, 1.234));
1210         System.out.println(line.toExcel());
1211         double[] relativeFractions =
1212             new double[]{0.0, 0.19827228089475762, 0.30549496392494213, 0.5824753163948581, 0.6815307752261827,
1213                 0.7903990449840241, 0.8942375145295614, 1.0};
1214         double[] offsets =
1215             new double[]{2.9779999256134, 4.6029999256134, 3.886839156071996, 2.3664845198627207, 1.7858981925396709,
1216                 1.472348149010167, 2.0416709053157285, 2.798692100483229};
1217         System.out.println(line.offsetLine(relativeFractions, offsets).toExcel());
1218     }
1219 }