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