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