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