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