View Javadoc
1   package org.opentrafficsim.road.gtu.lane;
2   
3   import java.util.ArrayList;
4   import java.util.HashMap;
5   import java.util.HashSet;
6   import java.util.LinkedHashMap;
7   import java.util.List;
8   import java.util.Map;
9   import java.util.Set;
10  
11  import javax.media.j3d.Bounds;
12  import javax.vecmath.Point3d;
13  
14  import org.djunits.unit.LengthUnit;
15  import org.djunits.value.vdouble.scalar.Duration;
16  import org.djunits.value.vdouble.scalar.Length;
17  import org.djunits.value.vdouble.scalar.Speed;
18  import org.djunits.value.vdouble.scalar.Time;
19  import org.opentrafficsim.core.dsol.OTSDEVSSimulatorInterface;
20  import org.opentrafficsim.core.dsol.OTSSimTimeDouble;
21  import org.opentrafficsim.core.geometry.OTSGeometryException;
22  import org.opentrafficsim.core.gtu.AbstractGTU;
23  import org.opentrafficsim.core.gtu.GTUDirectionality;
24  import org.opentrafficsim.core.gtu.GTUException;
25  import org.opentrafficsim.core.gtu.GTUType;
26  import org.opentrafficsim.core.gtu.RelativePosition;
27  import org.opentrafficsim.core.gtu.behavioralcharacteristics.BehavioralCharacteristics;
28  import org.opentrafficsim.core.gtu.behavioralcharacteristics.ParameterException;
29  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlanException;
30  import org.opentrafficsim.core.network.LateralDirectionality;
31  import org.opentrafficsim.core.network.Link;
32  import org.opentrafficsim.core.network.NetworkException;
33  import org.opentrafficsim.core.network.Node;
34  import org.opentrafficsim.core.network.OTSNetwork;
35  import org.opentrafficsim.road.gtu.lane.tactical.LaneBasedTacticalPlanner;
36  import org.opentrafficsim.road.gtu.strategical.LaneBasedStrategicalPlanner;
37  import org.opentrafficsim.road.gtu.strategical.route.LaneBasedStrategicalRoutePlanner;
38  import org.opentrafficsim.road.network.lane.CrossSectionElement;
39  import org.opentrafficsim.road.network.lane.CrossSectionLink;
40  import org.opentrafficsim.road.network.lane.DirectedLanePosition;
41  import org.opentrafficsim.road.network.lane.Lane;
42  
43  import nl.tudelft.simulation.dsol.SimRuntimeException;
44  import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEvent;
45  import nl.tudelft.simulation.language.Throw;
46  import nl.tudelft.simulation.language.d3.BoundingBox;
47  import nl.tudelft.simulation.language.d3.DirectedPoint;
48  
49  /**
50   * This class contains most of the code that is needed to run a lane based GTU. <br>
51   * The starting point of a LaneBasedTU is that it can be in <b>multiple lanes</b> at the same time. This can be due to a lane
52   * change (lateral), or due to crossing a link (front of the GTU is on another Lane than rear of the GTU). If a Lane is shorter
53   * than the length of the GTU (e.g. when we do node expansion on a crossing, this is very well possible), a GTU could occupy
54   * dozens of Lanes at the same time.
55   * <p>
56   * When calculating a headway, the GTU has to look in successive lanes. When Lanes (or underlying CrossSectionLinks) diverge,
57   * the headway algorithms have to look at multiple Lanes and return the minimum headway in each of the Lanes. When the Lanes (or
58   * underlying CrossSectionLinks) converge, "parallel" traffic is not taken into account in the headway calculation. Instead, gap
59   * acceptance algorithms or their equivalent should guide the merging behavior.
60   * <p>
61   * To decide its movement, an AbstractLaneBasedGTU applies its car following algorithm and lane change algorithm to set the
62   * acceleration and any lane change operation to perform. It then schedules the triggers that will add it to subsequent lanes
63   * and remove it from current lanes as needed during the time step that is has committed to. Finally, it re-schedules its next
64   * movement evaluation with the simulator.
65   * <p>
66   * Copyright (c) 2013-2016 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
67   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
68   * <p>
69   * @version $Revision: 1408 $, $LastChangedDate: 2015-09-24 15:17:25 +0200 (Thu, 24 Sep 2015) $, by $Author: pknoppers $,
70   *          initial version Oct 22, 2014 <br>
71   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
72   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
73   */
74  public abstract class AbstractLaneBasedGTU extends AbstractGTU implements LaneBasedGTU
75  {
76      /** */
77      private static final long serialVersionUID = 20140822L;
78  
79      /**
80       * Fractional longitudinal positions of the reference point of the GTU on one or more links at the start of the current
81       * operational plan. Because the reference point of the GTU might not be on all the links the GTU is registered on, the
82       * fractional longitudinal positions can be more than one, or less than zero.
83       */
84      private Map<Link, Double> fractionalLinkPositions = new LinkedHashMap<>();
85  
86      /**
87       * The lanes the GTU is registered on. Each lane has to have its link registered in the fractionalLinkPositions as well to
88       * keep consistency. Each link from the fractionalLinkPositions can have one or more Lanes on which the vehicle is
89       * registered. This is a list to improve reproducibility: The 'oldest' lanes on which the vehicle is registered are at the
90       * front of the list, the later ones more to the back.
91       */
92      private final Map<Lane, GTUDirectionality> lanesCurrentOperationalPlan = new HashMap<>();
93  
94      /** Pending triggers for each lane. */
95      private Map<Lane, List<SimEvent<OTSSimTimeDouble>>> pendingTriggers = new HashMap<>();
96  
97      /** The object to lock to make the GTU thread safe. */
98      private Object lock = new Object();
99  
100     /**
101      * Construct a Lane Based GTU.
102      * @param id the id of the GTU
103      * @param gtuType the type of GTU, e.g. TruckType, CarType, BusType
104      * @param simulator to initialize the move method and to get the current time
105      * @param network the network that the GTU is initially registered in
106      * @throws GTUException when initial values are not correct
107      */
108     @SuppressWarnings("checkstyle:parameternumber")
109     public AbstractLaneBasedGTU(final String id, final GTUType gtuType, final OTSDEVSSimulatorInterface simulator,
110             final OTSNetwork network) throws GTUException
111     {
112         super(id, gtuType, simulator, network);
113     }
114 
115     /**
116      * @param strategicalPlanner the strategical planner (e.g., route determination) to use
117      * @param initialLongitudinalPositions the initial positions of the car on one or more lanes with their directions
118      * @param initialSpeed the initial speed of the car on the lane
119      * @throws NetworkException when the GTU cannot be placed on the given lane
120      * @throws SimRuntimeException when the move method cannot be scheduled
121      * @throws GTUException when initial values are not correct
122      * @throws OTSGeometryException when the initial path is wrong
123      */
124     public final void init(final LaneBasedStrategicalPlanner strategicalPlanner,
125             final Set<DirectedLanePosition> initialLongitudinalPositions, final Speed initialSpeed)
126             throws NetworkException, SimRuntimeException, GTUException, OTSGeometryException
127     {
128         Throw.when(null == initialLongitudinalPositions, GTUException.class, "InitialLongitudinalPositions is null");
129         Throw.when(0 == initialLongitudinalPositions.size(), GTUException.class, "InitialLongitudinalPositions is empty set");
130 
131         DirectedPoint lastPoint = null;
132         for (DirectedLanePosition pos : initialLongitudinalPositions)
133         {
134             Throw.when(lastPoint != null && pos.getLocation().distance(lastPoint) > 1E-6, GTUException.class,
135                     "initial locations for GTU have distance > 1 mm");
136             lastPoint = pos.getLocation();
137         }
138         DirectedPoint initialLocation = lastPoint;
139 
140         // register the GTU on the lanes
141         for (DirectedLanePosition directedLanePosition : initialLongitudinalPositions)
142         {
143             Lane lane = directedLanePosition.getLane();
144             enterLane(lane, directedLanePosition.getPosition(), directedLanePosition.getGtuDirection());
145         }
146 
147         // initiate the actual move
148         super.init(strategicalPlanner, initialLocation, initialSpeed);
149 
150         Lane referenceLane = getReferencePosition().getLane();
151         fireTimedEvent(LaneBasedGTU.LANEBASED_INIT_EVENT,
152                 new Object[] { getId(), initialLocation, getLength(), getWidth(), getBaseColor(), referenceLane,
153                         position(referenceLane, getReference()), this.lanesCurrentOperationalPlan.get(referenceLane) },
154                 getSimulator().getSimulatorTime());
155     }
156 
157     /** {@inheritDoc} */
158     @Override
159     public final void enterLane(final Lane lane, final Length position, final GTUDirectionality gtuDirection)
160             throws GTUException
161     {
162         if (lane == null || gtuDirection == null || position == null)
163         {
164             throw new GTUException("enterLane - one of the arguments is null");
165         }
166         if (this.lanesCurrentOperationalPlan.containsKey(lane))
167         {
168             System.err.println(this + " is already registered on lane: " + lane + " at fractional position "
169                     + this.fractionalPosition(lane, RelativePosition.REFERENCE_POSITION) + " intended position is " + position
170                     + " length of lane is " + lane.getLength());
171             return;
172         }
173 
174         // if the GTU is already registered on a lane of the same link, do not change its fractional position, as
175         // this might lead to a "jump".
176         if (!this.fractionalLinkPositions.containsKey(lane.getParentLink()))
177         {
178             this.fractionalLinkPositions.put(lane.getParentLink(), lane.fraction(position));
179         }
180         this.lanesCurrentOperationalPlan.put(lane, gtuDirection);
181         lane.addGTU(this, position);
182     }
183 
184     /** {@inheritDoc} */
185     @Override
186     public final void leaveLane(final Lane lane) throws GTUException
187     {
188         leaveLane(lane, false);
189     }
190 
191     /**
192      * Leave a lane but do not complain about having no lanes left when beingDestroyed is true.
193      * @param lane the lane to leave
194      * @param beingDestroyed if true, no complaints about having no lanes left
195      * @throws GTUException in case leaveLane should not be called
196      */
197     public final void leaveLane(final Lane lane, final boolean beingDestroyed) throws GTUException
198     {
199         this.lanesCurrentOperationalPlan.remove(lane);
200         List<SimEvent<OTSSimTimeDouble>> pending = this.pendingTriggers.get(lane);
201         if (null != pending)
202         {
203             for (SimEvent<OTSSimTimeDouble> event : pending)
204             {
205                 if (event.getAbsoluteExecutionTime().get().ge(getSimulator().getSimulatorTime().get()))
206                 {
207                     boolean result = getSimulator().cancelEvent(event);
208                     if (!result && event.getAbsoluteExecutionTime().get().ne(getSimulator().getSimulatorTime().get()))
209                     {
210                         System.err.println("leaveLane, trying to remove event: NOTHING REMOVED");
211                     }
212                 }
213             }
214             this.pendingTriggers.remove(lane);
215         }
216         // check of there are any lanes for this link left. If not, remove the link.
217         boolean found = false;
218         for (Lane l : this.lanesCurrentOperationalPlan.keySet())
219         {
220             if (l.getParentLink().equals(lane.getParentLink()))
221             {
222                 found = true;
223             }
224         }
225         if (!found)
226         {
227             this.fractionalLinkPositions.remove(lane.getParentLink());
228         }
229         lane.removeGTU(this, !found);
230         if (this.lanesCurrentOperationalPlan.size() == 0 && !beingDestroyed)
231         {
232             System.err.println("leaveLane: lanes.size() = 0 for GTU " + getId());
233         }
234     }
235 
236     /** {@inheritDoc} */
237     @Override
238     public final void changeLaneInstantaneously(final LateralDirectionality laneChangeDirection) throws GTUException
239     {
240         // keep a copy of the lanes and directions (!)
241         Map<Lane, GTUDirectionality> lanesCopy = new HashMap<>(this.lanesCurrentOperationalPlan);
242         Set<Lane> lanesToBeRemoved = new HashSet<>(lanesCopy.keySet());
243         Map<Lane, Double> fractionalLanePositions = new HashMap<>();
244 
245         for (Lane lane : lanesCopy.keySet())
246         {
247             fractionalLanePositions.put(lane, fractionalPosition(lane, getReference()));
248             lanesToBeRemoved.add(lane);
249         }
250 
251         for (Lane lane : lanesCopy.keySet())
252         {
253             Lane adjacentLane = lane.accessibleAdjacentLanes(laneChangeDirection, getGTUType()).iterator().next();
254             enterLane(adjacentLane, adjacentLane.getLength().multiplyBy(fractionalLanePositions.get(lane)),
255                     lanesCopy.get(lane));
256             lanesToBeRemoved.remove(adjacentLane); // don't remove lanes we might end up
257         }
258 
259         // TODO shouldn't this loop over lanesToBeRemoved?
260         for (Lane lane : lanesCopy.keySet())
261         {
262             leaveLane(lane);
263         }
264 
265         System.out.println(this + " changes lane from " + lanesCopy.keySet() + " to "
266                 + this.lanesCurrentOperationalPlan.keySet() + " at time " + getSimulator().getSimulatorTime().get());
267     }
268 
269     /**
270      * Register on lanes in target lane.
271      * @param laneChangeDirection direction of lane change
272      * @throws GTUException exception
273      */
274     // TODO this is a simple fix, should be based on path
275     public final void initLaneChange(final LateralDirectionality laneChangeDirection) throws GTUException
276     {
277         Map<Lane, GTUDirectionality> lanesCopy = new HashMap<>(this.lanesCurrentOperationalPlan);
278         Map<Lane, Double> fractionalLanePositions = new HashMap<>();
279         for (Lane lane : lanesCopy.keySet())
280         {
281             fractionalLanePositions.put(lane, fractionalPosition(lane, getReference()));
282         }
283         for (Lane lane : lanesCopy.keySet())
284         {
285             Lane adjacentLane = lane.accessibleAdjacentLanes(laneChangeDirection, getGTUType()).iterator().next();
286             System.out.println("GTU " + getId() + " registered on lane " + adjacentLane);
287             enterLane(adjacentLane, adjacentLane.getLength().multiplyBy(fractionalLanePositions.get(lane)),
288                     lanesCopy.get(lane));
289         }
290     }
291 
292     /**
293      * Unregister on lanes in source lane.
294      * @param laneChangeDirection direction of lane change
295      * @throws GTUException exception
296      */
297     // TODO this is a simple fix, should be based on path
298     public final void finalizeLaneChange(final LateralDirectionality laneChangeDirection) throws GTUException
299     {
300         Map<Lane, GTUDirectionality> lanesCopy = new HashMap<>(this.lanesCurrentOperationalPlan);
301         Set<Lane> lanesToBeRemoved = new HashSet<>(lanesCopy.keySet());
302         Map<Lane, Double> fractionalLanePositions = new HashMap<>();
303         for (Lane lane : lanesCopy.keySet())
304         {
305             fractionalLanePositions.put(lane, fractionalPosition(lane, getReference()));
306         }
307         for (Lane lane : lanesCopy.keySet())
308         {
309             Lane adjacentLane = lane.accessibleAdjacentLanes(laneChangeDirection, getGTUType()).iterator().next();
310             if (lanesCopy.keySet().contains(adjacentLane))
311             {
312                 lanesToBeRemoved.add(lane);
313             }
314         }
315         for (Lane lane : lanesToBeRemoved)
316         {
317             System.out.println("GTU " + getId() + " unregistered on lane " + lane);
318             leaveLane(lane);
319         }
320     }
321 
322     /** {@inheritDoc} */
323     @Override
324     public final GTUDirectionality getDirection(final Lane lane) throws GTUException
325     {
326         Throw.when(!this.lanesCurrentOperationalPlan.containsKey(lane), GTUException.class,
327                 "getDirection: Lanes %s does not contain %s", this.lanesCurrentOperationalPlan.keySet(), lane);
328         return this.lanesCurrentOperationalPlan.get(lane);
329     }
330 
331     /** {@inheritDoc} */
332     @Override
333     protected final void move(final DirectedPoint fromLocation)
334             throws SimRuntimeException, GTUException, OperationalPlanException, NetworkException, ParameterException
335     {
336         // Only carry out move() if we still have lane(s) to drive on.
337         // Note: a (Sink) trigger can have 'destroyed' us between the previous evaluation step and this one.
338         if (this.lanesCurrentOperationalPlan.isEmpty())
339         {
340             destroy();
341             return; // Done; do not re-schedule execution of this move method.
342         }
343 
344         // store the new positions, and sample statistics
345         Map<Link, Double> newLinkPositions = new HashMap<>();
346         for (Lane lane : this.lanesCurrentOperationalPlan.keySet())
347         {
348             newLinkPositions.put(lane.getParentLink(), lane.fraction(position(lane, getReference())));
349         }
350 
351         // generate the next operational plan and carry it out
352         super.move(fromLocation);
353 
354         // update the positions on the lanes we are registered on
355         this.fractionalLinkPositions = newLinkPositions;
356 
357         DirectedLanePosition dlp = getReferencePosition();
358         fireTimedEvent(
359                 LaneBasedGTU.LANEBASED_MOVE_EVENT, new Object[] { getId(), fromLocation, getSpeed(), getAcceleration(),
360                         getTurnIndicatorStatus(), getOdometer(), dlp.getLane(), dlp.getPosition(), dlp.getGtuDirection() },
361                 getSimulator().getSimulatorTime());
362 
363         if (getOperationalPlan().getAcceleration(Duration.ZERO).si < -10)
364         {
365             System.err.println("(getOperationalPlan().getAcceleration(Duration.ZERO).si < -10)");
366         }
367 
368         // schedule triggers and determine when to enter lanes with front and leave lanes with rear
369         scheduleEnterLeaveTriggers();
370     }
371 
372     /** {@inheritDoc} */
373     @Override
374     public final Map<Lane, Length> positions(final RelativePosition relativePosition) throws GTUException
375     {
376         return positions(relativePosition, getSimulator().getSimulatorTime().getTime());
377     }
378 
379     /** {@inheritDoc} */
380     @Override
381     public final Map<Lane, Length> positions(final RelativePosition relativePosition, final Time when) throws GTUException
382     {
383         Map<Lane, Length> positions = new LinkedHashMap<>();
384         for (Lane lane : this.lanesCurrentOperationalPlan.keySet())
385         {
386             positions.put(lane, position(lane, relativePosition, when));
387         }
388         return positions;
389     }
390 
391     /** {@inheritDoc} */
392     @Override
393     public final Length position(final Lane lane, final RelativePosition relativePosition) throws GTUException
394     {
395         return position(lane, relativePosition, getSimulator().getSimulatorTime().getTime());
396     }
397 
398     /** {@inheritDoc} */
399     public final Length projectedPosition(final Lane projectionLane, final RelativePosition relativePosition, final Time when)
400             throws GTUException
401     {
402         // do not make a wedge in a curve of the projected position!
403         CrossSectionLink link = projectionLane.getParentLink();
404         for (CrossSectionElement cse : link.getCrossSectionElementList())
405         {
406             if (cse instanceof Lane)
407             {
408                 Lane cseLane = (Lane) cse;
409                 if (null != this.lanesCurrentOperationalPlan.get(cseLane))
410                 {
411                     double fractionalPosition = fractionalPosition(cseLane, RelativePosition.REFERENCE_POSITION, when);
412                     Length pos = new Length(projectionLane.getLength().getSI() * fractionalPosition, LengthUnit.SI);
413                     if (this.lanesCurrentOperationalPlan.get(cseLane).isPlus())
414                     {
415                         return pos.plus(relativePosition.getDx());
416                     }
417                     return pos.minus(relativePosition.getDx());
418                 }
419             }
420         }
421         throw new GTUException(this + " is not on any lane of Link " + link);
422     }
423 
424     /** {@inheritDoc} */
425     @Override
426     public final Length position(final Lane lane, final RelativePosition relativePosition, final Time when) throws GTUException
427     {
428         if (null == lane)
429         {
430             throw new GTUException("lane is null");
431         }
432         synchronized (this.lock)
433         {
434             if (!this.lanesCurrentOperationalPlan.containsKey(lane))
435             {
436                 throw new GTUException("position() : GTU " + toString() + " is not on lane " + lane);
437             }
438             if (!this.fractionalLinkPositions.containsKey(lane.getParentLink()))
439             {
440                 // DO NOT USE toString() here, as it will cause an endless loop...
441                 throw new GTUException(this + " does not have a fractional position on " + lane.toString());
442             }
443             double longitudinalPosition = lane.positionSI(this.fractionalLinkPositions.get(lane.getParentLink()));
444             if (getOperationalPlan() == null)
445             {
446                 // no valid operational plan, e.g. during generation of a new plan
447                 return new Length(longitudinalPosition + relativePosition.getDx().si, LengthUnit.SI);
448             }
449             double loc;
450             try
451             {
452                 if (this.lanesCurrentOperationalPlan.get(lane).isPlus())
453                 {
454                     loc = longitudinalPosition + getOperationalPlan().getTraveledDistanceSI(when) + relativePosition.getDx().si;
455                 }
456                 else
457                 {
458                     loc = longitudinalPosition - getOperationalPlan().getTraveledDistanceSI(when) - relativePosition.getDx().si;
459                 }
460             }
461             catch (Exception e)
462             {
463                 System.err.println(toString());
464                 System.err.println(this.lanesCurrentOperationalPlan);
465                 System.err.println(this.fractionalLinkPositions);
466                 throw new GTUException(e);
467             }
468             if (Double.isNaN(loc))
469             {
470                 System.out.println("loc is NaN");
471             }
472             return new Length(loc, LengthUnit.SI);
473         }
474     }
475 
476     /** {@inheritDoc} */
477     @Override
478     public final DirectedLanePosition getReferencePosition() throws GTUException
479     {
480         for (Lane lane : this.lanesCurrentOperationalPlan.keySet())
481         {
482             double fraction = fractionalPosition(lane, getReference());
483             if (fraction >= 0.0 && fraction <= 1.0)
484             {
485                 // TODO widest lane in case we are registered on more than one lane with the reference point
486                 // TODO lane that leads to our location or not if we are registered on parallel lanes?
487                 return new DirectedLanePosition(lane, position(lane, getReference()), this.getDirection(lane));
488             }
489         }
490         throw new GTUException("The reference point of GTU " + this + " is not on any of the lanes on which it is registered");
491     }
492 
493     /**
494      * Schedule the triggers for this GTU that are going to happen until the next evaluation time. Also schedule the
495      * registration and deregistration of lanes when the vehicle enters or leaves them, at the exact right time. <br>
496      * Note: when the GTU makes a lane change, the vehicle will be registered for both lanes during the entire maneuver.
497      * @throws NetworkException on network inconsistency
498      * @throws SimRuntimeException should never happen
499      * @throws GTUException when a branch is reached where the GTU does not know where to go next
500      */
501     private void scheduleEnterLeaveTriggers() throws NetworkException, SimRuntimeException, GTUException
502     {
503         /*
504          * Move the vehicle into any new lanes with the front, and schedule entrance during this time step and calculate the
505          * current position based on the fractional position, because THE POSITION METHOD DOES NOT WORK FOR THIS. IT CALCULATES
506          * THE POSITION BASED ON THE NEWLY CALCULATED ACCELERATION AND SPEED AND CAN THEREFORE MAKE AN ERROR.
507          */
508         double moveSI = getOperationalPlan().getTotalLength().si;
509         Map<Lane, GTUDirectionality> lanesCopy = new LinkedHashMap<>(this.lanesCurrentOperationalPlan);
510         for (Lane lane : lanesCopy.keySet()) // use a copy because this.lanes can change
511         {
512             // schedule triggers on this lane
513             double referenceStartSI = this.fractionalLinkPositions.get(lane.getParentLink()) * lane.getLength().getSI();
514             double sign = lanesCopy.get(lane).equals(GTUDirectionality.DIR_PLUS) ? 1.0 : -1.0;
515             if (lanesCopy.get(lane).equals(GTUDirectionality.DIR_PLUS))
516             {
517                 lane.scheduleSensorTriggers(this, referenceStartSI, moveSI);
518             }
519             else
520             {
521                 // TODO extra argument for DIR_MINUS driving direction?
522                 lane.scheduleSensorTriggers(this, referenceStartSI - moveSI, moveSI);
523             }
524 
525             // determine when our FRONT will pass the end of this registered lane in case of driving DIR_PLUS or
526             // the start of this registered lane when driving DIR_MINUS.
527             // if the time is earlier than the end of the timestep: schedule the enterLane method.
528             // TODO look if more lanes are entered in one timestep, and continue the algorithm with the remainder of the time...
529             double frontPosSI = referenceStartSI + sign * getFront().getDx().getSI();
530             double nextFrontPosSI = frontPosSI + sign * moveSI;
531 
532             // LANE WE COME FROM IS IN PLUS DIRECTION
533             if (lanesCopy.get(lane).equals(GTUDirectionality.DIR_PLUS))
534             {
535                 if (frontPosSI <= lane.getLength().si && nextFrontPosSI > lane.getLength().si)
536                 {
537                     Lane nextLane = determineNextLane(lane);
538                     GTUDirectionality direction = lane.nextLanes(getGTUType()).get(nextLane);
539                     /*
540                      * We have to register the position at the previous timestep to keep calculations consistent. And we have to
541                      * correct for the position of the reference point. The idea is that we register the vehicle 'before' the
542                      * entrance of the new lane at the time of the last timestep, so for a DIR_PLUS on a negative position, and
543                      * for a DIR_MINUS on a position beyond the length of the next lane.
544                      */
545                     if (direction.equals(GTUDirectionality.DIR_PLUS))
546                     {
547                         Length refPosAtLastTimestep =
548                                 new Length(-(lane.getLength().si - frontPosSI) - getFront().getDx().si, LengthUnit.SI);
549                         enterLane(nextLane, refPosAtLastTimestep, direction);
550                         // schedule any sensor triggers on this lane for the remainder time
551                         nextLane.scheduleSensorTriggers(this, refPosAtLastTimestep.getSI(), moveSI);
552                     }
553                     else if (direction.equals(GTUDirectionality.DIR_MINUS))
554                     {
555                         Length refPosAtLastTimestep =
556                                 new Length(nextLane.getLength().si + (lane.getLength().si - frontPosSI) + getFront().getDx().si,
557                                         LengthUnit.SI);
558                         enterLane(nextLane, refPosAtLastTimestep, direction);
559                         // schedule any sensor triggers on this lane for the remainder time
560                         // TODO extra argument for DIR_MINUS driving direction?
561                         nextLane.scheduleSensorTriggers(this, refPosAtLastTimestep.getSI() - moveSI, moveSI);
562                     }
563                     else
564                     {
565                         throw new NetworkException("scheduleTriggers DIR_PLUS for GTU " + toString() + ", nextLane " + nextLane
566                                 + ", direction not DIR_PLUS or DIR_MINUS");
567                     }
568                 }
569             }
570 
571             // LANE WE COME FROM IS IN MINUS DIRECTION
572             else if (lanesCopy.get(lane).equals(GTUDirectionality.DIR_MINUS))
573             {
574                 if (frontPosSI >= 0.0 && nextFrontPosSI < 0.0)
575                 {
576                     Lane prevLane = determinePrevLane(lane);
577                     GTUDirectionality direction = lane.prevLanes(getGTUType()).get(prevLane);
578                     /*
579                      * We have to register the position at the previous timestep to keep calculations consistent. And we have to
580                      * correct for the position of the reference point. The idea is that we register the vehicle 'before' the
581                      * entrance of the new lane at the time of the last timestep, so for a DIR_MINUS on a negative position, and
582                      * for a DIR_PLUS on a position beyond the length of the next lane.
583                      */
584                     if (direction.equals(GTUDirectionality.DIR_MINUS))
585                     {
586                         // TODO do this at the right time, scheduled with triggers. 4x
587                         Length refPosAtLastTimestep =
588                                 new Length(prevLane.getLength().si + frontPosSI + getFront().getDx().si, LengthUnit.SI);
589                         enterLane(prevLane, refPosAtLastTimestep, direction);
590                         // schedule any sensor triggers on this lane for the remainder time
591                         prevLane.scheduleSensorTriggers(this, refPosAtLastTimestep.getSI() - moveSI, moveSI);
592                     }
593                     else if (direction.equals(GTUDirectionality.DIR_PLUS))
594                     {
595                         Length refPosAtLastTimestep = new Length(-frontPosSI - getFront().getDx().si, LengthUnit.SI);
596                         enterLane(prevLane, refPosAtLastTimestep, direction);
597                         // schedule any sensor triggers on this lane for the remainder time
598                         // TODO extra argument for DIR_MINUS driving direction?
599                         prevLane.scheduleSensorTriggers(this, refPosAtLastTimestep.getSI(), moveSI);
600                     }
601                     else
602                     {
603                         throw new NetworkException("scheduleTriggers DIR_MINUS for GTU " + toString() + ", prevLane " + prevLane
604                                 + ", direction not DIR_PLUS or DIR_MINUS");
605                     }
606                 }
607             }
608 
609             else
610             {
611                 throw new NetworkException(
612                         "scheduleTriggers for GTU " + toString() + ", lane " + lane + ", direction not DIR_PLUS or DIR_MINUS");
613             }
614         }
615 
616         // move the vehicle out of any lanes with the BACK, and schedule exit during this time step
617         for (Lane lane : this.lanesCurrentOperationalPlan.keySet())
618         {
619             // Determine when our REAR will pass the end of this registered lane.
620             // TODO look if more lanes are exited in one timestep, and continue the algorithm with the remainder of the time...
621             double referenceStartSI = this.fractionalLinkPositions.get(lane.getParentLink()) * lane.getLength().getSI();
622             double sign = this.lanesCurrentOperationalPlan.get(lane).equals(GTUDirectionality.DIR_PLUS) ? 1.0 : -1.0;
623             double rearPosSI = referenceStartSI + sign * getRear().getDx().getSI();
624 
625             if (this.lanesCurrentOperationalPlan.get(lane).equals(GTUDirectionality.DIR_PLUS))
626             {
627                 if (rearPosSI <= lane.getLength().si && rearPosSI + moveSI > lane.getLength().si)
628                 {
629                     double distanceToLeave = lane.getLength().si - rearPosSI;
630                     Time exitTime = getOperationalPlan().timeAtDistance(new Length(distanceToLeave, LengthUnit.SI));
631                     SimEvent<OTSSimTimeDouble> event = new SimEvent<>(new OTSSimTimeDouble(exitTime), this, this, "leaveLane",
632                             new Object[] { lane, new Boolean(false) });
633                     getSimulator().scheduleEvent(event);
634                     addTrigger(lane, event);
635                     // XXXXXXXXXXXXXXXXXX Minus one ULP is not safe if you want to add the current time
636                     // XXXXXXXXXXXXXXXXXX Should compute the time time at which the rear of the GTU exits the lane???
637                     // getSimulator().scheduleEventRel(new Duration(timestep - Math.ulp(timestep), TimeUnit.SI), this, this,
638                     // "leaveLane", new Object[] { lane, new Boolean(true) }); // TODO should be false?
639                 }
640             }
641             else
642             {
643                 if (rearPosSI >= 0.0 && rearPosSI - moveSI < 0.0)
644                 {
645                     double distanceToLeave = rearPosSI;
646                     Time exitTime = getOperationalPlan().timeAtDistance(new Length(distanceToLeave, LengthUnit.SI));
647                     SimEvent<OTSSimTimeDouble> event = new SimEvent<>(new OTSSimTimeDouble(exitTime), this, this, "leaveLane",
648                             new Object[] { lane, new Boolean(false) });
649                     getSimulator().scheduleEvent(event);
650                     addTrigger(lane, event);
651 
652                     // getSimulator().scheduleEventRel(new Duration(timestep - Math.ulp(timestep), TimeUnit.SI), this, this,
653                     // "leaveLane", new Object[] { lane, new Boolean(true) }); // XXX: should be false?
654                 }
655             }
656         }
657     }
658 
659     /**
660      * XXX: direction dependent... <br>
661      * @param lane the lane to find the successor for
662      * @return the next lane for this GTU
663      * @throws NetworkException when no next lane exists or the route branches into multiple next lanes
664      * @throws GTUException when no route could be found or the routeNavigator returns null
665      */
666     private Lane determineNextLane(final Lane lane) throws NetworkException, GTUException
667     {
668         Lane nextLane = null;
669         if (lane.nextLanes(getGTUType()).size() == 0)
670         {
671             throw new NetworkException(this + " - lane " + lane + " does not have a successor");
672         }
673         if (lane.nextLanes(getGTUType()).size() == 1)
674         {
675             nextLane = lane.nextLanes(getGTUType()).keySet().iterator().next();
676         }
677         else
678         {
679             if (!(getStrategicalPlanner() instanceof LaneBasedStrategicalRoutePlanner))
680             {
681                 throw new GTUException(this + " reaches branch but has no route navigator");
682             }
683             Node nextNode = ((LaneBasedStrategicalRoutePlanner) getStrategicalPlanner()).nextNode(lane.getParentLink(),
684                     GTUDirectionality.DIR_PLUS, getGTUType());
685             if (null == nextNode)
686             {
687                 throw new GTUException(this + " reaches branch and the route returns null as nextNodeToVisit");
688             }
689             int continuingLaneCount = 0;
690             for (Lane candidateLane : lane.nextLanes(getGTUType()).keySet())
691             {
692                 if (null != this.lanesCurrentOperationalPlan.get(candidateLane))
693                 {
694                     continue; // Already on this lane
695                 }
696                 // XXX Hack - this should be done more considerate -- fails at loops...
697                 if (nextNode == candidateLane.getParentLink().getEndNode()
698                         || nextNode == candidateLane.getParentLink().getStartNode())
699                 {
700                     nextLane = candidateLane;
701                     continuingLaneCount++;
702                 }
703             }
704             if (continuingLaneCount == 0)
705             {
706                 throw new NetworkException(this + " reached branch and the route specifies a nextNodeToVisit (" + nextNode
707                         + ") that is not a next node " + "at this branch at (" + lane.getParentLink().getEndNode() + ")");
708             }
709             if (continuingLaneCount > 1)
710             {
711                 throw new NetworkException(
712                         this + " reached branch and the route specifies multiple lanes to continue on at this branch ("
713                                 + lane.getParentLink().getEndNode() + "). This is not yet supported");
714             }
715         }
716         return nextLane;
717     }
718 
719     /**
720      * XXX: direction dependent... <br>
721      * @param lane the lane to find the predecessor for
722      * @return the next lane for this GTU
723      * @throws NetworkException when no next lane exists or the route branches into multiple next lanes
724      * @throws GTUException when no route could be found or the routeNavigator returns null
725      */
726     private Lane determinePrevLane(final Lane lane) throws NetworkException, GTUException
727     {
728         Lane prevLane = null;
729         if (lane.prevLanes(getGTUType()).size() == 0)
730         {
731             throw new NetworkException(this + " - lane " + lane + " does not have a predecessor");
732         }
733         if (lane.prevLanes(getGTUType()).size() == 1)
734         {
735             prevLane = lane.prevLanes(getGTUType()).keySet().iterator().next();
736         }
737         else
738         {
739             if (!(getStrategicalPlanner() instanceof LaneBasedStrategicalRoutePlanner))
740             {
741                 throw new GTUException(this + " reaches branch but has no route navigator");
742             }
743             Node prevNode = ((LaneBasedStrategicalRoutePlanner) getStrategicalPlanner()).nextNode(lane.getParentLink(),
744                     GTUDirectionality.DIR_MINUS, getGTUType());
745             if (null == prevNode)
746             {
747                 throw new GTUException(this + " reaches branch and the route returns null as nextNodeToVisit");
748             }
749             int continuingLaneCount = 0;
750             for (Lane candidateLane : lane.prevLanes(getGTUType()).keySet())
751             {
752                 if (null != this.lanesCurrentOperationalPlan.get(candidateLane))
753                 {
754                     continue; // Already on this lane
755                 }
756                 // XXX Hack - this should be done more considerate -- fails at loops...
757                 if (prevNode == candidateLane.getParentLink().getEndNode()
758                         || prevNode == candidateLane.getParentLink().getStartNode())
759                 {
760                     prevLane = candidateLane;
761                     continuingLaneCount++;
762                 }
763             }
764             if (continuingLaneCount == 0)
765             {
766                 throw new NetworkException(this + " reached branch and the route specifies a nextNodeToVisit (" + prevNode
767                         + ") that is not a next node " + "at this branch at (" + lane.getParentLink().getStartNode() + ")");
768             }
769             if (continuingLaneCount > 1)
770             {
771                 throw new NetworkException(
772                         this + " reached branch and the route specifies multiple lanes to continue on at this branch ("
773                                 + lane.getParentLink().getStartNode() + "). This is not yet supported");
774             }
775         }
776         return prevLane;
777     }
778 
779     /** {@inheritDoc} */
780     @Override
781     public final Map<Lane, Double> fractionalPositions(final RelativePosition relativePosition) throws GTUException
782     {
783         return fractionalPositions(relativePosition, getSimulator().getSimulatorTime().getTime());
784     }
785 
786     /** {@inheritDoc} */
787     @Override
788     public final Map<Lane, Double> fractionalPositions(final RelativePosition relativePosition, final Time when)
789             throws GTUException
790     {
791         Map<Lane, Double> positions = new LinkedHashMap<>();
792         for (Lane lane : this.lanesCurrentOperationalPlan.keySet())
793         {
794             positions.put(lane, fractionalPosition(lane, relativePosition, when));
795         }
796         return positions;
797     }
798 
799     /** {@inheritDoc} */
800     @Override
801     public final double fractionalPosition(final Lane lane, final RelativePosition relativePosition, final Time when)
802             throws GTUException
803     {
804         return position(lane, relativePosition, when).getSI() / lane.getLength().getSI();
805     }
806 
807     /** {@inheritDoc} */
808     @Override
809     public final double fractionalPosition(final Lane lane, final RelativePosition relativePosition) throws GTUException
810     {
811         return position(lane, relativePosition).getSI() / lane.getLength().getSI();
812     }
813 
814     /** {@inheritDoc} */
815     @Override
816     public final LaneBasedStrategicalPlanner getStrategicalPlanner()
817     {
818         return (LaneBasedStrategicalPlanner) super.getStrategicalPlanner();
819     }
820 
821     /** {@inheritDoc} */
822     @Override
823     public final BehavioralCharacteristics getBehavioralCharacteristics()
824     {
825         return getStrategicalPlanner().getBehavioralCharacteristics();
826     }
827 
828     /** {@inheritDoc} */
829     @Override
830     public final LaneBasedTacticalPlanner getTacticalPlanner()
831     {
832         return (LaneBasedTacticalPlanner) super.getTacticalPlanner();
833     }
834 
835     /** {@inheritDoc} */
836     public final void addTrigger(final Lane lane, final SimEvent<OTSSimTimeDouble> event)
837     {
838         List<SimEvent<OTSSimTimeDouble>> list = this.pendingTriggers.get(lane);
839         if (null == list)
840         {
841             list = new ArrayList<>();
842         }
843         list.add(event);
844         this.pendingTriggers.put(lane, list);
845     }
846 
847     /** {@inheritDoc} */
848     @Override
849     @SuppressWarnings("checkstyle:designforextension")
850     public void destroy()
851     {
852         DirectedLanePosition dlp = null;
853         try
854         {
855             dlp = getReferencePosition();
856         }
857         catch (GTUException e)
858         {
859             // ignore. not important at destroy
860         }
861         DirectedPoint location = this.getOperationalPlan() == null ? new DirectedPoint(0.0, 0.0, 0.0) : getLocation();
862 
863         synchronized (this.lock)
864         {
865             Set<Lane> laneSet = new HashSet<>(this.lanesCurrentOperationalPlan.keySet()); // Operate on a copy of the key set
866             for (Lane lane : laneSet)
867             {
868                 try
869                 {
870                     leaveLane(lane, true);
871                 }
872                 catch (GTUException e)
873                 {
874                     // ignore. not important at destroy
875                 }
876             }
877         }
878 
879         if (dlp != null && dlp.getLane() != null)
880         {
881             Lane referenceLane = dlp.getLane();
882             fireTimedEvent(LaneBasedGTU.LANEBASED_DESTROY_EVENT,
883                     new Object[] { getId(), location, getOdometer(), referenceLane, dlp.getPosition(), dlp.getGtuDirection() },
884                     getSimulator().getSimulatorTime());
885         }
886         else
887         {
888             fireTimedEvent(LaneBasedGTU.LANEBASED_DESTROY_EVENT,
889                     new Object[] { getId(), location, getOdometer(), null, Length.ZERO, null },
890                     getSimulator().getSimulatorTime());
891         }
892 
893         super.destroy();
894     }
895 
896     /** {@inheritDoc} */
897     @Override
898     public final Bounds getBounds()
899     {
900         double dx = 0.5 * getLength().doubleValue();
901         double dy = 0.5 * getWidth().doubleValue();
902         return new BoundingBox(new Point3d(-dx, -dy, 0.0), new Point3d(dx, dy, 0.0));
903     }
904 
905     /** {@inheritDoc} */
906     @SuppressWarnings("checkstyle:designforextension")
907     public String toString()
908     {
909         return String.format("GTU " + getId());
910     }
911 }