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.List;
6   import java.util.Map;
7   import java.util.Set;
8   
9   import org.djunits.value.vdouble.scalar.Duration;
10  import org.djunits.value.vdouble.scalar.Length;
11  import org.djunits.value.vdouble.scalar.Speed;
12  import org.opentrafficsim.core.geometry.Bezier;
13  import org.opentrafficsim.core.geometry.OTSGeometryException;
14  import org.opentrafficsim.core.geometry.OTSLine3D;
15  import org.opentrafficsim.core.geometry.OTSPoint3D;
16  import org.opentrafficsim.core.gtu.GTUException;
17  import org.opentrafficsim.core.gtu.Try;
18  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlanException;
19  import org.opentrafficsim.core.network.LateralDirectionality;
20  import org.opentrafficsim.road.gtu.lane.AbstractLaneBasedGTU;
21  import org.opentrafficsim.road.gtu.lane.LaneBasedGTU;
22  import org.opentrafficsim.road.gtu.lane.perception.RelativeLane;
23  import org.opentrafficsim.road.network.lane.DirectedLanePosition;
24  import org.opentrafficsim.road.network.lane.Lane;
25  import org.opentrafficsim.road.network.lane.LaneDirection;
26  
27  import nl.tudelft.simulation.dsol.SimRuntimeException;
28  import nl.tudelft.simulation.language.Throw;
29  import nl.tudelft.simulation.language.d3.DirectedPoint;
30  
31  /**
32   * Lane change status across operational plans. This class allows lane based tactical planners to perform lane changes without
33   * having to deal with many complexities concerning paths and lane registration. The main purpose of the tactical planner is to
34   * request a path using {@code getPath()} for each step of the tactical planner.
35   * <p>
36   * Copyright (c) 2013-2018 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
37   * BSD-style license. See <a href="http://opentrafficsim.org/docs/current/license.html">OpenTrafficSim License</a>.
38   * <p>
39   * @version $Revision$, $LastChangedDate$, by $Author$, initial version Jul 26, 2016 <br>
40   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
41   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
42   * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
43   */
44  public class LaneChange implements Serializable
45  {
46  
47      /** */
48      private static final long serialVersionUID = 20160811L;
49  
50      /** Total lane change duration. */
51      private Duration desiredLaneChangeDuration;
52  
53      /** Fraction of lane change had. */
54      private double fraction;
55  
56      /** Boundary length within which the lane change should be performed. */
57      private Length boundary;
58  
59      /** Lane change path. */
60      private LaneChangePath laneChangePath = LaneChangePath.BEZIER;
61  
62      /** Whether the GTU is changing lane. */
63      private LateralDirectionality laneChangeDirectionality = null;
64  
65      /** Instance to invoke static method through scheduled event on. */
66      private static final LaneOperationalPlanBuilder BUILDER = new LaneOperationalPlanBuilder();
67      
68      /** Minimum distance required to perform a lane change as factor on vehicle length. */
69      public static double MIN_LC_LENGTH_FACTOR = 2.0;
70  
71      /**
72       * Sets the desired lane change duration. Should be set by a tactical planner.
73       * @param duration Duration; desired lane change duration
74       */
75      public void setDesiredLaneChangeDuration(final Duration duration)
76      {
77          this.desiredLaneChangeDuration = duration;
78      }
79  
80      /**
81       * Sets the distance within which a lane change should be finished. Should be set by a tactical planner. In case of a single
82       * lane change required before some point, this is not required as the found center line length is intrinsically limited.
83       * For multiple lane changes being required, space after a lane change is required.
84       * @param boundary DirectedPoint; boundary
85       */
86      public void setBoundary(final Length boundary)
87      {
88          this.boundary = boundary;
89      }
90      
91      /**
92       * Returns the fraction of the lane change performed.
93       * @return double; fraction of lane change performed
94       */
95      public double getFraction()
96      {
97          return this.fraction;
98      }
99  
100     /**
101      * Sets a lane change path.
102      * @param laneChangePath LaneChangePath; lane change path
103      */
104     public void setLaneChangePath(final LaneChangePath laneChangePath)
105     {
106         this.laneChangePath = laneChangePath;
107     }
108 
109     /**
110      * Return whether the GTU is changing lane.
111      * @return whether the GTU is changing lane
112      */
113     public final boolean isChangingLane()
114     {
115         return this.laneChangeDirectionality != null;
116     }
117 
118     /**
119      * Return whether the GTU is changing left.
120      * @return whether the GTU is changing left
121      */
122     public final boolean isChangingLeft()
123     {
124         return LateralDirectionality.LEFT.equals(this.laneChangeDirectionality);
125     }
126 
127     /**
128      * Return whether the GTU is changing right.
129      * @return whether the GTU is changing right
130      */
131     public final boolean isChangingRight()
132     {
133         return LateralDirectionality.RIGHT.equals(this.laneChangeDirectionality);
134     }
135 
136     /**
137      * Return lateral lane change direction.
138      * @return LateralDirectionality; lateral lane change direction
139      */
140     public final LateralDirectionality getDirection()
141     {
142         return this.laneChangeDirectionality;
143     }
144 
145     /**
146      * Second lane of lane change relative to the reference lane. Note that the reference lane may either be the source or the
147      * target lane. Thus, the second lane during a lane change may either be the left or right lane, regardless of the lane
148      * change direction.
149      * @param gtu LaneBasedGTU; the GTU
150      * @return target lane of lane change
151      * @throws OperationalPlanException If no lane change is being performed.
152      */
153     public final RelativeLane getSecondLane(final LaneBasedGTU gtu) throws OperationalPlanException
154     {
155         Throw.when(!isChangingLane(), OperationalPlanException.class,
156                 "Target lane is requested, but no lane change is being performed.");
157         Map<Lane, Length> map;
158         DirectedLanePosition dlp;
159         try
160         {
161             map = gtu.positions(gtu.getReference());
162             dlp = gtu.getReferencePosition();
163         }
164         catch (GTUException exception)
165         {
166             throw new OperationalPlanException("Second lane of lane change could not be determined.", exception);
167         }
168         Set<Lane> accessibleLanes = dlp.getLane().accessibleAdjacentLanesPhysical(this.laneChangeDirectionality,
169                 gtu.getGTUType(), dlp.getGtuDirection());
170         if (!accessibleLanes.isEmpty() && map.containsKey(accessibleLanes.iterator().next()))
171         {
172             return isChangingLeft() ? RelativeLane.LEFT : RelativeLane.RIGHT;
173         }
174         return isChangingLeft() ? RelativeLane.RIGHT : RelativeLane.LEFT;
175     }
176 
177     /**
178      * Returns the path for a lane change. Lane change initialization and finalization events are automatically performed.
179      * @param timeStep Duration; plan time step
180      * @param gtu LaneBasedGTU; gtu
181      * @param from DirectedLanePosition; current position on the from lane (i.e. not necessarily the reference position)
182      * @param startPosition DirectedPoint; current position in 2D
183      * @param planDistance Length; absolute distance that will be covered during the time step
184      * @param laneChangeDirection LateralDirectionality; lane change direction
185      * @return OTSLine3D; path
186      * @throws OTSGeometryException on path or shape error
187      */
188     public final OTSLine3D getPath(final Duration timeStep, final LaneBasedGTU gtu, final DirectedLanePosition from,
189             final DirectedPoint startPosition, final Length planDistance, final LateralDirectionality laneChangeDirection)
190             throws OTSGeometryException
191     {
192 
193         // initiate lane change
194         if (!isChangingLane())
195         {
196             this.laneChangeDirectionality = laneChangeDirection;
197             Try.execute(() -> ((AbstractLaneBasedGTU) gtu).initLaneChange(laneChangeDirection),
198                     "Error during lane change initialization.");
199         }
200 
201         // determine longitudinal distance along the from lanes
202         /*
203          * We take 3 factors in to account. The first two are 1) minimum physical lane change length, and 2) desired lane change
204          * duration. With the current mean speed of the plan, we take the maximum. So at very low speeds, the minimum physical
205          * length may increase the lane change duration. We also have 3) the maximum length before a lane change needs to have
206          * been performed. To overcome simulation troubles, we allow this to result in an even shorted length than the minimum
207          * physical distance. So: length = min( max("1", "2"), "3" ). These distances are all considered along the from-lanes.
208          * Actual path distance is different.
209          */
210         Speed meanSpeed = planDistance.divideBy(timeStep);
211         double minDistance = gtu.getLength().si * MIN_LC_LENGTH_FACTOR; // simple bare minimum
212         double minDuration = minDistance / meanSpeed.si;
213         double laneChangeDuration = Math.max(this.desiredLaneChangeDuration.si, minDuration);
214         if (this.boundary != null)
215         {
216             double maxDuration = this.boundary.si / meanSpeed.si;
217             laneChangeDuration = Math.min(laneChangeDuration, maxDuration);
218         }
219 
220         double totalLength = laneChangeDuration * meanSpeed.si;
221         double fromDist = (1.0 - this.fraction) * totalLength; // remaining distance along from lanes to lane change end
222         Throw.when(fromDist < 0.0, RuntimeException.class, "Lane change results in negative distance along from lanes.");
223 
224         // get fractional location there, build lane lists as we search over the distance
225         LaneDirection fromLane = from.getLaneDirection();
226         List<LaneDirection> fromLanes = new ArrayList<>();
227         List<LaneDirection> toLanes = new ArrayList<>();
228         fromLanes.add(fromLane);
229         toLanes.add(fromLane.getAdjacentLaneDirection(this.laneChangeDirectionality, gtu));
230         double endPosFrom = from.getPosition().si + fromDist;
231         while (endPosFrom + gtu.getFront().getDx().si > fromLane.getLane().getLength().si)
232         {
233             LaneDirection nextFromLane = fromLane.getNextLaneDirection(gtu);
234             if (nextFromLane == null)
235             {
236                 // there are no lanes to move on, restrict lane change length/duration (given fixed mean speed)
237                 double endFromPosLimit = fromLane.getLane().getLength().si - gtu.getFront().getDx().si;
238                 double f = 1.0 - (endPosFrom - endFromPosLimit) / fromDist;
239                 laneChangeDuration *= f;
240                 endPosFrom = endFromPosLimit;
241                 break;
242             }
243             endPosFrom -= fromLane.getLane().getLength().si;
244             LaneDirection nextToLane = nextFromLane.getAdjacentLaneDirection(this.laneChangeDirectionality, gtu);
245             if (nextToLane == null)
246             {
247                 // there are no lanes to move change to, restrict lane change length/duration (given fixed mean speed)
248                 double endFromPosLimit = fromLane.getLane().getLength().si - gtu.getFront().getDx().si;
249                 double f = 1.0 - (endPosFrom - endFromPosLimit) / fromDist;
250                 laneChangeDuration *= f;
251                 endPosFrom = endFromPosLimit;
252                 break;
253             }
254             fromLane = nextFromLane;
255             fromLanes.add(fromLane);
256             toLanes.add(nextToLane);
257         }
258         // for long vehicles and short lanes, revert
259         while (endPosFrom < 0.0)
260         {
261             fromLanes.remove(fromLanes.size() - 1);
262             toLanes.remove(toLanes.size() - 1);
263             fromLane = fromLanes.get(fromLanes.size() - 1);
264             endPosFrom += fromLane.getLane().getLength().si;
265         }
266         // finally, get location at the final lane available
267         double endFraction = fromLane.fractionAtCoveredDistance(Length.createSI(endPosFrom));
268 
269         // get path from shape
270         OTSLine3D path = this.laneChangePath.getPath(timeStep, planDistance, meanSpeed, from, startPosition,
271                 laneChangeDirection, fromLanes, toLanes, endFraction, Duration.createSI(laneChangeDuration), this.fraction);
272 
273         // update
274         this.fraction += timeStep.si / laneChangeDuration; // the total fraction this step increases
275 
276         // deal with lane change end
277         double requiredLength = planDistance.si - path.getLength().si;
278         if (requiredLength > 0.0 || this.fraction > 0.999)
279         {
280             try
281             {
282                 // TODO get rid of cast to AbstractLaneBasedGTU
283                 gtu.getSimulator().scheduleEventNow(gtu, BUILDER, "scheduleLaneChangeFinalization", new Object[] {
284                         (AbstractLaneBasedGTU) gtu, Length.min(planDistance, path.getLength()), laneChangeDirection });
285             }
286             catch (SimRuntimeException exception)
287             {
288                 throw new RuntimeException("Error during lane change finalization.", exception);
289             }
290             // add length to path on to lanes
291             if (requiredLength > 0.0)
292             {
293                 LaneDirection toLane = toLanes.get(toLanes.size() - 1);
294                 int n = path.size();
295                 // ignore remainder of first lane if fraction is at the end of the lane
296                 if (0.0 < endFraction && endFraction < 1.0)
297                 {
298                     OTSLine3D remainder = toLane.getDirection().isPlus()
299                             ? toLane.getLane().getCenterLine().extractFractional(endFraction, 1.0)
300                             : toLane.getLane().getCenterLine().extractFractional(0.0, endFraction).reverse();
301                     path = OTSLine3D.concatenate(0.001, path, remainder);
302                     requiredLength = planDistance.si - path.getLength().si;
303                 }
304                 // add further lanes
305                 while (requiredLength > 0.0)
306                 {
307                     toLane = toLane.getNextLaneDirection(gtu);
308                     OTSLine3D remainder = toLane.getDirection().isPlus() ? toLane.getLane().getCenterLine()
309                             : toLane.getLane().getCenterLine().reverse();
310                     path = OTSLine3D.concatenate(Lane.MARGIN.si, path, remainder);
311                     requiredLength = planDistance.si - path.getLength().si + Lane.MARGIN.si;
312                 }
313                 // filter near-duplicate point which results in projection exceptions
314                 if (this.fraction > 0.999) // this means point 'target' is essentially at the design line
315                 {
316                     OTSPoint3D[] points = new OTSPoint3D[path.size() - 1];
317                     System.arraycopy(path.getPoints(), 0, points, 0, n - 1);
318                     System.arraycopy(path.getPoints(), n, points, n - 1, path.size() - n);
319                     path = new OTSLine3D(points);
320                 }
321             }
322             // reset lane change
323             this.laneChangeDirectionality = null;
324             this.boundary = null;
325             this.fraction = 0.0;
326         }
327         return path;
328     }
329 
330     /** {@inheritDoc} */
331     @Override
332     public String toString()
333     {
334         return "LaneChange [fraction=" + this.fraction + ", laneChangeDirectionality=" + this.laneChangeDirectionality + "]";
335     }
336 
337     /**
338      * Provides a (partial) path during lane changes.
339      * <p>
340      * Copyright (c) 2013-2018 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
341      * <br>
342      * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
343      * <p>
344      * @version $Revision$, $LastChangedDate$, by $Author$, initial version 30 apr. 2018 <br>
345      * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
346      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
347      * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
348      */
349     public interface LaneChangePath
350     {
351         /** A simple Bezier curve directly to the lane change target position. */
352         public final static LaneChangePath BEZIER = new LaneChangePath()
353         {
354             @Override
355             public OTSLine3D getPath(final Duration timeStep, final Length planDistance, final Speed meanSpeed,
356                     final DirectedLanePosition from, final DirectedPoint startPosition,
357                     final LateralDirectionality laneChangeDirection, final List<LaneDirection> fromLanes,
358                     final List<LaneDirection> toLanes, final double endFractionalPosition, final Duration laneChangeDuration,
359                     final double lcFraction) throws OTSGeometryException
360             {
361                 DirectedPoint target = toLanes.get(toLanes.size() - 1).getLocationFraction(endFractionalPosition);
362                 return Bezier.cubic(64, startPosition, target, 0.5);
363             }
364         };
365 
366         /** The target point (including rotation) for the coming time step is based on a sine wave. */
367         public final static LaneChangePath SINE = new SequentialLaneChangePath()
368         {
369             /** {@inheritDoc} */
370             @Override
371             protected double lateralFraction(final double lcFraction)
372             {
373                 return -1.0 / (2 * Math.PI) * Math.sin(2 * Math.PI * lcFraction) + lcFraction;
374             }
375 
376             /** {@inheritDoc} */
377             @Override
378             protected double angle(final double width, final double cumulLcLength, final double totalLcLength)
379             {
380                 return Math.atan((-width * Math.cos(2 * Math.PI * cumulLcLength / totalLcLength) / totalLcLength)
381                         + width / totalLcLength);
382             }
383         };
384 
385         /** The target point (including rotation) for the coming time step is based on a 3rd-degree polynomial. */
386         public final static LaneChangePath POLY3 = new SequentialLaneChangePath()
387         {
388             /** {@inheritDoc} */
389             @Override
390             protected double lateralFraction(final double lcFraction)
391             {
392                 return 3 * (lcFraction * lcFraction) - 2 * (lcFraction * lcFraction * lcFraction);
393             }
394 
395             /** {@inheritDoc} */
396             @Override
397             protected double angle(final double width, final double cumulLcLength, final double totalLcLength)
398             {
399                 return Math.atan(cumulLcLength * 6 * width / (totalLcLength * totalLcLength)
400                         - cumulLcLength * cumulLcLength * 6 * width / (totalLcLength * totalLcLength * totalLcLength));
401             }
402         };
403 
404         /**
405          * A helper class to allow a lane change to follow a sequential determination of the target position (including
406          * rotation) for each time step.
407          * <p>
408          * Copyright (c) 2013-2018 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights
409          * reserved. <br>
410          * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
411          * <p>
412          * @version $Revision$, $LastChangedDate$, by $Author$, initial version 30 apr. 2018 <br>
413          * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
414          * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
415          * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
416          */
417         public abstract static class SequentialLaneChangePath implements LaneChangePath
418         {
419             /** {@inheritDoc} */
420             @Override
421             public OTSLine3D getPath(final Duration timeStep, final Length planDistance, final Speed meanSpeed,
422                     final DirectedLanePosition from, final DirectedPoint startPosition,
423                     final LateralDirectionality laneChangeDirection, final List<LaneDirection> fromLanes,
424                     final List<LaneDirection> toLanes, final double endFractionalPosition, final Duration laneChangeDuration,
425                     final double lcFraction) throws OTSGeometryException
426             {
427                 DirectedPoint toTarget = toLanes.get(toLanes.size() - 1).getLocationFraction(endFractionalPosition);
428                 DirectedPoint fromTarget = fromLanes.get(fromLanes.size() - 1).getLocationFraction(endFractionalPosition);
429                 double width = laneChangeDirection.isRight() ? fromTarget.distance(toTarget) : -fromTarget.distance(toTarget);
430                 double dFraction = timeStep.si / laneChangeDuration.si;
431                 return getPathRecursive(planDistance, meanSpeed, 1.0, width, from, startPosition, fromLanes, toLanes,
432                         laneChangeDuration, lcFraction, dFraction);
433             }
434 
435             /**
436              * Attempts to derive a path. If the resulting path is shorter than {@code planDistance} (e.g. lane change towards
437              * the inside of a curve), this method calls itself using a larger look-ahead distance.
438              * @param planDistance Length; plan distance
439              * @param meanSpeed Speed; mean speed during plan
440              * @param buffer double; buffer factor to assure sufficient path length is found, increased recursively
441              * @param width double; lateral deviation from from lanes at lane change end
442              * @param from DirectedLanePosition; current position on the from-lanes
443              * @param startPosition DirectedPoint; current 2D position
444              * @param fromLanes List&lt;LaneDirection&gt;; lane list of the from-lane concerned
445              * @param toLanes List&lt;LaneDirection&gt;; lane list of the to-lane concerned
446              * @param laneChangeDuration Duration; current considered duration of the entire lane change
447              * @param lcFraction double; lane change fraction at beginning of the plan
448              * @param dFraction double; additional lane change fraction to be made during the plan
449              * @return OTSLine3D a (partial) path for a lane change
450              * @throws OTSGeometryException on wrong fractional position
451              */
452             private OTSLine3D getPathRecursive(final Length planDistance, final Speed meanSpeed, final double buffer,
453                     final double width, final DirectedLanePosition from, final DirectedPoint startPosition,
454                     final List<LaneDirection> fromLanes, final List<LaneDirection> toLanes, final Duration laneChangeDuration,
455                     final double lcFraction, final double dFraction) throws OTSGeometryException
456             {
457                 // factor on path length to not overshoot a fraction of 1.0 in lane change progress, i.e. <1 if lane change will
458                 // be finished in the coming time step
459                 double cutoff = (1.0 - lcFraction) / (dFraction * buffer);
460                 cutoff = cutoff > 1.0 ? 1.0 : cutoff;
461 
462                 // lane change fraction at end of plan
463                 double lcFractionEnd = lcFraction + dFraction * buffer * cutoff;
464 
465                 // lateral fraction at that point according to shape
466                 double f = lateralFraction(lcFractionEnd);
467 
468                 // from-lane length
469                 double totalLcLength = meanSpeed.si * laneChangeDuration.si;
470                 double cumulLcLength = totalLcLength * lcFractionEnd;
471 
472                 // find lane we will end up on at the end of the plan
473                 double positionAtEnd = (from.getGtuDirection().isPlus() ? from.getPosition().si
474                         : from.getLane().getLength().si - from.getPosition().si) + planDistance.si * buffer * cutoff;
475                 for (int i = 0; i < fromLanes.size(); i++)
476                 {
477                     LaneDirection fromLane = fromLanes.get(i);
478                     if (fromLane.getLength().si >= positionAtEnd)
479                     {
480                         // get target point by interpolation between from and to lane
481                         double endFraction = fromLane.fractionAtCoveredDistance(Length.createSI(positionAtEnd));
482                         DirectedPoint pFrom = fromLane.getLocationFraction(endFraction);
483                         DirectedPoint pTo = toLanes.get(i).getLocationFraction(endFraction);
484                         DirectedPoint target = new DirectedPoint((1 - f) * pFrom.x + f * pTo.x, (1 - f) * pFrom.y + f * pTo.y,
485                                 (1 - f) * pFrom.z + f * pTo.z);
486                         // set rotation according to shape, relative to lane center line
487                         target.setRotZ(
488                                 (1 - f) * pFrom.getRotZ() + f * pTo.getRotZ() - angle(width, cumulLcLength, totalLcLength));
489                         // Bezier path towards that point
490                         OTSLine3D path = Bezier.cubic(64, startPosition, target, 0.5);
491                         // check if long enough, otherwise look further (e.g. changing to inside of curve gives a shorter path)
492                         if (path.getLength().si < planDistance.si && cutoff == 1.0)
493                         {
494                             return getPathRecursive(planDistance, meanSpeed, buffer * 1.25, width, from, startPosition,
495                                     fromLanes, toLanes, laneChangeDuration, lcFraction, dFraction);
496                         }
497                         return path;
498                     }
499                     positionAtEnd -= fromLane.getLength().si;
500                 }
501                 Throw.when(lcFraction + dFraction < 0.999, RuntimeException.class,
502                         "No partial path for lane change could be determined; fromLanes are too short.");
503                 return null;
504             }
505 
506             /**
507              * Returns the fractional lateral deviation given a fraction of lane change being completed.
508              * @param lcFraction double; fraction of lane change
509              * @return double; lateral deviation
510              */
511             protected abstract double lateralFraction(double lcFraction);
512 
513             /**
514              * Returns the angle, relative to the lane center line, at the given cumulative length for a lane change of given
515              * total length and lateral deviation.
516              * @param width double; lateral deviation from from lanes at lane change end
517              * @param cumulLcLength double; cumulative length (along from lanes) covered so far
518              * @param totalLcLength double; total (along from lanes) length to cover in lane change
519              * @return double; angle, relative to the lane center line
520              */
521             protected abstract double angle(double width, double cumulLcLength, double totalLcLength);
522         }
523 
524         /**
525          * Returns a (partial) path for a lane change. The method is called both at the start and during a lane change, and
526          * should return a valid path. This path should at least have a length of {@code planDistance}, unless the lane change
527          * will be finished during the coming time step. In that case, the caller of this method is to lengthen the path along
528          * the center line of the target lane.
529          * @param timeStep Duration; time step
530          * @param planDistance Length; distance covered during the plan
531          * @param meanSpeed Speed; mean speed during time step
532          * @param from DirectedLanePosition; current position on the from-lanes
533          * @param startPosition DirectedPoint; current 2D position
534          * @param laneChangeDirection LateralDirectionality; lane change direction
535          * @param fromLanes List&lt;LaneDirection&gt;; lane list of the from-lane concerned
536          * @param toLanes List&lt;LaneDirection&gt;; lane list of the to-lane concerned
537          * @param endFractionalPosition double; fractional position at the end of the plan at the last lanes in the lane lists
538          * @param laneChangeDuration Duration; current considered duration of the entire lane change
539          * @param lcFraction double; fraction of lane change done so far
540          * @return OTSLine3D a (partial) path for a lane change
541          * @throws OTSGeometryException on wrong fractional position
542          */
543         OTSLine3D getPath(Duration timeStep, Length planDistance, Speed meanSpeed, DirectedLanePosition from,
544                 DirectedPoint startPosition, LateralDirectionality laneChangeDirection, List<LaneDirection> fromLanes,
545                 List<LaneDirection> toLanes, double endFractionalPosition, Duration laneChangeDuration, double lcFraction)
546                 throws OTSGeometryException;
547     }
548 }