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