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.exceptions.Throw;
15  import org.djutils.exceptions.Try;
16  import org.opentrafficsim.base.parameters.ParameterTypes;
17  import org.opentrafficsim.core.geometry.Bezier;
18  import org.opentrafficsim.core.geometry.DirectedPoint;
19  import org.opentrafficsim.core.geometry.OtsGeometryException;
20  import org.opentrafficsim.core.geometry.OtsLine3d;
21  import org.opentrafficsim.core.geometry.OtsLine3d.FractionalFallback;
22  import org.opentrafficsim.core.geometry.OtsPoint3d;
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-2023 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://dittlab.tudelft.nl">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 =
205                 dlp.getLane().accessibleAdjacentLanesPhysical(this.laneChangeDirectionality, gtu.getType());
206         if (!accessibleLanes.isEmpty() && map.containsKey(accessibleLanes.iterator().next()))
207         {
208             return isChangingLeft() ? RelativeLane.LEFT : RelativeLane.RIGHT;
209         }
210         return isChangingLeft() ? RelativeLane.RIGHT : RelativeLane.LEFT;
211     }
212 
213     /**
214      * Returns the path for a lane change. Lane change initialization and finalization events are automatically performed.
215      * @param timeStep Duration; plan time step
216      * @param gtu LaneBasedGtu; gtu
217      * @param from LanePosition; current position on the from lane (i.e. not necessarily the reference position)
218      * @param startPosition DirectedPoint; current position in 2D
219      * @param planDistance Length; absolute distance that will be covered during the time step
220      * @param laneChangeDirection LateralDirectionality; lane change direction
221      * @return OtsLine3d; path
222      * @throws OtsGeometryException on path or shape error
223      */
224     public final OtsLine3d getPath(final Duration timeStep, final LaneBasedGtu gtu, final LanePosition from,
225             final DirectedPoint startPosition, final Length planDistance, final LateralDirectionality laneChangeDirection)
226             throws OtsGeometryException
227     {
228         // initiate lane change
229         boolean favoured = false;
230         if (!isChangingLane())
231         {
232             favoured = true;
233             this.laneChangeDirectionality = laneChangeDirection;
234             Try.execute(() -> gtu.initLaneChange(laneChangeDirection), "Error during lane change initialization.");
235         }
236         // Wouter: why would we ever not favor a side during a lane change
237         favoured = true;
238 
239         // determine longitudinal distance along the from lanes
240         /*
241          * We take 3 factors in to account. The first two are 1) minimum physical lane change length, and 2) desired lane change
242          * duration. With the current mean speed of the plan, we take the maximum. So at very low speeds, the minimum physical
243          * length may increase the lane change duration. We also have 3) the maximum length before a lane change needs to have
244          * been performed. To overcome simulation troubles, we allow this to result in an even shorter length than the minimum
245          * physical distance. So: length = min( max("1", "2"), "3" ). These distances are all considered along the from-lanes.
246          * Actual path distance is different.
247          */
248         Speed meanSpeed = planDistance.divide(timeStep);
249         double minDuration = this.minimumLaneChangeDistance.si / meanSpeed.si;
250         double laneChangeDuration = Math.max(this.desiredLaneChangeDuration.si, minDuration);
251         if (this.boundary != null)
252         {
253             double maxDuration = this.boundary.si / meanSpeed.si;
254             laneChangeDuration = Math.min(laneChangeDuration, maxDuration);
255         }
256 
257         double totalLength = laneChangeDuration * meanSpeed.si;
258         double fromDist = (1.0 - this.fraction) * totalLength; // remaining distance along from lanes to lane change end
259         Throw.when(fromDist < 0.0, RuntimeException.class, "Lane change results in negative distance along from lanes.");
260 
261         // get fractional location there, build lane lists as we search over the distance
262         Lane fromLane = from.getLane();
263         List<Lane> fromLanes = new ArrayList<>();
264         List<Lane> toLanes = new ArrayList<>();
265         fromLanes.add(fromLane);
266         toLanes.add(fromLane.getAdjacentLane(this.laneChangeDirectionality, gtu));
267         double endPosFrom = from.getPosition().si + fromDist;
268         while (endPosFrom + gtu.getFront().getDx().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                 // there are no lanes to move on, restrict lane change length/duration (given fixed mean speed)
296                 double endFromPosLimit = fromLane.getLength().si - gtu.getFront().getDx().si;
297                 double f = 1.0 - (endPosFrom - endFromPosLimit) / fromDist;
298                 laneChangeDuration *= f;
299                 endPosFrom = endFromPosLimit;
300                 break;
301             }
302             endPosFrom -= fromLane.getLength().si;
303             Lane nextToLane = nextFromLane.getAdjacentLane(this.laneChangeDirectionality, gtu);
304             if (nextToLane == null)
305             {
306                 // there are no lanes to change to, restrict lane change length/duration (given fixed mean speed)
307                 double endFromPosLimit = fromLane.getLength().si - gtu.getFront().getDx().si;
308                 double f = 1.0 - (endPosFrom - endFromPosLimit) / fromDist;
309                 laneChangeDuration *= f;
310                 endPosFrom = endFromPosLimit;
311                 break;
312             }
313             fromLane = nextFromLane;
314             fromLanes.add(fromLane);
315             toLanes.add(nextToLane);
316         }
317         // for long vehicles and short lanes, revert
318         while (endPosFrom < 0.0)
319         {
320             fromLanes.remove(fromLanes.size() - 1);
321             toLanes.remove(toLanes.size() - 1);
322             fromLane = fromLanes.get(fromLanes.size() - 1);
323             endPosFrom += fromLane.getLength().si;
324         }
325         // finally, get location at the final lane available
326         double endFractionalPositionFrom = fromLane.fractionAtCoveredDistance(Length.instantiateSI(endPosFrom));
327 
328         LanePosition fromAdjusted = from;
329         while (fromAdjusted.getPosition().gt(fromAdjusted.getLane().getLength()))
330         {
331             // the from position is beyond the first lane (can occur if it is not the ref position)
332             fromLanes.remove(0);
333             toLanes.remove(0);
334             Length beyond = fromAdjusted.getPosition().minus(fromAdjusted.getLane().getLength());
335             Length pos = beyond;
336             fromAdjusted = Try.assign(() -> new LanePosition(fromLanes.get(0), pos), OtsGeometryException.class,
337                     "Info for lane is null.");
338         }
339 
340         // get path from shape
341 
342         // create center lines
343         double startFractionalPositionFrom = from.getPosition().si / from.getLane().getLength().si;
344         OtsLine3d fromLine = getLine(fromLanes, startFractionalPositionFrom, endFractionalPositionFrom);
345         // project for toLane
346         double startFractionalPositionTo = toLanes.get(0).getCenterLine().projectFractional(null, null, startPosition.x,
347                 startPosition.y, FractionalFallback.ENDPOINT);
348         int last = fromLanes.size() - 1;
349         double frac = endFractionalPositionFrom;
350         DirectedPoint p = fromLanes.get(last).getCenterLine().getLocationFraction(frac);
351         double endFractionalPositionTo =
352                 toLanes.get(last).getCenterLine().projectFractional(null, null, p.x, p.y, FractionalFallback.ENDPOINT);
353         startFractionalPositionTo = startFractionalPositionTo >= 0.0 ? startFractionalPositionTo : 0.0;
354         endFractionalPositionTo = endFractionalPositionTo <= 1.0 ? endFractionalPositionTo : 1.0;
355         endFractionalPositionTo = endFractionalPositionTo <= 0.0 ? endFractionalPositionFrom : endFractionalPositionTo;
356         // check for poor projection (end location is difficult to project; we have no path yet so we use the from lane)
357         if (fromLanes.size() == 1 && endFractionalPositionTo <= startFractionalPositionTo)
358         {
359             endFractionalPositionTo = Math.min(Math.max(startFractionalPositionTo + 0.001, endFractionalPositionFrom), 1.0);
360         }
361         if (startFractionalPositionTo >= 1.0)
362         {
363             toLanes.remove(0);
364             startFractionalPositionTo = 0.0;
365         }
366         OtsLine3d toLine = getLine(toLanes, startFractionalPositionTo, endFractionalPositionTo);
367 
368         OtsLine3d path = this.laneChangePath.getPath(timeStep, planDistance, meanSpeed, fromAdjusted, startPosition,
369                 laneChangeDirection, fromLine, toLine, Duration.instantiateSI(laneChangeDuration), this.fraction);
370         // update
371         // TODO: this assumes the time step will not be interrupted
372         this.fraction += timeStep.si / laneChangeDuration; // the total fraction this step increases
373 
374         // deal with lane change end
375         double requiredLength = planDistance.si - path.getLength().si;
376         if (requiredLength > 0.0 || this.fraction > 0.999)
377         {
378             try
379             {
380                 gtu.getSimulator().scheduleEventNow(BUILDER, "scheduleLaneChangeFinalization",
381                         new Object[] {gtu, Length.min(planDistance, path.getLength()), laneChangeDirection});
382             }
383             catch (SimRuntimeException exception)
384             {
385                 throw new RuntimeException("Error during lane change finalization.", exception);
386             }
387             // add length to path on to lanes
388             if (requiredLength > 0.0)
389             {
390                 Lane toLane = toLanes.get(toLanes.size() - 1);
391                 int n = path.size();
392                 // ignore remainder of first lane if fraction is at the end of the lane
393                 if (0.0 < endFractionalPositionFrom && endFractionalPositionFrom < 1.0)
394                 {
395                     OtsLine3d remainder = toLane.getCenterLine().extractFractional(endFractionalPositionTo, 1.0);
396                     path = OtsLine3d.concatenate(0.001, path, remainder);
397                     requiredLength = planDistance.si - path.getLength().si;
398                 }
399                 // add further lanes
400                 while (toLane != null && requiredLength > 0.0)
401                 {
402                     toLane = gtu.getNextLaneForRoute(toLane);
403                     if (toLane != null) // let's hope we will move on to a sink
404                     {
405                         OtsLine3d remainder = toLane.getCenterLine();
406                         path = OtsLine3d.concatenate(Lane.MARGIN.si, path, remainder);
407                         requiredLength = planDistance.si - path.getLength().si + Lane.MARGIN.si;
408                     }
409                 }
410                 // filter near-duplicate point which results in projection exceptions
411                 if (this.fraction > 0.999) // this means point 'target' is essentially at the design line
412                 {
413                     OtsPoint3d[] points = new OtsPoint3d[path.size() - 1];
414                     System.arraycopy(path.getPoints(), 0, points, 0, n - 1);
415                     System.arraycopy(path.getPoints(), n, points, n - 1, path.size() - n);
416                     path = new OtsLine3d(points);
417                 }
418             }
419             // reset lane change
420             this.laneChangeDirectionality = null;
421             this.boundary = null;
422             this.fraction = 0.0;
423         }
424 
425         return path;
426     }
427 
428     /**
429      * Returns a line from the lane center lines, cutting of at the from position and the end fractional position.
430      * @param lanes List&lt;Lane&gt;; lanes
431      * @param startFractionalPosition double; current fractional GTU position on first lane
432      * @param endFractionalPosition double; target fractional GTU position on last lane
433      * @return OtsLine3d; line from the lane center lines
434      * @throws OtsGeometryException on fraction outside of range
435      */
436     private OtsLine3d getLine(final List<Lane> lanes, final double startFractionalPosition, final double endFractionalPosition)
437             throws OtsGeometryException
438     {
439         OtsLine3d line = null;
440         for (Lane lane : lanes)
441         {
442             if (line == null && lane.equals(lanes.get(lanes.size() - 1)))
443             {
444                 line = lane.getCenterLine().extractFractional(startFractionalPosition, endFractionalPosition);
445             }
446             else if (line == null)
447             {
448                 line = lane.getCenterLine().extractFractional(startFractionalPosition, 1.0);
449             }
450             else if (lane.equals(lanes.get(lanes.size() - 1)))
451             {
452                 line = OtsLine3d.concatenate(Lane.MARGIN.si, line,
453                         lane.getCenterLine().extractFractional(0.0, endFractionalPosition));
454             }
455             else
456             {
457                 line = OtsLine3d.concatenate(Lane.MARGIN.si, line, lane.getCenterLine());
458             }
459         }
460         return line;
461     }
462 
463     /**
464      * Checks whether the given GTU has sufficient space relative to a {@code Headway}.
465      * @param gtu LaneBasedGtu; gtu
466      * @param headway Headway; headway
467      * @return boolean; whether the given GTU has sufficient space relative to a {@code Headway}
468      */
469     public boolean checkRoom(final LaneBasedGtu gtu, final Headway headway)
470     {
471         if (this.desiredLaneChangeDuration == null)
472         {
473             this.desiredLaneChangeDuration = Try.assign(() -> gtu.getParameters().getParameter(ParameterTypes.LCDUR),
474                     "LaneChange; the desired lane change duration should be set or paramater LCDUR should be defined.");
475         }
476 
477         EgoPerception<?, ?> ego = gtu.getTacticalPlanner().getPerception().getPerceptionCategoryOrNull(EgoPerception.class);
478         Speed egoSpeed = ego == null ? gtu.getSpeed() : ego.getSpeed();
479         Acceleration egoAcceleration = ego == null ? gtu.getAcceleration() : ego.getAcceleration();
480         Speed speed = headway.getSpeed() == null ? Speed.ZERO : headway.getSpeed();
481         Acceleration acceleration = headway.getAcceleration() == null ? Acceleration.ZERO : headway.getAcceleration();
482         Length s0 = gtu.getParameters().getParameterOrNull(ParameterTypes.S0);
483         s0 = s0 == null ? Length.ZERO : s0;
484 
485         Length distanceToStop;
486         if (speed.eq0())
487         {
488             distanceToStop = Length.ZERO;
489         }
490         else
491         {
492             Acceleration b = gtu.getParameters().getParameterOrNull(ParameterTypes.B);
493             b = b == null ? Acceleration.ZERO : b.neg();
494             Acceleration a = Acceleration.min(Acceleration.max(b, acceleration.plus(b)), acceleration);
495             if (a.ge0())
496             {
497                 return true;
498             }
499             double t = speed.si / -a.si;
500             distanceToStop = Length.instantiateSI(speed.si * t + .5 * a.si * t * t);
501         }
502 
503         Length availableDistance = headway.getDistance().plus(distanceToStop);
504         double t = this.desiredLaneChangeDuration.si;
505         if (egoAcceleration.lt0())
506         {
507             t = Math.min(egoSpeed.si / -egoAcceleration.si, t);
508         }
509         Length requiredDistance = Length
510                 .max(Length.instantiateSI(egoSpeed.si * t + .5 * egoAcceleration.si * t * t), this.minimumLaneChangeDistance)
511                 .plus(s0);
512         return availableDistance.gt(requiredDistance);
513     }
514 
515     /** {@inheritDoc} */
516     @Override
517     public String toString()
518     {
519         return "LaneChange [fraction=" + this.fraction + ", laneChangeDirectionality=" + this.laneChangeDirectionality + "]";
520     }
521 
522     /**
523      * Provides a (partial) path during lane changes.
524      * <p>
525      * Copyright (c) 2013-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
526      * <br>
527      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
528      * </p>
529      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
530      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
531      * @author <a href="https://dittlab.tudelft.nl">Wouter Schakel</a>
532      */
533     public interface LaneChangePath
534     {
535 
536         /** Sine-shaped interpolation between center lines. */
537         LaneChangePath SINE_INTERP = new InterpolatedLaneChangePath()
538         {
539             /** {@inheritDoc} */
540             @Override
541             double longitudinalFraction(final double lateralFraction)
542             {
543                 return 1.0 - Math.acos(2.0 * (lateralFraction - 0.5)) / Math.PI;
544             }
545 
546             /** {@inheritDoc} */
547             @Override
548             double lateralFraction(final double longitudinalFraction)
549             {
550                 return 0.5 - 0.5 * Math.cos(longitudinalFraction * Math.PI);
551             }
552         };
553 
554         /** Linear interpolation between center lines. */
555         LaneChangePath LINEAR = new InterpolatedLaneChangePath()
556         {
557 
558             @Override
559             double longitudinalFraction(final double lateralFraction)
560             {
561                 return lateralFraction;
562             }
563 
564             @Override
565             double lateralFraction(final double longitudinalFraction)
566             {
567                 return longitudinalFraction;
568             }
569         };
570 
571         /** A simple Bezier curve directly to the lane change target position. */
572         LaneChangePath BEZIER = new LaneChangePath()
573         {
574             /** {@inheritDoc} */
575             @Override
576             public OtsLine3d getPath(final Duration timeStep, final Length planDistance, final Speed meanSpeed,
577                     final LanePosition from, final DirectedPoint startPosition, final LateralDirectionality laneChangeDirection,
578                     final OtsLine3d fromLine, final OtsLine3d toLine, final Duration laneChangeDuration,
579                     final double lcFraction) throws OtsGeometryException
580             {
581                 DirectedPoint target = toLine.getLocationFraction(1.0);
582                 return Bezier.cubic(64, startPosition, target, 0.5);
583             }
584         };
585 
586         /** The target point (including rotation) for the coming time step is based on a sine wave. */
587         LaneChangePath SINE = new SequentialLaneChangePath()
588         {
589             /** {@inheritDoc} */
590             @Override
591             protected double lateralFraction(final double lcFraction)
592             {
593                 return -1.0 / (2 * Math.PI) * Math.sin(2 * Math.PI * lcFraction) + lcFraction;
594             }
595 
596             /** {@inheritDoc} */
597             @Override
598             protected double angle(final double width, final double cumulLcLength, final double totalLcLength)
599             {
600                 return Math.atan((-width * Math.cos(2 * Math.PI * cumulLcLength / totalLcLength) / totalLcLength)
601                         + width / totalLcLength);
602             }
603         };
604 
605         /** The target point (including rotation) for the coming time step is based on a 3rd-degree polynomial. */
606         LaneChangePath POLY3 = new SequentialLaneChangePath()
607         {
608             /** {@inheritDoc} */
609             @Override
610             protected double lateralFraction(final double lcFraction)
611             {
612                 return 3 * (lcFraction * lcFraction) - 2 * (lcFraction * lcFraction * lcFraction);
613             }
614 
615             /** {@inheritDoc} */
616             @Override
617             protected double angle(final double width, final double cumulLcLength, final double totalLcLength)
618             {
619                 return Math.atan(cumulLcLength * 6 * width / (totalLcLength * totalLcLength)
620                         - cumulLcLength * cumulLcLength * 6 * width / (totalLcLength * totalLcLength * totalLcLength));
621             }
622         };
623 
624         /**
625          * A helper class to allow a lane change to follow a sequential determination of the target position (including
626          * rotation) for each time step.
627          * <p>
628          * Copyright (c) 2013-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights
629          * reserved. <br>
630          * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
631          * <p>
632          * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
633          * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
634          * @author <a href="https://dittlab.tudelft.nl">Wouter Schakel</a>
635          */
636         abstract class SequentialLaneChangePath implements LaneChangePath
637         {
638             /** {@inheritDoc} */
639             @Override
640             public OtsLine3d getPath(final Duration timeStep, final Length planDistance, final Speed meanSpeed,
641                     final LanePosition from, final DirectedPoint startPosition, final LateralDirectionality laneChangeDirection,
642                     final OtsLine3d fromLine, final OtsLine3d toLine, final Duration laneChangeDuration,
643                     final double lcFraction) throws OtsGeometryException
644             {
645                 DirectedPoint toTarget = toLine.getLocationFraction(1.0);
646                 DirectedPoint fromTarget = fromLine.getLocationFraction(1.0);
647                 double width = laneChangeDirection.isRight() ? fromTarget.distance(toTarget) : -fromTarget.distance(toTarget);
648                 double dFraction = timeStep.si / laneChangeDuration.si;
649                 return getPathRecursive(planDistance, meanSpeed, 1.0, width, from, startPosition, fromLine, toLine,
650                         laneChangeDuration, lcFraction, dFraction);
651             }
652 
653             /**
654              * Attempts to derive a path. If the resulting path is shorter than {@code planDistance} (e.g. lane change towards
655              * the inside of a curve), this method calls itself using a larger look-ahead distance.
656              * @param planDistance Length; plan distance
657              * @param meanSpeed Speed; mean speed during plan
658              * @param buffer double; buffer factor to assure sufficient path length is found, increased recursively
659              * @param width double; lateral deviation from from lanes at lane change end
660              * @param from LanePosition; current position on the from-lanes
661              * @param startPosition DirectedPoint; current 2D position
662              * @param fromLine OtsLine3d; from line
663              * @param toLine OtsLine3d; to line
664              * @param laneChangeDuration Duration; current considered duration of the entire lane change
665              * @param lcFraction double; lane change fraction at beginning of the plan
666              * @param dFraction double; additional lane change fraction to be made during the plan
667              * @return OtsLine3d a (partial) path for a lane change
668              * @throws OtsGeometryException on wrong fractional position
669              */
670             private OtsLine3d getPathRecursive(final Length planDistance, final Speed meanSpeed, final double buffer,
671                     final double width, final LanePosition from, final DirectedPoint startPosition, final OtsLine3d fromLine,
672                     final OtsLine3d toLine, final Duration laneChangeDuration, final double lcFraction, final double dFraction)
673                     throws OtsGeometryException
674             {
675                 // factor on path length to not overshoot a fraction of 1.0 in lane change progress, i.e. <1 if lane change will
676                 // be finished in the coming time step
677                 double cutoff = (1.0 - lcFraction) / (dFraction * buffer);
678                 cutoff = cutoff > 1.0 ? 1.0 : cutoff;
679 
680                 // lane change fraction at end of plan
681                 double lcFractionEnd = lcFraction + dFraction * buffer * cutoff;
682 
683                 // lateral fraction at that point according to shape
684                 double f = lateralFraction(lcFractionEnd);
685 
686                 // from-lane length
687                 double totalLcLength = meanSpeed.si * laneChangeDuration.si;
688                 double cumulLcLength = totalLcLength * lcFractionEnd;
689 
690                 // TODO: sequential is disabled as LaneChangePath now uses 2 OtsLine3d's instead of 2 List<Lane>'s. This was
691                 // done as the code using LaneChangePath (i.e. LaneChange) required more details on fractional positions itself.
692                 return null;
693             }
694 
695             /**
696              * Returns the fractional lateral deviation given a fraction of lane change being completed.
697              * @param lcFraction double; fraction of lane change
698              * @return double; lateral deviation
699              */
700             protected abstract double lateralFraction(double lcFraction);
701 
702             /**
703              * Returns the angle, relative to the lane center line, at the given cumulative length for a lane change of given
704              * total length and lateral deviation.
705              * @param width double; lateral deviation from from lanes at lane change end
706              * @param cumulLcLength double; cumulative length (along from lanes) covered so far
707              * @param totalLcLength double; total (along from lanes) length to cover in lane change
708              * @return double; angle, relative to the lane center line
709              */
710             protected abstract double angle(double width, double cumulLcLength, double totalLcLength);
711         }
712 
713         /**
714          * Helper class for interpolation between the from and to center lines.
715          * <p>
716          * Copyright (c) 2013-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights
717          * reserved. <br>
718          * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
719          * <p>
720          * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
721          * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
722          * @author <a href="https://dittlab.tudelft.nl">Wouter Schakel</a>
723          */
724         abstract class InterpolatedLaneChangePath implements LaneChangePath
725         {
726 
727             /** {@inheritDoc} */
728             @Override
729             public OtsLine3d getPath(final Duration timeStep, final Length planDistance, final Speed meanSpeed,
730                     final LanePosition from, final DirectedPoint startPosition, final LateralDirectionality laneChangeDirection,
731                     final OtsLine3d fromLine, final OtsLine3d toLine, final Duration laneChangeDuration,
732                     final double lcFraction) throws OtsGeometryException
733             {
734 
735                 double dx = fromLine.get(0).getLocation().x - startPosition.x;
736                 double dy = fromLine.get(0).getLocation().y - startPosition.y;
737                 double distFromLoc = Math.sqrt(dx * dx + dy * dy);
738                 dx = fromLine.get(0).getLocation().x - toLine.get(0).getLocation().x;
739                 dy = fromLine.get(0).getLocation().y - toLine.get(0).getLocation().y;
740                 double distFromTo = Math.sqrt(dx * dx + dy * dy);
741                 double startLateralFraction = distFromLoc / distFromTo;
742                 // Location is not on path in z-direction, so using .distance() create bugs
743                 // PK: added test for NaN (which occurs when fromLine and toLine start on top of each other.
744                 if (Double.isNaN(startLateralFraction) || startLateralFraction > 1.0)
745                 {
746                     startLateralFraction = 1.0;
747                 }
748                 double startLongitudinalFractionTotal = longitudinalFraction(startLateralFraction);
749 
750                 double nSegments = Math.ceil((64 * (1.0 - lcFraction)));
751                 List<OtsPoint3d> pointList = new ArrayList<>();
752                 double zStart = (1.0 - startLateralFraction) * fromLine.get(0).z + startLateralFraction * toLine.get(0).z;
753                 pointList.add(new OtsPoint3d(startPosition.x, startPosition.y, zStart));
754                 for (int i = 1; i <= nSegments; i++)
755                 {
756                     double f = i / nSegments;
757                     double longitudinalFraction = startLongitudinalFractionTotal + f * (1.0 - startLongitudinalFractionTotal);
758                     double lateralFraction = lateralFraction(longitudinalFraction);
759                     double lateralFractionInv = 1.0 - lateralFraction;
760                     DirectedPoint fromPoint = fromLine.getLocationFraction(f);
761                     DirectedPoint toPoint = toLine.getLocationFraction(f);
762                     pointList.add(new OtsPoint3d(lateralFractionInv * fromPoint.x + lateralFraction * toPoint.x,
763                             lateralFractionInv * fromPoint.y + lateralFraction * toPoint.y,
764                             lateralFractionInv * fromPoint.z + lateralFraction * toPoint.z));
765                 }
766 
767                 OtsLine3d line = new OtsLine3d(pointList);
768                 // clean line for projection inconsistencies (position -> center lines -> interpolated new position)
769                 double angleChange = Math.abs(line.getLocation(Length.ZERO).getRotZ() - startPosition.getRotZ());
770                 int i = 1;
771                 while (angleChange > Math.PI / 4)
772                 {
773                     i++;
774                     if (i >= pointList.size() - 2)
775                     {
776                         // return original if we can't clean the line, perhaps extreme road curvature or line with 2 points
777                         return new OtsLine3d(pointList);
778                     }
779                     List<OtsPoint3d> newPointList = new ArrayList<>(pointList.subList(i, pointList.size()));
780                     newPointList.add(0, pointList.get(0));
781                     line = new OtsLine3d(newPointList);
782                     angleChange = Math.abs(line.getLocation(Length.ZERO).getRotZ() - startPosition.getRotZ());
783                 }
784                 return line;
785             }
786 
787             /**
788              * Transform lateral to longitudinal fraction.
789              * @param lateralFraction double; lateral fraction
790              * @return double; transformation of lateral to longitudinal fraction
791              */
792             abstract double longitudinalFraction(double lateralFraction);
793 
794             /**
795              * Transform longitudinal to lateral fraction.
796              * @param longitudinalFraction double; longitudinal fraction
797              * @return double; transformation of longitudinal to lateral fraction
798              */
799             abstract double lateralFraction(double longitudinalFraction);
800 
801         }
802 
803         /**
804          * Returns a (partial) path for a lane change. The method is called both at the start and during a lane change, and
805          * should return a valid path. This path should at least have a length of {@code planDistance}, unless the lane change
806          * will be finished during the coming time step. In that case, the caller of this method is to lengthen the path along
807          * the center line of the target lane.
808          * @param timeStep Duration; time step
809          * @param planDistance Length; distance covered during the operational plan
810          * @param meanSpeed Speed; mean speed during time step
811          * @param from LanePosition; current position on the from-lanes
812          * @param startPosition DirectedPoint; current 2D position
813          * @param laneChangeDirection LateralDirectionality; lane change direction
814          * @param fromLine OtsLine3d; from line
815          * @param toLine OtsLine3d; to line
816          * @param laneChangeDuration Duration; current considered duration of the entire lane change
817          * @param lcFraction double; fraction of lane change done so far
818          * @return OtsLine3d a (partial) path for a lane change
819          * @throws OtsGeometryException on wrong fractional position
820          */
821         OtsLine3d getPath(Duration timeStep, Length planDistance, Speed meanSpeed, LanePosition from,
822                 DirectedPoint startPosition, LateralDirectionality laneChangeDirection, OtsLine3d fromLine, OtsLine3d toLine,
823                 Duration laneChangeDuration, double lcFraction) throws OtsGeometryException;
824     }
825 }