View Javadoc
1   package org.opentrafficsim.road.gtu.lane.plan.operational;
2   
3   import java.io.Serializable;
4   import java.util.ArrayList;
5   import java.util.Iterator;
6   import java.util.List;
7   import java.util.Map;
8   import java.util.Set;
9   
10  import org.djunits.value.vdouble.scalar.Acceleration;
11  import org.djunits.value.vdouble.scalar.Duration;
12  import org.djunits.value.vdouble.scalar.Length;
13  import org.djunits.value.vdouble.scalar.Speed;
14  import org.djutils.draw.point.OrientedPoint2d;
15  import org.djutils.draw.point.Point2d;
16  import org.djutils.exceptions.Throw;
17  import org.djutils.exceptions.Try;
18  import org.opentrafficsim.base.parameters.ParameterTypes;
19  import org.opentrafficsim.core.geometry.Bezier;
20  import org.opentrafficsim.core.geometry.OtsGeometryException;
21  import org.opentrafficsim.core.geometry.OtsLine2d;
22  import org.opentrafficsim.core.geometry.OtsLine2d.FractionalFallback;
23  import org.opentrafficsim.core.gtu.GtuException;
24  import org.opentrafficsim.core.gtu.perception.EgoPerception;
25  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlanException;
26  import org.opentrafficsim.core.network.LateralDirectionality;
27  import org.opentrafficsim.road.gtu.lane.LaneBasedGtu;
28  import org.opentrafficsim.road.gtu.lane.perception.RelativeLane;
29  import org.opentrafficsim.road.gtu.lane.perception.headway.Headway;
30  import org.opentrafficsim.road.gtu.lane.tactical.LaneBasedTacticalPlanner;
31  import org.opentrafficsim.road.network.lane.Lane;
32  import org.opentrafficsim.road.network.lane.LanePosition;
33  
34  import nl.tudelft.simulation.dsol.SimRuntimeException;
35  
36  /**
37   * Lane change status across operational plans. This class allows lane based tactical planners to perform lane changes without
38   * having to deal with many complexities concerning paths and lane registration. The main purpose of the tactical planner is to
39   * request a path using {@code getPath()} for each step of the tactical planner.
40   * <p>
41   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
42   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
43   * </p>
44   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
45   * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
46   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
47   */
48  public class LaneChange implements Serializable
49  {
50  
51      /** */
52      private static final long serialVersionUID = 20160811L;
53  
54      /** Total lane change duration. */
55      private Duration desiredLaneChangeDuration;
56  
57      /** Fraction of lane change had. */
58      private double fraction;
59  
60      /** Boundary length within which the lane change should be performed. */
61      private Length boundary;
62  
63      /** Lane change path. */
64      private LaneChangePath laneChangePath = LaneChangePath.SINE_INTERP;
65  
66      /** Whether the GTU is changing lane. */
67      private LateralDirectionality laneChangeDirectionality = null;
68  
69      /** Minimum lane change distance. */
70      private final Length minimumLaneChangeDistance;
71  
72      /** Instance to invoke static method through scheduled event on. */
73      private static final LaneOperationalPlanBuilder BUILDER = new LaneOperationalPlanBuilder();
74  
75      /** Minimum distance required to perform a lane change as factor on vehicle length. */
76      public static double MIN_LC_LENGTH_FACTOR = 1.5;
77  
78      /**
79       * Constructor.
80       * @param gtu LaneBasedGtu; gtu
81       */
82      public LaneChange(final LaneBasedGtu gtu)
83      {
84          this.minimumLaneChangeDistance = gtu.getLength().times(MIN_LC_LENGTH_FACTOR);
85      }
86  
87      /**
88       * Constructor.
89       * @param minimumLaneChangeDistance Length; minimum lane change distance
90       * @param desiredLaneChangeDuration Duration; deaired lane change duration
91       */
92      public LaneChange(final Length minimumLaneChangeDistance, final Duration desiredLaneChangeDuration)
93      {
94          this.minimumLaneChangeDistance = minimumLaneChangeDistance;
95          this.desiredLaneChangeDuration = desiredLaneChangeDuration;
96      }
97  
98      /**
99       * Returns the minimum lane change distance.
100      * @return Length; minimum lane change distance
101      */
102     public Length getMinimumLaneChangeDistance()
103     {
104         return this.minimumLaneChangeDistance;
105     }
106 
107     /**
108      * Sets the desired lane change duration. Should be set by a tactical planner.
109      * @param duration Duration; desired lane change duration
110      */
111     public void setDesiredLaneChangeDuration(final Duration duration)
112     {
113         this.desiredLaneChangeDuration = duration;
114     }
115 
116     /**
117      * Sets the distance within which a lane change should be finished. Should be set by a tactical planner. In case of a single
118      * lane change required before some point, this is not required as the found center line length is intrinsically limited.
119      * For multiple lane changes being required, space after a lane change is required.
120      * @param boundary Length; boundary
121      */
122     public void setBoundary(final Length boundary)
123     {
124         this.boundary = boundary;
125     }
126 
127     /**
128      * Returns the fraction of the lane change performed.
129      * @return double; fraction of lane change performed
130      */
131     public double getFraction()
132     {
133         return this.fraction;
134     }
135 
136     /**
137      * Sets a lane change path.
138      * @param laneChangePath LaneChangePath; lane change path
139      */
140     public void setLaneChangePath(final LaneChangePath laneChangePath)
141     {
142         this.laneChangePath = laneChangePath;
143     }
144 
145     /**
146      * Return whether the GTU is changing lane.
147      * @return whether the GTU is changing lane
148      */
149     public final boolean isChangingLane()
150     {
151         return this.laneChangeDirectionality != null;
152     }
153 
154     /**
155      * Return whether the GTU is changing left.
156      * @return whether the GTU is changing left
157      */
158     public final boolean isChangingLeft()
159     {
160         return LateralDirectionality.LEFT.equals(this.laneChangeDirectionality);
161     }
162 
163     /**
164      * Return whether the GTU is changing right.
165      * @return whether the GTU is changing right
166      */
167     public final boolean isChangingRight()
168     {
169         return LateralDirectionality.RIGHT.equals(this.laneChangeDirectionality);
170     }
171 
172     /**
173      * Return lateral lane change direction.
174      * @return LateralDirectionality; lateral lane change direction
175      */
176     public final LateralDirectionality getDirection()
177     {
178         return this.laneChangeDirectionality;
179     }
180 
181     /**
182      * Second lane of lane change relative to the reference lane. Note that the reference lane may either be the source or the
183      * target lane. Thus, the second lane during a lane change may either be the left or right lane, regardless of the lane
184      * change direction.
185      * @param gtu LaneBasedGtu; the GTU
186      * @return target lane of lane change
187      * @throws OperationalPlanException If no lane change is being performed.
188      */
189     public final RelativeLane getSecondLane(final LaneBasedGtu gtu) throws OperationalPlanException
190     {
191         Throw.when(!isChangingLane(), OperationalPlanException.class,
192                 "Target lane is requested, but no lane change is being performed.");
193         Map<Lane, Length> map;
194         LanePosition dlp;
195         try
196         {
197             map = gtu.positions(gtu.getReference());
198             dlp = gtu.getReferencePosition();
199         }
200         catch (GtuException exception)
201         {
202             throw new OperationalPlanException("Second lane of lane change could not be determined.", exception);
203         }
204         Set<Lane> accessibleLanes = dlp.lane().accessibleAdjacentLanesPhysical(this.laneChangeDirectionality, gtu.getType());
205         if (!accessibleLanes.isEmpty() && map.containsKey(accessibleLanes.iterator().next()))
206         {
207             return isChangingLeft() ? RelativeLane.LEFT : RelativeLane.RIGHT;
208         }
209         return isChangingLeft() ? RelativeLane.RIGHT : RelativeLane.LEFT;
210     }
211 
212     /**
213      * Returns the path for a lane change. Lane change initialization and finalization events are automatically performed.
214      * @param timeStep Duration; plan time step
215      * @param gtu LaneBasedGtu; gtu
216      * @param from LanePosition; current position on the from lane (i.e. not necessarily the reference position)
217      * @param startPosition OrientedPoint2d; current position in 2D
218      * @param planDistance Length; absolute distance that will be covered during the time step
219      * @param laneChangeDirection LateralDirectionality; lane change direction
220      * @return OtsLine2d; path
221      * @throws OtsGeometryException on path or shape error
222      */
223     public final OtsLine2d getPath(final Duration timeStep, final LaneBasedGtu gtu, final LanePosition from,
224             final OrientedPoint2d startPosition, final Length planDistance, final LateralDirectionality laneChangeDirection)
225             throws OtsGeometryException
226     {
227         // initiate lane change
228         boolean favoured = false;
229         if (!isChangingLane())
230         {
231             favoured = true;
232             this.laneChangeDirectionality = laneChangeDirection;
233             Try.execute(() -> gtu.initLaneChange(laneChangeDirection), "Error during lane change initialization.");
234         }
235         // Wouter: why would we ever not favor a side during a lane change
236         favoured = true;
237 
238         // determine longitudinal distance along the from lanes
239         /*
240          * We take 3 factors in to account. The first two are 1) minimum physical lane change length, and 2) desired lane change
241          * duration. With the current mean speed of the plan, we take the maximum. So at very low speeds, the minimum physical
242          * length may increase the lane change duration. We also have 3) the maximum length before a lane change needs to have
243          * been performed. To overcome simulation troubles, we allow this to result in an even shorter length than the minimum
244          * physical distance. So: length = min( max("1", "2"), "3" ). These distances are all considered along the from-lanes.
245          * Actual path distance is different.
246          */
247         Speed meanSpeed = planDistance.divide(timeStep);
248         double minDuration = this.minimumLaneChangeDistance.si / meanSpeed.si;
249         double laneChangeDuration = Math.max(this.desiredLaneChangeDuration.si, minDuration);
250         if (this.boundary != null)
251         {
252             double maxDuration = this.boundary.si / meanSpeed.si;
253             laneChangeDuration = Math.min(laneChangeDuration, maxDuration);
254         }
255 
256         double totalLength = laneChangeDuration * meanSpeed.si;
257         double fromDist = (1.0 - this.fraction) * totalLength; // remaining distance along from lanes to lane change end
258         Throw.when(fromDist < 0.0, RuntimeException.class, "Lane change results in negative distance along from lanes.");
259 
260         // get fractional location there, build lane lists as we search over the distance
261         Lane fromLane = from.lane();
262         List<Lane> fromLanes = new ArrayList<>();
263         List<Lane> toLanes = new ArrayList<>();
264         fromLanes.add(fromLane);
265         toLanes.add(fromLane.getAdjacentLane(this.laneChangeDirectionality, gtu.getType()));
266         double endPosFrom = from.position().si + fromDist;
267         while (endPosFrom + gtu.getFront().dx().si > fromLane.getLength().si)
268         {
269             Lane nextFromLane;
270             if (!favoured)
271             {
272                 nextFromLane = gtu.getNextLaneForRoute(fromLane);
273             }
274             else
275             {
276                 Set<Lane> nexts = gtu.getNextLanesForRoute(fromLane);
277                 if (nexts != null && !nexts.isEmpty())
278                 {
279                     Iterator<Lane> it = nexts.iterator();
280                     nextFromLane = it.next();
281                     while (it.hasNext())
282                     {
283                         nextFromLane =
284                                 LaneBasedTacticalPlanner.mostOnSide(nextFromLane, it.next(), this.laneChangeDirectionality);
285                     }
286                 }
287                 else
288                 {
289                     nextFromLane = null;
290                 }
291             }
292             if (nextFromLane == null)
293             {
294                 // there are no lanes to move on, restrict lane change length/duration (given fixed mean speed)
295                 double endFromPosLimit = fromLane.getLength().si - gtu.getFront().dx().si;
296                 double f = 1.0 - (endPosFrom - endFromPosLimit) / fromDist;
297                 laneChangeDuration *= f;
298                 endPosFrom = endFromPosLimit;
299                 break;
300             }
301             endPosFrom -= fromLane.getLength().si;
302             Lane nextToLane = nextFromLane.getAdjacentLane(this.laneChangeDirectionality, gtu.getType());
303             if (nextToLane == null)
304             {
305                 // there are no lanes to change to, restrict lane change length/duration (given fixed mean speed)
306                 double endFromPosLimit = fromLane.getLength().si - gtu.getFront().dx().si;
307                 double f = 1.0 - (endPosFrom - endFromPosLimit) / fromDist;
308                 laneChangeDuration *= f;
309                 endPosFrom = endFromPosLimit;
310                 break;
311             }
312             fromLane = nextFromLane;
313             fromLanes.add(fromLane);
314             toLanes.add(nextToLane);
315         }
316         // for long vehicles and short lanes, revert
317         while (endPosFrom < 0.0)
318         {
319             fromLanes.remove(fromLanes.size() - 1);
320             toLanes.remove(toLanes.size() - 1);
321             fromLane = fromLanes.get(fromLanes.size() - 1);
322             endPosFrom += fromLane.getLength().si;
323         }
324         // finally, get location at the final lane available
325         double endFractionalPositionFrom = fromLane.fractionAtCoveredDistance(Length.instantiateSI(endPosFrom));
326 
327         LanePosition fromAdjusted = from;
328         while (fromAdjusted.position().gt(fromAdjusted.lane().getLength()))
329         {
330             // the from position is beyond the first lane (can occur if it is not the ref position)
331             fromLanes.remove(0);
332             toLanes.remove(0);
333             Length beyond = fromAdjusted.position().minus(fromAdjusted.lane().getLength());
334             Length pos = beyond;
335             fromAdjusted = Try.assign(() -> new LanePosition(fromLanes.get(0), pos), OtsGeometryException.class,
336                     "Info for lane is null.");
337         }
338 
339         // get path from shape
340 
341         // create center lines
342         double startFractionalPositionFrom = from.position().si / from.lane().getLength().si;
343         OtsLine2d fromLine = getLine(fromLanes, startFractionalPositionFrom, endFractionalPositionFrom);
344         // project for toLane
345         double startFractionalPositionTo = toLanes.get(0).getCenterLine().projectFractional(null, null, startPosition.x,
346                 startPosition.y, FractionalFallback.ENDPOINT);
347         int last = fromLanes.size() - 1;
348         double frac = endFractionalPositionFrom;
349         OrientedPoint2d p = fromLanes.get(last).getCenterLine().getLocationFraction(frac);
350         double endFractionalPositionTo =
351                 toLanes.get(last).getCenterLine().projectFractional(null, null, p.x, p.y, FractionalFallback.ENDPOINT);
352         startFractionalPositionTo = startFractionalPositionTo >= 0.0 ? startFractionalPositionTo : 0.0;
353         endFractionalPositionTo = endFractionalPositionTo <= 1.0 ? endFractionalPositionTo : 1.0;
354         endFractionalPositionTo = endFractionalPositionTo <= 0.0 ? endFractionalPositionFrom : endFractionalPositionTo;
355         // check for poor projection (end location is difficult to project; we have no path yet so we use the from lane)
356         if (fromLanes.size() == 1 && endFractionalPositionTo <= startFractionalPositionTo)
357         {
358             endFractionalPositionTo = Math.min(Math.max(startFractionalPositionTo + 0.001, endFractionalPositionFrom), 1.0);
359         }
360         if (startFractionalPositionTo >= 1.0)
361         {
362             toLanes.remove(0);
363             startFractionalPositionTo = 0.0;
364         }
365         OtsLine2d toLine = getLine(toLanes, startFractionalPositionTo, endFractionalPositionTo);
366 
367         OtsLine2d path = this.laneChangePath.getPath(timeStep, planDistance, meanSpeed, fromAdjusted, startPosition,
368                 laneChangeDirection, fromLine, toLine, Duration.instantiateSI(laneChangeDuration), this.fraction);
369         // update
370         // TODO: this assumes the time step will not be interrupted
371         this.fraction += timeStep.si / laneChangeDuration; // the total fraction this step increases
372 
373         // deal with lane change end
374         double requiredLength = planDistance.si - path.getLength().si;
375         if (requiredLength > 0.0 || this.fraction > 0.999)
376         {
377             try
378             {
379                 gtu.getSimulator().scheduleEventNow(BUILDER, "scheduleLaneChangeFinalization",
380                         new Object[] {gtu, Length.min(planDistance, path.getLength()), laneChangeDirection});
381             }
382             catch (SimRuntimeException exception)
383             {
384                 throw new RuntimeException("Error during lane change finalization.", exception);
385             }
386             // add length to path on to lanes
387             if (requiredLength > 0.0)
388             {
389                 Lane toLane = toLanes.get(toLanes.size() - 1);
390                 int n = path.size();
391                 // ignore remainder of first lane if fraction is at the end of the lane
392                 if (0.0 < endFractionalPositionFrom && endFractionalPositionFrom < 1.0)
393                 {
394                     OtsLine2d remainder = toLane.getCenterLine().extractFractional(endFractionalPositionTo, 1.0);
395                     path = OtsLine2d.concatenate(0.001, path, remainder);
396                     requiredLength = planDistance.si - path.getLength().si;
397                 }
398                 // add further lanes
399                 while (toLane != null && requiredLength > 0.0)
400                 {
401                     toLane = gtu.getNextLaneForRoute(toLane);
402                     if (toLane != null) // let's hope we will move on to a sink
403                     {
404                         OtsLine2d remainder = toLane.getCenterLine();
405                         path = OtsLine2d.concatenate(Lane.MARGIN.si, path, remainder);
406                         requiredLength = planDistance.si - path.getLength().si + Lane.MARGIN.si;
407                     }
408                 }
409                 // filter near-duplicate point which results in projection exceptions
410                 if (this.fraction > 0.999) // this means point 'target' is essentially at the design line
411                 {
412                     Point2d[] points = new Point2d[path.size() - 1];
413                     System.arraycopy(path.getPoints(), 0, points, 0, n - 1);
414                     System.arraycopy(path.getPoints(), n, points, n - 1, path.size() - n);
415                     path = new OtsLine2d(points);
416                 }
417             }
418             // reset lane change
419             this.laneChangeDirectionality = null;
420             this.boundary = null;
421             this.fraction = 0.0;
422         }
423 
424         return path;
425     }
426 
427     /**
428      * Returns a line from the lane center lines, cutting of at the from position and the end fractional position.
429      * @param lanes List&lt;Lane&gt;; lanes
430      * @param startFractionalPosition double; current fractional GTU position on first lane
431      * @param endFractionalPosition double; target fractional GTU position on last lane
432      * @return OtsLine2d; line from the lane center lines
433      * @throws OtsGeometryException on fraction outside of range
434      */
435     private OtsLine2d getLine(final List<Lane> lanes, final double startFractionalPosition, final double endFractionalPosition)
436             throws OtsGeometryException
437     {
438         OtsLine2d line = null;
439         for (Lane lane : lanes)
440         {
441             if (line == null && lane.equals(lanes.get(lanes.size() - 1)))
442             {
443                 if (endFractionalPosition < startFractionalPosition)
444                 {
445                     System.out.println("hmmm");
446                 }
447                 line = lane.getCenterLine().extractFractional(startFractionalPosition, endFractionalPosition);
448             }
449             else if (line == null)
450             {
451                 line = lane.getCenterLine().extractFractional(startFractionalPosition, 1.0);
452             }
453             else if (lane.equals(lanes.get(lanes.size() - 1)))
454             {
455                 line = OtsLine2d.concatenate(Lane.MARGIN.si, line,
456                         lane.getCenterLine().extractFractional(0.0, endFractionalPosition));
457             }
458             else
459             {
460                 line = OtsLine2d.concatenate(Lane.MARGIN.si, line, lane.getCenterLine());
461             }
462         }
463         return line;
464     }
465 
466     /**
467      * Checks whether the given GTU has sufficient space relative to a {@code Headway}.
468      * @param gtu LaneBasedGtu; gtu
469      * @param headway Headway; headway
470      * @return boolean; whether the given GTU has sufficient space relative to a {@code Headway}
471      */
472     public boolean checkRoom(final LaneBasedGtu gtu, final Headway headway)
473     {
474         if (this.desiredLaneChangeDuration == null)
475         {
476             this.desiredLaneChangeDuration = Try.assign(() -> gtu.getParameters().getParameter(ParameterTypes.LCDUR),
477                     "LaneChange; the desired lane change duration should be set or paramater LCDUR should be defined.");
478         }
479 
480         EgoPerception<?, ?> ego = gtu.getTacticalPlanner().getPerception().getPerceptionCategoryOrNull(EgoPerception.class);
481         Speed egoSpeed = ego == null ? gtu.getSpeed() : ego.getSpeed();
482         Acceleration egoAcceleration = ego == null ? gtu.getAcceleration() : ego.getAcceleration();
483         Speed speed = headway.getSpeed() == null ? Speed.ZERO : headway.getSpeed();
484         Acceleration acceleration = headway.getAcceleration() == null ? Acceleration.ZERO : headway.getAcceleration();
485         Length s0 = gtu.getParameters().getParameterOrNull(ParameterTypes.S0);
486         s0 = s0 == null ? Length.ZERO : s0;
487 
488         Length distanceToStop;
489         if (speed.eq0())
490         {
491             distanceToStop = Length.ZERO;
492         }
493         else
494         {
495             Acceleration b = gtu.getParameters().getParameterOrNull(ParameterTypes.B);
496             b = b == null ? Acceleration.ZERO : b.neg();
497             Acceleration a = Acceleration.min(Acceleration.max(b, acceleration.plus(b)), acceleration);
498             if (a.ge0())
499             {
500                 return true;
501             }
502             double t = speed.si / -a.si;
503             distanceToStop = Length.instantiateSI(speed.si * t + .5 * a.si * t * t);
504         }
505 
506         Length availableDistance = headway.getDistance().plus(distanceToStop);
507         double t = this.desiredLaneChangeDuration.si;
508         if (egoAcceleration.lt0())
509         {
510             t = Math.min(egoSpeed.si / -egoAcceleration.si, t);
511         }
512         Length requiredDistance = Length
513                 .max(Length.instantiateSI(egoSpeed.si * t + .5 * egoAcceleration.si * t * t), this.minimumLaneChangeDistance)
514                 .plus(s0);
515         return availableDistance.gt(requiredDistance);
516     }
517 
518     /** {@inheritDoc} */
519     @Override
520     public String toString()
521     {
522         return "LaneChange [fraction=" + this.fraction + ", laneChangeDirectionality=" + this.laneChangeDirectionality + "]";
523     }
524 
525     /**
526      * Provides a (partial) path during lane changes.
527      * <p>
528      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
529      * <br>
530      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
531      * </p>
532      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
533      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
534      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
535      */
536     public interface LaneChangePath
537     {
538 
539         /** Sine-shaped interpolation between center lines. */
540         LaneChangePath SINE_INTERP = new InterpolatedLaneChangePath()
541         {
542             /** {@inheritDoc} */
543             @Override
544             double longitudinalFraction(final double lateralFraction)
545             {
546                 return 1.0 - Math.acos(2.0 * (lateralFraction - 0.5)) / Math.PI;
547             }
548 
549             /** {@inheritDoc} */
550             @Override
551             double lateralFraction(final double longitudinalFraction)
552             {
553                 return 0.5 - 0.5 * Math.cos(longitudinalFraction * Math.PI);
554             }
555         };
556 
557         /** Linear interpolation between center lines. */
558         LaneChangePath LINEAR = new InterpolatedLaneChangePath()
559         {
560 
561             @Override
562             double longitudinalFraction(final double lateralFraction)
563             {
564                 return lateralFraction;
565             }
566 
567             @Override
568             double lateralFraction(final double longitudinalFraction)
569             {
570                 return longitudinalFraction;
571             }
572         };
573 
574         /** A simple Bezier curve directly to the lane change target position. */
575         LaneChangePath BEZIER = new LaneChangePath()
576         {
577             /** {@inheritDoc} */
578             @Override
579             public OtsLine2d getPath(final Duration timeStep, final Length planDistance, final Speed meanSpeed,
580                     final LanePosition from, final OrientedPoint2d startPosition,
581                     final LateralDirectionality laneChangeDirection, final OtsLine2d fromLine, final OtsLine2d toLine,
582                     final Duration laneChangeDuration, final double lcFraction) throws OtsGeometryException
583             {
584                 OrientedPoint2d target = toLine.getLocationFraction(1.0);
585                 return Bezier.cubic(64, startPosition, target, 0.5);
586             }
587         };
588 
589         /** The target point (including rotation) for the coming time step is based on a sine wave. */
590         LaneChangePath SINE = new SequentialLaneChangePath()
591         {
592             /** {@inheritDoc} */
593             @Override
594             protected double lateralFraction(final double lcFraction)
595             {
596                 return -1.0 / (2 * Math.PI) * Math.sin(2 * Math.PI * lcFraction) + lcFraction;
597             }
598 
599             /** {@inheritDoc} */
600             @Override
601             protected double angle(final double width, final double cumulLcLength, final double totalLcLength)
602             {
603                 return Math.atan((-width * Math.cos(2 * Math.PI * cumulLcLength / totalLcLength) / totalLcLength)
604                         + width / totalLcLength);
605             }
606         };
607 
608         /** The target point (including rotation) for the coming time step is based on a 3rd-degree polynomial. */
609         LaneChangePath POLY3 = new SequentialLaneChangePath()
610         {
611             /** {@inheritDoc} */
612             @Override
613             protected double lateralFraction(final double lcFraction)
614             {
615                 return 3 * (lcFraction * lcFraction) - 2 * (lcFraction * lcFraction * lcFraction);
616             }
617 
618             /** {@inheritDoc} */
619             @Override
620             protected double angle(final double width, final double cumulLcLength, final double totalLcLength)
621             {
622                 return Math.atan(cumulLcLength * 6 * width / (totalLcLength * totalLcLength)
623                         - cumulLcLength * cumulLcLength * 6 * width / (totalLcLength * totalLcLength * totalLcLength));
624             }
625         };
626 
627         /**
628          * A helper class to allow a lane change to follow a sequential determination of the target position (including
629          * rotation) for each time step.
630          * <p>
631          * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights
632          * reserved. <br>
633          * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
634          * <p>
635          * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
636          * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
637          * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
638          */
639         abstract class SequentialLaneChangePath implements LaneChangePath
640         {
641             /** {@inheritDoc} */
642             @Override
643             public OtsLine2d getPath(final Duration timeStep, final Length planDistance, final Speed meanSpeed,
644                     final LanePosition from, final OrientedPoint2d startPosition,
645                     final LateralDirectionality laneChangeDirection, final OtsLine2d fromLine, final OtsLine2d toLine,
646                     final Duration laneChangeDuration, final double lcFraction) throws OtsGeometryException
647             {
648                 OrientedPoint2d toTarget = toLine.getLocationFraction(1.0);
649                 OrientedPoint2d fromTarget = fromLine.getLocationFraction(1.0);
650                 double width = laneChangeDirection.isRight() ? fromTarget.distance(toTarget) : -fromTarget.distance(toTarget);
651                 double dFraction = timeStep.si / laneChangeDuration.si;
652                 return getPathRecursive(planDistance, meanSpeed, 1.0, width, from, startPosition, fromLine, toLine,
653                         laneChangeDuration, lcFraction, dFraction);
654             }
655 
656             /**
657              * Attempts to derive a path. If the resulting path is shorter than {@code planDistance} (e.g. lane change towards
658              * the inside of a curve), this method calls itself using a larger look-ahead distance.
659              * @param planDistance Length; plan distance
660              * @param meanSpeed Speed; mean speed during plan
661              * @param buffer double; buffer factor to assure sufficient path length is found, increased recursively
662              * @param width double; lateral deviation from from lanes at lane change end
663              * @param from LanePosition; current position on the from-lanes
664              * @param startPosition OrientedPoint2d; current 2D position
665              * @param fromLine OtsLine2d; from line
666              * @param toLine OtsLine2d; to line
667              * @param laneChangeDuration Duration; current considered duration of the entire lane change
668              * @param lcFraction double; lane change fraction at beginning of the plan
669              * @param dFraction double; additional lane change fraction to be made during the plan
670              * @return OtsLine2d a (partial) path for a lane change
671              * @throws OtsGeometryException on wrong fractional position
672              */
673             private OtsLine2d getPathRecursive(final Length planDistance, final Speed meanSpeed, final double buffer,
674                     final double width, final LanePosition from, final OrientedPoint2d startPosition, final OtsLine2d fromLine,
675                     final OtsLine2d toLine, final Duration laneChangeDuration, final double lcFraction, final double dFraction)
676                     throws OtsGeometryException
677             {
678                 // factor on path length to not overshoot a fraction of 1.0 in lane change progress, i.e. <1 if lane change will
679                 // be finished in the coming time step
680                 double cutoff = (1.0 - lcFraction) / (dFraction * buffer);
681                 cutoff = cutoff > 1.0 ? 1.0 : cutoff;
682 
683                 // lane change fraction at end of plan
684                 double lcFractionEnd = lcFraction + dFraction * buffer * cutoff;
685 
686                 // lateral fraction at that point according to shape
687                 double f = lateralFraction(lcFractionEnd);
688 
689                 // from-lane length
690                 double totalLcLength = meanSpeed.si * laneChangeDuration.si;
691                 double cumulLcLength = totalLcLength * lcFractionEnd;
692 
693                 // TODO: sequential is disabled as LaneChangePath now uses 2 OtsLine2d's instead of 2 List<Lane>'s. This was
694                 // done as the code using LaneChangePath (i.e. LaneChange) required more details on fractional positions itself.
695                 return null;
696             }
697 
698             /**
699              * Returns the fractional lateral deviation given a fraction of lane change being completed.
700              * @param lcFraction double; fraction of lane change
701              * @return double; lateral deviation
702              */
703             protected abstract double lateralFraction(double lcFraction);
704 
705             /**
706              * Returns the angle, relative to the lane center line, at the given cumulative length for a lane change of given
707              * total length and lateral deviation.
708              * @param width double; lateral deviation from from lanes at lane change end
709              * @param cumulLcLength double; cumulative length (along from lanes) covered so far
710              * @param totalLcLength double; total (along from lanes) length to cover in lane change
711              * @return double; angle, relative to the lane center line
712              */
713             protected abstract double angle(double width, double cumulLcLength, double totalLcLength);
714         }
715 
716         /**
717          * Helper class for interpolation between the from and to center lines.
718          * <p>
719          * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights
720          * reserved. <br>
721          * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
722          * <p>
723          * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
724          * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
725          * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
726          */
727         abstract class InterpolatedLaneChangePath implements LaneChangePath
728         {
729 
730             /** {@inheritDoc} */
731             @Override
732             public OtsLine2d getPath(final Duration timeStep, final Length planDistance, final Speed meanSpeed,
733                     final LanePosition from, final OrientedPoint2d startPosition,
734                     final LateralDirectionality laneChangeDirection, final OtsLine2d fromLine, final OtsLine2d toLine,
735                     final Duration laneChangeDuration, final double lcFraction) throws OtsGeometryException
736             {
737 
738                 double dx = fromLine.get(0).x - startPosition.x;
739                 double dy = fromLine.get(0).y - startPosition.y;
740                 double distFromLoc = Math.hypot(dx, dy);
741                 dx = fromLine.get(0).x - toLine.get(0).x;
742                 dy = fromLine.get(0).y - toLine.get(0).y;
743                 double distFromTo = Math.hypot(dx, dy);
744                 double startLateralFraction = distFromLoc / distFromTo;
745                 // Location is not on path in z-direction, so using .distance() create bugs
746                 // PK: added test for NaN (which occurs when fromLine and toLine start on top of each other.
747                 if (Double.isNaN(startLateralFraction) || startLateralFraction > 1.0)
748                 {
749                     startLateralFraction = 1.0;
750                 }
751                 double startLongitudinalFractionTotal = longitudinalFraction(startLateralFraction);
752 
753                 double nSegments = Math.ceil((64 * (1.0 - lcFraction)));
754                 List<Point2d> pointList = new ArrayList<>();
755                 // double zStart = (1.0 - startLateralFraction) * fromLine.get(0).z + startLateralFraction * toLine.get(0).z;
756                 pointList.add(startPosition);
757                 for (int i = 1; i <= nSegments; i++)
758                 {
759                     double f = i / nSegments;
760                     double longitudinalFraction = startLongitudinalFractionTotal + f * (1.0 - startLongitudinalFractionTotal);
761                     double lateralFraction = lateralFraction(longitudinalFraction);
762                     double lateralFractionInv = 1.0 - lateralFraction;
763                     OrientedPoint2d fromPoint = fromLine.getLocationFraction(f);
764                     OrientedPoint2d toPoint = toLine.getLocationFraction(f);
765                     pointList.add(new Point2d(lateralFractionInv * fromPoint.x + lateralFraction * toPoint.x,
766                             lateralFractionInv * fromPoint.y + lateralFraction * toPoint.y));
767                 }
768 
769                 OtsLine2d line = new OtsLine2d(pointList);
770                 // clean line for projection inconsistencies (position -> center lines -> interpolated new position)
771                 double angleChange = Math.abs(line.getLocation(Length.ZERO).getDirZ() - startPosition.getDirZ());
772                 int i = 1;
773                 while (angleChange > Math.PI / 4)
774                 {
775                     i++;
776                     if (i >= pointList.size() - 2)
777                     {
778                         // return original if we can't clean the line, perhaps extreme road curvature or line with 2 points
779                         return new OtsLine2d(pointList);
780                     }
781                     List<Point2d> newPointList = new ArrayList<>(pointList.subList(i, pointList.size()));
782                     newPointList.add(0, pointList.get(0));
783                     line = new OtsLine2d(newPointList);
784                     angleChange = Math.abs(line.getLocation(Length.ZERO).getDirZ() - startPosition.getDirZ());
785                 }
786                 return line;
787             }
788 
789             /**
790              * Transform lateral to longitudinal fraction.
791              * @param lateralFraction double; lateral fraction
792              * @return double; transformation of lateral to longitudinal fraction
793              */
794             abstract double longitudinalFraction(double lateralFraction);
795 
796             /**
797              * Transform longitudinal to lateral fraction.
798              * @param longitudinalFraction double; longitudinal fraction
799              * @return double; transformation of longitudinal to lateral fraction
800              */
801             abstract double lateralFraction(double longitudinalFraction);
802 
803         }
804 
805         /**
806          * Returns a (partial) path for a lane change. The method is called both at the start and during a lane change, and
807          * should return a valid path. This path should at least have a length of {@code planDistance}, unless the lane change
808          * will be finished during the coming time step. In that case, the caller of this method is to lengthen the path along
809          * the center line of the target lane.
810          * @param timeStep Duration; time step
811          * @param planDistance Length; distance covered during the operational plan
812          * @param meanSpeed Speed; mean speed during time step
813          * @param from LanePosition; current position on the from-lanes
814          * @param startPosition OrientedPoint2d; current 2D position
815          * @param laneChangeDirection LateralDirectionality; lane change direction
816          * @param fromLine OtsLine2d; from line
817          * @param toLine OtsLine2d; to line
818          * @param laneChangeDuration Duration; current considered duration of the entire lane change
819          * @param lcFraction double; fraction of lane change done so far
820          * @return OtsLine2d a (partial) path for a lane change
821          * @throws OtsGeometryException on wrong fractional position
822          */
823         OtsLine2d getPath(Duration timeStep, Length planDistance, Speed meanSpeed, LanePosition from,
824                 OrientedPoint2d startPosition, LateralDirectionality laneChangeDirection, OtsLine2d fromLine, OtsLine2d toLine,
825                 Duration laneChangeDuration, double lcFraction) throws OtsGeometryException;
826     }
827 }