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.Iterator;
7   import java.util.List;
8   
9   import org.djunits.value.vdouble.scalar.Length;
10  import org.djutils.event.LocalEventProducer;
11  import org.djutils.exceptions.Throw;
12  import org.djutils.exceptions.Try;
13  import org.djutils.logger.CategoryLogger;
14  import org.opentrafficsim.base.Identifiable;
15  import org.opentrafficsim.core.animation.Drawable;
16  import org.opentrafficsim.core.geometry.Bezier;
17  import org.opentrafficsim.core.geometry.Bounds;
18  import org.opentrafficsim.core.geometry.DirectedPoint;
19  import org.opentrafficsim.core.geometry.OtsGeometryException;
20  import org.opentrafficsim.core.geometry.OtsLine3d;
21  import org.opentrafficsim.core.geometry.OtsPoint3d;
22  import org.opentrafficsim.core.geometry.OtsShape;
23  import org.opentrafficsim.core.network.LateralDirectionality;
24  import org.opentrafficsim.core.network.NetworkException;
25  import org.opentrafficsim.road.network.RoadNetwork;
26  
27  import nl.tudelft.simulation.dsol.animation.Locatable;
28  
29  /**
30   * Cross section elements are used to compose a CrossSectionLink.
31   * <p>
32   * Copyright (c) 2013-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
33   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
34   * </p>
35   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
36   * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
37   * @author <a href="https://www.citg.tudelft.nl">Guus Tamminga</a>
38   */
39  public class CrossSectionElement extends LocalEventProducer implements Locatable, Serializable, Identifiable, Drawable
40  {
41      /** */
42      private static final long serialVersionUID = 20150826L;
43  
44      /** The id. Should be unique within the parentLink. */
45      private final String id;
46  
47      /** Cross Section Link to which the element belongs. */
48      @SuppressWarnings("checkstyle:visibilitymodifier")
49      protected final CrossSectionLink parentLink;
50  
51      /** The offsets and widths at positions along the line, relative to the design line of the parent link. */
52      @SuppressWarnings("checkstyle:visibilitymodifier")
53      protected final List<CrossSectionSlice> crossSectionSlices;
54  
55      /** The length of the line. Calculated once at the creation. */
56      @SuppressWarnings("checkstyle:visibilitymodifier")
57      protected final Length length;
58  
59      /** The center line of the element. Calculated once at the creation. */
60      private final OtsLine3d centerLine;
61  
62      /** The contour of the element. Calculated once at the creation. */
63      private final OtsShape contour;
64  
65      /** Maximum direction difference w.r.t. node direction at beginning and end of a CrossSectionElement. */
66      public static final double MAXIMUMDIRECTIONERROR = Math.toRadians(0.1);
67  
68      /**
69       * At what fraction of the first segment will an extra point be inserted if the <code>MAXIMUMDIRECTIONERROR</code> is
70       * exceeded.
71       */
72      public static final double FIXUPPOINTPROPORTION = 1.0 / 3;
73  
74      /**
75       * Construct a new CrossSectionElement. <b>Note:</b> LEFT is seen as a positive lateral direction, RIGHT as a negative
76       * lateral direction, with the direction from the StartNode towards the EndNode as the longitudinal direction.
77       * @param id String; The id of the CrossSectionElement. Should be unique within the parentLink.
78       * @param parentLink CrossSectionLink; Link to which the element belongs.
79       * @param crossSectionSlices List&lt;CrossSectionSlice&gt;; the offsets and widths at positions along the line, relative to
80       *            the design line of the parent link. If there is just one with and offset, there should just be one element in
81       *            the list with Length = 0. If there are more slices, the last one should be at the length of the design line.
82       *            If not, a NetworkException is thrown.
83       * @throws OtsGeometryException when creation of the geometry fails
84       * @throws NetworkException when id equal to null or not unique, or there are multiple slices and the last slice does not
85       *             end at the length of the design line.
86       */
87      public CrossSectionElement(final CrossSectionLink parentLink, final String id,
88              final List<CrossSectionSlice> crossSectionSlices) throws OtsGeometryException, NetworkException
89      {
90          Throw.when(parentLink == null, NetworkException.class,
91                  "Constructor of CrossSectionElement for id %s, parentLink cannot be null", id);
92          Throw.when(id == null, NetworkException.class, "Constructor of CrossSectionElement -- id cannot be null");
93          for (CrossSectionElement cse : parentLink.getCrossSectionElementList())
94          {
95              Throw.when(cse.getId().equals(id), NetworkException.class,
96                      "Constructor of CrossSectionElement -- id %s not unique within the Link", id);
97          }
98          Throw.whenNull(crossSectionSlices, "crossSectionSlices may not be null");
99          this.id = id;
100         this.parentLink = parentLink;
101 
102         this.crossSectionSlices = new ArrayList<>(crossSectionSlices); // copy of list with immutable slices
103         Throw.when(this.crossSectionSlices.size() == 0, NetworkException.class,
104                 "CrossSectionElement %s is created with zero slices for %s", id, parentLink);
105         Throw.when(this.crossSectionSlices.get(0).getRelativeLength().si != 0.0, NetworkException.class,
106                 "CrossSectionElement %s for %s has a first slice with relativeLength is not equal to 0.0", id, parentLink);
107         Throw.when(
108                 this.crossSectionSlices.size() > 1 && this.crossSectionSlices.get(this.crossSectionSlices.size() - 1)
109                         .getRelativeLength().ne(this.parentLink.getLength()),
110                 NetworkException.class, "CrossSectionElement %s for %s has a last slice with relativeLength is not equal "
111                         + "to the length of the parent link",
112                 id, parentLink);
113         OtsLine3d proposedCenterLine = null;
114         if (this.crossSectionSlices.size() <= 2)
115         {
116             proposedCenterLine = fixTightInnerCurve(new double[] {0.0, 1.0},
117                     new double[] {getDesignLineOffsetAtBegin().getSI(), getDesignLineOffsetAtEnd().getSI()});
118         }
119         else
120         {
121             double[] fractions = 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                 fractions[i] = this.crossSectionSlices.get(i).getRelativeLength().si / this.parentLink.getLength().si;
126                 offsets[i] = this.crossSectionSlices.get(i).getDesignLineOffset().si;
127             }
128             proposedCenterLine = fixTightInnerCurve(fractions, offsets);
129         }
130         // Make positions and directions of begin and end of CrossSection exact
131         List<OtsPoint3d> points = new ArrayList<OtsPoint3d>(Arrays.asList(proposedCenterLine.getPoints()));
132         // Make position at begin exact
133         DirectedPoint linkFrom = Try.assign(() -> parentLink.getStartNode().getLocation(), "Cannot happen");
134         double fromDirection = linkFrom.getRotZ();
135         points.remove(0);
136         points.add(0, new OtsPoint3d(linkFrom.x + getDesignLineOffsetAtBegin().getSI() * Math.cos(fromDirection + Math.PI / 2),
137                 linkFrom.y + getDesignLineOffsetAtBegin().getSI() * Math.sin(fromDirection + Math.PI / 2)));
138         // Make position at end exact
139         DirectedPoint linkTo = Try.assign(() -> parentLink.getEndNode().getLocation(), "Cannot happen");
140         double toDirection = linkTo.getRotZ();
141         points.remove(points.size() - 1);
142         points.add(new OtsPoint3d(linkTo.x + getDesignLineOffsetAtEnd().getSI() * Math.cos(toDirection + Math.PI / 2),
143                 linkTo.y + getDesignLineOffsetAtEnd().getSI() * Math.sin(toDirection + Math.PI / 2)));
144         // Check direction at begin
145         double direction = points.get(0).horizontalDirectionSI(points.get(1));
146         OtsPoint3d extraPointAfterStart = null;
147         if (Math.abs(direction - fromDirection) > MAXIMUMDIRECTIONERROR)
148         {
149             // Insert an extra point to ensure that the new CrossSectionElement starts off in the right direction
150             OtsPoint3d from = points.get(0);
151             OtsPoint3d next = points.get(1);
152             double distance =
153                     Math.min(from.horizontalDistanceSI(next) * FIXUPPOINTPROPORTION, crossSectionSlices.get(0).getWidth().si);
154             extraPointAfterStart = new OtsPoint3d(from.x + Math.cos(fromDirection) * distance,
155                     from.y + Math.sin(fromDirection) * distance, from.z + FIXUPPOINTPROPORTION * (next.z - from.z));
156             // Do not insert it yet because that could cause a similar point near the end to be put at the wrong distance
157         }
158         // Check direction at end
159         int pointCount = points.size();
160         direction = points.get(pointCount - 2).horizontalDirectionSI(points.get(pointCount - 1));
161         if (Math.abs(direction - toDirection) > MAXIMUMDIRECTIONERROR)
162         {
163             // Insert an extra point to ensure that the new CrossSectionElement ends in the right direction
164             OtsPoint3d to = points.get(pointCount - 1);
165             OtsPoint3d before = points.get(pointCount - 2);
166             double distance = Math.min(before.horizontalDistanceSI(to) * FIXUPPOINTPROPORTION,
167                     crossSectionSlices.get(Math.max(0, crossSectionSlices.size() - 2)).getWidth().si);
168             points.add(pointCount - 1, new OtsPoint3d(to.x - Math.cos(toDirection) * distance,
169                     to.y - Math.sin(toDirection) * distance, to.z - FIXUPPOINTPROPORTION * (before.z - to.z)));
170         }
171         if (null != extraPointAfterStart)
172         {
173             points.add(1, extraPointAfterStart);
174         }
175         this.centerLine = new OtsLine3d(points);
176         this.length = this.centerLine.getLength();
177         this.contour = constructContour(this);
178         this.parentLink.addCrossSectionElement(this);
179 
180         // clear lane change info cache for each cross section element created
181         parentLink.getNetwork().clearLaneChangeInfoCache();
182     }
183 
184     /**
185      * <b>Note:</b> LEFT is seen as a positive lateral direction, RIGHT as a negative lateral direction, with the direction from
186      * the StartNode towards the EndNode as the longitudinal direction.
187      * @param id String; The id of the CrossSectionElement. Should be unique within the parentLink.
188      * @param parentLink CrossSectionLink; Link to which the element belongs.
189      * @param lateralOffsetAtBegin Length; the lateral offset of the design line of the new CrossSectionLink with respect to the
190      *            design line of the parent Link at the start of the parent Link
191      * @param lateralOffsetAtEnd Length; the lateral offset of the design line of the new CrossSectionLink with respect to the
192      *            design line of the parent Link at the end of the parent Link
193      * @param beginWidth Length; width at start, positioned <i>symmetrically around</i> the design line
194      * @param endWidth Length; width at end, positioned <i>symmetrically around</i> the design line
195      * @param fixGradualLateralOffset boolean; true if gradualLateralOffset needs to be fixed
196      * @throws OtsGeometryException when creation of the geometry fails
197      * @throws NetworkException when id equal to null or not unique
198      */
199     public CrossSectionElement(final CrossSectionLink parentLink, final String id, final Length lateralOffsetAtBegin,
200             final Length lateralOffsetAtEnd, final Length beginWidth, final Length endWidth,
201             final boolean fixGradualLateralOffset) throws OtsGeometryException, NetworkException
202     {
203         this(parentLink, id, fixLateralOffset(parentLink, lateralOffsetAtBegin, lateralOffsetAtEnd, beginWidth, endWidth,
204                 fixGradualLateralOffset));
205     }
206 
207     /**
208      * Construct a list of cross section slices, using sinusoidal interpolation for changing lateral offset.
209      * @param parentLink CrossSectionLink; Link to which the element belongs.
210      * @param lateralOffsetAtBegin Length; the lateral offset of the design line of the new CrossSectionLink with respect to the
211      *            design line of the parent Link at the start of the parent Link
212      * @param lateralOffsetAtEnd Length; the lateral offset of the design line of the new CrossSectionLink with respect to the
213      *            design line of the parent Link at the end of the parent Link
214      * @param beginWidth Length; width at start, positioned <i>symmetrically around</i> the design line
215      * @param endWidth Length; width at end, positioned <i>symmetrically around</i> the design line
216      * @param fixGradualLateralOffset boolean; true if gradualLateralOffset needs to be fixed
217      * @return List&ltCrossSectionSlice&gt;; the cross section slices
218      */
219     private static List<CrossSectionSlice> fixLateralOffset(final CrossSectionLink parentLink,
220             final Length lateralOffsetAtBegin, final Length lateralOffsetAtEnd, final Length beginWidth, final Length endWidth,
221             final boolean fixGradualLateralOffset)
222     {
223         List<CrossSectionSlice> result = new ArrayList<>();
224         int numPoints = !fixGradualLateralOffset ? 2 : lateralOffsetAtBegin.equals(lateralOffsetAtEnd) ? 2 : 16;
225         Length parentLength = parentLink.getLength();
226         for (int index = 0; index < numPoints; index++)
227         {
228             double fraction = index * 1.0 / (numPoints - 1);
229             Length lengthAtCrossSection = parentLength.times(fraction);
230             double relativeOffsetAtFraction = (1 + Math.sin((fraction - 0.5) * Math.PI)) / 2;
231             Length offsetAtFraction = Length.interpolate(lateralOffsetAtBegin, lateralOffsetAtEnd, relativeOffsetAtFraction);
232             result.add(new CrossSectionSlice(lengthAtCrossSection, offsetAtFraction,
233                     Length.interpolate(beginWidth, endWidth, fraction)));
234         }
235         return result;
236     }
237 
238     /**
239      * Returns the center line for this cross section element by adhering to the given offsets relative to the link design line.
240      * This method will create a Bezier curve, ignoring the link design line, if the offset at any vertex is larger than the
241      * radius, and on the inside of the curve.
242      * @param fractions double[]; length fractions of offsets
243      * @param offsets double[]; offsets
244      * @return OtsPoint3d; center line
245      * @throws OtsGeometryException index out of bounds
246      */
247     private OtsLine3d fixTightInnerCurve(final double[] fractions, final double[] offsets) throws OtsGeometryException
248     {
249         OtsLine3d linkCenterLine = getParentLink().getDesignLine();
250         for (int i = 1; i < linkCenterLine.size() - 1; i++)
251         {
252             double fraction = linkCenterLine.getVertexFraction(i);
253             int index = 0;
254             while (index < fractions.length - 2 && fraction > fractions[index + 1])
255             {
256                 index++;
257             }
258             double w = (fraction - fractions[index]) / (fractions[index + 1] - fractions[index]);
259             double offset = (1.0 - w) * offsets[index] + w * offsets[index + 1];
260             double radius = 1.0;
261             try
262             {
263                 radius = linkCenterLine.getProjectedVertexRadius(i).si;
264             }
265             catch (Exception e)
266             {
267                 CategoryLogger.always().error(e, "fixTightInnerCurve.getVertexFraction for " + linkCenterLine);
268             }
269             if ((!Double.isNaN(radius))
270                     && ((radius < 0.0 && offset < 0.0 && offset < radius) || (radius > 0.0 && offset > 0.0 && offset > radius)))
271             {
272                 double offsetStart = getDesignLineOffsetAtBegin().getSI();
273                 double offsetEnd = getDesignLineOffsetAtEnd().getSI();
274                 DirectedPoint start = linkCenterLine.getLocationFraction(0.0);
275                 DirectedPoint end = linkCenterLine.getLocationFraction(1.0);
276                 start = new DirectedPoint(start.x - Math.sin(start.getRotZ()) * offsetStart,
277                         start.y + Math.cos(start.getRotZ()) * offsetStart, start.z, start.getRotX(), start.getRotY(),
278                         start.getRotZ());
279                 end = new DirectedPoint(end.x - Math.sin(end.getRotZ()) * offsetEnd,
280                         end.y + Math.cos(end.getRotZ()) * offsetEnd, end.z, end.getRotX(), end.getRotY(), end.getRotZ());
281                 while (this.crossSectionSlices.size() > 2)
282                 {
283                     this.crossSectionSlices.remove(1);
284                 }
285                 return Bezier.cubic(start, end);
286             }
287         }
288         if (this.crossSectionSlices.size() <= 2)
289         {
290             OtsLine3d designLine = this.getParentLink().getDesignLine();
291             if (designLine.size() > 2)
292             {
293                 // TODO: this produces near-duplicate points on lane 925_J1.FORWARD1 in the Aimsun network
294                 // hack: clean nearby points
295                 OtsLine3d line =
296                         designLine.offsetLine(getDesignLineOffsetAtBegin().getSI(), getDesignLineOffsetAtEnd().getSI());
297                 List<OtsPoint3d> points = new ArrayList<>(Arrays.asList(line.getPoints()));
298                 Iterator<OtsPoint3d> it = points.iterator();
299                 OtsPoint3d prevPoint = null;
300                 while (it.hasNext())
301                 {
302                     OtsPoint3d point = it.next();
303                     if (prevPoint != null && prevPoint.distance(point).si < 1e-4)
304                     {
305                         it.remove();
306                     }
307                     prevPoint = point;
308                 }
309                 return new OtsLine3d(points);
310             }
311             else
312             {
313                 DirectedPoint refStart = getParentLink().getStartNode().getLocation();
314                 double startRot = refStart.getRotZ();
315                 double startOffset = this.crossSectionSlices.get(0).getDesignLineOffset().si;
316                 OtsPoint3d start = new OtsPoint3d(refStart.x - Math.sin(startRot) * startOffset,
317                         refStart.y + Math.cos(startRot) * startOffset, refStart.z);
318                 DirectedPoint refEnd = getParentLink().getEndNode().getLocation();
319                 double endRot = refEnd.getRotZ();
320                 double endOffset = this.crossSectionSlices.get(this.crossSectionSlices.size() - 1).getDesignLineOffset().si;
321                 OtsPoint3d end = new OtsPoint3d(refEnd.x - Math.sin(endRot) * endOffset,
322                         refEnd.y + Math.cos(endRot) * endOffset, refEnd.z);
323                 return new OtsLine3d(start, end);
324             }
325         }
326         else
327         {
328             for (int i = 0; i < this.crossSectionSlices.size(); i++)
329             {
330                 fractions[i] = this.crossSectionSlices.get(i).getRelativeLength().si / this.parentLink.getLength().si;
331                 offsets[i] = this.crossSectionSlices.get(i).getDesignLineOffset().si;
332             }
333             return this.getParentLink().getDesignLine().offsetLine(fractions, offsets);
334         }
335     }
336 
337     /**
338      * <b>Note:</b> LEFT is seen as a positive lateral direction, RIGHT as a negative lateral direction, with the direction from
339      * the StartNode towards the EndNode as the longitudinal direction.
340      * @param id String; The id of the CrosssSectionElement. Should be unique within the parentLink.
341      * @param parentLink CrossSectionLink; Link to which the element belongs.
342      * @param lateralOffset Length; the lateral offset of the design line of the new CrossSectionLink with respect to the design
343      *            line of the parent Link
344      * @param width Length; width, positioned <i>symmetrically around</i> the design line
345      * @throws OtsGeometryException when creation of the geometry fails
346      * @throws NetworkException when id equal to null or not unique
347      */
348     public CrossSectionElement(final CrossSectionLink parentLink, final String id, final Length lateralOffset,
349             final Length width) throws OtsGeometryException, NetworkException
350     {
351         this(parentLink, id, Arrays.asList(new CrossSectionSlice[] {new CrossSectionSlice(Length.ZERO, lateralOffset, width)}));
352     }
353 
354     /**
355      * @return parentLink.
356      */
357     public final CrossSectionLink getParentLink()
358     {
359         return this.parentLink;
360     }
361 
362     /**
363      * @return the road network to which the lane belongs
364      */
365     public final RoadNetwork getNetwork()
366     {
367         return this.parentLink.getNetwork();
368     }
369 
370     /**
371      * Calculate the slice the fractional position is in.
372      * @param fractionalPosition double; the fractional position between 0 and 1 compared to the design line
373      * @return int; the lower slice number between 0 and number of slices - 1.
374      */
375     private int calculateSliceNumber(final double fractionalPosition)
376     {
377         double linkLength = this.parentLink.getLength().si;
378         for (int i = 0; i < this.crossSectionSlices.size() - 1; i++)
379         {
380             if (fractionalPosition >= this.crossSectionSlices.get(i).getRelativeLength().si / linkLength
381                     && fractionalPosition <= this.crossSectionSlices.get(i + 1).getRelativeLength().si / linkLength)
382             {
383                 return i;
384             }
385         }
386         return this.crossSectionSlices.size() - 2;
387     }
388 
389     /**
390      * Retrieve the lateral offset from the Link design line at the specified longitudinal position.
391      * @param fractionalPosition double; fractional longitudinal position on this Lane
392      * @return Length; the lateralCenterPosition at the specified longitudinal position
393      */
394     public final Length getLateralCenterPosition(final double fractionalPosition)
395     {
396         if (this.crossSectionSlices.size() == 1)
397         {
398             return this.getDesignLineOffsetAtBegin();
399         }
400         if (this.crossSectionSlices.size() == 2)
401         {
402             return Length.interpolate(this.getDesignLineOffsetAtBegin(), this.getDesignLineOffsetAtEnd(), fractionalPosition);
403         }
404         int sliceNr = calculateSliceNumber(fractionalPosition);
405         return Length.interpolate(this.crossSectionSlices.get(sliceNr).getDesignLineOffset(),
406                 this.crossSectionSlices.get(sliceNr + 1).getDesignLineOffset(), fractionalPosition
407                         - this.crossSectionSlices.get(sliceNr).getRelativeLength().si / this.parentLink.getLength().si);
408     }
409 
410     /**
411      * Retrieve the lateral offset from the Link design line at the specified longitudinal position.
412      * @param longitudinalPosition Length; the longitudinal position on this Lane
413      * @return Length; the lateralCenterPosition at the specified longitudinal position
414      */
415     public final Length getLateralCenterPosition(final Length longitudinalPosition)
416     {
417         return getLateralCenterPosition(longitudinalPosition.getSI() / getLength().getSI());
418     }
419 
420     /**
421      * Return the width of this CrossSectionElement at a specified longitudinal position.
422      * @param longitudinalPosition Length; the longitudinal position
423      * @return Length; the width of this CrossSectionElement at the specified longitudinal position.
424      */
425     public final Length getWidth(final Length longitudinalPosition)
426     {
427         return getWidth(longitudinalPosition.getSI() / getLength().getSI());
428     }
429 
430     /**
431      * Return the width of this CrossSectionElement at a specified fractional longitudinal position.
432      * @param fractionalPosition double; the fractional longitudinal position
433      * @return Length; the width of this CrossSectionElement at the specified fractional longitudinal position.
434      */
435     public final Length getWidth(final double fractionalPosition)
436     {
437         if (this.crossSectionSlices.size() == 1)
438         {
439             return this.getBeginWidth();
440         }
441         if (this.crossSectionSlices.size() == 2)
442         {
443             return Length.interpolate(this.getBeginWidth(), this.getEndWidth(), fractionalPosition);
444         }
445         int sliceNr = calculateSliceNumber(fractionalPosition);
446         return Length.interpolate(this.crossSectionSlices.get(sliceNr).getWidth(),
447                 this.crossSectionSlices.get(sliceNr + 1).getWidth(), fractionalPosition
448                         - this.crossSectionSlices.get(sliceNr).getRelativeLength().si / this.parentLink.getLength().si);
449     }
450 
451     /**
452      * Return the length of this CrossSectionElement as measured along the design line (which equals the center line).
453      * @return Length; the length of this CrossSectionElement
454      */
455     public final Length getLength()
456     {
457         return this.length;
458     }
459 
460     /**
461      * Retrieve the offset from the design line at the begin of the parent link.
462      * @return Length; the offset of this CrossSectionElement at the begin of the parent link
463      */
464     public final Length getDesignLineOffsetAtBegin()
465     {
466         return this.crossSectionSlices.get(0).getDesignLineOffset();
467     }
468 
469     /**
470      * Retrieve the offset from the design line at the end of the parent link.
471      * @return Length; the offset of this CrossSectionElement at the end of the parent link
472      */
473     public final Length getDesignLineOffsetAtEnd()
474     {
475         return this.crossSectionSlices.get(this.crossSectionSlices.size() - 1).getDesignLineOffset();
476     }
477 
478     /**
479      * Retrieve the width at the begin of the parent link.
480      * @return Length; the width of this CrossSectionElement at the begin of the parent link
481      */
482     public final Length getBeginWidth()
483     {
484         return this.crossSectionSlices.get(0).getWidth();
485     }
486 
487     /**
488      * Retrieve the width at the end of the parent link.
489      * @return Length; the width of this CrossSectionElement at the end of the parent link
490      */
491     public final Length getEndWidth()
492     {
493         return this.crossSectionSlices.get(this.crossSectionSlices.size() - 1).getWidth();
494     }
495 
496     /**
497      * Retrieve the Z offset (used to determine what covers what when drawing).
498      * @return double; the Z-offset for drawing (what's on top, what's underneath).
499      */
500     @Override
501     public double getZ()
502     {
503         // default implementation returns 0.0 in case of a null location or a 2D location
504         return Try.assign(() -> Locatable.super.getZ(), "Remote exception on calling getZ()");
505     }
506 
507     /**
508      * Retrieve the center line of this CrossSectionElement.
509      * @return OtsLine3d; the center line of this CrossSectionElement
510      */
511     public final OtsLine3d getCenterLine()
512     {
513         return this.centerLine;
514     }
515 
516     /**
517      * Retrieve the contour of this CrossSectionElement.
518      * @return OtsShape; the contour of this CrossSectionElement
519      */
520     public final OtsShape getContour()
521     {
522         return this.contour;
523     }
524 
525     /**
526      * Retrieve the id of this CrossSectionElement.
527      * @return String; the id of this CrossSectionElement
528      */
529     @Override
530     public final String getId()
531     {
532         return this.id;
533     }
534 
535     /**
536      * Retrieve the id of this CrossSectionElement.
537      * @return String; the id of this CrossSectionElement
538      */
539     public final String getFullId()
540     {
541         return getParentLink().getId() + "." + this.id;
542     }
543 
544     /**
545      * Return the lateral offset from the design line of the parent Link of the Left or Right boundary of this
546      * CrossSectionElement at the specified fractional longitudinal position.
547      * @param lateralDirection LateralDirectionality; LEFT, or RIGHT
548      * @param fractionalLongitudinalPosition double; ranges from 0.0 (begin of parentLink) to 1.0 (end of parentLink)
549      * @return Length
550      */
551     public final Length getLateralBoundaryPosition(final LateralDirectionality lateralDirection,
552             final double fractionalLongitudinalPosition)
553     {
554         Length designLineOffset;
555         Length halfWidth;
556         if (this.crossSectionSlices.size() <= 2)
557         {
558             designLineOffset = Length.interpolate(getDesignLineOffsetAtBegin(), getDesignLineOffsetAtEnd(),
559                     fractionalLongitudinalPosition);
560             halfWidth = Length.interpolate(getBeginWidth(), getEndWidth(), fractionalLongitudinalPosition).times(0.5);
561         }
562         else
563         {
564             int sliceNr = calculateSliceNumber(fractionalLongitudinalPosition);
565             double startFractionalPosition =
566                     this.crossSectionSlices.get(sliceNr).getRelativeLength().si / this.parentLink.getLength().si;
567             designLineOffset = Length.interpolate(this.crossSectionSlices.get(sliceNr).getDesignLineOffset(),
568                     this.crossSectionSlices.get(sliceNr + 1).getDesignLineOffset(),
569                     fractionalLongitudinalPosition - startFractionalPosition);
570             halfWidth = Length.interpolate(this.crossSectionSlices.get(sliceNr).getWidth(),
571                     this.crossSectionSlices.get(sliceNr + 1).getWidth(),
572                     fractionalLongitudinalPosition - startFractionalPosition).times(0.5);
573         }
574 
575         switch (lateralDirection)
576         {
577             case LEFT:
578                 return designLineOffset.minus(halfWidth);
579             case RIGHT:
580                 return designLineOffset.plus(halfWidth);
581             default:
582                 throw new Error("Bad switch on LateralDirectionality " + lateralDirection);
583         }
584     }
585 
586     /**
587      * Return the lateral offset from the design line of the parent Link of the Left or Right boundary of this
588      * CrossSectionElement at the specified longitudinal position.
589      * @param lateralDirection LateralDirectionality; LEFT, or RIGHT
590      * @param longitudinalPosition Length; the position along the length of this CrossSectionElement
591      * @return Length
592      */
593     public final Length getLateralBoundaryPosition(final LateralDirectionality lateralDirection,
594             final Length longitudinalPosition)
595     {
596         return getLateralBoundaryPosition(lateralDirection, longitudinalPosition.getSI() / getLength().getSI());
597     }
598 
599     /**
600      * Construct a buffer geometry by offsetting the linear geometry line with a distance and constructing a so-called "buffer"
601      * around it.
602      * @param cse CrossSectionElement; the cross section element to construct the contour for
603      * @return OtsShape; the geometry belonging to this CrossSectionElement.
604      * @throws OtsGeometryException when construction of the geometry fails
605      * @throws NetworkException when the resulting contour is degenerate (cannot happen; we hope)
606      */
607     public static OtsShape constructContour(final CrossSectionElement cse) throws OtsGeometryException, NetworkException
608     {
609         OtsPoint3d[] result = null;
610 
611         if (cse.crossSectionSlices.size() <= 2)
612         {
613             OtsLine3d crossSectionDesignLine = cse.centerLine;
614             OtsLine3d rightBoundary =
615                     crossSectionDesignLine.offsetLine(-cse.getBeginWidth().getSI() / 2, -cse.getEndWidth().getSI() / 2);
616             OtsLine3d leftBoundary =
617                     crossSectionDesignLine.offsetLine(cse.getBeginWidth().getSI() / 2, cse.getEndWidth().getSI() / 2);
618             result = new OtsPoint3d[rightBoundary.size() + leftBoundary.size() + 1];
619             int resultIndex = 0;
620             for (int index = 0; index < rightBoundary.size(); index++)
621             {
622                 result[resultIndex++] = rightBoundary.get(index);
623             }
624             for (int index = leftBoundary.size(); --index >= 0;)
625             {
626                 result[resultIndex++] = leftBoundary.get(index);
627             }
628             result[resultIndex] = rightBoundary.get(0); // close the contour
629         }
630         else
631         {
632             List<OtsPoint3d> resultList = new ArrayList<>();
633             List<OtsPoint3d> rightBoundary = new ArrayList<>();
634             for (int i = 0; i < cse.crossSectionSlices.size() - 1; i++)
635             {
636                 double plLength = cse.getParentLink().getLength().si;
637                 double so = cse.crossSectionSlices.get(i).getDesignLineOffset().si;
638                 double eo = cse.crossSectionSlices.get(i + 1).getDesignLineOffset().si;
639                 double sw2 = cse.crossSectionSlices.get(i).getWidth().si / 2.0;
640                 double ew2 = cse.crossSectionSlices.get(i + 1).getWidth().si / 2.0;
641                 double sf = cse.crossSectionSlices.get(i).getRelativeLength().si / plLength;
642                 double ef = cse.crossSectionSlices.get(i + 1).getRelativeLength().si / plLength;
643                 OtsLine3d crossSectionDesignLine =
644                         cse.getParentLink().getDesignLine().extractFractional(sf, ef).offsetLine(so, eo);
645                 resultList.addAll(Arrays.asList(crossSectionDesignLine.offsetLine(-sw2, -ew2).getPoints()));
646                 rightBoundary.addAll(Arrays.asList(crossSectionDesignLine.offsetLine(sw2, ew2).getPoints()));
647             }
648             for (int index = rightBoundary.size(); --index >= 0;)
649             {
650                 resultList.add(rightBoundary.get(index));
651             }
652             // close the contour (might not be needed)
653             resultList.add(resultList.get(0));
654             result = resultList.toArray(new OtsPoint3d[] {});
655         }
656         return OtsShape.createAndCleanOtsShape(result);
657     }
658 
659     /** {@inheritDoc} */
660     @Override
661     @SuppressWarnings("checkstyle:designforextension")
662     public DirectedPoint getLocation()
663     {
664         DirectedPoint centroid = this.contour.getLocation();
665         return new DirectedPoint(centroid.x, centroid.y, getZ());
666     }
667 
668     /** {@inheritDoc} */
669     @Override
670     @SuppressWarnings("checkstyle:designforextension")
671     public Bounds getBounds()
672     {
673         return this.contour.getBounds();
674     }
675 
676     /** {@inheritDoc} */
677     @Override
678     @SuppressWarnings("checkstyle:designforextension")
679     public String toString()
680     {
681         return String.format("CSE offset %.2fm..%.2fm, width %.2fm..%.2fm", getDesignLineOffsetAtBegin().getSI(),
682                 getDesignLineOffsetAtEnd().getSI(), getBeginWidth().getSI(), getEndWidth().getSI());
683     }
684 
685     /** {@inheritDoc} */
686     @Override
687     @SuppressWarnings("checkstyle:designforextension")
688     public int hashCode()
689     {
690         final int prime = 31;
691         int result = 1;
692         result = prime * result + ((this.id == null) ? 0 : this.id.hashCode());
693         result = prime * result + ((this.parentLink == null) ? 0 : this.parentLink.hashCode());
694         return result;
695     }
696 
697     /** {@inheritDoc} */
698     @Override
699     @SuppressWarnings({"checkstyle:designforextension", "checkstyle:needbraces"})
700     public boolean equals(final Object obj)
701     {
702         if (this == obj)
703             return true;
704         if (obj == null)
705             return false;
706         if (getClass() != obj.getClass())
707             return false;
708         CrossSectionElement other = (CrossSectionElement) obj;
709         if (this.id == null)
710         {
711             if (other.id != null)
712                 return false;
713         }
714         else if (!this.id.equals(other.id))
715             return false;
716         if (this.parentLink == null)
717         {
718             if (other.parentLink != null)
719                 return false;
720         }
721         else if (!this.parentLink.equals(other.parentLink))
722             return false;
723         return true;
724     }
725 }