View Javadoc
1   package org.opentrafficsim.core.geometry;
2   
3   import java.io.Serializable;
4   import java.util.Arrays;
5   import java.util.List;
6   
7   import javax.media.j3d.Bounds;
8   
9   import nl.tudelft.simulation.dsol.animation.LocatableInterface;
10  import nl.tudelft.simulation.language.d3.BoundingBox;
11  import nl.tudelft.simulation.language.d3.DirectedPoint;
12  
13  import org.djunits.unit.LengthUnit;
14  import org.opentrafficsim.core.OTS_SCALAR;
15  import org.opentrafficsim.core.network.NetworkException;
16  
17  import com.vividsolutions.jts.geom.Coordinate;
18  import com.vividsolutions.jts.geom.CoordinateSequence;
19  import com.vividsolutions.jts.geom.Geometry;
20  import com.vividsolutions.jts.geom.GeometryFactory;
21  import com.vividsolutions.jts.geom.LineString;
22  
23  /**
24   * <p>
25   * Copyright (c) 2013-2015 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
26   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
27   * <p>
28   * $LastChangedDate: 2015-07-16 10:20:53 +0200 (Thu, 16 Jul 2015) $, @version $Revision: 1124 $, by $Author: pknoppers $,
29   * initial version Jul 22, 2015 <br>
30   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
31   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
32   * @author <a href="http://www.citg.tudelft.nl">Guus Tamminga</a>
33   */
34  public class OTSLine3D implements LocatableInterface, Serializable, OTS_SCALAR
35  {
36      /** */
37      private static final long serialVersionUID = 20150722L;
38  
39      /** the points of the line. */
40      private final OTSPoint3D[] points;
41  
42      /** the cumulative length of the line at point 'i'. */
43      private double[] lengthIndexedLine = null;
44  
45      /** the cached length; will be calculated when needed for the first time. */
46      private double length = Double.NaN;
47  
48      /** the cached centroid; will be calculated when needed for the first time. */
49      private OTSPoint3D centroid = null;
50  
51      /** the cached bounds; will be calculated when needed for the first time. */
52      private Bounds bounds = null;
53  
54      /**
55       * @param points the array of points to construct this OTSLine3D from.
56       */
57      public OTSLine3D(final OTSPoint3D[] points)
58      {
59          this.points = points;
60      }
61  
62      /**
63       * @param coordinates the array of coordinates to construct this OTSLine3D from.
64       */
65      public OTSLine3D(final Coordinate[] coordinates)
66      {
67          this.points = new OTSPoint3D[coordinates.length];
68          int i = 0;
69          for (Coordinate c : coordinates)
70          {
71              this.points[i++] = new OTSPoint3D(c);
72          }
73      }
74  
75      /**
76       * @param lineString the lineString to construct this OTSLine3D from.
77       */
78      public OTSLine3D(final LineString lineString)
79      {
80          this(lineString.getCoordinates());
81      }
82  
83      /**
84       * @param geometry the geometry to construct this OTSLine3D from.
85       */
86      public OTSLine3D(final Geometry geometry)
87      {
88          this(geometry.getCoordinates());
89      }
90  
91      /**
92       * @param pointList the list of points to construct this OTSLine3D from.
93       */
94      public OTSLine3D(final List<OTSPoint3D> pointList)
95      {
96          this(pointList.toArray(new OTSPoint3D[pointList.size()]));
97      }
98  
99      /**
100      * @return an array of Coordinates corresponding to this OTSLine.
101      */
102     public final Coordinate[] getCoordinates()
103     {
104         Coordinate[] result = new Coordinate[size()];
105         for (int i = 0; i < size(); i++)
106         {
107             result[i] = this.points[i].getCoordinate();
108         }
109         return result;
110     }
111 
112     /**
113      * @return a LineString corresponding to this OTSLine.
114      */
115     public final LineString getLineString()
116     {
117         GeometryFactory factory = new GeometryFactory();
118         Coordinate[] coordinates = getCoordinates();
119         CoordinateSequence cs = factory.getCoordinateSequenceFactory().create(coordinates);
120         return new LineString(cs, factory);
121     }
122 
123     /**
124      * @return the number of points on the line.
125      */
126     public final int size()
127     {
128         return this.points.length;
129     }
130 
131     /**
132      * @param i the index of the point to retrieve
133      * @return the i-th point of the line.
134      * @throws OTSGeometryException when i &lt; 0 or i &gt; the number of points
135      */
136     public final OTSPoint3D get(final int i) throws OTSGeometryException
137     {
138         if (i < 0 || i > size() - 1)
139         {
140             throw new OTSGeometryException("OTSLine3D.get(i=" + i + "); i<0 or i>=size(), which is " + size());
141         }
142         return this.points[i];
143     }
144 
145     /**
146      * @return the length of the line in SI units.
147      */
148     public final synchronized double getLengthSI()
149     {
150         if (Double.isNaN(this.length))
151         {
152             this.length = 0.0;
153             for (int i = 0; i < size() - 1; i++)
154             {
155                 this.length += this.points[i].distanceSI(this.points[i + 1]);
156             }
157         }
158         return this.length;
159     }
160 
161     /**
162      * @return the length of the line.
163      */
164     public final Length.Rel getLength()
165     {
166         return new Length.Rel(getLengthSI(), LengthUnit.SI);
167     }
168 
169     /**
170      * @return the points of this line.
171      */
172     public final OTSPoint3D[] getPoints()
173     {
174         return this.points;
175     }
176 
177     /**
178      * make the length indexed line if it does not exist yet, and cache it.
179      */
180     private void makeLengthIndexedLine()
181     {
182         if (this.lengthIndexedLine == null)
183         {
184             this.lengthIndexedLine = new double[this.points.length];
185             this.lengthIndexedLine[0] = 0.0;
186             for (int i = 1; i < this.points.length; i++)
187             {
188                 this.lengthIndexedLine[i] = this.lengthIndexedLine[i - 1] + this.points[i - 1].distanceSI(this.points[i]);
189             }
190         }
191     }
192 
193     /**
194      * Get the location at a position on the line, with its direction. Position can be below 0 or more than the line length. In
195      * that case, the position will be extrapolated in the direction of the line at its start or end.
196      * @param position the position on the line for which to calculate the point on, before, of after the line
197      * @return a directed point
198      * @throws NetworkException when position could not be calculated
199      */
200     public final DirectedPoint getLocationExtended(final Length.Rel position) throws NetworkException
201     {
202         return getLocationExtendedSI(position.getSI());
203     }
204 
205     /**
206      * Get the location at a position on the line, with its direction. Position can be below 0 or more than the line length. In
207      * that case, the position will be extrapolated in the direction of the line at its start or end.
208      * @param positionSI the position on the line for which to calculate the point on, before, of after the line, in SI units
209      * @return a directed point
210      * @throws NetworkException when position could not be calculated
211      */
212     public final DirectedPoint getLocationExtendedSI(final double positionSI) throws NetworkException
213     {
214         makeLengthIndexedLine();
215         if (positionSI >= 0.0 && positionSI <= getLengthSI())
216         {
217             return getLocationSI(positionSI);
218         }
219 
220         // position before start point -- extrapolate
221         if (positionSI < 0.0)
222         {
223             double len = positionSI;
224             double fraction = len / (this.lengthIndexedLine[1] - this.lengthIndexedLine[0]);
225             OTSPoint3D p1 = this.points[0];
226             OTSPoint3D p2 = this.points[1];
227             return new DirectedPoint(p1.x + fraction * (p2.x - p1.x), p1.y + fraction * (p2.y - p1.y), p1.z + fraction
228                 * (p2.z - p1.z), 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
229         }
230 
231         // position beyond end point -- extrapolate
232         int n1 = this.lengthIndexedLine.length - 1;
233         int n2 = this.lengthIndexedLine.length - 2;
234         double len = positionSI - getLengthSI();
235         double fraction = len / (this.lengthIndexedLine[n1] - this.lengthIndexedLine[n2]);
236         OTSPoint3D p1 = this.points[n2];
237         OTSPoint3D p2 = this.points[n1];
238         return new DirectedPoint(p2.x + fraction * (p2.x - p1.x), p2.y + fraction * (p2.y - p1.y), p2.z + fraction
239             * (p2.z - p1.z), 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
240     }
241 
242     /**
243      * Get the location at a fraction of the line, with its direction. Fraction should be between 0.0 and 1.0.
244      * @param fraction the fraction for which to calculate the point on the line
245      * @return a directed point
246      * @throws NetworkException when fraction less than 0.0 or more than 1.0.
247      */
248     public final DirectedPoint getLocationFraction(final double fraction) throws NetworkException
249     {
250         if (fraction < 0.0 || fraction > 1.0)
251         {
252             throw new NetworkException("getLocationFraction for line: fraction < 0.0 or > 1.0. fraction = " + fraction);
253         }
254         return getLocationSI(fraction * getLengthSI());
255     }
256 
257     /**
258      * Get the location at a position on the line, with its direction. Position should be between 0.0 and line length.
259      * @param position the position on the line for which to calculate the point on the line
260      * @return a directed point
261      * @throws NetworkException when position less than 0.0 or more than line length.
262      */
263     public final DirectedPoint getLocation(final Length.Rel position) throws NetworkException
264     {
265         return getLocationSI(position.getSI());
266     }
267 
268     /**
269      * Binary search for a position on the line.
270      * @param pos the position to look for.
271      * @return the index below the position; the position is between points[index] and points[index+1]
272      * @throws NetworkException when index could not be found
273      */
274     private int find(final double pos) throws NetworkException
275     {
276         if (pos == 0)
277         {
278             return 0;
279         }
280 
281         for (int i = 0; i < this.lengthIndexedLine.length - 2; i++)
282         {
283             if (pos > this.lengthIndexedLine[i] && pos <= this.lengthIndexedLine[i + 1])
284             {
285                 return i;
286             }
287         }
288 
289         return this.lengthIndexedLine.length - 2;
290 
291         /*- binary variant
292         int lo = 0;
293         int hi = this.lengthIndexedLine.length - 1;
294         while (lo <= hi)
295         {
296             if (hi - lo <= 1)
297             {
298                 return lo;
299             }
300             int mid = lo + (hi - lo) / 2;
301             if (pos < this.lengthIndexedLine[mid])
302             {
303                 hi = mid - 1;
304             }
305             else if (pos > this.lengthIndexedLine[mid])
306             {
307                 lo = mid + 1;
308             }
309         }
310         throw new NetworkException("Could not find position " + pos + " on line with length indexes: "
311             + this.lengthIndexedLine);
312          */
313     }
314 
315     /**
316      * Get the location at a position on the line, with its direction. Position should be between 0.0 and line length.
317      * @param positionSI the position on the line for which to calculate the point on the line
318      * @return a directed point
319      * @throws NetworkException when position less than 0.0 or more than line length.
320      */
321     public final DirectedPoint getLocationSI(final double positionSI) throws NetworkException
322     {
323         makeLengthIndexedLine();
324         if (positionSI < 0.0 || positionSI > getLengthSI())
325         {
326             throw new NetworkException("getLocationSI for line: position < 0.0 or > line length. Position = " + positionSI
327                 + " m. Length = " + getLengthSI() + " m.");
328         }
329 
330         // handle special cases: position == 0.0, or position == length
331         if (positionSI == 0.0)
332         {
333             OTSPoint3D p1 = this.points[0];
334             OTSPoint3D p2 = this.points[1];
335             return new DirectedPoint(p1.x, p1.y, p1.z, 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
336         }
337         if (positionSI == getLengthSI())
338         {
339             OTSPoint3D p1 = this.points[this.points.length - 2];
340             OTSPoint3D p2 = this.points[this.points.length - 1];
341             return new DirectedPoint(p2.x, p2.y, p2.z, 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
342         }
343 
344         // find the index of the line segment, use binary search
345         int index = find(positionSI);
346         double remainder = positionSI - this.lengthIndexedLine[index];
347         double fraction = remainder / (this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index]);
348         OTSPoint3D p1 = this.points[index];
349         OTSPoint3D p2 = this.points[index + 1];
350         return new DirectedPoint(p1.x + fraction * (p2.x - p1.x), p1.y + fraction * (p2.y - p1.y), p1.z + fraction
351             * (p2.z - p1.z), 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
352     }
353 
354     /**
355      * Calculate the centroid of this line, and the bounds, and cache for later use. Make sure the dx, dy and dz are at least
356      * 0.5 m wide.
357      */
358     private void calcCentroidBounds()
359     {
360         double minX = Double.POSITIVE_INFINITY;
361         double minY = Double.POSITIVE_INFINITY;
362         double minZ = Double.POSITIVE_INFINITY;
363         double maxX = Double.NEGATIVE_INFINITY;
364         double maxY = Double.NEGATIVE_INFINITY;
365         double maxZ = Double.NEGATIVE_INFINITY;
366         for (OTSPoint3D p : this.points)
367         {
368             minX = Math.min(minX, p.x);
369             minY = Math.min(minY, p.y);
370             minZ = Math.min(minZ, p.z);
371             maxX = Math.max(maxX, p.x);
372             maxY = Math.max(maxY, p.y);
373             maxZ = Math.max(maxZ, p.z);
374         }
375         this.centroid = new OTSPoint3D((maxX + minX) / 2, (maxY + minY) / 2, (maxZ + minZ) / 2);
376         double deltaX = Math.max(maxX - minX, 0.5);
377         double deltaY = Math.max(maxY - minY, 0.5);
378         double deltaZ = Math.max(maxZ - minZ, 0.5);
379         this.bounds = new BoundingBox(deltaX, deltaY, deltaZ);
380     }
381 
382     /** {@inheritDoc} */
383     @Override
384     @SuppressWarnings("checkstyle:designforextension")
385     public DirectedPoint getLocation() 
386     {
387         if (this.centroid == null)
388         {
389             calcCentroidBounds();
390         }
391         return this.centroid.getDirectedPoint();
392     }
393 
394     /** {@inheritDoc} */
395     @Override
396     @SuppressWarnings("checkstyle:designforextension")
397     public Bounds getBounds() 
398     {
399         if (this.bounds == null)
400         {
401             calcCentroidBounds();
402         }
403         return this.bounds;
404     }
405 
406     /** {@inheritDoc} */
407     @Override
408     @SuppressWarnings("checkstyle:designforextension")
409     public String toString()
410     {
411         return Arrays.toString(this.points);
412     }
413 
414     /** {@inheritDoc} */
415     @Override
416     @SuppressWarnings("checkstyle:designforextension")
417     public int hashCode()
418     {
419         final int prime = 31;
420         int result = 1;
421         result = prime * result + ((this.bounds == null) ? 0 : this.bounds.hashCode());
422         result = prime * result + ((this.centroid == null) ? 0 : this.centroid.hashCode());
423         long temp;
424         temp = Double.doubleToLongBits(this.length);
425         result = prime * result + (int) (temp ^ (temp >>> 32));
426         result = prime * result + Arrays.hashCode(this.points);
427         return result;
428     }
429 
430     /** {@inheritDoc} */
431     @Override
432     @SuppressWarnings({"checkstyle:designforextension", "checkstyle:needbraces"})
433     public boolean equals(final Object obj)
434     {
435         if (this == obj)
436             return true;
437         if (obj == null)
438             return false;
439         if (getClass() != obj.getClass())
440             return false;
441         OTSLine3D other = (OTSLine3D) obj;
442         if (this.bounds == null)
443         {
444             if (other.bounds != null)
445                 return false;
446         }
447         else if (!this.bounds.equals(other.bounds))
448             return false;
449         if (this.centroid == null)
450         {
451             if (other.centroid != null)
452                 return false;
453         }
454         else if (!this.centroid.equals(other.centroid))
455             return false;
456         if (Double.doubleToLongBits(this.length) != Double.doubleToLongBits(other.length))
457             return false;
458         if (!Arrays.equals(this.points, other.points))
459             return false;
460         return true;
461     }
462 
463 }