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