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