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