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.HashSet;
6   import java.util.List;
7   import java.util.Set;
8   
9   import nl.tudelft.simulation.dsol.SimRuntimeException;
10  import nl.tudelft.simulation.language.d3.DirectedPoint;
11  
12  import org.djunits.unit.AccelerationUnit;
13  import org.djunits.unit.SpeedUnit;
14  import org.djunits.unit.TimeUnit;
15  import org.djunits.value.vdouble.scalar.Acceleration;
16  import org.djunits.value.vdouble.scalar.Duration;
17  import org.djunits.value.vdouble.scalar.Length;
18  import org.djunits.value.vdouble.scalar.Speed;
19  import org.djunits.value.vdouble.scalar.Time;
20  import org.opentrafficsim.core.geometry.OTSGeometryException;
21  import org.opentrafficsim.core.geometry.OTSLine3D;
22  import org.opentrafficsim.core.geometry.OTSPoint3D;
23  import org.opentrafficsim.core.gtu.GTUDirectionality;
24  import org.opentrafficsim.core.gtu.GTUException;
25  import org.opentrafficsim.core.gtu.TurnIndicatorStatus;
26  import org.opentrafficsim.core.gtu.behavioralcharacteristics.BehavioralCharacteristics;
27  import org.opentrafficsim.core.gtu.behavioralcharacteristics.ParameterException;
28  import org.opentrafficsim.core.gtu.behavioralcharacteristics.ParameterTypes;
29  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlan;
30  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlan.Segment;
31  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlanException;
32  import org.opentrafficsim.core.network.LateralDirectionality;
33  import org.opentrafficsim.core.network.NetworkException;
34  import org.opentrafficsim.road.gtu.lane.LaneBasedGTU;
35  import org.opentrafficsim.road.gtu.lane.perception.LanePerception;
36  import org.opentrafficsim.road.gtu.lane.perception.categories.DefaultAlexander;
37  import org.opentrafficsim.road.gtu.lane.perception.headway.Headway;
38  import org.opentrafficsim.road.gtu.lane.tactical.directedlanechange.DirectedAltruistic;
39  import org.opentrafficsim.road.gtu.lane.tactical.directedlanechange.DirectedEgoistic;
40  import org.opentrafficsim.road.gtu.lane.tactical.directedlanechange.DirectedLaneChangeModel;
41  import org.opentrafficsim.road.gtu.lane.tactical.directedlanechange.DirectedLaneMovementStep;
42  import org.opentrafficsim.road.gtu.lane.tactical.following.AccelerationStep;
43  import org.opentrafficsim.road.gtu.lane.tactical.following.GTUFollowingModelOld;
44  import org.opentrafficsim.road.network.lane.Lane;
45  import org.opentrafficsim.road.network.lane.LaneDirection;
46  
47  /**
48   * Lane-based tactical planner that implements car following behavior and rule-based lane change. This tactical planner
49   * retrieves the car following model from the strategical planner and will generate an operational plan for the GTU.
50   * <p>
51   * A lane change occurs when:
52   * <ol>
53   * <li>The route indicates that the current lane does not lead to the destination; main choices are the time when the GTU
54   * switches to the "right" lane, and what should happen when the split gets closer and the lane change has failed. Observations
55   * indicate that vehicles if necessary stop in their current lane until they can go to the desired lane. A lane drop is
56   * automatically part of this implementation, because the lane with a lane drop will not lead to the GTU's destination.</li>
57   * <li>The desired speed of the vehicle is a particular delta-speed higher than its predecessor, the headway to the predecessor
58   * in the current lane has exceeded a certain value, it is allowed to change to the target lane, the target lane lies on the
59   * GTU's route, and the gap in the target lane is acceptable (including the evaluation of the perceived speed of a following GTU
60   * in the target lane).</li>
61   * <li>The current lane is not the optimum lane given the traffic rules (for example, to keep right), the headway to the
62   * predecessor on the target lane is greater than a certain value, the speed of the predecessor on the target lane is greater
63   * than or equal to our speed, the target lane is on the route, it is allowed to switch to the target lane, and the gap at the
64   * target lane is acceptable (including the perceived speed of any vehicle in front or behind on the target lane).</li>
65   * </ol>
66   * <p>
67   * This lane-based tactical planner makes decisions based on headway (GTU following model). It can ask the strategic planner for
68   * assistance on the route to take when the network splits.
69   * <p>
70   * Copyright (c) 2013-2016 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
71   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
72   * </p>
73   * $LastChangedDate: 2015-07-24 02:58:59 +0200 (Fri, 24 Jul 2015) $, @version $Revision: 1147 $, by $Author: averbraeck $,
74   * initial version Nov 25, 2015 <br>
75   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
76   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
77   */
78  public class LaneBasedGTUFollowingLaneChangeTacticalPlanner extends AbstractLaneBasedTacticalPlanner
79  {
80      /** */
81      private static final long serialVersionUID = 20160129L;
82  
83      /** Lane change time (fixed foe now. */
84      private static final double LANECHANGETIME = 2.0;
85  
86      /** Earliest next lane change time. */
87      private Time earliestNexLaneChangeTime = Time.ZERO;
88  
89      /** Bezier curve points for gradual lane change. */
90      private static final double[] SCURVE;
91  
92      static
93      {
94          SCURVE = new double[65];
95          for (int i = 0; i <= 64; i++)
96          {
97              double t = i / 64.0;
98              double ot = 1.0 - t;
99              double t3 = t * t * t;
100             SCURVE[i] = 10.0 * t3 * ot * ot + 5.0 * t3 * t * ot + t3 * t * t;
101         }
102     }
103 
104     /** Following model for this tactical planner. */
105     private GTUFollowingModelOld carFollowingModel;
106 
107     /**
108      * Instantiated a tactical planner with just GTU following behavior and no lane changes.
109      * @param carFollowingModel Car-following model.
110      * @param gtu GTU
111      */
112     public LaneBasedGTUFollowingLaneChangeTacticalPlanner(final GTUFollowingModelOld carFollowingModel,
113         final LaneBasedGTU gtu)
114     {
115         super(carFollowingModel, gtu);
116     }
117 
118     /** {@inheritDoc} */
119     @Override
120     public OperationalPlan generateOperationalPlan(final Time startTime, final DirectedPoint locationAtStartTime)
121         throws OperationalPlanException, NetworkException, GTUException, ParameterException
122     {
123         // ask Perception for the local situation
124         LaneBasedGTU laneBasedGTU = getGtu();
125         LanePerception perception = getPerception();
126         BehavioralCharacteristics behaviorCharacteristics = laneBasedGTU.getBehavioralCharacteristics();
127         behaviorCharacteristics.setParameter(ParameterTypes.LOOKAHEAD, ParameterTypes.LOOKAHEAD.getDefaultValue());
128 
129         // start with the turn indicator off -- this can change during the method
130         laneBasedGTU.setTurnIndicatorStatus(TurnIndicatorStatus.NONE);
131 
132         // if the GTU's maximum speed is zero (block), generate a stand still plan for one second
133         if (laneBasedGTU.getMaximumSpeed().si < OperationalPlan.DRIFTING_SPEED_SI)
134         {
135             return new OperationalPlan(getGtu(), locationAtStartTime, startTime, new Duration(1.0, TimeUnit.SECOND));
136         }
137 
138         // perceive the forward headway, accessible lanes and speed limit.
139         perception.getPerceptionCategory(DefaultAlexander.class).updateForwardHeadway();
140         perception.getPerceptionCategory(DefaultAlexander.class).updateAccessibleAdjacentLanesLeft();
141         perception.getPerceptionCategory(DefaultAlexander.class).updateAccessibleAdjacentLanesRight();
142         perception.getPerceptionCategory(DefaultAlexander.class).updateSpeedLimit();
143 
144         // find out where we are going
145         Length forwardHeadway = behaviorCharacteristics.getParameter(ParameterTypes.LOOKAHEAD);
146         LanePathInfo lanePathInfo = buildLanePathInfo(laneBasedGTU, forwardHeadway);
147         NextSplitInfo nextSplitInfo = determineNextSplit(laneBasedGTU, forwardHeadway);
148         Set<Lane> correctLanes = laneBasedGTU.getLanes().keySet();
149         correctLanes.retainAll(nextSplitInfo.getCorrectCurrentLanes());
150 
151         // Step 1: Do we want to change lanes because of the current lane not leading to our destination?
152         if (lanePathInfo.getPath().getLength().lt(forwardHeadway))
153         {
154             if (correctLanes.isEmpty())
155             {
156                 LateralDirectionality direction = determineLeftRight(laneBasedGTU, nextSplitInfo);
157                 if (direction != null)
158                 {
159                     getGtu().setTurnIndicatorStatus(
160                         direction.isLeft() ? TurnIndicatorStatus.LEFT : TurnIndicatorStatus.RIGHT);
161                     OperationalPlan laneChangePlan =
162                         makeLaneChangePlanMobil(laneBasedGTU, perception, lanePathInfo, direction);
163                     if (laneChangePlan != null)
164                     {
165                         return laneChangePlan;
166                     }
167                 }
168             }
169         }
170 
171         // Condition, if we have just changed lane, let's not change immediately again.
172         if (getGtu().getSimulator().getSimulatorTime().getTime().lt(this.earliestNexLaneChangeTime))
173         {
174             return currentLanePlan(laneBasedGTU, startTime, locationAtStartTime, lanePathInfo);
175         }
176 
177         // Step 2. Do we want to change lanes to the left because of our predecessor on the current lane?
178         // does the lane left of us [TODO: driving direction] bring us to our destination as well?
179         Set<Lane> leftLanes =
180             perception.getPerceptionCategory(DefaultAlexander.class).getAccessibleAdjacentLanesLeft().get(
181                 lanePathInfo.getReferenceLane());
182         if (nextSplitInfo.isSplit())
183         {
184             leftLanes.retainAll(nextSplitInfo.getCorrectCurrentLanes());
185         }
186         if (!leftLanes.isEmpty() && laneBasedGTU.getSpeed().si > 4.0) // XXX we are driving...
187         {
188             perception.getPerceptionCategory(DefaultAlexander.class).updateBackwardHeadway();
189             perception.getPerceptionCategory(DefaultAlexander.class).updateParallelHeadwaysLeft();
190             perception.getPerceptionCategory(DefaultAlexander.class).updateNeighboringHeadwaysLeft();
191             if (perception.getPerceptionCategory(DefaultAlexander.class).getParallelHeadwaysLeft().isEmpty())
192             {
193                 Collection<Headway> sameLaneTraffic = new HashSet<>();
194                 // TODO should it be getObjectType().isGtu() or !getObjectType().isDistanceOnly() ?
195                 if (perception.getPerceptionCategory(DefaultAlexander.class).getForwardHeadway() != null
196                     && perception.getPerceptionCategory(DefaultAlexander.class).getForwardHeadway().getObjectType().isGtu())
197                 {
198                     sameLaneTraffic.add(perception.getPerceptionCategory(DefaultAlexander.class).getForwardHeadway());
199                 }
200                 if (perception.getPerceptionCategory(DefaultAlexander.class).getBackwardHeadway() != null
201                     && perception.getPerceptionCategory(DefaultAlexander.class).getBackwardHeadway().getObjectType().isGtu())
202                 {
203                     sameLaneTraffic.add(perception.getPerceptionCategory(DefaultAlexander.class).getBackwardHeadway());
204                 }
205                 DirectedLaneChangeModel dlcm = new DirectedAltruistic(getPerception());
206                 DirectedLaneMovementStep dlms =
207                     dlcm.computeLaneChangeAndAcceleration(laneBasedGTU, LateralDirectionality.LEFT, sameLaneTraffic,
208                         perception.getPerceptionCategory(DefaultAlexander.class).getNeighboringHeadwaysLeft(),
209                         behaviorCharacteristics.getParameter(ParameterTypes.LOOKAHEAD), perception.getPerceptionCategory(
210                             DefaultAlexander.class).getSpeedLimit(), new Acceleration(1.0, AccelerationUnit.SI),
211                         new Acceleration(0.5, AccelerationUnit.SI), new Duration(LANECHANGETIME, TimeUnit.SECOND));
212                 if (dlms.getLaneChange() != null)
213                 {
214                     getGtu().setTurnIndicatorStatus(TurnIndicatorStatus.LEFT);
215                     OperationalPlan laneChangePlan =
216                         makeLaneChangePlanMobil(laneBasedGTU, perception, lanePathInfo, LateralDirectionality.LEFT);
217                     if (laneChangePlan != null)
218                     {
219                         return laneChangePlan;
220                     }
221                 }
222             }
223         }
224 
225         // Step 3. Do we want to change lanes to the right because of traffic rules?
226         Set<Lane> rightLanes =
227             perception.getPerceptionCategory(DefaultAlexander.class).getAccessibleAdjacentLanesRight().get(
228                 lanePathInfo.getReferenceLane());
229         if (nextSplitInfo.isSplit())
230         {
231             rightLanes.retainAll(nextSplitInfo.getCorrectCurrentLanes());
232         }
233         if (!rightLanes.isEmpty() && laneBasedGTU.getSpeed().si > 4.0) // XXX we are driving...
234         {
235             perception.getPerceptionCategory(DefaultAlexander.class).updateBackwardHeadway();
236             perception.getPerceptionCategory(DefaultAlexander.class).updateParallelHeadwaysRight();
237             perception.getPerceptionCategory(DefaultAlexander.class).updateNeighboringHeadwaysRight();
238             if (perception.getPerceptionCategory(DefaultAlexander.class).getParallelHeadwaysRight().isEmpty())
239             {
240                 // TODO should it be getObjectType().isGtu() or !getObjectType().isDistanceOnly() ?
241                 Collection<Headway> sameLaneTraffic = new HashSet<>();
242                 if (perception.getPerceptionCategory(DefaultAlexander.class).getForwardHeadway() != null
243                     && perception.getPerceptionCategory(DefaultAlexander.class).getForwardHeadway().getObjectType().isGtu())
244                 {
245                     sameLaneTraffic.add(perception.getPerceptionCategory(DefaultAlexander.class).getForwardHeadway());
246                 }
247                 if (perception.getPerceptionCategory(DefaultAlexander.class).getBackwardHeadway() != null
248                     && perception.getPerceptionCategory(DefaultAlexander.class).getBackwardHeadway().getObjectType().isGtu())
249                 {
250                     sameLaneTraffic.add(perception.getPerceptionCategory(DefaultAlexander.class).getBackwardHeadway());
251                 }
252                 DirectedLaneChangeModel dlcm = new DirectedAltruistic(getPerception());
253                 DirectedLaneMovementStep dlms =
254                     dlcm.computeLaneChangeAndAcceleration(laneBasedGTU, LateralDirectionality.RIGHT, sameLaneTraffic,
255                         perception.getPerceptionCategory(DefaultAlexander.class).getNeighboringHeadwaysRight(),
256                         behaviorCharacteristics.getParameter(ParameterTypes.LOOKAHEAD), perception.getPerceptionCategory(
257                             DefaultAlexander.class).getSpeedLimit(), new Acceleration(1.0, AccelerationUnit.SI),
258                         new Acceleration(0.5, AccelerationUnit.SI), new Duration(LANECHANGETIME, TimeUnit.SECOND));
259                 if (dlms.getLaneChange() != null)
260                 {
261                     getGtu().setTurnIndicatorStatus(TurnIndicatorStatus.RIGHT);
262                     OperationalPlan laneChangePlan =
263                         makeLaneChangePlanMobil(laneBasedGTU, perception, lanePathInfo, LateralDirectionality.RIGHT);
264                     if (laneChangePlan != null)
265                     {
266                         return laneChangePlan;
267                     }
268                 }
269             }
270         }
271 
272         return currentLanePlan(laneBasedGTU, startTime, locationAtStartTime, lanePathInfo);
273     }
274 
275     /**
276      * Make a plan for the current lane.
277      * @param laneBasedGTU the gtu to generate the plan for
278      * @param startTime the time from which the new operational plan has to be operational
279      * @param locationAtStartTime the location of the GTU at the start time of the new plan
280      * @param lanePathInfo the lane path for the current lane.
281      * @return An operation plan for staying in the current lane.
282      * @throws OperationalPlanException when there is a problem planning a path in the network
283      * @throws GTUException when there is a problem with the state of the GTU when planning a path
284      */
285     private OperationalPlan currentLanePlan(final LaneBasedGTU laneBasedGTU, final Time startTime,
286         final DirectedPoint locationAtStartTime, final LanePathInfo lanePathInfo) throws OperationalPlanException,
287         GTUException
288     {
289         LanePerception perception = getPerception();
290 
291         // No lane change. Continue on current lane.
292         AccelerationStep accelerationStep;
293         Headway headway = perception.getPerceptionCategory(DefaultAlexander.class).getForwardHeadway();
294         Length maxDistance = lanePathInfo.getPath().getLength().minus(laneBasedGTU.getLength().multiplyBy(2.0));
295         accelerationStep =
296             this.carFollowingModel.computeAccelerationStep(laneBasedGTU, headway.getSpeed(), headway.getDistance(),
297                 maxDistance, perception.getPerceptionCategory(DefaultAlexander.class).getSpeedLimit());
298 
299         // see if we have to continue standing still. In that case, generate a stand still plan
300         if (accelerationStep.getAcceleration().si < 1E-6 && laneBasedGTU.getSpeed().si < OperationalPlan.DRIFTING_SPEED_SI)
301         {
302             return new OperationalPlan(laneBasedGTU, locationAtStartTime, startTime, accelerationStep.getDuration());
303         }
304 
305         // build a list of lanes forward, with a maximum headway.
306         List<Segment> operationalPlanSegmentList = new ArrayList<>();
307         if (accelerationStep.getAcceleration().si == 0.0)
308         {
309             Segment segment = new OperationalPlan.SpeedSegment(accelerationStep.getDuration());
310             operationalPlanSegmentList.add(segment);
311         }
312         else
313         {
314             Segment segment =
315                 new OperationalPlan.AccelerationSegment(accelerationStep.getDuration(), accelerationStep.getAcceleration());
316             operationalPlanSegmentList.add(segment);
317         }
318         OperationalPlan op =
319             new OperationalPlan(laneBasedGTU, lanePathInfo.getPath(), startTime, laneBasedGTU.getSpeed(),
320                 operationalPlanSegmentList);
321         return op;
322     }
323 
324     /**
325      * We are not on a lane that leads to our destination. Determine whether the lateral direction to go is left or right.
326      * @param laneBasedGTU the gtu
327      * @param nextSplitInfo the information about the next split
328      * @return the lateral direction to go, or null if this cannot be determined
329      */
330     private LateralDirectionality determineLeftRight(final LaneBasedGTU laneBasedGTU, final NextSplitInfo nextSplitInfo)
331     {
332         // are the lanes in nextSplitInfo.getCorrectCurrentLanes() left or right of the current lane(s) of the GTU?
333         for (Lane correctLane : nextSplitInfo.getCorrectCurrentLanes())
334         {
335             for (Lane currentLane : laneBasedGTU.getLanes().keySet())
336             {
337                 if (correctLane.getParentLink().equals(currentLane.getParentLink()))
338                 {
339                     double deltaOffset =
340                         correctLane.getDesignLineOffsetAtBegin().si - currentLane.getDesignLineOffsetAtBegin().si;
341                     if (laneBasedGTU.getLanes().get(currentLane).equals(GTUDirectionality.DIR_PLUS))
342                     {
343                         return deltaOffset > 0 ? LateralDirectionality.LEFT : LateralDirectionality.RIGHT;
344                     }
345                     else
346                     {
347                         return deltaOffset < 0 ? LateralDirectionality.LEFT : LateralDirectionality.RIGHT;
348                     }
349                 }
350             }
351         }
352         return null;
353     }
354 
355     /**
356      * Make a lane change in the given direction if possible, and return the operational plan, or null if a lane change is not
357      * possible.
358      * @param gtu the GTU that has to make the lane change
359      * @param perception the perception, where forward headway, accessible lanes and speed limit have been assessed
360      * @param lanePathInfo the information for the path on the current lane
361      * @param direction the lateral direction, either LEFT or RIGHT
362      * @return the operational plan for the required lane change, or null if a lane change is not possible.
363      * @throws NetworkException when there is a network inconsistency in updating the perception
364      * @throws GTUException when there is an issue retrieving GTU information for the perception update
365      * @throws ParameterException In case af a parameter problem.
366      * @throws OperationalPlanException if a perception category is not present
367      */
368     private OperationalPlan makeLaneChangePlanMobil(final LaneBasedGTU gtu, final LanePerception perception,
369         final LanePathInfo lanePathInfo, final LateralDirectionality direction) throws GTUException, NetworkException,
370         ParameterException, OperationalPlanException
371     {
372         Collection<Headway> otherLaneTraffic;
373         perception.getPerceptionCategory(DefaultAlexander.class).updateForwardHeadway();
374         perception.getPerceptionCategory(DefaultAlexander.class).updateBackwardHeadway();
375         if (direction.isLeft())
376         {
377             perception.getPerceptionCategory(DefaultAlexander.class).updateParallelHeadwaysLeft();
378             perception.getPerceptionCategory(DefaultAlexander.class).updateNeighboringHeadwaysLeft();
379             otherLaneTraffic = perception.getPerceptionCategory(DefaultAlexander.class).getNeighboringHeadwaysLeft();
380         }
381         else if (direction.isRight())
382         {
383             perception.getPerceptionCategory(DefaultAlexander.class).updateParallelHeadwaysRight();
384             perception.getPerceptionCategory(DefaultAlexander.class).updateNeighboringHeadwaysRight();
385             otherLaneTraffic = perception.getPerceptionCategory(DefaultAlexander.class).getNeighboringHeadwaysRight();
386         }
387         else
388         {
389             throw new GTUException("lateral direction during lane change neither LEFT nor RIGHT");
390         }
391         if (!perception.getPerceptionCategory(DefaultAlexander.class).getParallelHeadways(direction).isEmpty())
392         {
393             return null;
394         }
395 
396         Collection<Headway> sameLaneTraffic = new HashSet<>();
397         // TODO should it be getObjectType().isGtu() or !getObjectType().isDistanceOnly() ?
398         if (perception.getPerceptionCategory(DefaultAlexander.class).getForwardHeadway() != null
399             && perception.getPerceptionCategory(DefaultAlexander.class).getForwardHeadway().getObjectType().isGtu())
400         {
401             sameLaneTraffic.add(perception.getPerceptionCategory(DefaultAlexander.class).getForwardHeadway());
402         }
403         if (perception.getPerceptionCategory(DefaultAlexander.class).getBackwardHeadway() != null
404             && perception.getPerceptionCategory(DefaultAlexander.class).getBackwardHeadway().getObjectType().isGtu())
405         {
406             sameLaneTraffic.add(perception.getPerceptionCategory(DefaultAlexander.class).getBackwardHeadway());
407         }
408         // XXX do not understamd why this is added...
409         // Length maxDistance = lanePathInfo.getPath().getLength().minus(gtu.getLength().multiplyBy(2.0));
410         // sameLaneTraffic.add(new HeadwayGTU("ENDPATH", Speed.ZERO, maxDistance, GTUType.NONE));
411         // otherLaneTraffic.add(new HeadwayGTU("ENDPATH", Speed.ZERO, maxDistance, GTUType.NONE));
412 
413         // TODO if we move from standstill, create a longer plan, e.g. 4-5 seconds, with high acceleration!
414         // TODO make type of plan (Egoistic, Altruistic) parameter of the class
415         DirectedLaneChangeModel dlcm = new DirectedEgoistic(getPerception());
416         // TODO make the elasticities 2.0 and 0.1 parameters of the class
417         DirectedLaneMovementStep dlms =
418             dlcm.computeLaneChangeAndAcceleration(gtu, direction, sameLaneTraffic, otherLaneTraffic, gtu
419                 .getBehavioralCharacteristics().getParameter(ParameterTypes.LOOKAHEAD), perception.getPerceptionCategory(
420                 DefaultAlexander.class).getSpeedLimit(), new Acceleration(2.0, AccelerationUnit.SI), new Acceleration(0.1,
421                 AccelerationUnit.SI), new Duration(LANECHANGETIME, TimeUnit.SECOND));
422         if (dlms.getLaneChange() == null)
423         {
424             return null;
425         }
426 
427         Lane startLane = getReferenceLane(gtu);
428         Set<Lane> adjacentLanes = startLane.accessibleAdjacentLanes(direction, gtu.getGTUType());
429         // TODO take the widest (now a random one)
430         Lane adjacentLane = adjacentLanes.iterator().next();
431         Length startPosition = gtu.position(startLane, gtu.getReference());
432         double fraction2 = startLane.fraction(startPosition);
433         LanePathInfo lanePathInfo2 =
434             buildLanePathInfo(gtu, gtu.getBehavioralCharacteristics().getParameter(ParameterTypes.LOOKAHEAD), adjacentLane,
435                 fraction2, gtu.getLanes().get(startLane));
436 
437         // interpolate the path for the most conservative one
438         AccelerationStep accelerationStep = dlms.getGfmr();
439         Speed v0 = gtu.getSpeed();
440         double t = accelerationStep.getDuration().si;
441         double distanceSI = v0.si * t + 0.5 * accelerationStep.getAcceleration().si * t * t;
442         Speed vt = v0.plus(accelerationStep.getAcceleration().multiplyBy(accelerationStep.getDuration()));
443 
444         // XXX if the distance is too small, do not build a path. Minimum = 0.5 * vehicle length
445         // TODO this should be solved in the time domain, not in the distance domain...
446         if (distanceSI < 2.0) // XXX arbitrary...
447         {
448             return null;
449         }
450 
451         // TODO can forwardHeadway still return null?
452         if (perception.getPerceptionCategory(DefaultAlexander.class).getForwardHeadway() == null
453             || (perception.getPerceptionCategory(DefaultAlexander.class).getForwardHeadway() != null && perception
454                 .getPerceptionCategory(DefaultAlexander.class).getForwardHeadway().getDistance().si < 5.0))
455         {
456             return null;
457         }
458 
459         OTSLine3D path;
460         try
461         {
462             path = interpolateScurve(lanePathInfo.getPath(), lanePathInfo2.getPath(), distanceSI);
463         }
464         catch (OTSGeometryException exception)
465         {
466             System.err.println("GTU          : " + gtu);
467             System.err.println("LanePathInfo : " + lanePathInfo.getPath());
468             System.err.println("LanePathInfo2: " + lanePathInfo2.getPath());
469             System.err.println("distanceSI   : " + distanceSI);
470             System.err.println("v0, t, vt, a : " + v0 + ", " + t + ", " + vt + ", " + accelerationStep.getAcceleration());
471             throw new GTUException(exception);
472         }
473 
474         try
475         {
476             double a = accelerationStep.getAcceleration().si;
477             // recalculate based on actual path length...
478             if (path.getLengthSI() > distanceSI * 1.5) // XXX arbitrary...
479             {
480                 a = (path.getLengthSI() - v0.si) / LANECHANGETIME;
481                 vt = new Speed(v0.si + LANECHANGETIME * a, SpeedUnit.SI);
482             }
483 
484             // enter the other lane(s) at the same fractional position as the current position on the lane(s)
485             // schedule leaving the current lane(s) that do not overlap with the target lane(s)
486             for (Lane lane : gtu.getLanes().keySet())
487             {
488                 gtu.getSimulator().scheduleEventRel(new Duration(LANECHANGETIME - 0.001, TimeUnit.SI), this, gtu,
489                     "leaveLane", new Object[] {lane});
490             }
491 
492             // also leave the lanes that we will still ENTER from the 'old' lanes:
493             for (LaneDirection laneDirection : lanePathInfo.getLaneDirectionList())
494             {
495                 if (!gtu.getLanes().keySet().contains(laneDirection.getLane()))
496                 {
497                     gtu.getSimulator().scheduleEventRel(new Duration(LANECHANGETIME - 0.001, TimeUnit.SI), this, gtu,
498                         "leaveLane", new Object[] {laneDirection.getLane()});
499                 }
500             }
501 
502             gtu.enterLane(adjacentLane, adjacentLane.getLength().multiplyBy(fraction2), gtu.getLanes().get(startLane));
503             System.out.println("gtu " + gtu.getId() + " entered lane " + adjacentLane + " at pos "
504                 + adjacentLane.getLength().multiplyBy(fraction2));
505 
506             List<Segment> operationalPlanSegmentList = new ArrayList<>();
507             if (a == 0.0)
508             {
509                 Segment segment = new OperationalPlan.SpeedSegment(new Duration(LANECHANGETIME, TimeUnit.SI));
510                 operationalPlanSegmentList.add(segment);
511             }
512             else
513             {
514                 Segment segment =
515                     new OperationalPlan.AccelerationSegment(new Duration(LANECHANGETIME, TimeUnit.SI), new Acceleration(a,
516                         AccelerationUnit.SI));
517                 operationalPlanSegmentList.add(segment);
518             }
519             OperationalPlan op =
520                 new OperationalPlan(gtu, path, gtu.getSimulator().getSimulatorTime().getTime(), v0,
521                     operationalPlanSegmentList);
522             this.earliestNexLaneChangeTime =
523                 gtu.getSimulator().getSimulatorTime().getTime().plus(new Duration(17, TimeUnit.SECOND));
524 
525             // make sure out turn indicator is on!
526             gtu.setTurnIndicatorStatus(direction.isLeft() ? TurnIndicatorStatus.LEFT : TurnIndicatorStatus.RIGHT);
527 
528             return op;
529         }
530         catch (OperationalPlanException | SimRuntimeException exception)
531         {
532             throw new GTUException(exception);
533         }
534     }
535 
536     /**
537      * Linearly interpolate between two lines.
538      * @param line1 first line
539      * @param line2 second line
540      * @param lengthSI length of the interpolation (at this point 100% line 2)
541      * @return a line between line 1 and line 2
542      * @throws OTSGeometryException when interpolation fails
543      */
544     private static OTSLine3D interpolateLinear(final OTSLine3D line1, final OTSLine3D line2, final double lengthSI)
545         throws OTSGeometryException
546     {
547         OTSLine3D l1 = line1.extract(0, lengthSI);
548         OTSLine3D l2 = line2.extract(0, lengthSI);
549         List<OTSPoint3D> line = new ArrayList<>();
550         int num = 32;
551         for (int i = 0; i <= num; i++)
552         {
553             double f0 = 1.0 * i / num;
554             double f1 = 1.0 - f0;
555             DirectedPoint p1 = l1.getLocationFraction(f0);
556             DirectedPoint p2 = l2.getLocationFraction(f0);
557             line.add(new OTSPoint3D(p1.x * f1 + p2.x * f0, p1.y * f1 + p2.y * f0, p1.z * f1 + p2.z * f0));
558         }
559         return new OTSLine3D(line);
560     }
561 
562     /**
563      * S-curve interpolation between two lines. We use a 5th order Bezier curve for this.
564      * @param line1 first line
565      * @param line2 second line
566      * @param lengthSI length of the interpolation (at this point 100% line 2)
567      * @return a line between line 1 and line 2
568      * @throws OTSGeometryException when interpolation fails
569      */
570     private static OTSLine3D interpolateScurve(final OTSLine3D line1, final OTSLine3D line2, final double lengthSI)
571         throws OTSGeometryException
572     {
573         OTSLine3D l1 = line1.extract(0, lengthSI);
574         OTSLine3D l2 = line2.extract(0, lengthSI);
575         List<OTSPoint3D> line = new ArrayList<>();
576         int num = 64;
577         for (int i = 0; i <= num; i++)
578         {
579             double factor = SCURVE[i];
580             DirectedPoint p1 = l1.getLocationFraction(i / 64.0);
581             DirectedPoint p2 = l2.getLocationFraction(i / 64.0);
582             line.add(new OTSPoint3D(p1.x + factor * (p2.x - p1.x), p1.y + factor * (p2.y - p1.y), p1.z + factor
583                 * (p2.z - p1.z)));
584         }
585         return new OTSLine3D(line);
586     }
587 
588     /** {@inheritDoc} */
589     @Override
590     public final String toString()
591     {
592         return "LaneBasedGTUFollowingLaneChangeTacticalPlanner [earliestNexLaneChangeTime=" + this.earliestNexLaneChangeTime
593             + ", carFollowingModel=" + this.carFollowingModel + "]";
594     }
595 }