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