View Javadoc
1   package org.opentrafficsim.road.gtu.lane;
2   
3   import java.util.ArrayList;
4   import java.util.Collections;
5   import java.util.Comparator;
6   import java.util.LinkedHashMap;
7   import java.util.LinkedHashSet;
8   import java.util.List;
9   import java.util.Map;
10  import java.util.Map.Entry;
11  import java.util.Set;
12  import java.util.SortedMap;
13  
14  import org.djunits.unit.DirectionUnit;
15  import org.djunits.unit.DurationUnit;
16  import org.djunits.unit.LengthUnit;
17  import org.djunits.unit.PositionUnit;
18  import org.djunits.value.vdouble.scalar.Acceleration;
19  import org.djunits.value.vdouble.scalar.Duration;
20  import org.djunits.value.vdouble.scalar.Length;
21  import org.djunits.value.vdouble.scalar.Speed;
22  import org.djunits.value.vdouble.scalar.Time;
23  import org.djutils.draw.point.Point3d;
24  import org.djutils.exceptions.Throw;
25  import org.djutils.exceptions.Try;
26  import org.djutils.immutablecollections.ImmutableMap;
27  import org.djutils.logger.CategoryLogger;
28  import org.djutils.multikeymap.MultiKeyMap;
29  import org.opentrafficsim.base.parameters.ParameterException;
30  import org.opentrafficsim.core.dsol.OTSSimulatorInterface;
31  import org.opentrafficsim.core.geometry.Bounds;
32  import org.opentrafficsim.core.geometry.DirectedPoint;
33  import org.opentrafficsim.core.geometry.OTSGeometryException;
34  import org.opentrafficsim.core.geometry.OTSLine3D;
35  import org.opentrafficsim.core.geometry.OTSLine3D.FractionalFallback;
36  import org.opentrafficsim.core.geometry.OTSPoint3D;
37  import org.opentrafficsim.core.gtu.AbstractGTU;
38  import org.opentrafficsim.core.gtu.GTU;
39  import org.opentrafficsim.core.gtu.GTUDirectionality;
40  import org.opentrafficsim.core.gtu.GTUException;
41  import org.opentrafficsim.core.gtu.GTUType;
42  import org.opentrafficsim.core.gtu.RelativePosition;
43  import org.opentrafficsim.core.gtu.TurnIndicatorStatus;
44  import org.opentrafficsim.core.gtu.perception.EgoPerception;
45  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlan;
46  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlanBuilder;
47  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlanException;
48  import org.opentrafficsim.core.network.LateralDirectionality;
49  import org.opentrafficsim.core.network.NetworkException;
50  import org.opentrafficsim.core.perception.Historical;
51  import org.opentrafficsim.core.perception.HistoricalValue;
52  import org.opentrafficsim.core.perception.HistoryManager;
53  import org.opentrafficsim.core.perception.collections.HistoricalArrayList;
54  import org.opentrafficsim.core.perception.collections.HistoricalList;
55  import org.opentrafficsim.road.gtu.lane.perception.LanePerception;
56  import org.opentrafficsim.road.gtu.lane.perception.PerceptionCollectable;
57  import org.opentrafficsim.road.gtu.lane.perception.RelativeLane;
58  import org.opentrafficsim.road.gtu.lane.perception.categories.InfrastructurePerception;
59  import org.opentrafficsim.road.gtu.lane.perception.categories.neighbors.NeighborsPerception;
60  import org.opentrafficsim.road.gtu.lane.perception.headway.HeadwayGTU;
61  import org.opentrafficsim.road.gtu.lane.plan.operational.LaneBasedOperationalPlan;
62  import org.opentrafficsim.road.gtu.strategical.LaneBasedStrategicalPlanner;
63  import org.opentrafficsim.road.network.OTSRoadNetwork;
64  import org.opentrafficsim.road.network.RoadNetwork;
65  import org.opentrafficsim.road.network.lane.CrossSectionLink;
66  import org.opentrafficsim.road.network.lane.DirectedLanePosition;
67  import org.opentrafficsim.road.network.lane.Lane;
68  import org.opentrafficsim.road.network.lane.LaneDirection;
69  import org.opentrafficsim.road.network.lane.object.sensor.SingleSensor;
70  import org.opentrafficsim.road.network.speed.SpeedLimitInfo;
71  import org.opentrafficsim.road.network.speed.SpeedLimitTypes;
72  
73  import nl.tudelft.simulation.dsol.SimRuntimeException;
74  import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEventInterface;
75  
76  /**
77   * This class contains most of the code that is needed to run a lane based GTU. <br>
78   * The starting point of a LaneBasedTU is that it can be in <b>multiple lanes</b> at the same time. This can be due to a lane
79   * change (lateral), or due to crossing a link (front of the GTU is on another Lane than rear of the GTU). If a Lane is shorter
80   * than the length of the GTU (e.g. when we do node expansion on a crossing, this is very well possible), a GTU could occupy
81   * dozens of Lanes at the same time.
82   * <p>
83   * When calculating a headway, the GTU has to look in successive lanes. When Lanes (or underlying CrossSectionLinks) diverge,
84   * the headway algorithms have to look at multiple Lanes and return the minimum headway in each of the Lanes. When the Lanes (or
85   * underlying CrossSectionLinks) converge, "parallel" traffic is not taken into account in the headway calculation. Instead, gap
86   * acceptance algorithms or their equivalent should guide the merging behavior.
87   * <p>
88   * To decide its movement, an AbstractLaneBasedGTU applies its car following algorithm and lane change algorithm to set the
89   * acceleration and any lane change operation to perform. It then schedules the triggers that will add it to subsequent lanes
90   * and remove it from current lanes as needed during the time step that is has committed to. Finally, it re-schedules its next
91   * movement evaluation with the simulator.
92   * <p>
93   * Copyright (c) 2013-2022 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
94   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
95   * <p>
96   * @version $Revision: 1408 $, $LastChangedDate: 2015-09-24 15:17:25 +0200 (Thu, 24 Sep 2015) $, by $Author: pknoppers $,
97   *          initial version Oct 22, 2014 <br>
98   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
99   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
100  */
101 public abstract class AbstractLaneBasedGTU2 extends AbstractGTU implements LaneBasedGTU
102 {
103     /** */
104     private static final long serialVersionUID = 20140822L;
105 
106     /** Lanes. */
107     private final HistoricalList<CrossSection> crossSections;
108 
109     /** Reference lane index (0 = left or only lane, 1 = right lane). */
110     private int referenceLaneIndex = 0;
111 
112     /** Time of reference position cache. */
113     private double referencePositionTime = Double.NaN;
114 
115     /** Cached reference position. */
116     private DirectedLanePosition cachedReferencePosition = null;
117 
118     /** Pending leave triggers for each lane. */
119     private SimEventInterface<Duration> pendingLeaveTrigger;
120 
121     /** Pending enter triggers for each lane. */
122     private SimEventInterface<Duration> pendingEnterTrigger;
123 
124     /** Event to finalize lane change. */
125     private SimEventInterface<Duration> finalizeLaneChangeEvent;
126 
127     /** Sensor events. */
128     private Set<SimEventInterface<Duration>> sensorEvents = new LinkedHashSet<>();
129 
130     /** Cached desired speed. */
131     private Speed cachedDesiredSpeed;
132 
133     /** Time desired speed was cached. */
134     private Time desiredSpeedTime;
135 
136     /** Cached car-following acceleration. */
137     private Acceleration cachedCarFollowingAcceleration;
138 
139     /** Time car-following acceleration was cached. */
140     private Time carFollowingAccelerationTime;
141 
142     /** The object to lock to make the GTU thread safe. */
143     private Object lock = new Object();
144 
145     /** The threshold distance for differences between initial locations of the GTU on different lanes. */
146     @SuppressWarnings("checkstyle:visibilitymodifier")
147     public static Length initialLocationThresholdDifference = new Length(1.0, LengthUnit.MILLIMETER);
148 
149     /** Margin to add to plan to check of the path will enter the next section. */
150     public static Length eventMargin = Length.instantiateSI(50.0);
151 
152     /** Turn indicator status. */
153     private final Historical<TurnIndicatorStatus> turnIndicatorStatus;
154 
155     /** Caching on or off. */
156     // TODO: should be indicated with a Parameter
157     public static boolean CACHING = true;
158 
159     /** cached position count. */
160     // TODO: can be removed after testing period
161     public static int CACHED_POSITION = 0;
162 
163     /** cached position count. */
164     // TODO: can be removed after testing period
165     public static int NON_CACHED_POSITION = 0;
166 
167     /** Vehicle model. */
168     private VehicleModel vehicleModel = VehicleModel.MINMAX;
169 
170     /** Whether the GTU perform lane changes instantaneously or not. */
171     private boolean instantaneousLaneChange = false;
172 
173     /**
174      * Construct a Lane Based GTU.
175      * @param id String; the id of the GTU
176      * @param gtuType GTUType; the type of GTU, e.g. TruckType, CarType, BusType
177      * @param network OTSRoadNetwork; the network that the GTU is initially registered in
178      * @throws GTUException when initial values are not correct
179      */
180     public AbstractLaneBasedGTU2(final String id, final GTUType gtuType, final OTSRoadNetwork network) throws GTUException
181     {
182         super(id, gtuType, network.getSimulator(), network);
183         OTSSimulatorInterface simulator = network.getSimulator();
184         HistoryManager historyManager = simulator.getReplication().getHistoryManager(simulator);
185         this.crossSections = new HistoricalArrayList<>(historyManager);
186         this.turnIndicatorStatus = new HistoricalValue<>(historyManager, TurnIndicatorStatus.NOTPRESENT);
187     }
188 
189     /**
190      * @param strategicalPlanner LaneBasedStrategicalPlanner; the strategical planner (e.g., route determination) to use
191      * @param initialLongitudinalPositions Set&lt;DirectedLanePosition&gt;; the initial positions of the car on one or more
192      *            lanes with their directions
193      * @param initialSpeed Speed; the initial speed of the car on the lane
194      * @throws NetworkException when the GTU cannot be placed on the given lane
195      * @throws SimRuntimeException when the move method cannot be scheduled
196      * @throws GTUException when initial values are not correct
197      * @throws OTSGeometryException when the initial path is wrong
198      */
199     @SuppressWarnings("checkstyle:designforextension")
200     public void init(final LaneBasedStrategicalPlanner strategicalPlanner,
201             final Set<DirectedLanePosition> initialLongitudinalPositions, final Speed initialSpeed)
202             throws NetworkException, SimRuntimeException, GTUException, OTSGeometryException
203     {
204         Throw.when(null == initialLongitudinalPositions, GTUException.class, "InitialLongitudinalPositions is null");
205         Throw.when(0 == initialLongitudinalPositions.size(), GTUException.class, "InitialLongitudinalPositions is empty set");
206 
207         for (DirectedLanePosition pos : new LinkedHashSet<DirectedLanePosition>(initialLongitudinalPositions)) // copy
208         {
209             double fracFront = (pos.getPosition().si + getFront().getDx().si) / pos.getLane().getLength().si;
210             if (fracFront > 1.0)
211             {
212                 System.err.println("GTU " + toString() + " has been destroyed at init since it occupied multiple lanes");
213                 this.destroy();
214                 return;
215             }
216             double fracRear = (pos.getPosition().si - getRear().getDx().si) / pos.getLane().getLength().si;
217             if (fracRear < 0.0)
218             {
219                 System.err.println("GTU " + toString() + " has been destroyed at init since it occupied multiple lanes");
220                 this.destroy();
221                 return;
222             }
223 
224         }
225 
226         DirectedPoint lastPoint = null;
227         for (DirectedLanePosition pos : initialLongitudinalPositions)
228         {
229             // Throw.when(lastPoint != null && pos.getLocation().distance(lastPoint) > initialLocationThresholdDifference.si,
230             // GTUException.class, "initial locations for GTU have distance > " + initialLocationThresholdDifference);
231             lastPoint = pos.getLocation();
232         }
233         DirectedPoint initialLocation = lastPoint;
234 
235         // Give the GTU a 1 micrometer long operational plan, or a stand-still plan, so the first move and events will work
236         Time now = getSimulator().getSimulatorAbsTime();
237         try
238         {
239             if (initialSpeed.si < OperationalPlan.DRIFTING_SPEED_SI)
240             {
241                 this.operationalPlan
242                         .set(new OperationalPlan(this, initialLocation, now, new Duration(1E-6, DurationUnit.SECOND)));
243             }
244             else
245             {
246                 OTSPoint3D p2 = new OTSPoint3D(initialLocation.x + 1E-6 * Math.cos(initialLocation.getRotZ()),
247                         initialLocation.y + 1E-6 * Math.sin(initialLocation.getRotZ()), initialLocation.z);
248                 OTSLine3D path = new OTSLine3D(new OTSPoint3D(initialLocation), p2);
249                 this.operationalPlan.set(OperationalPlanBuilder.buildConstantSpeedPlan(this, path, now, initialSpeed));
250             }
251         }
252         catch (OperationalPlanException e)
253         {
254             throw new RuntimeException("Initial operational plan could not be created.", e);
255         }
256 
257         // XXX: Added code AV 20220903
258         // Check if the gtu has been registered on all lanes where it resides
259         // Could not make the code work, so destroy the vehicles that are residing in multiple lanes...
260         /*-
261         for (DirectedLanePosition pos : new LinkedHashSet<DirectedLanePosition>(initialLongitudinalPositions)) // copy
262         {
263             DirectedLanePosition dlp = null;
264             DirectedLanePosition posRef = pos;
265             double fracFront = (posRef.getPosition().si + getFront().getDx().si) / posRef.getLane().getLength().si;
266             while (fracFront > 1.0) // only take DIR_PLUS into account for now
267             {
268                 Map<Lane, GTUDirectionality> nextLanes = posRef.getLane().nextLanes(getGTUType());
269                 Throw.when(nextLanes.size() > 1, GTUException.class, "next lane for GTU %s: init has multiple choices", this);
270                 Lane nextLane = nextLanes.keySet().iterator().next();
271                 boolean alreadyPresent = false;
272                 for (DirectedLanePosition pos2 : initialLongitudinalPositions)
273                 {
274                     if (pos2.getLane().equals(nextLane))
275                     {
276                         alreadyPresent = true;
277                         dlp = pos2;
278                         break;
279                     }
280                 }
281                 if (!alreadyPresent)
282                 {
283                     // calculate relative position for reference
284                     Length len2 = posRef.getPosition().minus(posRef.getLane().getLength());
285                     dlp = new DirectedLanePosition(nextLane, len2, GTUDirectionality.DIR_PLUS);
286                     initialLongitudinalPositions.add(dlp);
287                 }
288                 fracFront = 0.0; // TODO: Change this code for a full while loop for multiple lanes
289                 posRef = dlp;
290             }
291             
292             dlp = null;
293             posRef = pos;
294             double fracRear = (posRef.getPosition().si - getRear().getDx().si) / posRef.getLane().getLength().si;
295             if (fracRear < 0.0) 
296             {
297                 System.err.println("GTU " + toString() + " has been destroyed at init since it occupied multiple lanes");
298                 this.destroy();
299             }
300             while (fracRear < 0.0) // only take DIR_PLUS into account for now
301             {
302                 Map<Lane, GTUDirectionality> prevLanes = posRef.getLane().prevLanes(getGTUType());
303                 Throw.when(prevLanes.size() > 1, GTUException.class, "prev lane for GTU %s: init has multiple choices", this);
304                 Lane prevLane = prevLanes.keySet().iterator().next();
305                 boolean alreadyPresent = false;
306                 for (DirectedLanePosition pos2 : initialLongitudinalPositions)
307                 {
308                     if (pos2.getLane().equals(prevLane))
309                     {
310                         alreadyPresent = true;
311                         dlp = pos2;
312                         break;
313                     }
314                 }
315                 if (!alreadyPresent)
316                 {
317                     // calculate relative position for reference
318                     Length len2 = prevLane.getLength().plus(posRef.getPosition());
319                     dlp = new DirectedLanePosition(prevLane, len2, GTUDirectionality.DIR_PLUS);
320                     initialLongitudinalPositions.add(dlp);
321                 }
322                 fracRear = 1.0; // TODO: change this code for a full while loop for multiple lanes
323                 posRef = dlp;
324             }
325         }
326         */
327 
328         // register the GTU on the lanes
329         List<DirectedLanePosition> inits = new ArrayList<>(); // need to sort them
330         inits.addAll(initialLongitudinalPositions);
331         Collections.sort(inits, new Comparator<DirectedLanePosition>()
332         {
333             @Override
334             public int compare(final DirectedLanePosition o1, final DirectedLanePosition o2)
335             {
336                 Length length1 =
337                         o1.getGtuDirection().isPlus() ? o1.getPosition() : o1.getLane().getLength().minus(o1.getPosition());
338                 Length length2 =
339                         o2.getGtuDirection().isPlus() ? o2.getPosition() : o2.getLane().getLength().minus(o2.getPosition());
340                 return length1.compareTo(length2);
341             }
342         });
343         for (DirectedLanePosition directedLanePosition : inits)
344         {
345             List<Lane> lanes = new ArrayList<>();
346             lanes.add(directedLanePosition.getLane());
347             this.crossSections.add(new CrossSection(lanes, directedLanePosition.getGtuDirection())); // enter lane part 1
348         }
349 
350         // init event
351         DirectedLanePosition referencePosition = getReferencePosition();
352         fireTimedEvent(LaneBasedGTU.LANEBASED_INIT_EVENT,
353                 new Object[] {getId(), new OTSPoint3D(initialLocation).doubleVector(PositionUnit.METER),
354                         OTSPoint3D.direction(initialLocation, DirectionUnit.EAST_RADIAN), getLength(), getWidth(),
355                         referencePosition.getLane().getParentLink().getId(), referencePosition.getLane().getId(),
356                         referencePosition.getPosition(), referencePosition.getGtuDirection().name(), getGTUType().getId()},
357                 getSimulator().getSimulatorTime());
358 
359         // register the GTU on the lanes
360         for (DirectedLanePosition directedLanePosition : initialLongitudinalPositions)
361         {
362             Lane lane = directedLanePosition.getLane();
363             lane.addGTU(this, directedLanePosition.getPosition()); // enter lane part 2
364         }
365 
366         // initiate the actual move
367         super.init(strategicalPlanner, initialLocation, initialSpeed);
368 
369         this.referencePositionTime = Double.NaN; // remove cache, it may be invalid as the above init results in a lane change
370     }
371 
372     /**
373      * {@inheritDoc} All lanes the GTU is on will be left.
374      */
375     @Override
376     public synchronized void setParent(final GTU gtu) throws GTUException
377     {
378         leaveAllLanes();
379         super.setParent(gtu);
380     }
381 
382     /**
383      * Removes the registration between this GTU and all the lanes.
384      */
385     private void leaveAllLanes()
386     {
387         for (CrossSection crossSection : this.crossSections)
388         {
389             boolean removeFromParentLink = true;
390             for (Lane lane : crossSection.getLanes())
391             {
392                 // GTU should be on this lane as we loop the registration
393                 Length pos = Try.assign(() -> position(lane, getReference()), "Unexpected exception.");
394                 lane.removeGTU(this, removeFromParentLink, pos);
395                 removeFromParentLink = false;
396             }
397         }
398         this.crossSections.clear();
399     }
400 
401     /**
402      * Reinitializes the GTU on the network using the existing strategical planner and zero speed.
403      * @param initialLongitudinalPositions Set&lt;DirectedLanePosition&gt;; initial position
404      * @throws NetworkException when the GTU cannot be placed on the given lane
405      * @throws SimRuntimeException when the move method cannot be scheduled
406      * @throws GTUException when initial values are not correct
407      * @throws OTSGeometryException when the initial path is wrong
408      */
409     public void reinit(final Set<DirectedLanePosition> initialLongitudinalPositions)
410             throws NetworkException, SimRuntimeException, GTUException, OTSGeometryException
411     {
412         init(getStrategicalPlanner(), initialLongitudinalPositions, Speed.ZERO);
413     }
414 
415     /** {@inheritDoc} */
416     @Override
417     public synchronized void changeLaneInstantaneously(final LateralDirectionality laneChangeDirection) throws GTUException
418     {
419 
420         // from info
421         DirectedLanePosition from = getReferencePosition();
422 
423         // obtain position on lane adjacent to reference lane and enter lanes upstream/downstream from there
424         GTUDirectionality direction = getDirection(from.getLane());
425         Set<Lane> adjLanes = from.getLane().accessibleAdjacentLanesPhysical(laneChangeDirection, getGTUType(), direction);
426         Lane adjLane = adjLanes.iterator().next();
427         Length position = adjLane.position(from.getLane().fraction(from.getPosition()));
428         leaveAllLanes();
429         enterLaneRecursive(new LaneDirection(adjLane, direction), position, 0);
430 
431         // stored positions no longer valid
432         this.referencePositionTime = Double.NaN;
433         this.cachedPositions.clear();
434 
435         // fire event
436         this.fireTimedEvent(
437                 LaneBasedGTU.LANE_CHANGE_EVENT, new Object[] {getId(), laneChangeDirection.name(),
438                         from.getLane().getParentLink().getId(), from.getLane().getId(), from.getPosition()},
439                 getSimulator().getSimulatorTime());
440 
441     }
442 
443     /**
444      * Enters lanes upstream and downstream of the new location after an instantaneous lane change.
445      * @param lane LaneDirection; considered lane
446      * @param position Length; position to add GTU at
447      * @param dir int; below 0 for upstream, above 0 for downstream, 0 for both
448      * @throws GTUException on exception
449      */
450     private void enterLaneRecursive(final LaneDirection lane, final Length position, final int dir) throws GTUException
451     {
452         List<Lane> lanes = new ArrayList<>();
453         lanes.add(lane.getLane());
454         int index = dir > 0 ? this.crossSections.size() : 0;
455         this.crossSections.add(index, new CrossSection(lanes, lane.getDirection()));
456         lane.getLane().addGTU(this, position);
457 
458         // upstream
459         if (dir < 1)
460         {
461             Length rear = lane.getDirection().isPlus() ? position.plus(getRear().getDx()) : position.minus(getRear().getDx());
462             Length before = null;
463             if (lane.getDirection().isPlus() && rear.si < 0.0)
464             {
465                 before = rear.neg();
466             }
467             else if (lane.getDirection().isMinus() && rear.si > lane.getLength().si)
468             {
469                 before = rear.minus(lane.getLength());
470             }
471             if (before != null)
472             {
473                 GTUDirectionality upDir = lane.getDirection();
474                 ImmutableMap<Lane, GTUDirectionality> upstream = lane.getLane().upstreamLanes(upDir, getGTUType());
475                 if (!upstream.isEmpty())
476                 {
477                     Lane upLane = null;
478                     for (Lane nextUp : upstream.keySet())
479                     {
480                         for (CrossSection crossSection : this.crossSections)
481                         {
482                             if (crossSection.getLanes().contains(nextUp))
483                             {
484                                 // multiple upstream lanes could belong to the same link, we pick an arbitrary lane
485                                 // (a conflict should solve this)
486                                 upLane = nextUp;
487                                 break;
488                             }
489                         }
490                     }
491                     if (upLane == null)
492                     {
493                         // the rear is on an upstream section we weren't before the lane change, due to curvature, we pick an
494                         // arbitrary lane (a conflict should solve this)
495                         upLane = upstream.keySet().iterator().next();
496                     }
497                     upDir = upstream.get(upLane);
498                     LaneDirection next = new LaneDirection(upLane, upDir);
499                     Length nextPos = upDir.isPlus() ? next.getLength().minus(before).minus(getRear().getDx())
500                             : before.plus(getRear().getDx());
501                     enterLaneRecursive(next, nextPos, -1);
502                 }
503             }
504         }
505 
506         // downstream
507         if (dir > -1)
508         {
509             Length front =
510                     lane.getDirection().isPlus() ? position.plus(getFront().getDx()) : position.minus(getFront().getDx());
511             Length passed = null;
512             if (lane.getDirection().isPlus() && front.si > lane.getLength().si)
513             {
514                 passed = front.minus(lane.getLength());
515             }
516             else if (lane.getDirection().isMinus() && front.si < 0.0)
517             {
518                 passed = front.neg();
519             }
520             if (passed != null)
521             {
522                 LaneDirection next = lane.getNextLaneDirection(this);
523                 Length nextPos = next.getDirection().isPlus() ? passed.minus(getFront().getDx())
524                         : next.getLength().minus(passed).plus(getFront().getDx());
525                 enterLaneRecursive(next, nextPos, 1);
526             }
527         }
528     }
529 
530     /**
531      * Register on lanes in target lane.
532      * @param laneChangeDirection LateralDirectionality; direction of lane change
533      * @throws GTUException exception
534      */
535     @Override
536     @SuppressWarnings("checkstyle:designforextension")
537     public synchronized void initLaneChange(final LateralDirectionality laneChangeDirection) throws GTUException
538     {
539         List<CrossSection> newLanes = new ArrayList<>();
540         int index = laneChangeDirection.isLeft() ? 0 : 1;
541         int numRegistered = 0;
542         DirectedPoint point = getLocation();
543         Map<Lane, Double> addToLanes = new LinkedHashMap<>();
544         for (CrossSection crossSection : this.crossSections)
545         {
546             List<Lane> resultingLanes = new ArrayList<>();
547             Lane lane = crossSection.getLanes().get(0);
548             resultingLanes.add(lane);
549             Set<Lane> laneSet = lane.accessibleAdjacentLanesLegal(laneChangeDirection, getGTUType(), getDirection(lane));
550             if (laneSet.size() > 0)
551             {
552                 numRegistered++;
553                 Lane adjacentLane = laneSet.iterator().next();
554                 double f = adjacentLane.getCenterLine().projectFractional(null, null, point.x, point.y, FractionalFallback.NaN);
555                 if (Double.isNaN(f))
556                 {
557                     // the GTU is upstream or downstream of the lane, or on the edge and we have rounding problems
558                     // in either case we add the GTU at an extreme
559                     // (this is only for ordering on the lane, the position is not used otherwise)
560                     Length pos = position(lane, getReference());
561                     addToLanes.put(adjacentLane, pos.si < lane.getLength().si / 2 ? 0.0 : 1.0);
562                 }
563                 else
564                 {
565                     f = crossSection.getDirection().isPlus() ? f : 1.0 - f;
566                     addToLanes.put(adjacentLane, adjacentLane.getLength().times(f).si / adjacentLane.getLength().si);
567                 }
568                 resultingLanes.add(index, adjacentLane);
569             }
570             newLanes.add(new CrossSection(resultingLanes, crossSection.getDirection()));
571         }
572         Throw.when(numRegistered == 0, GTUException.class, "Gtu %s starting %s lane change, but no adjacent lane found.",
573                 getId(), laneChangeDirection);
574         this.crossSections.clear();
575         this.crossSections.addAll(newLanes);
576         for (Entry<Lane, Double> entry : addToLanes.entrySet())
577         {
578             entry.getKey().addGTU(this, entry.getValue());
579         }
580         this.referenceLaneIndex = 1 - index;
581     }
582 
583     /**
584      * Performs the finalization of a lane change by leaving the from lanes.
585      * @param laneChangeDirection LateralDirectionality; direction of lane change
586      * @throws GTUException if position or direction could not be obtained
587      */
588     @SuppressWarnings("checkstyle:designforextension")
589     protected synchronized void finalizeLaneChange(final LateralDirectionality laneChangeDirection) throws GTUException
590     {
591         List<CrossSection> newLanes = new ArrayList<>();
592         Lane fromLane = null;
593         Length fromPosition = null;
594         GTUDirectionality fromDirection = null;
595         for (CrossSection crossSection : this.crossSections)
596         {
597             Lane lane = crossSection.getLanes().get(this.referenceLaneIndex);
598             if (lane != null)
599             {
600                 Length pos = position(lane, RelativePosition.REFERENCE_POSITION);
601                 if (0.0 <= pos.si && pos.si <= lane.getLength().si)
602                 {
603                     fromLane = lane;
604                     fromPosition = pos;
605                     fromDirection = getDirection(lane);
606                 }
607                 lane.removeGTU(this, false, pos);
608             }
609             List<Lane> remainingLane = new ArrayList<>();
610             remainingLane.add(crossSection.getLanes().get(1 - this.referenceLaneIndex));
611             newLanes.add(new CrossSection(remainingLane, crossSection.getDirection()));
612         }
613         this.crossSections.clear();
614         this.crossSections.addAll(newLanes);
615         this.referenceLaneIndex = 0;
616 
617         Throw.when(fromLane == null, RuntimeException.class, "No from lane for lane change event.");
618         DirectedLanePosition from;
619         try
620         {
621             from = new DirectedLanePosition(fromLane, fromPosition, fromDirection);
622         }
623         catch (GTUException exception)
624         {
625             throw new RuntimeException(exception);
626         }
627 
628         // XXX: WRONG: this.fireTimedEvent(LaneBasedGTU.LANE_CHANGE_EVENT, new Object[] {getId(), laneChangeDirection, from},
629         // XXX: WRONG: getSimulator().getSimulatorTime());
630         this.fireTimedEvent(
631                 LaneBasedGTU.LANE_CHANGE_EVENT, new Object[] {getId(), laneChangeDirection.name(),
632                         from.getLane().getParentLink().getId(), from.getLane().getId(), from.getPosition()},
633                 getSimulator().getSimulatorTime());
634 
635         this.finalizeLaneChangeEvent = null;
636     }
637 
638     /** {@inheritDoc} */
639     @Override
640     public void setFinalizeLaneChangeEvent(final SimEventInterface<Duration> event)
641     {
642         this.finalizeLaneChangeEvent = event;
643     }
644 
645     /** {@inheritDoc} */
646     @Override
647     public final synchronized GTUDirectionality getDirection(final Lane lane) throws GTUException
648     {
649         for (CrossSection crossSection : this.crossSections)
650         {
651             if (crossSection.getLanes().contains(lane))
652             {
653                 return crossSection.getDirection();
654             }
655         }
656         throw new GTUException("getDirection: GTU does not contain " + lane);
657     }
658 
659     /** {@inheritDoc} */
660     @Override
661     @SuppressWarnings("checkstyle:designforextension")
662     protected synchronized boolean move(final DirectedPoint fromLocation)
663             throws SimRuntimeException, GTUException, OperationalPlanException, NetworkException, ParameterException
664     {
665         if (this.isDestroyed())
666         {
667             return false;
668         }
669         try
670         {
671             if (this.crossSections.isEmpty())
672             {
673                 destroy();
674                 return false; // Done; do not re-schedule execution of this move method.
675             }
676 
677             // cancel events, if any
678             // FIXME: If there are still events left, clearly something went wrong?
679             // XXX: Added boolean to indicate whether warnings need to be given when events were found
680             cancelAllEvents();
681 
682             // generate the next operational plan and carry it out
683             // in case of an instantaneous lane change, fractionalLinkPositions will be accordingly adjusted to the new lane
684             try
685             {
686                 boolean error = super.move(fromLocation);
687                 if (error)
688                 {
689                     return error;
690                 }
691             }
692             catch (Exception exception)
693             {
694                 System.err.println(exception.getMessage());
695                 System.err.println("  GTU " + this + " DESTROYED AND REMOVED FROM THE SIMULATION");
696                 this.destroy();
697                 this.cancelAllEvents();
698                 return true;
699             }
700 
701             DirectedLanePosition dlp = getReferencePosition();
702 
703             scheduleEnterEvent();
704             scheduleLeaveEvent();
705 
706             // sensors
707             for (CrossSection crossSection : this.crossSections)
708             {
709                 for (Lane lane : crossSection.getLanes())
710                 {
711                     scheduleTriggers(lane, crossSection.getDirection());
712                 }
713             }
714 
715             fireTimedEvent(LaneBasedGTU.LANEBASED_MOVE_EVENT,
716                     new Object[] {getId(), new OTSPoint3D(fromLocation).doubleVector(PositionUnit.METER),
717                             OTSPoint3D.direction(fromLocation, DirectionUnit.EAST_RADIAN), getSpeed(), getAcceleration(),
718                             getTurnIndicatorStatus().name(), getOdometer(), dlp.getLane().getParentLink().getId(),
719                             dlp.getLane().getId(), dlp.getPosition(), dlp.getGtuDirection().name()},
720                     getSimulator().getSimulatorTime());
721 
722             return false;
723 
724         }
725         catch (Exception ex)
726         {
727             try
728             {
729                 getErrorHandler().handle(this, ex);
730             }
731             catch (Exception exception)
732             {
733                 throw new GTUException(exception);
734             }
735             return true;
736         }
737 
738     }
739 
740     /**
741      * Cancels all future events.
742      */
743     private void cancelAllEvents()
744     {
745         if (this.pendingEnterTrigger != null)
746         {
747             getSimulator().cancelEvent(this.pendingEnterTrigger);
748         }
749         if (this.pendingLeaveTrigger != null)
750         {
751             getSimulator().cancelEvent(this.pendingLeaveTrigger);
752         }
753         if (this.finalizeLaneChangeEvent != null)
754         {
755             getSimulator().cancelEvent(this.finalizeLaneChangeEvent);
756         }
757         for (SimEventInterface<Duration> event : this.sensorEvents)
758         {
759             if (event.getAbsoluteExecutionTime().gt(getSimulator().getSimulatorTime()))
760             {
761                 getSimulator().cancelEvent(event);
762             }
763         }
764         this.sensorEvents.clear();
765     }
766 
767     /**
768      * Checks whether the GTU will enter a next cross-section during the (remainder of) the tactical plan. Only one event will
769      * be scheduled. Possible additional events are scheduled upon entering the cross-section.
770      * @throws GTUException exception
771      * @throws OperationalPlanException exception
772      * @throws SimRuntimeException exception
773      */
774     protected void scheduleEnterEvent() throws GTUException, OperationalPlanException, SimRuntimeException
775     {
776         CrossSection lastCrossSection = this.crossSections.get(this.crossSections.size() - 1);
777         // heuristic to prevent geometric calculation if the next section is quite far away anyway
778         Length remain = remainingEventDistance();
779         Lane lane = lastCrossSection.getLanes().get(this.referenceLaneIndex);
780         Length position = position(lane, getFront());
781         boolean possiblyNearNextSection =
782                 lastCrossSection.getDirection().isPlus() ? lane.getLength().minus(position).lt(remain) : position.lt(remain);
783         if (possiblyNearNextSection)
784         {
785             CrossSectionLink link = lastCrossSection.getLanes().get(0).getParentLink();
786             OTSLine3D enterLine = lastCrossSection.getDirection().isPlus() ? link.getEndLine() : link.getStartLine();
787             Time enterTime = timeAtLine(enterLine, getFront());
788             if (enterTime != null)
789             {
790                 if (enterTime.lt(getSimulator().getSimulatorAbsTime()))
791                 {
792                     System.err.println(
793                             "Time travel? enterTime=" + enterTime + "; simulator time=" + getSimulator().getSimulatorAbsTime());
794                     enterTime = getSimulator().getSimulatorAbsTime();
795                 }
796                 this.pendingEnterTrigger =
797                         getSimulator().scheduleEventAbsTime(enterTime, this, this, "enterCrossSection", null);
798             }
799         }
800     }
801 
802     /**
803      * Appends a new cross-section at the downstream end. Possibly schedules a next enter event.
804      * @throws GTUException exception
805      * @throws OperationalPlanException exception
806      * @throws SimRuntimeException exception
807      */
808     protected synchronized void enterCrossSection() throws GTUException, OperationalPlanException, SimRuntimeException
809     {
810         CrossSection lastCrossSection = this.crossSections.get(this.crossSections.size() - 1);
811         LaneDirection laneDirection =
812                 new LaneDirection(lastCrossSection.getLanes().get(this.referenceLaneIndex), lastCrossSection.getDirection());
813         LaneDirection nextLaneDirection = laneDirection.getNextLaneDirection(this);
814         if (nextLaneDirection == null)
815         {
816             forceLaneChangeFinalization();
817             return;
818         }
819         double insertFraction = nextLaneDirection.getDirection().isPlus() ? 0.0 : 1.0;
820         List<Lane> nextLanes = new ArrayList<>();
821         for (int i = 0; i < lastCrossSection.getLanes().size(); i++)
822         {
823             if (i == this.referenceLaneIndex)
824             {
825                 nextLanes.add(nextLaneDirection.getLane());
826             }
827             else
828             {
829                 Lane lane = lastCrossSection.getLanes().get(i);
830                 ImmutableMap<Lane, GTUDirectionality> lanes = lane.downstreamLanes(laneDirection.getDirection(), getGTUType());
831                 if (lanes.size() == 1)
832                 {
833                     Lane nextLane = lanes.keySet().iterator().next();
834                     nextLanes.add(nextLane);
835                 }
836                 else
837                 {
838                     boolean added = false;
839                     for (Lane nextLane : lanes.keySet())
840                     {
841                         if (nextLane.getParentLink().equals(nextLaneDirection.getLane().getParentLink())
842                                 && nextLane.accessibleAdjacentLanesPhysical(
843                                         this.referenceLaneIndex == 0 ? LateralDirectionality.LEFT : LateralDirectionality.RIGHT,
844                                         getGTUType(), nextLaneDirection.getDirection()).contains(nextLaneDirection.getLane()))
845                         {
846                             nextLanes.add(nextLane);
847                             added = true;
848                             break;
849                         }
850                     }
851                     if (!added)
852                     {
853                         forceLaneChangeFinalization();
854                         return;
855                     }
856                 }
857             }
858         }
859         this.crossSections.add(new CrossSection(nextLanes, nextLaneDirection.getDirection()));
860         for (Lane lane : nextLanes)
861         {
862             lane.addGTU(this, insertFraction);
863         }
864         this.pendingEnterTrigger = null;
865         scheduleEnterEvent();
866         for (Lane lane : nextLanes)
867         {
868             scheduleTriggers(lane, nextLaneDirection.getDirection());
869         }
870     }
871 
872     /**
873      * Helper method for {@code enterCrossSection}. In some cases the GTU should first finalize the lane change. This method
874      * checks whether such an event is scheduled, and performs it. This method then re-attempts to enter the cross-section. So
875      * the calling method should return after calling this.
876      * @throws GTUException exception
877      * @throws OperationalPlanException exception
878      * @throws SimRuntimeException exception
879      */
880     private void forceLaneChangeFinalization() throws GTUException, OperationalPlanException, SimRuntimeException
881     {
882         if (this.finalizeLaneChangeEvent != null)
883         {
884             // a lane change should be finalized at this time, but the event is later in the queue, force it now
885             SimEventInterface<Duration> tmp = this.finalizeLaneChangeEvent;
886             finalizeLaneChange(this.referenceLaneIndex == 0 ? LateralDirectionality.RIGHT : LateralDirectionality.LEFT);
887             getSimulator().cancelEvent(tmp);
888             enterCrossSection();
889         }
890         // or a sink sensor should delete us
891     }
892 
893     /**
894      * Checks whether the GTU will leave a cross-section during the (remainder of) the tactical plan. Only one event will be
895      * scheduled. Possible additional events are scheduled upon leaving the cross-section.
896      * @throws GTUException exception
897      * @throws OperationalPlanException exception
898      * @throws SimRuntimeException exception
899      */
900     protected void scheduleLeaveEvent() throws GTUException, OperationalPlanException, SimRuntimeException
901     {
902         if (this.crossSections.isEmpty())
903         {
904             CategoryLogger.always().error("GTU {} has empty crossSections", this);
905             return;
906         }
907         CrossSection firstCrossSection = this.crossSections.get(0);
908         // check, if reference lane is not in first cross section
909         boolean possiblyNearNextSection =
910                 !getReferencePosition().getLane().equals(firstCrossSection.getLanes().get(this.referenceLaneIndex));
911         if (!possiblyNearNextSection)
912         {
913             Length remain = remainingEventDistance();
914             Lane lane = firstCrossSection.getLanes().get(this.referenceLaneIndex);
915             Length position = position(lane, getRear());
916             possiblyNearNextSection = firstCrossSection.getDirection().isPlus() ? lane.getLength().minus(position).lt(remain)
917                     : position.lt(remain);
918         }
919         if (possiblyNearNextSection)
920         {
921             CrossSectionLink link = firstCrossSection.getLanes().get(0).getParentLink();
922             OTSLine3D leaveLine = firstCrossSection.getDirection().isPlus() ? link.getEndLine() : link.getStartLine();
923             Time leaveTime = timeAtLine(leaveLine, getRear());
924             if (leaveTime == null)
925             {
926                 // no intersect, let's do a check on the rear
927                 Lane lane = this.crossSections.get(0).getLanes().get(this.referenceLaneIndex);
928                 Length pos = position(lane, getRear());
929                 if (pos.gt(lane.getLength()))
930                 {
931                     pos = position(lane, getRear());
932                     this.pendingLeaveTrigger = getSimulator().scheduleEventNow(this, this, "leaveCrossSection", null);
933                     getSimulator().getLogger().always().info("Forcing leave for GTU {} on lane {}", getId(), lane.getFullId());
934                 }
935             }
936             if (leaveTime != null)
937             {
938                 if (leaveTime.lt(getSimulator().getSimulatorAbsTime()))
939                 {
940                     System.err.println(
941                             "Time travel? leaveTime=" + leaveTime + "; simulator time=" + getSimulator().getSimulatorAbsTime());
942                     leaveTime = getSimulator().getSimulatorAbsTime();
943                 }
944                 this.pendingLeaveTrigger =
945                         getSimulator().scheduleEventAbsTime(leaveTime, this, this, "leaveCrossSection", null);
946             }
947         }
948     }
949 
950     /**
951      * Removes registration between the GTU and the lanes in the most upstream cross-section. Possibly schedules a next leave
952      * event.
953      * @throws GTUException exception
954      * @throws OperationalPlanException exception
955      * @throws SimRuntimeException exception
956      */
957     protected synchronized void leaveCrossSection() throws GTUException, OperationalPlanException, SimRuntimeException
958     {
959 
960         List<Lane> lanes = this.crossSections.get(0).getLanes();
961         for (int i = 0; i < lanes.size(); i++)
962         {
963             Lane lane = lanes.get(i);
964             if (lane != null)
965             {
966                 lane.removeGTU(this, i == lanes.size() - 1, position(lane, getReference()));
967             }
968         }
969         this.crossSections.remove(0);
970         this.pendingLeaveTrigger = null;
971         scheduleLeaveEvent();
972     }
973 
974     /**
975      * Schedules all trigger events during the current operational plan on the lane.
976      * @param lane Lane; lane
977      * @param direction GTUDirectionality; direction
978      * @throws GTUException exception
979      * @throws OperationalPlanException exception
980      * @throws SimRuntimeException exception
981      */
982     protected void scheduleTriggers(final Lane lane, final GTUDirectionality direction)
983             throws GTUException, OperationalPlanException, SimRuntimeException
984     {
985         double min;
986         double max;
987         Length remain = remainingEventDistance();
988         if (direction.isPlus())
989         {
990             min = position(lane, getRear()).si;
991             max = min + remain.si + getLength().si;
992         }
993         else
994         {
995             max = position(lane, getRear()).si;
996             min = max - remain.si - getLength().si;
997         }
998         SortedMap<Double, List<SingleSensor>> sensors = lane.getSensorMap(getGTUType(), direction).subMap(min, max);
999         for (List<SingleSensor> list : sensors.values())
1000         {
1001             for (SingleSensor sensor : list)
1002             {
1003                 RelativePosition pos = this.getRelativePositions().get(sensor.getPositionType());
1004                 Time time = timeAtLine(sensor.getGeometry(), pos);
1005                 if (time != null)
1006                 {
1007                     this.sensorEvents
1008                             .add(getSimulator().scheduleEventAbsTime(time, this, sensor, "trigger", new Object[] {this}));
1009                 }
1010             }
1011         }
1012     }
1013 
1014     /**
1015      * Returns a safe distance beyond which a line will definitely not be crossed during the current operational plan.
1016      * @return Length; safe distance beyond which a line will definitely not be crossed during the current operational plan
1017      * @throws OperationalPlanException exception
1018      */
1019     private Length remainingEventDistance() throws OperationalPlanException
1020     {
1021         if (getOperationalPlan() instanceof LaneBasedOperationalPlan)
1022         {
1023             LaneBasedOperationalPlan plan = (LaneBasedOperationalPlan) getOperationalPlan();
1024             return plan.getTotalLength().minus(plan.getTraveledDistance(getSimulator().getSimulatorAbsTime()))
1025                     .plus(eventMargin);
1026         }
1027         return getOperationalPlan().getTotalLength().plus(eventMargin);
1028     }
1029 
1030     /**
1031      * Returns an estimation of when the relative position will reach the line. Returns {@code null} if this does not occur
1032      * during the current operational plan.
1033      * @param line OTSLine3D; line, i.e. lateral line at link start or lateral entrance of sensor
1034      * @param relativePosition RelativePosition; position to cross the line
1035      * @return estimation of when the relative position will reach the line, {@code null} if this does not occur during the
1036      *         current operational plan
1037      * @throws GTUException position error
1038      */
1039     private Time timeAtLine(final OTSLine3D line, final RelativePosition relativePosition) throws GTUException
1040     {
1041         Throw.when(line.size() != 2, IllegalArgumentException.class, "Line to cross with path should have 2 points.");
1042         OTSLine3D path = getOperationalPlan().getPath();
1043         OTSPoint3D[] points;
1044         double adjust;
1045         if (relativePosition.getDx().gt0())
1046         {
1047             // as the position is downstream of the reference, we need to attach some distance at the end
1048             points = new OTSPoint3D[path.size() + 1];
1049             System.arraycopy(path.getPoints(), 0, points, 0, path.size());
1050             points[path.size()] = new OTSPoint3D(path.getLocationExtendedSI(path.getLengthSI() + relativePosition.getDx().si));
1051             adjust = -relativePosition.getDx().si;
1052         }
1053         else if (relativePosition.getDx().lt0())
1054         {
1055             points = new OTSPoint3D[path.size() + 1];
1056             System.arraycopy(path.getPoints(), 0, points, 1, path.size());
1057             points[0] = new OTSPoint3D(path.getLocationExtendedSI(relativePosition.getDx().si));
1058             adjust = 0.0;
1059         }
1060         else
1061         {
1062             points = path.getPoints();
1063             adjust = 0.0;
1064         }
1065 
1066         // find intersection
1067         double cumul = 0.0;
1068         for (int i = 0; i < points.length - 1; i++)
1069         {
1070             OTSPoint3D intersect;
1071             try
1072             {
1073                 intersect = OTSPoint3D.intersectionOfLineSegments(points[i], points[i + 1], line.get(0), line.get(1));
1074             }
1075             catch (OTSGeometryException exception)
1076             {
1077                 // should not occur, we check line.size() == 2
1078                 throw new RuntimeException("Unexpected exception while obtaining points from line to cross.", exception);
1079             }
1080             if (intersect != null)
1081             {
1082                 cumul += points[i].distanceSI(intersect);
1083                 cumul += adjust; // , 0.0); // possible rear is already considered in first segment
1084                 // return time at distance
1085                 if (cumul < 0.0)
1086                 {
1087                     // return getSimulator().getSimulatorAbsTime(); // this was a mistake...
1088                     // relative position already crossed the point, e.g. FRONT
1089                     return null;
1090                 }
1091                 if (cumul <= getOperationalPlan().getTotalLength().si)
1092                 {
1093                     return getOperationalPlan().timeAtDistance(Length.instantiateSI(cumul));
1094                 }
1095                 // ref will cross the line, but GTU will not travel enough for rear to cross
1096                 return null;
1097             }
1098             else if (i < points.length - 2)
1099             {
1100                 cumul += points[i].distanceSI(points[i + 1]);
1101             }
1102         }
1103         // no intersect
1104         return null;
1105     }
1106 
1107     /** {@inheritDoc} */
1108     @Override
1109     public final Map<Lane, Length> positions(final RelativePosition relativePosition) throws GTUException
1110     {
1111         return positions(relativePosition, getSimulator().getSimulatorAbsTime());
1112     }
1113 
1114     /** {@inheritDoc} */
1115     @Override
1116     public final Map<Lane, Length> positions(final RelativePosition relativePosition, final Time when) throws GTUException
1117     {
1118         Map<Lane, Length> positions = new LinkedHashMap<>();
1119         for (CrossSection crossSection : this.crossSections.get(when))
1120         {
1121             for (Lane lane : crossSection.getLanes())
1122             {
1123                 positions.put(lane, position(lane, relativePosition, when));
1124             }
1125         }
1126         return positions;
1127     }
1128 
1129     /** {@inheritDoc} */
1130     @Override
1131     public final Length position(final Lane lane, final RelativePosition relativePosition) throws GTUException
1132     {
1133         return position(lane, relativePosition, getSimulator().getSimulatorAbsTime());
1134     }
1135 
1136     /** Caching of time field for last stored position(s). */
1137     private double cachePositionsTime = Double.NaN;
1138 
1139     /** Caching of operation plan for last stored position(s). */
1140     private OperationalPlan cacheOperationalPlan = null;
1141 
1142     /** caching of last stored position(s). */
1143     private MultiKeyMap<Length> cachedPositions = new MultiKeyMap<>(Lane.class, RelativePosition.class);
1144 
1145     /** {@inheritDoc} */
1146     @Override
1147     @SuppressWarnings("checkstyle:designforextension")
1148     public Length position(final Lane lane, final RelativePosition relativePosition, final Time when) throws GTUException
1149     {
1150         synchronized (this)
1151         {
1152             OperationalPlan plan = getOperationalPlan(when);
1153             if (CACHING)
1154             {
1155                 if (when.si == this.cachePositionsTime && plan == this.cacheOperationalPlan)
1156                 {
1157                     Length l = this.cachedPositions.get(lane, relativePosition);
1158                     if (l != null && (!Double.isNaN(l.si)))
1159                     {
1160                         CACHED_POSITION++;
1161                         // PK verify the result; uncomment if you don't trust the cache
1162                         // this.cachedPositions.clear();
1163                         // Length difficultWay = position(lane, relativePosition, when);
1164                         // if (Math.abs(l.si - difficultWay.si) > 0.00001)
1165                         // {
1166                         // System.err.println("Whoops: cache returns bad value for GTU " + getId() + " cache returned " + l
1167                         // + ", re-computing yielded " + difficultWay);
1168                         // l = null; // Invalidate; to debug and try again
1169                         // }
1170                         // }
1171                         // if (l != null)
1172                         // {
1173                         return l;
1174                     }
1175                 }
1176                 if (when.si != this.cachePositionsTime || plan != this.cacheOperationalPlan)
1177                 {
1178                     this.cachePositionsTime = Double.NaN;
1179                     this.cacheOperationalPlan = null;
1180                     this.cachedPositions.clear();
1181                 }
1182             }
1183             NON_CACHED_POSITION++;
1184 
1185             synchronized (this.lock)
1186             {
1187                 List<CrossSection> whenCrossSections = this.crossSections.get(when);
1188                 double loc = Double.NaN;
1189 
1190                 try
1191                 {
1192                     int crossSectionIndex = -1;
1193                     int lateralIndex = -1;
1194                     for (int i = 0; i < whenCrossSections.size(); i++)
1195                     {
1196                         if (whenCrossSections.get(i).getLanes().contains(lane))
1197                         {
1198                             crossSectionIndex = i;
1199                             lateralIndex = whenCrossSections.get(i).getLanes().indexOf(lane);
1200                             break;
1201                         }
1202                     }
1203                     Throw.when(lateralIndex == -1, GTUException.class, "GTU %s is not on lane %s.", this, lane);
1204 
1205                     DirectedPoint p = plan.getLocation(when, relativePosition);
1206                     double f = lane.getCenterLine().projectFractional(null, null, p.x, p.y, FractionalFallback.NaN);
1207                     if (!Double.isNaN(f))
1208                     {
1209                         loc = f * lane.getLength().si;
1210                     }
1211                     else
1212                     {
1213                         // the point does not project fractionally to this lane, it has to be up- or downstream of the lane
1214                         // try upstream
1215                         double distance = 0.0;
1216                         for (int i = crossSectionIndex - 1; i >= 0; i--)
1217                         {
1218                             Lane tryLane = whenCrossSections.get(i).getLanes().get(lateralIndex);
1219                             f = tryLane.getCenterLine().projectFractional(null, null, p.x, p.y, FractionalFallback.NaN);
1220                             if (!Double.isNaN(f))
1221                             {
1222                                 f = whenCrossSections.get(i).getDirection() == GTUDirectionality.DIR_PLUS ? 1 - f : f;
1223                                 loc = distance - f * tryLane.getLength().si;
1224                                 break;
1225                             }
1226                             distance -= tryLane.getLength().si;
1227                         }
1228                         // try downstream
1229                         if (Double.isNaN(loc))
1230                         {
1231                             distance = lane.getLength().si;
1232                             for (int i = crossSectionIndex + 1; i < whenCrossSections.size(); i++)
1233                             {
1234                                 Lane tryLane = whenCrossSections.get(i).getLanes().get(lateralIndex);
1235                                 f = tryLane.getCenterLine().projectFractional(null, null, p.x, p.y, FractionalFallback.NaN);
1236                                 if (!Double.isNaN(f))
1237                                 {
1238                                     f = whenCrossSections.get(i).getDirection() == GTUDirectionality.DIR_PLUS ? f : 1 - f;
1239                                     loc = distance + f * tryLane.getLength().si;
1240                                     break;
1241                                 }
1242                                 distance += tryLane.getLength().si;
1243                             }
1244                         }
1245 
1246                     }
1247 
1248                     if (Double.isNaN(loc))
1249                     {
1250                         // the GTU is not on the lane with the relativePosition, nor is it registered on next/previous lanes
1251                         // this can occur as the GTU was generated with the rear upstream of the lane, or due to rounding errors
1252                         // use different fraction projection fallback
1253                         f = lane.getCenterLine().projectFractional(null, null, p.x, p.y, FractionalFallback.ENDPOINT);
1254                         if (Double.isNaN(f))
1255                         {
1256                             CategoryLogger.always().error("GTU {} at location {} cannot project itself onto {}; p is {}", this,
1257                                     getLocation(), lane.getCenterLine(), p);
1258                             plan.getLocation(when, relativePosition);
1259                         }
1260                         loc = lane.getLength().si * f;
1261 
1262                         // if (CACHING)
1263                         // {
1264                         // this.cachedPositions.put(cacheIndex, null);
1265                         // }
1266                         // return null;
1267                         // if (getOdometer().lt(getLength()))
1268                         // {
1269                         // // this occurs when the GTU is generated with the rear upstream of the lane, which we often do
1270                         // loc = position(lane, getFront(), when).si + relativePosition.getDx().si - getFront().getDx().si;
1271                         // }
1272                         // else
1273                         // {
1274                         // System.out.println("loc is NaN");
1275                         // }
1276                     }
1277                 }
1278                 catch (Exception e)
1279                 {
1280                     // System.err.println(toString() + ": " + e.getMessage());
1281                     throw new GTUException(e);
1282                 }
1283 
1284                 Length length = Length.instantiateSI(loc);
1285                 if (CACHING)
1286                 {
1287                     this.cachedPositions.put(length, lane, relativePosition);
1288                     this.cachePositionsTime = when.si;
1289                     this.cacheOperationalPlan = plan;
1290                 }
1291                 return length;
1292             }
1293         }
1294     }
1295 
1296     /** {@inheritDoc} */
1297     @Override
1298     @SuppressWarnings("checkstyle:designforextension")
1299     public DirectedLanePosition getReferencePosition() throws GTUException
1300     {
1301         synchronized (this)
1302         {
1303             if (this.referencePositionTime == getSimulator().getSimulatorAbsTime().si)
1304             {
1305                 return this.cachedReferencePosition;
1306             }
1307             Lane refLane = null;
1308             for (CrossSection crossSection : this.crossSections)
1309             {
1310                 Lane lane = crossSection.getLanes().get(this.referenceLaneIndex);
1311                 double fraction = fractionalPosition(lane, getReference());
1312                 if (fraction >= 0.0 && fraction <= 1.0)
1313                 {
1314                     refLane = lane;
1315                     break;
1316                 }
1317             }
1318             if (refLane != null)
1319             {
1320                 this.cachedReferencePosition =
1321                         new DirectedLanePosition(refLane, position(refLane, getReference()), this.getDirection(refLane));
1322                 this.referencePositionTime = getSimulator().getSimulatorAbsTime().si;
1323                 return this.cachedReferencePosition;
1324             }
1325             CategoryLogger.always().error("The reference point of GTU {} is not on any of the lanes on which it is registered",
1326                     this);
1327             for (CrossSection crossSection : this.crossSections)
1328             {
1329                 Lane lane = crossSection.getLanes().get(this.referenceLaneIndex);
1330                 double fraction = fractionalPosition(lane, getReference());
1331                 CategoryLogger.always().error("\tGTU is on lane \"{}\" at fraction {}", lane, fraction);
1332             }
1333             throw new GTUException(
1334                     "The reference point of GTU " + this + " is not on any of the lanes on which it is registered");
1335         }
1336     }
1337 
1338     /** {@inheritDoc} */
1339     @Override
1340     public final Map<Lane, Double> fractionalPositions(final RelativePosition relativePosition) throws GTUException
1341     {
1342         return fractionalPositions(relativePosition, getSimulator().getSimulatorAbsTime());
1343     }
1344 
1345     /** {@inheritDoc} */
1346     @Override
1347     public final Map<Lane, Double> fractionalPositions(final RelativePosition relativePosition, final Time when)
1348             throws GTUException
1349     {
1350         Map<Lane, Double> positions = new LinkedHashMap<>();
1351         for (CrossSection crossSection : this.crossSections)
1352         {
1353             for (Lane lane : crossSection.getLanes())
1354             {
1355                 positions.put(lane, fractionalPosition(lane, relativePosition, when));
1356             }
1357         }
1358         return positions;
1359     }
1360 
1361     /** {@inheritDoc} */
1362     @Override
1363     public final double fractionalPosition(final Lane lane, final RelativePosition relativePosition, final Time when)
1364             throws GTUException
1365     {
1366         return position(lane, relativePosition, when).getSI() / lane.getLength().getSI();
1367     }
1368 
1369     /** {@inheritDoc} */
1370     @Override
1371     public final double fractionalPosition(final Lane lane, final RelativePosition relativePosition) throws GTUException
1372     {
1373         return position(lane, relativePosition).getSI() / lane.getLength().getSI();
1374     }
1375 
1376     /** {@inheritDoc} */
1377     @Override
1378     public final void addTrigger(final Lane lane, final SimEventInterface<Duration> event)
1379     {
1380         throw new UnsupportedOperationException("Method addTrigger is not supported.");
1381     }
1382 
1383     /**
1384      * Sets a vehicle model.
1385      * @param vehicleModel VehicleModel; vehicle model
1386      */
1387     public void setVehicleModel(final VehicleModel vehicleModel)
1388     {
1389         this.vehicleModel = vehicleModel;
1390     }
1391 
1392     /** {@inheritDoc} */
1393     @Override
1394     public VehicleModel getVehicleModel()
1395     {
1396         return this.vehicleModel;
1397     }
1398 
1399     /** {@inheritDoc} */
1400     @Override
1401     @SuppressWarnings("checkstyle:designforextension")
1402     public void destroy()
1403     {
1404         DirectedLanePosition dlp = null;
1405         try
1406         {
1407             dlp = getReferencePosition();
1408         }
1409         catch (GTUException e)
1410         {
1411             // ignore. not important at destroy
1412         }
1413         DirectedPoint location = this.getOperationalPlan() == null ? new DirectedPoint(0.0, 0.0, 0.0) : getLocation();
1414         synchronized (this.lock)
1415         {
1416             for (CrossSection crossSection : this.crossSections)
1417             {
1418                 boolean removeFromParentLink = true;
1419                 for (Lane lane : crossSection.getLanes())
1420                 {
1421                     Length position;
1422                     try
1423                     {
1424                         position = position(lane, getReference());
1425                     }
1426                     catch (GTUException exception)
1427                     {
1428                         // TODO: hard remove over whole network
1429                         // TODO: logger notification
1430                         throw new RuntimeException(exception);
1431                     }
1432                     lane.removeGTU(this, removeFromParentLink, position);
1433                     removeFromParentLink = false;
1434                 }
1435             }
1436         }
1437         if (dlp != null && dlp.getLane() != null)
1438         {
1439             Lane referenceLane = dlp.getLane();
1440             fireTimedEvent(LaneBasedGTU.LANEBASED_DESTROY_EVENT,
1441                     new Object[] {getId(), new OTSPoint3D(location).doubleVector(PositionUnit.METER),
1442                             OTSPoint3D.direction(location, DirectionUnit.EAST_RADIAN), getOdometer(),
1443                             referenceLane.getParentLink().getId(), referenceLane.getId(), dlp.getPosition(),
1444                             dlp.getGtuDirection().name()},
1445                     getSimulator().getSimulatorTime());
1446         }
1447         else
1448         {
1449             fireTimedEvent(LaneBasedGTU.LANEBASED_DESTROY_EVENT,
1450                     new Object[] {getId(), new OTSPoint3D(location).doubleVector(PositionUnit.METER),
1451                             OTSPoint3D.direction(location, DirectionUnit.EAST_RADIAN), getOdometer(), null, null, null, null},
1452                     getSimulator().getSimulatorTime());
1453         }
1454         cancelAllEvents();
1455 
1456         super.destroy();
1457     }
1458 
1459     /** {@inheritDoc} */
1460     @Override
1461     public final Bounds getBounds()
1462     {
1463         double dx = 0.5 * getLength().doubleValue();
1464         double dy = 0.5 * getWidth().doubleValue();
1465         return new Bounds(new Point3d(-dx, -dy, 0.0), new Point3d(dx, dy, 0.0));
1466     }
1467 
1468     /** {@inheritDoc} */
1469     @Override
1470     public final LaneBasedStrategicalPlanner getStrategicalPlanner()
1471     {
1472         return (LaneBasedStrategicalPlanner) super.getStrategicalPlanner();
1473     }
1474 
1475     /** {@inheritDoc} */
1476     @Override
1477     public final LaneBasedStrategicalPlanner getStrategicalPlanner(final Time time)
1478     {
1479         return (LaneBasedStrategicalPlanner) super.getStrategicalPlanner(time);
1480     }
1481 
1482     /** {@inheritDoc} */
1483     @Override
1484     public RoadNetwork getNetwork()
1485     {
1486         return (RoadNetwork) super.getPerceivableContext();
1487     }
1488 
1489     /** {@inheritDoc} */
1490     @Override
1491     public Speed getDesiredSpeed()
1492     {
1493         synchronized (this)
1494         {
1495             Time simTime = getSimulator().getSimulatorAbsTime();
1496             if (this.desiredSpeedTime == null || this.desiredSpeedTime.si < simTime.si)
1497             {
1498                 InfrastructurePerception infra =
1499                         getTacticalPlanner().getPerception().getPerceptionCategoryOrNull(InfrastructurePerception.class);
1500                 SpeedLimitInfo speedInfo;
1501                 if (infra == null)
1502                 {
1503                     speedInfo = new SpeedLimitInfo();
1504                     speedInfo.addSpeedInfo(SpeedLimitTypes.MAX_VEHICLE_SPEED, getMaximumSpeed());
1505                 }
1506                 else
1507                 {
1508                     // Throw.whenNull(infra, "InfrastructurePerception is required to determine the desired speed.");
1509                     speedInfo = infra.getSpeedLimitProspect(RelativeLane.CURRENT).getSpeedLimitInfo(Length.ZERO);
1510                 }
1511                 this.cachedDesiredSpeed =
1512                         Try.assign(() -> getTacticalPlanner().getCarFollowingModel().desiredSpeed(getParameters(), speedInfo),
1513                                 "Parameter exception while obtaining the desired speed.");
1514                 this.desiredSpeedTime = simTime;
1515             }
1516             return this.cachedDesiredSpeed;
1517         }
1518     }
1519 
1520     /** {@inheritDoc} */
1521     @Override
1522     public Acceleration getCarFollowingAcceleration()
1523     {
1524         synchronized (this)
1525         {
1526             Time simTime = getSimulator().getSimulatorAbsTime();
1527             if (this.carFollowingAccelerationTime == null || this.carFollowingAccelerationTime.si < simTime.si)
1528             {
1529                 LanePerception perception = getTacticalPlanner().getPerception();
1530                 // speed
1531                 EgoPerception<?, ?> ego = perception.getPerceptionCategoryOrNull(EgoPerception.class);
1532                 Throw.whenNull(ego, "EgoPerception is required to determine the speed.");
1533                 Speed speed = ego.getSpeed();
1534                 // speed limit info
1535                 InfrastructurePerception infra = perception.getPerceptionCategoryOrNull(InfrastructurePerception.class);
1536                 Throw.whenNull(infra, "InfrastructurePerception is required to determine the desired speed.");
1537                 SpeedLimitInfo speedInfo = infra.getSpeedLimitProspect(RelativeLane.CURRENT).getSpeedLimitInfo(Length.ZERO);
1538                 // leaders
1539                 NeighborsPerception neighbors = perception.getPerceptionCategoryOrNull(NeighborsPerception.class);
1540                 Throw.whenNull(neighbors, "NeighborsPerception is required to determine the car-following acceleration.");
1541                 PerceptionCollectable<HeadwayGTU, LaneBasedGTU> leaders = neighbors.getLeaders(RelativeLane.CURRENT);
1542                 // obtain
1543                 this.cachedCarFollowingAcceleration =
1544                         Try.assign(() -> getTacticalPlanner().getCarFollowingModel().followingAcceleration(getParameters(),
1545                                 speed, speedInfo, leaders), "Parameter exception while obtaining the desired speed.");
1546                 this.carFollowingAccelerationTime = simTime;
1547             }
1548             return this.cachedCarFollowingAcceleration;
1549         }
1550     }
1551 
1552     /** {@inheritDoc} */
1553     @Override
1554     public final TurnIndicatorStatus getTurnIndicatorStatus()
1555     {
1556         return this.turnIndicatorStatus.get();
1557     }
1558 
1559     /** {@inheritDoc} */
1560     @Override
1561     public final TurnIndicatorStatus getTurnIndicatorStatus(final Time time)
1562     {
1563         return this.turnIndicatorStatus.get(time);
1564     }
1565 
1566     /** {@inheritDoc} */
1567     @Override
1568     public final void setTurnIndicatorStatus(final TurnIndicatorStatus turnIndicatorStatus)
1569     {
1570         this.turnIndicatorStatus.set(turnIndicatorStatus);
1571     }
1572 
1573     /** {@inheritDoc} */
1574     @Override
1575     public Length getLateralPosition(final Lane lane) throws GTUException
1576     {
1577         OperationalPlan plan = getOperationalPlan();
1578         if (plan instanceof LaneBasedOperationalPlan && !((LaneBasedOperationalPlan) plan).isDeviative())
1579         {
1580             return Length.ZERO;
1581         }
1582         DirectedLanePosition ref = getReferencePosition();
1583         int latIndex = -1;
1584         int longIndex = -1;
1585         for (int i = 0; i < this.crossSections.size(); i++)
1586         {
1587             List<Lane> lanes = this.crossSections.get(i).getLanes();
1588             if (lanes.contains(lane))
1589             {
1590                 latIndex = lanes.indexOf(lane);
1591             }
1592             if (lanes.contains(ref.getLane()))
1593             {
1594                 longIndex = i;
1595             }
1596         }
1597         Throw.when(latIndex == -1 || longIndex == -1, GTUException.class, "GTU %s is not on %s", getId(), lane);
1598         Lane refCrossSectionLane = this.crossSections.get(longIndex).getLanes().get(latIndex);
1599         DirectedPoint loc = getLocation();
1600         double f = refCrossSectionLane.getCenterLine().projectOrthogonal(loc.x, loc.y);
1601         DirectedPoint p = Try.assign(() -> refCrossSectionLane.getCenterLine().getLocationFraction(f), GTUException.class,
1602                 "GTU %s is not orthogonal to the reference lane.", getId());
1603         double d = p.distance(loc);
1604         d = ref.getGtuDirection().isPlus() ? d : -d;
1605         if (this.crossSections.get(0).getLanes().size() > 1)
1606         {
1607             return Length.instantiateSI(latIndex == 0 ? -d : d);
1608         }
1609         double x2 = p.x + Math.cos(p.getRotZ());
1610         double y2 = p.y + Math.sin(p.getRotZ());
1611         double det = (loc.x - p.x) * (y2 - p.y) - (loc.y - p.y) * (x2 - p.x);
1612         return Length.instantiateSI(det < 0.0 ? -d : d);
1613     }
1614 
1615     /** {@inheritDoc} */
1616     @Override
1617     public void setInstantaneousLaneChange(final boolean instantaneous)
1618     {
1619         this.instantaneousLaneChange = instantaneous;
1620     }
1621 
1622     /** {@inheritDoc} */
1623     @Override
1624     public boolean isInstantaneousLaneChange()
1625     {
1626         return this.instantaneousLaneChange;
1627     }
1628 
1629     /** {@inheritDoc} */
1630     @Override
1631     @SuppressWarnings("checkstyle:designforextension")
1632     public String toString()
1633     {
1634         return String.format("GTU " + getId());
1635     }
1636 
1637     /** Cross section of lanes. */
1638     private static class CrossSection
1639     {
1640 
1641         /** Lanes. */
1642         private final List<Lane> lanes;
1643 
1644         /** GTU directionality. */
1645         private final GTUDirectionality direction;
1646 
1647         /**
1648          * @param lanes List&lt;Lane&gt;; lanes
1649          * @param direction GTUDirectionality; GTU directionality
1650          */
1651         protected CrossSection(final List<Lane> lanes, final GTUDirectionality direction)
1652         {
1653             this.lanes = lanes;
1654             this.direction = direction;
1655         }
1656 
1657         /**
1658          * @return lanes.
1659          */
1660         protected List<Lane> getLanes()
1661         {
1662             return this.lanes;
1663         }
1664 
1665         /**
1666          * @return direction.
1667          */
1668         protected GTUDirectionality getDirection()
1669         {
1670             return this.direction;
1671         }
1672 
1673     }
1674 
1675 }