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