View Javadoc
1   package org.opentrafficsim.road.gtu.lane.tactical;
2   
3   import java.util.ArrayList;
4   import java.util.Collection;
5   import java.util.LinkedHashMap;
6   import java.util.Map;
7   
8   import org.djunits.unit.AccelerationUnit;
9   import org.djunits.unit.DurationUnit;
10  import org.djunits.unit.LengthUnit;
11  import org.djunits.value.ValueRuntimeException;
12  import org.djunits.value.storage.StorageType;
13  import org.djunits.value.vdouble.scalar.Acceleration;
14  import org.djunits.value.vdouble.scalar.Duration;
15  import org.djunits.value.vdouble.scalar.Length;
16  import org.djunits.value.vdouble.scalar.Speed;
17  import org.djunits.value.vdouble.scalar.Time;
18  import org.djunits.value.vdouble.vector.AccelerationVector;
19  import org.djutils.draw.point.OrientedPoint2d;
20  import org.opentrafficsim.base.geometry.OtsLine2d;
21  import org.opentrafficsim.base.parameters.ParameterException;
22  import org.opentrafficsim.base.parameters.ParameterTypeLength;
23  import org.opentrafficsim.base.parameters.ParameterTypes;
24  import org.opentrafficsim.core.gtu.GtuException;
25  import org.opentrafficsim.core.gtu.GtuType;
26  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlan;
27  import org.opentrafficsim.core.gtu.plan.operational.Segments;
28  import org.opentrafficsim.core.network.LateralDirectionality;
29  import org.opentrafficsim.core.network.Link;
30  import org.opentrafficsim.core.network.NetworkException;
31  import org.opentrafficsim.core.network.Node;
32  import org.opentrafficsim.road.gtu.lane.LaneBasedGtu;
33  import org.opentrafficsim.road.gtu.lane.perception.CategoricalLanePerception;
34  import org.opentrafficsim.road.gtu.lane.perception.LanePerception;
35  import org.opentrafficsim.road.gtu.lane.perception.categories.DefaultSimplePerception;
36  import org.opentrafficsim.road.gtu.lane.perception.categories.DirectDefaultSimplePerception;
37  import org.opentrafficsim.road.gtu.lane.perception.headway.Headway;
38  import org.opentrafficsim.road.gtu.lane.perception.headway.HeadwayTrafficLight;
39  import org.opentrafficsim.road.gtu.lane.tactical.following.GtuFollowingModelOld;
40  import org.opentrafficsim.road.gtu.lane.tactical.lanechangemobil.LaneChangeModel;
41  import org.opentrafficsim.road.gtu.lane.tactical.lanechangemobil.LaneMovementStep;
42  import org.opentrafficsim.road.network.lane.CrossSectionElement;
43  import org.opentrafficsim.road.network.lane.CrossSectionLink;
44  import org.opentrafficsim.road.network.lane.Lane;
45  import org.opentrafficsim.road.network.lane.LanePosition;
46  import org.opentrafficsim.road.network.lane.object.detector.LaneDetector;
47  import org.opentrafficsim.road.network.lane.object.detector.SinkDetector;
48  
49  /**
50   * Lane-based tactical planner that implements car following and lane change behavior. This lane-based tactical planner makes
51   * decisions based on headway (GTU following model) and lane change (Lane Change model), and will generate an operational plan
52   * for the GTU. It can ask the strategic planner for assistance on the route to take when the network splits.
53   * <p>
54   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
55   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
56   * </p>
57   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
58   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
59   */
60  public class LaneBasedCfLcTacticalPlanner extends AbstractLaneBasedTacticalPlanner
61  {
62      /** */
63      private static final long serialVersionUID = 20151125L;
64  
65      /** Look back parameter type. */
66      protected static final ParameterTypeLength LOOKBACKOLD = ParameterTypes.LOOKBACKOLD;
67  
68      /** Standard incentive to stay in the current lane. */
69      private static final Acceleration STAYINCURRENTLANEINCENTIVE = new Acceleration(0.1, AccelerationUnit.METER_PER_SECOND_2);
70  
71      /** Standard incentive to stay in the current lane. */
72      private static final Acceleration PREFERREDLANEINCENTIVE = new Acceleration(0.3, AccelerationUnit.METER_PER_SECOND_2);
73  
74      /** Standard incentive to stay in the current lane. */
75      private static final Acceleration NONPREFERREDLANEINCENTIVE = new Acceleration(-0.3, AccelerationUnit.METER_PER_SECOND_2);
76  
77      /** Return value of suitability when no lane change is required within the time horizon. */
78      public static final Length NOLANECHANGENEEDED = new Length(Double.MAX_VALUE, LengthUnit.SI);
79  
80      /** Return value of suitability when a lane change is required <i>right now</i>. */
81      public static final Length GETOFFTHISLANENOW = Length.ZERO;
82  
83      /** Standard time horizon for route choices. */
84      private static final Duration TIMEHORIZON = new Duration(90, DurationUnit.SECOND);
85  
86      /** Lane change model for this tactical planner. */
87      private LaneChangeModel laneChangeModel;
88  
89      /**
90       * Instantiated a tactical planner with GTU following and lane change behavior.
91       * @param carFollowingModel Car-following model.
92       * @param laneChangeModel Lane change model.
93       * @param gtu GTU
94       */
95      public LaneBasedCfLcTacticalPlanner(final GtuFollowingModelOld carFollowingModel, final LaneChangeModel laneChangeModel,
96              final LaneBasedGtu gtu)
97      {
98          super(carFollowingModel, gtu, new CategoricalLanePerception(gtu));
99          this.laneChangeModel = laneChangeModel;
100         getPerception().addPerceptionCategory(new DirectDefaultSimplePerception(getPerception()));
101     }
102 
103     @Override
104     public final OperationalPlan generateOperationalPlan(final Time startTime, final OrientedPoint2d locationAtStartTime)
105             throws NetworkException, GtuException, ParameterException
106     {
107         try
108         {
109             // define some basic variables
110             LaneBasedGtu laneBasedGTU = getGtu();
111             LanePerception perception = getPerception();
112 
113             // if the GTU's maximum speed is zero (block), generate a stand still plan for one second
114             if (laneBasedGTU.getMaximumSpeed().si < OperationalPlan.DRIFTING_SPEED_SI)
115             {
116                 return OperationalPlan.standStill(getGtu(), getGtu().getLocation(), startTime, Duration.ONE);
117             }
118 
119             Length maximumForwardHeadway = laneBasedGTU.getParameters().getParameter(LOOKAHEAD);
120             DefaultSimplePerception simplePerception = perception.getPerceptionCategory(DefaultSimplePerception.class);
121             simplePerception.updateSpeedLimit();
122             simplePerception.updateForwardHeadwayGtu();
123             simplePerception.updateForwardHeadwayObject();
124             simplePerception.updateBackwardHeadway();
125             simplePerception.updateNeighboringHeadwaysLeft();
126             simplePerception.updateNeighboringHeadwaysRight();
127             Speed speedLimit = simplePerception.getSpeedLimit();
128 
129             // look at the conditions for headway on the current lane
130             Headway sameLaneLeader = simplePerception.getForwardHeadwayGtu();
131             // TODO how to handle objects on this lane or another lane???
132             Headway sameLaneFollower = simplePerception.getBackwardHeadway();
133             Collection<Headway> sameLaneTraffic = new ArrayList<>();
134             if (sameLaneLeader.getObjectType().isGtu())
135             {
136                 sameLaneTraffic.add(sameLaneLeader);
137             }
138             if (sameLaneFollower.getObjectType().isGtu())
139             {
140                 sameLaneTraffic.add(sameLaneFollower);
141             }
142 
143             // Are we in the right lane for the route?
144             LanePathInfo lanePathInfo = buildLanePathInfo(laneBasedGTU, maximumForwardHeadway);
145 
146             // TODO these two info's are not used
147             NextSplitInfo nextSplitInfo = determineNextSplit(laneBasedGTU, maximumForwardHeadway);
148             boolean currentLaneFine = nextSplitInfo.correctCurrentLanes().contains(lanePathInfo.getReferenceLane());
149 
150             // calculate the lane change step
151             // TODO skip if:
152             // - we are in the right lane and drive at max speed or we accelerate maximally
153             // - there are no other lanes
154             Collection<Headway> leftLaneTraffic = simplePerception.getNeighboringHeadwaysLeft();
155             Collection<Headway> rightLaneTraffic = simplePerception.getNeighboringHeadwaysRight();
156 
157             // FIXME: whether we drive on the right should be stored in some central place.
158             final LateralDirectionality preferred = LateralDirectionality.RIGHT;
159             final Acceleration defaultLeftLaneIncentive =
160                     preferred.isLeft() ? PREFERREDLANEINCENTIVE : NONPREFERREDLANEINCENTIVE;
161             final Acceleration defaultRightLaneIncentive =
162                     preferred.isRight() ? PREFERREDLANEINCENTIVE : NONPREFERREDLANEINCENTIVE;
163 
164             AccelerationVector defaultLaneIncentives = new AccelerationVector(new double[] {defaultLeftLaneIncentive.getSI(),
165                     STAYINCURRENTLANEINCENTIVE.getSI(), defaultRightLaneIncentive.getSI()}, AccelerationUnit.SI);
166             AccelerationVector laneIncentives = laneIncentives(laneBasedGTU, defaultLaneIncentives);
167             LaneMovementStep lcmr = this.laneChangeModel.computeLaneChangeAndAcceleration(laneBasedGTU, sameLaneTraffic,
168                     rightLaneTraffic, leftLaneTraffic, speedLimit,
169                     new Acceleration(laneIncentives.get(preferred.isRight() ? 2 : 0)), new Acceleration(laneIncentives.get(1)),
170                     new Acceleration(laneIncentives.get(preferred.isRight() ? 0 : 2)));
171             Duration duration = lcmr.getGfmr().getValidUntil().minus(getGtu().getSimulator().getSimulatorAbsTime());
172             if (lcmr.getLaneChangeDirection() != null)
173             {
174                 laneBasedGTU.changeLaneInstantaneously(lcmr.getLaneChangeDirection());
175 
176                 // create the path to drive in this timestep.
177                 lanePathInfo = buildLanePathInfo(laneBasedGTU, maximumForwardHeadway);
178             }
179 
180             // incorporate traffic light
181             Headway object = simplePerception.getForwardHeadwayObject();
182             Acceleration a = lcmr.getGfmr().getAcceleration();
183             if (object instanceof HeadwayTrafficLight)
184             {
185                 // if it was perceived, it was red, or yellow and judged as requiring to stop
186                 a = Acceleration.min(a, ((GtuFollowingModelOld) getCarFollowingModel()).computeAcceleration(getGtu().getSpeed(),
187                         getGtu().getMaximumSpeed(), Speed.ZERO, object.getDistance(), speedLimit));
188             }
189 
190             // incorporate dead-end/split
191             Length dist = lanePathInfo.path().getTypedLength().minus(getGtu().getFront().dx());
192             a = Acceleration.min(a, ((GtuFollowingModelOld) getCarFollowingModel()).computeAcceleration(getGtu().getSpeed(),
193                     getGtu().getMaximumSpeed(), Speed.ZERO, dist, speedLimit));
194 
195             // build a list of lanes forward, with a maximum headway.
196             if (a.si < 1E-6 && laneBasedGTU.getSpeed().si < OperationalPlan.DRIFTING_SPEED_SI)
197             {
198                 return OperationalPlan.standStill(getGtu(), getGtu().getLocation(), startTime, Duration.ONE);
199             }
200             OtsLine2d path = lanePathInfo.path();
201             OperationalPlan op = new OperationalPlan(getGtu(), path, startTime, Segments.off(getGtu().getSpeed(), duration, a));
202             return op;
203         }
204         catch (ValueRuntimeException exception)
205         {
206             throw new GtuException(exception);
207         }
208     }
209 
210     /**
211      * TODO: move laneIncentives to LanePerception? Figure out if the default lane incentives are OK, or override them with
212      * values that should keep this GTU on the intended route.
213      * @param gtu the GTU for which to calculate the incentives
214      * @param defaultLaneIncentives the three lane incentives for the next left adjacent lane, the current lane and the next
215      *            right adjacent lane
216      * @return the (possibly adjusted) lane incentives
217      * @throws NetworkException on network inconsistency
218      * @throws ValueRuntimeException cannot happen
219      * @throws GtuException when the position of the GTU cannot be correctly determined
220      */
221     private AccelerationVector laneIncentives(final LaneBasedGtu gtu, final AccelerationVector defaultLaneIncentives)
222             throws NetworkException, ValueRuntimeException, GtuException
223     {
224         Length leftSuitability = suitability(gtu, LateralDirectionality.LEFT);
225         Length currentSuitability = suitability(gtu, null);
226         Length rightSuitability = suitability(gtu, LateralDirectionality.RIGHT);
227         if (leftSuitability == NOLANECHANGENEEDED && currentSuitability == NOLANECHANGENEEDED
228                 && rightSuitability == NOLANECHANGENEEDED)
229         {
230             return checkLaneDrops(gtu, defaultLaneIncentives);
231         }
232         if ((leftSuitability == NOLANECHANGENEEDED || leftSuitability == GETOFFTHISLANENOW)
233                 && currentSuitability == NOLANECHANGENEEDED
234                 && (rightSuitability == NOLANECHANGENEEDED || rightSuitability == GETOFFTHISLANENOW))
235         {
236             return checkLaneDrops(gtu, new AccelerationVector(new double[] {acceleration(gtu, leftSuitability),
237                     defaultLaneIncentives.get(1).getSI(), acceleration(gtu, rightSuitability)}, AccelerationUnit.SI));
238         }
239         if (currentSuitability == NOLANECHANGENEEDED)
240         {
241             return new AccelerationVector(new double[] {acceleration(gtu, leftSuitability),
242                     defaultLaneIncentives.get(1).getSI(), acceleration(gtu, rightSuitability)}, AccelerationUnit.SI);
243         }
244         return new AccelerationVector(new double[] {acceleration(gtu, leftSuitability), acceleration(gtu, currentSuitability),
245                 acceleration(gtu, rightSuitability)}, AccelerationUnit.SI);
246     }
247 
248     /**
249      * Figure out if the default lane incentives are OK, or override them with values that should keep this GTU from running out
250      * of road at an upcoming lane drop.
251      * @param gtu the GTU for which to check the lane drops
252      * @param defaultLaneIncentives DoubleVector.Rel.Dense&lt;AccelerationUnit&gt; the three lane incentives for the next left
253      *            adjacent lane, the current lane and the next right adjacent lane
254      * @return DoubleVector.Rel.Dense&lt;AccelerationUnit&gt;; the (possibly adjusted) lane incentives
255      * @throws NetworkException on network inconsistency
256      * @throws ValueRuntimeException cannot happen
257      * @throws GtuException when the positions of the GTU cannot be determined
258      */
259     private AccelerationVector checkLaneDrops(final LaneBasedGtu gtu, final AccelerationVector defaultLaneIncentives)
260             throws NetworkException, ValueRuntimeException, GtuException
261     {
262         // FIXME: these comparisons to -10 is ridiculous.
263         Length leftSuitability = Double.isNaN(defaultLaneIncentives.get(0).si) || defaultLaneIncentives.get(0).si < -10
264                 ? GETOFFTHISLANENOW : laneDrop(gtu, LateralDirectionality.LEFT);
265         Length currentSuitability = laneDrop(gtu, null);
266         Length rightSuitability = Double.isNaN(defaultLaneIncentives.get(2).si) || defaultLaneIncentives.get(2).si < -10
267                 ? GETOFFTHISLANENOW : laneDrop(gtu, LateralDirectionality.RIGHT);
268         // @formatter:off
269         if ((leftSuitability == NOLANECHANGENEEDED || leftSuitability == GETOFFTHISLANENOW)
270                 && currentSuitability == NOLANECHANGENEEDED
271                 && (rightSuitability == NOLANECHANGENEEDED || rightSuitability == GETOFFTHISLANENOW))
272         {
273             return defaultLaneIncentives;
274         }
275         // @formatter:on
276         if (currentSuitability == NOLANECHANGENEEDED)
277         {
278             return new AccelerationVector(new double[] {acceleration(gtu, leftSuitability),
279                     defaultLaneIncentives.get(1).getSI(), acceleration(gtu, rightSuitability)}, AccelerationUnit.SI);
280         }
281         if (currentSuitability.le(leftSuitability))
282         {
283             return new AccelerationVector(
284                     new double[] {PREFERREDLANEINCENTIVE.getSI(), NONPREFERREDLANEINCENTIVE.getSI(), GETOFFTHISLANENOW.getSI()},
285                     AccelerationUnit.SI);
286         }
287         if (currentSuitability.le(rightSuitability))
288         {
289             return new AccelerationVector(
290                     new double[] {GETOFFTHISLANENOW.getSI(), NONPREFERREDLANEINCENTIVE.getSI(), PREFERREDLANEINCENTIVE.getSI()},
291                     AccelerationUnit.SI, StorageType.DENSE);
292         }
293         return new AccelerationVector(new double[] {acceleration(gtu, leftSuitability), acceleration(gtu, currentSuitability),
294                 acceleration(gtu, rightSuitability)}, AccelerationUnit.SI);
295     }
296 
297     /**
298      * Return the distance until the next lane drop in the specified (nearby) lane.
299      * @param gtu the GTU to determine the next lane drop for
300      * @param direction one of the values <cite>LateralDirectionality.LEFT</cite> (use the left-adjacent lane), or
301      *            <cite>LateralDirectionality.RIGHT</cite> (use the right-adjacent lane), or <cite>null</cite> (use the current
302      *            lane)
303      * @return DoubleScalar.Rel&lt;LengthUnit&gt;; distance until the next lane drop if it occurs within the TIMEHORIZON, or
304      *         LaneBasedRouteNavigator.NOLANECHANGENEEDED if this lane can be followed until the next split junction or until
305      *         beyond the TIMEHORIZON
306      * @throws NetworkException on network inconsistency
307      * @throws GtuException when the positions of the GTU cannot be determined
308      */
309     private Length laneDrop(final LaneBasedGtu gtu, final LateralDirectionality direction) throws NetworkException, GtuException
310     {
311         LanePosition dlp = gtu.getReferencePosition();
312         Lane lane = dlp.lane();
313         Length longitudinalPosition = dlp.position();
314         if (null != direction)
315         {
316             lane = getPerception().getPerceptionCategory(DefaultSimplePerception.class).bestAccessibleAdjacentLane(lane,
317                     direction, longitudinalPosition);
318         }
319         if (null == lane)
320         {
321             return GETOFFTHISLANENOW;
322         }
323         double remainingLength = lane.getLength().getSI() - longitudinalPosition.getSI();
324         double remainingTimeSI = TIMEHORIZON.getSI() - remainingLength / lane.getSpeedLimit(gtu.getType()).getSI();
325         while (remainingTimeSI >= 0)
326         {
327             for (LaneDetector detector : lane.getDetectors())
328             {
329                 if (detector instanceof SinkDetector)
330                 {
331                     return NOLANECHANGENEEDED;
332                 }
333             }
334             int branching = lane.nextLanes(gtu.getType()).size();
335             if (branching == 0)
336             {
337                 return new Length(remainingLength, LengthUnit.SI);
338             }
339             if (branching > 1)
340             {
341                 return NOLANECHANGENEEDED;
342             }
343             lane = lane.nextLanes(gtu.getType()).iterator().next();
344             remainingTimeSI -= lane.getLength().getSI() / lane.getSpeedLimit(gtu.getType()).getSI();
345             remainingLength += lane.getLength().getSI();
346         }
347         return NOLANECHANGENEEDED;
348     }
349 
350     /**
351      * TODO: move suitability to LanePerception? Return the suitability for the current lane, left adjacent lane or right
352      * adjacent lane.
353      * @param gtu the GTU for which to calculate the incentives
354      * @param direction one of the values <cite>null</cite>, <cite>LateralDirectionality.LEFT</cite>, or
355      *            <cite>LateralDirectionality.RIGHT</cite>
356      * @return DoubleScalar.Rel&lt;LengthUnit&gt;; the suitability of the lane for reaching the (next) destination
357      * @throws NetworkException on network inconsistency
358      * @throws GtuException when position cannot be determined
359      */
360     private Length suitability(final LaneBasedGtu gtu, final LateralDirectionality direction)
361             throws NetworkException, GtuException
362     {
363         LanePosition dlp = gtu.getReferencePosition();
364         Lane lane = dlp.lane();
365         Length longitudinalPosition = dlp.position().plus(gtu.getFront().dx());
366         if (null != direction)
367         {
368             lane = getPerception().getPerceptionCategory(DefaultSimplePerception.class).bestAccessibleAdjacentLane(lane,
369                     direction, longitudinalPosition);
370         }
371         if (null == lane)
372         {
373             return GETOFFTHISLANENOW;
374         }
375         try
376         {
377             return suitability(lane, longitudinalPosition, gtu, TIMEHORIZON);
378             // return suitability(lane, lane.getLength().minus(longitudinalPosition), gtu, TIMEHORIZON);
379         }
380         catch (NetworkException ne)
381         {
382             System.err.println(gtu + " has a route problem in suitability: " + ne.getMessage());
383             return NOLANECHANGENEEDED;
384         }
385     }
386 
387     /**
388      * Compute deceleration needed to stop at a specified distance.
389      * @param gtu the GTU for which to calculate the acceleration to come to a full stop at the distance
390      * @param stopDistance the distance
391      * @return the acceleration (deceleration) needed to stop at the specified distance in m/s/s
392      */
393     private double acceleration(final LaneBasedGtu gtu, final Length stopDistance)
394     {
395         // What is the deceleration that will bring this GTU to a stop at exactly the suitability distance?
396         // Answer: a = -v^2 / 2 / suitabilityDistance
397         double v = gtu.getSpeed().getSI();
398         double a = -v * v / 2 / stopDistance.getSI();
399         return a;
400     }
401 
402     /**
403      * Determine the suitability of being at a particular longitudinal position in a particular Lane for following this Route.
404      * <br>
405      * @param lane the lane to consider
406      * @param longitudinalPosition the longitudinal position in the lane
407      * @param gtu the GTU (used to check lane compatibility of lanes, and current lane the GTU is on)
408      * @param timeHorizon the maximum time that a driver may want to look ahead
409      * @return DoubleScalar.Rel&lt;LengthUnit&gt;; a value that indicates within what distance the GTU should try to vacate this
410      *         lane.
411      * @throws NetworkException on network inconsistency, or when the continuation Link at a branch cannot be determined
412      */
413     private Length suitability(final Lane lane, final Length longitudinalPosition, final LaneBasedGtu gtu,
414             final Duration timeHorizon) throws NetworkException
415     {
416         double remainingDistance = lane.getLength().getSI() - longitudinalPosition.getSI();
417         double spareTime = timeHorizon.getSI() - remainingDistance / lane.getSpeedLimit(gtu.getType()).getSI();
418         // Find the first upcoming Node where there is a branch
419         Node nextNode = lane.getLink().getEndNode();
420         Link lastLink = lane.getLink();
421         Node nextSplitNode = null;
422         Lane currentLane = lane;
423         CrossSectionLink linkBeforeBranch = lane.getLink();
424         while (null != nextNode)
425         {
426             if (spareTime <= 0)
427             {
428                 return NOLANECHANGENEEDED; // It is not yet time to worry; this lane will do as well as any other
429             }
430             int laneCount = countCompatibleLanes(linkBeforeBranch, gtu.getType());
431             if (0 == laneCount)
432             {
433                 throw new NetworkException("No compatible Lanes on Link " + linkBeforeBranch);
434             }
435             if (1 == laneCount)
436             {
437                 return NOLANECHANGENEEDED; // Only one compatible lane available; we'll get there "automatically";
438                 // i.e. without influence from the Route
439             }
440             int branching = nextNode.getLinks().size();
441             if (branching > 2)
442             { // Found a split
443                 nextSplitNode = nextNode;
444                 break;
445             }
446             else if (1 == branching)
447             {
448                 return NOLANECHANGENEEDED; // dead end; no more choices to make
449             }
450             else
451             { // Look beyond this nextNode
452                 Link nextLink = gtu.getStrategicalPlanner().nextLink(lastLink, gtu.getType());
453                 if (nextLink instanceof CrossSectionLink)
454                 {
455                     nextNode = nextLink.getEndNode();
456                     // Oops: wrong code added the length of linkBeforeBranch in stead of length of nextLink
457                     remainingDistance += nextLink.getLength().getSI();
458                     linkBeforeBranch = (CrossSectionLink) nextLink;
459                     // Figure out the new currentLane
460                     if (currentLane.nextLanes(gtu.getType()).size() == 0)
461                     {
462                         // Lane drop; our lane disappears. This is a compulsory lane change; which is not controlled
463                         // by the Route. Perform the forced lane change.
464                         if (currentLane.accessibleAdjacentLanesLegal(LateralDirectionality.RIGHT, gtu.getType()).size() > 0)
465                         {
466                             for (Lane adjacentLane : currentLane.accessibleAdjacentLanesLegal(LateralDirectionality.RIGHT,
467                                     gtu.getType()))
468                             {
469                                 if (adjacentLane.nextLanes(gtu.getType()).size() > 0)
470                                 {
471                                     currentLane = adjacentLane;
472                                     break;
473                                 }
474                                 // If there are several adjacent lanes that have non empty nextLanes, we simple take the
475                                 // first in the set
476                             }
477                         }
478                         for (Lane adjacentLane : currentLane.accessibleAdjacentLanesLegal(LateralDirectionality.LEFT,
479                                 gtu.getType()))
480                         {
481                             if (adjacentLane.nextLanes(gtu.getType()).size() > 0)
482                             {
483                                 currentLane = adjacentLane;
484                                 break;
485                             }
486                             // If there are several adjacent lanes that have non empty nextLanes, we simple take the
487                             // first in the set
488                         }
489                         if (currentLane.nextLanes(gtu.getType()).size() == 0)
490                         {
491                             throw new NetworkException(
492                                     "Lane ends and there is not a compatible adjacent lane that does " + "not end");
493                         }
494                     }
495                     // Any compulsory lane change(s) have been performed and there is guaranteed a compatible next lane.
496                     for (Lane nextLane : currentLane.nextLanes(gtu.getType()))
497                     {
498                         currentLane = currentLane.nextLanes(gtu.getType()).iterator().next();
499                         break;
500                     }
501                     spareTime -= currentLane.getLength().getSI() / currentLane.getSpeedLimit(gtu.getType()).getSI();
502                 }
503                 else
504                 {
505                     // There is a non-CrossSectionLink on the path to the next branch. A non-CrossSectionLink does not
506                     // have identifiable Lanes, therefore we can't aim for a particular Lane
507                     return NOLANECHANGENEEDED; // Any Lane will do equally well
508                 }
509                 lastLink = nextLink;
510             }
511         }
512         if (null == nextNode)
513         {
514             throw new NetworkException("Cannot find the next branch or sink node");
515         }
516         // We have now found the first upcoming branching Node
517         // Which continuing link is the one we need?
518         Map<Lane, Length> suitabilityOfLanesBeforeBranch = new LinkedHashMap<>();
519         Link linkAfterBranch = gtu.getStrategicalPlanner().nextLink(lastLink, gtu.getType());
520         for (CrossSectionElement cse : linkBeforeBranch.getCrossSectionElementList())
521         {
522             if (cse instanceof Lane)
523             {
524                 Lane l = (Lane) cse;
525                 if (l.getType().isCompatible(gtu.getType()))
526                 {
527                     for (Lane connectingLane : l.nextLanes(gtu.getType()))
528                     {
529                         if (connectingLane.getLink() == linkAfterBranch && connectingLane.getType().isCompatible(gtu.getType()))
530                         {
531                             Length currentValue = suitabilityOfLanesBeforeBranch.get(l);
532                             // Use recursion to find out HOW suitable this continuation lane is, but don't revert back
533                             // to the maximum time horizon (or we could end up in infinite recursion when there are
534                             // loops in the network).
535                             Length value = suitability(connectingLane, new Length(0, LengthUnit.SI), gtu,
536                                     new Duration(spareTime, DurationUnit.SI));
537                             // This line was missing...
538                             value = value.plus(new Length(remainingDistance, LengthUnit.SI));
539                             // Use the minimum of the value computed for the first split junction (if there is one)
540                             // and the value computed for the second split junction.
541                             suitabilityOfLanesBeforeBranch.put(l,
542                                     null == currentValue || value.le(currentValue) ? value : currentValue);
543                         }
544                     }
545                 }
546             }
547         }
548         if (suitabilityOfLanesBeforeBranch.size() == 0)
549         {
550             throw new NetworkException("No lanes available on Link " + linkBeforeBranch);
551         }
552         Length currentLaneSuitability = suitabilityOfLanesBeforeBranch.get(currentLane);
553         if (null != currentLaneSuitability)
554         {
555             return currentLaneSuitability; // Following the current lane will keep us on the Route
556         }
557         // Performing one or more lane changes (left or right) is required.
558         int totalLanes = countCompatibleLanes(currentLane.getLink(), gtu.getType());
559         Length leftSuitability = computeSuitabilityWithLaneChanges(currentLane, remainingDistance,
560                 suitabilityOfLanesBeforeBranch, totalLanes, LateralDirectionality.LEFT, gtu.getType());
561         Length rightSuitability = computeSuitabilityWithLaneChanges(currentLane, remainingDistance,
562                 suitabilityOfLanesBeforeBranch, totalLanes, LateralDirectionality.RIGHT, gtu.getType());
563         if (leftSuitability.ge(rightSuitability))
564         {
565             return leftSuitability;
566         }
567         else if (rightSuitability.ge(leftSuitability))
568         {
569             // TODO
570             return rightSuitability;
571         }
572         if (leftSuitability.le(GETOFFTHISLANENOW))
573         {
574             throw new NetworkException("Changing lanes in any direction does not get the GTU on a suitable lane");
575         }
576         return leftSuitability; // left equals right; this is odd but topologically possible
577     }
578 
579     /**
580      * Compute the suitability of a lane from which lane changes are required to get to the next point on the Route.<br>
581      * This method weighs the suitability of the nearest suitable lane by (m - n) / m where n is the number of lane changes
582      * required and m is the total number of lanes in the CrossSectionLink.
583      * @param startLane the current lane of the GTU
584      * @param remainingDistance distance in m of GTU to first branch
585      * @param suitabilities the set of suitable lanes and their suitability
586      * @param totalLanes total number of lanes compatible with the GTU type
587      * @param direction the direction of the lane changes to attempt
588      * @param gtuType the type of the GTU
589      * @return the suitability of the <cite>startLane</cite> for following the Route
590      */
591     protected final Length computeSuitabilityWithLaneChanges(final Lane startLane, final double remainingDistance,
592             final Map<Lane, Length> suitabilities, final int totalLanes, final LateralDirectionality direction,
593             final GtuType gtuType)
594     {
595         /*-
596          * The time per required lane change seems more relevant than distance per required lane change.
597          * Total time required does not grow linearly with the number of required lane changes. Logarithmic, arc tangent 
598          * is more like it.
599          * Rijkswaterstaat appears to use a fixed time for ANY number of lane changes (about 60s). 
600          * TomTom navigation systems give more time (about 90s).
601          * In this method the returned suitability decreases linearly with the number of required lane changes. This
602          * ensures that there is a gradient that coaches the GTU towards the most suitable lane.
603          */
604         int laneChangesUsed = 0;
605         Lane currentLane = startLane;
606         Length currentSuitability = null;
607         while (null == currentSuitability)
608         {
609             laneChangesUsed++;
610             if (currentLane.accessibleAdjacentLanesLegal(direction, gtuType).size() == 0)
611             {
612                 return GETOFFTHISLANENOW;
613             }
614             currentLane = currentLane.accessibleAdjacentLanesLegal(direction, gtuType).iterator().next();
615             currentSuitability = suitabilities.get(currentLane);
616         }
617         double fraction = currentSuitability == NOLANECHANGENEEDED ? 0 : 0.5;
618         int notSuitableLaneCount = totalLanes - suitabilities.size();
619         return new Length(
620                 remainingDistance * (notSuitableLaneCount - laneChangesUsed + 1 + fraction) / (notSuitableLaneCount + fraction),
621                 LengthUnit.SI);
622     }
623 
624     /**
625      * Determine how many lanes on a CrossSectionLink are compatible with a particular GTU type.<br>
626      * TODO: this method should probably be moved into the CrossSectionLink class
627      * @param link the link
628      * @param gtuType the GTU type
629      * @return the number of lanes on the link that are compatible with the GTU type
630      */
631     protected final int countCompatibleLanes(final CrossSectionLink link, final GtuType gtuType)
632     {
633         int result = 0;
634         for (CrossSectionElement cse : link.getCrossSectionElementList())
635         {
636             if (cse instanceof Lane)
637             {
638                 Lane l = (Lane) cse;
639                 if (l.getType().isCompatible(gtuType))
640                 {
641                     result++;
642                 }
643             }
644         }
645         return result;
646     }
647 
648     @Override
649     public final String toString()
650     {
651         return "LaneBasedCFLCTacticalPlanner [laneChangeModel=" + this.laneChangeModel + "]";
652     }
653 
654 }