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