View Javadoc
1   package org.opentrafficsim.road.gtu.lane;
2   
3   import java.util.ArrayList;
4   import java.util.Collections;
5   import java.util.Iterator;
6   import java.util.LinkedHashMap;
7   import java.util.LinkedHashSet;
8   import java.util.List;
9   import java.util.Map;
10  import java.util.Set;
11  
12  import org.djunits.unit.DirectionUnit;
13  import org.djunits.unit.DurationUnit;
14  import org.djunits.unit.LengthUnit;
15  import org.djunits.unit.PositionUnit;
16  import org.djunits.value.vdouble.scalar.Acceleration;
17  import org.djunits.value.vdouble.scalar.Duration;
18  import org.djunits.value.vdouble.scalar.Length;
19  import org.djunits.value.vdouble.scalar.Speed;
20  import org.djunits.value.vdouble.scalar.Time;
21  import org.djutils.draw.point.Point3d;
22  import org.djutils.exceptions.Throw;
23  import org.djutils.exceptions.Try;
24  import org.djutils.immutablecollections.ImmutableMap;
25  import org.opentrafficsim.base.parameters.ParameterException;
26  import org.opentrafficsim.core.dsol.OTSSimulatorInterface;
27  import org.opentrafficsim.core.geometry.Bounds;
28  import org.opentrafficsim.core.geometry.DirectedPoint;
29  import org.opentrafficsim.core.geometry.OTSGeometryException;
30  import org.opentrafficsim.core.geometry.OTSLine3D;
31  import org.opentrafficsim.core.geometry.OTSLine3D.FractionalFallback;
32  import org.opentrafficsim.core.geometry.OTSPoint3D;
33  import org.opentrafficsim.core.gtu.AbstractGTU;
34  import org.opentrafficsim.core.gtu.GTU;
35  import org.opentrafficsim.core.gtu.GTUDirectionality;
36  import org.opentrafficsim.core.gtu.GTUException;
37  import org.opentrafficsim.core.gtu.GTUType;
38  import org.opentrafficsim.core.gtu.RelativePosition;
39  import org.opentrafficsim.core.gtu.TurnIndicatorStatus;
40  import org.opentrafficsim.core.gtu.perception.EgoPerception;
41  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlan;
42  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlanBuilder;
43  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlanException;
44  import org.opentrafficsim.core.network.LateralDirectionality;
45  import org.opentrafficsim.core.network.Link;
46  import org.opentrafficsim.core.network.NetworkException;
47  import org.opentrafficsim.core.perception.Historical;
48  import org.opentrafficsim.core.perception.HistoricalValue;
49  import org.opentrafficsim.core.perception.HistoryManager;
50  import org.opentrafficsim.core.perception.collections.HistoricalLinkedHashMap;
51  import org.opentrafficsim.core.perception.collections.HistoricalMap;
52  import org.opentrafficsim.road.gtu.lane.perception.LanePerception;
53  import org.opentrafficsim.road.gtu.lane.perception.PerceptionCollectable;
54  import org.opentrafficsim.road.gtu.lane.perception.RelativeLane;
55  import org.opentrafficsim.road.gtu.lane.perception.categories.DefaultSimplePerception;
56  import org.opentrafficsim.road.gtu.lane.perception.categories.InfrastructurePerception;
57  import org.opentrafficsim.road.gtu.lane.perception.categories.neighbors.NeighborsPerception;
58  import org.opentrafficsim.road.gtu.lane.perception.headway.HeadwayGTU;
59  import org.opentrafficsim.road.gtu.lane.plan.operational.LaneBasedOperationalPlan;
60  import org.opentrafficsim.road.gtu.strategical.LaneBasedStrategicalPlanner;
61  import org.opentrafficsim.road.network.OTSRoadNetwork;
62  import org.opentrafficsim.road.network.RoadNetwork;
63  import org.opentrafficsim.road.network.lane.CrossSectionElement;
64  import org.opentrafficsim.road.network.lane.CrossSectionLink;
65  import org.opentrafficsim.road.network.lane.DirectedLanePosition;
66  import org.opentrafficsim.road.network.lane.Lane;
67  import org.opentrafficsim.road.network.lane.LaneDirection;
68  import org.opentrafficsim.road.network.speed.SpeedLimitInfo;
69  import org.opentrafficsim.road.network.speed.SpeedLimitTypes;
70  
71  import nl.tudelft.simulation.dsol.SimRuntimeException;
72  import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEvent;
73  import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEventInterface;
74  import nl.tudelft.simulation.dsol.simtime.SimTimeDoubleUnit;
75  
76  /**
77   * This class contains most of the code that is needed to run a lane based GTU. <br>
78   * 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
79   * 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
80   * 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
81   * dozens of Lanes at the same time.
82   * <p>
83   * When calculating a headway, the GTU has to look in successive lanes. When Lanes (or underlying CrossSectionLinks) diverge,
84   * the headway algorithms have to look at multiple Lanes and return the minimum headway in each of the Lanes. When the Lanes (or
85   * underlying CrossSectionLinks) converge, "parallel" traffic is not taken into account in the headway calculation. Instead, gap
86   * acceptance algorithms or their equivalent should guide the merging behavior.
87   * <p>
88   * To decide its movement, an AbstractLaneBasedGTU applies its car following algorithm and lane change algorithm to set the
89   * acceleration and any lane change operation to perform. It then schedules the triggers that will add it to subsequent lanes
90   * and remove it from current lanes as needed during the time step that is has committed to. Finally, it re-schedules its next
91   * movement evaluation with the simulator.
92   * <p>
93   * Copyright (c) 2013-2022 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
94   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
95   * <p>
96   * @version $Revision: 1408 $, $LastChangedDate: 2015-09-24 15:17:25 +0200 (Thu, 24 Sep 2015) $, by $Author: pknoppers $,
97   *          initial version Oct 22, 2014 <br>
98   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
99   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
100  */
101 public abstract class AbstractLaneBasedGTU extends AbstractGTU implements LaneBasedGTU
102 {
103     /** */
104     private static final long serialVersionUID = 20140822L;
105 
106     /**
107      * Fractional longitudinal positions of the reference point of the GTU on one or more links at the start of the current
108      * operational plan. Because the reference point of the GTU might not be on all the links the GTU is registered on, the
109      * fractional longitudinal positions can be more than one, or less than zero.
110      */
111     private HistoricalMap<Link, Double> fractionalLinkPositions;
112 
113     /**
114      * The lanes the GTU is registered on. Each lane has to have its link registered in the fractionalLinkPositions as well to
115      * keep consistency. Each link from the fractionalLinkPositions can have one or more Lanes on which the vehicle is
116      * registered. This is a list to improve reproducibility: The 'oldest' lanes on which the vehicle is registered are at the
117      * front of the list, the later ones more to the back.
118      */
119     private final HistoricalMap<Lane, GTUDirectionality> currentLanes;
120 
121     /** Maps that we enter when initiating a lane change, but we may not actually enter given a deviative plan. */
122     private final Set<Lane> enteredLanes = new LinkedHashSet<>();
123 
124     /** Pending leave triggers for each lane. */
125     private Map<Lane, List<SimEventInterface<SimTimeDoubleUnit>>> pendingLeaveTriggers = new LinkedHashMap<>();
126 
127     /** Pending enter triggers for each lane. */
128     private Map<Lane, List<SimEventInterface<SimTimeDoubleUnit>>> pendingEnterTriggers = new LinkedHashMap<>();
129 
130     /** Event to finalize lane change. */
131     private SimEventInterface<SimTimeDoubleUnit> finalizeLaneChangeEvent = null;
132 
133     /** Cached desired speed. */
134     private Speed cachedDesiredSpeed;
135 
136     /** Time desired speed was cached. */
137     private Time desiredSpeedTime;
138 
139     /** Cached car-following acceleration. */
140     private Acceleration cachedCarFollowingAcceleration;
141 
142     /** Time car-following acceleration was cached. */
143     private Time carFollowingAccelerationTime;
144 
145     /** The object to lock to make the GTU thread safe. */
146     private Object lock = new Object();
147 
148     /** The threshold distance for differences between initial locations of the GTU on different lanes. */
149     @SuppressWarnings("checkstyle:visibilitymodifier")
150     public static Length initialLocationThresholdDifference = new Length(1.0, LengthUnit.MILLIMETER);
151 
152     /** Turn indicator status. */
153     private final Historical<TurnIndicatorStatus> turnIndicatorStatus;
154 
155     /** Caching on or off. */
156     // TODO: should be indicated with a Parameter
157     public static boolean CACHING = true;
158 
159     /** cached position count. */
160     // TODO: can be removed after testing period
161     public static int CACHED_POSITION = 0;
162 
163     /** cached position count. */
164     // TODO: can be removed after testing period
165     public static int NON_CACHED_POSITION = 0;
166 
167     /** Vehicle model. */
168     private VehicleModel vehicleModel = VehicleModel.MINMAX;
169 
170     /**
171      * Construct a Lane Based GTU.
172      * @param id String; the id of the GTU
173      * @param gtuType GTUType; the type of GTU, e.g. TruckType, CarType, BusType
174      * @param network OTSRoadNetwork; the network that the GTU is initially registered in
175      * @throws GTUException when initial values are not correct
176      */
177     public AbstractLaneBasedGTU(final String id, final GTUType gtuType, final OTSRoadNetwork network) throws GTUException
178     {
179         super(id, gtuType, network.getSimulator(), network);
180         OTSSimulatorInterface simulator = network.getSimulator();
181         HistoryManager historyManager = simulator.getReplication().getHistoryManager(simulator);
182         this.fractionalLinkPositions = new HistoricalLinkedHashMap<>(historyManager);
183         this.currentLanes = new HistoricalLinkedHashMap<>(historyManager);
184         this.turnIndicatorStatus = new HistoricalValue<>(historyManager, TurnIndicatorStatus.NOTPRESENT);
185     }
186 
187     /**
188      * @param strategicalPlanner LaneBasedStrategicalPlanner; the strategical planner (e.g., route determination) to use
189      * @param initialLongitudinalPositions Set&lt;DirectedLanePosition&gt;; the initial positions of the car on one or more
190      *            lanes with their directions
191      * @param initialSpeed Speed; the initial speed of the car on the lane
192      * @throws NetworkException when the GTU cannot be placed on the given lane
193      * @throws SimRuntimeException when the move method cannot be scheduled
194      * @throws GTUException when initial values are not correct
195      * @throws OTSGeometryException when the initial path is wrong
196      */
197     @SuppressWarnings("checkstyle:designforextension")
198     public void init(final LaneBasedStrategicalPlanner strategicalPlanner,
199             final Set<DirectedLanePosition> initialLongitudinalPositions, final Speed initialSpeed)
200             throws NetworkException, SimRuntimeException, GTUException, OTSGeometryException
201     {
202         Throw.when(null == initialLongitudinalPositions, GTUException.class, "InitialLongitudinalPositions is null");
203         Throw.when(0 == initialLongitudinalPositions.size(), GTUException.class, "InitialLongitudinalPositions is empty set");
204 
205         DirectedPoint lastPoint = null;
206         for (DirectedLanePosition pos : initialLongitudinalPositions)
207         {
208             // Throw.when(lastPoint != null && pos.getLocation().distance(lastPoint) > initialLocationThresholdDifference.si,
209             // GTUException.class, "initial locations for GTU have distance > " + initialLocationThresholdDifference);
210             lastPoint = pos.getLocation();
211         }
212         DirectedPoint initialLocation = lastPoint;
213 
214         // Give the GTU a 1 micrometer long operational plan, or a stand-still plan, so the first move and events will work
215         Time now = getSimulator().getSimulatorTime();
216         try
217         {
218             if (initialSpeed.si < OperationalPlan.DRIFTING_SPEED_SI)
219             {
220                 this.operationalPlan
221                         .set(new OperationalPlan(this, initialLocation, now, new Duration(1E-6, DurationUnit.SECOND)));
222             }
223             else
224             {
225                 OTSPoint3D p2 = new OTSPoint3D(initialLocation.x + 1E-6 * Math.cos(initialLocation.getRotZ()),
226                         initialLocation.y + 1E-6 * Math.sin(initialLocation.getRotZ()), initialLocation.z);
227                 OTSLine3D path = new OTSLine3D(new OTSPoint3D(initialLocation), p2);
228                 this.operationalPlan.set(OperationalPlanBuilder.buildConstantSpeedPlan(this, path, now, initialSpeed));
229             }
230         }
231         catch (OperationalPlanException e)
232         {
233             throw new RuntimeException("Initial operational plan could not be created.", e);
234         }
235 
236         // register the GTU on the lanes
237         for (DirectedLanePosition directedLanePosition : initialLongitudinalPositions)
238         {
239             Lane lane = directedLanePosition.getLane();
240             addLaneToGtu(lane, directedLanePosition.getPosition(), directedLanePosition.getGtuDirection()); // enter lane part 1
241         }
242 
243         // init event
244         DirectedLanePosition referencePosition = getReferencePosition();
245         fireTimedEvent(LaneBasedGTU.LANEBASED_INIT_EVENT,
246                 new Object[] { getId(), initialLocation, getLength(), getWidth(), referencePosition.getLane(),
247                         referencePosition.getPosition(), referencePosition.getGtuDirection(), getGTUType() },
248                 getSimulator().getSimulatorTime());
249 
250         // register the GTU on the lanes
251         for (DirectedLanePosition directedLanePosition : initialLongitudinalPositions)
252         {
253             Lane lane = directedLanePosition.getLane();
254             lane.addGTU(this, directedLanePosition.getPosition()); // enter lane part 2
255         }
256 
257         // initiate the actual move
258         super.init(strategicalPlanner, initialLocation, initialSpeed);
259 
260         this.referencePositionTime = Double.NaN; // remove cache, it may be invalid as the above init results in a lane change
261 
262     }
263 
264     /**
265      * {@inheritDoc} All lanes the GTU is on will be left.
266      */
267     @Override
268     public void setParent(final GTU gtu) throws GTUException
269     {
270         for (Lane lane : new LinkedHashSet<>(this.currentLanes.keySet())) // copy for concurrency problems
271         {
272             leaveLane(lane);
273         }
274         super.setParent(gtu);
275     }
276 
277     /**
278      * Reinitializes the GTU on the network using the existing strategical planner and zero speed.
279      * @param initialLongitudinalPositions Set&lt;DirectedLanePosition&gt;; initial position
280      * @throws NetworkException when the GTU cannot be placed on the given lane
281      * @throws SimRuntimeException when the move method cannot be scheduled
282      * @throws GTUException when initial values are not correct
283      * @throws OTSGeometryException when the initial path is wrong
284      */
285     public void reinit(final Set<DirectedLanePosition> initialLongitudinalPositions)
286             throws NetworkException, SimRuntimeException, GTUException, OTSGeometryException
287     {
288         init(getStrategicalPlanner(), initialLongitudinalPositions, Speed.ZERO);
289     }
290 
291     /**
292      * Hack method. TODO remove and solve better
293      * @return safe to change
294      * @throws GTUException on error
295      */
296     public final boolean isSafeToChange() throws GTUException
297     {
298         return this.fractionalLinkPositions.get(getReferencePosition().getLane().getParentLink()) > 0.0;
299     }
300 
301     /**
302      * insert GTU at a certain position. This can happen at setup (first initialization), and after a lane change of the GTU.
303      * The relative position that will be registered is the referencePosition (dx, dy, dz) = (0, 0, 0). Front and rear positions
304      * are relative towards this position.
305      * @param lane Lane; the lane to add to the list of lanes on which the GTU is registered.
306      * @param gtuDirection GTUDirectionality; the direction of the GTU on the lane (which can be bidirectional). If the GTU has
307      *            a positive speed, it is moving in this direction.
308      * @param position Length; the position on the lane.
309      * @throws GTUException when positioning the GTU on the lane causes a problem
310      */
311     @SuppressWarnings("checkstyle:designforextension")
312     public void enterLane(final Lane lane, final Length position, final GTUDirectionality gtuDirection) throws GTUException
313     {
314         if (lane == null || gtuDirection == null || position == null)
315         {
316             throw new GTUException("enterLane - one of the arguments is null");
317         }
318         addLaneToGtu(lane, position, gtuDirection);
319         addGtuToLane(lane, position);
320     }
321 
322     /**
323      * Registers the lane at the GTU. Only works at the start of a operational plan.
324      * @param lane Lane; the lane to add to the list of lanes on which the GTU is registered.
325      * @param gtuDirection GTUDirectionality; the direction of the GTU on the lane (which can be bidirectional). If the GTU has
326      *            a positive speed, it is moving in this direction.
327      * @param position Length; the position on the lane.
328      * @throws GTUException when positioning the GTU on the lane causes a problem
329      */
330     private void addLaneToGtu(final Lane lane, final Length position, final GTUDirectionality gtuDirection) throws GTUException
331     {
332         if (this.currentLanes.containsKey(lane))
333         {
334             System.err.println(this + " is already registered on lane: " + lane + " at fractional position "
335                     + this.fractionalPosition(lane, RelativePosition.REFERENCE_POSITION) + " intended position is " + position
336                     + " length of lane is " + lane.getLength());
337             return;
338         }
339         // if the GTU is already registered on a lane of the same link, do not change its fractional position, as
340         // this might lead to a "jump".
341         if (!this.fractionalLinkPositions.containsKey(lane.getParentLink()))
342         {
343             this.fractionalLinkPositions.put(lane.getParentLink(), lane.fraction(position));
344         }
345         this.currentLanes.put(lane, gtuDirection);
346     }
347 
348     /**
349      * Part of 'enterLane' which registers the GTU with the lane so the lane can report its GTUs.
350      * @param lane Lane; lane
351      * @param position Length; position
352      * @throws GTUException on exception
353      */
354     protected void addGtuToLane(final Lane lane, final Length position) throws GTUException
355     {
356         List<SimEventInterface<SimTimeDoubleUnit>> pending = this.pendingEnterTriggers.get(lane);
357         if (null != pending)
358         {
359             for (SimEventInterface<SimTimeDoubleUnit> event : pending)
360             {
361                 if (event.getAbsoluteExecutionTime().get().ge(getSimulator().getSimulatorTime()))
362                 {
363                     boolean result = getSimulator().cancelEvent(event);
364                     if (!result && event.getAbsoluteExecutionTime().get().ne(getSimulator().getSimulatorTime()))
365                     {
366                         System.err.println("addLaneToGtu, trying to remove event: NOTHING REMOVED -- result=" + result
367                                 + ", simTime=" + getSimulator().getSimulatorTime() + ", eventTime="
368                                 + event.getAbsoluteExecutionTime().get());
369                     }
370                 }
371             }
372             this.pendingEnterTriggers.remove(lane);
373         }
374         lane.addGTU(this, position);
375     }
376 
377     /**
378      * Unregister the GTU from a lane.
379      * @param lane Lane; the lane to remove from the list of lanes on which the GTU is registered.
380      * @throws GTUException when leaveLane should not be called
381      */
382     @SuppressWarnings("checkstyle:designforextension")
383     public void leaveLane(final Lane lane) throws GTUException
384     {
385         leaveLane(lane, false);
386     }
387 
388     /**
389      * Leave a lane but do not complain about having no lanes left when beingDestroyed is true.
390      * @param lane Lane; the lane to leave
391      * @param beingDestroyed boolean; if true, no complaints about having no lanes left
392      * @throws GTUException in case leaveLane should not be called
393      */
394     @SuppressWarnings("checkstyle:designforextension")
395     public void leaveLane(final Lane lane, final boolean beingDestroyed) throws GTUException
396     {
397         Length position = position(lane, getReference());
398         this.currentLanes.remove(lane);
399         removePendingEvents(lane, this.pendingLeaveTriggers);
400         removePendingEvents(lane, this.pendingEnterTriggers);
401         // check if there are any lanes for this link left. If not, remove the link.
402         boolean found = false;
403         for (Lane l : this.currentLanes.keySet())
404         {
405             if (l.getParentLink().equals(lane.getParentLink()))
406             {
407                 found = true;
408             }
409         }
410         if (!found)
411         {
412             this.fractionalLinkPositions.remove(lane.getParentLink());
413         }
414         lane.removeGTU(this, !found, position);
415         if (this.currentLanes.size() == 0 && !beingDestroyed)
416         {
417             System.err.println("leaveLane: lanes.size() = 0 for GTU " + getId());
418         }
419     }
420 
421     /**
422      * Removes and cancels events for the given lane.
423      * @param lane Lane; lane
424      * @param triggers Map&lt;Lane, List&lt;SimEventInterface&lt;SimTimeDoubleUnit&gt;&gt;&gt;; map to use
425      */
426     private void removePendingEvents(final Lane lane, final Map<Lane, List<SimEventInterface<SimTimeDoubleUnit>>> triggers)
427     {
428         List<SimEventInterface<SimTimeDoubleUnit>> pending = triggers.get(lane);
429         if (null != pending)
430         {
431             for (SimEventInterface<SimTimeDoubleUnit> event : pending)
432             {
433                 if (event.getAbsoluteExecutionTime().get().ge(getSimulator().getSimulatorTime()))
434                 {
435                     boolean result = getSimulator().cancelEvent(event);
436                     if (!result && event.getAbsoluteExecutionTime().get().ne(getSimulator().getSimulatorTime()))
437                     {
438                         System.err.println("leaveLane, trying to remove event: NOTHING REMOVED -- result=" + result
439                                 + ", simTime=" + getSimulator().getSimulatorTime() + ", eventTime="
440                                 + event.getAbsoluteExecutionTime().get());
441                     }
442                 }
443             }
444             triggers.remove(lane);
445         }
446     }
447 
448     /** {@inheritDoc} */
449     @Override
450     public void changeLaneInstantaneously(final LateralDirectionality laneChangeDirection) throws GTUException
451     {
452 
453         // from info
454         DirectedLanePosition from = getReferencePosition();
455 
456         // keep a copy of the lanes and directions (!)
457         Set<Lane> lanesToBeRemoved = new LinkedHashSet<>(this.currentLanes.keySet());
458 
459         // store the new positions
460         // start with current link position, these will be overwritten, except if from a lane no adjacent lane is found, i.e.
461         // changing over a continuous line when probably the reference point is past the line
462         Map<Link, Double> newLinkPositionsLC = new LinkedHashMap<>(this.fractionalLinkPositions);
463 
464         // obtain position on lane adjacent to reference lane and enter lanes upstream/downstream from there
465         Set<Lane> adjLanes = from.getLane().accessibleAdjacentLanesPhysical(laneChangeDirection, getGTUType(),
466                 this.currentLanes.get(from.getLane()));
467         Lane adjLane = adjLanes.iterator().next();
468         Length position = adjLane.position(from.getLane().fraction(from.getPosition()));
469         GTUDirectionality direction = getDirection(from.getLane());
470         Length planLength = Try.assign(() -> getOperationalPlan().getTraveledDistance(getSimulator().getSimulatorTime()),
471                 "Exception while determining plan length.");
472         enterLaneRecursive(new LaneDirection(adjLane, direction), position, newLinkPositionsLC, planLength, lanesToBeRemoved,
473                 0);
474 
475         // update the positions on the lanes we are registered on
476         this.fractionalLinkPositions.clear();
477         this.fractionalLinkPositions.putAll(newLinkPositionsLC);
478 
479         // leave the from lanes
480         for (Lane lane : lanesToBeRemoved)
481         {
482             leaveLane(lane);
483         }
484 
485         // stored positions no longer valid
486         this.referencePositionTime = Double.NaN;
487         this.cachedPositions.clear();
488 
489         // fire event
490         this.fireTimedEvent(LaneBasedGTU.LANE_CHANGE_EVENT, new Object[] { getId(), laneChangeDirection, from },
491                 getSimulator().getSimulatorTime());
492 
493     }
494 
495     /**
496      * Enters lanes upstream and downstream of the new location after an instantaneous lane change.
497      * @param lane LaneDirection; considered lane
498      * @param position Length; position to add GTU at
499      * @param newLinkPositionsLC Map&lt;Link, Double&gt;; new link fractions to store
500      * @param planLength Length; length of plan, to consider fractions at start
501      * @param lanesToBeRemoved Set&lt;Lane&gt;; lanes to leave, from which lanes are removed when entered (such that they arent
502      *            then left)
503      * @param dir int; below 0 for upstream, above 0 for downstream, 0 for both
504      * @throws GTUException on exception
505      */
506     private void enterLaneRecursive(final LaneDirection lane, final Length position, final Map<Link, Double> newLinkPositionsLC,
507             final Length planLength, final Set<Lane> lanesToBeRemoved, final int dir) throws GTUException
508     {
509         enterLane(lane.getLane(), position, lane.getDirection());
510         lanesToBeRemoved.remove(lane);
511         Length adjusted = lane.getDirection().isPlus() ? position.minus(planLength) : position.plus(planLength);
512         newLinkPositionsLC.put(lane.getLane().getParentLink(), adjusted.si / lane.getLength().si);
513 
514         // upstream
515         if (dir < 1)
516         {
517             Length rear = lane.getDirection().isPlus() ? position.plus(getRear().getDx()) : position.minus(getRear().getDx());
518             Length before = null;
519             if (lane.getDirection().isPlus() && rear.si < 0.0)
520             {
521                 before = rear.neg();
522             }
523             else if (lane.getDirection().isMinus() && rear.si > lane.getLength().si)
524             {
525                 before = rear.minus(lane.getLength());
526             }
527             if (before != null)
528             {
529                 GTUDirectionality upDir = lane.getDirection();
530                 ImmutableMap<Lane, GTUDirectionality> upstream = lane.getLane().upstreamLanes(upDir, getGTUType());
531                 if (!upstream.isEmpty())
532                 {
533                     Lane upLane = null;
534                     for (Lane nextUp : upstream.keySet())
535                     {
536                         if (newLinkPositionsLC.containsKey(nextUp.getParentLink()))
537                         {
538                             // multiple upstream lanes could belong to the same link, we pick an arbitrary lane
539                             // (a conflict should solve this)
540                             upLane = nextUp;
541                             break;
542                         }
543                     }
544                     if (upLane == null)
545                     {
546                         // the rear is on an upstream section we weren't before the lane change, due to curvature, we pick an
547                         // arbitrary lane (a conflict should solve this)
548                         upLane = upstream.keySet().iterator().next();
549                     }
550                     if (!this.currentLanes.containsKey(upLane))
551                     {
552                         upDir = upstream.get(upLane);
553                         LaneDirectionk/lane/LaneDirection.html#LaneDirection">LaneDirection next = new LaneDirection(upLane, upDir);
554                         Length nextPos = upDir.isPlus() ? next.getLength().minus(before).minus(getRear().getDx())
555                                 : before.plus(getRear().getDx());
556                         enterLaneRecursive(next, nextPos, newLinkPositionsLC, planLength, lanesToBeRemoved, -1);
557                     }
558                 }
559             }
560         }
561 
562         // downstream
563         if (dir > -1)
564         {
565             Length front =
566                     lane.getDirection().isPlus() ? position.plus(getFront().getDx()) : position.minus(getFront().getDx());
567             Length passed = null;
568             if (lane.getDirection().isPlus() && front.si > lane.getLength().si)
569             {
570                 passed = front.minus(lane.getLength());
571             }
572             else if (lane.getDirection().isMinus() && front.si < 0.0)
573             {
574                 passed = front.neg();
575             }
576             if (passed != null)
577             {
578                 LaneDirection next = lane.getNextLaneDirection(this);
579                 if (!this.currentLanes.containsKey(next.getLane()))
580                 {
581                     Length nextPos = next.getDirection().isPlus() ? passed.minus(getFront().getDx())
582                             : next.getLength().minus(passed).plus(getFront().getDx());
583                     enterLaneRecursive(next, nextPos, newLinkPositionsLC, planLength, lanesToBeRemoved, 1);
584                 }
585             }
586         }
587     }
588 
589     /**
590      * Register on lanes in target lane.
591      * @param laneChangeDirection LateralDirectionality; direction of lane change
592      * @throws GTUException exception
593      */
594     @Override
595     @SuppressWarnings("checkstyle:designforextension")
596     public void initLaneChange(final LateralDirectionality laneChangeDirection) throws GTUException
597     {
598         Map<Lane, GTUDirectionality> lanesCopy = new LinkedHashMap<>(this.currentLanes);
599         Map<Lane, Double> fractionalLanePositions = new LinkedHashMap<>();
600         for (Lane lane : lanesCopy.keySet())
601         {
602             fractionalLanePositions.put(lane, fractionalPosition(lane, getReference()));
603         }
604         int numRegistered = 0;
605         for (Lane lane : lanesCopy.keySet())
606         {
607             Set<Lane> laneSet = lane.accessibleAdjacentLanesLegal(laneChangeDirection, getGTUType(), getDirection(lane));
608             if (laneSet.size() > 0)
609             {
610                 numRegistered++;
611                 Lane adjacentLane = laneSet.iterator().next();
612                 Length position = adjacentLane.getLength().times(fractionalLanePositions.get(lane));
613                 if (lanesCopy.get(lane).isPlus() ? position.lt(lane.getLength().minus(getRear().getDx()))
614                         : position.gt(getFront().getDx().neg()))
615                 {
616                     this.enteredLanes.add(adjacentLane);
617                     enterLane(adjacentLane, position, lanesCopy.get(lane));
618                 }
619                 else
620                 {
621                     System.out.println("Skipping enterLane for GTU " + getId() + " on lane " + lane.getFullId() + " at "
622                             + position + ", lane length = " + lane.getLength() + " rear = " + getRear().getDx() + " front = "
623                             + getFront().getDx());
624                 }
625             }
626         }
627         Throw.when(numRegistered == 0, GTUException.class, "Gtu %s starting %s lane change, but no adjacent lane found.",
628                 getId(), laneChangeDirection);
629     }
630 
631     /**
632      * Performs the finalization of a lane change by leaving the from lanes.
633      * @param laneChangeDirection LateralDirectionality; direction of lane change
634      */
635     @SuppressWarnings("checkstyle:designforextension")
636     protected void finalizeLaneChange(final LateralDirectionality laneChangeDirection)
637     {
638         Map<Lane, GTUDirectionality> lanesCopy = new LinkedHashMap<>(this.currentLanes);
639         Set<Lane> lanesToBeRemoved = new LinkedHashSet<>();
640         Lane fromLane = null;
641         Length fromPosition = null;
642         GTUDirectionality fromDirection = null;
643         try
644         {
645             // find lanes to leave as they have an adjacent lane the GTU is also on in the lane change direction
646             for (Lane lane : lanesCopy.keySet())
647             {
648                 Iterator<Lane> iterator =
649                         lane.accessibleAdjacentLanesPhysical(laneChangeDirection, getGTUType(), getDirection(lane)).iterator();
650                 if (iterator.hasNext() && lanesCopy.keySet().contains(iterator.next()))
651                 {
652                     lanesToBeRemoved.add(lane);
653                 }
654             }
655             // some lanes registered to the GTU may be downstream of a split and have no adjacent lane, find longitudinally
656             boolean added = true;
657             while (added)
658             {
659                 added = false;
660                 Set<Lane> lanesToAlsoBeRemoved = new LinkedHashSet<>();
661                 for (Lane lane : lanesToBeRemoved)
662                 {
663                     GTUDirectionality direction = getDirection(lane);
664                     for (Lane nextLane : direction.isPlus() ? lane.nextLanes(getGTUType()).keySet()
665                             : lane.prevLanes(getGTUType()).keySet())
666                     {
667                         if (lanesCopy.containsKey(nextLane) && !lanesToBeRemoved.contains(nextLane))
668                         {
669                             added = true;
670                             lanesToAlsoBeRemoved.add(nextLane);
671                         }
672                     }
673                 }
674                 lanesToBeRemoved.addAll(lanesToAlsoBeRemoved);
675             }
676             double nearest = Double.POSITIVE_INFINITY;
677             for (Lane lane : lanesToBeRemoved)
678             {
679                 Length pos = position(lane, RelativePosition.REFERENCE_POSITION);
680                 if (0.0 <= pos.si && pos.si <= lane.getLength().si)
681                 {
682                     fromLane = lane;
683                     fromPosition = pos;
684                     fromDirection = getDirection(lane);
685                 }
686                 else if (fromLane == null && (getDirection(lane).isPlus() ? pos.si > lane.getLength().si : pos.le0()))
687                 {
688                     // if the reference point is in between two lanes, this recognizes the lane upstream of the gap
689                     double distance = getDirection(lane).isPlus() ? pos.si - lane.getLength().si : -pos.si;
690                     if (distance < nearest)
691                     {
692                         nearest = distance;
693                         fromLane = lane;
694                         fromPosition = pos;
695                         fromDirection = getDirection(lane);
696                     }
697                 }
698                 leaveLane(lane);
699             }
700             this.referencePositionTime = Double.NaN;
701             this.finalizeLaneChangeEvent = null;
702         }
703         catch (GTUException exception)
704         {
705             // should not happen, lane was obtained from GTU
706             throw new RuntimeException("position on lane not possible", exception);
707         }
708         Throw.when(fromLane == null, RuntimeException.class, "No from lane for lane change event.");
709         DirectedLanePosition from;
710         try
711         {
712             from = new DirectedLanePosition(fromLane, fromPosition, fromDirection);
713         }
714         catch (GTUException exception)
715         {
716             throw new RuntimeException(exception);
717         }
718         this.fireTimedEvent(LaneBasedGTU.LANE_CHANGE_EVENT, new Object[] { getId(), laneChangeDirection, from },
719                 getSimulator().getSimulatorTime());
720     }
721 
722     /** {@inheritDoc} */
723     @Override
724     public void setFinalizeLaneChangeEvent(final SimEventInterface<SimTimeDoubleUnit> event)
725     {
726         this.finalizeLaneChangeEvent = event;
727     }
728 
729     /** {@inheritDoc} */
730     @Override
731     public final GTUDirectionality getDirection(final Lane lane) throws GTUException
732     {
733         Throw.when(!this.currentLanes.containsKey(lane), GTUException.class, "getDirection: Lanes %s does not contain %s",
734                 this.currentLanes.keySet(), lane);
735         return this.currentLanes.get(lane);
736     }
737 
738     /** {@inheritDoc} */
739     @Override
740     @SuppressWarnings("checkstyle:designforextension")
741     protected boolean move(final DirectedPoint fromLocation)
742             throws SimRuntimeException, GTUException, OperationalPlanException, NetworkException, ParameterException
743     {
744         // DirectedPoint currentPoint = getLocation(); // used for "jump" detection that is also commented out
745         // Only carry out move() if we still have lane(s) to drive on.
746         // Note: a (Sink) trigger can have 'destroyed' us between the previous evaluation step and this one.
747         if (this.currentLanes.isEmpty())
748         {
749             destroy();
750             return false; // Done; do not re-schedule execution of this move method.
751         }
752 
753         // remove enter events
754         // WS: why?
755         // for (Lane lane : this.pendingEnterTriggers.keySet())
756         // {
757         // System.out.println("GTU " + getId() + " is canceling event on lane " + lane.getFullId());
758         // List<SimEventInterface<SimTimeDoubleUnit>> events = this.pendingEnterTriggers.get(lane);
759         // for (SimEventInterface<SimTimeDoubleUnit> event : events)
760         // {
761         // // also unregister from lane
762         // this.currentLanes.remove(lane);
763         // getSimulator().cancelEvent(event);
764         // }
765         // }
766         // this.pendingEnterTriggers.clear();
767 
768         // get distance covered in previous plan, to aid a shift in link fraction (from which a plan moves onwards)
769         Length covered;
770         if (getOperationalPlan() instanceof LaneBasedOperationalPlan
771                 && ((LaneBasedOperationalPlan) getOperationalPlan()).isDeviative())
772         {
773             // traveled distance as difference between start and current position on reference lane
774             // note that for a deviative plan the traveled distance along the path is not valuable here
775             LaneBasedOperationalPlanrg/opentrafficsim/road/gtu/lane/plan/operational/LaneBasedOperationalPlan.html#LaneBasedOperationalPlan">LaneBasedOperationalPlan plan = (LaneBasedOperationalPlan) getOperationalPlan();
776             DirectedLanePosition ref = getReferencePosition();
777             covered = ref.getGtuDirection().isPlus()
778                     ? position(ref.getLane(), getReference())
779                             .minus(position(ref.getLane(), getReference(), plan.getStartTime()))
780                     : position(ref.getLane(), getReference(), plan.getStartTime())
781                             .minus(position(ref.getLane(), getReference()));
782             // Note that distance is valid as the reference lane can not change (and location of previous plan is start location
783             // of current plan). Only instantaneous lane changes can do that, which do not result in deviative plans.
784         }
785         else
786         {
787             covered = getOperationalPlan().getTraveledDistance(getSimulator().getSimulatorTime());
788         }
789 
790         // generate the next operational plan and carry it out
791         // in case of an instantaneous lane change, fractionalLinkPositions will be accordingly adjusted to the new lane
792         super.move(fromLocation);
793 
794         // update the positions on the lanes we are registered on
795         // WS: this was previously done using fractions calculated before super.move() based on the GTU position, but an
796         // instantaneous lane change while e.g. the nose is on the next lane which is curved, results in a different fraction on
797         // the next link (the GTU doesn't stretch or shrink)
798         Map<Link, Double> newLinkFractions = new LinkedHashMap<>(this.fractionalLinkPositions);
799         Set<Link> done = new LinkedHashSet<>();
800         // WS: this used to be on all current lanes, skipping links already processed, but 'covered' regards the reference lane
801         updateLinkFraction(getReferencePosition().getLane(), newLinkFractions, done, false, covered, true);
802         updateLinkFraction(getReferencePosition().getLane(), newLinkFractions, done, true, covered, true);
803         this.fractionalLinkPositions.clear();
804         this.fractionalLinkPositions.putAll(newLinkFractions);
805 
806         DirectedLanePosition dlp = getReferencePosition();
807         fireTimedEvent(
808                 LaneBasedGTU.LANEBASED_MOVE_EVENT,
809                 new Object[] { getId(), new OTSPoint3D(fromLocation).doubleVector(PositionUnit.METER),
810                         OTSPoint3D.direction(fromLocation, DirectionUnit.EAST_RADIAN), getSpeed(), getAcceleration(),
811                         getTurnIndicatorStatus(), getOdometer(), dlp.getLane().getParentLink().getId(), dlp.getLane().getId(),
812                         dlp.getPosition(), dlp.getGtuDirection().name() },
813                 getSimulator().getSimulatorTime());
814 
815         if (getOperationalPlan().getAcceleration(Duration.ZERO).si < -10
816                 && getOperationalPlan().getSpeed(Duration.ZERO).si > 2.5)
817         {
818             System.err.println("GTU: " + getId() + " - getOperationalPlan().getAcceleration(Duration.ZERO).si < -10)");
819             System.err.println("Lanes in current plan: " + this.currentLanes.keySet());
820             if (getTacticalPlanner().getPerception().contains(DefaultSimplePerception.class))
821             {
822                 DefaultSimplePerception p =
823                         getTacticalPlanner().getPerception().getPerceptionCategory(DefaultSimplePerception.class);
824                 System.err.println("HeadwayGTU: " + p.getForwardHeadwayGTU());
825                 System.err.println("HeadwayObject: " + p.getForwardHeadwayObject());
826             }
827         }
828         // DirectedPoint currentPointAfterMove = getLocation();
829         // if (currentPoint.distance(currentPointAfterMove) > 0.1)
830         // {
831         // System.err.println(this.getId() + " jumped");
832         // }
833         // schedule triggers and determine when to enter lanes with front and leave lanes with rear
834         scheduleEnterLeaveTriggers();
835         return false;
836     }
837 
838     /**
839      * Recursive update of link fractions based on a moved distance.
840      * @param lane Lane; current lane, start with reference lane
841      * @param newLinkFractions Map&lt;Link, Double&gt;; map to put new fractions in
842      * @param done Set&lt;Link&gt;; links to skip as link are already done
843      * @param prevs boolean; whether to loop to the previous or next lanes, regardless of driving direction
844      * @param covered Length; covered distance along the reference lane
845      * @param isReferenceLane boolean; whether this lane is the reference lane (to skip in second call)
846      */
847     private void updateLinkFraction(final Lane lane, final Map<Link, Double> newLinkFractions, final Set<Link> done,
848             final boolean prevs, final Length covered, final boolean isReferenceLane)
849     {
850         if (!prevs || !isReferenceLane)
851         {
852             if (done.contains(lane.getParentLink()) || !this.currentLanes.containsKey(lane))
853             {
854                 return;
855             }
856             double sign;
857             try
858             {
859                 sign = getDirection(lane).isPlus() ? 1.0 : -1.0;
860             }
861             catch (GTUException exception)
862             {
863                 // can not happen as we check that the lane is in the currentLanes
864                 throw new RuntimeException("Unexpected exception: trying to obtain direction on lane.", exception);
865             }
866             newLinkFractions.put(lane.getParentLink(),
867                     this.fractionalLinkPositions.get(lane.getParentLink()) + sign * covered.si / lane.getLength().si);
868             done.add(lane.getParentLink());
869         }
870         for (Lane nextLane : (prevs ? lane.prevLanes(getGTUType()) : lane.nextLanes(getGTUType())).keySet())
871         {
872             updateLinkFraction(nextLane, newLinkFractions, done, prevs, covered, false);
873         }
874     }
875 
876     /** {@inheritDoc} */
877     @Override
878     public final Map<Lane, Length> positions(final RelativePosition relativePosition) throws GTUException
879     {
880         return positions(relativePosition, getSimulator().getSimulatorTime());
881     }
882 
883     /** {@inheritDoc} */
884     @Override
885     public final Map<Lane, Length> positions(final RelativePosition relativePosition, final Time when) throws GTUException
886     {
887         Map<Lane, Length> positions = new LinkedHashMap<>();
888         for (Lane lane : this.currentLanes.keySet())
889         {
890             positions.put(lane, position(lane, relativePosition, when));
891         }
892         return positions;
893     }
894 
895     /** {@inheritDoc} */
896     @Override
897     public final Length position(final Lane lane, final RelativePosition relativePosition) throws GTUException
898     {
899         return position(lane, relativePosition, getSimulator().getSimulatorTime());
900     }
901 
902     /**
903      * Return the longitudinal position that the indicated relative position of this GTU would have if it were to change to
904      * another Lane with a / the current CrossSectionLink. This point may be before the begin or after the end of the link of
905      * the projection lane of the GTU. This preserves the length of the GTU.
906      * @param projectionLane Lane; the lane onto which the position of this GTU must be projected
907      * @param relativePosition RelativePosition; the point on this GTU that must be projected
908      * @param when Time; the time for which to project the position of this GTU
909      * @return Length; the position of this GTU in the projectionLane
910      * @throws GTUException when projectionLane it not in any of the CrossSectionLink that the GTU is on
911      */
912     @SuppressWarnings("checkstyle:designforextension")
913     public Length translatedPosition(final Lane projectionLane, final RelativePosition relativePosition, final Time when)
914             throws GTUException
915     {
916         CrossSectionLink link = projectionLane.getParentLink();
917         for (CrossSectionElement cse : link.getCrossSectionElementList())
918         {
919             if (cse instanceof Lane)
920             {
921                 Lane"../../../../../org/opentrafficsim/road/network/lane/Lane.html#Lane">Lane cseLane = (Lane) cse;
922                 if (null != this.currentLanes.get(cseLane))
923                 {
924                     double fractionalPosition = fractionalPosition(cseLane, RelativePosition.REFERENCE_POSITION, when);
925                     Length pos = new Length(projectionLane.getLength().getSI() * fractionalPosition, LengthUnit.SI);
926                     if (this.currentLanes.get(cseLane).isPlus())
927                     {
928                         return pos.plus(relativePosition.getDx());
929                     }
930                     return pos.minus(relativePosition.getDx());
931                 }
932             }
933         }
934         throw new GTUException(this + " is not on any lane of Link " + link);
935     }
936 
937     /**
938      * Return the longitudinal position on the projection lane that has the same fractional position on one of the current lanes
939      * of the indicated relative position. This preserves the fractional positions of all relative positions of the GTU.
940      * @param projectionLane Lane; the lane onto which the position of this GTU must be projected
941      * @param relativePosition RelativePosition; the point on this GTU that must be projected
942      * @param when Time; the time for which to project the position of this GTU
943      * @return Length; the position of this GTU in the projectionLane
944      * @throws GTUException when projectionLane it not in any of the CrossSectionLink that the GTU is on
945      */
946     @SuppressWarnings("checkstyle:designforextension")
947     public Length projectedPosition(final Lane projectionLane, final RelativePosition relativePosition, final Time when)
948             throws GTUException
949     {
950         CrossSectionLink link = projectionLane.getParentLink();
951         for (CrossSectionElement cse : link.getCrossSectionElementList())
952         {
953             if (cse instanceof Lane)
954             {
955                 Lane"../../../../../org/opentrafficsim/road/network/lane/Lane.html#Lane">Lane cseLane = (Lane) cse;
956                 if (null != this.currentLanes.get(cseLane))
957                 {
958                     double fractionalPosition = fractionalPosition(cseLane, relativePosition, when);
959                     return new Length(projectionLane.getLength().getSI() * fractionalPosition, LengthUnit.SI);
960                 }
961             }
962         }
963         throw new GTUException(this + " is not on any lane of Link " + link);
964     }
965 
966     /** caching of time field for last stored position(s). */
967     private double cachePositionsTime = Double.NaN;
968 
969     /** caching of last stored position(s). */
970     private Map<Integer, Length> cachedPositions = new LinkedHashMap<>();
971 
972     /** {@inheritDoc} */
973     @Override
974     @SuppressWarnings("checkstyle:designforextension")
975     public Length position(final Lane lane, final RelativePosition relativePosition, final Time when) throws GTUException
976     {
977         int cacheIndex = 0;
978         if (CACHING)
979         {
980             cacheIndex = 17 * lane.hashCode() + relativePosition.hashCode();
981             Length l;
982             if (when.si == this.cachePositionsTime && (l = this.cachedPositions.get(cacheIndex)) != null)
983             {
984                 // PK verify the result; uncomment if you don't trust the cache
985                 // this.cachedPositions.clear();
986                 // Length difficultWay = position(lane, relativePosition, when);
987                 // if (Math.abs(l.si - difficultWay.si) > 0.00001)
988                 // {
989                 // System.err.println("Whoops: cache returns bad value for GTU " + getId());
990                 // }
991                 CACHED_POSITION++;
992                 return l;
993             }
994             if (when.si != this.cachePositionsTime)
995             {
996                 this.cachedPositions.clear();
997                 this.cachePositionsTime = when.si;
998             }
999         }
1000         NON_CACHED_POSITION++;
1001 
1002         synchronized (this.lock)
1003         {
1004             double loc = Double.NaN;
1005             try
1006             {
1007                 OperationalPlan plan = getOperationalPlan(when);
1008                 if (!(plan instanceof LaneBasedOperationalPlanorg/opentrafficsim/road/gtu/lane/plan/operational/LaneBasedOperationalPlan.html#LaneBasedOperationalPlan">LaneBasedOperationalPlan) || !((LaneBasedOperationalPlan) plan).isDeviative())
1009                 {
1010                     double longitudinalPosition;
1011                     try
1012                     {
1013                         longitudinalPosition =
1014                                 lane.positionSI(this.fractionalLinkPositions.get(when).get(lane.getParentLink()));
1015                     }
1016                     catch (NullPointerException exception)
1017                     {
1018                         throw exception;
1019                     }
1020                     if (this.currentLanes.get(when).get(lane).isPlus())
1021                     {
1022                         loc = longitudinalPosition + plan.getTraveledDistanceSI(when) + relativePosition.getDx().si;
1023                     }
1024                     else
1025                     {
1026                         loc = longitudinalPosition - plan.getTraveledDistanceSI(when) - relativePosition.getDx().si;
1027                     }
1028                 }
1029                 else
1030                 {
1031                     // deviative LaneBasedOperationalPlan, i.e. the GTU is not on a center line
1032                     DirectedPoint p = plan.getLocation(when, relativePosition);
1033                     double f = lane.getCenterLine().projectFractional(null, null, p.x, p.y, FractionalFallback.NaN);
1034                     if (!Double.isNaN(f))
1035                     {
1036                         loc = f * lane.getLength().si;
1037                     }
1038                     else
1039                     {
1040                         // the point does not project fractionally to this lane, it has to be up- or downstream of the lane
1041 
1042                         // simple heuristic to decide if we first look upstream or downstream
1043                         boolean upstream = this.fractionalLinkPositions.get(lane.getParentLink()) < 0.0 ? true : false;
1044 
1045                         // use loop up to 2 times (for loop creates 'loc not initialized' warning)
1046                         int i = 0;
1047                         while (true)
1048                         {
1049                             Set<Lane> otherLanesToConsider = new LinkedHashSet<>();
1050                             otherLanesToConsider.addAll(this.currentLanes.keySet());
1051                             double distance = getDistanceAtOtherLane(lane, when, upstream, 0.0, p, otherLanesToConsider);
1052                             // distance can be positive on an upstream lane due to a loop
1053                             if (!Double.isNaN(distance))
1054                             {
1055                                 if (i == 1 && !Double.isNaN(loc))
1056                                 {
1057                                     // loc was determined in both loops, this constitutes a lane-loop, select nearest
1058                                     double loc2 = upstream ? -distance : distance + lane.getLength().si;
1059                                     double d1 = loc < 0.0 ? -loc : loc - lane.getLength().si;
1060                                     double d2 = loc2 < 0.0 ? -loc2 : loc2 - lane.getLength().si;
1061                                     loc = d1 < d2 ? loc : loc2;
1062                                     break;
1063                                 }
1064                                 else
1065                                 {
1066                                     // loc was determined in second loop
1067                                     loc = upstream ? -distance : distance + lane.getLength().si;
1068                                 }
1069                             }
1070                             else if (!Double.isNaN(loc))
1071                             {
1072                                 // loc was determined in first loop
1073                                 break;
1074                             }
1075                             else if (i == 1)
1076                             {
1077                                 // loc was determined in neither loop
1078                                 // Lane change ended while moving to next link. The source lanes are left and for a leave-lane
1079                                 // event the position is required. This may depend on upstream or downstream lanes as the
1080                                 // reference position is projected to that lane. But if we already left that lane, we can't use
1081                                 // it. We thus use ENDPOINT fallback instead.
1082                                 loc = lane.getLength().si * lane.getCenterLine().projectFractional(null, null, p.x, p.y,
1083                                         FractionalFallback.ENDPOINT);
1084                                 break;
1085                             }
1086                             // try other direction
1087                             i++;
1088                             upstream = !upstream;
1089                         }
1090                     }
1091                 }
1092             }
1093             catch (NullPointerException e)
1094             {
1095                 throw new GTUException("lanesCurrentOperationalPlan or fractionalLinkPositions is null", e);
1096             }
1097             catch (Exception e)
1098             {
1099                 System.err.println(toString());
1100                 System.err.println(this.currentLanes.get(when));
1101                 System.err.println(this.fractionalLinkPositions.get(when));
1102                 throw new GTUException(e);
1103             }
1104             if (Double.isNaN(loc))
1105             {
1106                 System.out.println("loc is NaN");
1107             }
1108             Length length = Length.instantiateSI(loc);
1109             if (CACHING)
1110             {
1111                 this.cachedPositions.put(cacheIndex, length);
1112             }
1113             return length;
1114         }
1115     }
1116 
1117     /** Set of lane to attempt when determining the location with a deviative lane change. */
1118     // private Set<Lane> otherLanesToConsider;
1119 
1120     /**
1121      * In case of a deviative operational plan (not on the center lines), positions are projected fractionally to the center
1122      * lines. For points upstream or downstream of a lane, fractional projection is not valid. In such cases we need to project
1123      * the position to an upstream or downstream lane instead, and adjust length along the center lines.
1124      * @param lane Lane; lane to determine the position on
1125      * @param when Time; time
1126      * @param upstream boolean; whether to check upstream (or downstream)
1127      * @param distance double; cumulative distance in recursive search, starts at 0.0
1128      * @param point DirectedPoint; absolute point of GTU to be projected to center line
1129      * @param otherLanesToConsider Set&lt;Lane&gt;; lanes to consider
1130      * @return Length; position on lane being &lt;0 or &gt;{@code lane.getLength()}
1131      * @throws GTUException if GTU is not on the lane
1132      */
1133     private double getDistanceAtOtherLane(final Lane lane, final Time when, final boolean upstream, final double distance,
1134             final DirectedPoint point, final Set<Lane> otherLanesToConsider) throws GTUException
1135     {
1136         Set<Lane> nextLanes = new LinkedHashSet<>(upstream == getDirection(lane).isPlus()
1137                 ? lane.prevLanes(getGTUType()).keySet() : lane.nextLanes(getGTUType()).keySet()); // safe copy
1138         nextLanes.retainAll(otherLanesToConsider); // as we delete here
1139         if (!upstream && nextLanes.size() > 1)
1140         {
1141             LaneDirectionane/LaneDirection.html#LaneDirection">LaneDirection laneDir = new LaneDirection(lane, getDirection(lane)).getNextLaneDirection(this);
1142             if (nextLanes.contains(laneDir.getLane()))
1143             {
1144                 nextLanes.clear();
1145                 nextLanes.add(laneDir.getLane());
1146             }
1147             else
1148             {
1149                 getSimulator().getLogger().always().error("Distance on downstream lane could not be determined.");
1150             }
1151         }
1152         // TODO When requesting the position at the end of the plan, which will be on a further lane, this lane is not yet
1153         // part of the lanes in the current operational plan. This can be upstream or downstream depending on the direction of
1154         // travel. We might check whether getDirection(lane)=DIR_PLUS and upstream=false, or getDirection(lane)=DIR_MINUS and
1155         // upstream=true, to then use LaneDirection.getNextLaneDirection(this) to obtain the next lane. This is only required if
1156         // nextLanes originally had more than 1 lane, otherwise we can simply use that one lane. Problem is that the search
1157         // might go on far or even eternally (on a circular network), as projection simply keeps failing because the GTU is
1158         // actually towards the other longitudinal direction. Hence, the heuristic used before this method is called should
1159         // change and first always search against the direction of travel, and only consider lanes in currentLanes, while the
1160         // consecutive search in the direction of travel should then always find a point. We could build in a counter to prevent
1161         // a hanging software.
1162         if (nextLanes.size() == 0)
1163         {
1164             return Double.NaN; // point must be in the other direction
1165         }
1166         Throw.when(nextLanes.size() > 1, IllegalStateException.class,
1167                 "A position (%s) of GTU %s is not on any of the current registered lanes.", point, this.getId());
1168         Lane nextLane = nextLanes.iterator().next();
1169         otherLanesToConsider.remove(lane);
1170         double f = nextLane.getCenterLine().projectFractional(null, null, point.x, point.y, FractionalFallback.NaN);
1171         if (Double.isNaN(f))
1172         {
1173             return getDistanceAtOtherLane(nextLane, when, upstream, distance + nextLane.getLength().si, point,
1174                     otherLanesToConsider);
1175         }
1176         return distance + (upstream == this.currentLanes.get(nextLane).isPlus() ? 1.0 - f : f) * nextLane.getLength().si;
1177     }
1178 
1179     /** Time of reference position cache. */
1180     private double referencePositionTime = Double.NaN;
1181 
1182     /** Cached reference position. */
1183     private DirectedLanePosition cachedReferencePosition = null;
1184 
1185     /** {@inheritDoc} */
1186     @Override
1187     @SuppressWarnings("checkstyle:designforextension")
1188     public DirectedLanePosition getReferencePosition() throws GTUException
1189     {
1190         if (this.referencePositionTime == getSimulator().getSimulatorTime().si)
1191         {
1192             return this.cachedReferencePosition;
1193         }
1194         boolean anyOnLink = false;
1195         Lane refLane = null;
1196         double closest = Double.POSITIVE_INFINITY;
1197         double minEps = Double.POSITIVE_INFINITY;
1198         for (Lane lane : this.currentLanes.keySet())
1199         {
1200             double fraction = fractionalPosition(lane, getReference());
1201             if (fraction >= 0.0 && fraction <= 1.0)
1202             {
1203                 // TODO widest lane in case we are registered on more than one lane with the reference point?
1204                 // TODO lane that leads to our location or not if we are registered on parallel lanes?
1205                 if (!anyOnLink)
1206                 {
1207                     refLane = lane;
1208                 }
1209                 else
1210                 {
1211                     DirectedPoint loc = getLocation();
1212                     double f = lane.getCenterLine().projectFractional(null, null, loc.x, loc.y, FractionalFallback.ENDPOINT);
1213                     double distance = loc.distance(lane.getCenterLine().getLocationFractionExtended(f));
1214                     if (refLane != null && Double.isInfinite(closest))
1215                     {
1216                         f = refLane.getCenterLine().projectFractional(null, null, loc.x, loc.y, FractionalFallback.ENDPOINT);
1217                         closest = loc.distance(refLane.getCenterLine().getLocationFractionExtended(f));
1218                     }
1219                     if (distance < closest)
1220                     {
1221                         refLane = lane;
1222                         closest = distance;
1223                     }
1224                 }
1225                 anyOnLink = true;
1226             }
1227             else if (!anyOnLink && Double.isInfinite(closest))// && getOperationalPlan() instanceof LaneBasedOperationalPlan
1228             // && ((LaneBasedOperationalPlan) getOperationalPlan()).isDeviative())
1229             {
1230                 double eps = (fraction > 1.0 ? lane.getCenterLine().getLast() : lane.getCenterLine().getFirst())
1231                         .distanceSI(new OTSPoint3D(getLocation()));
1232                 if (eps < minEps)
1233                 {
1234                     minEps = eps;
1235                     refLane = lane;
1236                 }
1237             }
1238         }
1239         if (refLane != null)
1240         {
1241             this.cachedReferencePosition =
1242                     new DirectedLanePosition(refLane, position(refLane, getReference()), this.getDirection(refLane));
1243             this.referencePositionTime = getSimulator().getSimulatorTime().si;
1244             return this.cachedReferencePosition;
1245         }
1246         // for (Lane lane : this.currentLanes.keySet())
1247         // {
1248         // Length relativePosition = position(lane, RelativePosition.REFERENCE_POSITION);
1249         // System.err
1250         // .println(String.format("Lane %s of Link %s: absolute position %s, relative position %5.1f%%", lane.getId(),
1251         // lane.getParentLink().getId(), relativePosition, relativePosition.si * 100 / lane.getLength().si));
1252         // }
1253         throw new GTUException("The reference point of GTU " + this + " is not on any of the lanes on which it is registered");
1254     }
1255 
1256     /**
1257      * Schedule the triggers for this GTU that are going to happen until the next evaluation time. Also schedule the
1258      * registration and deregistration of lanes when the vehicle enters or leaves them, at the exact right time. <br>
1259      * Note: when the GTU makes a lane change, the vehicle will be registered for both lanes during the entire maneuver.
1260      * @throws NetworkException on network inconsistency
1261      * @throws SimRuntimeException should never happen
1262      * @throws GTUException when a branch is reached where the GTU does not know where to go next
1263      */
1264     @SuppressWarnings("checkstyle:designforextension")
1265     protected void scheduleEnterLeaveTriggers() throws NetworkException, SimRuntimeException, GTUException
1266     {
1267 
1268         LaneBasedOperationalPlan plan = null;
1269         double moveSI;
1270         if (getOperationalPlan() instanceof LaneBasedOperationalPlan)
1271         {
1272             plan = (LaneBasedOperationalPlan) getOperationalPlan();
1273             moveSI = plan.getTotalLengthAlongLane(this).si;
1274         }
1275         else
1276         {
1277             moveSI = getOperationalPlan().getTotalLength().si;
1278         }
1279 
1280         // Figure out which lanes this GTU will enter with its FRONT, and schedule the lane enter events
1281         Map<Lane, GTUDirectionality> lanesCopy = new LinkedHashMap<>(this.currentLanes);
1282         Iterator<Lane> it = lanesCopy.keySet().iterator();
1283         Lane enteredLane = null;
1284         // LateralDirectionality forceSide = LateralDirectionality.NONE;
1285         while (it.hasNext() || enteredLane != null) // use a copy because this.currentLanes can change
1286         {
1287             // next lane from 'lanesCopy', or asses the lane we just entered as it may be very short and also passed fully
1288             Lane lane;
1289             GTUDirectionality laneDir;
1290             if (enteredLane == null)
1291             {
1292                 lane = it.next();
1293                 laneDir = lanesCopy.get(lane);
1294             }
1295             else
1296             {
1297                 lane = enteredLane;
1298                 laneDir = this.currentLanes.get(lane);
1299             }
1300             double sign = laneDir.isPlus() ? 1.0 : -1.0;
1301             enteredLane = null;
1302 
1303             // skip if already on next lane
1304             if (!Collections.disjoint(this.currentLanes.keySet(),
1305                     lane.downstreamLanes(laneDir, getGTUType()).keySet().toCollection()))
1306             {
1307                 continue;
1308             }
1309 
1310             // schedule triggers on this lane
1311             double referenceStartSI = this.fractionalLinkPositions.get(lane.getParentLink()) * lane.getLength().getSI();
1312             // referenceStartSI is position of reference of GTU on current lane
1313             if (laneDir.isPlus())
1314             {
1315                 lane.scheduleSensorTriggers(this, referenceStartSI, moveSI);
1316             }
1317             else
1318             {
1319                 lane.scheduleSensorTriggers(this, referenceStartSI - moveSI, moveSI);
1320             }
1321 
1322             double nextFrontPosSI = referenceStartSI + sign * (moveSI + getFront().getDx().si);
1323             Lane nextLane = null;
1324             GTUDirectionality nextDirection = null;
1325             Length refPosAtLastTimestep = null;
1326             DirectedPoint end = null;
1327             if (laneDir.isPlus() ? nextFrontPosSI > lane.getLength().si : nextFrontPosSI < 0.0)
1328             {
1329                 LaneDirectionk/lane/LaneDirection.html#LaneDirection">LaneDirection next = new LaneDirection(lane, laneDir).getNextLaneDirection(this);
1330                 if (null == next)
1331                 {
1332                     // A sink should delete the GTU, or a lane change should end, before reaching the end of the lane
1333                     continue;
1334                 }
1335                 nextLane = next.getLane();
1336                 nextDirection = next.getDirection();
1337                 double endPos = laneDir.isPlus() ? lane.getLength().si - getFront().getDx().si : getFront().getDx().si;
1338                 Lane endLane = lane;
1339                 GTUDirectionality endLaneDir = laneDir;
1340                 while (endLaneDir.isPlus() ? endPos < 0.0 : endPos > endLane.getLength().si)
1341                 {
1342                     // GTU front is more than lane length, so end location can not be extracted from the lane, let's move then
1343                     Map<Lane, GTUDirectionality> map = endLane.upstreamLanes(endLaneDir, getGTUType()).toMap();
1344                     map.keySet().retainAll(this.currentLanes.keySet());
1345                     double remain = endLaneDir.isPlus() ? -endPos : endPos - endLane.getLength().si;
1346                     endLane = map.keySet().iterator().next();
1347                     endLaneDir = map.get(endLane);
1348                     endPos = endLaneDir.isPlus() ? endLane.getLength().si - remain : remain;
1349                 }
1350                 end = endLane.getCenterLine().getLocationExtendedSI(endPos);
1351                 if (laneDir.isPlus())
1352                 {
1353                     refPosAtLastTimestep = nextDirection.isPlus() ? Length.instantiateSI(referenceStartSI - lane.getLength().si)
1354                             : Length.instantiateSI(nextLane.getLength().si - referenceStartSI + lane.getLength().si);
1355                 }
1356                 else
1357                 {
1358                     refPosAtLastTimestep = nextDirection.isPlus() ? Length.instantiateSI(-referenceStartSI)
1359                             : Length.instantiateSI(nextLane.getLength().si + referenceStartSI);
1360                 }
1361             }
1362 
1363             if (end != null)
1364             {
1365                 Time enterTime = getOperationalPlan().timeAtPoint(end, false);
1366                 if (enterTime != null)
1367                 {
1368                     if (Double.isNaN(enterTime.si))
1369                     {
1370                         // TODO: this escape was in timeAtPoint, where it was changed to return null for leave lane events
1371                         enterTime = Time.instantiateSI(getOperationalPlan().getEndTime().si - 1e-9);
1372                         // -1e-9 prevents that next move() reschedules enter
1373                     }
1374                     addLaneToGtu(nextLane, refPosAtLastTimestep, nextDirection);
1375                     enteredLane = nextLane;
1376                     Length coveredDistance;
1377                     if (plan == null || !plan.isDeviative())
1378                     {
1379                         try
1380                         {
1381                             coveredDistance = getOperationalPlan().getTraveledDistance(enterTime);
1382                         }
1383                         catch (OperationalPlanException exception)
1384                         {
1385                             throw new RuntimeException("Enter time of lane beyond plan.", exception);
1386                         }
1387                     }
1388                     else
1389                     {
1390                         coveredDistance = plan.getDistanceAlongLane(this, end);
1391                     }
1392                     SimEventInterface<SimTimeDoubleUnit> event = getSimulator().scheduleEventAbs(enterTime, this, this,
1393                             "addGtuToLane", new Object[] { nextLane, refPosAtLastTimestep.plus(coveredDistance) });
1394                     addEnterTrigger(nextLane, event);
1395                 }
1396             }
1397         }
1398 
1399         // Figure out which lanes this GTU will exit with its BACK, and schedule the lane exit events
1400         for (Lane lane : this.currentLanes.keySet())
1401         {
1402 
1403             double referenceStartSI = this.fractionalLinkPositions.get(lane.getParentLink()) * lane.getLength().getSI();
1404             Time exitTime = null;
1405 
1406             GTUDirectionality laneDir = getDirection(lane);
1407 
1408             if (plan == null || !plan.isDeviative())
1409             {
1410                 double sign = laneDir.isPlus() ? 1.0 : -1.0;
1411                 double nextRearPosSI = referenceStartSI + sign * (getRear().getDx().si + moveSI);
1412                 if (laneDir.isPlus() ? nextRearPosSI > lane.getLength().si : nextRearPosSI < 0.0)
1413                 {
1414                     exitTime = getOperationalPlan().timeAtDistance(
1415                             Length.instantiateSI((laneDir.isPlus() ? lane.getLength().si - referenceStartSI : referenceStartSI)
1416                                     - getRear().getDx().si));
1417                 }
1418             }
1419             else
1420             {
1421                 DirectedPoint end = null;
1422                 double endPos = laneDir.isPlus() ? lane.getLength().si - getRear().getDx().si : getRear().getDx().si;
1423                 Lane endLane = lane;
1424                 GTUDirectionality endLaneDir = laneDir;
1425                 while (endLaneDir.isPlus() ? endPos > endLane.getLength().si : endPos < 0.0)
1426                 {
1427                     Map<Lane, GTUDirectionality> map = endLane.downstreamLanes(endLaneDir, getGTUType()).toMap();
1428                     map.keySet().retainAll(this.currentLanes.keySet());
1429                     if (!map.isEmpty())
1430                     {
1431                         double remain = endLaneDir.isPlus() ? endPos - endLane.getLength().si : -endPos;
1432                         endLane = map.keySet().iterator().next();
1433                         endLaneDir = map.get(endLane);
1434                         endPos = endLaneDir.isPlus() ? remain : endLane.getLength().si - remain;
1435                     }
1436                     else
1437                     {
1438                         endPos = endLaneDir.isPlus() ? endLane.getLength().si - getRear().getDx().si : getRear().getDx().si;
1439                         break;
1440                     }
1441                 }
1442                 end = endLane.getCenterLine().getLocationExtendedSI(endPos);
1443                 if (end != null)
1444                 {
1445                     exitTime = getOperationalPlan().timeAtPoint(end, false);
1446                     if (Double.isNaN(exitTime.si))
1447                     {
1448                         // code below will leave entered lanes if exitTime is null, make this so if NaN results due to the lane
1449                         // end being beyond the plan (rather than the GTU never having been there, but being registered there
1450                         // upon lane change initiation)
1451                         double sign = laneDir.isPlus() ? 1.0 : -1.0;
1452                         double nextRearPosSI = referenceStartSI + sign * (getRear().getDx().si + moveSI);
1453                         if (laneDir.isPlus() ? nextRearPosSI < lane.getLength().si : nextRearPosSI > 0.0)
1454                         {
1455                             exitTime = null;
1456                         }
1457                     }
1458                 }
1459             }
1460 
1461             if (exitTime != null && !Double.isNaN(exitTime.si))
1462             {
1463                 SimEvent<SimTimeDoubleUnit> event = new SimEvent<>(new SimTimeDoubleUnit(exitTime), this, this, "leaveLane",
1464                         new Object[] { lane, new Boolean(false) });
1465                 getSimulator().scheduleEvent(event);
1466                 addTrigger(lane, event);
1467             }
1468             else if (exitTime != null && this.enteredLanes.contains(lane))
1469             {
1470                 // This lane was entered when initiating the lane change due to a fractional calculation. Now, the deviative
1471                 // plan indicates we will never reach this location.
1472                 SimEvent<SimTimeDoubleUnit> event = new SimEvent<>(getSimulator().getSimTime(), this, this, "leaveLane",
1473                         new Object[] { lane, new Boolean(false) });
1474                 getSimulator().scheduleEvent(event);
1475                 addTrigger(lane, event);
1476             }
1477         }
1478 
1479         this.enteredLanes.clear();
1480     }
1481 
1482     /** {@inheritDoc} */
1483     @Override
1484     public final Map<Lane, Double> fractionalPositions(final RelativePosition relativePosition) throws GTUException
1485     {
1486         return fractionalPositions(relativePosition, getSimulator().getSimulatorTime());
1487     }
1488 
1489     /** {@inheritDoc} */
1490     @Override
1491     public final Map<Lane, Double> fractionalPositions(final RelativePosition relativePosition, final Time when)
1492             throws GTUException
1493     {
1494         Map<Lane, Double> positions = new LinkedHashMap<>();
1495         for (Lane lane : this.currentLanes.keySet())
1496         {
1497             positions.put(lane, fractionalPosition(lane, relativePosition, when));
1498         }
1499         return positions;
1500     }
1501 
1502     /** {@inheritDoc} */
1503     @Override
1504     public final double fractionalPosition(final Lane lane, final RelativePosition relativePosition, final Time when)
1505             throws GTUException
1506     {
1507         return position(lane, relativePosition, when).getSI() / lane.getLength().getSI();
1508     }
1509 
1510     /** {@inheritDoc} */
1511     @Override
1512     public final double fractionalPosition(final Lane lane, final RelativePosition relativePosition) throws GTUException
1513     {
1514         return position(lane, relativePosition).getSI() / lane.getLength().getSI();
1515     }
1516 
1517     /** {@inheritDoc} */
1518     @Override
1519     public final void addTrigger(final Lane lane, final SimEventInterface<SimTimeDoubleUnit> event)
1520     {
1521         List<SimEventInterface<SimTimeDoubleUnit>> list = this.pendingLeaveTriggers.get(lane);
1522         if (null == list)
1523         {
1524             list = new ArrayList<>();
1525         }
1526         list.add(event);
1527         this.pendingLeaveTriggers.put(lane, list);
1528     }
1529 
1530     /**
1531      * Add enter trigger.
1532      * @param lane Lane; lane
1533      * @param event SimEventInterface&lt;SimTimeDoubleUnit&gt;; event
1534      */
1535     private void addEnterTrigger(final Lane lane, final SimEventInterface<SimTimeDoubleUnit> event)
1536     {
1537         List<SimEventInterface<SimTimeDoubleUnit>> list = this.pendingEnterTriggers.get(lane);
1538         if (null == list)
1539         {
1540             list = new ArrayList<>();
1541         }
1542         list.add(event);
1543         this.pendingEnterTriggers.put(lane, list);
1544     }
1545 
1546     /**
1547      * Sets a vehicle model.
1548      * @param vehicleModel VehicleModel; vehicle model
1549      */
1550     public void setVehicleModel(final VehicleModel vehicleModel)
1551     {
1552         this.vehicleModel = vehicleModel;
1553     }
1554 
1555     /** {@inheritDoc} */
1556     @Override
1557     public VehicleModel getVehicleModel()
1558     {
1559         return this.vehicleModel;
1560     }
1561 
1562     /** {@inheritDoc} */
1563     @Override
1564     @SuppressWarnings("checkstyle:designforextension")
1565     public void destroy()
1566     {
1567         DirectedLanePosition dlp = null;
1568         try
1569         {
1570             dlp = getReferencePosition();
1571         }
1572         catch (GTUException e)
1573         {
1574             // ignore. not important at destroy
1575         }
1576         DirectedPoint location = this.getOperationalPlan() == null ? new DirectedPoint(0.0, 0.0, 0.0) : getLocation();
1577 
1578         synchronized (this.lock)
1579         {
1580             Set<Lane> laneSet = new LinkedHashSet<>(this.currentLanes.keySet()); // Operate on a copy of the key
1581                                                                                  // set
1582             for (Lane lane : laneSet)
1583             {
1584                 try
1585                 {
1586                     leaveLane(lane, true);
1587                 }
1588                 catch (GTUException e)
1589                 {
1590                     // ignore. not important at destroy
1591                 }
1592             }
1593         }
1594 
1595         if (dlp != null && dlp.getLane() != null)
1596         {
1597             Lane referenceLane = dlp.getLane();
1598             fireTimedEvent(LaneBasedGTU.LANEBASED_DESTROY_EVENT,
1599                     new Object[] { getId(), location, getOdometer(), referenceLane, dlp.getPosition(), dlp.getGtuDirection() },
1600                     getSimulator().getSimulatorTime());
1601         }
1602         else
1603         {
1604             fireTimedEvent(LaneBasedGTU.LANEBASED_DESTROY_EVENT,
1605                     new Object[] { getId(), location, getOdometer(), null, Length.ZERO, null },
1606                     getSimulator().getSimulatorTime());
1607         }
1608         if (this.finalizeLaneChangeEvent != null)
1609         {
1610             getSimulator().cancelEvent(this.finalizeLaneChangeEvent);
1611         }
1612 
1613         super.destroy();
1614     }
1615 
1616     /** {@inheritDoc} */
1617     @Override
1618     public final Bounds getBounds()
1619     {
1620         double dx = 0.5 * getLength().doubleValue();
1621         double dy = 0.5 * getWidth().doubleValue();
1622         return new Bounds(new Point3d(-dx, -dy, 0.0), new Point3d(dx, dy, 0.0));
1623     }
1624 
1625     /** {@inheritDoc} */
1626     @Override
1627     public final LaneBasedStrategicalPlanner getStrategicalPlanner()
1628     {
1629         return (LaneBasedStrategicalPlanner) super.getStrategicalPlanner();
1630     }
1631 
1632     /** {@inheritDoc} */
1633     @Override
1634     public final LaneBasedStrategicalPlanner getStrategicalPlanner(final Time time)
1635     {
1636         return (LaneBasedStrategicalPlanner) super.getStrategicalPlanner(time);
1637     }
1638 
1639     /** {@inheritDoc} */
1640     @Override
1641     public RoadNetwork getNetwork()
1642     {
1643         return (RoadNetwork) super.getPerceivableContext();
1644     }
1645 
1646     /** {@inheritDoc} */
1647     @Override
1648     public Speed getDesiredSpeed()
1649     {
1650         Time simTime = getSimulator().getSimulatorTime();
1651         if (this.desiredSpeedTime == null || this.desiredSpeedTime.si < simTime.si)
1652         {
1653             InfrastructurePerception infra =
1654                     getTacticalPlanner().getPerception().getPerceptionCategoryOrNull(InfrastructurePerception.class);
1655             SpeedLimitInfo speedInfo;
1656             if (infra == null)
1657             {
1658                 speedInfo = new SpeedLimitInfo();
1659                 speedInfo.addSpeedInfo(SpeedLimitTypes.MAX_VEHICLE_SPEED, getMaximumSpeed());
1660             }
1661             else
1662             {
1663                 // Throw.whenNull(infra, "InfrastructurePerception is required to determine the desired speed.");
1664                 speedInfo = infra.getSpeedLimitProspect(RelativeLane.CURRENT).getSpeedLimitInfo(Length.ZERO);
1665             }
1666             this.cachedDesiredSpeed =
1667                     Try.assign(() -> getTacticalPlanner().getCarFollowingModel().desiredSpeed(getParameters(), speedInfo),
1668                             "Parameter exception while obtaining the desired speed.");
1669             this.desiredSpeedTime = simTime;
1670         }
1671         return this.cachedDesiredSpeed;
1672     }
1673 
1674     /** {@inheritDoc} */
1675     @Override
1676     public Acceleration getCarFollowingAcceleration()
1677     {
1678         Time simTime = getSimulator().getSimulatorTime();
1679         if (this.carFollowingAccelerationTime == null || this.carFollowingAccelerationTime.si < simTime.si)
1680         {
1681             LanePerception perception = getTacticalPlanner().getPerception();
1682             // speed
1683             EgoPerception<?, ?> ego = perception.getPerceptionCategoryOrNull(EgoPerception.class);
1684             Throw.whenNull(ego, "EgoPerception is required to determine the speed.");
1685             Speed speed = ego.getSpeed();
1686             // speed limit info
1687             InfrastructurePerception infra = perception.getPerceptionCategoryOrNull(InfrastructurePerception.class);
1688             Throw.whenNull(infra, "InfrastructurePerception is required to determine the desired speed.");
1689             SpeedLimitInfo speedInfo = infra.getSpeedLimitProspect(RelativeLane.CURRENT).getSpeedLimitInfo(Length.ZERO);
1690             // leaders
1691             NeighborsPerception neighbors = perception.getPerceptionCategoryOrNull(NeighborsPerception.class);
1692             Throw.whenNull(neighbors, "NeighborsPerception is required to determine the car-following acceleration.");
1693             PerceptionCollectable<HeadwayGTU, LaneBasedGTU> leaders = neighbors.getLeaders(RelativeLane.CURRENT);
1694             // obtain
1695             this.cachedCarFollowingAcceleration =
1696                     Try.assign(() -> getTacticalPlanner().getCarFollowingModel().followingAcceleration(getParameters(), speed,
1697                             speedInfo, leaders), "Parameter exception while obtaining the desired speed.");
1698             this.carFollowingAccelerationTime = simTime;
1699         }
1700         return this.cachedCarFollowingAcceleration;
1701     }
1702 
1703     /** {@inheritDoc} */
1704     @Override
1705     public final TurnIndicatorStatus getTurnIndicatorStatus()
1706     {
1707         return this.turnIndicatorStatus.get();
1708     }
1709 
1710     /** {@inheritDoc} */
1711     @Override
1712     public final TurnIndicatorStatus getTurnIndicatorStatus(final Time time)
1713     {
1714         return this.turnIndicatorStatus.get(time);
1715     }
1716 
1717     /** {@inheritDoc} */
1718     @Override
1719     public final void setTurnIndicatorStatus(final TurnIndicatorStatus turnIndicatorStatus)
1720     {
1721         this.turnIndicatorStatus.set(turnIndicatorStatus);
1722     }
1723 
1724     /** {@inheritDoc} */
1725     @Override
1726     @SuppressWarnings("checkstyle:designforextension")
1727     public String toString()
1728     {
1729         return String.format("GTU " + getId());
1730     }
1731 
1732 }