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