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