View Javadoc
1   package org.opentrafficsim.road.network.lane;
2   
3   import java.io.Serializable;
4   import java.util.ArrayList;
5   import java.util.Arrays;
6   import java.util.List;
7   
8   import javax.media.j3d.Bounds;
9   
10  import nl.tudelft.simulation.dsol.animation.Locatable;
11  import nl.tudelft.simulation.event.EventProducer;
12  import nl.tudelft.simulation.language.d3.DirectedPoint;
13  
14  import org.djunits.value.vdouble.scalar.Length;
15  import org.opentrafficsim.core.geometry.OTSGeometryException;
16  import org.opentrafficsim.core.geometry.OTSLine3D;
17  import org.opentrafficsim.core.geometry.OTSPoint3D;
18  import org.opentrafficsim.core.geometry.OTSShape;
19  import org.opentrafficsim.core.network.LateralDirectionality;
20  import org.opentrafficsim.core.network.NetworkException;
21  
22  /**
23   * <p>
24   * Copyright (c) 2013-2016 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
25   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
26   * <p>
27   * $LastChangedDate: 2015-09-14 01:33:02 +0200 (Mon, 14 Sep 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $,
28   * initial version Aug 19, 2014 <br>
29   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
30   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
31   * @author <a href="http://www.citg.tudelft.nl">Guus Tamminga</a>
32   */
33  public abstract class CrossSectionElement extends EventProducer implements Locatable, Serializable
34  {
35      /** */
36      private static final long serialVersionUID = 20150826L;
37  
38      /** The id. Should be unique within the parentLink. */
39      private final String id;
40  
41      /** Cross Section Link to which the element belongs. */
42      @SuppressWarnings("checkstyle:visibilitymodifier")
43      protected final CrossSectionLink parentLink;
44  
45      /** The offsets and widths at positions along the line, relative to the design line of the parent link. */
46      @SuppressWarnings("checkstyle:visibilitymodifier")
47      protected final List<CrossSectionSlice> crossSectionSlices;
48  
49      /** The length of the line. Calculated once at the creation. */
50      @SuppressWarnings("checkstyle:visibilitymodifier")
51      protected final Length length;
52  
53      /** The center line of the element. Calculated once at the creation. */
54      private final OTSLine3D centerLine;
55  
56      /** The contour of the element. Calculated once at the creation. */
57      private final OTSShape contour;
58  
59      /**
60       * <b>Note:</b> LEFT is seen as a positive lateral direction, RIGHT as a negative lateral direction, with the direction from
61       * the StartNode towards the EndNode as the longitudinal direction.
62       * @param id String; The id of the CrosssSectionElement. Should be unique within the parentLink.
63       * @param parentLink CrossSectionLink; Link to which the element belongs.
64       * @param crossSectionSlices The offsets and widths at positions along the line, relative to the design line of the parent
65       *            link. If there is just one with and offset, there should just be one element in the list with Length = 0.
66       *            If there are more slices, the last one should be at the length of the design line. If not, a NetworkException
67       *            is thrown.
68       * @throws OTSGeometryException when creation of the geometry fails
69       * @throws NetworkException when id equal to null or not unique, or there are multiple slices and the last slice does not
70       *             end at the length of the design line.
71       */
72      public CrossSectionElement(final CrossSectionLink parentLink, final String id,
73          final List<CrossSectionSlice> crossSectionSlices) throws OTSGeometryException, NetworkException
74      {
75          if (parentLink == null)
76          {
77              throw new NetworkException("Constructor of CrossSectionElement for id " + id
78                  + ", parentLink cannot be null");
79          }
80          if (id == null)
81          {
82              throw new NetworkException("Constructor of CrossSectionElement -- id cannot be null");
83          }
84          for (CrossSectionElement cse : parentLink.getCrossSectionElementList())
85          {
86              if (cse.getId().equals(id))
87              {
88                  throw new NetworkException("Constructor of CrossSectionElement -- id " + id
89                      + " not unique within the Link");
90              }
91          }
92          this.id = id;
93          this.parentLink = parentLink;
94  
95          this.crossSectionSlices = new ArrayList<>(crossSectionSlices); // copy of list with immutable slices
96          if (this.crossSectionSlices.size() == 0)
97          {
98              throw new NetworkException("CrossSectionElement " + id + " is created with zero slices for " + parentLink);
99          }
100         if (this.crossSectionSlices.get(0).getRelativeLength().si != 0.0)
101         {
102             throw new NetworkException("CrossSectionElement " + id + " for " + parentLink
103                 + " has a first slice with relativeLength is not equal to 0.0");
104         }
105         if (this.crossSectionSlices.size() > 1
106             && this.crossSectionSlices.get(this.crossSectionSlices.size() - 1).getRelativeLength()
107                 .ne(this.parentLink.getLength()))
108         {
109             throw new NetworkException("CrossSectionElement " + id + " for " + parentLink
110                 + " has a last slice with relativeLength is not equal to the length of the parent link");
111         }
112 
113         if (this.crossSectionSlices.size() <= 2)
114         {
115             this.centerLine =
116                 this.getParentLink().getDesignLine()
117                     .offsetLine(getDesignLineOffsetAtBegin().getSI(), getDesignLineOffsetAtEnd().getSI());
118         }
119         else
120         {
121             double[] relativeFractions = new double[this.crossSectionSlices.size()];
122             double[] offsets = new double[this.crossSectionSlices.size()];
123             for (int i = 0; i < this.crossSectionSlices.size(); i++)
124             {
125                 relativeFractions[i] =
126                     this.crossSectionSlices.get(i).getRelativeLength().si / this.parentLink.getLength().si;
127                 offsets[i] = this.crossSectionSlices.get(i).getDesignLineOffset().si;
128             }
129             this.centerLine = this.getParentLink().getDesignLine().offsetLine(relativeFractions, offsets);
130         }
131 
132         this.length = this.centerLine.getLength();
133         this.contour = constructContour(this);
134 
135         this.parentLink.addCrossSectionElement(this);
136         // This commented-out code was used to find the cause of an unexpected lane narrowing in the testod.xodr network.
137         // if (new OTSPolygon3D(this.contour.getPoints()).contains(new OTSPoint3D(-180, -150)))
138         // {
139         // System.out.println("Contour contains p: " + this);
140         // OTSLine3D crossSectionDesignLine =
141         // getParentLink().getDesignLine()
142         // .offsetLine(getDesignLineOffsetAtBegin().getSI(), getDesignLineOffsetAtEnd().getSI());
143         // System.out.println(OTSGeometry.printCoordinates("#reference:\nc0,0,0\n#", crossSectionDesignLine, "\n    "));
144         // OTSLine3D rightBoundary =
145         // crossSectionDesignLine.offsetLine(-getBeginWidth().getSI() / 2, -getEndWidth().getSI() / 2);
146         // System.out.println(OTSGeometry.printCoordinates("#right boundary:\nc0,1,0\n#", rightBoundary, "\n    "));
147         // constructContour(this);
148         // }
149     }
150 
151     // TODO use throwIf
152     
153     /**
154      * <b>Note:</b> LEFT is seen as a positive lateral direction, RIGHT as a negative lateral direction, with the direction from
155      * the StartNode towards the EndNode as the longitudinal direction.
156      * @param id String; The id of the CrosssSectionElement. Should be unique within the parentLink.
157      * @param parentLink CrossSectionLink; Link to which the element belongs.
158      * @param lateralOffsetAtBegin Length; the lateral offset of the design line of the new CrossSectionLink with respect to
159      *            the design line of the parent Link at the start of the parent Link
160      * @param lateralOffsetAtEnd Length; the lateral offset of the design line of the new CrossSectionLink with respect to
161      *            the design line of the parent Link at the end of the parent Link
162      * @param beginWidth Length; width at start, positioned <i>symmetrically around</i> the design line
163      * @param endWidth Length; width at end, positioned <i>symmetrically around</i> the design line
164      * @throws OTSGeometryException when creation of the geometry fails
165      * @throws NetworkException when id equal to null or not unique
166      */
167     public CrossSectionElement(final CrossSectionLink parentLink, final String id,
168         final Length lateralOffsetAtBegin, final Length lateralOffsetAtEnd, final Length beginWidth,
169         final Length endWidth) throws OTSGeometryException, NetworkException
170     {
171         this(parentLink, id, Arrays.asList(new CrossSectionSlice[]{
172             new CrossSectionSlice(Length.ZERO, lateralOffsetAtBegin, beginWidth),
173             new CrossSectionSlice(parentLink.getLength(), lateralOffsetAtEnd, endWidth)}));
174     }
175 
176     /**
177      * <b>Note:</b> LEFT is seen as a positive lateral direction, RIGHT as a negative lateral direction, with the direction from
178      * the StartNode towards the EndNode as the longitudinal direction.
179      * @param id String; The id of the CrosssSectionElement. Should be unique within the parentLink.
180      * @param parentLink CrossSectionLink; Link to which the element belongs.
181      * @param lateralOffset Length; the lateral offset of the design line of the new CrossSectionLink with respect to the
182      *            design line of the parent Link
183      * @param width Length; width, positioned <i>symmetrically around</i> the design line
184      * @throws OTSGeometryException when creation of the geometry fails
185      * @throws NetworkException when id equal to null or not unique
186      */
187     public CrossSectionElement(final CrossSectionLink parentLink, final String id, final Length lateralOffset,
188         final Length width) throws OTSGeometryException, NetworkException
189     {
190         this(parentLink, id, Arrays.asList(new CrossSectionSlice[]{new CrossSectionSlice(Length.ZERO,
191             lateralOffset, width)}));
192     }
193 
194     /**
195      * @return parentLink.
196      */
197     public final CrossSectionLink getParentLink()
198     {
199         return this.parentLink;
200     }
201 
202     /**
203      * Calculate the slice the fractional position is in.
204      * @param fractionalPosition the fractional position between 0 and 1 compared to the design line
205      * @return the lower slice number between 0 and number of slices - 1.
206      */
207     private int calculateSliceNumber(final double fractionalPosition)
208     {
209         double linkLength = this.parentLink.getLength().si;
210         for (int i = 0; i < this.crossSectionSlices.size() - 1; i++)
211         {
212             if (fractionalPosition >= this.crossSectionSlices.get(i).getRelativeLength().si / linkLength
213                 && fractionalPosition <= this.crossSectionSlices.get(i + 1).getRelativeLength().si / linkLength)
214             {
215                 return i;
216             }
217         }
218         return this.crossSectionSlices.size() - 2;
219     }
220 
221     /**
222      * Retrieve the lateral offset from the Link design line at the specified longitudinal position.
223      * @param fractionalPosition double; fractional longitudinal position on this Lane
224      * @return Length the lateralCenterPosition at the specified longitudinal position
225      */
226     public final Length getLateralCenterPosition(final double fractionalPosition)
227     {
228         if (this.crossSectionSlices.size() == 1)
229         {
230             return this.getDesignLineOffsetAtBegin();
231         }
232         if (this.crossSectionSlices.size() == 2)
233         {
234             return Length.interpolate(this.getDesignLineOffsetAtBegin(), this.getDesignLineOffsetAtEnd(),
235                 fractionalPosition);
236         }
237         int sliceNr = calculateSliceNumber(fractionalPosition);
238         return Length.interpolate(this.crossSectionSlices.get(sliceNr).getDesignLineOffset(),
239             this.crossSectionSlices.get(sliceNr + 1).getDesignLineOffset(), fractionalPosition
240                 - this.crossSectionSlices.get(sliceNr).getRelativeLength().si / this.parentLink.getLength().si);
241     }
242 
243     /**
244      * Retrieve the lateral offset from the Link design line at the specified longitudinal position.
245      * @param longitudinalPosition Length; the longitudinal position on this Lane
246      * @return Length the lateralCenterPosition at the specified longitudinal position
247      */
248     public final Length getLateralCenterPosition(final Length longitudinalPosition)
249     {
250         return getLateralCenterPosition(longitudinalPosition.getSI() / getLength().getSI());
251     }
252 
253     /**
254      * Return the width of this CrossSectionElement at a specified longitudinal position.
255      * @param longitudinalPosition DoubleScalar&lt;LengthUnit&gt;; the longitudinal position
256      * @return Length; the width of this CrossSectionElement at the specified longitudinal position.
257      */
258     public final Length getWidth(final Length longitudinalPosition)
259     {
260         return getWidth(longitudinalPosition.getSI() / getLength().getSI());
261     }
262 
263     /**
264      * Return the width of this CrossSectionElement at a specified fractional longitudinal position.
265      * @param fractionalPosition double; the fractional longitudinal position
266      * @return Length; the width of this CrossSectionElement at the specified fractional longitudinal position.
267      */
268     public final Length getWidth(final double fractionalPosition)
269     {
270         if (this.crossSectionSlices.size() == 1)
271         {
272             return this.getBeginWidth();
273         }
274         if (this.crossSectionSlices.size() == 2)
275         {
276             return Length.interpolate(this.getBeginWidth(), this.getEndWidth(), fractionalPosition);
277         }
278         int sliceNr = calculateSliceNumber(fractionalPosition);
279         return Length.interpolate(
280             this.crossSectionSlices.get(sliceNr).getWidth(),
281             this.crossSectionSlices.get(sliceNr + 1).getWidth(),
282             fractionalPosition - this.crossSectionSlices.get(sliceNr).getRelativeLength().si
283                 / this.parentLink.getLength().si);
284     }
285 
286     /**
287      * Return the length of this CrossSectionElement as measured along the design line (which equals the center line).
288      * @return Length; the length of this CrossSectionElement
289      */
290     public final Length getLength()
291     {
292         return this.length;
293     }
294 
295     /**
296      * @return designLineOffsetAtBegin.
297      */
298     public final Length getDesignLineOffsetAtBegin()
299     {
300         return this.crossSectionSlices.get(0).getDesignLineOffset();
301     }
302 
303     /**
304      * @return designLineOffsetAtEnd.
305      */
306     public final Length getDesignLineOffsetAtEnd()
307     {
308         return this.crossSectionSlices.get(this.crossSectionSlices.size() - 1).getDesignLineOffset();
309     }
310 
311     /**
312      * @return beginWidth.
313      */
314     public final Length getBeginWidth()
315     {
316         return this.crossSectionSlices.get(0).getWidth();
317     }
318 
319     /**
320      * @return endWidth.
321      */
322     public final Length getEndWidth()
323     {
324         return this.crossSectionSlices.get(this.crossSectionSlices.size() - 1).getWidth();
325     }
326 
327     /**
328      * @return the z-offset for drawing (what's on top, what's underneath).
329      */
330     protected abstract double getZ();
331 
332     /**
333      * @return centerLine.
334      */
335     public final OTSLine3D getCenterLine()
336     {
337         return this.centerLine;
338     }
339 
340     /**
341      * @return contour.
342      */
343     public final OTSShape getContour()
344     {
345         return this.contour;
346     }
347 
348     /**
349      * @return id
350      */
351     public final String getId()
352     {
353         return this.id;
354     }
355 
356     /**
357      * Return the lateral offset from the design line of the parent Link of the Left or Right boundary of this
358      * CrossSectionElement at the specified fractional longitudinal position.
359      * @param lateralDirection LateralDirectionality; LEFT, or RIGHT
360      * @param fractionalLongitudinalPosition double; ranges from 0.0 (begin of parentLink) to 1.0 (end of parentLink)
361      * @return Length
362      */
363     public final Length getLateralBoundaryPosition(final LateralDirectionality lateralDirection,
364         final double fractionalLongitudinalPosition)
365     {
366         Length designLineOffset;
367         Length halfWidth;
368         if (this.crossSectionSlices.size() <= 2)
369         {
370             designLineOffset =
371                 Length.interpolate(getDesignLineOffsetAtBegin(), getDesignLineOffsetAtEnd(),
372                     fractionalLongitudinalPosition);
373             halfWidth =
374                 Length.interpolate(getBeginWidth(), getEndWidth(), fractionalLongitudinalPosition).multiplyBy(0.5);
375         }
376         else
377         {
378             int sliceNr = calculateSliceNumber(fractionalLongitudinalPosition);
379             double startFractionalPosition =
380                 this.crossSectionSlices.get(sliceNr).getRelativeLength().si / this.parentLink.getLength().si;
381             designLineOffset =
382                 Length.interpolate(this.crossSectionSlices.get(sliceNr).getDesignLineOffset(),
383                     this.crossSectionSlices.get(sliceNr + 1).getDesignLineOffset(), fractionalLongitudinalPosition
384                         - startFractionalPosition);
385             halfWidth =
386                 Length.interpolate(this.crossSectionSlices.get(sliceNr).getWidth(),
387                     this.crossSectionSlices.get(sliceNr + 1).getWidth(),
388                     fractionalLongitudinalPosition - startFractionalPosition).multiplyBy(0.5);
389         }
390 
391         switch (lateralDirection)
392         {
393             case LEFT:
394                 return designLineOffset.minus(halfWidth);
395             case RIGHT:
396                 return designLineOffset.plus(halfWidth);
397             default:
398                 throw new Error("Bad switch on LateralDirectionality " + lateralDirection);
399         }
400     }
401 
402     /**
403      * Return the lateral offset from the design line of the parent Link of the Left or Right boundary of this
404      * CrossSectionElement at the specified longitudinal position.
405      * @param lateralDirection LateralDirectionality; LEFT, or RIGHT
406      * @param longitudinalPosition Length; the position along the length of this CrossSectionElement
407      * @return Length
408      */
409     public final Length getLateralBoundaryPosition(final LateralDirectionality lateralDirection,
410         final Length longitudinalPosition)
411     {
412         return getLateralBoundaryPosition(lateralDirection, longitudinalPosition.getSI() / getLength().getSI());
413     }
414 
415     /**
416      * Construct a buffer geometry by offsetting the linear geometry line with a distance and constructing a so-called "buffer"
417      * around it.
418      * @param cse the CrossSectionElement to construct the contour for
419      * @return the geometry belonging to this CrossSectionElement.
420      * @throws OTSGeometryException when construction of the geometry fails
421      * @throws NetworkException when the resulting contour is degenerate (cannot happen; we hope)
422      */
423     public static OTSShape constructContour(final CrossSectionElement cse) throws OTSGeometryException,
424         NetworkException
425     {
426         OTSPoint3D[] result = null;
427 
428         if (cse.crossSectionSlices.size() <= 2)
429         {
430             OTSLine3D crossSectionDesignLine =
431                 cse.getParentLink().getDesignLine()
432                     .offsetLine(cse.getDesignLineOffsetAtBegin().getSI(), cse.getDesignLineOffsetAtEnd().getSI());
433             OTSLine3D rightBoundary =
434                 crossSectionDesignLine.offsetLine(-cse.getBeginWidth().getSI() / 2, -cse.getEndWidth().getSI() / 2);
435             OTSLine3D leftBoundary =
436                 crossSectionDesignLine.offsetLine(cse.getBeginWidth().getSI() / 2, cse.getEndWidth().getSI() / 2);
437             result = new OTSPoint3D[rightBoundary.size() + leftBoundary.size() + 1];
438             int resultIndex = 0;
439             for (int index = 0; index < rightBoundary.size(); index++)
440             {
441                 result[resultIndex++] = rightBoundary.get(index);
442             }
443             for (int index = leftBoundary.size(); --index >= 0;)
444             {
445                 result[resultIndex++] = leftBoundary.get(index);
446             }
447             result[resultIndex] = rightBoundary.get(0); // close the contour
448         }
449 
450         else
451 
452         {
453             List<OTSPoint3D> resultList = new ArrayList<>();
454             for (int i = 0; i < cse.crossSectionSlices.size() - 1; i++)
455             {
456                 double plLength = cse.getParentLink().getLength().si;
457                 double so = cse.crossSectionSlices.get(i).getDesignLineOffset().si;
458                 double eo = cse.crossSectionSlices.get(i + 1).getDesignLineOffset().si;
459                 double sw2 = cse.crossSectionSlices.get(i).getWidth().si / 2.0;
460                 double ew2 = cse.crossSectionSlices.get(i + 1).getWidth().si / 2.0;
461                 double sf = cse.crossSectionSlices.get(i).getRelativeLength().si / plLength;
462                 double ef = cse.crossSectionSlices.get(i + 1).getRelativeLength().si / plLength;
463                 OTSLine3D crossSectionDesignLine =
464                     cse.getParentLink().getDesignLine().extractFractional(sf, ef).offsetLine(so, eo);
465                 OTSLine3D rightBoundary = crossSectionDesignLine.offsetLine(-sw2, -ew2);
466                 OTSLine3D leftBoundary = crossSectionDesignLine.offsetLine(sw2, ew2);
467                 for (int index = 0; index < rightBoundary.size(); index++)
468                 {
469                     resultList.add(rightBoundary.get(index));
470                 }
471                 for (int index = leftBoundary.size(); --index >= 0;)
472                 {
473                     resultList.add(leftBoundary.get(index));
474                 }
475             }
476             // close the contour if needed
477             resultList.add(resultList.get(0));
478             result = resultList.toArray(new OTSPoint3D[]{});
479         }
480         
481         return OTSShape.createAndCleanOTSShape(result);
482     }
483 
484     /** {@inheritDoc} */
485     @Override
486     @SuppressWarnings("checkstyle:designforextension")
487     public DirectedPoint getLocation()
488     {
489         DirectedPoint centroid = this.contour.getLocation();
490         return new DirectedPoint(centroid.x, centroid.y, getZ());
491     }
492 
493     /** {@inheritDoc} */
494     @Override
495     @SuppressWarnings("checkstyle:designforextension")
496     public Bounds getBounds()
497     {
498         return this.contour.getBounds();
499     }
500 
501     /** {@inheritDoc} */
502     @Override
503     @SuppressWarnings("checkstyle:designforextension")
504     public String toString()
505     {
506         return String.format("CSE offset %.2fm..%.2fm, width %.2fm..%.2fm", getDesignLineOffsetAtBegin().getSI(),
507             getDesignLineOffsetAtEnd().getSI(), getBeginWidth().getSI(), getEndWidth().getSI());
508     }
509 
510     /** {@inheritDoc} */
511     @SuppressWarnings("checkstyle:designforextension")
512     @Override
513     public int hashCode()
514     {
515         final int prime = 31;
516         int result = 1;
517         result = prime * result + ((this.id == null) ? 0 : this.id.hashCode());
518         result = prime * result + ((this.parentLink == null) ? 0 : this.parentLink.hashCode());
519         return result;
520     }
521 
522     /** {@inheritDoc} */
523     @SuppressWarnings({"checkstyle:designforextension", "checkstyle:needbraces"})
524     @Override
525     public boolean equals(final Object obj)
526     {
527         if (this == obj)
528             return true;
529         if (obj == null)
530             return false;
531         if (getClass() != obj.getClass())
532             return false;
533         CrossSectionElement other = (CrossSectionElement) obj;
534         if (this.id == null)
535         {
536             if (other.id != null)
537                 return false;
538         }
539         else if (!this.id.equals(other.id))
540             return false;
541         if (this.parentLink == null)
542         {
543             if (other.parentLink != null)
544                 return false;
545         }
546         else if (!this.parentLink.equals(other.parentLink))
547             return false;
548         return true;
549     }
550 
551 }