View Javadoc
1   package org.opentrafficsim.road.gtu.lane;
2   
3   import java.util.ArrayList;
4   import java.util.Collections;
5   import java.util.Comparator;
6   import java.util.LinkedHashMap;
7   import java.util.LinkedHashSet;
8   import java.util.List;
9   import java.util.Map;
10  import java.util.Map.Entry;
11  import java.util.Set;
12  import java.util.SortedMap;
13  
14  import javax.media.j3d.Bounds;
15  import javax.vecmath.Point3d;
16  
17  import org.djunits.unit.DurationUnit;
18  import org.djunits.unit.LengthUnit;
19  import org.djunits.value.vdouble.scalar.Acceleration;
20  import org.djunits.value.vdouble.scalar.Duration;
21  import org.djunits.value.vdouble.scalar.Length;
22  import org.djunits.value.vdouble.scalar.Speed;
23  import org.djunits.value.vdouble.scalar.Time;
24  import org.djutils.exceptions.Throw;
25  import org.djutils.exceptions.Try;
26  import org.djutils.immutablecollections.ImmutableMap;
27  import org.opentrafficsim.base.parameters.ParameterException;
28  import org.opentrafficsim.core.dsol.OTSSimulatorInterface;
29  import org.opentrafficsim.core.geometry.OTSGeometryException;
30  import org.opentrafficsim.core.geometry.OTSLine3D;
31  import org.opentrafficsim.core.geometry.OTSLine3D.FractionalFallback;
32  import org.opentrafficsim.core.geometry.OTSPoint3D;
33  import org.opentrafficsim.core.gtu.AbstractGTU;
34  import org.opentrafficsim.core.gtu.GTU;
35  import org.opentrafficsim.core.gtu.GTUDirectionality;
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.OperationalPlanBuilder;
43  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlanException;
44  import org.opentrafficsim.core.network.LateralDirectionality;
45  import org.opentrafficsim.core.network.NetworkException;
46  import org.opentrafficsim.core.perception.Historical;
47  import org.opentrafficsim.core.perception.HistoricalValue;
48  import org.opentrafficsim.core.perception.HistoryManager;
49  import org.opentrafficsim.core.perception.collections.HistoricalArrayList;
50  import org.opentrafficsim.core.perception.collections.HistoricalList;
51  import org.opentrafficsim.road.gtu.lane.perception.LanePerception;
52  import org.opentrafficsim.road.gtu.lane.perception.PerceptionCollectable;
53  import org.opentrafficsim.road.gtu.lane.perception.RelativeLane;
54  import org.opentrafficsim.road.gtu.lane.perception.categories.InfrastructurePerception;
55  import org.opentrafficsim.road.gtu.lane.perception.categories.neighbors.NeighborsPerception;
56  import org.opentrafficsim.road.gtu.lane.perception.headway.HeadwayGTU;
57  import org.opentrafficsim.road.gtu.lane.plan.operational.LaneBasedOperationalPlan;
58  import org.opentrafficsim.road.gtu.strategical.LaneBasedStrategicalPlanner;
59  import org.opentrafficsim.road.network.OTSRoadNetwork;
60  import org.opentrafficsim.road.network.RoadNetwork;
61  import org.opentrafficsim.road.network.lane.CrossSectionLink;
62  import org.opentrafficsim.road.network.lane.DirectedLanePosition;
63  import org.opentrafficsim.road.network.lane.Lane;
64  import org.opentrafficsim.road.network.lane.LaneDirection;
65  import org.opentrafficsim.road.network.lane.object.sensor.SingleSensor;
66  import org.opentrafficsim.road.network.speed.SpeedLimitInfo;
67  import org.opentrafficsim.road.network.speed.SpeedLimitTypes;
68  
69  import nl.tudelft.simulation.dsol.SimRuntimeException;
70  import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEventInterface;
71  import nl.tudelft.simulation.dsol.simtime.SimTimeDoubleUnit;
72  import nl.tudelft.simulation.language.d3.BoundingBox;
73  import nl.tudelft.simulation.language.d3.DirectedPoint;
74  
75  /**
76   * This class contains most of the code that is needed to run a lane based GTU. <br>
77   * The starting point of a LaneBasedTU is that it can be in <b>multiple lanes</b> at the same time. This can be due to a lane
78   * change (lateral), or due to crossing a link (front of the GTU is on another Lane than rear of the GTU). If a Lane is shorter
79   * than the length of the GTU (e.g. when we do node expansion on a crossing, this is very well possible), a GTU could occupy
80   * dozens of Lanes at the same time.
81   * <p>
82   * When calculating a headway, the GTU has to look in successive lanes. When Lanes (or underlying CrossSectionLinks) diverge,
83   * the headway algorithms have to look at multiple Lanes and return the minimum headway in each of the Lanes. When the Lanes (or
84   * underlying CrossSectionLinks) converge, "parallel" traffic is not taken into account in the headway calculation. Instead, gap
85   * acceptance algorithms or their equivalent should guide the merging behavior.
86   * <p>
87   * To decide its movement, an AbstractLaneBasedGTU applies its car following algorithm and lane change algorithm to set the
88   * acceleration and any lane change operation to perform. It then schedules the triggers that will add it to subsequent lanes
89   * and remove it from current lanes as needed during the time step that is has committed to. Finally, it re-schedules its next
90   * movement evaluation with the simulator.
91   * <p>
92   * Copyright (c) 2013-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
93   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
94   * <p>
95   * @version $Revision: 1408 $, $LastChangedDate: 2015-09-24 15:17:25 +0200 (Thu, 24 Sep 2015) $, by $Author: pknoppers $,
96   *          initial version Oct 22, 2014 <br>
97   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
98   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
99   */
100 public abstract class AbstractLaneBasedGTU2 extends AbstractGTU implements LaneBasedGTU
101 {
102     /** */
103     private static final long serialVersionUID = 20140822L;
104 
105     /** Lanes. */
106     private final HistoricalList<CrossSection> crossSections;
107 
108     /** Reference lane index (0 = left or only lane, 1 = right lane). */
109     private int referenceLaneIndex = 0;
110 
111     /** Time of reference position cache. */
112     private double referencePositionTime = Double.NaN;
113 
114     /** Cached reference position. */
115     private DirectedLanePosition cachedReferencePosition = null;
116 
117     /** Pending leave triggers for each lane. */
118     private SimEventInterface<SimTimeDoubleUnit> pendingLeaveTrigger;
119 
120     /** Pending enter triggers for each lane. */
121     private SimEventInterface<SimTimeDoubleUnit> pendingEnterTrigger;
122 
123     /** Event to finalize lane change. */
124     private SimEventInterface<SimTimeDoubleUnit> finalizeLaneChangeEvent;
125 
126     /** Sensor events. */
127     private Set<SimEventInterface<SimTimeDoubleUnit>> sensorEvents = new LinkedHashSet<>();
128 
129     /** Cached desired speed. */
130     private Speed cachedDesiredSpeed;
131 
132     /** Time desired speed was cached. */
133     private Time desiredSpeedTime;
134 
135     /** Cached car-following acceleration. */
136     private Acceleration cachedCarFollowingAcceleration;
137 
138     /** Time car-following acceleration was cached. */
139     private Time carFollowingAccelerationTime;
140 
141     /** The object to lock to make the GTU thread safe. */
142     private Object lock = new Object();
143 
144     /** The threshold distance for differences between initial locations of the GTU on different lanes. */
145     @SuppressWarnings("checkstyle:visibilitymodifier")
146     public static Length initialLocationThresholdDifference = new Length(1.0, LengthUnit.MILLIMETER);
147 
148     /** Margin to add to plan to check of the path will enter the next section. */
149     public static Length eventMargin = Length.instantiateSI(50.0);
150 
151     /** Turn indicator status. */
152     private final Historical<TurnIndicatorStatus> turnIndicatorStatus;
153 
154     /** Caching on or off. */
155     // TODO: should be indicated with a Parameter
156     public static boolean CACHING = true;
157 
158     /** cached position count. */
159     // TODO: can be removed after testing period
160     public static int CACHED_POSITION = 0;
161 
162     /** cached position count. */
163     // TODO: can be removed after testing period
164     public static int NON_CACHED_POSITION = 0;
165 
166     /** Vehicle model. */
167     private VehicleModel vehicleModel = VehicleModel.MINMAX;
168 
169     /** Whether the GTU perform lane changes instantaneously or not. */
170     private boolean instantaneousLaneChange = false;
171 
172     /**
173      * Construct a Lane Based GTU.
174      * @param id String; the id of the GTU
175      * @param gtuType GTUType; the type of GTU, e.g. TruckType, CarType, BusType
176      * @param simulator OTSSimulatorInterface; to initialize the move method and to get the current time
177      * @param network OTSRoadNetwork; the network that the GTU is initially registered in
178      * @throws GTUException when initial values are not correct
179      */
180     public AbstractLaneBasedGTU2(final String id, final GTUType gtuType, final OTSSimulatorInterface simulator,
181             final OTSRoadNetwork network) throws GTUException
182     {
183         super(id, gtuType, simulator, network);
184         HistoryManager historyManager = simulator.getReplication().getHistoryManager(simulator);
185         this.crossSections = new HistoricalArrayList<>(historyManager);
186         this.turnIndicatorStatus = new HistoricalValue<>(historyManager, TurnIndicatorStatus.NOTPRESENT);
187     }
188 
189     /**
190      * @param strategicalPlanner LaneBasedStrategicalPlanner; the strategical planner (e.g., route determination) to use
191      * @param initialLongitudinalPositions Set&lt;DirectedLanePosition&gt;; the initial positions of the car on one or more
192      *            lanes with their directions
193      * @param initialSpeed Speed; the initial speed of the car on the lane
194      * @throws NetworkException when the GTU cannot be placed on the given lane
195      * @throws SimRuntimeException when the move method cannot be scheduled
196      * @throws GTUException when initial values are not correct
197      * @throws OTSGeometryException when the initial path is wrong
198      */
199     @SuppressWarnings("checkstyle:designforextension")
200     public void init(final LaneBasedStrategicalPlanner strategicalPlanner,
201             final Set<DirectedLanePosition> initialLongitudinalPositions, final Speed initialSpeed)
202             throws NetworkException, SimRuntimeException, GTUException, OTSGeometryException
203     {
204         Throw.when(null == initialLongitudinalPositions, GTUException.class, "InitialLongitudinalPositions is null");
205         Throw.when(0 == initialLongitudinalPositions.size(), GTUException.class, "InitialLongitudinalPositions is empty set");
206 
207         DirectedPoint lastPoint = null;
208         for (DirectedLanePosition pos : initialLongitudinalPositions)
209         {
210             // Throw.when(lastPoint != null && pos.getLocation().distance(lastPoint) > initialLocationThresholdDifference.si,
211             // GTUException.class, "initial locations for GTU have distance > " + initialLocationThresholdDifference);
212             lastPoint = pos.getLocation();
213         }
214         DirectedPoint initialLocation = lastPoint;
215 
216         // Give the GTU a 1 micrometer long operational plan, or a stand-still plan, so the first move and events will work
217         Time now = getSimulator().getSimulatorTime();
218         try
219         {
220             if (initialSpeed.si < OperationalPlan.DRIFTING_SPEED_SI)
221             {
222                 this.operationalPlan
223                         .set(new OperationalPlan(this, initialLocation, now, new Duration(1E-6, DurationUnit.SECOND)));
224             }
225             else
226             {
227                 OTSPoint3D p2 = new OTSPoint3D(initialLocation.x + 1E-6 * Math.cos(initialLocation.getRotZ()),
228                         initialLocation.y + 1E-6 * Math.sin(initialLocation.getRotZ()), initialLocation.z);
229                 OTSLine3D path = new OTSLine3D(new OTSPoint3D(initialLocation), p2);
230                 this.operationalPlan.set(OperationalPlanBuilder.buildConstantSpeedPlan(this, path, now, initialSpeed));
231             }
232         }
233         catch (OperationalPlanException e)
234         {
235             throw new RuntimeException("Initial operational plan could not be created.", e);
236         }
237 
238         // register the GTU on the lanes
239         List<DirectedLanePosition> inits = new ArrayList<>(); // need to sort them
240         inits.addAll(initialLongitudinalPositions);
241         Collections.sort(inits, new Comparator<DirectedLanePosition>()
242         {
243             @Override
244             public int compare(final DirectedLanePosition/DirectedLanePosition.html#DirectedLanePosition">DirectedLanePosition o1, final DirectedLanePosition o2)
245             {
246                 Length length1 =
247                         o1.getGtuDirection().isPlus() ? o1.getPosition() : o1.getLane().getLength().minus(o1.getPosition());
248                 Length length2 =
249                         o2.getGtuDirection().isPlus() ? o2.getPosition() : o2.getLane().getLength().minus(o2.getPosition());
250                 return length1.compareTo(length2);
251             }
252         });
253         for (DirectedLanePosition directedLanePosition : inits)
254         {
255             List<Lane> lanes = new ArrayList<>();
256             lanes.add(directedLanePosition.getLane());
257             this.crossSections.add(new CrossSection(lanes, directedLanePosition.getGtuDirection())); // enter lane part 1
258         }
259 
260         // init event
261         DirectedLanePosition referencePosition = getReferencePosition();
262         fireTimedEvent(LaneBasedGTU.LANEBASED_INIT_EVENT,
263                 new Object[] {getId(), initialLocation, getLength(), getWidth(), referencePosition.getLane(),
264                         referencePosition.getPosition(), referencePosition.getGtuDirection(), getGTUType()},
265                 getSimulator().getSimulatorTime());
266 
267         // register the GTU on the lanes
268         for (DirectedLanePosition directedLanePosition : initialLongitudinalPositions)
269         {
270             Lane lane = directedLanePosition.getLane();
271             lane.addGTU(this, directedLanePosition.getPosition()); // enter lane part 2
272         }
273 
274         // initiate the actual move
275         super.init(strategicalPlanner, initialLocation, initialSpeed);
276 
277         this.referencePositionTime = Double.NaN; // remove cache, it may be invalid as the above init results in a lane change
278 
279     }
280 
281     /**
282      * {@inheritDoc} All lanes the GTU is on will be left.
283      */
284     @Override
285     public synchronized void setParent(final GTU gtu) throws GTUException
286     {
287         leaveAllLanes();
288         super.setParent(gtu);
289     }
290 
291     /**
292      * Removes the registration between this GTU and all the lanes.
293      */
294     private void leaveAllLanes()
295     {
296         for (CrossSection crossSection : this.crossSections)
297         {
298             boolean removeFromParentLink = true;
299             for (Lane lane : crossSection.getLanes())
300             {
301                 // GTU should be on this lane as we loop the registration
302                 Length pos = Try.assign(() -> position(lane, getReference()), "Unexpected exception.");
303                 lane.removeGTU(this, removeFromParentLink, pos);
304                 removeFromParentLink = false;
305             }
306         }
307         this.crossSections.clear();
308     }
309 
310     /**
311      * Reinitializes the GTU on the network using the existing strategical planner and zero speed.
312      * @param initialLongitudinalPositions Set&lt;DirectedLanePosition&gt;; initial position
313      * @throws NetworkException when the GTU cannot be placed on the given lane
314      * @throws SimRuntimeException when the move method cannot be scheduled
315      * @throws GTUException when initial values are not correct
316      * @throws OTSGeometryException when the initial path is wrong
317      */
318     public void reinit(final Set<DirectedLanePosition> initialLongitudinalPositions)
319             throws NetworkException, SimRuntimeException, GTUException, OTSGeometryException
320     {
321         init(getStrategicalPlanner(), initialLongitudinalPositions, Speed.ZERO);
322     }
323 
324     /** {@inheritDoc} */
325     @Override
326     public synchronized void changeLaneInstantaneously(final LateralDirectionality laneChangeDirection) throws GTUException
327     {
328 
329         // from info
330         DirectedLanePosition from = getReferencePosition();
331 
332         // obtain position on lane adjacent to reference lane and enter lanes upstream/downstream from there
333         GTUDirectionality direction = getDirection(from.getLane());
334         Set<Lane> adjLanes = from.getLane().accessibleAdjacentLanesPhysical(laneChangeDirection, getGTUType(), direction);
335         Lane adjLane = adjLanes.iterator().next();
336         Length position = adjLane.position(from.getLane().fraction(from.getPosition()));
337         leaveAllLanes();
338         enterLaneRecursive(new LaneDirection(adjLane, direction), position, 0);
339 
340         // stored positions no longer valid
341         this.referencePositionTime = Double.NaN;
342         this.cachedPositions.clear();
343 
344         // fire event
345         this.fireTimedEvent(LaneBasedGTU.LANE_CHANGE_EVENT, new Object[] {getId(), laneChangeDirection, from},
346                 getSimulator().getSimulatorTime());
347 
348     }
349 
350     /**
351      * Enters lanes upstream and downstream of the new location after an instantaneous lane change.
352      * @param lane LaneDirection; considered lane
353      * @param position Length; position to add GTU at
354      * @param dir int; below 0 for upstream, above 0 for downstream, 0 for both
355      * @throws GTUException on exception
356      */
357     private void enterLaneRecursive(final LaneDirection lane, final Length position, final int dir) throws GTUException
358     {
359         List<Lane> lanes = new ArrayList<>();
360         lanes.add(lane.getLane());
361         int index = dir > 0 ? this.crossSections.size() : 0;
362         this.crossSections.add(index, new CrossSection(lanes, lane.getDirection()));
363         lane.getLane().addGTU(this, position);
364 
365         // upstream
366         if (dir < 1)
367         {
368             Length rear = lane.getDirection().isPlus() ? position.plus(getRear().getDx()) : position.minus(getRear().getDx());
369             Length before = null;
370             if (lane.getDirection().isPlus() && rear.si < 0.0)
371             {
372                 before = rear.neg();
373             }
374             else if (lane.getDirection().isMinus() && rear.si > lane.getLength().si)
375             {
376                 before = rear.minus(lane.getLength());
377             }
378             if (before != null)
379             {
380                 GTUDirectionality upDir = lane.getDirection();
381                 ImmutableMap<Lane, GTUDirectionality> upstream = lane.getLane().upstreamLanes(upDir, getGTUType());
382                 if (!upstream.isEmpty())
383                 {
384                     Lane upLane = null;
385                     for (Lane nextUp : upstream.keySet())
386                     {
387                         for (CrossSection crossSection : this.crossSections)
388                         {
389                             if (crossSection.getLanes().contains(nextUp))
390                             {
391                                 // multiple upstream lanes could belong to the same link, we pick an arbitrary lane
392                                 // (a conflict should solve this)
393                                 upLane = nextUp;
394                                 break;
395                             }
396                         }
397                     }
398                     if (upLane == null)
399                     {
400                         // the rear is on an upstream section we weren't before the lane change, due to curvature, we pick an
401                         // arbitrary lane (a conflict should solve this)
402                         upLane = upstream.keySet().iterator().next();
403                     }
404                     upDir = upstream.get(upLane);
405                     LaneDirectionk/lane/LaneDirection.html#LaneDirection">LaneDirection next = new LaneDirection(upLane, upDir);
406                     Length nextPos = upDir.isPlus() ? next.getLength().minus(before).minus(getRear().getDx())
407                             : before.plus(getRear().getDx());
408                     enterLaneRecursive(next, nextPos, -1);
409                 }
410             }
411         }
412 
413         // downstream
414         if (dir > -1)
415         {
416             Length front =
417                     lane.getDirection().isPlus() ? position.plus(getFront().getDx()) : position.minus(getFront().getDx());
418             Length passed = null;
419             if (lane.getDirection().isPlus() && front.si > lane.getLength().si)
420             {
421                 passed = front.minus(lane.getLength());
422             }
423             else if (lane.getDirection().isMinus() && front.si < 0.0)
424             {
425                 passed = front.neg();
426             }
427             if (passed != null)
428             {
429                 LaneDirection next = lane.getNextLaneDirection(this);
430                 Length nextPos = next.getDirection().isPlus() ? passed.minus(getFront().getDx())
431                         : next.getLength().minus(passed).plus(getFront().getDx());
432                 enterLaneRecursive(next, nextPos, 1);
433             }
434         }
435     }
436 
437     /**
438      * Register on lanes in target lane.
439      * @param laneChangeDirection LateralDirectionality; direction of lane change
440      * @throws GTUException exception
441      */
442     @Override
443     @SuppressWarnings("checkstyle:designforextension")
444     public synchronized void initLaneChange(final LateralDirectionality laneChangeDirection) throws GTUException
445     {
446         List<CrossSection> newLanes = new ArrayList<>();
447         int index = laneChangeDirection.isLeft() ? 0 : 1;
448         int numRegistered = 0;
449         DirectedPoint point = getLocation();
450         Map<Lane, Double> addToLanes = new LinkedHashMap<>();
451         for (CrossSection crossSection : this.crossSections)
452         {
453             List<Lane> resultingLanes = new ArrayList<>();
454             Lane lane = crossSection.getLanes().get(0);
455             resultingLanes.add(lane);
456             Set<Lane> laneSet = lane.accessibleAdjacentLanesLegal(laneChangeDirection, getGTUType(), getDirection(lane));
457             if (laneSet.size() > 0)
458             {
459                 numRegistered++;
460                 Lane adjacentLane = laneSet.iterator().next();
461                 double f = adjacentLane.getCenterLine().projectFractional(null, null, point.x, point.y, FractionalFallback.NaN);
462                 if (Double.isNaN(f))
463                 {
464                     // the GTU is upstream or downstream of the lane, or on the edge and we have rounding problems
465                     // in either case we add the GTU at an extreme
466                     // (this is only for ordering on the lane, the position is not used otherwise)
467                     Length pos = position(lane, getReference());
468                     addToLanes.put(adjacentLane, pos.si < lane.getLength().si / 2 ? 0.0 : 1.0);
469                 }
470                 else
471                 {
472                     f = crossSection.getDirection().isPlus() ? f : 1.0 - f;
473                     addToLanes.put(adjacentLane, adjacentLane.getLength().times(f).si / adjacentLane.getLength().si);
474                 }
475                 resultingLanes.add(index, adjacentLane);
476             }
477             newLanes.add(new CrossSection(resultingLanes, crossSection.getDirection()));
478         }
479         Throw.when(numRegistered == 0, GTUException.class, "Gtu %s starting %s lane change, but no adjacent lane found.",
480                 getId(), laneChangeDirection);
481         this.crossSections.clear();
482         this.crossSections.addAll(newLanes);
483         for (Entry<Lane, Double> entry : addToLanes.entrySet())
484         {
485             entry.getKey().addGTU(this, entry.getValue());
486         }
487         this.referenceLaneIndex = 1 - index;
488     }
489 
490     /**
491      * Performs the finalization of a lane change by leaving the from lanes.
492      * @param laneChangeDirection LateralDirectionality; direction of lane change
493      * @throws GTUException if position or direction could not be obtained
494      */
495     @SuppressWarnings("checkstyle:designforextension")
496     protected synchronized void finalizeLaneChange(final LateralDirectionality laneChangeDirection) throws GTUException
497     {
498         List<CrossSection> newLanes = new ArrayList<>();
499         Lane fromLane = null;
500         Length fromPosition = null;
501         GTUDirectionality fromDirection = null;
502         for (CrossSection crossSection : this.crossSections)
503         {
504             Lane lane = crossSection.getLanes().get(this.referenceLaneIndex);
505             if (lane != null)
506             {
507                 Length pos = position(lane, RelativePosition.REFERENCE_POSITION);
508                 if (0.0 <= pos.si && pos.si <= lane.getLength().si)
509                 {
510                     fromLane = lane;
511                     fromPosition = pos;
512                     fromDirection = getDirection(lane);
513                 }
514                 lane.removeGTU(this, false, pos);
515             }
516             List<Lane> remainingLane = new ArrayList<>();
517             remainingLane.add(crossSection.getLanes().get(1 - this.referenceLaneIndex));
518             newLanes.add(new CrossSection(remainingLane, crossSection.getDirection()));
519         }
520         this.crossSections.clear();
521         this.crossSections.addAll(newLanes);
522         this.referenceLaneIndex = 0;
523 
524         Throw.when(fromLane == null, RuntimeException.class, "No from lane for lane change event.");
525         DirectedLanePosition from;
526         try
527         {
528             from = new DirectedLanePosition(fromLane, fromPosition, fromDirection);
529         }
530         catch (GTUException exception)
531         {
532             throw new RuntimeException(exception);
533         }
534         this.fireTimedEvent(LaneBasedGTU.LANE_CHANGE_EVENT, new Object[] {getId(), laneChangeDirection, from},
535                 getSimulator().getSimulatorTime());
536         this.finalizeLaneChangeEvent = null;
537     }
538 
539     /** {@inheritDoc} */
540     @Override
541     public void setFinalizeLaneChangeEvent(final SimEventInterface<SimTimeDoubleUnit> event)
542     {
543         this.finalizeLaneChangeEvent = event;
544     }
545 
546     /** {@inheritDoc} */
547     @Override
548     public synchronized final GTUDirectionality getDirection(final Lane lane) throws GTUException
549     {
550         for (CrossSection crossSection : this.crossSections)
551         {
552             if (crossSection.getLanes().contains(lane))
553             {
554                 return crossSection.getDirection();
555             }
556         }
557         throw new GTUException("getDirection: GTU does not contain " + lane);
558     }
559 
560     /** {@inheritDoc} */
561     @Override
562     @SuppressWarnings("checkstyle:designforextension")
563     protected synchronized boolean move(final DirectedPoint fromLocation)
564             throws SimRuntimeException, GTUException, OperationalPlanException, NetworkException, ParameterException
565     {
566         try
567         {
568             if (this.crossSections.isEmpty())
569             {
570                 destroy();
571                 return false; // Done; do not re-schedule execution of this move method.
572             }
573 
574             // cancel events, if any
575             cancelAllEvents();
576 
577             // generate the next operational plan and carry it out
578             // in case of an instantaneous lane change, fractionalLinkPositions will be accordingly adjusted to the new lane
579             boolean error = super.move(fromLocation);
580             if (error)
581             {
582                 return error;
583             }
584 
585             DirectedLanePosition dlp = getReferencePosition();
586 
587             scheduleEnterEvent();
588             scheduleLeaveEvent();
589 
590             // sensors
591             for (CrossSection crossSection : this.crossSections)
592             {
593                 for (Lane lane : crossSection.getLanes())
594                 {
595                     scheduleTriggers(lane, crossSection.getDirection());
596                 }
597             }
598 
599             fireTimedEvent(
600                     LaneBasedGTU.LANEBASED_MOVE_EVENT, new Object[] {getId(), fromLocation, getSpeed(), getAcceleration(),
601                             getTurnIndicatorStatus(), getOdometer(), dlp.getLane(), dlp.getPosition(), dlp.getGtuDirection()},
602                     getSimulator().getSimulatorTime());
603 
604             return false;
605 
606         }
607         catch (Exception ex)
608         {
609             try
610             {
611                 getErrorHandler().handle(this, ex);
612             }
613             catch (Exception exception)
614             {
615                 throw new GTUException(exception);
616             }
617             return true;
618         }
619 
620     }
621 
622     /**
623      * Cancels all future events.
624      */
625     private void cancelAllEvents()
626     {
627         if (this.pendingEnterTrigger != null)
628         {
629             getSimulator().cancelEvent(this.pendingEnterTrigger);
630         }
631         if (this.pendingLeaveTrigger != null)
632         {
633             getSimulator().cancelEvent(this.pendingLeaveTrigger);
634         }
635         if (this.finalizeLaneChangeEvent != null)
636         {
637             getSimulator().cancelEvent(this.finalizeLaneChangeEvent);
638         }
639         for (SimEventInterface<SimTimeDoubleUnit> event : this.sensorEvents)
640         {
641             if (event.getAbsoluteExecutionTime().gt(getSimulator().getSimTime()))
642             {
643                 getSimulator().cancelEvent(event);
644             }
645         }
646         this.sensorEvents.clear();
647     }
648 
649     /**
650      * Checks whether the GTU will enter a next cross-section during the (remainder of) the tactical plan. Only one event will
651      * be scheduled. Possible additional events are scheduled upon entering the cross-section.
652      * @throws GTUException exception
653      * @throws OperationalPlanException exception
654      * @throws SimRuntimeException exception
655      */
656     protected void scheduleEnterEvent() throws GTUException, OperationalPlanException, SimRuntimeException
657     {
658         CrossSection lastCrossSection = this.crossSections.get(this.crossSections.size() - 1);
659         // heuristic to prevent geometric calculation if the next section is quite far away anyway
660         Length remain = remainingEventDistance();
661         Lane lane = lastCrossSection.getLanes().get(this.referenceLaneIndex);
662         Length position = position(lane, getFront());
663         boolean possiblyNearNextSection =
664                 lastCrossSection.getDirection().isPlus() ? lane.getLength().minus(position).lt(remain) : position.lt(remain);
665         if (possiblyNearNextSection)
666         {
667             CrossSectionLink link = lastCrossSection.getLanes().get(0).getParentLink();
668             OTSLine3D enterLine = lastCrossSection.getDirection().isPlus() ? link.getEndLine() : link.getStartLine();
669             Time enterTime = timeAtLine(enterLine, getFront());
670             if (enterTime != null)
671             {
672                 if (enterTime.lt(getSimulator().getSimulatorTime()))
673                 {
674                     System.err.println(
675                             "Time travel? enterTime=" + enterTime + "; simulator time=" + getSimulator().getSimulatorTime());
676                     enterTime = getSimulator().getSimulatorTime();
677                 }
678                 this.pendingEnterTrigger = getSimulator().scheduleEventAbs(enterTime, this, this, "enterCrossSection", null);
679             }
680         }
681     }
682 
683     /**
684      * Appends a new cross-section at the downstream end. Possibly schedules a next enter event.
685      * @throws GTUException exception
686      * @throws OperationalPlanException exception
687      * @throws SimRuntimeException exception
688      */
689     protected synchronized void enterCrossSection() throws GTUException, OperationalPlanException, SimRuntimeException
690     {
691         CrossSection lastCrossSection = this.crossSections.get(this.crossSections.size() - 1);
692         LaneDirection laneDirection =
693                 new LaneDirection(lastCrossSection.getLanes().get(this.referenceLaneIndex), lastCrossSection.getDirection());
694         LaneDirection nextLaneDirection = laneDirection.getNextLaneDirection(this);
695         if (nextLaneDirection == null)
696         {
697             forceLaneChangeFinalization();
698             return;
699         }
700         double insertFraction = nextLaneDirection.getDirection().isPlus() ? 0.0 : 1.0;
701         List<Lane> nextLanes = new ArrayList<>();
702         for (int i = 0; i < lastCrossSection.getLanes().size(); i++)
703         {
704             if (i == this.referenceLaneIndex)
705             {
706                 nextLanes.add(nextLaneDirection.getLane());
707             }
708             else
709             {
710                 Lane lane = lastCrossSection.getLanes().get(i);
711                 ImmutableMap<Lane, GTUDirectionality> lanes = lane.downstreamLanes(laneDirection.getDirection(), getGTUType());
712                 if (lanes.size() == 1)
713                 {
714                     Lane nextLane = lanes.keySet().iterator().next();
715                     nextLanes.add(nextLane);
716                 }
717                 else
718                 {
719                     boolean added = false;
720                     for (Lane nextLane : lanes.keySet())
721                     {
722                         if (nextLane.getParentLink().equals(nextLaneDirection.getLane().getParentLink())
723                                 && nextLane.accessibleAdjacentLanesPhysical(
724                                         this.referenceLaneIndex == 0 ? LateralDirectionality.LEFT : LateralDirectionality.RIGHT,
725                                         getGTUType(), nextLaneDirection.getDirection()).contains(nextLaneDirection.getLane()))
726                         {
727                             nextLanes.add(nextLane);
728                             added = true;
729                             break;
730                         }
731                     }
732                     if (!added)
733                     {
734                         forceLaneChangeFinalization();
735                         return;
736                     }
737                 }
738             }
739         }
740         this.crossSections.add(new CrossSection(nextLanes, nextLaneDirection.getDirection()));
741         for (Lane lane : nextLanes)
742         {
743             lane.addGTU(this, insertFraction);
744         }
745         this.pendingEnterTrigger = null;
746         scheduleEnterEvent();
747         for (Lane lane : nextLanes)
748         {
749             scheduleTriggers(lane, nextLaneDirection.getDirection());
750         }
751     }
752 
753     /**
754      * Helper method for {@code enterCrossSection}. In some cases the GTU should first finalize the lane change. This method
755      * checks whether such an event is scheduled, and performs it. This method then re-attempts to enter the cross-section. So
756      * the calling method should return after calling this.
757      * @throws GTUException exception
758      * @throws OperationalPlanException exception
759      * @throws SimRuntimeException exception
760      */
761     private void forceLaneChangeFinalization() throws GTUException, OperationalPlanException, SimRuntimeException
762     {
763         if (this.finalizeLaneChangeEvent != null)
764         {
765             // a lane change should be finalized at this time, but the event is later in the queue, force it now
766             SimEventInterface<SimTimeDoubleUnit> tmp = this.finalizeLaneChangeEvent;
767             finalizeLaneChange(this.referenceLaneIndex == 0 ? LateralDirectionality.RIGHT : LateralDirectionality.LEFT);
768             getSimulator().cancelEvent(tmp);
769             enterCrossSection();
770         }
771         // or a sink sensor should delete us
772     }
773 
774     /**
775      * Checks whether the GTU will leave a cross-section during the (remainder of) the tactical plan. Only one event will be
776      * scheduled. Possible additional events are scheduled upon leaving the cross-section.
777      * @throws GTUException exception
778      * @throws OperationalPlanException exception
779      * @throws SimRuntimeException exception
780      */
781     protected void scheduleLeaveEvent() throws GTUException, OperationalPlanException, SimRuntimeException
782     {
783         CrossSection firstCrossSection = this.crossSections.get(0);
784         // check, if reference lane is not in first cross section
785         boolean possiblyNearNextSection =
786                 !getReferencePosition().getLane().equals(firstCrossSection.getLanes().get(this.referenceLaneIndex));
787         if (!possiblyNearNextSection)
788         {
789             Length remain = remainingEventDistance();
790             Lane lane = firstCrossSection.getLanes().get(this.referenceLaneIndex);
791             Length position = position(lane, getRear());
792             possiblyNearNextSection = firstCrossSection.getDirection().isPlus() ? lane.getLength().minus(position).lt(remain)
793                     : position.lt(remain);
794         }
795         if (possiblyNearNextSection)
796         {
797             CrossSectionLink link = firstCrossSection.getLanes().get(0).getParentLink();
798             OTSLine3D leaveLine = firstCrossSection.getDirection().isPlus() ? link.getEndLine() : link.getStartLine();
799             Time leaveTime = timeAtLine(leaveLine, getRear());
800             if (leaveTime == null)
801             {
802                 // no intersect, let's do a check on the rear
803                 Lane lane = this.crossSections.get(0).getLanes().get(this.referenceLaneIndex);
804                 Length pos = position(lane, getRear());
805                 if (pos.gt(lane.getLength()))
806                 {
807                     pos = position(lane, getRear());
808                     this.pendingLeaveTrigger = getSimulator().scheduleEventNow(this, this, "leaveCrossSection", null);
809                     getSimulator().getLogger().always().info("Forcing leave for GTU {} on lane {}", getId(), lane.getFullId());
810                 }
811             }
812             if (leaveTime != null)
813             {
814                 if (leaveTime.lt(getSimulator().getSimulatorTime()))
815                 {
816                     System.err.println(
817                             "Time travel? leaveTime=" + leaveTime + "; simulator time=" + getSimulator().getSimulatorTime());
818                     leaveTime = getSimulator().getSimulatorTime();
819                 }
820                 this.pendingLeaveTrigger = getSimulator().scheduleEventAbs(leaveTime, this, this, "leaveCrossSection", null);
821             }
822         }
823     }
824 
825     /**
826      * Removes registration between the GTU and the lanes in the most upstream cross-section. Possibly schedules a next leave
827      * event.
828      * @throws GTUException exception
829      * @throws OperationalPlanException exception
830      * @throws SimRuntimeException exception
831      */
832     protected synchronized void leaveCrossSection() throws GTUException, OperationalPlanException, SimRuntimeException
833     {
834         List<Lane> lanes = this.crossSections.get(0).getLanes();
835         for (int i = 0; i < lanes.size(); i++)
836         {
837             Lane lane = lanes.get(i);
838             if (lane != null)
839             {
840                 lane.removeGTU(this, i == lanes.size() - 1, position(lane, getReference()));
841             }
842         }
843         this.crossSections.remove(0);
844         this.pendingLeaveTrigger = null;
845         scheduleLeaveEvent();
846     }
847 
848     /**
849      * Schedules all trigger events during the current operational plan on the lane.
850      * @param lane Lane; lane
851      * @param direction GTUDirectionality; direction
852      * @throws GTUException exception
853      * @throws OperationalPlanException exception
854      * @throws SimRuntimeException exception
855      */
856     protected void scheduleTriggers(final Lane lane, final GTUDirectionality direction)
857             throws GTUException, OperationalPlanException, SimRuntimeException
858     {
859         double min;
860         double max;
861         Length remain = remainingEventDistance();
862         if (direction.isPlus())
863         {
864             min = position(lane, getRear()).si;
865             max = min + remain.si + getLength().si;
866         }
867         else
868         {
869             max = position(lane, getRear()).si;
870             min = max - remain.si - getLength().si;
871         }
872         SortedMap<Double, List<SingleSensor>> sensors = lane.getSensorMap(getGTUType(), direction).subMap(min, max);
873         for (List<SingleSensor> list : sensors.values())
874         {
875             for (SingleSensor sensor : list)
876             {
877                 RelativePosition pos = this.getRelativePositions().get(sensor.getPositionType());
878                 Time time = timeAtLine(sensor.getGeometry(), pos);
879                 if (time != null)
880                 {
881                     this.sensorEvents.add(getSimulator().scheduleEventAbs(time, this, sensor, "trigger", new Object[] {this}));
882                 }
883             }
884         }
885     }
886 
887     /**
888      * Returns a safe distance beyond which a line will definitely not be crossed during the current operational plan.
889      * @return Length; safe distance beyond which a line will definitely not be crossed during the current operational plan
890      * @throws OperationalPlanException exception
891      */
892     private Length remainingEventDistance() throws OperationalPlanException
893     {
894         if (getOperationalPlan() instanceof LaneBasedOperationalPlan)
895         {
896             LaneBasedOperationalPlanrg/opentrafficsim/road/gtu/lane/plan/operational/LaneBasedOperationalPlan.html#LaneBasedOperationalPlan">LaneBasedOperationalPlan plan = (LaneBasedOperationalPlan) getOperationalPlan();
897             return plan.getTotalLength().minus(plan.getTraveledDistance(getSimulator().getSimulatorTime())).plus(eventMargin);
898         }
899         return getOperationalPlan().getTotalLength().plus(eventMargin);
900     }
901 
902     /**
903      * Returns an estimation of when the relative position will reach the line. Returns {@code null} if this does not occur
904      * during the current operational plan.
905      * @param line OTSLine3D; line, i.e. lateral line at link start or lateral entrance of sensor
906      * @param relativePosition RelativePosition; position to cross the line
907      * @return estimation of when the relative position will reach the line, {@code null} if this does not occur during the
908      *         current operational plan
909      * @throws GTUException position error
910      */
911     private Time timeAtLine(final OTSLine3D line, final RelativePosition relativePosition) throws GTUException
912     {
913         Throw.when(line.size() != 2, IllegalArgumentException.class, "Line to cross with path should have 2 points.");
914         OTSLine3D path = getOperationalPlan().getPath();
915         OTSPoint3D[] points;
916         double adjust;
917         if (relativePosition.getDx().gt0())
918         {
919             // as the position is downstream of the reference, we need to attach some distance at the end
920             points = new OTSPoint3D[path.size() + 1];
921             System.arraycopy(path.getPoints(), 0, points, 0, path.size());
922             points[path.size()] = new OTSPoint3D(path.getLocationExtendedSI(path.getLengthSI() + relativePosition.getDx().si));
923             adjust = -relativePosition.getDx().si;
924         }
925         else if (relativePosition.getDx().lt0())
926         {
927             points = new OTSPoint3D[path.size() + 1];
928             System.arraycopy(path.getPoints(), 0, points, 1, path.size());
929             points[0] = new OTSPoint3D(path.getLocationExtendedSI(relativePosition.getDx().si));
930             adjust = 0.0;
931         }
932         else
933         {
934             points = path.getPoints();
935             adjust = 0.0;
936         }
937 
938         // find intersection
939         double cumul = 0.0;
940         for (int i = 0; i < points.length - 1; i++)
941         {
942             OTSPoint3D intersect;
943             try
944             {
945                 intersect = OTSPoint3D.intersectionOfLineSegments(points[i], points[i + 1], line.get(0), line.get(1));
946             }
947             catch (OTSGeometryException exception)
948             {
949                 // should not occur, we check line.size() == 2
950                 throw new RuntimeException("Unexpected exception while obtaining points from line to cross.", exception);
951             }
952             if (intersect != null)
953             {
954                 cumul += points[i].distanceSI(intersect);
955                 cumul += adjust; // , 0.0); // possible rear is already considered in first segment
956                 // return time at distance
957                 if (cumul < 0.0)
958                 {
959                     return getSimulator().getSimulatorTime();
960                 }
961                 if (cumul <= getOperationalPlan().getTotalLength().si)
962                 {
963                     return getOperationalPlan().timeAtDistance(Length.instantiateSI(cumul));
964                 }
965                 // ref will cross the line, but GTU will not travel enough for rear to cross
966                 return null;
967             }
968             else if (i < points.length - 2)
969             {
970                 cumul += points[i].distanceSI(points[i + 1]);
971             }
972         }
973         // no intersect
974         return null;
975     }
976 
977     /** {@inheritDoc} */
978     @Override
979     public final Map<Lane, Length> positions(final RelativePosition relativePosition) throws GTUException
980     {
981         return positions(relativePosition, getSimulator().getSimulatorTime());
982     }
983 
984     /** {@inheritDoc} */
985     @Override
986     public final Map<Lane, Length> positions(final RelativePosition relativePosition, final Time when) throws GTUException
987     {
988         Map<Lane, Length> positions = new LinkedHashMap<>();
989         for (CrossSection crossSection : this.crossSections.get(when))
990         {
991             for (Lane lane : crossSection.getLanes())
992             {
993                 positions.put(lane, position(lane, relativePosition, when));
994             }
995         }
996         return positions;
997     }
998 
999     /** {@inheritDoc} */
1000     @Override
1001     public final Length position(final Lane lane, final RelativePosition relativePosition) throws GTUException
1002     {
1003         return position(lane, relativePosition, getSimulator().getSimulatorTime());
1004     }
1005 
1006     /** caching of time field for last stored position(s). */
1007     private double cachePositionsTime = Double.NaN;
1008 
1009     /** caching of last stored position(s). */
1010     private Map<Integer, Length> cachedPositions = new LinkedHashMap<>();
1011 
1012     /** {@inheritDoc} */
1013     @Override
1014     @SuppressWarnings("checkstyle:designforextension")
1015     public Length position(final Lane lane, final RelativePosition relativePosition, final Time when) throws GTUException
1016     {
1017         int cacheIndex = 0;
1018         if (CACHING)
1019         {
1020             cacheIndex = 17 * lane.hashCode() + relativePosition.hashCode();
1021             Length l;
1022             if (when.si == this.cachePositionsTime && (l = this.cachedPositions.get(cacheIndex)) != null)
1023             {
1024                 // PK verify the result; uncomment if you don't trust the cache
1025                 // this.cachedPositions.clear();
1026                 // Length difficultWay = position(lane, relativePosition, when);
1027                 // if (Math.abs(l.si - difficultWay.si) > 0.00001)
1028                 // {
1029                 // System.err.println("Whoops: cache returns bad value for GTU " + getId());
1030                 // }
1031                 CACHED_POSITION++;
1032                 return l;
1033             }
1034             if (when.si != this.cachePositionsTime)
1035             {
1036                 this.cachedPositions.clear();
1037                 this.cachePositionsTime = when.si;
1038             }
1039         }
1040         NON_CACHED_POSITION++;
1041 
1042         synchronized (this.lock)
1043         {
1044             List<CrossSection> whenCrossSections = this.crossSections.get(when);
1045             double loc = Double.NaN;
1046 
1047             try
1048             {
1049                 int crossSectionIndex = -1;
1050                 int lateralIndex = -1;
1051                 for (int i = 0; i < whenCrossSections.size(); i++)
1052                 {
1053                     if (whenCrossSections.get(i).getLanes().contains(lane))
1054                     {
1055                         crossSectionIndex = i;
1056                         lateralIndex = whenCrossSections.get(i).getLanes().indexOf(lane);
1057                         break;
1058                     }
1059                 }
1060                 Throw.when(lateralIndex == -1, GTUException.class, "GTU %s is not on lane %s.", this, lane);
1061 
1062                 OperationalPlan plan = getOperationalPlan(when);
1063                 DirectedPoint p = plan.getLocation(when, relativePosition);
1064                 double f = lane.getCenterLine().projectFractional(null, null, p.x, p.y, FractionalFallback.NaN);
1065                 if (!Double.isNaN(f))
1066                 {
1067                     loc = f * lane.getLength().si;
1068                 }
1069                 else
1070                 {
1071                     // the point does not project fractionally to this lane, it has to be up- or downstream of the lane
1072                     // try upstream
1073                     double distance = 0.0;
1074                     for (int i = crossSectionIndex - 1; i >= 0; i--)
1075                     {
1076                         Lane tryLane = whenCrossSections.get(i).getLanes().get(lateralIndex);
1077                         f = tryLane.getCenterLine().projectFractional(null, null, p.x, p.y, FractionalFallback.NaN);
1078                         if (!Double.isNaN(f))
1079                         {
1080                             f = whenCrossSections.get(i).getDirection() == GTUDirectionality.DIR_PLUS ? 1 - f : f;
1081                             loc = distance - f * tryLane.getLength().si;
1082                             break;
1083                         }
1084                         distance -= tryLane.getLength().si;
1085                     }
1086                     // try downstream
1087                     if (Double.isNaN(loc))
1088                     {
1089                         distance = lane.getLength().si;
1090                         for (int i = crossSectionIndex + 1; i < whenCrossSections.size(); i++)
1091                         {
1092                             Lane tryLane = whenCrossSections.get(i).getLanes().get(lateralIndex);
1093                             f = tryLane.getCenterLine().projectFractional(null, null, p.x, p.y, FractionalFallback.NaN);
1094                             if (!Double.isNaN(f))
1095                             {
1096                                 f = whenCrossSections.get(i).getDirection() == GTUDirectionality.DIR_PLUS ? f : 1 - f;
1097                                 loc = distance + f * tryLane.getLength().si;
1098                                 break;
1099                             }
1100                             distance += tryLane.getLength().si;
1101                         }
1102                     }
1103 
1104                 }
1105 
1106                 if (Double.isNaN(loc))
1107                 {
1108                     // the GTU is not on the lane with the relativePosition, nor is it registered on next/previous lanes
1109                     // this can occur as the GTU was generated with the rear upstream of the lane, or due to rounding errors
1110                     // use different fraction projection fallback
1111                     f = lane.getCenterLine().projectFractional(null, null, p.x, p.y, FractionalFallback.ENDPOINT);
1112                     loc = lane.getLength().si * f;
1113 
1114                     // if (CACHING)
1115                     // {
1116                     // this.cachedPositions.put(cacheIndex, null);
1117                     // }
1118                     // return null;
1119                     // if (getOdometer().lt(getLength()))
1120                     // {
1121                     // // this occurs when the GTU is generated with the rear upstream of the lane, which we often do
1122                     // loc = position(lane, getFront(), when).si + relativePosition.getDx().si - getFront().getDx().si;
1123                     // }
1124                     // else
1125                     // {
1126                     // System.out.println("loc is NaN");
1127                     // }
1128                 }
1129             }
1130             catch (Exception e)
1131             {
1132                 System.err.println(toString());
1133                 throw new GTUException(e);
1134             }
1135 
1136             Length length = Length.instantiateSI(loc);
1137             if (CACHING)
1138             {
1139                 this.cachedPositions.put(cacheIndex, length);
1140             }
1141             return length;
1142         }
1143     }
1144 
1145     /** {@inheritDoc} */
1146     @Override
1147     @SuppressWarnings("checkstyle:designforextension")
1148     public DirectedLanePosition getReferencePosition() throws GTUException
1149     {
1150         if (this.referencePositionTime == getSimulator().getSimulatorTime().si)
1151         {
1152             return this.cachedReferencePosition;
1153         }
1154         Lane refLane = null;
1155         for (CrossSection crossSection : this.crossSections)
1156         {
1157             Lane lane = crossSection.getLanes().get(this.referenceLaneIndex);
1158             double fraction = fractionalPosition(lane, getReference());
1159             if (fraction >= 0.0 && fraction <= 1.0)
1160             {
1161                 refLane = lane;
1162                 break;
1163             }
1164         }
1165         if (refLane != null)
1166         {
1167             this.cachedReferencePosition =
1168                     new DirectedLanePosition(refLane, position(refLane, getReference()), this.getDirection(refLane));
1169             this.referencePositionTime = getSimulator().getSimulatorTime().si;
1170             return this.cachedReferencePosition;
1171         }
1172         throw new GTUException("The reference point of GTU " + this + " is not on any of the lanes on which it is registered");
1173     }
1174 
1175     /** {@inheritDoc} */
1176     @Override
1177     public final Map<Lane, Double> fractionalPositions(final RelativePosition relativePosition) throws GTUException
1178     {
1179         return fractionalPositions(relativePosition, getSimulator().getSimulatorTime());
1180     }
1181 
1182     /** {@inheritDoc} */
1183     @Override
1184     public final Map<Lane, Double> fractionalPositions(final RelativePosition relativePosition, final Time when)
1185             throws GTUException
1186     {
1187         Map<Lane, Double> positions = new LinkedHashMap<>();
1188         for (CrossSection crossSection : this.crossSections)
1189         {
1190             for (Lane lane : crossSection.getLanes())
1191             {
1192                 positions.put(lane, fractionalPosition(lane, relativePosition, when));
1193             }
1194         }
1195         return positions;
1196     }
1197 
1198     /** {@inheritDoc} */
1199     @Override
1200     public final double fractionalPosition(final Lane lane, final RelativePosition relativePosition, final Time when)
1201             throws GTUException
1202     {
1203         return position(lane, relativePosition, when).getSI() / lane.getLength().getSI();
1204     }
1205 
1206     /** {@inheritDoc} */
1207     @Override
1208     public final double fractionalPosition(final Lane lane, final RelativePosition relativePosition) throws GTUException
1209     {
1210         return position(lane, relativePosition).getSI() / lane.getLength().getSI();
1211     }
1212 
1213     /** {@inheritDoc} */
1214     @Override
1215     public final void addTrigger(final Lane lane, final SimEventInterface<SimTimeDoubleUnit> event)
1216     {
1217         throw new UnsupportedOperationException("Method addTrigger is not supported.");
1218     }
1219 
1220     /**
1221      * Sets a vehicle model.
1222      * @param vehicleModel VehicleModel; vehicle model
1223      */
1224     public void setVehicleModel(final VehicleModel vehicleModel)
1225     {
1226         this.vehicleModel = vehicleModel;
1227     }
1228 
1229     /** {@inheritDoc} */
1230     @Override
1231     public VehicleModel getVehicleModel()
1232     {
1233         return this.vehicleModel;
1234     }
1235 
1236     /** {@inheritDoc} */
1237     @Override
1238     @SuppressWarnings("checkstyle:designforextension")
1239     public void destroy()
1240     {
1241         DirectedLanePosition dlp = null;
1242         try
1243         {
1244             dlp = getReferencePosition();
1245         }
1246         catch (GTUException e)
1247         {
1248             // ignore. not important at destroy
1249         }
1250         DirectedPoint location = this.getOperationalPlan() == null ? new DirectedPoint(0.0, 0.0, 0.0) : getLocation();
1251         synchronized (this.lock)
1252         {
1253             for (CrossSection crossSection : this.crossSections)
1254             {
1255                 boolean removeFromParentLink = true;
1256                 for (Lane lane : crossSection.getLanes())
1257                 {
1258                     Length position;
1259                     try
1260                     {
1261                         position = position(lane, getReference());
1262                     }
1263                     catch (GTUException exception)
1264                     {
1265                         // TODO: hard remove over whole network
1266                         // TODO: logger notification
1267                         throw new RuntimeException(exception);
1268                     }
1269                     lane.removeGTU(this, removeFromParentLink, position);
1270                     removeFromParentLink = false;
1271                 }
1272             }
1273         }
1274         if (dlp != null && dlp.getLane() != null)
1275         {
1276             Lane referenceLane = dlp.getLane();
1277             fireTimedEvent(LaneBasedGTU.LANEBASED_DESTROY_EVENT,
1278                     new Object[] {getId(), location, getOdometer(), referenceLane, dlp.getPosition(), dlp.getGtuDirection()},
1279                     getSimulator().getSimulatorTime());
1280         }
1281         else
1282         {
1283             fireTimedEvent(LaneBasedGTU.LANEBASED_DESTROY_EVENT,
1284                     new Object[] {getId(), location, getOdometer(), null, Length.ZERO, null},
1285                     getSimulator().getSimulatorTime());
1286         }
1287         cancelAllEvents();
1288 
1289         super.destroy();
1290     }
1291 
1292     /** {@inheritDoc} */
1293     @Override
1294     public final Bounds getBounds()
1295     {
1296         double dx = 0.5 * getLength().doubleValue();
1297         double dy = 0.5 * getWidth().doubleValue();
1298         return new BoundingBox(new Point3d(-dx, -dy, 0.0), new Point3d(dx, dy, 0.0));
1299     }
1300 
1301     /** {@inheritDoc} */
1302     @Override
1303     public final LaneBasedStrategicalPlanner getStrategicalPlanner()
1304     {
1305         return (LaneBasedStrategicalPlanner) super.getStrategicalPlanner();
1306     }
1307 
1308     /** {@inheritDoc} */
1309     @Override
1310     public final LaneBasedStrategicalPlanner getStrategicalPlanner(final Time time)
1311     {
1312         return (LaneBasedStrategicalPlanner) super.getStrategicalPlanner(time);
1313     }
1314 
1315     /** {@inheritDoc} */
1316     @Override
1317     public RoadNetwork getNetwork()
1318     {
1319         return (RoadNetwork) super.getPerceivableContext();
1320     }
1321 
1322     /** {@inheritDoc} */
1323     @Override
1324     public Speed getDesiredSpeed()
1325     {
1326         Time simTime = getSimulator().getSimulatorTime();
1327         if (this.desiredSpeedTime == null || this.desiredSpeedTime.si < simTime.si)
1328         {
1329             InfrastructurePerception infra =
1330                     getTacticalPlanner().getPerception().getPerceptionCategoryOrNull(InfrastructurePerception.class);
1331             SpeedLimitInfo speedInfo;
1332             if (infra == null)
1333             {
1334                 speedInfo = new SpeedLimitInfo();
1335                 speedInfo.addSpeedInfo(SpeedLimitTypes.MAX_VEHICLE_SPEED, getMaximumSpeed());
1336             }
1337             else
1338             {
1339                 // Throw.whenNull(infra, "InfrastructurePerception is required to determine the desired speed.");
1340                 speedInfo = infra.getSpeedLimitProspect(RelativeLane.CURRENT).getSpeedLimitInfo(Length.ZERO);
1341             }
1342             this.cachedDesiredSpeed =
1343                     Try.assign(() -> getTacticalPlanner().getCarFollowingModel().desiredSpeed(getParameters(), speedInfo),
1344                             "Parameter exception while obtaining the desired speed.");
1345             this.desiredSpeedTime = simTime;
1346         }
1347         return this.cachedDesiredSpeed;
1348     }
1349 
1350     /** {@inheritDoc} */
1351     @Override
1352     public Acceleration getCarFollowingAcceleration()
1353     {
1354         Time simTime = getSimulator().getSimulatorTime();
1355         if (this.carFollowingAccelerationTime == null || this.carFollowingAccelerationTime.si < simTime.si)
1356         {
1357             LanePerception perception = getTacticalPlanner().getPerception();
1358             // speed
1359             EgoPerception<?, ?> ego = perception.getPerceptionCategoryOrNull(EgoPerception.class);
1360             Throw.whenNull(ego, "EgoPerception is required to determine the speed.");
1361             Speed speed = ego.getSpeed();
1362             // speed limit info
1363             InfrastructurePerception infra = perception.getPerceptionCategoryOrNull(InfrastructurePerception.class);
1364             Throw.whenNull(infra, "InfrastructurePerception is required to determine the desired speed.");
1365             SpeedLimitInfo speedInfo = infra.getSpeedLimitProspect(RelativeLane.CURRENT).getSpeedLimitInfo(Length.ZERO);
1366             // leaders
1367             NeighborsPerception neighbors = perception.getPerceptionCategoryOrNull(NeighborsPerception.class);
1368             Throw.whenNull(neighbors, "NeighborsPerception is required to determine the car-following acceleration.");
1369             PerceptionCollectable<HeadwayGTU, LaneBasedGTU> leaders = neighbors.getLeaders(RelativeLane.CURRENT);
1370             // obtain
1371             this.cachedCarFollowingAcceleration =
1372                     Try.assign(() -> getTacticalPlanner().getCarFollowingModel().followingAcceleration(getParameters(), speed,
1373                             speedInfo, leaders), "Parameter exception while obtaining the desired speed.");
1374             this.carFollowingAccelerationTime = simTime;
1375         }
1376         return this.cachedCarFollowingAcceleration;
1377     }
1378 
1379     /** {@inheritDoc} */
1380     @Override
1381     public final TurnIndicatorStatus getTurnIndicatorStatus()
1382     {
1383         return this.turnIndicatorStatus.get();
1384     }
1385 
1386     /** {@inheritDoc} */
1387     @Override
1388     public final TurnIndicatorStatus getTurnIndicatorStatus(final Time time)
1389     {
1390         return this.turnIndicatorStatus.get(time);
1391     }
1392 
1393     /** {@inheritDoc} */
1394     @Override
1395     public final void setTurnIndicatorStatus(final TurnIndicatorStatus turnIndicatorStatus)
1396     {
1397         this.turnIndicatorStatus.set(turnIndicatorStatus);
1398     }
1399 
1400     /** {@inheritDoc} */
1401     @Override
1402     public Length getLateralPosition(final Lane lane) throws GTUException
1403     {
1404         OperationalPlan plan = getOperationalPlan();
1405         if (plan instanceof LaneBasedOperationalPlanntrafficsim/road/gtu/lane/plan/operational/LaneBasedOperationalPlan.html#LaneBasedOperationalPlan">LaneBasedOperationalPlan && !((LaneBasedOperationalPlan) plan).isDeviative())
1406         {
1407             return Length.ZERO;
1408         }
1409         DirectedLanePosition ref = getReferencePosition();
1410         int latIndex = -1;
1411         int longIndex = -1;
1412         for (int i = 0; i < this.crossSections.size(); i++)
1413         {
1414             List<Lane> lanes = this.crossSections.get(i).getLanes();
1415             if (lanes.contains(lane))
1416             {
1417                 latIndex = lanes.indexOf(lane);
1418             }
1419             if (lanes.contains(ref.getLane()))
1420             {
1421                 longIndex = i;
1422             }
1423         }
1424         Throw.when(latIndex == -1 || longIndex == -1, GTUException.class, "GTU %s is not on %s", getId(), lane);
1425         Lane refCrossSectionLane = this.crossSections.get(longIndex).getLanes().get(latIndex);
1426         DirectedPoint loc = getLocation();
1427         double f = refCrossSectionLane.getCenterLine().projectOrthogonal(loc.x, loc.y);
1428         DirectedPoint p = Try.assign(() -> refCrossSectionLane.getCenterLine().getLocationFraction(f), GTUException.class,
1429                 "GTU %s is not orthogonal to the reference lane.", getId());
1430         double d = p.distance(loc);
1431         d = ref.getGtuDirection().isPlus() ? d : -d;
1432         if (this.crossSections.get(0).getLanes().size() > 1)
1433         {
1434             return Length.instantiateSI(latIndex == 0 ? -d : d);
1435         }
1436         double x2 = p.x + Math.cos(p.getRotZ());
1437         double y2 = p.y + Math.sin(p.getRotZ());
1438         double det = (loc.x - p.x) * (y2 - p.y) - (loc.y - p.y) * (x2 - p.x);
1439         return Length.instantiateSI(det < 0.0 ? -d : d);
1440     }
1441 
1442     /** {@inheritDoc} */
1443     @Override
1444     public void setInstantaneousLaneChange(final boolean instantaneous)
1445     {
1446         this.instantaneousLaneChange = instantaneous;
1447     }
1448 
1449     /** {@inheritDoc} */
1450     @Override
1451     public boolean isInstantaneousLaneChange()
1452     {
1453         return this.instantaneousLaneChange;
1454     }
1455 
1456     /** {@inheritDoc} */
1457     @Override
1458     @SuppressWarnings("checkstyle:designforextension")
1459     public String toString()
1460     {
1461         return String.format("GTU " + getId());
1462     }
1463 
1464     /** Cross section of lanes. */
1465     private static class CrossSection
1466     {
1467 
1468         /** Lanes. */
1469         private final List<Lane> lanes;
1470 
1471         /** GTU directionality. */
1472         private final GTUDirectionality direction;
1473 
1474         /**
1475          * @param lanes List&lt;Lane&gt;; lanes
1476          * @param direction GTUDirectionality; GTU directionality
1477          */
1478         protected CrossSection(final List<Lane> lanes, final GTUDirectionality direction)
1479         {
1480             this.lanes = lanes;
1481             this.direction = direction;
1482         }
1483 
1484         /**
1485          * @return lanes.
1486          */
1487         protected List<Lane> getLanes()
1488         {
1489             return this.lanes;
1490         }
1491 
1492         /**
1493          * @return direction.
1494          */
1495         protected GTUDirectionality getDirection()
1496         {
1497             return this.direction;
1498         }
1499 
1500     }
1501 
1502 }