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