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