View Javadoc
1   package org.opentrafficsim.road.gtu;
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.NavigableMap;
10  import java.util.Optional;
11  import java.util.Set;
12  import java.util.TreeMap;
13  
14  import org.djunits.unit.DirectionUnit;
15  import org.djunits.unit.PositionUnit;
16  import org.djunits.value.vdouble.scalar.Acceleration;
17  import org.djunits.value.vdouble.scalar.Direction;
18  import org.djunits.value.vdouble.scalar.Duration;
19  import org.djunits.value.vdouble.scalar.Length;
20  import org.djunits.value.vdouble.scalar.Speed;
21  import org.djunits.value.vdouble.vector.PositionVector;
22  import org.djutils.draw.line.PolyLine2d;
23  import org.djutils.draw.point.DirectedPoint2d;
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.metadata.MetaData;
29  import org.djutils.metadata.ObjectDescriptor;
30  import org.opentrafficsim.base.OtsRuntimeException;
31  import org.opentrafficsim.base.geometry.FractionalProjectionHelper.FractionalFallback;
32  import org.opentrafficsim.base.geometry.OtsGeometryUtil;
33  import org.opentrafficsim.base.geometry.OtsLine2d;
34  import org.opentrafficsim.base.logger.Logger;
35  import org.opentrafficsim.base.parameters.ParameterException;
36  import org.opentrafficsim.core.gtu.Gtu;
37  import org.opentrafficsim.core.gtu.GtuException;
38  import org.opentrafficsim.core.gtu.GtuType;
39  import org.opentrafficsim.core.gtu.RelativePosition;
40  import org.opentrafficsim.core.gtu.TurnIndicatorStatus;
41  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlan;
42  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlanException;
43  import org.opentrafficsim.core.gtu.plan.operational.Segments;
44  import org.opentrafficsim.core.network.LateralDirectionality;
45  import org.opentrafficsim.core.network.Link;
46  import org.opentrafficsim.core.network.NetworkException;
47  import org.opentrafficsim.core.network.Node;
48  import org.opentrafficsim.core.network.route.Route;
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.road.gtu.operational.LaneBasedOperationalPlan;
53  import org.opentrafficsim.road.gtu.strategical.LaneBasedStrategicalPlanner;
54  import org.opentrafficsim.road.gtu.tactical.LaneBasedTacticalPlanner;
55  import org.opentrafficsim.road.network.CrossSectionLink;
56  import org.opentrafficsim.road.network.Lane;
57  import org.opentrafficsim.road.network.LanePosition;
58  import org.opentrafficsim.road.network.RoadNetwork;
59  import org.opentrafficsim.road.network.object.LaneBasedObject;
60  import org.opentrafficsim.road.network.object.detector.LaneDetector;
61  
62  import nl.tudelft.simulation.dsol.SimRuntimeException;
63  import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEventInterface;
64  
65  /**
66   * This class contains most of the code that is needed to run a lane based GTU. <br>
67   * 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
68   * 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
69   * 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
70   * dozens of Lanes at the same time.
71   * <p>
72   * When calculating a headway, the GTU has to look in successive lanes. When Lanes (or underlying CrossSectionLinks) diverge,
73   * the headway algorithms have to look at multiple Lanes and return the minimum headway in each of the Lanes. When the Lanes (or
74   * underlying CrossSectionLinks) converge, "parallel" traffic is not taken into account in the headway calculation. Instead, gap
75   * acceptance algorithms or their equivalent should guide the merging behavior.
76   * <p>
77   * To decide its movement, an AbstractLaneBasedGtu applies its car following algorithm and lane change algorithm to set the
78   * acceleration and any lane change operation to perform. It then schedules the triggers that will add it to subsequent lanes
79   * and remove it from current lanes as needed during the time step that is has committed to. Finally, it re-schedules its next
80   * movement evaluation with the simulator.
81   * <p>
82   * Copyright (c) 2013-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
83   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
84   * </p>
85   * @author Alexander Verbraeck
86   * @author Peter Knoppers
87   * @author Wouter Schakel
88   */
89  public class LaneBasedGtu extends Gtu implements LaneBasedObject
90  {
91      /**
92       * Margin to add to plan length to check if the path will enter the next section. This is because the plan might follow a
93       * shorter path than the lane center line.
94       */
95      private static final Length EVENT_MARGIN = Length.ofSI(50.0);
96  
97      /** Lane. */
98      private final HistoricalValue<Lane> lane;
99  
100     /** Time of reference position cache. */
101     private Duration cachedPositionTime = null;
102 
103     /** Cached reference position. */
104     private LanePosition cachedPosition = null;
105 
106     /** Time of roaming position cache. */
107     private Duration cachedRoamingPositionTime = null;
108 
109     /** Cached roaming position. */
110     private LanePosition cachedRoamingPosition = null;
111 
112     /** Lanes for which enter events are scheduled. */
113     private NavigableMap<Duration, Lane> pendingLanesToEnter = new TreeMap<>();
114 
115     /** Pending enter events. */
116     private Map<Lane, SimEventInterface<Duration>> pendingEnterEvents = new LinkedHashMap<>();
117 
118     /** Event to leave lane and start roaming. */
119     private SimEventInterface<Duration> roamEvent;
120 
121     /** Detector triggers (detector and odometer at trigger time). */
122     private Map<LaneDetector, Length> detectorTriggers = new LinkedHashMap<>();
123 
124     /** Detector events. */
125     private Set<SimEventInterface<Duration>> detectorEvents = new LinkedHashSet<>();
126 
127     /** Turn indicator status. */
128     private final Historical<TurnIndicatorStatus> turnIndicatorStatus;
129 
130     /** Vehicle model. */
131     private VehicleModel vehicleModel = VehicleModel.MINMAX;
132 
133     /** Lane bookkeeping. */
134     private LaneBookkeeping bookkeeping = LaneBookkeeping.START;
135 
136     /** Distance over which the GTU should not change lane after being created. */
137     private Length noLaneChangeDistance;
138 
139     /** Lane change direction. */
140     private final Historical<LateralDirectionality> laneChangeDirection;
141 
142     /**
143      * The lane-based event type for pub/sub indicating a move.<br>
144      * Payload: [String gtuId, PositionVector currentPosition, Direction currentDirection, Speed speed, Acceleration
145      * acceleration, TurnIndicatorStatus turnIndicatorStatus, Length odometer, String linkId, String laneId, Length
146      * positionOnLane]
147      */
148     public static final EventType LANEBASED_MOVE_EVENT = new EventType("LANEBASEDGTU.MOVE", new MetaData("Lane based GTU moved",
149             "Lane based GTU moved",
150             new ObjectDescriptor[] {new ObjectDescriptor("GTU id", "GTU id", String.class),
151                     new ObjectDescriptor("Position", "Position", PositionVector.class),
152                     new ObjectDescriptor("Direction", "Direction", Direction.class),
153                     new ObjectDescriptor("Speed", "Speed", Speed.class),
154                     new ObjectDescriptor("Acceleration", "Acceleration", Acceleration.class),
155                     new ObjectDescriptor("TurnIndicatorStatus", "Turn indicator status", String.class),
156                     new ObjectDescriptor("Odometer", "Odometer value", Length.class),
157                     new ObjectDescriptor("Link id", "Link id", String.class),
158                     new ObjectDescriptor("Lane id", "Lane id", String.class),
159                     new ObjectDescriptor("Longitudinal position on lane", "Longitudinal position on lane", Length.class)}));
160 
161     /**
162      * The lane-based event type for pub/sub indicating destruction of the GTU.<br>
163      * Payload: [String gtuId, PositionVector finalPosition, Direction finalDirection, Length finalOdometer, String linkId,
164      * String laneId, Length positionOnLane]
165      */
166     public static final EventType LANEBASED_DESTROY_EVENT = new EventType("LANEBASEDGTU.DESTROY", new MetaData(
167             "Lane based GTU destroyed", "Lane based GTU destroyed",
168             new ObjectDescriptor[] {new ObjectDescriptor("GTU id", "GTU id", String.class),
169                     new ObjectDescriptor("Position", "Position", PositionVector.class),
170                     new ObjectDescriptor("Direction", "Direction", Direction.class),
171                     new ObjectDescriptor("Odometer", "Odometer value", Length.class),
172                     new ObjectDescriptor("Link id", "Link id", String.class),
173                     new ObjectDescriptor("Lane id", "Lane id", String.class),
174                     new ObjectDescriptor("Longitudinal position on lane", "Longitudinal position on lane", Length.class)}));
175 
176     /**
177      * The event type for pub/sub indicating that the GTU entered a lane in either the lateral or longitudinal direction.<br>
178      * Payload: [String gtuId, String link id, String lane id]
179      */
180     public static final EventType LANE_ENTER_EVENT = new EventType("LANE.ENTER",
181             new MetaData("Lane based GTU entered lane", "Front of lane based GTU entered lane",
182                     new ObjectDescriptor[] {new ObjectDescriptor("GTU id", "GTU id", String.class),
183                             new ObjectDescriptor("Link id", "Link id", String.class),
184                             new ObjectDescriptor("Lane id", "Lane id", String.class)}));
185 
186     /**
187      * The event type for pub/sub indicating that the GTU exited a lane in either the lateral or longitudinal direction.<br>
188      * Payload: [String gtuId, String link id, String lane id]
189      */
190     public static final EventType LANE_EXIT_EVENT = new EventType("LANE.EXIT",
191             new MetaData("Lane based GTU exited lane", "Rear of lane based GTU exited lane",
192                     new ObjectDescriptor[] {new ObjectDescriptor("GTU id", "GTU id", String.class),
193                             new ObjectDescriptor("Link id", "Link id", String.class),
194                             new ObjectDescriptor("Lane id", "Lane id", String.class)}));
195 
196     /**
197      * The event type for pub/sub indicating that the GTU changed lane, laterally only.<br>
198      * Payload: [String gtuId, LateralDirectionality direction, String linkId, String fromLaneId, Length position]
199      */
200     public static final EventType LANE_CHANGE_EVENT = new EventType("LANE.CHANGE",
201             new MetaData("Lane based GTU changes lane", "Lane based GTU changes lane",
202                     new ObjectDescriptor[] {new ObjectDescriptor("GTU id", "GTU id", String.class),
203                             new ObjectDescriptor("Lateral direction of lane change", "Lateral direction of lane change",
204                                     String.class),
205                             new ObjectDescriptor("Link id", "Link id", String.class),
206                             new ObjectDescriptor("Lane id of exited lane", "Lane id of exited lane", String.class),
207                             new ObjectDescriptor("Position along exited lane", "Position along exited lane", Length.class)}));
208 
209     /**
210      * Construct a Lane Based GTU.
211      * @param id the id of the GTU
212      * @param gtuType the type of GTU, e.g. TruckType, CarType, BusType
213      * @param length the maximum length of the GTU (parallel with driving direction)
214      * @param width the maximum width of the GTU (perpendicular to driving direction)
215      * @param maximumSpeed the maximum speed of the GTU (in the driving direction)
216      * @param front front distance relative to the reference position
217      * @param network the network that the GTU is initially registered in
218      * @throws GtuException when initial values are not correct
219      */
220     public LaneBasedGtu(final String id, final GtuType gtuType, final Length length, final Length width,
221             final Speed maximumSpeed, final Length front, final RoadNetwork network) throws GtuException
222     {
223         super(id, gtuType, network.getSimulator(), network, length, width, front, maximumSpeed);
224         HistoryManager historyManager = network.getSimulator().getReplication().getHistoryManager(network.getSimulator());
225         this.lane = new HistoricalValue<>(historyManager, this);
226         this.turnIndicatorStatus = new HistoricalValue<>(historyManager, this, TurnIndicatorStatus.NOTPRESENT);
227         this.laneChangeDirection = new HistoricalValue<>(historyManager, this, LateralDirectionality.NONE);
228     }
229 
230     /**
231      * Initializes the GTU.
232      * @param strategicalPlanner the strategical planner (e.g., route determination) to use
233      * @param initialLocation initial location
234      * @param initialSpeed the initial speed of the car on the lane
235      * @throws NetworkException when the GTU cannot be placed on the given lane
236      * @throws SimRuntimeException when the move method cannot be scheduled
237      * @throws GtuException when initial values are not correct
238      */
239     @SuppressWarnings("checkstyle:designforextension")
240     public synchronized void init(final LaneBasedStrategicalPlanner strategicalPlanner, final DirectedPoint2d initialLocation,
241             final Speed initialSpeed) throws NetworkException, SimRuntimeException, GtuException
242     {
243         Throw.when(null == initialLocation, GtuException.class, "InitialLongitudinalPositions is null");
244 
245         // TODO: move this to super.init(...), and remove setOperationalPlan(...) method
246         // Give the GTU a 1 micrometer long operational plan, or a stand-still plan, so the first move and events will work
247         Duration now = getSimulator().getSimulatorTime();
248         if (initialSpeed.lt(OperationalPlan.DRIFTING_SPEED))
249         {
250             setOperationalPlan(OperationalPlan.standStill(this, initialLocation, now, Duration.ofSI(1E-6)));
251         }
252         else
253         {
254             Point2d p2 = new Point2d(initialLocation.x + 1E-6 * Math.cos(initialLocation.getDirZ()),
255                     initialLocation.y + 1E-6 * Math.sin(initialLocation.getDirZ()));
256             OtsLine2d path = new OtsLine2d(initialLocation, p2);
257             setOperationalPlan(new OperationalPlan(this, path, now,
258                     Segments.off(initialSpeed, path.getTypedLength().divide(initialSpeed), Acceleration.ZERO)));
259         }
260 
261         LanePosition longitudinalPosition = getRoamingPosition(initialLocation);
262         endRoaming(longitudinalPosition); // enters lane if sufficiently close
263         this.cachedPositionTime = null; // endRoaming() -> enterLane() -> getPosition() caches cachedPosition = null
264 
265         // initiate the actual move
266         super.init(strategicalPlanner, initialLocation, initialSpeed);
267 
268         this.cachedPositionTime = null; // remove cache, it may be invalid as the above init results in a lane change
269     }
270 
271     /**
272      * {@inheritDoc} The lane the GTU is on will be exited.
273      */
274     @Override
275     public synchronized void setParent(final Gtu gtu) throws GtuException
276     {
277         exitLane();
278         super.setParent(gtu);
279     }
280 
281     /**
282      * Removes the registration between this GTU and the lane.
283      */
284     protected synchronized void exitLane()
285     {
286         LanePosition exitLanePosition = getPosition();
287         if (exitLanePosition != null)
288         {
289             exitLanePosition.lane().removeGtu(this, true, exitLanePosition.position());
290             fireTimedEvent(LaneBasedGtu.LANE_EXIT_EVENT,
291                     new Object[] {getId(), exitLanePosition.lane().getLink().getId(), exitLanePosition.lane().getId()},
292                     getSimulator().getSimulatorTime());
293         }
294         this.lane.set(null);
295     }
296 
297     /**
298      * Enters a new lane, and removes the GTU from the previous lane.
299      * @param lane lane
300      * @param fraction fractional position
301      */
302     @SuppressWarnings("hiddenfield")
303     protected synchronized void enterLane(final Lane lane, final double fraction)
304     {
305         // The reason this method does not use exitLane() is that we do not want to set the lane to null in the historical.
306 
307         /*
308          * We cannot use the getLane() methods to obtain the exit lane. This is because at the time we enter the next lane (i.e.
309          * now) that method will return the lane that is entered at that time.
310          */
311         Lane exitLane = this.lane.get();
312         Length exitPosition = exitLane == null ? null : getPosition(exitLane);
313 
314         this.lane.set(lane);
315         Try.execute(() -> lane.addGtu(this, fraction), OtsRuntimeException.class, "Entering lane where the GTU is already at.");
316 
317         fireTimedEvent(LaneBasedGtu.LANE_ENTER_EVENT, new Object[] {getId(), lane.getLink().getId(), lane.getId()},
318                 getSimulator().getSimulatorTime());
319 
320         // First enter, then exit, as e.g. TrafficLightDetector checks whether a collection is empty to trigger an event that
321         // the detector is empty. However, the GTU might have entered the next lane where the detector continues.
322         if (exitLane != null)
323         {
324             exitLane.removeGtu(this, true, exitPosition);
325             this.pendingLanesToEnter.values().remove(lane);
326             this.pendingEnterEvents.remove(lane);
327             fireTimedEvent(LaneBasedGtu.LANE_EXIT_EVENT, new Object[] {getId(), exitLane.getLink().getId(), exitLane.getId()},
328                     getSimulator().getSimulatorTime());
329             if (exitLane.getLink().equals(lane.getLink()))
330             {
331                 // Same link, so must be a lane change
332                 setLaneChangeDirection(LateralDirectionality.NONE);
333                 String direction = lane.equals(exitLane.getLeft(getType()).orElse(null)) ? LateralDirectionality.LEFT.name()
334                         : LateralDirectionality.RIGHT.name();
335                 fireTimedEvent(LaneBasedGtu.LANE_CHANGE_EVENT,
336                         new Object[] {getId(), direction, exitLane.getLink().getId(), exitLane.getId(), exitPosition},
337                         getSimulator().getSimulatorTime());
338             }
339         }
340 
341         // Clear cache of old lane that getPosition() above created (or something else previously at this time)
342         this.cachedPositionTime = null;
343         this.cachedPosition = null;
344     }
345 
346     /**
347      * Returns whether the GTU is roaming (i.e. not on a lane). In this case all methods on lane and position should not be
348      * called as they will return {@code null}.
349      * @return whether the GTU is roaming
350      */
351     public boolean isRoaming()
352     {
353         return getLane() == null;
354     }
355 
356     /**
357      * Returns whether the GTU is roaming (i.e. not on a lane). In this case all methods on lane and position should not be
358      * called as they will return {@code null}.
359      * @param time simulation time to get the lane for
360      * @return whether the GTU is roaming
361      */
362     public boolean isRoaming(final Duration time)
363     {
364         return getLane(time) == null;
365     }
366 
367     /**
368      * Returns the position when the GTU is on a lane, or the roaming position otherwise.
369      * @return position when the GTU is on a lane, or the roaming position otherwise
370      */
371     public LanePosition getPositionOrRoaming()
372     {
373         return isRoaming() ? getRoamingPosition() : getPosition();
374     }
375 
376     @Override
377     public synchronized Lane getLane()
378     {
379         return this.lane.get();
380     }
381 
382     /**
383      * Returns the lane at the given time. This may be in the future during the plan, in which case it is a prospective lane.
384      * @param time simulation time to get the lane for
385      * @return lane at given time
386      */
387     public synchronized Lane getLane(final Duration time)
388     {
389         return this.pendingLanesToEnter.isEmpty() || this.pendingLanesToEnter.firstKey().gt(time) ? this.lane.get(time)
390                 : this.pendingLanesToEnter.floorEntry(time).getValue();
391     }
392 
393     /**
394      * Returns the lane and reference position on the lane of the GTU.
395      * @return lane position at time
396      */
397     public synchronized LanePosition getPosition()
398     {
399         if (!getSimulator().getSimulatorTime().equals(this.cachedPositionTime))
400         {
401             this.cachedPositionTime = getSimulator().getSimulatorTime();
402             this.cachedPosition = getPosition(getReference(), this.cachedPositionTime);
403         }
404         return this.cachedPosition;
405     }
406 
407     /**
408      * Returns the lane and reference position on the lane of the GTU.
409      * @param time simulation time to get the position for
410      * @return lane position at time
411      */
412     public synchronized LanePosition getPosition(final Duration time)
413     {
414         return getPosition(getReference(), time);
415     }
416 
417     /**
418      * Returns the lane and relative position on the lane of the GTU. The relative position is calculated by shifting the
419      * position of the reference by {@code dx} of the relative position.
420      * @param relativePosition relative position
421      * @return lane position
422      */
423     public synchronized LanePosition getPosition(final RelativePosition relativePosition)
424     {
425         LanePosition ref = getPosition();
426         return new LanePosition(ref.lane(), ref.position().plus(relativePosition.dx()));
427     }
428 
429     /**
430      * Returns the lane and relative position on the lane of the GTU. The relative position is calculated by shifting the
431      * position of the reference by {@code dx} of the relative position.
432      * @param relativePosition relative position
433      * @param time simulation time to get the position for
434      * @return lane position at time
435      */
436     public synchronized LanePosition getPosition(final RelativePosition relativePosition, final Duration time)
437     {
438         Lane laneAtTime = getLane(time);
439         if (laneAtTime == null)
440         {
441             return null;
442         }
443         return new LanePosition(laneAtTime, getPosition(laneAtTime, relativePosition, time));
444     }
445 
446     /**
447      * Returns the projected position of the GTU on the given lane.
448      * @param lane lane
449      * @return projected position of the GTU on the given lane
450      */
451     @SuppressWarnings("hiddenfield")
452     public synchronized Length getPosition(final Lane lane)
453     {
454         return getPosition(lane, getReference(), getSimulator().getSimulatorTime());
455     }
456 
457     /**
458      * Returns the projected position of the GTU on the given lane.
459      * @param lane lane
460      * @param time simulation time
461      * @return projected position of the GTU on the given lane
462      */
463     @SuppressWarnings("hiddenfield")
464     public synchronized Length getPosition(final Lane lane, final Duration time)
465     {
466         return getPosition(lane, getReference(), time);
467     }
468 
469     /**
470      * Returns the projected position of the GTU on the given lane. The relative position is calculated by shifting the position
471      * of the reference by {@code dx} of the relative position.
472      * @param lane lane
473      * @param relativePosition relative position
474      * @return projected position of the GTU on the given lane
475      */
476     @SuppressWarnings("hiddenfield")
477     public synchronized Length getPosition(final Lane lane, final RelativePosition relativePosition)
478     {
479         return getPosition(lane, relativePosition, getSimulator().getSimulatorTime());
480     }
481 
482     @Override
483     public synchronized Length getLongitudinalPosition()
484     {
485         return getPosition().position();
486     }
487 
488     /**
489      * Returns the projected position of the GTU on the given lane. The relative position is calculated by shifting the position
490      * of the reference by {@code dx} of the relative position.
491      * @param lane lane
492      * @param relativePosition relative position
493      * @param time simulation time
494      * @return projected position of the GTU on the given lane
495      */
496     @SuppressWarnings("hiddenfield")
497     public synchronized Length getPosition(final Lane lane, final RelativePosition relativePosition, final Duration time)
498     {
499         DirectedPoint2d p = Try.assign(() -> getOperationalPlan(time).getLocation(time, getReference()),
500                 "Operational plan at time is not valid at time.");
501         double f = lane.getCenterLine().projectFractionalAt(lane.getLink().getStartNode().getHeading(),
502                 lane.getLink().getEndNode().getHeading(), p.x, p.y, FractionalFallback.ORTHOGONAL_EXTENDED);
503         return lane.getLength().times(f).plus(relativePosition.dx());
504     }
505 
506     /**
507      * Returns the nearest lane position on the route / network. It is not strictly guaranteed that the position is closest, as
508      * this method will only search on links where either of the nodes is the closest node.
509      * @return nearest lane position on the route / network
510      * @throws IllegalStateException if the GTU is on a lane
511      */
512     public synchronized LanePosition getRoamingPosition()
513     {
514         Throw.when(getLane() != null, IllegalStateException.class, "GTU that is on a lane does not have a roaming position.");
515         if (!getSimulator().getSimulatorTime().equals(this.cachedRoamingPositionTime))
516         {
517             this.cachedRoamingPositionTime = getSimulator().getSimulatorTime();
518             this.cachedRoamingPosition = getRoamingPosition(getLocation());
519         }
520         return this.cachedRoamingPosition;
521     }
522 
523     /**
524      * Returns the nearest lane position on the route / network. It is not strictly guaranteed that the position is closest, as
525      * this method will only search on links where either of the nodes is the closest node.
526      * @param location location to find the nearest lane position for
527      * @return nearest lane position on the route / network
528      */
529     protected LanePosition getRoamingPosition(final Point2d location)
530     {
531         Optional<Route> route = getStrategicalPlanner() == null ? Optional.empty() : getStrategicalPlanner().getRoute();
532         // TODO instead of getNetwork().getNodeMap().values(), using spatial tree would be a good alternative
533         // perhaps even a findClosest() method.
534         Iterable<Node> nodes = route.isEmpty() ? getNetwork().getNodeMap().values() : route.get().getNodes();
535         List<CrossSectionLink> nearestLinks = new ArrayList<>(2);
536         double minDist = Double.POSITIVE_INFINITY;
537         for (Node node : nodes)
538         {
539             double dist = node.getPoint().distance(location);
540             if (dist < minDist)
541             {
542                 nearestLinks.clear();
543                 for (Link link : node.getLinks())
544                 {
545                     if (link instanceof CrossSectionLink cLink && (route.isEmpty() || route.get().containsLink(link)))
546                     {
547                         nearestLinks.add(cLink);
548                         minDist = dist;
549                     }
550                 }
551             }
552         }
553         Throw.when(nearestLinks.isEmpty(), IllegalStateException.class, "No lane in the route or in the network.");
554         LanePosition roamingPosition = null;
555         minDist = Double.POSITIVE_INFINITY;
556         for (CrossSectionLink nearestLink : nearestLinks)
557         {
558             for (Lane checkLane : nearestLink.getLanesAndShoulders())
559             {
560                 double fraction = checkLane.getCenterLine().projectOrthogonalSnapAt(location.x, location.y);
561                 DirectedPoint2d point = checkLane.getCenterLine().getLocationFraction(fraction);
562                 double dist = point.distance(location);
563                 if (dist < minDist)
564                 {
565                     roamingPosition =
566                             new LanePosition(checkLane, Length.ofSI(checkLane.getCenterLine().getLength() * fraction));
567                     minDist = dist;
568                 }
569             }
570         }
571         return roamingPosition;
572     }
573 
574     /**
575      * Deviation from lane center. Positive values are left, negative values are right.
576      * @return deviation from lane center line, positive values are left, negative values are right
577      */
578     public synchronized Length getDeviation()
579     {
580         return getDeviation(getLane(), getLocation());
581     }
582 
583     /**
584      * Deviation from lane center at time. Positive values are left, negative values are right.
585      * @param time simulation time
586      * @return deviation from lane center line, positive values are left, negative values are right
587      */
588     public synchronized Length getDeviation(final Duration time)
589     {
590         return getDeviation(getLane(time), getLocation(time));
591     }
592 
593     /**
594      * Returns the deviation from the center line of the given lane, using extension if the GTU is not on the lane. Positive
595      * values are left, negative values are right.
596      * @param lane lane
597      * @param location location
598      * @return deviation from lane center line, positive values are left, negative values are right
599      */
600     @SuppressWarnings("hiddenfield")
601     protected Length getDeviation(final Lane lane, final Point2d location)
602     {
603         double fraction = lane.getCenterLine().projectFractionalAt(lane.getLink().getStartNode().getHeading(),
604                 lane.getLink().getEndNode().getHeading(), location.x, location.y, FractionalFallback.ORTHOGONAL_EXTENDED);
605         DirectedPoint2d a = lane.getCenterLine().getLocationFractionExtended(fraction);
606         Point2d b = new Point2d(a.x + Math.cos(a.dirZ), a.y + Math.sin(a.dirZ));
607         double sign = (b.x - a.x) * (location.y - a.y) - (b.y - a.y) * (location.x - a.x) > 0.0 ? 1.0 : -1.0;
608         return Length.ofSI(sign * lane.getCenterLine().getLocationFractionExtended(fraction).distance(location));
609     }
610 
611     /**
612      * Change lanes instantaneously.
613      * @param laneChangeDirection the direction to change to
614      */
615     @SuppressWarnings("hiddenfield")
616     public synchronized void changeLaneInstantaneously(final LateralDirectionality laneChangeDirection)
617     {
618         LanePosition from = getPosition();
619         Set<Lane> adjLanes = from.lane().accessibleAdjacentLanesPhysical(laneChangeDirection, getType());
620         Lane adjLane = adjLanes.iterator().next();
621         Length position = getPosition(adjLane);
622         cancelAllEvents();
623         enterLane(adjLane, position.si / adjLane.getLength().si);
624         this.cachedPositionTime = null;
625         this.cachedPosition = null;
626 
627         // fire event
628         this.fireTimedEvent(
629                 LaneBasedGtu.LANE_CHANGE_EVENT, new Object[] {getId(), laneChangeDirection.name(),
630                         from.lane().getLink().getId(), from.lane().getId(), from.position()},
631                 getSimulator().getSimulatorTime());
632     }
633 
634     @Override
635     @SuppressWarnings("checkstyle:designforextension")
636     protected synchronized boolean move(final DirectedPoint2d fromLocation)
637             throws SimRuntimeException, GtuException, NetworkException, ParameterException
638     {
639         if (this.isDestroyed())
640         {
641             return false;
642         }
643         try
644         {
645             // cancel events, if any
646             cancelAllEvents();
647 
648             // generate the next operational plan and carry it out
649             try
650             {
651                 boolean error = super.move(fromLocation);
652                 if (error)
653                 {
654                     return error;
655                 }
656             }
657             catch (Exception exception)
658             {
659                 Logger.ots().error(exception);
660                 Logger.ots().error("  GTU {} DESTROYED AND REMOVED FROM THE SIMULATION", this);
661                 destroy();
662                 cancelAllEvents();
663                 return true;
664             }
665 
666             scheduleLaneEvents();
667             findDetectorTriggers(true);
668 
669             LanePosition position = getPosition();
670             String linkId = position != null ? position.lane().getLink().getId() : null;
671             String laneId = position != null ? position.lane().getId() : null;
672             Length pos = position != null ? position.position() : null;
673             fireTimedEvent(LaneBasedGtu.LANEBASED_MOVE_EVENT,
674                     new Object[] {getId(),
675                             new PositionVector(new double[] {fromLocation.x, fromLocation.y}, PositionUnit.METER),
676                             new Direction(fromLocation.getDirZ(), DirectionUnit.EAST_RADIAN), getSpeed(), getAcceleration(),
677                             getTurnIndicatorStatus().name(), getOdometer(), linkId, laneId, pos},
678                     getSimulator().getSimulatorTime());
679 
680             return false;
681 
682         }
683         catch (Exception ex)
684         {
685             try
686             {
687                 getErrorHandler().handle(this, ex);
688             }
689             catch (Exception exception)
690             {
691                 throw new GtuException(exception);
692             }
693             return true;
694         }
695 
696     }
697 
698     /**
699      * Cancels all future events.
700      */
701     protected void cancelAllEvents()
702     {
703         if (this.roamEvent != null)
704         {
705             getSimulator().cancelEvent(this.roamEvent);
706             this.roamEvent = null;
707         }
708         this.pendingLanesToEnter.clear();
709         this.pendingEnterEvents.values().forEach((event) -> getSimulator().cancelEvent(event));
710         this.pendingEnterEvents.clear();
711         // we should clear all detector events as triggers that remain in this.detectorTriggers will be rescheduled in move
712         this.detectorEvents.forEach((event) -> getSimulator().cancelEvent(event));
713         this.detectorEvents.clear();
714         findDetectorTriggers(false);
715     }
716 
717     /**
718      * Schedules when a lane is entered (and a previous one is left). Also schedules start of roaming (GTU not having a lane),
719      * or ends roaming if the GTU is on a lane.
720      */
721     protected void scheduleLaneEvents()
722     {
723         /*
724          * Implementation note: this method cannot use the getPosition() methods without lane input, as those depend on
725          * this.pendingLanesToEnter which this method is responsible for filling.
726          */
727         Lane laneOnPath = getLane();
728         if (laneOnPath == null)
729         {
730             // Check whether the GTU is on the network and stops roaming
731             endRoaming(getRoamingPosition());
732             laneOnPath = getLane();
733         }
734         // Add distance as plan path may be shorter than lane center line path
735         Length remain = getOperationalPlan().getTotalLength().plus(EVENT_MARGIN);
736         Length planStartPositionAtLaneOnPath = getLongitudinalPosition();
737         boolean checkLaneChange = getOperationalPlan() instanceof LaneBasedOperationalPlan lbop && lbop.isDeviative()
738                 && this.bookkeeping.isEdge() && (!this.bookkeeping.isInformed() || !this.laneChangeDirection.get().isNone());
739         while (true)
740         {
741             Duration enterTime;
742             if (laneOnPath.getLength().minus(planStartPositionAtLaneOnPath).lt(remain))
743             {
744                 CrossSectionLink link = laneOnPath.getLink();
745                 Link nextLink =
746                         Try.assign(() -> getStrategicalPlanner().nextLink(link, getType()), "Network issue during scheduling.");
747                 PolyLine2d enterLine = nextLink != null && nextLink instanceof CrossSectionLink
748                         ? ((CrossSectionLink) nextLink).getStartLine() : link.getEndLine();
749                 enterTime = timeAtLine(enterLine, getReference());
750             }
751             else
752             {
753                 enterTime = null;
754             }
755             Duration lastTimeOnLane = enterTime == null ? getOperationalPlan().getEndTime() : enterTime;
756 
757             // Check whether a lane is entered laterally before longitudinally
758             if (checkLaneChange && (enterTime == null || !Double.isNaN(enterTime.si)))
759             {
760                 Duration firstTimeOnLane = this.pendingLanesToEnter.isEmpty() ? getSimulator().getSimulatorTime()
761                         : this.pendingLanesToEnter.lastKey();
762                 Length startOvershoot = laneLateralOvershoot(firstTimeOnLane);
763                 Length endOvershoot = laneLateralOvershoot(lastTimeOnLane);
764                 if (startOvershoot.ge0())
765                 {
766                     // Already overshot the edge, change lane instantaneously
767                     LateralDirectionality lcDirection =
768                             getDeviation(firstTimeOnLane).ge0() ? LateralDirectionality.LEFT : LateralDirectionality.RIGHT;
769                     changeLaneInstantaneously(lcDirection);
770                 }
771                 else if (endOvershoot.gt0() && startOvershoot.le0())
772                 {
773                     Length deviation = getDeviation(lastTimeOnLane);
774                     boolean noAdjacentLane =
775                             (deviation.gt0() ? laneOnPath.getLeft(getType()) : laneOnPath.getRight(getType())).isEmpty();
776                     boolean willRoam = noAdjacentLane && endOvershoot.gt(getWidth().times(0.5));
777 
778                     Duration lateralCrossingTime = getTimeOfLateralCrossing(firstTimeOnLane, lastTimeOnLane, willRoam);
779                     if (lateralCrossingTime != null && willRoam)
780                     {
781                         this.roamEvent =
782                                 getSimulator().scheduleEventAbs(Duration.ofSI(lateralCrossingTime.si), () -> exitLane());
783                         return; // no further lanes to check when roaming
784                     }
785                     else if (lateralCrossingTime != null)
786                     {
787                         // Regular lane change
788                         LateralDirectionality lcDirection = getDeviation(lateralCrossingTime).ge0() ? LateralDirectionality.LEFT
789                                 : LateralDirectionality.RIGHT;
790                         Length distanceTillLaneChange =
791                                 getPosition(laneOnPath, lateralCrossingTime).minus(planStartPositionAtLaneOnPath);
792                         laneOnPath = laneOnPath.getAdjacentLane(lcDirection, getType()).orElse(null);
793                         if (laneOnPath != null)
794                         {
795                             Length positionOnTargetLane = getPosition(laneOnPath, lateralCrossingTime);
796                             double fractionOnTargetLane = positionOnTargetLane.si / laneOnPath.getLength().si;
797                             planStartPositionAtLaneOnPath = positionOnTargetLane.minus(distanceTillLaneChange);
798                             this.pendingLanesToEnter.put(lateralCrossingTime, laneOnPath);
799                             Lane finalLane = laneOnPath;
800                             this.pendingEnterEvents.put(laneOnPath, getSimulator().scheduleEventAbs(
801                                     Duration.ofSI(lateralCrossingTime.si), () -> enterLane(finalLane, fractionOnTargetLane)));
802                         }
803                         else
804                         {
805                             // no lane to change to, curve back or roam
806                             this.roamEvent =
807                                     getSimulator().scheduleEventAbs(Duration.ofSI(lateralCrossingTime.si), () -> exitLane());
808                             return; // no further lanes to check when roaming
809                         }
810                     }
811                     else
812                     {
813                         throw new OtsRuntimeException("GTU " + getId() + " expects a lane change from lane " + laneOnPath
814                                 + " as the overshoot goes from (-) to (+) in the episode, but no edge crossing was found.");
815                     }
816                 }
817             }
818 
819             if (enterTime != null)
820             {
821                 planStartPositionAtLaneOnPath = planStartPositionAtLaneOnPath.minus(laneOnPath.getLength());
822                 laneOnPath = getNextLaneForRoute(laneOnPath).orElse(null);
823                 if (laneOnPath == null && !Double.isNaN(enterTime.si))
824                 {
825                     // Check longitudinal roaming
826                     Duration timeRefLeaving = enterTime; // next link but no lane change, or determined at end of current link
827                     Length distanceRearLeaving = Try.assign(() -> getOperationalPlan().getTraveledDistance(timeRefLeaving),
828                             "Time link is left is beyond plan.").minus(getRear().dx());
829                     if (distanceRearLeaving.le(getOperationalPlan().getTotalLength()))
830                     {
831                         Duration timeRearLeaving = Try.assign(() -> getOperationalPlan().getTimeAtDistance(distanceRearLeaving),
832                                 "Distance till rear leaves link is beyond plan.");
833                         this.roamEvent = getSimulator().scheduleEventAbs(Duration.ofSI(timeRearLeaving.si), () -> exitLane());
834                     }
835                     return; // no further lanes to check
836                 }
837                 else
838                 {
839                     if (Double.isNaN(enterTime.si))
840                     {
841                         // NaN indicates we just missed it between moves, due to curvature and small gaps
842                         enterTime = getSimulator().getSimulatorTime();
843                         Logger.ots().error("GTU {} enters lane through hack.", getId());
844                     }
845                     this.pendingLanesToEnter.put(enterTime, laneOnPath);
846                     Lane finalLane = laneOnPath;
847                     this.pendingEnterEvents.put(laneOnPath,
848                             getSimulator().scheduleEventAbs(Duration.ofSI(enterTime.si), () -> enterLane(finalLane, 0.0)));
849                 }
850             }
851             else
852             {
853                 return; // no next link within plan, possible lane change on current link already checked
854             }
855         }
856     }
857 
858     /**
859      * Estimates when the path crosses a lateral lane boundary assuming the GTU is within the boundary. This is estimated
860      * through linear interpolation between the start and end deviation values of a line segment of the path. The line segment
861      * is found through a binary search.
862      * @param fromTime first time to consider on the lane
863      * @param toTime last time to consider on the lane
864      * @param roam when {@code true} the full width of the GTU is considered, when {@code false} only the reference position
865      * @return when the path crosses a lateral lane boundary
866      */
867     protected Duration getTimeOfLateralCrossing(final Duration fromTime, final Duration toTime, final boolean roam)
868     {
869         try
870         {
871             Length startPosition = getOperationalPlan().getTraveledDistance(fromTime);
872             Length endPosition = getOperationalPlan().getTraveledDistance(toTime);
873             Length lateralMargin = roam ? getWidth().times(0.5) : Length.ZERO;
874             OtsLine2d path = getOperationalPlan().getPath();
875             int low = 0;
876             while (path.size() > low + 1 && path.lengthAtIndex(low + 1) <= startPosition.si)
877             {
878                 low++;
879             }
880             int high = path.size() - 1;
881             while (high > 0 && path.lengthAtIndex(high - 1) > endPosition.si)
882             {
883                 high--;
884             }
885             int mid = 0;
886             Length position0 = null;
887             Length overshoot0 = null;
888             // based on Collections.indexedBinarySearch
889             while (low <= high)
890             {
891                 mid = (low + high) / 2;
892                 position0 = Length.max(startPosition, Length.min(Length.ofSI(path.lengthAtIndex(mid)), endPosition));
893                 Duration time0 = getOperationalPlan().getTimeAtDistance(position0);
894                 overshoot0 = laneLateralOvershoot(time0).minus(lateralMargin);
895                 if (overshoot0.le0())
896                 {
897                     low = mid + 1;
898                 }
899                 else
900                 {
901                     high = mid - 1;
902                 }
903             }
904             if (mid == low)
905             {
906                 if (low < 1 || low > path.size())
907                 {
908                     return null;
909                 }
910                 position0 = Length.max(startPosition, Length.min(Length.ofSI(path.lengthAtIndex(low - 1)), endPosition));
911                 Duration time0 = getOperationalPlan().getTimeAtDistance(position0);
912                 overshoot0 = laneLateralOvershoot(time0).minus(lateralMargin);
913             }
914             Length position1 = Length.min(endPosition, Length.ofSI(path.lengthAtIndex(low)));
915             Duration time1 = getOperationalPlan().getTimeAtDistance(position1);
916             Length overshoot1 = laneLateralOvershoot(time1);
917             double factor = overshoot0.neg().si / (overshoot1.si - overshoot0.si);
918             return getOperationalPlan().getTimeAtDistance(Length.interpolate(position0, position1, factor));
919         }
920         catch (OperationalPlanException ex)
921         {
922             throw new OtsRuntimeException("Lateral crossing time or distance beyond plan.", ex);
923         }
924     }
925 
926     /**
927      * Ends roaming if the roaming position is sufficiently close to enter the network.
928      * @param roamingPosition roaming position
929      */
930     protected void endRoaming(final LanePosition roamingPosition)
931     {
932         if (roamingPosition.getLocation().distance(
933                 getLocation()) < roamingPosition.lane().getWidth(roamingPosition.getFraction()).plus(getWidth()).times(0.5).si)
934         {
935             enterLane(roamingPosition.lane(), roamingPosition.getFraction());
936         }
937     }
938 
939     /**
940      * This method applies a detector finding algorithm that guarantees that detectors at the same location, triggered for
941      * different relative positions, are all always triggered in combination. As detectors might be triggered by the front,
942      * detectors beyond the current plan path may need to be triggered in the current plan duration. To achieve this, all
943      * detectors are found between the path start position + dx, up to the path end position + dx, where dx is the distance the
944      * front is before the reference point. Start and end position and dx are applied along the lane center lines. In case of a
945      * lane change dx is also applied on both lanes, meaning that all detectors overlapping the vehicle on the from lane are
946      * found, but at the target lane only detectors downstream of the front are found.<br>
947      * <br>
948      * This method stores all found detectors as detector triggers. This includes for each detector the odometer value of the
949      * reference point at which the detector should be triggered. The odometer value is adjusted for the relative position that
950      * should trigger the detector, along the lane center lines.<br>
951      * <br>
952      * Finally, this method schedules trigger events for all stored detector triggers when the reference point reaches the
953      * relevant odometer value in the current plan. This may include detector triggers that were stored in a previous time step
954      * as the front reached the detector, but no event was scheduled in a previous time step as the relevant relative position
955      * of the detector, e.g. the rear, did not reach the detector.<br>
956      * <br>
957      * Alternatively when {@code schedule = false} this method finds all detector triggers downstream of the current front
958      * location during the current plan using the same search algorithm, and removes them from the stored detector triggers. No
959      * events will be scheduled (nor removed by this method). Removing detector triggers is relevant when a plan is cancelled.
960      * Any downstream detectors may be found again and rescheduled depending on a new plan by a new move.
961      * @param schedule {@code true} adds downstream triggers and schedules them, {@code false} removes downstream triggers
962      */
963     protected void findDetectorTriggers(final boolean schedule)
964     {
965         Lane laneOnPath = getLane();
966         if (laneOnPath == null)
967         {
968             return;
969         }
970 
971         // Find detectors reached with the nose in the current plan
972         Duration time0 = getSimulator().getSimulatorTime();
973         LanePosition position0 = getPosition();
974         Length searchedDistanceAtFrom = Length.ZERO;
975         while (time0.lt(getOperationalPlan().getEndTime()))
976         {
977             /*
978              * This loop loops over the current lane and all future pending lanes (i.e. episodes). At these lanes detectors are
979              * found. The relevant range [from ... to] on the lane is bounded by the start and end of the whole plan, and the
980              * position of lane changes if that is the cause of a next pending lane.
981              */
982             Duration time1 = this.pendingLanesToEnter.higherKey(time0) == null ? getOperationalPlan().getEndTime()
983                     : this.pendingLanesToEnter.higherKey(time0);
984             Lane laneAtTime = getLane(time1);
985             LanePosition position1 = new LanePosition(laneAtTime, getPosition(laneAtTime, getReference(), time1));
986             searchedDistanceAtFrom = findDetectorTriggersInEpisode(searchedDistanceAtFrom, position0, position1, schedule);
987             time0 = time1;
988             position0 = position1;
989         }
990 
991         // Schedule odometer values crossed in current plan
992         if (schedule)
993         {
994             for (Entry<LaneDetector, Length> trigger : this.detectorTriggers.entrySet())
995             {
996                 Length toDetector = trigger.getValue().minus(getOdometer());
997                 if (toDetector.le(getOperationalPlan().getTotalLength()))
998                 {
999                     Duration triggerTime = Try.assign(() -> getOperationalPlan().getTimeAtDistance(toDetector),
1000                             "Distance to detector beyond plan length.");
1001                     this.detectorEvents
1002                             .add(getSimulator().scheduleEventAbs(triggerTime, () -> triggerDetector(trigger.getKey())));
1003                 }
1004             }
1005         }
1006     }
1007 
1008     /**
1009      * Finds all detectors within an episode, i.e. one lane in the plan, possibly amended by downstream lanes not in the plan
1010      * but within reach of the nose (downstream of the end of the plan, or downstream of a lane change location on from lane).
1011      * @param searchedDistance distance searched in earlier episodes up to {@code position0}
1012      * @param position0 start position of episode
1013      * @param position1 start position of next episode (or end of plan)
1014      * @param schedule {@code true} adds downstream triggers and schedules them, {@code false} removes downstream triggers
1015      * @return increased searched distance up to {@code position1}
1016      */
1017     private Length findDetectorTriggersInEpisode(final Length searchedDistance, final LanePosition position0,
1018             final LanePosition position1, final boolean schedule)
1019     {
1020         Lane searchLane = position0.lane();
1021         Length from = position0.position();
1022         Length to;
1023         Length delta = getFront().dx();
1024 
1025         // Bound 'to' by enter position of next pending lane
1026         if (searchLane.getLink().equals(position1.lane().getLink()))
1027         {
1028             if (searchLane.equals(position1.lane()))
1029             {
1030                 // Same link, same lane: use position of next pending lane
1031                 to = position1.position();
1032             }
1033             else
1034             {
1035                 // Same link, different lane: lane change so project position on target lane (position1) to searchLane
1036                 Point2d point = position1.getLocation();
1037                 double fraction = searchLane.getCenterLine().projectFractionalAt(searchLane.getLink().getStartNode().getHeading(),
1038                         searchLane.getLink().getEndNode().getHeading(), point.x, point.y, FractionalFallback.ENDPOINT);
1039                 to = searchLane.getLength().times(fraction);
1040             }
1041         }
1042         else
1043         {
1044             // End of the lane if position1 is on a different link
1045             to = searchLane.getLength();
1046         }
1047 
1048         // We now have from and to on the same lane, with which we can calculate the episode length and what to return
1049         Length out = searchedDistance.plus(to).minus(from);
1050 
1051         while (searchLane != null)
1052         {
1053             // Find all detectors in range [from ... to] + delta
1054             for (LaneDetector detector : searchLane.getDetectors(from.plus(delta), to.plus(delta), getType()))
1055             {
1056                 if (schedule)
1057                 {
1058                     Length dxTrigger = getRelativePositions().get(detector.getPositionType()).dx();
1059                     Length detectorLocation = detector.getLongitudinalPosition();
1060                     Length deltaOdometer = searchedDistance.plus(detectorLocation).minus(from).minus(dxTrigger);
1061                     this.detectorTriggers.put(detector, getOdometer().plus(deltaOdometer));
1062                 }
1063                 else
1064                 {
1065                     this.detectorTriggers.remove(detector);
1066                 }
1067             }
1068 
1069             // Shift 'from' and 'to', to the next lane, reaching beyond 'to' up to where the nose might be on downstream lanes
1070             if (to.plus(delta).gt(searchLane.getLength()))
1071             {
1072                 // Need to consider the next lane, update 'from' and 'to' to coordinates on that lane
1073                 from = from.minus(searchLane.getLength());
1074                 to = to.minus(searchLane.getLength());
1075                 searchLane = getNextLaneForRoute(searchLane).orElse(null);
1076             }
1077             else
1078             {
1079                 return out;
1080             }
1081         }
1082         return out;
1083     }
1084 
1085     /**
1086      * Trigger detector and remove it from detectors that need to be triggered.
1087      * @param detector detector
1088      */
1089     protected void triggerDetector(final LaneDetector detector)
1090     {
1091         this.detectorTriggers.remove(detector);
1092         detector.trigger(this);
1093     }
1094 
1095     /**
1096      * Returns the lateral overshoot at give time. This is the lateral distance by which the reference point exceeds either the
1097      * left or right edge of the lane. Negative values indicate the reference point is still on the lane.
1098      * @param time simulation time
1099      * @return lateral overshoot
1100      */
1101     protected Length laneLateralOvershoot(final Duration time)
1102     {
1103         Lane laneAtTime = getLane(time);
1104         Point2d location = getLocation(time);
1105         Length deviation = getDeviation(laneAtTime, location);
1106         LanePosition position = getPosition(time);
1107         Length laneWidth = position.lane().getWidth(position.position());
1108         return deviation.abs().minus(laneWidth.times(0.5));
1109     }
1110 
1111     /**
1112      * Returns the next lane for a given lane to stay on the route.
1113      * @param lane the lane for which we want to know the next Lane
1114      * @return next lane, empty if none
1115      */
1116     @SuppressWarnings("hiddenfield")
1117     public synchronized Optional<Lane> getNextLaneForRoute(final Lane lane)
1118     {
1119         // ask strategical planner
1120         Set<Lane> set = getNextLanesForRoute(lane);
1121         if (set.isEmpty())
1122         {
1123             return Optional.empty();
1124         }
1125         if (set.size() == 1)
1126         {
1127             return Optional.of(set.iterator().next());
1128         }
1129         // check if the GTU is registered on any
1130         for (Lane l : set)
1131         {
1132             if (l.getGtuList().contains(this))
1133             {
1134                 return Optional.of(l);
1135             }
1136         }
1137         // ask tactical planner
1138         return Optional.of(Try.assign(() -> getTacticalPlanner().chooseLaneAtSplit(lane, set),
1139                 "Could not find suitable lane at split after lane %s of link %s for GTU %s.", lane.getId(),
1140                 lane.getLink().getId(), getId()));
1141     }
1142 
1143     /**
1144      * Returns a set of {@code Lane}s that can be followed considering the route.
1145      * @param lane the lane for which we want to know the next Lane
1146      * @return set of {@code Lane}s that can be followed considering the route
1147      */
1148     @SuppressWarnings("hiddenfield")
1149     private Set<Lane> getNextLanesForRoute(final Lane lane)
1150     {
1151         Set<Lane> out = new LinkedHashSet<>();
1152         Set<Lane> nextPhysical = lane.nextLanes(null);
1153 
1154         Link link = Try.assign(() -> getStrategicalPlanner().nextLink(lane.getLink(), getType()),
1155                 "Strategical planner experiences exception on network.");
1156 
1157         if (nextPhysical.isEmpty())
1158         {
1159             return out;
1160         }
1161 
1162         Set<Lane> next = lane.nextLanes(getType());
1163         if (next.isEmpty())
1164         {
1165             next = nextPhysical;
1166         }
1167         for (Lane l : next)
1168         {
1169             if (l.getLink().equals(link))
1170             {
1171                 out.add(l);
1172             }
1173         }
1174         return out;
1175     }
1176 
1177     /**
1178      * Returns an estimation of when the relative position will reach the line. Returns {@code null} if this does not occur
1179      * during the current operational plan.
1180      * @param line line, i.e. lateral line at link start or lateral entrance of sensor
1181      * @param relativePosition position to cross the line
1182      * @return estimation of when the relative position will reach the line, {@code null} if this does not occur during the
1183      *         current operational plan
1184      */
1185     private Duration timeAtLine(final PolyLine2d line, final RelativePosition relativePosition)
1186     {
1187         Throw.when(line.size() != 2, IllegalArgumentException.class, "Line to cross with path should have 2 points.");
1188         OtsLine2d path = getOperationalPlan().getPath();
1189         List<Point2d> points = new ArrayList<>(path.size() + 1);
1190         points.addAll(path.getPointList());
1191         double adjust;
1192         if (relativePosition.dx().gt0())
1193         {
1194             // as the position is downstream of the reference, we need to attach some distance at the end
1195             points.add(path.getLocationExtended(path.getLength() + relativePosition.dx().si));
1196             adjust = -relativePosition.dx().si;
1197         }
1198         else if (relativePosition.dx().lt0())
1199         {
1200             points.add(0, path.getLocationExtended(relativePosition.dx().si));
1201             adjust = 0.0;
1202         }
1203         else
1204         {
1205             adjust = 0.0;
1206         }
1207 
1208         double cumul = 0.0;
1209         double x0 = line.get(0).x;
1210         double y0 = line.get(0).y;
1211         double x1 = line.get(1).x;
1212         double y1 = line.get(1).y;
1213         for (int i = 0; i < points.size() - 1; i++)
1214         {
1215             Point2d intersect = OtsGeometryUtil.intersectionOfLinesEps(points.get(i).x, points.get(i).y, points.get(i + 1).x,
1216                     points.get(i + 1).y, true, true, x0, y0, x1, y1, false, false, 1e-12);
1217             if (intersect != null)
1218             {
1219                 cumul += points.get(i).distance(intersect);
1220                 cumul += adjust;
1221                 // return time at distance
1222                 if (cumul < 0.0)
1223                 {
1224                     // return getSimulator().getSimulatorAbsTime(); // this was a mistake...
1225                     // relative position already crossed the point, e.g. FRONT
1226                     // SKL 08-02-2023: if the nose did not trigger at end of last move by mm's and due to vehicle rotation
1227                     // having been assumed straight, we should trigger it now. However, we should not double-trigger e.g.
1228                     // detectors. Let's return NaN to indicate this problem.
1229                     return Duration.NaN;
1230                 }
1231                 if (cumul <= getOperationalPlan().getTotalLength().si)
1232                 {
1233                     return getOperationalPlan().timeAtDistance(Length.ofSI(cumul));
1234                 }
1235                 // ref will cross the line, but GTU will not travel enough for rear to cross
1236                 return null;
1237             }
1238             else if (i < points.size() - 2)
1239             {
1240                 cumul += points.get(i).distance(points.get(i + 1));
1241             }
1242         }
1243         // no intersect
1244         return null;
1245     }
1246 
1247     /**
1248      * Sets a vehicle model.
1249      * @param vehicleModel vehicle model
1250      */
1251     public void setVehicleModel(final VehicleModel vehicleModel)
1252     {
1253         this.vehicleModel = vehicleModel;
1254     }
1255 
1256     /**
1257      * Returns the vehicle model.
1258      * @return vehicle model
1259      */
1260     public VehicleModel getVehicleModel()
1261     {
1262         return this.vehicleModel;
1263     }
1264 
1265     @Override
1266     public LaneBasedStrategicalPlanner getStrategicalPlanner()
1267     {
1268         return (LaneBasedStrategicalPlanner) super.getStrategicalPlanner();
1269     }
1270 
1271     @Override
1272     public LaneBasedStrategicalPlanner getStrategicalPlanner(final Duration time)
1273     {
1274         return (LaneBasedStrategicalPlanner) super.getStrategicalPlanner(time);
1275     }
1276 
1277     /**
1278      * Returns the network.
1279      * @return the road network to which the LaneBasedGtu belongs
1280      */
1281     public RoadNetwork getNetwork()
1282     {
1283         return (RoadNetwork) super.getPerceivableContext();
1284     }
1285 
1286     /**
1287      * Returns the turn indicator status.
1288      * @return the status of the turn indicator
1289      */
1290     public TurnIndicatorStatus getTurnIndicatorStatus()
1291     {
1292         return this.turnIndicatorStatus.get();
1293     }
1294 
1295     /**
1296      * Returns the turn indicator status at time.
1297      * @param time simulation time to obtain the turn indicator status at
1298      * @return the status of the turn indicator at the given time
1299      */
1300     public TurnIndicatorStatus getTurnIndicatorStatus(final Duration time)
1301     {
1302         return this.turnIndicatorStatus.get(time);
1303     }
1304 
1305     /**
1306      * Set the status of the turn indicator.
1307      * @param turnIndicatorStatus the new status of the turn indicator.
1308      */
1309     public void setTurnIndicatorStatus(final TurnIndicatorStatus turnIndicatorStatus)
1310     {
1311         this.turnIndicatorStatus.set(turnIndicatorStatus);
1312     }
1313 
1314     @Override
1315     public Length getHeight()
1316     {
1317         return Length.ZERO;
1318     }
1319 
1320     @Override
1321     public String getFullId()
1322     {
1323         return getId();
1324     }
1325 
1326     /**
1327      * Sets how lane bookkeeping at lane changes is done.
1328      * @param bookkeeping how lane bookkeeping at lane changes is done
1329      */
1330     public void setBookkeeping(final LaneBookkeeping bookkeeping)
1331     {
1332         this.bookkeeping = bookkeeping;
1333     }
1334 
1335     /**
1336      * Returns how lane bookkeeping at lane changes is done.
1337      * @return how lane bookkeeping at lane changes is done
1338      */
1339     public LaneBookkeeping getBookkeeping()
1340     {
1341         return this.bookkeeping;
1342     }
1343 
1344     @Override
1345     public LaneBasedTacticalPlanner getTacticalPlanner()
1346     {
1347         return getStrategicalPlanner().getTacticalPlanner();
1348     }
1349 
1350     @Override
1351     public LaneBasedTacticalPlanner getTacticalPlanner(final Duration time)
1352     {
1353         return getStrategicalPlanner(time).getTacticalPlanner(time);
1354     }
1355 
1356     /**
1357      * Set distance over which the GTU should not change lane after being created.
1358      * @param distance distance over which the GTU should not change lane after being created
1359      */
1360     public void setNoLaneChangeDistance(final Length distance)
1361     {
1362         this.noLaneChangeDistance = distance;
1363     }
1364 
1365     /**
1366      * Returns whether a lane change is allowed.
1367      * @return whether a lane change is allowed
1368      */
1369     public boolean laneChangeAllowed()
1370     {
1371         return this.noLaneChangeDistance == null ? true : getOdometer().gt(this.noLaneChangeDistance);
1372     }
1373 
1374     /**
1375      * Set lane change direction. This should only be set by a controller of the GTU, e.g. the tactical planner.
1376      * @param direction lane change direction
1377      */
1378     public void setLaneChangeDirection(final LateralDirectionality direction)
1379     {
1380         this.laneChangeDirection.set(direction);
1381     }
1382 
1383     /**
1384      * Returns the lane change direction.
1385      * @return lane change direction
1386      */
1387     public LateralDirectionality getLaneChangeDirection()
1388     {
1389         return this.laneChangeDirection.get();
1390     }
1391 
1392     /**
1393      * Returns the lane change direction at the given time.
1394      * @param time simulation time
1395      * @return lane change direction at the given time
1396      */
1397     public LateralDirectionality getLaneChangeDirection(final Duration time)
1398     {
1399         return this.laneChangeDirection.get(time);
1400     }
1401 
1402     /**
1403      * Returns whether the braking lights are on.
1404      * @return whether the braking lights are on
1405      */
1406     public boolean isBrakingLightsOn()
1407     {
1408         return isBrakingLightsOn(getSimulator().getSimulatorTime());
1409     }
1410 
1411     /**
1412      * Returns whether the braking lights are on.
1413      * @param time simulation time
1414      * @return whether the braking lights are on
1415      */
1416     public boolean isBrakingLightsOn(final Duration time)
1417     {
1418         return getVehicleModel().isBrakingLightsOn(getSpeed(time), getAcceleration(time));
1419     }
1420 
1421     /**
1422      * Get projected length on the lane.
1423      * @param lane lane to project the vehicle on
1424      * @return the length on the lane, which is different from the actual length during deviative tactical plans
1425      */
1426     @SuppressWarnings("hiddenfield")
1427     public Length getProjectedLength(final Lane lane)
1428     {
1429         Length front = getPosition(lane, getFront());
1430         Length rear = getPosition(lane, getRear());
1431         return front.minus(rear);
1432     }
1433 
1434     /**
1435      * Stops the GTU using a permanent stand-still operational plan.
1436      */
1437     public void stop()
1438     {
1439         getSimulator().cancelEvent(getNextMoveEvent());
1440         setOperationalPlan(
1441                 OperationalPlan.standStill(this, getLocation(), getSimulator().getSimulatorTime(), Duration.POSITIVE_INFINITY));
1442     }
1443 
1444     @Override
1445     @SuppressWarnings("checkstyle:designforextension")
1446     public void destroy()
1447     {
1448         LanePosition dlp = getPosition();
1449         DirectedPoint2d location = this.getOperationalPlan() == null ? new DirectedPoint2d(0.0, 0.0, 0.0) : getLocation();
1450         synchronized (this)
1451         {
1452             if (dlp != null && dlp.lane() != null)
1453             {
1454                 dlp.lane().removeGtu(this, true, dlp.position());
1455             }
1456         }
1457         if (dlp != null && dlp.lane() != null)
1458         {
1459             Lane referenceLane = dlp.lane();
1460             fireTimedEvent(LaneBasedGtu.LANEBASED_DESTROY_EVENT,
1461                     new Object[] {getId(), new PositionVector(new double[] {location.x, location.y}, PositionUnit.METER),
1462                             new Direction(location.getDirZ(), DirectionUnit.EAST_RADIAN), getOdometer(),
1463                             referenceLane.getLink().getId(), referenceLane.getId(), dlp.position()},
1464                     getSimulator().getSimulatorTime());
1465         }
1466         else
1467         {
1468             fireTimedEvent(LaneBasedGtu.LANEBASED_DESTROY_EVENT,
1469                     new Object[] {getId(), new PositionVector(new double[] {location.x, location.y}, PositionUnit.METER),
1470                             new Direction(location.getDirZ(), DirectionUnit.EAST_RADIAN), getOdometer(), null, null, null},
1471                     getSimulator().getSimulatorTime());
1472         }
1473         cancelAllEvents();
1474 
1475         super.destroy();
1476     }
1477 
1478     @Override
1479     @SuppressWarnings("checkstyle:designforextension")
1480     public String toString()
1481     {
1482         return "GTU " + getId();
1483     }
1484 
1485 }