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