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