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 nl.tudelft.simulation.dsol.SimRuntimeException;
18  import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEvent;
19  import nl.tudelft.simulation.language.d3.BoundingBox;
20  import nl.tudelft.simulation.language.d3.DirectedPoint;
21  
22  import org.djunits.unit.LengthUnit;
23  import org.djunits.value.vdouble.scalar.Length;
24  import org.djunits.value.vdouble.scalar.Speed;
25  import org.djunits.value.vdouble.scalar.Time;
26  import org.opentrafficsim.core.dsol.OTSDEVSSimulatorInterface;
27  import org.opentrafficsim.core.dsol.OTSSimTimeDouble;
28  import org.opentrafficsim.core.geometry.OTSGeometryException;
29  import org.opentrafficsim.core.geometry.OTSShape;
30  import org.opentrafficsim.core.gtu.AbstractGTU;
31  import org.opentrafficsim.core.gtu.GTUDirectionality;
32  import org.opentrafficsim.core.gtu.GTUException;
33  import org.opentrafficsim.core.gtu.GTUType;
34  import org.opentrafficsim.core.gtu.RelativePosition;
35  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlan;
36  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlanException;
37  import org.opentrafficsim.core.network.Link;
38  import org.opentrafficsim.core.network.NetworkException;
39  import org.opentrafficsim.core.network.Node;
40  import org.opentrafficsim.core.network.OTSNetwork;
41  import org.opentrafficsim.road.gtu.lane.driver.LaneBasedBehavioralCharacteristics;
42  import org.opentrafficsim.road.gtu.lane.perception.LanePerception;
43  import org.opentrafficsim.road.gtu.lane.perception.LanePerceptionFull;
44  import org.opentrafficsim.road.gtu.strategical.LaneBasedStrategicalPlanner;
45  import org.opentrafficsim.road.gtu.strategical.route.LaneBasedStrategicalRoutePlanner;
46  import org.opentrafficsim.road.network.lane.CrossSectionElement;
47  import org.opentrafficsim.road.network.lane.CrossSectionLink;
48  import org.opentrafficsim.road.network.lane.DirectedLanePosition;
49  import org.opentrafficsim.road.network.lane.Lane;
50  
51  /**
52   * This class contains most of the code that is needed to run a lane based GTU. <br>
53   * 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
54   * 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
55   * 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
56   * dozens of Lanes at the same time.
57   * <p>
58   * When calculating a headway, the GTU has to look in successive lanes. When Lanes (or underlying CrossSectionLinks) diverge,
59   * the headway algorithms have to look at multiple Lanes and return the minimum headway in each of the Lanes. When the Lanes (or
60   * underlying CrossSectionLinks) converge, "parallel" traffic is not taken into account in the headway calculation. Instead, gap
61   * acceptance algorithms or their equivalent should guide the merging behavior.
62   * <p>
63   * To decide its movement, an AbstractLaneBasedGTU applies its car following algorithm and lane change algorithm to set the
64   * acceleration and any lane change operation to perform. It then schedules the triggers that will add it to subsequent lanes
65   * and remove it from current lanes as needed during the time step that is has committed to. Finally, it re-schedules its next
66   * movement evaluation with the simulator.
67   * <p>
68   * Copyright (c) 2013-2015 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
69   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
70   * <p>
71   * @version $Revision: 1408 $, $LastChangedDate: 2015-09-24 15:17:25 +0200 (Thu, 24 Sep 2015) $, by $Author: pknoppers $,
72   *          initial version Oct 22, 2014 <br>
73   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
74   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
75   */
76  public abstract class AbstractLaneBasedGTU extends AbstractGTU implements LaneBasedGTU
77  {
78      /** */
79      private static final long serialVersionUID = 20140822L;
80  
81      /**
82       * Fractional longitudinal positions of the reference point of the GTU on one or more links at the lastEvaluationTime.
83       * Because the reference point of the GTU might not be on all the links the GTU is registered on, the fractional
84       * longitudinal positions can be more than one, or less than zero.
85       */
86      private Map<Link, Double> fractionalLinkPositions = new LinkedHashMap<>();
87  
88      /**
89       * The lanes the GTU is registered on. Each lane has to have its link registered in the fractionalLinkPositions as well to
90       * keep consistency. Each link from the fractionalLinkPositions can have one or more Lanes on which the vehicle is
91       * registered. This is a list to improve reproducibility: The 'oldest' lanes on which the vehicle is registered are at the
92       * front of the list, the later ones more to the back.
93       */
94      private final Map<Lane, GTUDirectionality> lanes = new HashMap<>();
95  
96      /** Pending triggers for each lane. */
97      private Map<Lane, List<SimEvent<OTSSimTimeDouble>>> pendingTriggers =
98          new HashMap<Lane, List<SimEvent<OTSSimTimeDouble>>>();
99  
100     /** The object to lock to make the GTU thread safe. */
101     private Object lock = new Object();
102 
103     /** The mode of movement: lane-based or path-based. */
104     private static final boolean MOVEMENT_LANE_BASED = true;
105 
106     /**
107      * Construct a Lane Based GTU.
108      * @param id the id of the GTU
109      * @param gtuType the type of GTU, e.g. TruckType, CarType, BusType
110      * @param initialLongitudinalPositions the initial positions of the car on one or more lanes with their directions
111      * @param initialSpeed the initial speed of the car on the lane
112      * @param simulator to initialize the move method and to get the current time
113      * @param strategicalPlanner the strategical planner (e.g., route determination) to use
114      * @param perception the lane-based perception model of the GTU
115      * @param network the network that the GTU is initially registered in
116      * @throws NetworkException when the GTU cannot be placed on the given lane
117      * @throws SimRuntimeException when the move method cannot be scheduled
118      * @throws GTUException when gtuFollowingModel is null
119      * @throws OTSGeometryException when the initial path is wrong
120      */
121     @SuppressWarnings("checkstyle:parameternumber")
122     public AbstractLaneBasedGTU(final String id, final GTUType gtuType,
123         final Set<DirectedLanePosition> initialLongitudinalPositions, final Speed initialSpeed,
124         final OTSDEVSSimulatorInterface simulator, final LaneBasedStrategicalPlanner strategicalPlanner,
125         final LanePerception perception, final OTSNetwork network) throws NetworkException, SimRuntimeException,
126         GTUException, OTSGeometryException
127     {
128         super(id, gtuType, simulator, strategicalPlanner, perception,
129             verifyInitialLocation(initialLongitudinalPositions), initialSpeed, network);
130 
131         // register the GTU in the perception module
132         getPerception().setGTU(this);
133 
134         // register the GTU on the lanes
135         for (DirectedLanePosition directedLanePosition : initialLongitudinalPositions)
136         {
137             Lane lane = directedLanePosition.getLane();
138             if (!this.fractionalLinkPositions.containsKey(lane.getParentLink()))
139             {
140                 // initially registered on parallel or overlapping lanes
141                 this.fractionalLinkPositions.put(lane.getParentLink(),
142                     lane.fraction(directedLanePosition.getPosition()));
143             }
144             this.lanes.put(lane, directedLanePosition.getGtuDirection());
145             lane.addGTU(this, lane.fraction(directedLanePosition.getPosition()));
146         }
147     }
148 
149     /**
150      * Throw a GTUException if the provided set of initial longitudinal positions is null or empty.
151      * @param initialLongitudinalPositions Set&lt;DirectedLanePosition&gt;; the set of initial longitudinal positions to check
152      * @return Set&lt;DirectedLanePosition&gt;; the argument of this method
153      * @throws GTUException when the provided set is null or empty
154      */
155     private static DirectedPoint verifyInitialLocation(Set<DirectedLanePosition> initialLongitudinalPositions)
156         throws GTUException
157     {
158         GTUException.failIf(null == initialLongitudinalPositions, "InitialLongitudinalPositions is null");
159         GTUException.failIf(0 == initialLongitudinalPositions.size(), "InitialLongitudinalPositions is empty set");
160         DirectedPoint lastPoint = null;
161         for (DirectedLanePosition pos : initialLongitudinalPositions)
162         {
163             GTUException.failIf(lastPoint != null && pos.getLocation().distance(lastPoint) > 1E-6,
164                 "initial locations for GTU have distance > 1 mm");
165             lastPoint = pos.getLocation();
166         }
167         return lastPoint;
168     }
169 
170     /** {@inheritDoc} */
171     @Override
172     public final void enterLane(final Lane lane, final Length.Rel position, final GTUDirectionality gtuDirection)
173         throws GTUException
174     {
175         GTUException.failIf(!MOVEMENT_LANE_BASED, "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 "
184                 + position + " 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         GTUException.failIf(!MOVEMENT_LANE_BASED, "MOVEMENT_LANE_BASED is true, but leaveLane() is called");
214         if (null == this.lanes.get(lane))
215         {
216             // No problem -- this method can be scheduled and the GTU can already have left the lane
217         }
218         this.lanes.remove(lane);
219         List<SimEvent<OTSSimTimeDouble>> pending = this.pendingTriggers.get(lane);
220         if (null != pending)
221         {
222             for (SimEvent<OTSSimTimeDouble> event : pending)
223             {
224                 if (event.getAbsoluteExecutionTime().get().ge(getSimulator().getSimulatorTime().get()))
225                 {
226                     // System.out.println(this + ": Cancelling trigger " + event);
227                     boolean result = getSimulator().cancelEvent(event);
228                     if (!result && event.getAbsoluteExecutionTime().get().ne(getSimulator().getSimulatorTime().get()))
229                     {
230                         System.err.println("NOTHING REMOVED");
231                     }
232                 }
233             }
234             // System.out.println(this + ": Removing list of triggers for lane " + lane);
235             this.pendingTriggers.remove(lane);
236         }
237         // check of there are any lanes for this link left. If not, remove the link.
238         boolean found = false;
239         for (Lane l : this.lanes.keySet())
240         {
241             if (l.getParentLink().equals(lane.getParentLink()))
242             {
243                 found = true;
244             }
245         }
246         if (!found)
247         {
248             this.fractionalLinkPositions.remove(lane.getParentLink());
249         }
250         lane.removeGTU(this);
251         if (this.lanes.size() == 0 && !beingDestroyed)
252         {
253             System.err.println("lanes.size() = 0 for GTU " + getId());
254         }
255     }
256 
257     /** {@inheritDoc} */
258     @Override
259     public final Map<Lane, GTUDirectionality> getLanes()
260     {
261         return new HashMap<Lane, GTUDirectionality>(this.lanes);
262     }
263 
264     /** {@inheritDoc} */
265     @Override
266     protected final void move(final DirectedPoint fromLocation) throws SimRuntimeException, GTUException,
267         OperationalPlanException, NetworkException
268     {
269         if (MOVEMENT_LANE_BASED)
270         {
271             // Only carry out move() if we still have lane(s) to drive on.
272             // Note: a (Sink) trigger can have 'destroyed' us between the previous evaluation step and this one.
273             if (this.lanes.isEmpty())
274             {
275                 destroy();
276                 return; // Done; do not re-schedule execution of this move method.
277             }
278 
279             for (Link link : this.fractionalLinkPositions.keySet())
280             {
281                 double d = this.fractionalLinkPositions.get(link);
282 
283                 double fractionalExcess = d < 0 ? -d : (d - 1);
284                 if (fractionalExcess > 0)
285                 {
286                     double excess = fractionalExcess * link.getLength().si;
287                     OperationalPlan op = this.getOperationalPlan();
288                     double maxLength = this.getLength().si + op.getTraveledDistanceSI(op.getTotalDuration());
289                     if (excess > maxLength)
290                     // if (d < -0.1 || d > 1.1)
291                     {
292                         System.err.println(this + " has extreme fractional position on Link " + link + ": " + d + " ("
293                             + excess + "m), time is " + this.getSimulator().getSimulatorTime().get());
294                     }
295                 }
296             }
297             // store the new positions, and sample statistics
298             Map<Link, Double> newLinkPositions = new HashMap<>();
299             for (Lane lane : this.lanes.keySet())
300             {
301                 lane.sample(this);
302                 newLinkPositions.put(lane.getParentLink(), lane.fraction(position(lane, getReference())));
303             }
304 
305             // generate the next operational plan and carry it out
306             super.move(fromLocation);
307             
308             if (getOperationalPlan().getAcceleration(Time.Rel.ZERO).si < -10)
309             {
310                 System.err.println("(getOperationalPlan().getAcceleration(Time.Rel.ZERO).si < -10)");
311             }
312 
313             // update the positions on the lanes we are registered on
314             this.fractionalLinkPositions = newLinkPositions;
315 
316             // schedule triggers and determine when to enter lanes with front and leave lanes with rear
317             scheduleTriggers();
318         }
319 
320         else
321         // MOVEMENT_PATH_BASED
322         {
323             try
324             {
325                 // 1. generate the next operational plan and carry it out
326                 super.move(fromLocation);
327 
328                 // 2. split the movement into a number of steps, so that each steps overlaps with the half GTU length
329                 int steps = Math.max(5, (int) (2.0 * getOperationalPlan().getPath().getLengthSI() / getLength().si));
330                 DirectedPoint[] points = new DirectedPoint[steps + 1];
331                 OTSShape[] rects = new OTSShape[steps + 1];
332                 for (int i = 0; i <= steps; i++)
333                 {
334                     points[i] = getOperationalPlan().getPath().getLocationFraction(1.0 * i / steps);
335                     double x = points[i].x;
336                     double y = points[i].y;
337                     double l = getLength().si;
338                     double w = getWidth().si;
339                     Rectangle2D rect = new Rectangle2D.Double(-l / 2.0, -w / 2.0, l, w);
340                     Path2D.Double path = new Path2D.Double(rect);
341                     AffineTransform t = new AffineTransform();
342                     t.translate(x, y);
343                     t.rotate(points[i].getRotZ());
344                     path.transform(t);
345                     rects[i] = new OTSShape(path);
346                 }
347 
348                 // 3. determine for each rectangle with which lanes there is an overlap
349                 List<Lane>[] laneLists = new ArrayList[steps + 1];
350 Set<Lane> laneSet = new HashSet<>();
351                 OTSNetwork network = (OTSNetwork) getPerceivableContext();
352                 for (int i = 0; i < rects.length; i++)
353                 {
354                     laneLists[i] = new ArrayList<Lane>();
355                     for (Link link : network.getLinkMap().values())
356                     {
357                         if (link instanceof CrossSectionLink)
358                         {
359                             for (Lane lane : ((CrossSectionLink) link).getLanes())
360                             {
361                                 if (lane.getContour().intersects(rects[i]))
362                                 {
363                                     laneLists[i].add(lane);
364 laneSet.add(lane);
365                                 }
366                             }
367                         }
368                     }
369                 }
370 System.out.println(this + " will be on lanes: " + laneSet);
371             }
372             catch (OTSGeometryException e)
373             {
374                 throw new GTUException(e);
375             }
376         }
377     }
378 
379     /** {@inheritDoc} */
380     @Override
381     public final Map<Lane, Length.Rel> positions(final RelativePosition relativePosition) throws GTUException
382     {
383         return positions(relativePosition, getSimulator().getSimulatorTime().getTime());
384     }
385 
386     /** {@inheritDoc} */
387     @Override
388     public final Map<Lane, Length.Rel> positions(final RelativePosition relativePosition, final Time.Abs when)
389         throws GTUException
390     {
391         Map<Lane, Length.Rel> positions = new LinkedHashMap<>();
392         for (Lane lane : this.lanes.keySet())
393         {
394             positions.put(lane, position(lane, relativePosition, when));
395         }
396         return positions;
397     }
398 
399     /** {@inheritDoc} */
400     @Override
401     public final Length.Rel position(final Lane lane, final RelativePosition relativePosition) throws GTUException
402     {
403         return position(lane, relativePosition, getSimulator().getSimulatorTime().getTime());
404     }
405 
406     /** {@inheritDoc} */
407     public final Length.Rel projectedPosition(final Lane projectionLane, final RelativePosition relativePosition,
408         final Time.Abs when) throws GTUException
409     {
410         // do not make a wedge in a curve of the projected position! 
411         CrossSectionLink link = projectionLane.getParentLink();
412         for (CrossSectionElement cse : link.getCrossSectionElementList())
413         {
414             if (cse instanceof Lane)
415             {
416                 Lane cseLane = (Lane) cse;
417                 if (null != this.lanes.get(cseLane))
418                 {
419                     double fractionalPosition = fractionalPosition(cseLane, RelativePosition.REFERENCE_POSITION, when);
420                     Length.Rel pos = new Length.Rel(projectionLane.getLength().getSI() * fractionalPosition, LengthUnit.SI);
421                     if (this.lanes.get(cseLane).isPlus())
422                     {
423                         return pos.plus(relativePosition.getDx());
424                     }
425                     return pos.minus(relativePosition.getDx());
426                 }
427             }
428         }
429         throw new GTUException(this + " is not on any lane of Link " + link);
430     }
431 
432     /** {@inheritDoc} */
433     @Override
434     public final Length.Rel position(final Lane lane, final RelativePosition relativePosition, final Time.Abs when)
435         throws GTUException
436     {
437         if (null == lane)
438         {
439             throw new GTUException("lane is null");
440         }
441         synchronized (this.lock)
442         {
443             if (!this.lanes.containsKey(lane))
444             {
445                 throw new GTUException("position() : GTU " + toString() + " is not on lane " + lane);
446             }
447             if (!this.fractionalLinkPositions.containsKey(lane.getParentLink()))
448             {
449                 // DO NOT USE toString() here, as it will cause an endless loop...
450                 throw new GTUException(this + " does not have a fractional position on " + lane.toString());
451             }
452             Length.Rel longitudinalPosition = lane.position(this.fractionalLinkPositions.get(lane.getParentLink()));
453             if (longitudinalPosition == null)
454             {
455                 // According to FindBugs; this cannot happen; PK is unsure whether FindBugs is correct.
456                 throw new GTUException("position(): GTU " + toString() + " no position for lane " + lane);
457             }
458             if (getOperationalPlan() == null)
459             {
460                 // no valid operational plan, e.g. during generation of a new plan
461                 return longitudinalPosition.plus(relativePosition.getDx());
462             }
463             Length.Rel loc;
464             try
465             {
466                 if (this.lanes.get(lane).equals(GTUDirectionality.DIR_PLUS))
467                 {
468                     loc =
469                         longitudinalPosition.plus(getOperationalPlan().getTraveledDistance(when)).plus(
470                             relativePosition.getDx());
471                 }
472                 else
473                 {
474                     loc =
475                         longitudinalPosition.minus(getOperationalPlan().getTraveledDistance(when)).minus(
476                             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 VELOCITY 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 = getVelocity().getSI() * timestep + 0.5 * getAcceleration().getSI() * timestep * timestep;
513         Map<Lane, GTUDirectionality> lanesCopy = new LinkedHashMap<Lane, GTUDirectionality>(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.Rel refPosAtLastTimestep =
552                             new Length.Rel(-(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.Rel refPosAtLastTimestep =
560                             new Length.Rel(nextLane.getLength().si + (lane.getLength().si - frontPosSI)
561                                 + getFront().getDx().si, 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 "
570                             + nextLane + ", 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.Rel refPosAtLastTimestep =
591                             new Length.Rel(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.Rel refPosAtLastTimestep =
599                             new Length.Rel(-frontPosSI - getFront().getDx().si, LengthUnit.SI);
600                         enterLane(prevLane, refPosAtLastTimestep, direction);
601                         // schedule any sensor triggers on this lane for the remainder time
602                         // TODO extra argument for DIR_MINUS driving direction?
603                         prevLane.scheduleTriggers(this, refPosAtLastTimestep.getSI(), moveSI);
604                     }
605                     else
606                     {
607                         throw new NetworkException("scheduleTriggers DIR_MINUS for GTU " + toString() + ", prevLane "
608                             + prevLane + ", direction not DIR_PLUS or DIR_MINUS");
609                     }
610                 }
611             }
612 
613             else
614             {
615                 throw new NetworkException("scheduleTriggers for GTU " + toString() + ", lane " + lane
616                     + ", direction not DIR_PLUS or DIR_MINUS");
617             }
618         }
619 
620         // move the vehicle out of any lanes with the BACK, and schedule exit during this time step
621         for (Lane lane : this.lanes.keySet())
622         {
623             // Determine when our REAR will pass the end of this registered lane.
624             // TODO look if more lanes are exited in one timestep, and continue the algorithm with the remainder of the time...
625             double referenceStartSI = this.fractionalLinkPositions.get(lane.getParentLink()) * lane.getLength().getSI();
626             double sign = this.lanes.get(lane).equals(GTUDirectionality.DIR_PLUS) ? 1.0 : -1.0;
627             double rearPosSI = referenceStartSI + sign * getRear().getDx().getSI();
628 
629             if (this.lanes.get(lane).equals(GTUDirectionality.DIR_PLUS))
630             {
631                 if (rearPosSI <= lane.getLength().si && rearPosSI + moveSI > lane.getLength().si)
632                 {
633                     double distanceToLeave = lane.getLength().si - rearPosSI;
634                     Time.Abs exitTime =
635                         getOperationalPlan().timeAtDistance(new Length.Rel(distanceToLeave, LengthUnit.SI));
636                     SimEvent<OTSSimTimeDouble> event =
637                         new SimEvent<OTSSimTimeDouble>(new OTSSimTimeDouble(exitTime), this, this, "leaveLane",
638                             new Object[]{lane, new Boolean(false)});
639                     getSimulator().scheduleEvent(event);
640                     addTrigger(lane, event);
641                     // XXXXXXXXXXXXXXXXXX Minus one ULP is not safe if you want to add the current time
642                     // XXXXXXXXXXXXXXXXXX Should compute the time time at which the rear of the GTU exits the lane???
643                     // getSimulator().scheduleEventRel(new Time.Rel(timestep - Math.ulp(timestep), TimeUnit.SI), this, this,
644                     // "leaveLane", new Object[] { lane, new Boolean(true) }); // TODO should be false?
645                 }
646             }
647             else
648             {
649                 if (rearPosSI >= 0.0 && rearPosSI - moveSI < 0.0)
650                 {
651                     double distanceToLeave = rearPosSI;
652                     Time.Abs exitTime =
653                         getOperationalPlan().timeAtDistance(new Length.Rel(distanceToLeave, LengthUnit.SI));
654                     SimEvent<OTSSimTimeDouble> event =
655                         new SimEvent<OTSSimTimeDouble>(new OTSSimTimeDouble(exitTime), this, this, "leaveLane",
656                             new Object[]{lane, new Boolean(false)});
657                     getSimulator().scheduleEvent(event);
658                     addTrigger(lane, event);
659 
660                     // getSimulator().scheduleEventRel(new Time.Rel(timestep - Math.ulp(timestep), TimeUnit.SI), this, this,
661                     // "leaveLane", new Object[] { lane, new Boolean(true) }); // XXX: should be false?
662                 }
663             }
664         }
665     }
666 
667     /**
668      * XXX: direction dependent... <br>
669      * @param lane the lane to find the successor for
670      * @return the next lane for this GTU
671      * @throws NetworkException when no next lane exists or the route branches into multiple next lanes
672      * @throws GTUException when no route could be found or the routeNavigator returns null
673      */
674     private Lane determineNextLane(final Lane lane) throws NetworkException, GTUException
675     {
676         Lane nextLane = null;
677         if (lane.nextLanes(getGTUType()).size() == 0)
678         {
679             throw new NetworkException(this + " - lane " + lane + " does not have a successor");
680         }
681         if (lane.nextLanes(getGTUType()).size() == 1)
682         {
683             nextLane = lane.nextLanes(getGTUType()).keySet().iterator().next();
684         }
685         else
686         {
687             if (!(getStrategicalPlanner() instanceof LaneBasedStrategicalRoutePlanner))
688             {
689                 throw new GTUException(this + " reaches branch but has no route navigator");
690             }
691             Node nextNode =
692                 ((LaneBasedStrategicalRoutePlanner) getStrategicalPlanner()).nextNode(lane.getParentLink(),
693                     GTUDirectionality.DIR_PLUS, getGTUType());
694             if (null == nextNode)
695             {
696                 throw new GTUException(this + " reaches branch and the route returns null as nextNodeToVisit");
697             }
698             int continuingLaneCount = 0;
699             for (Lane candidateLane : lane.nextLanes(getGTUType()).keySet())
700             {
701                 if (null != this.lanes.get(candidateLane))
702                 {
703                     continue; // Already on this lane
704                 }
705                 // XXX Hack - this should be done more considerate -- fails at loops...
706                 if (nextNode == candidateLane.getParentLink().getEndNode()
707                     || nextNode == candidateLane.getParentLink().getStartNode())
708                 {
709                     nextLane = candidateLane;
710                     continuingLaneCount++;
711                 }
712             }
713             if (continuingLaneCount == 0)
714             {
715                 throw new NetworkException(this + " reached branch and the route specifies a nextNodeToVisit ("
716                     + nextNode + ") that is not a next node " + "at this branch at ("
717                     + lane.getParentLink().getEndNode() + ")");
718             }
719             if (continuingLaneCount > 1)
720             {
721                 throw new NetworkException(this
722                     + " reached branch and the route specifies multiple lanes to continue on at this branch ("
723                     + lane.getParentLink().getEndNode() + "). This is not yet supported");
724             }
725         }
726         return nextLane;
727     }
728 
729     /**
730      * XXX: direction dependent... <br>
731      * @param lane the lane to find the predecessor for
732      * @return the next lane for this GTU
733      * @throws NetworkException when no next lane exists or the route branches into multiple next lanes
734      * @throws GTUException when no route could be found or the routeNavigator returns null
735      */
736     private Lane determinePrevLane(final Lane lane) throws NetworkException, GTUException
737     {
738         Lane prevLane = null;
739         if (lane.prevLanes(getGTUType()).size() == 0)
740         {
741             throw new NetworkException(this + " - lane " + lane + " does not have a predecessor");
742         }
743         if (lane.prevLanes(getGTUType()).size() == 1)
744         {
745             prevLane = lane.prevLanes(getGTUType()).keySet().iterator().next();
746         }
747         else
748         {
749             if (!(getStrategicalPlanner() instanceof LaneBasedStrategicalRoutePlanner))
750             {
751                 throw new GTUException(this + " reaches branch but has no route navigator");
752             }
753             Node prevNode =
754                 ((LaneBasedStrategicalRoutePlanner) getStrategicalPlanner()).nextNode(lane.getParentLink(),
755                     GTUDirectionality.DIR_MINUS, getGTUType());
756             if (null == prevNode)
757             {
758                 throw new GTUException(this + " reaches branch and the route returns null as nextNodeToVisit");
759             }
760             int continuingLaneCount = 0;
761             for (Lane candidateLane : lane.prevLanes(getGTUType()).keySet())
762             {
763                 if (null != this.lanes.get(candidateLane))
764                 {
765                     continue; // Already on this lane
766                 }
767                 // XXX Hack - this should be done more considerate -- fails at loops...
768                 if (prevNode == candidateLane.getParentLink().getEndNode()
769                     || prevNode == candidateLane.getParentLink().getStartNode())
770                 {
771                     prevLane = candidateLane;
772                     continuingLaneCount++;
773                 }
774             }
775             if (continuingLaneCount == 0)
776             {
777                 throw new NetworkException(this + " reached branch and the route specifies a nextNodeToVisit ("
778                     + prevNode + ") that is not a next node " + "at this branch at ("
779                     + lane.getParentLink().getStartNode() + ")");
780             }
781             if (continuingLaneCount > 1)
782             {
783                 throw new NetworkException(this
784                     + " reached branch and the route specifies multiple lanes to continue on at this branch ("
785                     + lane.getParentLink().getStartNode() + "). This is not yet supported");
786             }
787         }
788         return prevLane;
789     }
790 
791     /** {@inheritDoc} */
792     @Override
793     public final Map<Lane, Double> fractionalPositions(final RelativePosition relativePosition) throws GTUException
794     {
795         return fractionalPositions(relativePosition, getSimulator().getSimulatorTime().getTime());
796     }
797 
798     /** {@inheritDoc} */
799     @Override
800     public final Map<Lane, Double> fractionalPositions(final RelativePosition relativePosition, final Time.Abs when)
801         throws GTUException
802     {
803         Map<Lane, Double> positions = new LinkedHashMap<>();
804         for (Lane lane : this.lanes.keySet())
805         {
806             positions.put(lane, fractionalPosition(lane, relativePosition, when));
807         }
808         return positions;
809     }
810 
811     /** {@inheritDoc} */
812     @Override
813     public final double
814         fractionalPosition(final Lane lane, final RelativePosition relativePosition, final Time.Abs when)
815             throws GTUException
816     {
817         return position(lane, relativePosition, when).getSI() / lane.getLength().getSI();
818     }
819 
820     /** {@inheritDoc} */
821     @Override
822     public final double fractionalPosition(final Lane lane, final RelativePosition relativePosition)
823         throws GTUException
824     {
825         return position(lane, relativePosition).getSI() / lane.getLength().getSI();
826     }
827 
828     /** {@inheritDoc} */
829     @Override
830     public LanePerceptionFull getPerception()
831     {
832         return (LanePerceptionFull) super.getPerception();
833     }
834 
835     /** {@inheritDoc} */
836     @Override
837     public LaneBasedStrategicalPlanner getStrategicalPlanner()
838     {
839         return (LaneBasedStrategicalPlanner) super.getStrategicalPlanner();
840     }
841 
842     /** {@inheritDoc} */
843     @Override
844     public LaneBasedBehavioralCharacteristics getBehavioralCharacteristics()
845     {
846         return getStrategicalPlanner().getDrivingCharacteristics();
847     }
848 
849     /** {@inheritDoc} */
850     public void addTrigger(final Lane lane, final SimEvent<OTSSimTimeDouble> event)
851     {
852         List<SimEvent<OTSSimTimeDouble>> list = this.pendingTriggers.get(lane);
853         if (null == list)
854         {
855             list = new ArrayList<SimEvent<OTSSimTimeDouble>>();
856         }
857         list.add(event);
858         this.pendingTriggers.put(lane, list);
859     }
860 
861     /** {@inheritDoc} */
862     @Override
863     @SuppressWarnings("checkstyle:designforextension")
864     public void destroy()
865     {
866         synchronized (this.lock)
867         {
868             Set<Lane> laneSet = new HashSet<>(this.lanes.keySet()); // Operate on a copy of the key set
869             for (Lane lane : laneSet)
870             {
871                 try
872                 {
873                     leaveLane(lane, true);
874                 }
875                 catch (GTUException e)
876                 {
877                     // ignore. not important at destroy
878                 }
879             }
880         }
881         super.destroy();
882     }
883 
884     /** {@inheritDoc} */
885     @Override
886     public final Bounds getBounds()
887     {
888         double dx = 0.5 * getLength().doubleValue();
889         double dy = 0.5 * getWidth().doubleValue();
890         return new BoundingBox(new Point3d(-dx, -dy, 0.0), new Point3d(dx, dy, 0.0));
891     }
892 
893     /** {@inheritDoc} */
894     public String toString()
895     {
896         return String.format("GTU " + getId());
897     }
898 }