LaneChange.java

  1. package org.opentrafficsim.road.gtu.lane.plan.operational;

  2. import java.io.Serializable;
  3. import java.util.ArrayList;
  4. import java.util.Iterator;
  5. import java.util.List;
  6. import java.util.Map;
  7. import java.util.Set;

  8. import org.djunits.value.vdouble.scalar.Acceleration;
  9. import org.djunits.value.vdouble.scalar.Duration;
  10. import org.djunits.value.vdouble.scalar.Length;
  11. import org.djunits.value.vdouble.scalar.Speed;
  12. import org.djutils.draw.point.OrientedPoint2d;
  13. import org.djutils.draw.point.Point2d;
  14. import org.djutils.exceptions.Throw;
  15. import org.djutils.exceptions.Try;
  16. import org.opentrafficsim.base.geometry.OtsGeometryException;
  17. import org.opentrafficsim.base.geometry.OtsLine2d;
  18. import org.opentrafficsim.base.geometry.OtsLine2d.FractionalFallback;
  19. import org.opentrafficsim.base.parameters.ParameterTypes;
  20. import org.opentrafficsim.core.geometry.Bezier;
  21. import org.opentrafficsim.core.gtu.GtuException;
  22. import org.opentrafficsim.core.gtu.perception.EgoPerception;
  23. import org.opentrafficsim.core.gtu.plan.operational.OperationalPlanException;
  24. import org.opentrafficsim.core.network.LateralDirectionality;
  25. import org.opentrafficsim.road.gtu.lane.LaneBasedGtu;
  26. import org.opentrafficsim.road.gtu.lane.perception.RelativeLane;
  27. import org.opentrafficsim.road.gtu.lane.perception.headway.Headway;
  28. import org.opentrafficsim.road.gtu.lane.tactical.LaneBasedTacticalPlanner;
  29. import org.opentrafficsim.road.network.lane.Lane;
  30. import org.opentrafficsim.road.network.lane.LanePosition;
  31. import org.opentrafficsim.road.network.lane.object.detector.LaneDetector;
  32. import org.opentrafficsim.road.network.lane.object.detector.SinkDetector;

  33. import nl.tudelft.simulation.dsol.SimRuntimeException;

  34. /**
  35.  * Lane change status across operational plans. This class allows lane based tactical planners to perform lane changes without
  36.  * having to deal with many complexities concerning paths and lane registration. The main purpose of the tactical planner is to
  37.  * request a path using {@code getPath()} for each step of the tactical planner.
  38.  * <p>
  39.  * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
  40.  * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  41.  * </p>
  42.  * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
  43.  * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
  44.  * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
  45.  */
  46. public class LaneChange implements Serializable
  47. {

  48.     /** */
  49.     private static final long serialVersionUID = 20160811L;

  50.     /** Total lane change duration. */
  51.     private Duration desiredLaneChangeDuration;

  52.     /** Fraction of lane change had. */
  53.     private double fraction;

  54.     /** Boundary length within which the lane change should be performed. */
  55.     private Length boundary;

  56.     /** Lane change path. */
  57.     private LaneChangePath laneChangePath = LaneChangePath.SINE_INTERP;

  58.     /** Whether the GTU is changing lane. */
  59.     private LateralDirectionality laneChangeDirectionality = null;

  60.     /** Minimum lane change distance. */
  61.     private final Length minimumLaneChangeDistance;

  62.     /** Instance to invoke static method through scheduled event on. */
  63.     private static final LaneOperationalPlanBuilder BUILDER = new LaneOperationalPlanBuilder();

  64.     /** Minimum distance required to perform a lane change as factor on vehicle length. */
  65.     public static double MIN_LC_LENGTH_FACTOR = 1.5;

  66.     /**
  67.      * Constructor.
  68.      * @param gtu gtu
  69.      */
  70.     public LaneChange(final LaneBasedGtu gtu)
  71.     {
  72.         this.minimumLaneChangeDistance = gtu.getLength().times(MIN_LC_LENGTH_FACTOR);
  73.     }

  74.     /**
  75.      * Constructor.
  76.      * @param minimumLaneChangeDistance minimum lane change distance
  77.      * @param desiredLaneChangeDuration deaired lane change duration
  78.      */
  79.     public LaneChange(final Length minimumLaneChangeDistance, final Duration desiredLaneChangeDuration)
  80.     {
  81.         this.minimumLaneChangeDistance = minimumLaneChangeDistance;
  82.         this.desiredLaneChangeDuration = desiredLaneChangeDuration;
  83.     }

  84.     /**
  85.      * Returns the minimum lane change distance.
  86.      * @return minimum lane change distance
  87.      */
  88.     public Length getMinimumLaneChangeDistance()
  89.     {
  90.         return this.minimumLaneChangeDistance;
  91.     }

  92.     /**
  93.      * Sets the desired lane change duration. Should be set by a tactical planner.
  94.      * @param duration desired lane change duration
  95.      */
  96.     public void setDesiredLaneChangeDuration(final Duration duration)
  97.     {
  98.         this.desiredLaneChangeDuration = duration;
  99.     }

  100.     /**
  101.      * Sets the distance within which a lane change should be finished. Should be set by a tactical planner. In case of a single
  102.      * lane change required before some point, this is not required as the found center line length is intrinsically limited.
  103.      * For multiple lane changes being required, space after a lane change is required.
  104.      * @param boundary boundary
  105.      */
  106.     public void setBoundary(final Length boundary)
  107.     {
  108.         this.boundary = boundary;
  109.     }

  110.     /**
  111.      * Returns the fraction of the lane change performed.
  112.      * @return fraction of lane change performed
  113.      */
  114.     public double getFraction()
  115.     {
  116.         return this.fraction;
  117.     }

  118.     /**
  119.      * Sets a lane change path.
  120.      * @param laneChangePath lane change path
  121.      */
  122.     public void setLaneChangePath(final LaneChangePath laneChangePath)
  123.     {
  124.         this.laneChangePath = laneChangePath;
  125.     }

  126.     /**
  127.      * Return whether the GTU is changing lane.
  128.      * @return whether the GTU is changing lane
  129.      */
  130.     public final boolean isChangingLane()
  131.     {
  132.         return this.laneChangeDirectionality != null;
  133.     }

  134.     /**
  135.      * Return whether the GTU is changing left.
  136.      * @return whether the GTU is changing left
  137.      */
  138.     public final boolean isChangingLeft()
  139.     {
  140.         return LateralDirectionality.LEFT.equals(this.laneChangeDirectionality);
  141.     }

  142.     /**
  143.      * Return whether the GTU is changing right.
  144.      * @return whether the GTU is changing right
  145.      */
  146.     public final boolean isChangingRight()
  147.     {
  148.         return LateralDirectionality.RIGHT.equals(this.laneChangeDirectionality);
  149.     }

  150.     /**
  151.      * Return lateral lane change direction.
  152.      * @return lateral lane change direction
  153.      */
  154.     public final LateralDirectionality getDirection()
  155.     {
  156.         return this.laneChangeDirectionality;
  157.     }

  158.     /**
  159.      * Second lane of lane change relative to the reference lane. Note that the reference lane may either be the source or the
  160.      * target lane. Thus, the second lane during a lane change may either be the left or right lane, regardless of the lane
  161.      * change direction.
  162.      * @param gtu the GTU
  163.      * @return target lane of lane change
  164.      * @throws OperationalPlanException If no lane change is being performed.
  165.      */
  166.     public final RelativeLane getSecondLane(final LaneBasedGtu gtu) throws OperationalPlanException
  167.     {
  168.         Throw.when(!isChangingLane(), OperationalPlanException.class,
  169.                 "Target lane is requested, but no lane change is being performed.");
  170.         Map<Lane, Length> map;
  171.         LanePosition dlp;
  172.         try
  173.         {
  174.             map = gtu.positions(gtu.getReference());
  175.             dlp = gtu.getReferencePosition();
  176.         }
  177.         catch (GtuException exception)
  178.         {
  179.             throw new OperationalPlanException("Second lane of lane change could not be determined.", exception);
  180.         }
  181.         Set<Lane> accessibleLanes = dlp.lane().accessibleAdjacentLanesPhysical(this.laneChangeDirectionality, gtu.getType());
  182.         if (!accessibleLanes.isEmpty() && map.containsKey(accessibleLanes.iterator().next()))
  183.         {
  184.             return isChangingLeft() ? RelativeLane.LEFT : RelativeLane.RIGHT;
  185.         }
  186.         return isChangingLeft() ? RelativeLane.RIGHT : RelativeLane.LEFT;
  187.     }

  188.     /**
  189.      * Returns the path for a lane change. Lane change initialization and finalization events are automatically performed.
  190.      * @param timeStep plan time step
  191.      * @param gtu gtu
  192.      * @param from current position on the from lane (i.e. not necessarily the reference position)
  193.      * @param startPosition current position in 2D
  194.      * @param planDistance absolute distance that will be covered during the time step
  195.      * @param laneChangeDirection lane change direction
  196.      * @return path
  197.      */
  198.     public final OtsLine2d getPath(final Duration timeStep, final LaneBasedGtu gtu, final LanePosition from,
  199.             final OrientedPoint2d startPosition, final Length planDistance, final LateralDirectionality laneChangeDirection)
  200.     {
  201.         // initiate lane change
  202.         boolean favoured = false;
  203.         if (!isChangingLane())
  204.         {
  205.             favoured = true;
  206.             this.laneChangeDirectionality = laneChangeDirection;
  207.             Try.execute(() -> gtu.initLaneChange(laneChangeDirection), "Error during lane change initialization.");
  208.         }
  209.         // Wouter: why would we ever not favor a side during a lane change
  210.         favoured = true;

  211.         // determine longitudinal distance along the from lanes
  212.         /*
  213.          * We take 3 factors in to account. The first two are 1) minimum physical lane change length, and 2) desired lane change
  214.          * duration. With the current mean speed of the plan, we take the maximum. So at very low speeds, the minimum physical
  215.          * length may increase the lane change duration. We also have 3) the maximum length before a lane change needs to have
  216.          * been performed. To overcome simulation troubles, we allow this to result in an even shorter length than the minimum
  217.          * physical distance. So: length = min( max("1", "2"), "3" ). These distances are all considered along the from-lanes.
  218.          * Actual path distance is different.
  219.          */
  220.         Speed meanSpeed = planDistance.divide(timeStep);
  221.         double minDuration = this.minimumLaneChangeDistance.si / meanSpeed.si;
  222.         double laneChangeDuration = Math.max(this.desiredLaneChangeDuration.si, minDuration);
  223.         if (this.boundary != null)
  224.         {
  225.             double maxDuration = this.boundary.si / meanSpeed.si;
  226.             laneChangeDuration = Math.min(laneChangeDuration, maxDuration);
  227.         }

  228.         double totalLength = laneChangeDuration * meanSpeed.si;
  229.         double fromDist = (1.0 - this.fraction) * totalLength; // remaining distance along from lanes to lane change end
  230.         Throw.when(fromDist < 0.0, RuntimeException.class, "Lane change results in negative distance along from lanes.");

  231.         // get fractional location there, build lane lists as we search over the distance
  232.         Lane fromLane = from.lane();
  233.         List<Lane> fromLanes = new ArrayList<>();
  234.         List<Lane> toLanes = new ArrayList<>();
  235.         fromLanes.add(fromLane);
  236.         toLanes.add(fromLane.getAdjacentLane(this.laneChangeDirectionality, gtu.getType()));
  237.         double endPosFrom = from.position().si + fromDist;
  238.         boolean sink = false;
  239.         while (endPosFrom + gtu.getFront().dx().si > fromLane.getLength().si)
  240.         {
  241.             Lane nextFromLane;
  242.             if (!favoured)
  243.             {
  244.                 nextFromLane = gtu.getNextLaneForRoute(fromLane);
  245.             }
  246.             else
  247.             {
  248.                 Set<Lane> nexts = gtu.getNextLanesForRoute(fromLane);
  249.                 if (nexts != null && !nexts.isEmpty())
  250.                 {
  251.                     Iterator<Lane> it = nexts.iterator();
  252.                     nextFromLane = it.next();
  253.                     while (it.hasNext())
  254.                     {
  255.                         nextFromLane =
  256.                                 LaneBasedTacticalPlanner.mostOnSide(nextFromLane, it.next(), this.laneChangeDirectionality);
  257.                     }
  258.                 }
  259.                 else
  260.                 {
  261.                     nextFromLane = null;
  262.                 }
  263.             }
  264.             if (nextFromLane == null)
  265.             {
  266.                 for (LaneDetector detector : fromLane.getDetectors(fromLane.getLength(), fromLane.getLength(), gtu.getType()))
  267.                 {
  268.                     if (detector instanceof SinkDetector)
  269.                     {
  270.                         sink = true;
  271.                     }
  272.                 }
  273.             }
  274.             if (nextFromLane == null)
  275.             {
  276.                 // there are no lanes to move on, restrict lane change length/duration (given fixed mean speed)
  277.                 if (!sink)
  278.                 {
  279.                     double endFromPosLimit = fromLane.getLength().si - gtu.getFront().dx().si;
  280.                     double f = 1.0 - (endPosFrom - endFromPosLimit) / fromDist;
  281.                     laneChangeDuration *= f;
  282.                     endPosFrom = endFromPosLimit;
  283.                 }
  284.                 break;
  285.             }
  286.             endPosFrom -= fromLane.getLength().si;
  287.             Lane nextToLane = nextFromLane.getAdjacentLane(this.laneChangeDirectionality, gtu.getType());
  288.             if (nextToLane == null)
  289.             {
  290.                 // there are no lanes to change to, restrict lane change length/duration (given fixed mean speed)
  291.                 if (!sink)
  292.                 {
  293.                     double endFromPosLimit = fromLane.getLength().si - gtu.getFront().dx().si;
  294.                     double f = 1.0 - (endPosFrom - endFromPosLimit) / fromDist;
  295.                     laneChangeDuration *= f;
  296.                     endPosFrom = endFromPosLimit;
  297.                 }
  298.                 break;
  299.             }
  300.             fromLane = nextFromLane;
  301.             fromLanes.add(fromLane);
  302.             toLanes.add(nextToLane);
  303.         }
  304.         // for long vehicles and short lanes, revert
  305.         while (endPosFrom < 0.0)
  306.         {
  307.             fromLanes.remove(fromLanes.size() - 1);
  308.             toLanes.remove(toLanes.size() - 1);
  309.             fromLane = fromLanes.get(fromLanes.size() - 1);
  310.             endPosFrom += fromLane.getLength().si;
  311.         }
  312.         // finally, get location at the final lane available
  313.         double endFractionalPositionFrom = fromLane.fractionAtCoveredDistance(Length.instantiateSI(endPosFrom));
  314.         endFractionalPositionFrom = Math.min(endFractionalPositionFrom, 1.0);

  315.         LanePosition fromAdjusted = from;
  316.         while (fromAdjusted.position().gt(fromAdjusted.lane().getLength()))
  317.         {
  318.             // the from position is beyond the first lane (can occur if it is not the ref position)
  319.             fromLanes.remove(0);
  320.             toLanes.remove(0);
  321.             Length beyond = fromAdjusted.position().minus(fromAdjusted.lane().getLength());
  322.             Length pos = beyond;
  323.             fromAdjusted = Try.assign(() -> new LanePosition(fromLanes.get(0), pos), OtsGeometryException.class,
  324.                     "Info for lane is null.");
  325.         }

  326.         // get path from shape

  327.         // create center lines
  328.         double startFractionalPositionFrom = from.position().si / from.lane().getLength().si;
  329.         OtsLine2d fromLine = getLine(fromLanes, startFractionalPositionFrom, endFractionalPositionFrom);
  330.         // project for toLane
  331.         double startFractionalPositionTo = toLanes.get(0).getCenterLine().projectFractional(null, null, startPosition.x,
  332.                 startPosition.y, FractionalFallback.ENDPOINT);
  333.         int last = fromLanes.size() - 1;
  334.         double frac = endFractionalPositionFrom;
  335.         OrientedPoint2d p = fromLanes.get(last).getCenterLine().getLocationPointFraction(frac);
  336.         double endFractionalPositionTo =
  337.                 toLanes.get(last).getCenterLine().projectFractional(null, null, p.x, p.y, FractionalFallback.ENDPOINT);
  338.         startFractionalPositionTo = startFractionalPositionTo >= 0.0 ? startFractionalPositionTo : 0.0;
  339.         endFractionalPositionTo = endFractionalPositionTo <= 1.0 ? endFractionalPositionTo : 1.0;
  340.         endFractionalPositionTo = endFractionalPositionTo <= 0.0 ? endFractionalPositionFrom : endFractionalPositionTo;
  341.         // check for poor projection (end location is difficult to project; we have no path yet so we use the from lane)
  342.         if (fromLanes.size() == 1 && endFractionalPositionTo <= startFractionalPositionTo)
  343.         {
  344.             endFractionalPositionTo = Math.min(Math.max(startFractionalPositionTo + 0.001, endFractionalPositionFrom), 1.0);
  345.         }
  346.         if (startFractionalPositionTo >= 1.0)
  347.         {
  348.             toLanes.remove(0);
  349.             startFractionalPositionTo = 0.0;
  350.         }
  351.         OtsLine2d toLine = getLine(toLanes, startFractionalPositionTo, endFractionalPositionTo);

  352.         OtsLine2d path = this.laneChangePath.getPath(timeStep, planDistance, meanSpeed, fromAdjusted, startPosition,
  353.                 laneChangeDirection, fromLine, toLine, Duration.instantiateSI(laneChangeDuration), this.fraction);
  354.         // update
  355.         // TODO: this assumes the time step will not be interrupted
  356.         this.fraction += timeStep.si / laneChangeDuration; // the total fraction this step increases

  357.         // deal with lane change end
  358.         double requiredLength = planDistance.si - path.getLength();
  359.         if (requiredLength > 0.0 || this.fraction > 0.999)
  360.         {
  361.             try
  362.             {
  363.                 gtu.getSimulator().scheduleEventNow(BUILDER, "scheduleLaneChangeFinalization",
  364.                         new Object[] {gtu, Length.min(planDistance, path.getTypedLength()), laneChangeDirection});
  365.             }
  366.             catch (SimRuntimeException exception)
  367.             {
  368.                 throw new RuntimeException("Error during lane change finalization.", exception);
  369.             }
  370.             // add length to path on to lanes
  371.             if (requiredLength > 0.0)
  372.             {
  373.                 Lane toLane = toLanes.get(toLanes.size() - 1);
  374.                 int n = path.size();
  375.                 // ignore remainder of first lane if fraction is at the end of the lane
  376.                 if (0.0 < endFractionalPositionFrom && endFractionalPositionFrom < 1.0)
  377.                 {
  378.                     OtsLine2d remainder = toLane.getCenterLine().extractFractional(endFractionalPositionTo, 1.0);
  379.                     path = OtsLine2d.concatenate(0.001, path, remainder);
  380.                     requiredLength = planDistance.si - path.getLength();
  381.                 }
  382.                 // add further lanes
  383.                 while (toLane != null && requiredLength > 0.0)
  384.                 {
  385.                     Lane prevToLane = toLane;
  386.                     toLane = gtu.getNextLaneForRoute(toLane);
  387.                     if (toLane != null) // let's hope we will move on to a sink
  388.                     {
  389.                         OtsLine2d remainder = toLane.getCenterLine();
  390.                         path = OtsLine2d.concatenate(Lane.MARGIN.si, path, remainder);
  391.                         requiredLength = planDistance.si - path.getLength() + Lane.MARGIN.si;
  392.                     }
  393.                     else if (sink)
  394.                     {
  395.                         // just add some line distance
  396.                         Point2d extra = prevToLane.getCenterLine().getLocationExtendedSI(prevToLane.getLength().si + 100);
  397.                         List<Point2d> points = path.getPointList();
  398.                         points.add(extra);
  399.                         path = new OtsLine2d(points);
  400.                     }
  401.                 }
  402.                 // filter near-duplicate point which results in projection exceptions
  403.                 if (this.fraction > 0.999) // this means point 'target' is essentially at the design line
  404.                 {
  405.                     List<Point2d> points = path.getPointList();
  406.                     points.remove(n);
  407.                     path = new OtsLine2d(points);
  408.                 }
  409.             }
  410.             // reset lane change
  411.             this.laneChangeDirectionality = null;
  412.             this.boundary = null;
  413.             this.fraction = 0.0;
  414.         }

  415.         return path;
  416.     }

  417.     /**
  418.      * Returns a line from the lane center lines, cutting of at the from position and the end fractional position.
  419.      * @param lanes lanes
  420.      * @param startFractionalPosition current fractional GTU position on first lane
  421.      * @param endFractionalPosition target fractional GTU position on last lane
  422.      * @return line from the lane center lines
  423.      */
  424.     private OtsLine2d getLine(final List<Lane> lanes, final double startFractionalPosition, final double endFractionalPosition)
  425.     {
  426.         OtsLine2d line = null;
  427.         for (Lane lane : lanes)
  428.         {
  429.             if (line == null && lane.equals(lanes.get(lanes.size() - 1)))
  430.             {
  431.                 if (endFractionalPosition < startFractionalPosition)
  432.                 {
  433.                     System.out.println("hmmm");
  434.                 }
  435.                 line = lane.getCenterLine().extractFractional(startFractionalPosition, endFractionalPosition);
  436.             }
  437.             else if (line == null)
  438.             {
  439.                 line = lane.getCenterLine().extractFractional(startFractionalPosition, 1.0);
  440.             }
  441.             else if (lane.equals(lanes.get(lanes.size() - 1)))
  442.             {
  443.                 line = OtsLine2d.concatenate(Lane.MARGIN.si, line,
  444.                         lane.getCenterLine().extractFractional(0.0, endFractionalPosition));
  445.             }
  446.             else
  447.             {
  448.                 line = OtsLine2d.concatenate(Lane.MARGIN.si, line, lane.getCenterLine());
  449.             }
  450.         }
  451.         return line;
  452.     }

  453.     /**
  454.      * Checks whether the given GTU has sufficient space relative to a {@code Headway}.
  455.      * @param gtu gtu
  456.      * @param headway headway
  457.      * @return whether the given GTU has sufficient space relative to a {@code Headway}
  458.      */
  459.     public boolean checkRoom(final LaneBasedGtu gtu, final Headway headway)
  460.     {
  461.         if (this.desiredLaneChangeDuration == null)
  462.         {
  463.             this.desiredLaneChangeDuration = Try.assign(() -> gtu.getParameters().getParameter(ParameterTypes.LCDUR),
  464.                     "LaneChange; the desired lane change duration should be set or paramater LCDUR should be defined.");
  465.         }

  466.         EgoPerception<?, ?> ego = gtu.getTacticalPlanner().getPerception().getPerceptionCategoryOrNull(EgoPerception.class);
  467.         Speed egoSpeed = ego == null ? gtu.getSpeed() : ego.getSpeed();
  468.         Acceleration egoAcceleration = ego == null ? gtu.getAcceleration() : ego.getAcceleration();
  469.         Speed speed = headway.getSpeed() == null ? Speed.ZERO : headway.getSpeed();
  470.         Acceleration acceleration = headway.getAcceleration() == null ? Acceleration.ZERO : headway.getAcceleration();
  471.         Length s0 = gtu.getParameters().getParameterOrNull(ParameterTypes.S0);
  472.         s0 = s0 == null ? Length.ZERO : s0;

  473.         Length distanceToStop;
  474.         if (speed.eq0())
  475.         {
  476.             distanceToStop = Length.ZERO;
  477.         }
  478.         else
  479.         {
  480.             Acceleration b = gtu.getParameters().getParameterOrNull(ParameterTypes.B);
  481.             b = b == null ? Acceleration.ZERO : b.neg();
  482.             Acceleration a = Acceleration.min(Acceleration.max(b, acceleration.plus(b)), acceleration);
  483.             if (a.ge0())
  484.             {
  485.                 return true;
  486.             }
  487.             double t = speed.si / -a.si;
  488.             distanceToStop = Length.instantiateSI(speed.si * t + .5 * a.si * t * t);
  489.         }

  490.         Length availableDistance = headway.getDistance().plus(distanceToStop);
  491.         double t = this.desiredLaneChangeDuration.si;
  492.         if (egoAcceleration.lt0())
  493.         {
  494.             t = Math.min(egoSpeed.si / -egoAcceleration.si, t);
  495.         }
  496.         Length requiredDistance = Length
  497.                 .max(Length.instantiateSI(egoSpeed.si * t + .5 * egoAcceleration.si * t * t), this.minimumLaneChangeDistance)
  498.                 .plus(s0);
  499.         return availableDistance.gt(requiredDistance);
  500.     }

  501.     @Override
  502.     public String toString()
  503.     {
  504.         return "LaneChange [fraction=" + this.fraction + ", laneChangeDirectionality=" + this.laneChangeDirectionality + "]";
  505.     }

  506.     /**
  507.      * Provides a (partial) path during lane changes.
  508.      * <p>
  509.      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
  510.      * <br>
  511.      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  512.      * </p>
  513.      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
  514.      * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
  515.      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
  516.      */
  517.     public interface LaneChangePath
  518.     {

  519.         /** Sine-shaped interpolation between center lines. */
  520.         LaneChangePath SINE_INTERP = new InterpolatedLaneChangePath()
  521.         {
  522.             @Override
  523.             double longitudinalFraction(final double lateralFraction)
  524.             {
  525.                 return 1.0 - Math.acos(2.0 * (lateralFraction - 0.5)) / Math.PI;
  526.             }

  527.             @Override
  528.             double lateralFraction(final double longitudinalFraction)
  529.             {
  530.                 return 0.5 - 0.5 * Math.cos(longitudinalFraction * Math.PI);
  531.             }
  532.         };

  533.         /** Linear interpolation between center lines. */
  534.         LaneChangePath LINEAR = new InterpolatedLaneChangePath()
  535.         {

  536.             @Override
  537.             double longitudinalFraction(final double lateralFraction)
  538.             {
  539.                 return lateralFraction;
  540.             }

  541.             @Override
  542.             double lateralFraction(final double longitudinalFraction)
  543.             {
  544.                 return longitudinalFraction;
  545.             }
  546.         };

  547.         /** A simple Bezier curve directly to the lane change target position. */
  548.         LaneChangePath BEZIER = new LaneChangePath()
  549.         {
  550.             @Override
  551.             public OtsLine2d getPath(final Duration timeStep, final Length planDistance, final Speed meanSpeed,
  552.                     final LanePosition from, final OrientedPoint2d startPosition,
  553.                     final LateralDirectionality laneChangeDirection, final OtsLine2d fromLine, final OtsLine2d toLine,
  554.                     final Duration laneChangeDuration, final double lcFraction)
  555.             {
  556.                 OrientedPoint2d target = toLine.getLocationPointFraction(1.0);
  557.                 return Bezier.cubic(64, startPosition, target, 0.5);
  558.             }
  559.         };

  560.         /** The target point (including rotation) for the coming time step is based on a sine wave. */
  561.         LaneChangePath SINE = new SequentialLaneChangePath()
  562.         {
  563.             @Override
  564.             protected double lateralFraction(final double lcFraction)
  565.             {
  566.                 return -1.0 / (2 * Math.PI) * Math.sin(2 * Math.PI * lcFraction) + lcFraction;
  567.             }

  568.             @Override
  569.             protected double angle(final double width, final double cumulLcLength, final double totalLcLength)
  570.             {
  571.                 return Math.atan((-width * Math.cos(2 * Math.PI * cumulLcLength / totalLcLength) / totalLcLength)
  572.                         + width / totalLcLength);
  573.             }
  574.         };

  575.         /** The target point (including rotation) for the coming time step is based on a 3rd-degree polynomial. */
  576.         LaneChangePath POLY3 = new SequentialLaneChangePath()
  577.         {
  578.             @Override
  579.             protected double lateralFraction(final double lcFraction)
  580.             {
  581.                 return 3 * (lcFraction * lcFraction) - 2 * (lcFraction * lcFraction * lcFraction);
  582.             }

  583.             @Override
  584.             protected double angle(final double width, final double cumulLcLength, final double totalLcLength)
  585.             {
  586.                 return Math.atan(cumulLcLength * 6 * width / (totalLcLength * totalLcLength)
  587.                         - cumulLcLength * cumulLcLength * 6 * width / (totalLcLength * totalLcLength * totalLcLength));
  588.             }
  589.         };

  590.         /**
  591.          * A helper class to allow a lane change to follow a sequential determination of the target position (including
  592.          * rotation) for each time step.
  593.          * <p>
  594.          * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights
  595.          * reserved. <br>
  596.          * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  597.          * <p>
  598.          * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
  599.          * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
  600.          * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
  601.          */
  602.         abstract class SequentialLaneChangePath implements LaneChangePath
  603.         {
  604.             @Override
  605.             public OtsLine2d getPath(final Duration timeStep, final Length planDistance, final Speed meanSpeed,
  606.                     final LanePosition from, final OrientedPoint2d startPosition,
  607.                     final LateralDirectionality laneChangeDirection, final OtsLine2d fromLine, final OtsLine2d toLine,
  608.                     final Duration laneChangeDuration, final double lcFraction)
  609.             {
  610.                 OrientedPoint2d toTarget = toLine.getLocationPointFraction(1.0);
  611.                 OrientedPoint2d fromTarget = fromLine.getLocationPointFraction(1.0);
  612.                 double width = laneChangeDirection.isRight() ? fromTarget.distance(toTarget) : -fromTarget.distance(toTarget);
  613.                 double dFraction = timeStep.si / laneChangeDuration.si;
  614.                 return getPathRecursive(planDistance, meanSpeed, 1.0, width, from, startPosition, fromLine, toLine,
  615.                         laneChangeDuration, lcFraction, dFraction);
  616.             }

  617.             /**
  618.              * Attempts to derive a path. If the resulting path is shorter than {@code planDistance} (e.g. lane change towards
  619.              * the inside of a curve), this method calls itself using a larger look-ahead distance.
  620.              * @param planDistance plan distance
  621.              * @param meanSpeed mean speed during plan
  622.              * @param buffer buffer factor to assure sufficient path length is found, increased recursively
  623.              * @param width lateral deviation from from lanes at lane change end
  624.              * @param from current position on the from-lanes
  625.              * @param startPosition current 2D position
  626.              * @param fromLine from line
  627.              * @param toLine to line
  628.              * @param laneChangeDuration current considered duration of the entire lane change
  629.              * @param lcFraction lane change fraction at beginning of the plan
  630.              * @param dFraction additional lane change fraction to be made during the plan
  631.              * @return OtsLine2d a (partial) path for a lane change
  632.              */
  633.             private OtsLine2d getPathRecursive(final Length planDistance, final Speed meanSpeed, final double buffer,
  634.                     final double width, final LanePosition from, final OrientedPoint2d startPosition, final OtsLine2d fromLine,
  635.                     final OtsLine2d toLine, final Duration laneChangeDuration, final double lcFraction, final double dFraction)
  636.             {
  637.                 // factor on path length to not overshoot a fraction of 1.0 in lane change progress, i.e. <1 if lane change will
  638.                 // be finished in the coming time step
  639.                 double cutoff = (1.0 - lcFraction) / (dFraction * buffer);
  640.                 cutoff = cutoff > 1.0 ? 1.0 : cutoff;

  641.                 // lane change fraction at end of plan
  642.                 double lcFractionEnd = lcFraction + dFraction * buffer * cutoff;

  643.                 // lateral fraction at that point according to shape
  644.                 double f = lateralFraction(lcFractionEnd);

  645.                 // from-lane length
  646.                 double totalLcLength = meanSpeed.si * laneChangeDuration.si;
  647.                 double cumulLcLength = totalLcLength * lcFractionEnd;

  648.                 // TODO: sequential is disabled as LaneChangePath now uses 2 OtsLine2d's instead of 2 List<Lane>'s. This was
  649.                 // done as the code using LaneChangePath (i.e. LaneChange) required more details on fractional positions itself.
  650.                 return null;
  651.             }

  652.             /**
  653.              * Returns the fractional lateral deviation given a fraction of lane change being completed.
  654.              * @param lcFraction fraction of lane change
  655.              * @return lateral deviation
  656.              */
  657.             protected abstract double lateralFraction(double lcFraction);

  658.             /**
  659.              * Returns the angle, relative to the lane center line, at the given cumulative length for a lane change of given
  660.              * total length and lateral deviation.
  661.              * @param width lateral deviation from from lanes at lane change end
  662.              * @param cumulLcLength cumulative length (along from lanes) covered so far
  663.              * @param totalLcLength total (along from lanes) length to cover in lane change
  664.              * @return angle, relative to the lane center line
  665.              */
  666.             protected abstract double angle(double width, double cumulLcLength, double totalLcLength);
  667.         }

  668.         /**
  669.          * Helper class for interpolation between the from and to center lines.
  670.          * <p>
  671.          * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights
  672.          * reserved. <br>
  673.          * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  674.          * <p>
  675.          * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
  676.          * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
  677.          * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
  678.          */
  679.         abstract class InterpolatedLaneChangePath implements LaneChangePath
  680.         {

  681.             @Override
  682.             public OtsLine2d getPath(final Duration timeStep, final Length planDistance, final Speed meanSpeed,
  683.                     final LanePosition from, final OrientedPoint2d startPosition,
  684.                     final LateralDirectionality laneChangeDirection, final OtsLine2d fromLine, final OtsLine2d toLine,
  685.                     final Duration laneChangeDuration, final double lcFraction)
  686.             {

  687.                 double dx = fromLine.get(0).x - startPosition.x;
  688.                 double dy = fromLine.get(0).y - startPosition.y;
  689.                 double distFromLoc = Math.hypot(dx, dy);
  690.                 dx = fromLine.get(0).x - toLine.get(0).x;
  691.                 dy = fromLine.get(0).y - toLine.get(0).y;
  692.                 double distFromTo = Math.hypot(dx, dy);
  693.                 double startLateralFraction = distFromLoc / distFromTo;
  694.                 // Location is not on path in z-direction, so using .distance() create bugs
  695.                 // PK: added test for NaN (which occurs when fromLine and toLine start on top of each other.
  696.                 if (Double.isNaN(startLateralFraction) || startLateralFraction > 1.0)
  697.                 {
  698.                     startLateralFraction = 1.0;
  699.                 }
  700.                 double startLongitudinalFractionTotal = longitudinalFraction(startLateralFraction);

  701.                 double nSegments = Math.ceil((64 * (1.0 - lcFraction)));
  702.                 List<Point2d> pointList = new ArrayList<>();
  703.                 // double zStart = (1.0 - startLateralFraction) * fromLine.get(0).z + startLateralFraction * toLine.get(0).z;
  704.                 pointList.add(startPosition);
  705.                 for (int i = 1; i <= nSegments; i++)
  706.                 {
  707.                     double f = i / nSegments;
  708.                     double longitudinalFraction = startLongitudinalFractionTotal + f * (1.0 - startLongitudinalFractionTotal);
  709.                     double lateralFraction = lateralFraction(longitudinalFraction);
  710.                     double lateralFractionInv = 1.0 - lateralFraction;
  711.                     OrientedPoint2d fromPoint = fromLine.getLocationPointFraction(f);
  712.                     OrientedPoint2d toPoint = toLine.getLocationPointFraction(f);
  713.                     pointList.add(new Point2d(lateralFractionInv * fromPoint.x + lateralFraction * toPoint.x,
  714.                             lateralFractionInv * fromPoint.y + lateralFraction * toPoint.y));
  715.                 }

  716.                 OtsLine2d line = new OtsLine2d(pointList);
  717.                 // clean line for projection inconsistencies (position -> center lines -> interpolated new position)
  718.                 double angleChange = Math.abs(line.getLocation(Length.ZERO).getDirZ() - startPosition.getDirZ());
  719.                 int i = 1;
  720.                 while (angleChange > Math.PI / 4)
  721.                 {
  722.                     i++;
  723.                     if (i >= pointList.size() - 2)
  724.                     {
  725.                         // return original if we can't clean the line, perhaps extreme road curvature or line with 2 points
  726.                         return new OtsLine2d(pointList);
  727.                     }
  728.                     List<Point2d> newPointList = new ArrayList<>(pointList.subList(i, pointList.size()));
  729.                     newPointList.add(0, pointList.get(0));
  730.                     line = new OtsLine2d(newPointList);
  731.                     angleChange = Math.abs(line.getLocation(Length.ZERO).getDirZ() - startPosition.getDirZ());
  732.                 }
  733.                 return line;
  734.             }

  735.             /**
  736.              * Transform lateral to longitudinal fraction.
  737.              * @param lateralFraction lateral fraction
  738.              * @return transformation of lateral to longitudinal fraction
  739.              */
  740.             abstract double longitudinalFraction(double lateralFraction);

  741.             /**
  742.              * Transform longitudinal to lateral fraction.
  743.              * @param longitudinalFraction longitudinal fraction
  744.              * @return transformation of longitudinal to lateral fraction
  745.              */
  746.             abstract double lateralFraction(double longitudinalFraction);

  747.         }

  748.         /**
  749.          * Returns a (partial) path for a lane change. The method is called both at the start and during a lane change, and
  750.          * should return a valid path. This path should at least have a length of {@code planDistance}, unless the lane change
  751.          * will be finished during the coming time step. In that case, the caller of this method is to lengthen the path along
  752.          * the center line of the target lane.
  753.          * @param timeStep time step
  754.          * @param planDistance distance covered during the operational plan
  755.          * @param meanSpeed mean speed during time step
  756.          * @param from current position on the from-lanes
  757.          * @param startPosition current 2D position
  758.          * @param laneChangeDirection lane change direction
  759.          * @param fromLine from line
  760.          * @param toLine to line
  761.          * @param laneChangeDuration current considered duration of the entire lane change
  762.          * @param lcFraction fraction of lane change done so far
  763.          * @return OtsLine2d a (partial) path for a lane change
  764.          */
  765.         OtsLine2d getPath(Duration timeStep, Length planDistance, Speed meanSpeed, LanePosition from,
  766.                 OrientedPoint2d startPosition, LateralDirectionality laneChangeDirection, OtsLine2d fromLine, OtsLine2d toLine,
  767.                 Duration laneChangeDuration, double lcFraction);
  768.     }
  769. }