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