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-2019 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 
377         // update
378         // TODO: this assumes the time step will not be interrupted
379         this.fraction += timeStep.si / laneChangeDuration; // the total fraction this step increases
380 
381         // deal with lane change end
382         double requiredLength = planDistance.si - path.getLength().si;
383         if (requiredLength > 0.0 || this.fraction > 0.999)
384         {
385             try
386             {
387                 gtu.getSimulator().scheduleEventNow(gtu, BUILDER, "scheduleLaneChangeFinalization",
388                         new Object[] { gtu, Length.min(planDistance, path.getLength()), laneChangeDirection });
389             }
390             catch (SimRuntimeException exception)
391             {
392                 throw new RuntimeException("Error during lane change finalization.", exception);
393             }
394             // add length to path on to lanes
395             if (requiredLength > 0.0)
396             {
397                 LaneDirection toLane = toLanes.get(toLanes.size() - 1);
398                 int n = path.size();
399                 // ignore remainder of first lane if fraction is at the end of the lane
400                 if (0.0 < endFractionalPositionFrom && endFractionalPositionFrom < 1.0)
401                 {
402                     OTSLine3D remainder = toLane.getDirection().isPlus()
403                             ? toLane.getLane().getCenterLine().extractFractional(endFractionalPositionTo, 1.0)
404                             : toLane.getLane().getCenterLine().extractFractional(0.0, endFractionalPositionTo).reverse();
405                     path = OTSLine3D.concatenate(0.001, path, remainder);
406                     requiredLength = planDistance.si - path.getLength().si;
407                 }
408                 // add further lanes
409                 while (toLane != null && requiredLength > 0.0)
410                 {
411                     toLane = toLane.getNextLaneDirection(gtu);
412                     if (toLane != null) // let's hope we will move on to a sink
413                     {
414                         OTSLine3D remainder = toLane.getDirection().isPlus() ? toLane.getLane().getCenterLine()
415                                 : toLane.getLane().getCenterLine().reverse();
416                         path = OTSLine3D.concatenate(Lane.MARGIN.si, path, remainder);
417                         requiredLength = planDistance.si - path.getLength().si + Lane.MARGIN.si;
418                     }
419                 }
420                 // filter near-duplicate point which results in projection exceptions
421                 if (this.fraction > 0.999) // this means point 'target' is essentially at the design line
422                 {
423                     OTSPoint3D[] points = new OTSPoint3D[path.size() - 1];
424                     System.arraycopy(path.getPoints(), 0, points, 0, n - 1);
425                     System.arraycopy(path.getPoints(), n, points, n - 1, path.size() - n);
426                     path = new OTSLine3D(points);
427                 }
428             }
429             // reset lane change
430             this.laneChangeDirectionality = null;
431             this.boundary = null;
432             this.fraction = 0.0;
433         }
434 
435         return path;
436     }
437 
438     /**
439      * Returns a line from the lane center lines, cutting of at the from position and the end fractional position.
440      * @param lanes List&lt;LaneDirection&gt;; lanes
441      * @param startFractionalPosition double; current fractional GTU position on first lane
442      * @param endFractionalPosition double; target fractional GTU position on last lane
443      * @return OTSLine3D; line from the lane center lines
444      * @throws OTSGeometryException on fraction outside of range
445      */
446     private OTSLine3D getLine(final List<LaneDirection> lanes, final double startFractionalPosition,
447             final double endFractionalPosition) throws OTSGeometryException
448     {
449         OTSLine3D line = null;
450         for (LaneDirection lane : lanes)
451         {
452             if (line == null && lane.equals(lanes.get(lanes.size() - 1)))
453             {
454                 if (lane.getDirection().isPlus())
455                 {
456                     line = lane.getLane().getCenterLine().extractFractional(startFractionalPosition, endFractionalPosition);
457                 }
458                 else
459                 {
460                     line = lane.getLane().getCenterLine().extractFractional(startFractionalPosition, endFractionalPosition)
461                             .reverse();
462                 }
463             }
464             else if (line == null)
465             {
466                 if (lane.getDirection().isPlus())
467                 {
468                     line = lane.getLane().getCenterLine().extractFractional(startFractionalPosition, 1.0);
469                 }
470                 else
471                 {
472                     line = lane.getLane().getCenterLine().extractFractional(0.0, startFractionalPosition).reverse();
473                 }
474             }
475             else if (lane.equals(lanes.get(lanes.size() - 1)))
476             {
477                 if (lane.getDirection().isPlus())
478                 {
479                     line = OTSLine3D.concatenate(Lane.MARGIN.si, line,
480                             lane.getLane().getCenterLine().extractFractional(0.0, endFractionalPosition));
481                 }
482                 else
483                 {
484                     line = OTSLine3D.concatenate(Lane.MARGIN.si, line,
485                             lane.getLane().getCenterLine().extractFractional(endFractionalPosition, 1.0).reverse());
486                 }
487             }
488             else
489             {
490                 if (lane.getDirection().isPlus())
491                 {
492                     line = OTSLine3D.concatenate(Lane.MARGIN.si, line, lane.getLane().getCenterLine());
493                 }
494                 else
495                 {
496                     line = OTSLine3D.concatenate(Lane.MARGIN.si, line, lane.getLane().getCenterLine().reverse());
497                 }
498             }
499         }
500         return line;
501     }
502 
503     /**
504      * Checks whether the given GTU has sufficient space relative to a {@code Headway}.
505      * @param gtu LaneBasedGTU; gtu
506      * @param headway Headway; headway
507      * @return boolean; whether the given GTU has sufficient space relative to a {@code Headway}
508      */
509     public boolean checkRoom(final LaneBasedGTU gtu, final Headway headway)
510     {
511         if (this.desiredLaneChangeDuration == null)
512         {
513             this.desiredLaneChangeDuration = Try.assign(() -> gtu.getParameters().getParameter(ParameterTypes.LCDUR),
514                     "LaneChange; the desired lane change duration should be set or paramater LCDUR should be defined.");
515         }
516 
517         EgoPerception<?, ?> ego = gtu.getTacticalPlanner().getPerception().getPerceptionCategoryOrNull(EgoPerception.class);
518         Speed egoSpeed = ego == null ? gtu.getSpeed() : ego.getSpeed();
519         Acceleration egoAcceleration = ego == null ? gtu.getAcceleration() : ego.getAcceleration();
520         Speed speed = headway.getSpeed() == null ? Speed.ZERO : headway.getSpeed();
521         Acceleration acceleration = headway.getAcceleration() == null ? Acceleration.ZERO : headway.getAcceleration();
522         Length s0 = gtu.getParameters().getParameterOrNull(ParameterTypes.S0);
523         s0 = s0 == null ? Length.ZERO : s0;
524 
525         Length distanceToStop;
526         if (speed.eq0())
527         {
528             distanceToStop = Length.ZERO;
529         }
530         else
531         {
532             Acceleration b = gtu.getParameters().getParameterOrNull(ParameterTypes.B);
533             b = b == null ? Acceleration.ZERO : b.neg();
534             Acceleration a = Acceleration.min(Acceleration.max(b, acceleration.plus(b)), acceleration);
535             if (a.ge0())
536             {
537                 return true;
538             }
539             double t = speed.si / -a.si;
540             distanceToStop = Length.instantiateSI(speed.si * t + .5 * a.si * t * t);
541         }
542 
543         Length availableDistance = headway.getDistance().plus(distanceToStop);
544         double t = this.desiredLaneChangeDuration.si;
545         if (egoAcceleration.lt0())
546         {
547             t = Math.min(egoSpeed.si / -egoAcceleration.si, t);
548         }
549         Length requiredDistance =
550                 Length.max(Length.instantiateSI(egoSpeed.si * t + .5 * egoAcceleration.si * t * t), this.minimumLaneChangeDistance)
551                         .plus(s0);
552         return availableDistance.gt(requiredDistance);
553     }
554 
555     /** {@inheritDoc} */
556     @Override
557     public String toString()
558     {
559         return "LaneChange [fraction=" + this.fraction + ", laneChangeDirectionality=" + this.laneChangeDirectionality + "]";
560     }
561 
562     /**
563      * Provides a (partial) path during lane changes.
564      * <p>
565      * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
566      * <br>
567      * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
568      * <p>
569      * @version $Revision$, $LastChangedDate$, by $Author$, initial version 30 apr. 2018 <br>
570      * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
571      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
572      * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
573      */
574     public interface LaneChangePath
575     {
576 
577         /** Sine-shaped interpolation between center lines. */
578         LaneChangePath SINE_INTERP = new InterpolatedLaneChangePath()
579         {
580             /** {@inheritDoc} */
581             @Override
582             double longitudinalFraction(final double lateralFraction)
583             {
584                 return 1.0 - Math.acos(2.0 * (lateralFraction - 0.5)) / Math.PI;
585             }
586 
587             /** {@inheritDoc} */
588             @Override
589             double lateralFraction(final double longitudinalFraction)
590             {
591                 return 0.5 - 0.5 * Math.cos(longitudinalFraction * Math.PI);
592             }
593         };
594 
595         /** Linear interpolation between center lines. */
596         LaneChangePath LINEAR = new InterpolatedLaneChangePath()
597         {
598 
599             @Override
600             double longitudinalFraction(final double lateralFraction)
601             {
602                 return lateralFraction;
603             }
604 
605             @Override
606             double lateralFraction(final double longitudinalFraction)
607             {
608                 return longitudinalFraction;
609             }
610         };
611 
612         /** A simple Bezier curve directly to the lane change target position. */
613         LaneChangePath BEZIER = new LaneChangePath()
614         {
615             /** {@inheritDoc} */
616             @Override
617             public OTSLine3D getPath(final Duration timeStep, final Length planDistance, final Speed meanSpeed,
618                     final DirectedLanePosition from, final DirectedPoint startPosition,
619                     final LateralDirectionality laneChangeDirection, final OTSLine3D fromLine, final OTSLine3D toLine,
620                     final Duration laneChangeDuration, final double lcFraction) throws OTSGeometryException
621             {
622                 DirectedPoint target = toLine.getLocationFraction(1.0);
623                 return Bezier.cubic(64, startPosition, target, 0.5);
624             }
625         };
626 
627         /** The target point (including rotation) for the coming time step is based on a sine wave. */
628         LaneChangePath SINE = new SequentialLaneChangePath()
629         {
630             /** {@inheritDoc} */
631             @Override
632             protected double lateralFraction(final double lcFraction)
633             {
634                 return -1.0 / (2 * Math.PI) * Math.sin(2 * Math.PI * lcFraction) + lcFraction;
635             }
636 
637             /** {@inheritDoc} */
638             @Override
639             protected double angle(final double width, final double cumulLcLength, final double totalLcLength)
640             {
641                 return Math.atan((-width * Math.cos(2 * Math.PI * cumulLcLength / totalLcLength) / totalLcLength)
642                         + width / totalLcLength);
643             }
644         };
645 
646         /** The target point (including rotation) for the coming time step is based on a 3rd-degree polynomial. */
647         LaneChangePath POLY3 = new SequentialLaneChangePath()
648         {
649             /** {@inheritDoc} */
650             @Override
651             protected double lateralFraction(final double lcFraction)
652             {
653                 return 3 * (lcFraction * lcFraction) - 2 * (lcFraction * lcFraction * lcFraction);
654             }
655 
656             /** {@inheritDoc} */
657             @Override
658             protected double angle(final double width, final double cumulLcLength, final double totalLcLength)
659             {
660                 return Math.atan(cumulLcLength * 6 * width / (totalLcLength * totalLcLength)
661                         - cumulLcLength * cumulLcLength * 6 * width / (totalLcLength * totalLcLength * totalLcLength));
662             }
663         };
664 
665         /**
666          * A helper class to allow a lane change to follow a sequential determination of the target position (including
667          * rotation) for each time step.
668          * <p>
669          * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights
670          * reserved. <br>
671          * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
672          * <p>
673          * @version $Revision$, $LastChangedDate$, by $Author$, initial version 30 apr. 2018 <br>
674          * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
675          * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
676          * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
677          */
678         abstract class SequentialLaneChangePath implements LaneChangePath
679         {
680             /** {@inheritDoc} */
681             @Override
682             public OTSLine3D getPath(final Duration timeStep, final Length planDistance, final Speed meanSpeed,
683                     final DirectedLanePosition from, final DirectedPoint startPosition,
684                     final LateralDirectionality laneChangeDirection, final OTSLine3D fromLine, final OTSLine3D toLine,
685                     final Duration laneChangeDuration, final double lcFraction) throws OTSGeometryException
686             {
687                 DirectedPoint toTarget = toLine.getLocationFraction(1.0);
688                 DirectedPoint fromTarget = fromLine.getLocationFraction(1.0);
689                 double width = laneChangeDirection.isRight() ? fromTarget.distance(toTarget) : -fromTarget.distance(toTarget);
690                 double dFraction = timeStep.si / laneChangeDuration.si;
691                 return getPathRecursive(planDistance, meanSpeed, 1.0, width, from, startPosition, fromLine, toLine,
692                         laneChangeDuration, lcFraction, dFraction);
693             }
694 
695             /**
696              * Attempts to derive a path. If the resulting path is shorter than {@code planDistance} (e.g. lane change towards
697              * the inside of a curve), this method calls itself using a larger look-ahead distance.
698              * @param planDistance Length; plan distance
699              * @param meanSpeed Speed; mean speed during plan
700              * @param buffer double; buffer factor to assure sufficient path length is found, increased recursively
701              * @param width double; lateral deviation from from lanes at lane change end
702              * @param from DirectedLanePosition; current position on the from-lanes
703              * @param startPosition DirectedPoint; current 2D position
704              * @param fromLine OTSLine3D; from line
705              * @param toLine OTSLine3D; to line
706              * @param laneChangeDuration Duration; current considered duration of the entire lane change
707              * @param lcFraction double; lane change fraction at beginning of the plan
708              * @param dFraction double; additional lane change fraction to be made during the plan
709              * @return OTSLine3D a (partial) path for a lane change
710              * @throws OTSGeometryException on wrong fractional position
711              */
712             private OTSLine3D getPathRecursive(final Length planDistance, final Speed meanSpeed, final double buffer,
713                     final double width, final DirectedLanePosition from, final DirectedPoint startPosition,
714                     final OTSLine3D fromLine, final OTSLine3D toLine, final Duration laneChangeDuration,
715                     final double lcFraction, final double dFraction) throws OTSGeometryException
716             {
717                 // factor on path length to not overshoot a fraction of 1.0 in lane change progress, i.e. <1 if lane change will
718                 // be finished in the coming time step
719                 double cutoff = (1.0 - lcFraction) / (dFraction * buffer);
720                 cutoff = cutoff > 1.0 ? 1.0 : cutoff;
721 
722                 // lane change fraction at end of plan
723                 double lcFractionEnd = lcFraction + dFraction * buffer * cutoff;
724 
725                 // lateral fraction at that point according to shape
726                 double f = lateralFraction(lcFractionEnd);
727 
728                 // from-lane length
729                 double totalLcLength = meanSpeed.si * laneChangeDuration.si;
730                 double cumulLcLength = totalLcLength * lcFractionEnd;
731 
732                 // TODO: sequential is disabled as LaneChangePath now uses 2 OTSLine3D's instead of 2 List<Lane>'s. This was
733                 // done as the code using LaneChangePath (i.e. LaneChange) required more details on fractional positions itself.
734                 return null;
735                 /*-
736                 // find lane we will end up on at the end of the plan
737                 double positionAtEnd = (from.getGtuDirection().isPlus() ? from.getPosition().si
738                         : from.getLane().getLength().si - from.getPosition().si) + planDistance.si * buffer * cutoff;
739                 for (int i = 0; i < fromLanes.size(); i++)
740                 {
741                     LaneDirection fromLane = fromLanes.get(i);
742                     if (fromLane.getLength().si >= positionAtEnd)
743                     {
744                         // get target point by interpolation between from and to lane
745                         double endFraction = fromLane.fractionAtCoveredDistance(Length.instantiateSI(positionAtEnd));
746                         DirectedPoint pFrom = fromLane.getLocationFraction(endFraction);
747                         DirectedPoint pTo = toLanes.get(i).getLocationFraction(endFraction);
748                         DirectedPoint target = new DirectedPoint((1 - f) * pFrom.x + f * pTo.x, (1 - f) * pFrom.y + f * pTo.y,
749                                 (1 - f) * pFrom.z + f * pTo.z);
750                         // set rotation according to shape, relative to lane center line
751                         target.setRotZ(
752                                 (1 - f) * pFrom.getRotZ() + f * pTo.getRotZ() - angle(width, cumulLcLength, totalLcLength));
753                         // Bezier path towards that point
754                         OTSLine3D path = Bezier.cubic(64, startPosition, target, 0.5);
755                         // check if long enough, otherwise look further (e.g. changing to inside of curve gives a shorter path)
756                         if (path.getLength().si < planDistance.si && cutoff == 1.0)
757                         {
758                             return getPathRecursive(planDistance, meanSpeed, buffer * 1.25, width, from, startPosition,
759                                     fromLine, toLine, laneChangeDuration, lcFraction, dFraction);
760                         }
761                         return path;
762                     }
763                     positionAtEnd -= fromLane.getLength().si;
764                 }
765                 Throw.when(lcFraction + dFraction < 0.999, RuntimeException.class,
766                         "No partial path for lane change could be determined; fromLanes are too short.");
767                 return null;
768                 */
769             }
770 
771             /**
772              * Returns the fractional lateral deviation given a fraction of lane change being completed.
773              * @param lcFraction double; fraction of lane change
774              * @return double; lateral deviation
775              */
776             protected abstract double lateralFraction(double lcFraction);
777 
778             /**
779              * Returns the angle, relative to the lane center line, at the given cumulative length for a lane change of given
780              * total length and lateral deviation.
781              * @param width double; lateral deviation from from lanes at lane change end
782              * @param cumulLcLength double; cumulative length (along from lanes) covered so far
783              * @param totalLcLength double; total (along from lanes) length to cover in lane change
784              * @return double; angle, relative to the lane center line
785              */
786             protected abstract double angle(double width, double cumulLcLength, double totalLcLength);
787         }
788 
789         /**
790          * Helper class for interpolation between the from and to center lines.
791          * <p>
792          * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights
793          * reserved. <br>
794          * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
795          * <p>
796          * @version $Revision$, $LastChangedDate$, by $Author$, initial version May 3, 2019 <br>
797          * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
798          * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
799          * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
800          */
801         abstract class InterpolatedLaneChangePath implements LaneChangePath
802         {
803 
804             /** {@inheritDoc} */
805             @Override
806             public OTSLine3D getPath(final Duration timeStep, final Length planDistance, final Speed meanSpeed,
807                     final DirectedLanePosition from, final DirectedPoint startPosition,
808                     final LateralDirectionality laneChangeDirection, final OTSLine3D fromLine, final OTSLine3D toLine,
809                     final Duration laneChangeDuration, final double lcFraction) throws OTSGeometryException
810             {
811 
812                 double dx = fromLine.get(0).getLocation().x - startPosition.x;
813                 double dy = fromLine.get(0).getLocation().y - startPosition.y;
814                 double distFromLoc = Math.sqrt(dx * dx + dy * dy);
815                 dx = fromLine.get(0).getLocation().x - toLine.get(0).getLocation().x;
816                 dy = fromLine.get(0).getLocation().y - toLine.get(0).getLocation().y;
817                 double distFromTo = Math.sqrt(dx * dx + dy * dy);
818                 double startLateralFraction = distFromLoc / distFromTo;
819                 // Location is not on path in z-direction, so using .distance() create bugs 
820                 if (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 }