View Javadoc
1   package org.opentrafficsim.road.gtu.lane.perception;
2   
3   import java.io.Serializable;
4   import java.rmi.RemoteException;
5   import java.util.Iterator;
6   import java.util.LinkedHashMap;
7   import java.util.LinkedHashSet;
8   import java.util.List;
9   import java.util.Map;
10  import java.util.Set;
11  import java.util.SortedSet;
12  import java.util.TreeMap;
13  import java.util.TreeSet;
14  
15  import org.djunits.value.vdouble.scalar.Length;
16  import org.djunits.value.vdouble.scalar.Time;
17  import org.djutils.event.Event;
18  import org.djutils.event.EventListener;
19  import org.djutils.exceptions.Throw;
20  import org.djutils.exceptions.Try;
21  import org.opentrafficsim.core.gtu.GtuException;
22  import org.opentrafficsim.core.gtu.GtuType;
23  import org.opentrafficsim.core.gtu.RelativePosition;
24  import org.opentrafficsim.core.network.LateralDirectionality;
25  import org.opentrafficsim.core.network.Link;
26  import org.opentrafficsim.core.network.Node;
27  import org.opentrafficsim.core.network.route.Route;
28  import org.opentrafficsim.core.perception.Historical;
29  import org.opentrafficsim.core.perception.HistoricalValue;
30  import org.opentrafficsim.core.perception.HistoryManager;
31  import org.opentrafficsim.road.gtu.lane.LaneBasedGtu;
32  import org.opentrafficsim.road.gtu.lane.perception.RollingLaneStructureRecord.RecordLink;
33  import org.opentrafficsim.road.gtu.lane.plan.operational.LaneBasedOperationalPlan;
34  import org.opentrafficsim.road.network.lane.CrossSectionLink;
35  import org.opentrafficsim.road.network.lane.Lane;
36  import org.opentrafficsim.road.network.lane.LanePosition;
37  import org.opentrafficsim.road.network.lane.object.LaneBasedObject;
38  
39  /**
40   * This data structure can clearly indicate the lane structure ahead of us, e.g. in the following situation:
41   * 
42   * <pre>
43   *     (---- a ----)(---- b ----)(---- c ----)(---- d ----)(---- e ----)(---- f ----)(---- g ----)  
44   *                                             __________                             __________
45   *                                            / _________ 1                          / _________ 2
46   *                                           / /                                    / /
47   *                                __________/ /             _______________________/ /
48   *  1  ____________ ____________ /_ _ _ _ _ _/____________ /_ _ _ _ _ _ _ _ _ _ _ _ /      
49   *  0 |_ _X_ _ _ _ |_ _ _ _ _ _ |_ _ _ _ _ _ |_ _ _ _ _ _ |_ _ _ _ _ _ |_ _ _ _ _ _ \____________
50   * -1 |____________|_ _ _ _ _ _ |____________|____________|  __________|____________|____________| 3
51   * -2              / __________/                           \ \  
52   *        ________/ /                                       \ \___________  
53   *      5 _________/                                         \____________  4
54   * </pre>
55   * 
56   * When the GTU is looking ahead, it needs to know that when it continues to destination 3, it needs to shift one lane to the
57   * right at some point, but <b>not</b> two lanes to the right in link b, and not later than at the end of link f. When it needs
58   * to go to destination 1, it needs to shift to the left in link c. When it has to go to destination 2, it has to shift to the
59   * left, but not earlier than at link e. At node [de], it is possible to leave the rightmost lane of link e, and go to
60   * destination 4. The rightmost lane just splits into two lanes at the end of link d, and the GTU can either continue driving to
61   * destination 3, turn right to destination 4. This means that the right lane of link d has <b>two</b> successor lanes.
62   * <p>
63   * In the data structures, lanes are numbered laterally. Suppose that the lane where vehicle X resides would be number 0.
64   * Consistent with "left is positive" for angles, the lane right of X would have number -1, and entry 5 would have number -2.
65   * <p>
66   * In the data structure, this can be indicated as follows (N = next, P = previous, L = left, R = right, D = lane drop, . =
67   * continued but not in this structure). The merge lane in b is considered "off limits" for the GTUs on the "main" lane -1; the
68   * "main" lane 0 is considered off limits from the exit lanes on c, e, and f. Still, we need to maintain pointers to these
69   * lanes, as we are interested in the GTUs potentially driving next to us, feeding into our lane, etc.
70   * 
71   * <pre>
72   *       1                0               -1               -2
73   *       
74   *                       ROOT 
75   *                   _____|_____      ___________      ___________            
76   *                  |_-_|_._|_R_|----|_L_|_._|_-_|    |_-_|_._|_-_|  a           
77   *                        |                |                |
78   *                   _____V_____      _____V_____      _____V_____            
79   *                  |_-_|_N_|_R_|----|_L_|_N_|_R_|&lt;---|_L_|_D_|_-_|  b           
80   *                        |                |                 
81   *  ___________      _____V_____      _____V_____                 
82   * |_-_|_N_|_R_|&lt;---|_L_|_N_|_R_|----|_L_|_N_|_-_|                   c
83   *       |                |                |                 
84   *  _____V_____      _____V_____      _____V_____                 
85   * |_-_|_._|_-_|    |_-_|_N_|_R_|----|_L_|_NN|_-_|                   d          
86   *                        |                ||_______________ 
87   *  ___________      _____V_____      _____V_____      _____V_____            
88   * |_-_|_N_|_R_|&lt;---|_L_|_N_|_R_|----|_L_|_N_|_-_|    |_-_|_N_|_-_|  e          
89   *       |                |                |                |
90   *  _____V_____      _____V_____      _____V_____      _____V_____            
91   * |_-_|_N_|_R_|&lt;---|_L_|_D_|_R_|----|_L_|_N_|_-_|    |_-_|_._|_-_|  f          
92   *       |                                 |                 
93   *  _____V_____                       _____V_____                             
94   * |_-_|_._|_-_|                     |_-_|_._|_-_|                   g
95   * </pre>
96   * <p>
97   * Copyright (c) 2013-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
98   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
99   * </p>
100  * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
101  */
102 public class RollingLaneStructure implements LaneStructure, Serializable, EventListener
103 {
104     /** */
105     private static final long serialVersionUID = 20160400L;
106 
107     /** The lanes from which we observe the situation. */
108     private final Historical<RollingLaneStructureRecord> root;
109 
110     /** Look ahead distance. */
111     private Length lookAhead;
112 
113     /** Route the structure is based on. */
114     private Route previousRoute;
115 
116     /** Whether the previous plan was deviative. */
117     private boolean previouslyDeviative = false;
118 
119     /** Lane structure records of the cross section. */
120     private TreeMap<RelativeLane, RollingLaneStructureRecord> crossSectionRecords = new TreeMap<>();
121 
122     /** First lane structure records. */
123     private TreeMap<RelativeLane, RollingLaneStructureRecord> firstRecords = new TreeMap<>();
124 
125     /** Lane structure records grouped per relative lane. */
126     private Map<RelativeLane, Set<RollingLaneStructureRecord>> relativeLaneMap = new LinkedHashMap<>();
127 
128     /** Relative lanes storage per record, such that other records can be linked to the correct relative lane. */
129     private Map<LaneStructureRecord, RelativeLane> relativeLanes = new LinkedHashMap<>();
130 
131     /** Set of lanes that can be ignored as they are beyond build bounds. */
132     private final Set<Lane> ignoreSet = new LinkedHashSet<>();
133 
134     /** Upstream edges. */
135     private final Set<RollingLaneStructureRecord> upstreamEdge = new LinkedHashSet<>();
136 
137     /** Downstream edges. */
138     private final Set<RollingLaneStructureRecord> downstreamEdge = new LinkedHashSet<>();
139 
140     /** Downstream distance over which the structure is made. */
141     private final Length down;
142 
143     /** Upstream distance over which the structure is made. */
144     private final Length up;
145 
146     /** Downstream distance at splits (links not on route) included in the structure. */
147     private final Length downSplit;
148 
149     /** Upstream distance at downstream merges (links not on route) included in the structure. */
150     private final Length upMerge;
151 
152     /** GTU. */
153     private final LaneBasedGtu containingGtu;
154 
155     /** the animation access. */
156     @SuppressWarnings("checkstyle:visibilitymodifier")
157     public AnimationAccess animationAccess = new AnimationAccess();
158 
159     /**
160      * Constructor.
161      * @param lookAhead Length; distance over which visual objects are included
162      * @param down Length; downstream distance over which the structure is made
163      * @param up Length; upstream distance over which the structure is made, should include a margin for reaction time
164      * @param downSplit Length; downstream distance at splits (links not on route) included in the structure
165      * @param upMerge Length; upstream distance at downstream merges (links not on route) included in the structure
166      * @param gtu LaneBasedGtu; GTU
167      */
168     public RollingLaneStructure(final Length lookAhead, final Length down, final Length up, final Length downSplit,
169             final Length upMerge, final LaneBasedGtu gtu)
170     {
171         HistoryManager historyManager = gtu.getSimulator().getReplication().getHistoryManager(gtu.getSimulator());
172         this.root = new HistoricalValue<>(historyManager);
173         this.lookAhead = lookAhead;
174         this.down = down;
175         this.up = up;
176         this.downSplit = downSplit;
177         this.upMerge = upMerge;
178         this.containingGtu = gtu;
179         gtu.addListener(this, LaneBasedGtu.LANE_CHANGE_EVENT);
180     }
181 
182     /**
183      * Updates the underlying structure shifting the root position to the input.
184      * @param pos LanePosition; current position of the GTU
185      * @param route Route; current route of the GTU
186      * @param gtuType GtuType; GTU type
187      * @throws GtuException on a problem while updating the structure
188      */
189     @Override
190     public final void update(final LanePosition pos, final Route route, final GtuType gtuType) throws GtuException
191     {
192         /*
193          * Implementation note: the LaneStructure was previously generated by AbstractLanePerception every time step. This
194          * functionality has been moved to LaneStructure itself, in a manner that can update the LaneStructure. Start distances
195          * of individual records are therefore made dynamic, calculated relative to a neighboring source record. For many time
196          * steps this means that only these distances have to be updated. In other cases, the sources for start distances are
197          * changed concerning the records that were involved in the previous time step. The LaneStructure now also maintains an
198          * upstream and a downstream edge, i.e. set of records. These are moved forward as the GTU moves.
199          */
200 
201         // fractional position
202         Lane lane = pos.getLane();
203         Length position = pos.getPosition();
204         double fracPos = position.si / lane.getLength().si;
205         boolean deviative = this.containingGtu.getOperationalPlan() instanceof LaneBasedOperationalPlan
206                 && ((LaneBasedOperationalPlan) this.containingGtu.getOperationalPlan()).isDeviative();
207 
208         // TODO on complex networks, e.g. with sections connectors where lane changes are not possible, the update may fail
209         if (this.previousRoute != route || this.root.get() == null || deviative != this.previouslyDeviative)
210         {
211             // create new LaneStructure
212             this.previousRoute = route;
213 
214             // clear
215             this.upstreamEdge.clear();
216             this.downstreamEdge.clear();
217             this.crossSectionRecords.clear();
218             this.relativeLanes.clear();
219             this.relativeLaneMap.clear();
220             this.firstRecords.clear();
221 
222             // build cross-section
223             RollingLaneStructureRecord newRoot = constructRecord(lane, null, RecordLink.CROSS, RelativeLane.CURRENT);
224             this.root.set(newRoot);
225             this.crossSectionRecords.put(RelativeLane.CURRENT, newRoot);
226             for (LateralDirectionality latDirection : new LateralDirectionality[] {LateralDirectionality.LEFT,
227                     LateralDirectionality.RIGHT})
228             {
229                 RollingLaneStructureRecord current = newRoot;
230                 RelativeLane relativeLane = RelativeLane.CURRENT;
231                 Set<Lane> adjacentLanes = current.getLane().accessibleAdjacentLanesPhysical(latDirection, gtuType);
232                 while (!adjacentLanes.isEmpty())
233                 {
234                     Throw.when(adjacentLanes.size() > 1, RuntimeException.class,
235                             "Multiple adjacent lanes encountered during construction of lane map.");
236                     relativeLane = latDirection.isLeft() ? relativeLane.getLeft() : relativeLane.getRight();
237                     Lane adjacentLane = adjacentLanes.iterator().next();
238                     RollingLaneStructureRecord adjacentRecord =
239                             constructRecord(adjacentLane, current, RecordLink.CROSS, relativeLane);
240                     this.crossSectionRecords.put(relativeLane, adjacentRecord);
241                     if (latDirection.isLeft())
242                     {
243                         if (adjacentLane.accessibleAdjacentLanesPhysical(LateralDirectionality.RIGHT, gtuType)
244                                 .contains(current.getLane()))
245                         {
246                             adjacentRecord.setRight(current, gtuType);
247                         }
248                         current.setLeft(adjacentRecord, gtuType);
249                     }
250                     else
251                     {
252                         if (adjacentLane.accessibleAdjacentLanesPhysical(LateralDirectionality.LEFT, gtuType)
253                                 .contains(current.getLane()))
254                         {
255                             adjacentRecord.setLeft(current, gtuType);
256                         }
257                         current.setRight(adjacentRecord, gtuType);
258                     }
259                     current = adjacentRecord;
260                     adjacentLanes = current.getLane().accessibleAdjacentLanesPhysical(latDirection, gtuType);
261                 }
262             }
263             this.upstreamEdge.addAll(this.crossSectionRecords.values());
264             this.downstreamEdge.addAll(this.crossSectionRecords.values());
265             this.firstRecords.putAll(this.crossSectionRecords);
266 
267             // set the start distances so the upstream expand can work
268             newRoot.updateStartDistance(fracPos, this);
269 
270             // expand upstream edge
271             expandUpstreamEdge(gtuType, fracPos);
272 
273             // derive first records
274             deriveFirstRecords();
275         }
276         else
277         {
278 
279             // update LaneStructure
280             RollingLaneStructureRecord newRoot = this.root.get();
281             if (!lane.equals(newRoot.getLane()))
282             {
283                 // find the root, and possible lateral shift if changed lane
284                 newRoot = null;
285                 RelativeLane lateralMove = null;
286                 double closest = Double.POSITIVE_INFINITY;
287                 for (RelativeLane relativeLane : this.relativeLaneMap.keySet())
288                 {
289                     for (RollingLaneStructureRecord record : this.relativeLaneMap.get(relativeLane))
290                     {
291                         if (record.getLane().equals(lane) && record.getStartDistance().si < closest
292                                 && record.getStartDistance().si + record.getLength().si > 0.0)
293                         {
294                             newRoot = record;
295                             lateralMove = relativeLane;
296                             // multiple records may be present for the current lane due to a loop
297                             closest = record.getStartDistance().si;
298                         }
299                     }
300                     if (newRoot != null)
301                     {
302                         break;
303                     }
304                 }
305                 // newRoot.getPrev().contains(newRoot.getStartDistanceSource())
306                 this.root.set(newRoot);
307 
308                 // update start distance sources
309                 updateStartDistanceSources();
310 
311                 // shift if changed lane
312                 if (!lateralMove.isCurrent())
313                 {
314                     RelativeLane delta =
315                             new RelativeLane(lateralMove.getLateralDirectionality().flip(), lateralMove.getNumLanes());
316 
317                     TreeMap<RelativeLane, Set<RollingLaneStructureRecord>> newRelativeLaneMap = new TreeMap<>();
318                     for (RelativeLane relativeLane : this.relativeLaneMap.keySet())
319                     {
320                         RelativeLane newRelativeLane = relativeLane.add(delta);
321                         newRelativeLaneMap.put(newRelativeLane, this.relativeLaneMap.get(relativeLane));
322                     }
323                     this.relativeLaneMap = newRelativeLaneMap;
324 
325                     Map<LaneStructureRecord, RelativeLane> newRelativeLanes = new LinkedHashMap<>();
326                     for (LaneStructureRecord record : this.relativeLanes.keySet())
327                     {
328                         newRelativeLanes.put(record, this.relativeLanes.get(record).add(delta));
329                     }
330                     this.relativeLanes = newRelativeLanes;
331                 }
332 
333                 this.crossSectionRecords.clear();
334                 this.crossSectionRecords.put(RelativeLane.CURRENT, newRoot);
335                 for (LateralDirectionality latDirection : new LateralDirectionality[] {LateralDirectionality.LEFT,
336                         LateralDirectionality.RIGHT})
337                 {
338                     RollingLaneStructureRecord record = newRoot;
339                     RollingLaneStructureRecord next = newRoot;
340                     RelativeLane delta = new RelativeLane(latDirection, 1);
341                     RelativeLane relLane = RelativeLane.CURRENT;
342                     while (next != null)
343                     {
344                         next = latDirection.isLeft() ? record.getLeft() : record.getRight();
345                         if (next != null)
346                         {
347                             next.changeStartDistanceSource(record, RecordLink.CROSS);
348                             relLane = relLane.add(delta);
349                             this.crossSectionRecords.put(relLane, next);
350                             record = next;
351                         }
352                     }
353                 }
354 
355             }
356             newRoot.updateStartDistance(fracPos, this);
357 
358             // update upstream edges
359             retreatUpstreamEdge();
360 
361             // derive first records
362             deriveFirstRecords();
363 
364         }
365 
366         this.previouslyDeviative = deviative;
367 
368         // update downstream edges
369         expandDownstreamEdge(gtuType, fracPos, route);
370 
371     }
372 
373     /**
374      * Derives the first downstream records so the extended cross-section can be returned.
375      */
376     private void deriveFirstRecords()
377     {
378         this.firstRecords.clear();
379         this.firstRecords.putAll(this.crossSectionRecords);
380         for (RelativeLane lane : this.relativeLaneMap.keySet())
381         {
382             getFirstRecord(lane); // store non-null values internally
383         }
384     }
385 
386     /**
387      * Upstream algorithm from the new source, where records along the way are assigned a new start distance source. These
388      * records were downstream in the previous time step, but are now upstream. Hence, their start distance source should now
389      * become their downstream record. This algorithm acts like an upstream tree, where each branch is stopped if there are no
390      * upstream records, or the upstream records already have their downstream record as source (i.e. the record was already
391      * upstream in the previous time step).
392      */
393     private void updateStartDistanceSources()
394     {
395 
396         // initial cross section
397         Set<RollingLaneStructureRecord> set = new LinkedHashSet<>();
398         RollingLaneStructureRecord rootRecord = this.root.get();
399         set.add(rootRecord);
400         rootRecord.changeStartDistanceSource(null, RecordLink.CROSS);
401         RollingLaneStructureRecord prev = rootRecord;
402         RollingLaneStructureRecord next = prev.getLeft();
403         while (next != null)
404         {
405             set.add(next);
406             next.changeStartDistanceSource(prev, RecordLink.CROSS);
407             prev = next;
408             next = next.getLeft();
409         }
410         prev = rootRecord;
411         next = prev.getRight();
412         while (next != null)
413         {
414             set.add(next);
415             next.changeStartDistanceSource(prev, RecordLink.CROSS);
416             prev = next;
417             next = next.getRight();
418         }
419         // tree algorithm (branches flattened to a single set)
420         while (!set.isEmpty())
421         {
422             // lateral
423             Set<RollingLaneStructureRecord> newSet = new LinkedHashSet<>();
424             for (RollingLaneStructureRecord record : set)
425             {
426                 for (LateralDirectionality latDirection : new LateralDirectionality[] {LateralDirectionality.LEFT,
427                         LateralDirectionality.RIGHT})
428                 {
429                     prev = record;
430                     next = latDirection.isLeft() ? record.getLeft() : record.getRight();
431                     while (next != null && !set.contains(next))
432                     {
433                         next.changeStartDistanceSource(prev, RecordLink.LATERAL_END);
434                         removeDownstream(next, latDirection.flip()); // split not taken can be thrown away
435                         newSet.add(next);
436                         prev = next;
437                         next = latDirection.isLeft() ? next.getLeft() : next.getRight();
438                     }
439                 }
440             }
441             set.addAll(newSet);
442 
443             // longitudinal
444             newSet.clear();
445             for (RollingLaneStructureRecord record : set)
446             {
447                 for (RollingLaneStructureRecord prevRecord : record.getPrev())
448                 {
449                     Iterator<RollingLaneStructureRecord> it = prevRecord.getNext().iterator();
450                     while (it.hasNext())
451                     {
452                         RollingLaneStructureRecord otherDown = it.next();
453                         if (!otherDown.getLane().getParentLink().equals(record.getLane().getParentLink()))
454                         {
455                             // split not taken can be thrown away
456                             otherDown.changeStartDistanceSource(null, null);
457                             // this can throw away records that are laterally connected as they later merge... ??
458                             removeDownstream(otherDown, LateralDirectionality.NONE);
459                             removeRecord(otherDown);
460                             it.remove();
461                         }
462                     }
463                     LaneStructureRecord source = prevRecord.getStartDistanceSource();
464                     if (source == null || (source != null && !source.equals(record)))
465                     {
466                         prevRecord.changeStartDistanceSource(record, RecordLink.UP);
467                         newSet.add(prevRecord);
468                     }
469                 }
470             }
471 
472             // next loop
473             set = newSet;
474         }
475     }
476 
477     /**
478      * Removes all records downstream of the given record from underlying data structures.
479      * @param record RollingLaneStructureRecord; record, downstream of which to remove all records
480      * @param lat LateralDirectionality; records with an adjacent record at this side are not deleted
481      */
482     private void removeDownstream(final RollingLaneStructureRecord record, final LateralDirectionality lat)
483     {
484         for (RollingLaneStructureRecord next : record.getNext())
485         {
486             RollingLaneStructureRecord adj = lat.isLeft() ? next.getLeft() : lat.isRight() ? next.getRight() : null;
487             if (adj == null)
488             {
489                 next.changeStartDistanceSource(null, null);
490                 removeDownstream(next, lat);
491                 removeRecord(next);
492             }
493         }
494         record.clearNextList();
495     }
496 
497     /**
498      * Removes the record from underlying data structures.
499      * @param record RollingLaneStructureRecord; record to remove
500      */
501     private void removeRecord(final RollingLaneStructureRecord record)
502     {
503         RelativeLane lane = this.relativeLanes.get(record);
504         if (lane != null)
505         {
506             this.relativeLaneMap.get(lane).remove(record);
507             this.relativeLanes.remove(record);
508         }
509         record.changeStartDistanceSource(null, null);
510 
511     }
512 
513     /**
514      * On a new build, this method is used to create the upstream map.
515      * @param gtuType GtuType; GTU type
516      * @param fractionalPosition double; fractional position on reference link
517      * @throws GtuException on exception
518      */
519     private void expandUpstreamEdge(final GtuType gtuType, final double fractionalPosition) throws GtuException
520     {
521         this.ignoreSet.clear();
522         for (LaneStructureRecord record : this.upstreamEdge)
523         {
524             this.ignoreSet.add(record.getLane());
525         }
526         Set<RollingLaneStructureRecord> nextSet = new LinkedHashSet<>();
527         boolean expand = true;
528         while (expand)
529         {
530             expand = false;
531 
532             // longitudinal
533             Iterator<RollingLaneStructureRecord> iterator = this.upstreamEdge.iterator();
534             Set<RollingLaneStructureRecord> modifiedEdge = new LinkedHashSet<>(this.upstreamEdge);
535             while (iterator.hasNext())
536             {
537                 RollingLaneStructureRecord prev = iterator.next();
538                 Set<Lane> nexts = prev.getLane().prevLanes(gtuType);
539                 if (prev.getStartDistance().si < this.up.si)
540                 {
541                     // upstream search ends on this lane
542                     prev.setCutOffStart(this.up.minus(prev.getStartDistance()));
543                     for (Lane prevLane : nexts)
544                     {
545                         this.ignoreSet.add(prevLane); // exclude in lateral search
546                     }
547                 }
548                 else
549                 {
550                     // upstream search goes further upstream
551                     prev.clearCutOffStart();
552                     iterator.remove();
553                     if (!nexts.isEmpty())
554                     {
555                         for (Lane prevLane : nexts)
556                         {
557                             RelativeLane relativeLane = this.relativeLanes.get(prev);
558                             RollingLaneStructureRecord next = constructRecord(prevLane, prev, RecordLink.UP, relativeLane);
559                             this.ignoreSet.add(prevLane);
560                             next.updateStartDistance(fractionalPosition, this);
561                             connectLaterally(next, gtuType, modifiedEdge);
562                             next.addNext(prev);
563                             prev.addPrev(next);
564                             nextSet.add(next);
565                             modifiedEdge.add(next);
566                         }
567                     }
568                 }
569             }
570             this.upstreamEdge.addAll(nextSet);
571             expand |= !nextSet.isEmpty();
572             nextSet.clear();
573 
574             // lateral
575             Set<RollingLaneStructureRecord> lateralSet =
576                     expandLateral(this.upstreamEdge, RecordLink.LATERAL_END, gtuType, fractionalPosition);
577             nextSet.addAll(lateralSet);
578 
579             // next iteration
580             this.upstreamEdge.addAll(nextSet);
581             expand |= !nextSet.isEmpty();
582             nextSet.clear();
583         }
584     }
585 
586     /**
587      * Helper method for upstream and downstream expansion. This method returns all lanes that can be laterally found from the
588      * input set.
589      * @param edge Set&lt;RollingLaneStructureRecord&gt;; input set
590      * @param recordLink RecordLink; link to add between lateral records, depends on upstream or downstream search
591      * @param gtuType GtuType; GTU type
592      * @param fractionalPosition double; fractional position on reference link
593      * @return Set&lt;LaneStructureRecord&gt;; output set with all laterally found lanes
594      */
595     private Set<RollingLaneStructureRecord> expandLateral(final Set<RollingLaneStructureRecord> edge,
596             final RecordLink recordLink, final GtuType gtuType, final double fractionalPosition)
597     {
598         Set<RollingLaneStructureRecord> nextSet = new LinkedHashSet<>();
599         Set<Lane> laneSet = new LinkedHashSet<>(); // set to check that an adjacent lane is not another lane already in the set
600         for (LaneStructureRecord record : edge)
601         {
602             laneSet.add(record.getLane());
603         }
604         Iterator<RollingLaneStructureRecord> iterator = edge.iterator();
605         while (iterator.hasNext())
606         {
607             RollingLaneStructureRecord record = iterator.next();
608             for (LateralDirectionality latDirection : new LateralDirectionality[] {LateralDirectionality.LEFT,
609                     LateralDirectionality.RIGHT})
610             {
611                 if (record.getRight() != null && latDirection.isRight() || record.getLeft() != null && latDirection.isLeft())
612                 {
613                     // skip if there already is a record on that side
614                     continue;
615                 }
616                 RelativeLane relativeLane = this.relativeLanes.get(record);
617                 RollingLaneStructureRecord prev = record;
618                 Set<Lane> adjacentLanes = prev.getLane().accessibleAdjacentLanesPhysical(latDirection, gtuType);
619                 while (!adjacentLanes.isEmpty())
620                 {
621                     Throw.when(adjacentLanes.size() > 1, RuntimeException.class,
622                             "Multiple adjacent lanes encountered during construction of lane map.");
623                     relativeLane = latDirection.isLeft() ? relativeLane.getLeft() : relativeLane.getRight();
624                     Lane nextLane = adjacentLanes.iterator().next();
625                     if (!laneSet.contains(nextLane) && !this.ignoreSet.contains(nextLane))
626                     {
627                         RollingLaneStructureRecord next = constructRecord(nextLane, prev, recordLink, relativeLane);
628                         this.ignoreSet.add(nextLane);
629                         next.updateStartDistance(fractionalPosition, this);
630                         nextSet.add(next);
631                         laneSet.add(nextLane);
632                         if (latDirection.isLeft())
633                         {
634                             prev.setLeft(next, gtuType);
635                             if (nextLane.accessibleAdjacentLanesPhysical(LateralDirectionality.RIGHT, gtuType)
636                                     .contains(prev.getLane()))
637                             {
638                                 next.setRight(prev, gtuType);
639                             }
640                             for (RollingLaneStructureRecord edgeRecord : edge)
641                             {
642                                 if (!edgeRecord.equals(prev)
643                                         && edgeRecord.getLane().getParentLink().equals(next.getLane().getParentLink()))
644                                 {
645                                     for (Lane adjLane : edgeRecord.getLane()
646                                             .accessibleAdjacentLanesPhysical(LateralDirectionality.RIGHT, gtuType))
647                                     {
648                                         if (adjLane.equals(next.getLane()))
649                                         {
650                                             edgeRecord.setRight(next, gtuType);
651                                             next.setLeft(edgeRecord, gtuType);
652                                         }
653                                     }
654                                 }
655                             }
656                         }
657                         else
658                         {
659                             prev.setRight(next, gtuType);
660                             if (nextLane.accessibleAdjacentLanesPhysical(LateralDirectionality.LEFT, gtuType)
661                                     .contains(prev.getLane()))
662                             {
663                                 next.setLeft(prev, gtuType);
664                             }
665                             for (RollingLaneStructureRecord edgeRecord : edge)
666                             {
667                                 if (!edgeRecord.equals(prev)
668                                         && edgeRecord.getLane().getParentLink().equals(next.getLane().getParentLink()))
669                                 {
670                                     for (Lane adjLane : edgeRecord.getLane()
671                                             .accessibleAdjacentLanesPhysical(LateralDirectionality.LEFT, gtuType))
672                                     {
673                                         if (adjLane.equals(next.getLane()))
674                                         {
675                                             edgeRecord.setLeft(next, gtuType);
676                                             next.setRight(edgeRecord, gtuType);
677                                         }
678                                     }
679                                 }
680                             }
681                         }
682                         // connect longitudinally due to merge or split
683                         Set<RollingLaneStructureRecord> adjs = new LinkedHashSet<>();
684                         if (next.getLeft() != null)
685                         {
686                             adjs.add(next.getLeft());
687                         }
688                         if (next.getRight() != null)
689                         {
690                             adjs.add(next.getRight());
691                         }
692                         for (RollingLaneStructureRecord adj : adjs)
693                         {
694                             for (Lane lane : next.getLane().prevLanes(gtuType))
695                             {
696                                 for (RollingLaneStructureRecord adjPrev : adj.getPrev())
697                                 {
698                                     if (lane.equals(adjPrev.getLane()))
699                                     {
700                                         Try.execute(() -> next.addPrev(adjPrev), "Cut-off record added as prev.");
701                                     }
702                                 }
703                             }
704                             for (Lane lane : next.getLane().nextLanes(gtuType))
705                             {
706                                 for (RollingLaneStructureRecord adjNext : adj.getNext())
707                                 {
708                                     if (lane.equals(adjNext.getLane()))
709                                     {
710                                         Try.execute(() -> next.addNext(adjNext), "Cut-off record added as next.");
711                                     }
712                                 }
713                             }
714                         }
715 
716                         prev = next;
717                         adjacentLanes = prev.getLane().accessibleAdjacentLanesPhysical(latDirection, gtuType);
718                     }
719                     else
720                     {
721                         break;
722                     }
723                 }
724             }
725         }
726         return nextSet;
727     }
728 
729     /**
730      * This method makes sure that not all history is maintained forever, and the upstream edge moves with the GTU.
731      * @throws GtuException on exception
732      */
733     private void retreatUpstreamEdge() throws GtuException
734     {
735         boolean moved = true;
736         Set<RollingLaneStructureRecord> nexts = new LinkedHashSet<>();
737         while (moved)
738         {
739             moved = false;
740             nexts.clear();
741             Iterator<RollingLaneStructureRecord> iterator = this.upstreamEdge.iterator();
742             while (iterator.hasNext())
743             {
744                 RollingLaneStructureRecord prev = iterator.next();
745                 // nexts may contain 'prev' as two lanes are removed from the upstream edge that merge in to 1 downstream lane
746                 if (!nexts.contains(prev) && prev.getStartDistance().si + prev.getLane().getLength().si < this.up.si)
747                 {
748                     for (RollingLaneStructureRecord next : prev.getNext())
749                     {
750                         next.clearPrevList();
751                         next.setCutOffStart(this.up.minus(next.getStartDistance()));
752                         moved = true;
753                         nexts.add(next);
754                         RollingLaneStructureRecord lat = next.getLeft();
755                         while (lat != null && lat.getPrev().isEmpty())
756                         {
757                             nexts.add(lat);
758                             lat = lat.getLeft();
759                         }
760                         lat = next.getRight();
761                         while (lat != null && lat.getPrev().isEmpty())
762                         {
763                             nexts.add(lat);
764                             lat = lat.getRight();
765                         }
766                     }
767                     prev.clearNextList();
768                     removeRecord(prev);
769                     iterator.remove();
770                 }
771                 else
772                 {
773                     Length cutOff = this.up.minus(prev.getStartDistance());
774                     if (cutOff.si > 0)
775                     {
776                         prev.setCutOffStart(cutOff);
777                     }
778                 }
779             }
780             this.upstreamEdge.addAll(nexts);
781 
782             // check adjacent lanes
783             for (RollingLaneStructureRecord record : nexts)
784             {
785                 RollingLaneStructureRecord prev = record;
786                 for (LateralDirectionality latDirection : new LateralDirectionality[] {LateralDirectionality.LEFT,
787                         LateralDirectionality.RIGHT})
788                 {
789                     while (prev != null)
790                     {
791                         RollingLaneStructureRecord next = latDirection.isLeft() ? prev.getLeft() : prev.getRight();
792                         if (next != null && !this.upstreamEdge.contains(next))
793                         {
794                             moved |= findUpstreamEdge(next);
795                         }
796                         prev = next;
797                     }
798                 }
799             }
800         }
801     }
802 
803     /**
804      * Recursive method to find downstream record(s) on the upstream edge, as the edge was moved downstream and a laterally
805      * connected lane was not yet in the upstream edge. All edge records are added to the edge set.
806      * @param record RollingLaneStructureRecord; newly found adjacent record after moving the upstream edge downstream
807      * @return boolean; whether a record was added to the edge, note that no record is added of the record is fully downstream
808      *         of the upstream view distance
809      * @throws GtuException on exception
810      */
811     private boolean findUpstreamEdge(final RollingLaneStructureRecord record) throws GtuException
812     {
813         Length cutOff = this.up.minus(record.getStartDistance());
814         boolean moved = false;
815         if (cutOff.gt0())
816         {
817             if (cutOff.lt(record.getLane().getLength()))
818             {
819                 record.clearPrevList();
820                 record.setCutOffStart(cutOff);
821                 this.upstreamEdge.add(record);
822                 moved = true;
823             }
824             else
825             {
826                 if (this.relativeLanes.containsKey(record))
827                 {
828                     // could have been removed from upstream already
829                     removeRecord(record);
830                 }
831                 for (RollingLaneStructureRecord next : record.getNext())
832                 {
833                     moved |= findUpstreamEdge(next);
834                 }
835             }
836         }
837         return moved;
838     }
839 
840     /**
841      * Main downstream search for the map. Can be used at initial build and to update.
842      * @param gtuType GtuType; GTU type
843      * @param fractionalPosition double; fractional position on reference link
844      * @param route Route; route of the GTU
845      * @throws GtuException on exception
846      */
847     private void expandDownstreamEdge(final GtuType gtuType, final double fractionalPosition, final Route route)
848             throws GtuException
849     {
850         this.ignoreSet.clear();
851         for (LaneStructureRecord record : this.downstreamEdge)
852         {
853             this.ignoreSet.add(record.getLane());
854         }
855         Set<RollingLaneStructureRecord> nextSet = new LinkedHashSet<>();
856         Set<RollingLaneStructureRecord> splitSet = new LinkedHashSet<>();
857         boolean expand = true;
858         while (expand)
859         {
860             expand = false;
861 
862             // longitudinal
863             // find links to extend from so we can add lanes if -any- of the next lanes comes within the perception distance
864             Set<Link> linksToExpandFrom = new LinkedHashSet<>();
865             Iterator<RollingLaneStructureRecord> iterator = this.downstreamEdge.iterator();
866             while (iterator.hasNext())
867             {
868                 RollingLaneStructureRecord record = iterator.next();
869                 if (record.getStartDistance().si + record.getLane().getLength().si < this.down.si)
870                 {
871                     linksToExpandFrom.add(record.getLane().getParentLink());
872                 }
873             }
874             Set<RollingLaneStructureRecord> modifiedEdge = new LinkedHashSet<>(this.downstreamEdge);
875             iterator = this.downstreamEdge.iterator();
876             while (iterator.hasNext())
877             {
878                 RollingLaneStructureRecord record = iterator.next();
879                 Set<Lane> nexts = record.getLane().nextLanes(gtuType);
880                 if (!linksToExpandFrom.contains(record.getLane().getParentLink()))
881                 {
882                     // downstream search ends on this lane
883                     record.setCutOffEnd(this.down.minus(record.getStartDistance()));
884                     for (Lane nextLane : nexts)
885                     {
886                         this.ignoreSet.add(nextLane); // exclude in lateral search
887                     }
888                 }
889                 else
890                 {
891                     // downstream search goes further downstream
892 
893                     // in case there are multiple lanes on the same link after a lane split, we need to choose one
894                     Lane nextLaneForSplit = this.containingGtu.getNextLaneForRoute(record.getLane());
895 
896                     record.clearCutOffEnd();
897                     iterator.remove(); // can remove from edge, no algorithm needs it anymore in the downstream edge
898                     for (Lane nextLane : nexts)
899                     {
900                         if (nextLaneForSplit != null && nextLane.getParentLink().equals(nextLaneForSplit.getParentLink())
901                                 && !nextLane.equals(nextLaneForSplit))
902                         {
903                             // skip this lane as its a not chosen lane on the next link after a lane split
904                             continue;
905                         }
906                         RelativeLane relativeLane = this.relativeLanes.get(record);
907                         RollingLaneStructureRecord next = constructRecord(nextLane, record, RecordLink.DOWN, relativeLane);
908                         this.ignoreSet.add(nextLane);
909                         next.updateStartDistance(fractionalPosition, this);
910                         record.addNext(next);
911                         next.addPrev(record);
912                         connectLaterally(next, gtuType, modifiedEdge);
913                         // check route
914                         int from = route == null ? 0 : route.indexOf(next.getFromNode());
915                         int to = route == null ? 1 : route.indexOf(next.getToNode());
916                         if (to < 0 || to - from != 1)
917                         {
918                             // not on our route, add some distance and stop
919                             splitSet.add(next);
920                         }
921                         else
922                         {
923                             // regular edge
924                             nextSet.add(next);
925                         }
926                         modifiedEdge.add(next);
927                         // expand upstream over any possible other lane that merges in to this
928                         Set<RollingLaneStructureRecord> set = new LinkedHashSet<>();
929                         set.add(next);
930                         expandUpstreamMerge(set, gtuType, fractionalPosition, route);
931                     }
932                 }
933             }
934             this.downstreamEdge.addAll(nextSet);
935             expand |= !nextSet.isEmpty();
936             nextSet.clear();
937 
938             // split
939             expandDownstreamSplit(splitSet, gtuType, fractionalPosition, route);
940             splitSet.clear();
941 
942             // lateral
943             Set<RollingLaneStructureRecord> lateralSet =
944                     expandLateral(this.downstreamEdge, RecordLink.LATERAL_END, gtuType, fractionalPosition);
945             nextSet.addAll(lateralSet);
946             expandUpstreamMerge(lateralSet, gtuType, fractionalPosition, route);
947 
948             // next iteration
949             this.downstreamEdge.addAll(nextSet);
950             expand |= !nextSet.isEmpty();
951             nextSet.clear();
952         }
953     }
954 
955     /**
956      * Expand the map to include a limited section downstream of a split, regarding links not on the route.
957      * @param set Set&lt;RollingLaneStructureRecord&gt;; set of lanes that have been laterally found
958      * @param gtuType GtuType; GTU type
959      * @param fractionalPosition double; fractional position on reference link
960      * @param route Route; route
961      * @throws GtuException on exception
962      */
963     private void expandDownstreamSplit(final Set<RollingLaneStructureRecord> set, final GtuType gtuType,
964             final double fractionalPosition, final Route route) throws GtuException
965     {
966         Map<RollingLaneStructureRecord, Length> prevs = new LinkedHashMap<>();
967         Map<RollingLaneStructureRecord, Length> nexts = new LinkedHashMap<>();
968         for (RollingLaneStructureRecord record : set)
969         {
970             prevs.put(record, record.getStartDistance().plus(this.downSplit));
971         }
972         while (!prevs.isEmpty())
973         {
974             for (RollingLaneStructureRecord prev : prevs.keySet())
975             {
976                 Set<Lane> nextLanes = prev.getLane().nextLanes(gtuType);
977                 RelativeLane relativeLane = this.relativeLanes.get(prev);
978                 for (Lane nextLane : nextLanes)
979                 {
980                     Node fromNode = nextLane.getParentLink().getStartNode();
981                     Node toNode = nextLane.getParentLink().getEndNode();
982                     int from = route.indexOf(fromNode);
983                     int to = route.indexOf(toNode);
984                     if (from == -1 || to == -1 || to - from != 1)
985                     {
986                         RollingLaneStructureRecord next = constructRecord(nextLane, prev, RecordLink.DOWN, relativeLane);
987                         next.updateStartDistance(fractionalPosition, this);
988                         next.addPrev(prev);
989                         prev.addNext(next);
990                         connectLaterally(next, gtuType, nexts.keySet());
991                         Length downHere = prevs.get(prev);
992                         if (next.getStartDistance().si > downHere.si)
993                         {
994                             next.setCutOffEnd(downHere.minus(next.getStartDistance()));
995                         }
996                         else
997                         {
998                             nexts.put(next, downHere);
999                         }
1000                     }
1001                 }
1002             }
1003             prevs = nexts;
1004             nexts = new LinkedHashMap<>();
1005         }
1006     }
1007 
1008     /**
1009      * Expand the map to include a limited section upstream of a merge that is downstream, regarding links not on the route.
1010      * @param set Set&lt;RollingLaneStructureRecord&gt;; set of lanes that have been laterally found
1011      * @param gtuType GtuType; GTU type
1012      * @param fractionalPosition double; fractional position on reference link
1013      * @param route Route; route of the GTU
1014      * @throws GtuException on exception
1015      */
1016     private void expandUpstreamMerge(final Set<RollingLaneStructureRecord> set, final GtuType gtuType,
1017             final double fractionalPosition, final Route route) throws GtuException
1018     {
1019         Map<RollingLaneStructureRecord, Length> prevs = new LinkedHashMap<>();
1020         Map<RollingLaneStructureRecord, Length> nexts = new LinkedHashMap<>();
1021         for (RollingLaneStructureRecord record : set)
1022         {
1023             prevs.put(record, record.getStartDistance().plus(this.upMerge)); // upMerge is negative
1024         }
1025         while (!prevs.isEmpty())
1026         {
1027             for (RollingLaneStructureRecord prev : prevs.keySet())
1028             {
1029                 Set<Lane> nextLanes = prev.getLane().prevLanes(gtuType);
1030                 boolean anyAdded = false;
1031                 for (Lane nextLane : nextLanes)
1032                 {
1033                     Node fromNode = nextLane.getParentLink().getStartNode();
1034                     Node toNode = nextLane.getParentLink().getEndNode();
1035                     int from = route == null ? 0 : route.indexOf(fromNode);
1036                     int to = route == null ? 1 : route.indexOf(toNode);
1037                     // TODO we now assume everything is on the route, but merges could be ok without route
1038                     // so, without a route we should be able to recognize which upstream 'nextLane' is on the other link
1039                     if (from == -1 || to == -1 || to - from != 1)
1040                     {
1041                         anyAdded = true;
1042                         RelativeLane relativeLane = this.relativeLanes.get(prev);
1043                         RollingLaneStructureRecord next = constructRecord(nextLane, prev, RecordLink.UP, relativeLane);
1044                         next.updateStartDistance(fractionalPosition, this);
1045                         next.addNext(prev);
1046                         prev.addPrev(next);
1047                         connectLaterally(next, gtuType, nexts.keySet());
1048                         Length upHere = prevs.get(prev);
1049                         if (next.getStartDistance().si < upHere.si)
1050                         {
1051                             next.setCutOffStart(upHere.minus(next.getStartDistance()));
1052                             this.upstreamEdge.add(next);
1053                         }
1054                         else
1055                         {
1056                             nexts.put(next, upHere);
1057                         }
1058                     }
1059                 }
1060                 if (!anyAdded && !set.contains(prev))
1061                 {
1062                     this.upstreamEdge.add(prev);
1063                 }
1064             }
1065             prevs = nexts;
1066             nexts = new LinkedHashMap<>();
1067         }
1068     }
1069 
1070     /**
1071      * Helper method of various other methods that laterally couples lanes that have been longitudinally found.
1072      * @param record RollingLaneStructureRecord; longitudinally found lane
1073      * @param gtuType GtuType; GTU type
1074      * @param nextSet Set&lt;RollingLaneStructureRecord&gt;; set of records on current build edge
1075      */
1076     private void connectLaterally(final RollingLaneStructureRecord record, final GtuType gtuType,
1077             final Set<RollingLaneStructureRecord> nextSet)
1078     {
1079         for (RollingLaneStructureRecord other : nextSet)
1080         {
1081             for (LateralDirectionality latDirection : new LateralDirectionality[] {LateralDirectionality.LEFT,
1082                     LateralDirectionality.RIGHT})
1083             {
1084                 if ((latDirection.isLeft() ? other.getLeft() : other.getRight()) == null)
1085                 {
1086                     for (Lane otherLane : other.getLane().accessibleAdjacentLanesPhysical(latDirection, gtuType))
1087                     {
1088                         if (otherLane.equals(record.getLane()))
1089                         {
1090                             if (latDirection.isLeft())
1091                             {
1092                                 other.setLeft(record, gtuType);
1093                                 record.setRight(other, gtuType);
1094                             }
1095                             else
1096                             {
1097                                 other.setRight(record, gtuType);
1098                                 record.setLeft(other, gtuType);
1099                             }
1100                         }
1101                     }
1102                 }
1103             }
1104         }
1105     }
1106 
1107     /**
1108      * Creates a lane structure record and adds it to relevant maps.
1109      * @param lane Lane; lane
1110      * @param startDistanceSource RollingLaneStructureRecord; source of the start distance
1111      * @param recordLink RecordLink; record link
1112      * @param relativeLane RelativeLane; relative lane
1113      * @return created lane structure record
1114      */
1115     private RollingLaneStructureRecord constructRecord(final Lane lane, final RollingLaneStructureRecord startDistanceSource,
1116             final RecordLink recordLink, final RelativeLane relativeLane)
1117     {
1118         RollingLaneStructureRecord record = new RollingLaneStructureRecord(lane, startDistanceSource, recordLink);
1119         if (!this.relativeLaneMap.containsKey(relativeLane))
1120         {
1121             this.relativeLaneMap.put(relativeLane, new LinkedHashSet<>());
1122         }
1123         this.relativeLaneMap.get(relativeLane).add(record);
1124         this.relativeLanes.put(record, relativeLane);
1125         return record;
1126     }
1127 
1128     /** {@inheritDoc} */
1129     @Override
1130     public final LaneStructureRecord getRootRecord()
1131     {
1132         return this.root.get();
1133     }
1134 
1135     /**
1136      * @param time Time; time to obtain the root at
1137      * @return rootRecord
1138      */
1139     public final LaneStructureRecord getRootRecord(final Time time)
1140     {
1141         return this.root.get(time);
1142     }
1143 
1144     /** {@inheritDoc} */
1145     @Override
1146     public final SortedSet<RelativeLane> getExtendedCrossSection()
1147     {
1148         return this.firstRecords.navigableKeySet();
1149     }
1150 
1151     /**
1152      * Returns the first record on the given lane. This is often a record in the current cross section, but it may be one
1153      * downstream for a lane that starts further downstream.
1154      * @param lane RelativeLane; lane
1155      * @return first record on the given lane, or {@code null} if no such record
1156      */
1157     @Override
1158     public final RollingLaneStructureRecord getFirstRecord(final RelativeLane lane)
1159     {
1160         if (this.firstRecords.containsKey(lane))
1161         {
1162             return this.firstRecords.get(lane);
1163         }
1164         // not in current cross section, get first via downstream
1165         RelativeLane rel = RelativeLane.CURRENT;
1166         int dMin = Integer.MAX_VALUE;
1167         for (RelativeLane relLane : this.crossSectionRecords.keySet())
1168         {
1169             if (relLane.getLateralDirectionality().equals(lane.getLateralDirectionality()))
1170             {
1171                 int d = lane.getNumLanes() - relLane.getNumLanes();
1172                 if (d < dMin)
1173                 {
1174                     rel = relLane;
1175                     d = dMin;
1176                 }
1177             }
1178         }
1179         RollingLaneStructureRecord record = this.crossSectionRecords.get(rel);
1180         // move downstream until a lateral move is made to the right relative lane
1181         while (rel.getNumLanes() < lane.getNumLanes())
1182         {
1183             RollingLaneStructureRecord adj = lane.getLateralDirectionality().isLeft() ? record.getLeft() : record.getRight();
1184             if (adj != null)
1185             {
1186                 rel = lane.getLateralDirectionality().isLeft() ? rel.getLeft() : rel.getRight();
1187                 record = adj;
1188             }
1189             else if (!record.getNext().isEmpty())
1190             {
1191                 Lane nextLane = this.containingGtu.getNextLaneForRoute(record.getLane());
1192                 if (nextLane == null)
1193                 {
1194                     record = null;
1195                     break;
1196                 }
1197                 RollingLaneStructureRecord chosenNext = null;
1198                 for (RollingLaneStructureRecord next : record.getNext())
1199                 {
1200                     if (next.getLane().equals(nextLane))
1201                     {
1202                         chosenNext = next;
1203                         break;
1204                     }
1205                 }
1206                 // Throw.when(chosenNext == null, RuntimeException.class,
1207                 // "Unexpected exception while deriving first record not on the cross-section.");
1208                 record = chosenNext;
1209                 if (record == null)
1210                 {
1211                     // TODO: Temporary fix for Aimsun demo
1212                     break;
1213                 }
1214             }
1215             else
1216             {
1217                 // reached a dead-end
1218                 record = null;
1219                 break;
1220             }
1221         }
1222         if (record != null)
1223         {
1224             // now move upstream until we are at x = 0
1225             while (record.getPrev().size() == 1 && record.getStartDistance().gt0())
1226             {
1227                 record = record.getPrev().get(0);
1228             }
1229             this.firstRecords.put(lane, record);
1230         }
1231         return record;
1232     }
1233 
1234     /**
1235      * Retrieve objects of a specific type. Returns objects over a maximum length of the look ahead distance downstream from the
1236      * relative position, or as far as the lane map goes.
1237      * @param clazz Class&lt;T&gt;; class of objects to find
1238      * @param gtu LaneBasedGtu; gtu
1239      * @param pos RelativePosition.TYPE; relative position to start search from
1240      * @param <T> type of objects to find
1241      * @return Sorted set of objects of requested type per lane
1242      * @throws GtuException if lane is not in current set
1243      */
1244     @Override
1245     public final <T extends LaneBasedObject> Map<RelativeLane, SortedSet<Entry<T>>> getDownstreamObjects(final Class<T> clazz,
1246             final LaneBasedGtu gtu, final RelativePosition.TYPE pos) throws GtuException
1247     {
1248         Map<RelativeLane, SortedSet<Entry<T>>> out = new LinkedHashMap<>();
1249         for (RelativeLane relativeLane : this.relativeLaneMap.keySet())
1250         {
1251             out.put(relativeLane, getDownstreamObjects(relativeLane, clazz, gtu, pos));
1252         }
1253         return out;
1254     }
1255 
1256     /**
1257      * Retrieve objects on a lane of a specific type. Returns objects over a maximum length of the look ahead distance
1258      * downstream from the relative position, or as far as the lane map goes.
1259      * @param lane RelativeLane; lane
1260      * @param clazz Class&lt;T&gt;; class of objects to find
1261      * @param gtu LaneBasedGtu; gtu
1262      * @param pos RelativePosition.TYPE; relative position to start search from
1263      * @param <T> type of objects to find
1264      * @return Sorted set of objects of requested type
1265      * @throws GtuException if lane is not in current set
1266      */
1267     @Override
1268     @SuppressWarnings("unchecked")
1269     public final <T extends LaneBasedObject> SortedSet<Entry<T>> getDownstreamObjects(final RelativeLane lane,
1270             final Class<T> clazz, final LaneBasedGtu gtu, final RelativePosition.TYPE pos) throws GtuException
1271     {
1272         LaneStructureRecord record = getFirstRecord(lane);
1273         SortedSet<Entry<T>> set = new TreeSet<>();
1274         if (record != null)
1275         {
1276             double ds = gtu.getRelativePositions().get(pos).getDx().si - gtu.getReference().getDx().si;
1277             if (record.isDownstreamBranch())
1278             {
1279                 // the list is ordered, but only for DIR_PLUS, need to do our own ordering
1280                 Length minimumPosition = Length.instantiateSI(ds - record.getStartDistance().si);
1281                 Length maximumPosition = Length.instantiateSI(record.getLane().getLength().si);
1282 
1283                 for (LaneBasedObject object : record.getLane().getLaneBasedObjects(minimumPosition, maximumPosition))
1284                 {
1285                     if (clazz.isAssignableFrom(object.getClass()))
1286                     {
1287                         // unchecked, but the above isAssignableFrom assures correctness
1288                         double distance = record.getDistanceToPosition(object.getLongitudinalPosition()).si - ds;
1289                         if (distance <= this.lookAhead.si)
1290                         {
1291                             set.add(new Entry<>(Length.instantiateSI(distance), (T) object));
1292                         }
1293                     }
1294                 }
1295             }
1296             getDownstreamObjectsRecursive(set, record, clazz, ds);
1297         }
1298         return set;
1299     }
1300 
1301     /**
1302      * Retrieve objects on a lane of a specific type. Returns objects over a maximum length of the look ahead distance
1303      * downstream from the relative position, or as far as the lane map goes. Objects on links not on the route are ignored.
1304      * @param lane RelativeLane; lane
1305      * @param clazz Class&lt;T&gt;; class of objects to find
1306      * @param gtu LaneBasedGtu; gtu
1307      * @param pos RelativePosition.TYPE; relative position to start search from
1308      * @param <T> type of objects to find
1309      * @param route Route; the route
1310      * @return Sorted set of objects of requested type
1311      * @throws GtuException if lane is not in current set
1312      */
1313     @Override
1314     public final <T extends LaneBasedObject> SortedSet<Entry<T>> getDownstreamObjectsOnRoute(final RelativeLane lane,
1315             final Class<T> clazz, final LaneBasedGtu gtu, final RelativePosition.TYPE pos, final Route route)
1316             throws GtuException
1317     {
1318         SortedSet<Entry<T>> set = getDownstreamObjects(lane, clazz, gtu, pos);
1319         if (route != null)
1320         {
1321             Iterator<Entry<T>> iterator = set.iterator();
1322             while (iterator.hasNext())
1323             {
1324                 Entry<T> entry = iterator.next();
1325                 CrossSectionLink link = entry.getLaneBasedObject().getLane().getParentLink();
1326                 if (!route.contains(link.getStartNode()) || !route.contains(link.getEndNode())
1327                         || Math.abs(route.indexOf(link.getStartNode()) - route.indexOf(link.getEndNode())) != 1)
1328                 {
1329                     iterator.remove();
1330                 }
1331             }
1332         }
1333         return set;
1334     }
1335 
1336     /**
1337      * Recursive search for lane based objects downstream.
1338      * @param set SortedSet&lt;Entry&lt;T&gt;&gt;; set to store entries into
1339      * @param record LaneStructureRecord; current record
1340      * @param clazz Class&lt;T&gt;; class of objects to find
1341      * @param ds double; distance from reference to chosen relative position
1342      * @param <T> type of objects to find
1343      */
1344     @SuppressWarnings("unchecked")
1345     private <T extends LaneBasedObject> void getDownstreamObjectsRecursive(final SortedSet<Entry<T>> set,
1346             final LaneStructureRecord record, final Class<T> clazz, final double ds)
1347     {
1348         if (record.getNext().isEmpty() || record.getNext().get(0).getStartDistance().gt(this.lookAhead))
1349         {
1350             return;
1351         }
1352         for (LaneStructureRecord next : record.getNext())
1353         {
1354             if (next.isDownstreamBranch())
1355             {
1356                 List<LaneBasedObject> list = next.getLane().getLaneBasedObjects();
1357                 int iStart = 0;
1358                 int di = 1;
1359                 for (int i = iStart; i >= 0 & i < list.size(); i += di)
1360                 {
1361                     LaneBasedObject object = list.get(i);
1362                     if (clazz.isAssignableFrom(object.getClass()))
1363                     {
1364                         // unchecked, but the above isAssignableFrom assures correctness
1365                         double distance = next.getDistanceToPosition(object.getLongitudinalPosition()).si - ds;
1366                         if (distance <= this.lookAhead.si)
1367                         {
1368                             set.add(new Entry<>(Length.instantiateSI(distance), (T) object));
1369                         }
1370                         else
1371                         {
1372                             return;
1373                         }
1374                     }
1375                 }
1376             }
1377             getDownstreamObjectsRecursive(set, next, clazz, ds);
1378         }
1379     }
1380 
1381     /**
1382      * Retrieve objects of a specific type. Returns objects over a maximum length of the look ahead distance downstream from the
1383      * relative position, or as far as the lane map goes. Objects on links not on the route are ignored.
1384      * @param clazz Class&lt;T&gt;; class of objects to find
1385      * @param gtu LaneBasedGtu; gtu
1386      * @param pos RelativePosition.TYPE; relative position to start search from
1387      * @param <T> type of objects to find
1388      * @param route Route; the route
1389      * @return Sorted set of objects of requested type per lane
1390      * @throws GtuException if lane is not in current set
1391      */
1392     @Override
1393     public final <T extends LaneBasedObject> Map<RelativeLane, SortedSet<Entry<T>>> getDownstreamObjectsOnRoute(
1394             final Class<T> clazz, final LaneBasedGtu gtu, final RelativePosition.TYPE pos, final Route route)
1395             throws GtuException
1396     {
1397         Map<RelativeLane, SortedSet<Entry<T>>> out = new LinkedHashMap<>();
1398         for (RelativeLane relativeLane : this.relativeLaneMap.keySet())
1399         {
1400             out.put(relativeLane, getDownstreamObjectsOnRoute(relativeLane, clazz, gtu, pos, route));
1401         }
1402         return out;
1403     }
1404 
1405     /**
1406      * Retrieve objects on a lane of a specific type. Returns upstream objects from the relative position for as far as the lane
1407      * map goes. Distances to upstream objects are given as positive values.
1408      * @param lane RelativeLane; lane
1409      * @param clazz Class&lt;T&gt;; class of objects to find
1410      * @param gtu LaneBasedGtu; gtu
1411      * @param pos RelativePosition.TYPE; relative position to start search from
1412      * @param <T> type of objects to find
1413      * @return Sorted set of objects of requested type
1414      * @throws GtuException if lane is not in current set
1415      */
1416     @Override
1417     @SuppressWarnings("unchecked")
1418     public final <T extends LaneBasedObject> SortedSet<Entry<T>> getUpstreamObjects(final RelativeLane lane,
1419             final Class<T> clazz, final LaneBasedGtu gtu, final RelativePosition.TYPE pos) throws GtuException
1420     {
1421         SortedSet<Entry<T>> set = new TreeSet<>();
1422         LaneStructureRecord record = this.getFirstRecord(lane);
1423         if (record.getStartDistance().gt0())
1424         {
1425             return set; // this lane is only downstream
1426         }
1427         Length ds = gtu.getReference().getDx().minus(gtu.getRelativePositions().get(pos).getDx());
1428         // the list is ordered, but only for DIR_PLUS, need to do our own ordering
1429         Length minimumPosition = Length.ZERO;
1430         Length maximumPosition = record.getStartDistance().neg().minus(ds);
1431         Length distance;
1432         for (LaneBasedObject object : record.getLane().getLaneBasedObjects(minimumPosition, maximumPosition))
1433         {
1434             if (clazz.isAssignableFrom(object.getClass()))
1435             {
1436                 distance = record.getDistanceToPosition(object.getLongitudinalPosition()).neg().minus(ds);
1437                 // unchecked, but the above isAssignableFrom assures correctness
1438                 set.add(new Entry<>(distance, (T) object));
1439             }
1440         }
1441         getUpstreamObjectsRecursive(set, record, clazz, ds);
1442         return set;
1443     }
1444 
1445     /**
1446      * Recursive search for lane based objects upstream.
1447      * @param set SortedSet&lt;Entry&lt;T&gt;&gt;; set to store entries into
1448      * @param record LaneStructureRecord; current record
1449      * @param clazz Class&lt;T&gt;; class of objects to find
1450      * @param ds Length; distance from reference to chosen relative position
1451      * @param <T> type of objects to find
1452      */
1453     @SuppressWarnings("unchecked")
1454     private <T extends LaneBasedObject> void getUpstreamObjectsRecursive(final SortedSet<Entry<T>> set,
1455             final LaneStructureRecord record, final Class<T> clazz, final Length ds)
1456     {
1457         for (LaneStructureRecord prev : record.getPrev())
1458         {
1459             Length distance;
1460             for (LaneBasedObject object : prev.getLane().getLaneBasedObjects())
1461             {
1462                 if (clazz.isAssignableFrom(object.getClass()))
1463                 {
1464                     distance = prev.getDistanceToPosition(object.getLongitudinalPosition()).neg().minus(ds);
1465                     // unchecked, but the above isAssignableFrom assures correctness
1466                     set.add(new Entry<>(distance, (T) object));
1467                 }
1468             }
1469             getUpstreamObjectsRecursive(set, prev, clazz, ds);
1470         }
1471     }
1472 
1473     /**
1474      * Print the lane structure as a number of lines in a String.
1475      * @param ls RollingLaneStructure; the lane structure to print
1476      * @param gtu LaneBasedGtu; the GTTU for which the lane structure is printed
1477      * @return a String with information about the RollingLaneStructire
1478      */
1479     public static String print(final RollingLaneStructure ls, final LaneBasedGtu gtu)
1480     {
1481         StringBuffer s = new StringBuffer();
1482         s.append(gtu.getSimulator().getSimulatorTime() + " " + gtu.getId() + " LANESTRUCTURE: ");
1483         for (LaneStructureRecord lsr : ls.relativeLanes.keySet())
1484         {
1485             s.append(lsr.toString() + "  ");
1486         }
1487         int totSize = 0;
1488         for (Set<RollingLaneStructureRecord> set : ls.relativeLaneMap.values())
1489         {
1490             totSize += set.size();
1491         }
1492         s.append("\n  relativeLanes.size()=" + ls.relativeLanes.size() + "  relativeLaneMap.totalSize()=" + totSize);
1493         return s.toString();
1494     }
1495 
1496     /** {@inheritDoc} */
1497     @Override
1498     public final String toString()
1499     {
1500         return "LaneStructure [rootLSR=" + this.root + "]";
1501     }
1502 
1503     /**
1504      * AnimationAccess provides access to a number of private fields in the structure, which should only be used read-only! <br>
1505      * <br>
1506      * Copyright (c) 2003-2023 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
1507      * See for project information <a href="https://www.simulation.tudelft.nl/" target="_blank">www.simulation.tudelft.nl</a>.
1508      * The source code and binary code of this software is proprietary information of Delft University of Technology.
1509      * @author <a href="https://github.com/averbraeck" target="_blank">Alexander Verbraeck</a>
1510      */
1511     public class AnimationAccess
1512     {
1513         /**
1514          * @return the lane structure records of the cross section
1515          */
1516         @SuppressWarnings("synthetic-access")
1517         public TreeMap<RelativeLane, RollingLaneStructureRecord> getCrossSectionRecords()
1518         {
1519             return RollingLaneStructure.this.crossSectionRecords;
1520         }
1521 
1522         /**
1523          * @return the upstream edge
1524          */
1525         @SuppressWarnings("synthetic-access")
1526         public Set<RollingLaneStructureRecord> getUpstreamEdge()
1527         {
1528             return RollingLaneStructure.this.upstreamEdge;
1529         }
1530 
1531         /**
1532          * @return the downstream edge
1533          */
1534         @SuppressWarnings("synthetic-access")
1535         public Set<RollingLaneStructureRecord> getDownstreamEdge()
1536         {
1537             return RollingLaneStructure.this.downstreamEdge;
1538         }
1539     }
1540 
1541     /** {@inheritDoc} */
1542     @Override
1543     public void notify(final Event event) throws RemoteException
1544     {
1545         // triggers an update of the lane structure at the end of the final plan during the lane change, which is deviative
1546         this.previouslyDeviative = false;
1547     }
1548 }