CrossSectionElement.java

  1. package org.opentrafficsim.road.network.lane;

  2. import java.io.Serializable;
  3. import java.util.ArrayList;
  4. import java.util.Arrays;
  5. import java.util.Iterator;
  6. import java.util.List;

  7. import org.djunits.value.vdouble.scalar.Length;
  8. import org.djutils.event.LocalEventProducer;
  9. import org.djutils.exceptions.Throw;
  10. import org.djutils.exceptions.Try;
  11. import org.djutils.logger.CategoryLogger;
  12. import org.opentrafficsim.base.Identifiable;
  13. import org.opentrafficsim.core.animation.Drawable;
  14. import org.opentrafficsim.core.geometry.Bezier;
  15. import org.opentrafficsim.core.geometry.Bounds;
  16. import org.opentrafficsim.core.geometry.DirectedPoint;
  17. import org.opentrafficsim.core.geometry.OtsGeometryException;
  18. import org.opentrafficsim.core.geometry.OtsLine3d;
  19. import org.opentrafficsim.core.geometry.OtsPoint3d;
  20. import org.opentrafficsim.core.geometry.OtsShape;
  21. import org.opentrafficsim.core.network.LateralDirectionality;
  22. import org.opentrafficsim.core.network.NetworkException;
  23. import org.opentrafficsim.road.network.RoadNetwork;

  24. import nl.tudelft.simulation.dsol.animation.Locatable;

  25. /**
  26.  * Cross section elements are used to compose a CrossSectionLink.
  27.  * <p>
  28.  * Copyright (c) 2013-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
  29.  * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  30.  * </p>
  31.  * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
  32.  * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
  33.  * @author <a href="https://www.citg.tudelft.nl">Guus Tamminga</a>
  34.  */
  35. public class CrossSectionElement extends LocalEventProducer implements Locatable, Serializable, Identifiable, Drawable
  36. {
  37.     /** */
  38.     private static final long serialVersionUID = 20150826L;

  39.     /** The id. Should be unique within the parentLink. */
  40.     private final String id;

  41.     /** Cross Section Link to which the element belongs. */
  42.     @SuppressWarnings("checkstyle:visibilitymodifier")
  43.     protected final CrossSectionLink parentLink;

  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.     /** The length of the line. Calculated once at the creation. */
  48.     @SuppressWarnings("checkstyle:visibilitymodifier")
  49.     protected final Length length;

  50.     /** The center line of the element. Calculated once at the creation. */
  51.     private final OtsLine3d centerLine;

  52.     /** The contour of the element. Calculated once at the creation. */
  53.     private final OtsShape contour;

  54.     /** Maximum direction difference w.r.t. node direction at beginning and end of a CrossSectionElement. */
  55.     public static final double MAXIMUMDIRECTIONERROR = Math.toRadians(0.1);

  56.     /**
  57.      * At what fraction of the first segment will an extra point be inserted if the <code>MAXIMUMDIRECTIONERROR</code> is
  58.      * exceeded.
  59.      */
  60.     public static final double FIXUPPOINTPROPORTION = 1.0 / 3;

  61.     /**
  62.      * Construct a new CrossSectionElement. <b>Note:</b> LEFT is seen as a positive lateral direction, RIGHT as a negative
  63.      * lateral direction, with the direction from the StartNode towards the EndNode as the longitudinal direction.
  64.      * @param id String; The id of the CrossSectionElement. Should be unique within the parentLink.
  65.      * @param parentLink CrossSectionLink; Link to which the element belongs.
  66.      * @param crossSectionSlices List&lt;CrossSectionSlice&gt;; the offsets and widths at positions along the line, relative to
  67.      *            the design line of the parent link. If there is just one with and offset, there should just be one element in
  68.      *            the list with Length = 0. If there are more slices, the last one should be at the length of the design line.
  69.      *            If not, a NetworkException is thrown.
  70.      * @throws OtsGeometryException when creation of the geometry fails
  71.      * @throws NetworkException when id equal to null or not unique, or there are multiple slices and the last slice does not
  72.      *             end at the length of the design line.
  73.      */
  74.     public CrossSectionElement(final CrossSectionLink parentLink, final String id,
  75.             final List<CrossSectionSlice> crossSectionSlices) throws OtsGeometryException, NetworkException
  76.     {
  77.         Throw.when(parentLink == null, NetworkException.class,
  78.                 "Constructor of CrossSectionElement for id %s, parentLink cannot be null", id);
  79.         Throw.when(id == null, NetworkException.class, "Constructor of CrossSectionElement -- id cannot be null");
  80.         for (CrossSectionElement cse : parentLink.getCrossSectionElementList())
  81.         {
  82.             Throw.when(cse.getId().equals(id), NetworkException.class,
  83.                     "Constructor of CrossSectionElement -- id %s not unique within the Link", id);
  84.         }
  85.         Throw.whenNull(crossSectionSlices, "crossSectionSlices may not be null");
  86.         this.id = id;
  87.         this.parentLink = parentLink;

  88.         this.crossSectionSlices = new ArrayList<>(crossSectionSlices); // copy of list with immutable slices
  89.         Throw.when(this.crossSectionSlices.size() == 0, NetworkException.class,
  90.                 "CrossSectionElement %s is created with zero slices for %s", id, parentLink);
  91.         Throw.when(this.crossSectionSlices.get(0).getRelativeLength().si != 0.0, NetworkException.class,
  92.                 "CrossSectionElement %s for %s has a first slice with relativeLength is not equal to 0.0", id, parentLink);
  93.         Throw.when(
  94.                 this.crossSectionSlices.size() > 1 && this.crossSectionSlices.get(this.crossSectionSlices.size() - 1)
  95.                         .getRelativeLength().ne(this.parentLink.getLength()),
  96.                 NetworkException.class, "CrossSectionElement %s for %s has a last slice with relativeLength is not equal "
  97.                         + "to the length of the parent link",
  98.                 id, parentLink);
  99.         OtsLine3d proposedCenterLine = null;
  100.         if (this.crossSectionSlices.size() <= 2)
  101.         {
  102.             proposedCenterLine = fixTightInnerCurve(new double[] {0.0, 1.0},
  103.                     new double[] {getDesignLineOffsetAtBegin().getSI(), getDesignLineOffsetAtEnd().getSI()});
  104.         }
  105.         else
  106.         {
  107.             double[] fractions = new double[this.crossSectionSlices.size()];
  108.             double[] offsets = new double[this.crossSectionSlices.size()];
  109.             for (int i = 0; i < this.crossSectionSlices.size(); i++)
  110.             {
  111.                 fractions[i] = this.crossSectionSlices.get(i).getRelativeLength().si / this.parentLink.getLength().si;
  112.                 offsets[i] = this.crossSectionSlices.get(i).getDesignLineOffset().si;
  113.             }
  114.             proposedCenterLine = fixTightInnerCurve(fractions, offsets);
  115.         }
  116.         // Make positions and directions of begin and end of CrossSection exact
  117.         List<OtsPoint3d> points = new ArrayList<OtsPoint3d>(Arrays.asList(proposedCenterLine.getPoints()));
  118.         // Make position at begin exact
  119.         DirectedPoint linkFrom = Try.assign(() -> parentLink.getStartNode().getLocation(), "Cannot happen");
  120.         double fromDirection = linkFrom.getRotZ();
  121.         points.remove(0);
  122.         points.add(0, new OtsPoint3d(linkFrom.x + getDesignLineOffsetAtBegin().getSI() * Math.cos(fromDirection + Math.PI / 2),
  123.                 linkFrom.y + getDesignLineOffsetAtBegin().getSI() * Math.sin(fromDirection + Math.PI / 2)));
  124.         // Make position at end exact
  125.         DirectedPoint linkTo = Try.assign(() -> parentLink.getEndNode().getLocation(), "Cannot happen");
  126.         double toDirection = linkTo.getRotZ();
  127.         points.remove(points.size() - 1);
  128.         points.add(new OtsPoint3d(linkTo.x + getDesignLineOffsetAtEnd().getSI() * Math.cos(toDirection + Math.PI / 2),
  129.                 linkTo.y + getDesignLineOffsetAtEnd().getSI() * Math.sin(toDirection + Math.PI / 2)));
  130.         // Check direction at begin
  131.         double direction = points.get(0).horizontalDirectionSI(points.get(1));
  132.         OtsPoint3d extraPointAfterStart = null;
  133.         if (Math.abs(direction - fromDirection) > MAXIMUMDIRECTIONERROR)
  134.         {
  135.             // Insert an extra point to ensure that the new CrossSectionElement starts off in the right direction
  136.             OtsPoint3d from = points.get(0);
  137.             OtsPoint3d next = points.get(1);
  138.             double distance =
  139.                     Math.min(from.horizontalDistanceSI(next) * FIXUPPOINTPROPORTION, crossSectionSlices.get(0).getWidth().si);
  140.             extraPointAfterStart = new OtsPoint3d(from.x + Math.cos(fromDirection) * distance,
  141.                     from.y + Math.sin(fromDirection) * distance, from.z + FIXUPPOINTPROPORTION * (next.z - from.z));
  142.             // Do not insert it yet because that could cause a similar point near the end to be put at the wrong distance
  143.         }
  144.         // Check direction at end
  145.         int pointCount = points.size();
  146.         direction = points.get(pointCount - 2).horizontalDirectionSI(points.get(pointCount - 1));
  147.         if (Math.abs(direction - toDirection) > MAXIMUMDIRECTIONERROR)
  148.         {
  149.             // Insert an extra point to ensure that the new CrossSectionElement ends in the right direction
  150.             OtsPoint3d to = points.get(pointCount - 1);
  151.             OtsPoint3d before = points.get(pointCount - 2);
  152.             double distance = Math.min(before.horizontalDistanceSI(to) * FIXUPPOINTPROPORTION,
  153.                     crossSectionSlices.get(Math.max(0, crossSectionSlices.size() - 2)).getWidth().si);
  154.             points.add(pointCount - 1, new OtsPoint3d(to.x - Math.cos(toDirection) * distance,
  155.                     to.y - Math.sin(toDirection) * distance, to.z - FIXUPPOINTPROPORTION * (before.z - to.z)));
  156.         }
  157.         if (null != extraPointAfterStart)
  158.         {
  159.             points.add(1, extraPointAfterStart);
  160.         }
  161.         this.centerLine = new OtsLine3d(points);
  162.         this.length = this.centerLine.getLength();
  163.         this.contour = constructContour(this);
  164.         this.parentLink.addCrossSectionElement(this);

  165.         // clear lane change info cache for each cross section element created
  166.         parentLink.getNetwork().clearLaneChangeInfoCache();
  167.     }

  168.     /**
  169.      * <b>Note:</b> LEFT is seen as a positive lateral direction, RIGHT as a negative lateral direction, with the direction from
  170.      * the StartNode towards the EndNode as the longitudinal direction.
  171.      * @param id String; The id of the CrossSectionElement. Should be unique within the parentLink.
  172.      * @param parentLink CrossSectionLink; Link to which the element belongs.
  173.      * @param lateralOffsetAtBegin Length; the lateral offset of the design line of the new CrossSectionLink with respect to the
  174.      *            design line of the parent Link at the start of the parent Link
  175.      * @param lateralOffsetAtEnd Length; the lateral offset of the design line of the new CrossSectionLink with respect to the
  176.      *            design line of the parent Link at the end of the parent Link
  177.      * @param beginWidth Length; width at start, positioned <i>symmetrically around</i> the design line
  178.      * @param endWidth Length; width at end, positioned <i>symmetrically around</i> the design line
  179.      * @param fixGradualLateralOffset boolean; true if gradualLateralOffset needs to be fixed
  180.      * @throws OtsGeometryException when creation of the geometry fails
  181.      * @throws NetworkException when id equal to null or not unique
  182.      */
  183.     public CrossSectionElement(final CrossSectionLink parentLink, final String id, final Length lateralOffsetAtBegin,
  184.             final Length lateralOffsetAtEnd, final Length beginWidth, final Length endWidth,
  185.             final boolean fixGradualLateralOffset) throws OtsGeometryException, NetworkException
  186.     {
  187.         this(parentLink, id, fixLateralOffset(parentLink, lateralOffsetAtBegin, lateralOffsetAtEnd, beginWidth, endWidth,
  188.                 fixGradualLateralOffset));
  189.     }

  190.     /**
  191.      * Construct a list of cross section slices, using sinusoidal interpolation for changing lateral offset.
  192.      * @param parentLink CrossSectionLink; Link to which the element belongs.
  193.      * @param lateralOffsetAtBegin 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 start of the parent Link
  195.      * @param lateralOffsetAtEnd Length; the lateral offset of the design line of the new CrossSectionLink with respect to the
  196.      *            design line of the parent Link at the end of the parent Link
  197.      * @param beginWidth Length; width at start, positioned <i>symmetrically around</i> the design line
  198.      * @param endWidth Length; width at end, positioned <i>symmetrically around</i> the design line
  199.      * @param fixGradualLateralOffset boolean; true if gradualLateralOffset needs to be fixed
  200.      * @return List&ltCrossSectionSlice&gt;; the cross section slices
  201.      */
  202.     private static List<CrossSectionSlice> fixLateralOffset(final CrossSectionLink parentLink,
  203.             final Length lateralOffsetAtBegin, final Length lateralOffsetAtEnd, final Length beginWidth, final Length endWidth,
  204.             final boolean fixGradualLateralOffset)
  205.     {
  206.         List<CrossSectionSlice> result = new ArrayList<>();
  207.         int numPoints = !fixGradualLateralOffset ? 2 : lateralOffsetAtBegin.equals(lateralOffsetAtEnd) ? 2 : 16;
  208.         Length parentLength = parentLink.getLength();
  209.         for (int index = 0; index < numPoints; index++)
  210.         {
  211.             double fraction = index * 1.0 / (numPoints - 1);
  212.             Length lengthAtCrossSection = parentLength.times(fraction);
  213.             double relativeOffsetAtFraction = (1 + Math.sin((fraction - 0.5) * Math.PI)) / 2;
  214.             Length offsetAtFraction = Length.interpolate(lateralOffsetAtBegin, lateralOffsetAtEnd, relativeOffsetAtFraction);
  215.             result.add(new CrossSectionSlice(lengthAtCrossSection, offsetAtFraction,
  216.                     Length.interpolate(beginWidth, endWidth, fraction)));
  217.         }
  218.         return result;
  219.     }

  220.     /**
  221.      * Returns the center line for this cross section element by adhering to the given offsets relative to the link design line.
  222.      * This method will create a Bezier curve, ignoring the link design line, if the offset at any vertex is larger than the
  223.      * radius, and on the inside of the curve.
  224.      * @param fractions double[]; length fractions of offsets
  225.      * @param offsets double[]; offsets
  226.      * @return OtsPoint3d; center line
  227.      * @throws OtsGeometryException index out of bounds
  228.      */
  229.     private OtsLine3d fixTightInnerCurve(final double[] fractions, final double[] offsets) throws OtsGeometryException
  230.     {
  231.         OtsLine3d linkCenterLine = getParentLink().getDesignLine();
  232.         for (int i = 1; i < linkCenterLine.size() - 1; i++)
  233.         {
  234.             double fraction = linkCenterLine.getVertexFraction(i);
  235.             int index = 0;
  236.             while (index < fractions.length - 2 && fraction > fractions[index + 1])
  237.             {
  238.                 index++;
  239.             }
  240.             double w = (fraction - fractions[index]) / (fractions[index + 1] - fractions[index]);
  241.             double offset = (1.0 - w) * offsets[index] + w * offsets[index + 1];
  242.             double radius = 1.0;
  243.             try
  244.             {
  245.                 radius = linkCenterLine.getProjectedVertexRadius(i).si;
  246.             }
  247.             catch (Exception e)
  248.             {
  249.                 CategoryLogger.always().error(e, "fixTightInnerCurve.getVertexFraction for " + linkCenterLine);
  250.             }
  251.             if ((!Double.isNaN(radius))
  252.                     && ((radius < 0.0 && offset < 0.0 && offset < radius) || (radius > 0.0 && offset > 0.0 && offset > radius)))
  253.             {
  254.                 double offsetStart = getDesignLineOffsetAtBegin().getSI();
  255.                 double offsetEnd = getDesignLineOffsetAtEnd().getSI();
  256.                 DirectedPoint start = linkCenterLine.getLocationFraction(0.0);
  257.                 DirectedPoint end = linkCenterLine.getLocationFraction(1.0);
  258.                 start = new DirectedPoint(start.x - Math.sin(start.getRotZ()) * offsetStart,
  259.                         start.y + Math.cos(start.getRotZ()) * offsetStart, start.z, start.getRotX(), start.getRotY(),
  260.                         start.getRotZ());
  261.                 end = new DirectedPoint(end.x - Math.sin(end.getRotZ()) * offsetEnd,
  262.                         end.y + Math.cos(end.getRotZ()) * offsetEnd, end.z, end.getRotX(), end.getRotY(), end.getRotZ());
  263.                 while (this.crossSectionSlices.size() > 2)
  264.                 {
  265.                     this.crossSectionSlices.remove(1);
  266.                 }
  267.                 return Bezier.cubic(start, end);
  268.             }
  269.         }
  270.         if (this.crossSectionSlices.size() <= 2)
  271.         {
  272.             OtsLine3d designLine = this.getParentLink().getDesignLine();
  273.             if (designLine.size() > 2)
  274.             {
  275.                 // TODO: this produces near-duplicate points on lane 925_J1.FORWARD1 in the Aimsun network
  276.                 // hack: clean nearby points
  277.                 OtsLine3d line =
  278.                         designLine.offsetLine(getDesignLineOffsetAtBegin().getSI(), getDesignLineOffsetAtEnd().getSI());
  279.                 List<OtsPoint3d> points = new ArrayList<>(Arrays.asList(line.getPoints()));
  280.                 Iterator<OtsPoint3d> it = points.iterator();
  281.                 OtsPoint3d prevPoint = null;
  282.                 while (it.hasNext())
  283.                 {
  284.                     OtsPoint3d point = it.next();
  285.                     if (prevPoint != null && prevPoint.distance(point).si < 1e-4)
  286.                     {
  287.                         it.remove();
  288.                     }
  289.                     prevPoint = point;
  290.                 }
  291.                 return new OtsLine3d(points);
  292.             }
  293.             else
  294.             {
  295.                 DirectedPoint refStart = getParentLink().getStartNode().getLocation();
  296.                 double startRot = refStart.getRotZ();
  297.                 double startOffset = this.crossSectionSlices.get(0).getDesignLineOffset().si;
  298.                 OtsPoint3d start = new OtsPoint3d(refStart.x - Math.sin(startRot) * startOffset,
  299.                         refStart.y + Math.cos(startRot) * startOffset, refStart.z);
  300.                 DirectedPoint refEnd = getParentLink().getEndNode().getLocation();
  301.                 double endRot = refEnd.getRotZ();
  302.                 double endOffset = this.crossSectionSlices.get(this.crossSectionSlices.size() - 1).getDesignLineOffset().si;
  303.                 OtsPoint3d end = new OtsPoint3d(refEnd.x - Math.sin(endRot) * endOffset,
  304.                         refEnd.y + Math.cos(endRot) * endOffset, refEnd.z);
  305.                 return new OtsLine3d(start, end);
  306.             }
  307.         }
  308.         else
  309.         {
  310.             for (int i = 0; i < this.crossSectionSlices.size(); i++)
  311.             {
  312.                 fractions[i] = this.crossSectionSlices.get(i).getRelativeLength().si / this.parentLink.getLength().si;
  313.                 offsets[i] = this.crossSectionSlices.get(i).getDesignLineOffset().si;
  314.             }
  315.             return this.getParentLink().getDesignLine().offsetLine(fractions, offsets);
  316.         }
  317.     }

  318.     /**
  319.      * <b>Note:</b> LEFT is seen as a positive lateral direction, RIGHT as a negative lateral direction, with the direction from
  320.      * the StartNode towards the EndNode as the longitudinal direction.
  321.      * @param id String; The id of the CrosssSectionElement. Should be unique within the parentLink.
  322.      * @param parentLink CrossSectionLink; Link to which the element belongs.
  323.      * @param lateralOffset Length; the lateral offset of the design line of the new CrossSectionLink with respect to the design
  324.      *            line of the parent Link
  325.      * @param width Length; width, positioned <i>symmetrically around</i> the design line
  326.      * @throws OtsGeometryException when creation of the geometry fails
  327.      * @throws NetworkException when id equal to null or not unique
  328.      */
  329.     public CrossSectionElement(final CrossSectionLink parentLink, final String id, final Length lateralOffset,
  330.             final Length width) throws OtsGeometryException, NetworkException
  331.     {
  332.         this(parentLink, id, Arrays.asList(new CrossSectionSlice[] {new CrossSectionSlice(Length.ZERO, lateralOffset, width)}));
  333.     }

  334.     /**
  335.      * @return parentLink.
  336.      */
  337.     public final CrossSectionLink getParentLink()
  338.     {
  339.         return this.parentLink;
  340.     }

  341.     /**
  342.      * @return the road network to which the lane belongs
  343.      */
  344.     public final RoadNetwork getNetwork()
  345.     {
  346.         return this.parentLink.getNetwork();
  347.     }

  348.     /**
  349.      * Calculate the slice the fractional position is in.
  350.      * @param fractionalPosition double; the fractional position between 0 and 1 compared to the design line
  351.      * @return int; the lower slice number between 0 and number of slices - 1.
  352.      */
  353.     private int calculateSliceNumber(final double fractionalPosition)
  354.     {
  355.         double linkLength = this.parentLink.getLength().si;
  356.         for (int i = 0; i < this.crossSectionSlices.size() - 1; i++)
  357.         {
  358.             if (fractionalPosition >= this.crossSectionSlices.get(i).getRelativeLength().si / linkLength
  359.                     && fractionalPosition <= this.crossSectionSlices.get(i + 1).getRelativeLength().si / linkLength)
  360.             {
  361.                 return i;
  362.             }
  363.         }
  364.         return this.crossSectionSlices.size() - 2;
  365.     }

  366.     /**
  367.      * Retrieve the lateral offset from the Link design line at the specified longitudinal position.
  368.      * @param fractionalPosition double; fractional longitudinal position on this Lane
  369.      * @return Length; the lateralCenterPosition at the specified longitudinal position
  370.      */
  371.     public final Length getLateralCenterPosition(final double fractionalPosition)
  372.     {
  373.         if (this.crossSectionSlices.size() == 1)
  374.         {
  375.             return this.getDesignLineOffsetAtBegin();
  376.         }
  377.         if (this.crossSectionSlices.size() == 2)
  378.         {
  379.             return Length.interpolate(this.getDesignLineOffsetAtBegin(), this.getDesignLineOffsetAtEnd(), fractionalPosition);
  380.         }
  381.         int sliceNr = calculateSliceNumber(fractionalPosition);
  382.         return Length.interpolate(this.crossSectionSlices.get(sliceNr).getDesignLineOffset(),
  383.                 this.crossSectionSlices.get(sliceNr + 1).getDesignLineOffset(), fractionalPosition
  384.                         - this.crossSectionSlices.get(sliceNr).getRelativeLength().si / this.parentLink.getLength().si);
  385.     }

  386.     /**
  387.      * Retrieve the lateral offset from the Link design line at the specified longitudinal position.
  388.      * @param longitudinalPosition Length; the longitudinal position on this Lane
  389.      * @return Length; the lateralCenterPosition at the specified longitudinal position
  390.      */
  391.     public final Length getLateralCenterPosition(final Length longitudinalPosition)
  392.     {
  393.         return getLateralCenterPosition(longitudinalPosition.getSI() / getLength().getSI());
  394.     }

  395.     /**
  396.      * Return the width of this CrossSectionElement at a specified longitudinal position.
  397.      * @param longitudinalPosition Length; the longitudinal position
  398.      * @return Length; the width of this CrossSectionElement at the specified longitudinal position.
  399.      */
  400.     public final Length getWidth(final Length longitudinalPosition)
  401.     {
  402.         return getWidth(longitudinalPosition.getSI() / getLength().getSI());
  403.     }

  404.     /**
  405.      * Return the width of this CrossSectionElement at a specified fractional longitudinal position.
  406.      * @param fractionalPosition double; the fractional longitudinal position
  407.      * @return Length; the width of this CrossSectionElement at the specified fractional longitudinal position.
  408.      */
  409.     public final Length getWidth(final double fractionalPosition)
  410.     {
  411.         if (this.crossSectionSlices.size() == 1)
  412.         {
  413.             return this.getBeginWidth();
  414.         }
  415.         if (this.crossSectionSlices.size() == 2)
  416.         {
  417.             return Length.interpolate(this.getBeginWidth(), this.getEndWidth(), fractionalPosition);
  418.         }
  419.         int sliceNr = calculateSliceNumber(fractionalPosition);
  420.         return Length.interpolate(this.crossSectionSlices.get(sliceNr).getWidth(),
  421.                 this.crossSectionSlices.get(sliceNr + 1).getWidth(), fractionalPosition
  422.                         - this.crossSectionSlices.get(sliceNr).getRelativeLength().si / this.parentLink.getLength().si);
  423.     }

  424.     /**
  425.      * Return the length of this CrossSectionElement as measured along the design line (which equals the center line).
  426.      * @return Length; the length of this CrossSectionElement
  427.      */
  428.     public final Length getLength()
  429.     {
  430.         return this.length;
  431.     }

  432.     /**
  433.      * Retrieve the offset from the design line at the begin of the parent link.
  434.      * @return Length; the offset of this CrossSectionElement at the begin of the parent link
  435.      */
  436.     public final Length getDesignLineOffsetAtBegin()
  437.     {
  438.         return this.crossSectionSlices.get(0).getDesignLineOffset();
  439.     }

  440.     /**
  441.      * Retrieve the offset from the design line at the end of the parent link.
  442.      * @return Length; the offset of this CrossSectionElement at the end of the parent link
  443.      */
  444.     public final Length getDesignLineOffsetAtEnd()
  445.     {
  446.         return this.crossSectionSlices.get(this.crossSectionSlices.size() - 1).getDesignLineOffset();
  447.     }

  448.     /**
  449.      * Retrieve the width at the begin of the parent link.
  450.      * @return Length; the width of this CrossSectionElement at the begin of the parent link
  451.      */
  452.     public final Length getBeginWidth()
  453.     {
  454.         return this.crossSectionSlices.get(0).getWidth();
  455.     }

  456.     /**
  457.      * Retrieve the width at the end of the parent link.
  458.      * @return Length; the width of this CrossSectionElement at the end of the parent link
  459.      */
  460.     public final Length getEndWidth()
  461.     {
  462.         return this.crossSectionSlices.get(this.crossSectionSlices.size() - 1).getWidth();
  463.     }

  464.     /**
  465.      * Retrieve the Z offset (used to determine what covers what when drawing).
  466.      * @return double; the Z-offset for drawing (what's on top, what's underneath).
  467.      */
  468.     @Override
  469.     public double getZ()
  470.     {
  471.         // default implementation returns 0.0 in case of a null location or a 2D location
  472.         return Try.assign(() -> Locatable.super.getZ(), "Remote exception on calling getZ()");
  473.     }

  474.     /**
  475.      * Retrieve the center line of this CrossSectionElement.
  476.      * @return OtsLine3d; the center line of this CrossSectionElement
  477.      */
  478.     public final OtsLine3d getCenterLine()
  479.     {
  480.         return this.centerLine;
  481.     }

  482.     /**
  483.      * Retrieve the contour of this CrossSectionElement.
  484.      * @return OtsShape; the contour of this CrossSectionElement
  485.      */
  486.     public final OtsShape getContour()
  487.     {
  488.         return this.contour;
  489.     }

  490.     /**
  491.      * Retrieve the id of this CrossSectionElement.
  492.      * @return String; the id of this CrossSectionElement
  493.      */
  494.     @Override
  495.     public final String getId()
  496.     {
  497.         return this.id;
  498.     }

  499.     /**
  500.      * Retrieve the id of this CrossSectionElement.
  501.      * @return String; the id of this CrossSectionElement
  502.      */
  503.     public final String getFullId()
  504.     {
  505.         return getParentLink().getId() + "." + this.id;
  506.     }

  507.     /**
  508.      * Return the lateral offset from the design line of the parent Link of the Left or Right boundary of this
  509.      * CrossSectionElement at the specified fractional longitudinal position.
  510.      * @param lateralDirection LateralDirectionality; LEFT, or RIGHT
  511.      * @param fractionalLongitudinalPosition double; ranges from 0.0 (begin of parentLink) to 1.0 (end of parentLink)
  512.      * @return Length
  513.      */
  514.     public final Length getLateralBoundaryPosition(final LateralDirectionality lateralDirection,
  515.             final double fractionalLongitudinalPosition)
  516.     {
  517.         Length designLineOffset;
  518.         Length halfWidth;
  519.         if (this.crossSectionSlices.size() <= 2)
  520.         {
  521.             designLineOffset = Length.interpolate(getDesignLineOffsetAtBegin(), getDesignLineOffsetAtEnd(),
  522.                     fractionalLongitudinalPosition);
  523.             halfWidth = Length.interpolate(getBeginWidth(), getEndWidth(), fractionalLongitudinalPosition).times(0.5);
  524.         }
  525.         else
  526.         {
  527.             int sliceNr = calculateSliceNumber(fractionalLongitudinalPosition);
  528.             double startFractionalPosition =
  529.                     this.crossSectionSlices.get(sliceNr).getRelativeLength().si / this.parentLink.getLength().si;
  530.             designLineOffset = Length.interpolate(this.crossSectionSlices.get(sliceNr).getDesignLineOffset(),
  531.                     this.crossSectionSlices.get(sliceNr + 1).getDesignLineOffset(),
  532.                     fractionalLongitudinalPosition - startFractionalPosition);
  533.             halfWidth = Length.interpolate(this.crossSectionSlices.get(sliceNr).getWidth(),
  534.                     this.crossSectionSlices.get(sliceNr + 1).getWidth(),
  535.                     fractionalLongitudinalPosition - startFractionalPosition).times(0.5);
  536.         }

  537.         switch (lateralDirection)
  538.         {
  539.             case LEFT:
  540.                 return designLineOffset.minus(halfWidth);
  541.             case RIGHT:
  542.                 return designLineOffset.plus(halfWidth);
  543.             default:
  544.                 throw new Error("Bad switch on LateralDirectionality " + lateralDirection);
  545.         }
  546.     }

  547.     /**
  548.      * Return the lateral offset from the design line of the parent Link of the Left or Right boundary of this
  549.      * CrossSectionElement at the specified longitudinal position.
  550.      * @param lateralDirection LateralDirectionality; LEFT, or RIGHT
  551.      * @param longitudinalPosition Length; the position along the length of this CrossSectionElement
  552.      * @return Length
  553.      */
  554.     public final Length getLateralBoundaryPosition(final LateralDirectionality lateralDirection,
  555.             final Length longitudinalPosition)
  556.     {
  557.         return getLateralBoundaryPosition(lateralDirection, longitudinalPosition.getSI() / getLength().getSI());
  558.     }

  559.     /**
  560.      * Construct a buffer geometry by offsetting the linear geometry line with a distance and constructing a so-called "buffer"
  561.      * around it.
  562.      * @param cse CrossSectionElement; the cross section element to construct the contour for
  563.      * @return OtsShape; the geometry belonging to this CrossSectionElement.
  564.      * @throws OtsGeometryException when construction of the geometry fails
  565.      * @throws NetworkException when the resulting contour is degenerate (cannot happen; we hope)
  566.      */
  567.     public static OtsShape constructContour(final CrossSectionElement cse) throws OtsGeometryException, NetworkException
  568.     {
  569.         OtsPoint3d[] result = null;

  570.         if (cse.crossSectionSlices.size() <= 2)
  571.         {
  572.             OtsLine3d crossSectionDesignLine = cse.centerLine;
  573.             OtsLine3d rightBoundary =
  574.                     crossSectionDesignLine.offsetLine(-cse.getBeginWidth().getSI() / 2, -cse.getEndWidth().getSI() / 2);
  575.             OtsLine3d leftBoundary =
  576.                     crossSectionDesignLine.offsetLine(cse.getBeginWidth().getSI() / 2, cse.getEndWidth().getSI() / 2);
  577.             result = new OtsPoint3d[rightBoundary.size() + leftBoundary.size() + 1];
  578.             int resultIndex = 0;
  579.             for (int index = 0; index < rightBoundary.size(); index++)
  580.             {
  581.                 result[resultIndex++] = rightBoundary.get(index);
  582.             }
  583.             for (int index = leftBoundary.size(); --index >= 0;)
  584.             {
  585.                 result[resultIndex++] = leftBoundary.get(index);
  586.             }
  587.             result[resultIndex] = rightBoundary.get(0); // close the contour
  588.         }
  589.         else
  590.         {
  591.             List<OtsPoint3d> resultList = new ArrayList<>();
  592.             List<OtsPoint3d> rightBoundary = new ArrayList<>();
  593.             for (int i = 0; i < cse.crossSectionSlices.size() - 1; i++)
  594.             {
  595.                 double plLength = cse.getParentLink().getLength().si;
  596.                 double so = cse.crossSectionSlices.get(i).getDesignLineOffset().si;
  597.                 double eo = cse.crossSectionSlices.get(i + 1).getDesignLineOffset().si;
  598.                 double sw2 = cse.crossSectionSlices.get(i).getWidth().si / 2.0;
  599.                 double ew2 = cse.crossSectionSlices.get(i + 1).getWidth().si / 2.0;
  600.                 double sf = cse.crossSectionSlices.get(i).getRelativeLength().si / plLength;
  601.                 double ef = cse.crossSectionSlices.get(i + 1).getRelativeLength().si / plLength;
  602.                 OtsLine3d crossSectionDesignLine =
  603.                         cse.getParentLink().getDesignLine().extractFractional(sf, ef).offsetLine(so, eo);
  604.                 resultList.addAll(Arrays.asList(crossSectionDesignLine.offsetLine(-sw2, -ew2).getPoints()));
  605.                 rightBoundary.addAll(Arrays.asList(crossSectionDesignLine.offsetLine(sw2, ew2).getPoints()));
  606.             }
  607.             for (int index = rightBoundary.size(); --index >= 0;)
  608.             {
  609.                 resultList.add(rightBoundary.get(index));
  610.             }
  611.             // close the contour (might not be needed)
  612.             resultList.add(resultList.get(0));
  613.             result = resultList.toArray(new OtsPoint3d[] {});
  614.         }
  615.         return OtsShape.createAndCleanOtsShape(result);
  616.     }

  617.     /** {@inheritDoc} */
  618.     @Override
  619.     @SuppressWarnings("checkstyle:designforextension")
  620.     public DirectedPoint getLocation()
  621.     {
  622.         DirectedPoint centroid = this.contour.getLocation();
  623.         return new DirectedPoint(centroid.x, centroid.y, getZ());
  624.     }

  625.     /** {@inheritDoc} */
  626.     @Override
  627.     @SuppressWarnings("checkstyle:designforextension")
  628.     public Bounds getBounds()
  629.     {
  630.         return this.contour.getBounds();
  631.     }

  632.     /** {@inheritDoc} */
  633.     @Override
  634.     @SuppressWarnings("checkstyle:designforextension")
  635.     public String toString()
  636.     {
  637.         return String.format("CSE offset %.2fm..%.2fm, width %.2fm..%.2fm", getDesignLineOffsetAtBegin().getSI(),
  638.                 getDesignLineOffsetAtEnd().getSI(), getBeginWidth().getSI(), getEndWidth().getSI());
  639.     }

  640.     /** {@inheritDoc} */
  641.     @Override
  642.     @SuppressWarnings("checkstyle:designforextension")
  643.     public int hashCode()
  644.     {
  645.         final int prime = 31;
  646.         int result = 1;
  647.         result = prime * result + ((this.id == null) ? 0 : this.id.hashCode());
  648.         result = prime * result + ((this.parentLink == null) ? 0 : this.parentLink.hashCode());
  649.         return result;
  650.     }

  651.     /** {@inheritDoc} */
  652.     @Override
  653.     @SuppressWarnings({"checkstyle:designforextension", "checkstyle:needbraces"})
  654.     public boolean equals(final Object obj)
  655.     {
  656.         if (this == obj)
  657.             return true;
  658.         if (obj == null)
  659.             return false;
  660.         if (getClass() != obj.getClass())
  661.             return false;
  662.         CrossSectionElement other = (CrossSectionElement) obj;
  663.         if (this.id == null)
  664.         {
  665.             if (other.id != null)
  666.                 return false;
  667.         }
  668.         else if (!this.id.equals(other.id))
  669.             return false;
  670.         if (this.parentLink == null)
  671.         {
  672.             if (other.parentLink != null)
  673.                 return false;
  674.         }
  675.         else if (!this.parentLink.equals(other.parentLink))
  676.             return false;
  677.         return true;
  678.     }
  679. }