RollingLaneStructure.java

  1. package org.opentrafficsim.road.gtu.lane.perception;

  2. import java.awt.BasicStroke;
  3. import java.awt.Color;
  4. import java.awt.Graphics2D;
  5. import java.awt.geom.Path2D;
  6. import java.awt.image.ImageObserver;
  7. import java.io.Serializable;
  8. import java.lang.reflect.Field;
  9. import java.rmi.RemoteException;
  10. import java.util.HashMap;
  11. import java.util.HashSet;
  12. import java.util.Iterator;
  13. import java.util.LinkedHashMap;
  14. import java.util.LinkedHashSet;
  15. import java.util.List;
  16. import java.util.Map;
  17. import java.util.Set;
  18. import java.util.SortedSet;
  19. import java.util.TreeMap;
  20. import java.util.TreeSet;

  21. import javax.media.j3d.BoundingBox;
  22. import javax.media.j3d.Bounds;
  23. import javax.naming.NamingException;
  24. import javax.vecmath.Point3d;

  25. import org.djunits.value.vdouble.scalar.Length;
  26. import org.djunits.value.vdouble.scalar.Time;
  27. import org.opentrafficsim.core.geometry.OTSGeometryException;
  28. import org.opentrafficsim.core.geometry.OTSLine3D;
  29. import org.opentrafficsim.core.geometry.OTSPoint3D;
  30. import org.opentrafficsim.core.gtu.GTU;
  31. import org.opentrafficsim.core.gtu.GTUDirectionality;
  32. import org.opentrafficsim.core.gtu.GTUException;
  33. import org.opentrafficsim.core.gtu.GTUType;
  34. import org.opentrafficsim.core.gtu.RelativePosition;
  35. import org.opentrafficsim.core.gtu.Try;
  36. import org.opentrafficsim.core.network.LateralDirectionality;
  37. import org.opentrafficsim.core.network.Node;
  38. import org.opentrafficsim.core.network.route.Route;
  39. import org.opentrafficsim.core.perception.Historical;
  40. import org.opentrafficsim.core.perception.HistoricalValue;
  41. import org.opentrafficsim.core.perception.HistoryManager;
  42. import org.opentrafficsim.road.gtu.lane.LaneBasedGTU;
  43. import org.opentrafficsim.road.gtu.lane.perception.RollingLaneStructureRecord.RecordLink;
  44. import org.opentrafficsim.road.network.lane.CrossSectionLink;
  45. import org.opentrafficsim.road.network.lane.DirectedLanePosition;
  46. import org.opentrafficsim.road.network.lane.Lane;
  47. import org.opentrafficsim.road.network.lane.LaneDirection;
  48. import org.opentrafficsim.road.network.lane.object.LaneBasedObject;

  49. import nl.tudelft.simulation.dsol.animation.Locatable;
  50. import nl.tudelft.simulation.dsol.animation.D2.Renderable2D;
  51. import nl.tudelft.simulation.language.Throw;
  52. import nl.tudelft.simulation.language.d3.DirectedPoint;

  53. /**
  54.  * This data structure can clearly indicate the lane structure ahead of us, e.g. in the following situation:
  55.  *
  56.  * <pre>
  57.  *     (---- a ----)(---- b ----)(---- c ----)(---- d ----)(---- e ----)(---- f ----)(---- g ----)  
  58.  *                                             __________                             __________
  59.  *                                            / _________ 1                          / _________ 2
  60.  *                                           / /                                    / /
  61.  *                                __________/ /             _______________________/ /
  62.  *  1  ____________ ____________ /_ _ _ _ _ _/____________ /_ _ _ _ _ _ _ _ _ _ _ _ /      
  63.  *  0 |_ _X_ _ _ _ |_ _ _ _ _ _ |_ _ _ _ _ _ |_ _ _ _ _ _ |_ _ _ _ _ _ |_ _ _ _ _ _ \____________
  64.  * -1 |____________|_ _ _ _ _ _ |____________|____________|  __________|____________|____________| 3
  65.  * -2              / __________/                           \ \  
  66.  *        ________/ /                                       \ \___________  
  67.  *      5 _________/                                         \____________  4
  68.  * </pre>
  69.  *
  70.  * 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
  71.  * 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
  72.  * 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
  73.  * 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
  74.  * destination 4. The rightmost lane just splits into two lanes at the end of link d, and the GTU can either continue driving to
  75.  * destination 3, turn right to destination 4. This means that the right lane of link d has <b>two</b> successor lanes.
  76.  * <p>
  77.  * In the data structures, lanes are numbered laterally. Suppose that the lane where vehicle X resides would be number 0.
  78.  * Consistent with "left is positive" for angles, the lane right of X would have number -1, and entry 5 would have number -2.
  79.  * <p>
  80.  * In the data structure, this can be indicated as follows (N = next, P = previous, L = left, R = right, D = lane drop, . =
  81.  * continued but not in this structure). The merge lane in b is considered "off limits" for the GTUs on the "main" lane -1; the
  82.  * "main" lane 0 is considered off limits from the exit lanes on c, e, and f. Still, we need to maintain pointers to these
  83.  * lanes, as we are interested in the GTUs potentially driving next to us, feeding into our lane, etc.
  84.  *
  85.  * <pre>
  86.  *       1                0               -1               -2
  87.  *      
  88.  *                       ROOT
  89.  *                   _____|_____      ___________      ___________            
  90.  *                  |_-_|_._|_R_|----|_L_|_._|_-_|    |_-_|_._|_-_|  a          
  91.  *                        |                |                |
  92.  *                   _____V_____      _____V_____      _____V_____            
  93.  *                  |_-_|_N_|_R_|----|_L_|_N_|_R_|&lt;---|_L_|_D_|_-_|  b          
  94.  *                        |                |                
  95.  *  ___________      _____V_____      _____V_____                
  96.  * |_-_|_N_|_R_|&lt;---|_L_|_N_|_R_|----|_L_|_N_|_-_|                   c
  97.  *       |                |                |                
  98.  *  _____V_____      _____V_____      _____V_____                
  99.  * |_-_|_._|_-_|    |_-_|_N_|_R_|----|_L_|_NN|_-_|                   d          
  100.  *                        |                ||_______________
  101.  *  ___________      _____V_____      _____V_____      _____V_____            
  102.  * |_-_|_N_|_R_|&lt;---|_L_|_N_|_R_|----|_L_|_N_|_-_|    |_-_|_N_|_-_|  e          
  103.  *       |                |                |                |
  104.  *  _____V_____      _____V_____      _____V_____      _____V_____            
  105.  * |_-_|_N_|_R_|&lt;---|_L_|_D_|_R_|----|_L_|_N_|_-_|    |_-_|_._|_-_|  f          
  106.  *       |                                 |                
  107.  *  _____V_____                       _____V_____                            
  108.  * |_-_|_._|_-_|                     |_-_|_._|_-_|                   g
  109.  * </pre>
  110.  * <p>
  111.  * Copyright (c) 2013-2018 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
  112.  * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  113.  * </p>
  114.  * $LastChangedDate: 2015-07-24 02:58:59 +0200 (Fri, 24 Jul 2015) $, @version $Revision: 1147 $, by $Author: averbraeck $,
  115.  * initial version Feb 20, 2016 <br>
  116.  * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
  117.  */
  118. public class RollingLaneStructure implements LaneStructure, Serializable
  119. {

  120.     /** */
  121.     private static final long serialVersionUID = 20160400L;

  122.     /** The lanes from which we observe the situation. */
  123.     private final Historical<RollingLaneStructureRecord> root;

  124.     /** Look ahead distance. */
  125.     private Length lookAhead;

  126.     /** Route the structure is based on. */
  127.     private Route previousRoute;

  128.     /** Lane structure records of the cross section. */
  129.     private TreeMap<RelativeLane, RollingLaneStructureRecord> crossSectionRecords = new TreeMap<>();

  130.     /** First lane structure records. */
  131.     private TreeMap<RelativeLane, RollingLaneStructureRecord> firstRecords = new TreeMap<>();

  132.     /** Lane structure records grouped per relative lane. */
  133.     private Map<RelativeLane, Set<RollingLaneStructureRecord>> relativeLaneMap = new HashMap<>();

  134.     /** Relative lanes storage per record, such that other records can be linked to the correct relative lane. */
  135.     private Map<LaneStructureRecord, RelativeLane> relativeLanes = new HashMap<>();

  136.     /** Set of lanes that can be ignored as they are beyond build bounds. */
  137.     private final Set<Lane> ignoreSet = new HashSet<>();

  138.     /** Upstream edges. */
  139.     private final Set<RollingLaneStructureRecord> upstreamEdge = new LinkedHashSet<>();

  140.     /** Downstream edges. */
  141.     private final Set<RollingLaneStructureRecord> downstreamEdge = new LinkedHashSet<>();

  142.     /** Downstream distance over which the structure is made. */
  143.     private final Length down;

  144.     /** Upstream distance over which the structure is made. */
  145.     private final Length up;

  146.     /** Downstream distance at splits (links not on route) included in the structure. */
  147.     private final Length downSplit;

  148.     /** Upstream distance at downstream merges (links not on route) included in the structure. */
  149.     private final Length upMerge;

  150.     /** GTU. */
  151.     private final LaneBasedGTU containingGtu;

  152.     /**
  153.      * Constructor.
  154.      * @param lookAhead Length; distance over which visual objects are included
  155.      * @param down Length; downstream distance over which the structure is made
  156.      * @param up Length; upstream distance over which the structure is made, should include a margin for reaction time
  157.      * @param downSplit Length; downstream distance at splits (links not on route) included in the structure
  158.      * @param upMerge Length; upstream distance at downstream merges (links not on route) included in the structure
  159.      * @param gtu LaneBasedGTU; GTU
  160.      */
  161.     public RollingLaneStructure(final Length lookAhead, final Length down, final Length up, final Length downSplit,
  162.             final Length upMerge, final LaneBasedGTU gtu)
  163.     {
  164.         this.root = new HistoricalValue<>(HistoryManager.get(gtu.getSimulator()));
  165.         this.lookAhead = lookAhead;
  166.         this.down = down;
  167.         this.up = up;
  168.         this.downSplit = downSplit;
  169.         this.upMerge = upMerge;
  170.         this.containingGtu = gtu;
  171.         // if (gtu.getId().equals("922"))
  172.         // {
  173.         // visualize(gtu);
  174.         // }
  175.     }

  176.     /**
  177.      * Updates the underlying structure shifting the root position to the input.
  178.      * @param pos DirectedLanePosition; current position of the GTU
  179.      * @param route Route; current route of the GTU
  180.      * @param gtuType GTUType; GTU type
  181.      * @throws GTUException on a problem while updating the structure
  182.      */
  183.     public final void update(final DirectedLanePosition pos, final Route route, final GTUType gtuType) throws GTUException
  184.     {

  185.         /*
  186.          * Implementation note: the LaneStructure was previously generated by AbstractLanePerception every time step. This
  187.          * functionality has been moved to LaneStructure itself, in a manner that can update the LaneStructure. Start distances
  188.          * of individual records are therefore made dynamic, calculated relative to a neighboring source record. For many time
  189.          * steps this means that only these distances have to be updated. In other cases, the sources for start distances are
  190.          * changed concerning the records that were involved in the previous time step. The LaneStructure now also maintains an
  191.          * upstream and a downstream edge, i.e. set of records. These are moved forward as the GTU moves.
  192.          */

  193.         // fractional position
  194.         Lane lane = pos.getLane();
  195.         GTUDirectionality direction = pos.getGtuDirection();
  196.         Length position = pos.getPosition();
  197.         double fracPos = direction.isPlus() ? position.si / lane.getLength().si : 1.0 - position.si / lane.getLength().si;

  198.         // TODO on complex networks, e.g. with sections connectors where lane changes are not possible, the update may fail
  199.         if (this.previousRoute != route || this.root.get() == null)
  200.         {
  201.             // create new LaneStructure
  202.             this.previousRoute = route;

  203.             // clear
  204.             this.upstreamEdge.clear();
  205.             this.downstreamEdge.clear();
  206.             this.crossSectionRecords.clear();
  207.             this.relativeLanes.clear();

  208.             // build cross-section
  209.             RollingLaneStructureRecord newRoot = constructRecord(lane, direction, null, RecordLink.CROSS, RelativeLane.CURRENT);
  210.             this.root.set(newRoot);
  211.             this.crossSectionRecords.put(RelativeLane.CURRENT, newRoot);
  212.             for (LateralDirectionality latDirection : new LateralDirectionality[] { LateralDirectionality.LEFT,
  213.                     LateralDirectionality.RIGHT })
  214.             {
  215.                 RollingLaneStructureRecord current = newRoot;
  216.                 RelativeLane relativeLane = RelativeLane.CURRENT;
  217.                 Set<Lane> adjacentLanes =
  218.                         current.getLane().accessibleAdjacentLanesPhysical(latDirection, gtuType, current.getDirection());
  219.                 while (!adjacentLanes.isEmpty())
  220.                 {
  221.                     Throw.when(adjacentLanes.size() > 1, RuntimeException.class,
  222.                             "Multiple adjacent lanes encountered during construction of lane map.");
  223.                     relativeLane = latDirection.isLeft() ? relativeLane.getLeft() : relativeLane.getRight();
  224.                     Lane adjacentLane = adjacentLanes.iterator().next();
  225.                     RollingLaneStructureRecord adjacentRecord =
  226.                             constructRecord(adjacentLane, direction, current, RecordLink.CROSS, relativeLane);
  227.                     this.crossSectionRecords.put(relativeLane, adjacentRecord);
  228.                     if (latDirection.isLeft())
  229.                     {
  230.                         if (adjacentLane
  231.                                 .accessibleAdjacentLanesPhysical(LateralDirectionality.RIGHT, gtuType, current.getDirection())
  232.                                 .contains(current.getLane()))
  233.                         {
  234.                             adjacentRecord.setRight(current, gtuType);
  235.                         }
  236.                         current.setLeft(adjacentRecord, gtuType);
  237.                     }
  238.                     else
  239.                     {
  240.                         if (adjacentLane
  241.                                 .accessibleAdjacentLanesPhysical(LateralDirectionality.LEFT, gtuType, current.getDirection())
  242.                                 .contains(current.getLane()))
  243.                         {
  244.                             adjacentRecord.setLeft(current, gtuType);
  245.                         }
  246.                         current.setRight(adjacentRecord, gtuType);
  247.                     }
  248.                     current = adjacentRecord;
  249.                     adjacentLanes =
  250.                             current.getLane().accessibleAdjacentLanesPhysical(latDirection, gtuType, current.getDirection());
  251.                 }
  252.             }
  253.             this.upstreamEdge.addAll(this.crossSectionRecords.values());
  254.             this.downstreamEdge.addAll(this.crossSectionRecords.values());
  255.             this.firstRecords.putAll(this.crossSectionRecords);

  256.             // set the start distances so the upstream expand can work
  257.             newRoot.updateStartDistance(fracPos, this);

  258.             // expand upstream edge
  259.             expandUpstreamEdge(gtuType, fracPos);

  260.             // derive first records
  261.             deriveFirstRecords();
  262.         }
  263.         else
  264.         {

  265.             // update LaneStructure
  266.             RollingLaneStructureRecord newRoot = this.root.get();
  267.             if (!lane.equals(newRoot.getLane()))
  268.             {

  269.                 // find the root, and possible lateral shift if changed lane
  270.                 newRoot = null;
  271.                 RelativeLane lateralMove = null;
  272.                 double closest = Double.POSITIVE_INFINITY;
  273.                 for (RelativeLane relativeLane : this.relativeLaneMap.keySet())
  274.                 {
  275.                     for (RollingLaneStructureRecord record : this.relativeLaneMap.get(relativeLane))
  276.                     {
  277.                         if (record.getLane().equals(lane) && record.getStartDistance().si < closest
  278.                                 && record.getStartDistance().si + record.getLength().si > 0.0)
  279.                         {
  280.                             newRoot = record;
  281.                             lateralMove = relativeLane;
  282.                             // multiple records may be present for the current lane due to a loop
  283.                             closest = record.getStartDistance().si;
  284.                         }
  285.                     }
  286.                 }
  287.                 // newRoot.getPrev().contains(newRoot.getStartDistanceSource())
  288.                 this.root.set(newRoot);

  289.                 // update start distance sources
  290.                 updateStartDistanceSources();

  291.                 // shift if changed lane
  292.                 if (!lateralMove.isCurrent())
  293.                 {
  294.                     RelativeLane delta =
  295.                             new RelativeLane(lateralMove.getLateralDirectionality().flip(), lateralMove.getNumLanes());

  296.                     TreeMap<RelativeLane, Set<RollingLaneStructureRecord>> newRelativeLaneMap = new TreeMap<>();
  297.                     for (RelativeLane relativeLane : this.relativeLaneMap.keySet())
  298.                     {
  299.                         RelativeLane newRelativeLane = relativeLane.add(delta);
  300.                         newRelativeLaneMap.put(newRelativeLane, this.relativeLaneMap.get(relativeLane));
  301.                     }
  302.                     this.relativeLaneMap = newRelativeLaneMap;

  303.                     Map<LaneStructureRecord, RelativeLane> newRelativeLanes = new HashMap<>();
  304.                     for (LaneStructureRecord record : this.relativeLanes.keySet())
  305.                     {
  306.                         newRelativeLanes.put(record, this.relativeLanes.get(record).add(delta));
  307.                     }
  308.                     this.relativeLanes = newRelativeLanes;
  309.                 }

  310.                 this.crossSectionRecords.clear();
  311.                 this.crossSectionRecords.put(RelativeLane.CURRENT, newRoot);
  312.                 for (LateralDirectionality latDirection : new LateralDirectionality[] { LateralDirectionality.LEFT,
  313.                         LateralDirectionality.RIGHT })
  314.                 {
  315.                     RollingLaneStructureRecord record = newRoot;
  316.                     RollingLaneStructureRecord next = newRoot;
  317.                     RelativeLane delta = new RelativeLane(latDirection, 1);
  318.                     RelativeLane relLane = RelativeLane.CURRENT;
  319.                     while (next != null)
  320.                     {
  321.                         next = latDirection.isLeft() ? record.getLeft() : record.getRight();
  322.                         if (next != null)
  323.                         {
  324.                             next.changeStartDistanceSource(record, RecordLink.CROSS);
  325.                             relLane = relLane.add(delta);
  326.                             this.crossSectionRecords.put(relLane, next);
  327.                             record = next;
  328.                         }
  329.                     }
  330.                 }

  331.             }
  332.             newRoot.updateStartDistance(fracPos, this);

  333.             // update upstream edges
  334.             retreatUpstreamEdge();

  335.             // derive first records
  336.             deriveFirstRecords();

  337.         }

  338.         // update downstream edges
  339.         expandDownstreamEdge(gtuType, fracPos, route);

  340.     }

  341.     /**
  342.      * Derives the first downstream records so the extended cross-section can be returned.
  343.      */
  344.     private void deriveFirstRecords()
  345.     {
  346.         this.firstRecords.clear();
  347.         this.firstRecords.putAll(this.crossSectionRecords);
  348.         for (RelativeLane lane : this.relativeLaneMap.keySet())
  349.         {
  350.             getFirstRecord(lane); // store non-null values internally
  351.         }
  352.     }

  353.     /**
  354.      * Upstream algorithm from the new source, where records along the way are assigned a new start distance source. These
  355.      * records were downstream in the previous time step, but are now upstream. Hence, their start distance source should now
  356.      * become their downstream record. This algorithm acts like an upstream tree, where each branch is stopped if there are no
  357.      * upstream records, or the upstream records already have their downstream record as source (i.e. the record was already
  358.      * upstream in the previous time step).
  359.      */
  360.     private void updateStartDistanceSources()
  361.     {

  362.         // initial cross section
  363.         Set<RollingLaneStructureRecord> set = new HashSet<>();
  364.         RollingLaneStructureRecord rootRecord = this.root.get();
  365.         set.add(rootRecord);
  366.         rootRecord.changeStartDistanceSource(null, RecordLink.CROSS);
  367.         RollingLaneStructureRecord prev = rootRecord;
  368.         RollingLaneStructureRecord next = prev.getLeft();
  369.         while (next != null)
  370.         {
  371.             set.add(next);
  372.             next.changeStartDistanceSource(prev, RecordLink.CROSS);
  373.             prev = next;
  374.             next = next.getLeft();
  375.         }
  376.         prev = rootRecord;
  377.         next = prev.getRight();
  378.         while (next != null)
  379.         {
  380.             set.add(next);
  381.             next.changeStartDistanceSource(prev, RecordLink.CROSS);
  382.             prev = next;
  383.             next = next.getRight();
  384.         }
  385.         // tree algorithm (branches flattened to a single set)
  386.         while (!set.isEmpty())
  387.         {
  388.             // lateral
  389.             Set<RollingLaneStructureRecord> newSet = new HashSet<>();
  390.             for (RollingLaneStructureRecord record : set)
  391.             {
  392.                 for (LateralDirectionality latDirection : new LateralDirectionality[] { LateralDirectionality.LEFT,
  393.                         LateralDirectionality.RIGHT })
  394.                 {
  395.                     prev = record;
  396.                     next = latDirection.isLeft() ? record.getLeft() : record.getRight();
  397.                     while (next != null && !set.contains(next))
  398.                     {
  399.                         next.changeStartDistanceSource(prev, RecordLink.LATERAL_END);
  400.                         removeDownstream(next, latDirection.flip()); // split not taken can be thrown away
  401.                         newSet.add(next);
  402.                         prev = next;
  403.                         next = latDirection.isLeft() ? next.getLeft() : next.getRight();
  404.                     }
  405.                 }
  406.             }
  407.             set.addAll(newSet);

  408.             // longitudinal
  409.             newSet.clear();
  410.             for (RollingLaneStructureRecord record : set)
  411.             {
  412.                 for (RollingLaneStructureRecord prevRecord : record.getPrev())
  413.                 {
  414.                     Iterator<RollingLaneStructureRecord> it = prevRecord.getNext().iterator();
  415.                     while (it.hasNext())
  416.                     {
  417.                         RollingLaneStructureRecord otherDown = it.next();
  418.                         if (!otherDown.getLane().getParentLink().equals(record.getLane().getParentLink()))
  419.                         {
  420.                             // split not taken can be thrown away
  421.                             otherDown.changeStartDistanceSource(null, null);
  422.                             // this can throw away records that are laterally connected as they later merge... ??
  423.                             removeDownstream(otherDown, LateralDirectionality.NONE);
  424.                             removeRecord(otherDown);
  425.                             it.remove();
  426.                         }
  427.                     }
  428.                     LaneStructureRecord source = prevRecord.getStartDistanceSource();
  429.                     if (source == null || (source != null && !source.equals(record)))
  430.                     {
  431.                         prevRecord.changeStartDistanceSource(record, RecordLink.UP);
  432.                         newSet.add(prevRecord);
  433.                     }
  434.                 }
  435.             }

  436.             // next loop
  437.             set = newSet;
  438.         }
  439.     }

  440.     /**
  441.      * Removes all records downstream of the given record from underlying data structures.
  442.      * @param record LaneStructureRecord; record, downstream of which to remove all records
  443.      * @param lat LateralDirectionality; records with an adjacent record at this side are not deleted
  444.      */
  445.     private void removeDownstream(final RollingLaneStructureRecord record, final LateralDirectionality lat)
  446.     {
  447.         for (RollingLaneStructureRecord next : record.getNext())
  448.         {
  449.             RollingLaneStructureRecord adj = lat.isLeft() ? next.getLeft() : lat.isRight() ? next.getRight() : null;
  450.             if (adj == null)
  451.             {
  452.                 next.changeStartDistanceSource(null, null);
  453.                 removeDownstream(next, lat);
  454.                 removeRecord(next);
  455.             }
  456.         }
  457.         record.clearNextList();
  458.     }

  459.     /**
  460.      * Removes the record from underlying data structures.
  461.      * @param record LaneStructureRecord; record to remove
  462.      */
  463.     private void removeRecord(final RollingLaneStructureRecord record)
  464.     {
  465.         RelativeLane lane = this.relativeLanes.get(record);
  466.         if (lane != null)
  467.         {
  468.             this.relativeLanes.remove(record);
  469.         }
  470.         record.changeStartDistanceSource(null, null);

  471.     }

  472.     /**
  473.      * On a new build, this method is used to create the upstream map.
  474.      * @param gtuType GTUType; GTU type
  475.      * @param fractionalPosition double; fractional position on reference link
  476.      * @throws GTUException on exception
  477.      */
  478.     private void expandUpstreamEdge(final GTUType gtuType, final double fractionalPosition) throws GTUException
  479.     {
  480.         this.ignoreSet.clear();
  481.         for (LaneStructureRecord record : this.upstreamEdge)
  482.         {
  483.             this.ignoreSet.add(record.getLane());
  484.         }
  485.         Set<RollingLaneStructureRecord> nextSet = new LinkedHashSet<>();
  486.         boolean expand = true;
  487.         while (expand)
  488.         {
  489.             expand = false;

  490.             // longitudinal
  491.             Iterator<RollingLaneStructureRecord> iterator = this.upstreamEdge.iterator();
  492.             Set<RollingLaneStructureRecord> modifiedEdge = new LinkedHashSet<>(this.upstreamEdge);
  493.             while (iterator.hasNext())
  494.             {
  495.                 RollingLaneStructureRecord prev = iterator.next();
  496.                 Map<Lane, GTUDirectionality> nexts = prev.getLane().upstreamLanes(prev.getDirection(), gtuType);
  497.                 if (prev.getStartDistance().si < this.up.si)
  498.                 {
  499.                     // upstream search ends on this lane
  500.                     prev.setCutOffStart(this.up.minus(prev.getStartDistance()));
  501.                     for (Lane prevLane : nexts.keySet())
  502.                     {
  503.                         this.ignoreSet.add(prevLane); // exclude in lateral search
  504.                     }
  505.                 }
  506.                 else
  507.                 {
  508.                     // upstream search goes further upstream
  509.                     prev.clearCutOffStart();
  510.                     iterator.remove();
  511.                     if (!nexts.isEmpty())
  512.                     {
  513.                         for (Lane prevLane : nexts.keySet())
  514.                         {
  515.                             RelativeLane relativeLane = this.relativeLanes.get(prev);
  516.                             RollingLaneStructureRecord next =
  517.                                     constructRecord(prevLane, nexts.get(prevLane), prev, RecordLink.UP, relativeLane);
  518.                             this.ignoreSet.add(prevLane);
  519.                             next.updateStartDistance(fractionalPosition, this);
  520.                             connectLaterally(next, gtuType, modifiedEdge);
  521.                             next.addNext(prev);
  522.                             prev.addPrev(next);
  523.                             nextSet.add(next);
  524.                             modifiedEdge.add(next);
  525.                         }
  526.                     }
  527.                 }
  528.             }
  529.             this.upstreamEdge.addAll(nextSet);
  530.             expand |= !nextSet.isEmpty();
  531.             nextSet.clear();

  532.             // lateral
  533.             Set<RollingLaneStructureRecord> lateralSet =
  534.                     expandLateral(this.upstreamEdge, RecordLink.LATERAL_END, gtuType, fractionalPosition);
  535.             nextSet.addAll(lateralSet);

  536.             // next iteration
  537.             this.upstreamEdge.addAll(nextSet);
  538.             expand |= !nextSet.isEmpty();
  539.             nextSet.clear();
  540.         }
  541.     }

  542.     /**
  543.      * Helper method for upstream and downstream expansion. This method returns all lanes that can be laterally found from the
  544.      * input set.
  545.      * @param edge Set&lt;LaneStructureRecord&gt;; input set
  546.      * @param recordLink RecordLink; link to add between lateral records, depends on upstream or downstream search
  547.      * @param gtuType GTUType; GTU type
  548.      * @param fractionalPosition double; fractional position on reference link
  549.      * @return Set&lt;LaneStructureRecord&gt;; output set with all laterally found lanes
  550.      */
  551.     private Set<RollingLaneStructureRecord> expandLateral(final Set<RollingLaneStructureRecord> edge,
  552.             final RecordLink recordLink, final GTUType gtuType, final double fractionalPosition)
  553.     {
  554.         Set<RollingLaneStructureRecord> nextSet = new LinkedHashSet<>();
  555.         Set<Lane> laneSet = new HashSet<>(); // set to check that an adjacent lane is not another lane already in the set
  556.         for (LaneStructureRecord record : edge)
  557.         {
  558.             laneSet.add(record.getLane());
  559.         }
  560.         Iterator<RollingLaneStructureRecord> iterator = edge.iterator();
  561.         while (iterator.hasNext())
  562.         {
  563.             RollingLaneStructureRecord record = iterator.next();
  564.             for (LateralDirectionality latDirection : new LateralDirectionality[] { LateralDirectionality.LEFT,
  565.                     LateralDirectionality.RIGHT })
  566.             {
  567.                 RelativeLane relativeLane = this.relativeLanes.get(record);
  568.                 RollingLaneStructureRecord prev = record;
  569.                 Set<Lane> adjacentLanes =
  570.                         prev.getLane().accessibleAdjacentLanesPhysical(latDirection, gtuType, prev.getDirection());
  571.                 while (!adjacentLanes.isEmpty())
  572.                 {
  573.                     Throw.when(adjacentLanes.size() > 1, RuntimeException.class,
  574.                             "Multiple adjacent lanes encountered during construction of lane map.");
  575.                     relativeLane = latDirection.isLeft() ? relativeLane.getLeft() : relativeLane.getRight();
  576.                     Lane nextLane = adjacentLanes.iterator().next();
  577.                     if (!laneSet.contains(nextLane) && !this.ignoreSet.contains(nextLane))
  578.                     {
  579.                         RollingLaneStructureRecord next =
  580.                                 constructRecord(nextLane, record.getDirection(), prev, recordLink, relativeLane);
  581.                         this.ignoreSet.add(nextLane);
  582.                         next.updateStartDistance(fractionalPosition, this);
  583.                         nextSet.add(next);
  584.                         laneSet.add(nextLane);
  585.                         if (latDirection.isLeft())
  586.                         {
  587.                             prev.setLeft(next, gtuType);
  588.                             if (nextLane
  589.                                     .accessibleAdjacentLanesPhysical(LateralDirectionality.RIGHT, gtuType, prev.getDirection())
  590.                                     .contains(prev.getLane()))
  591.                             {
  592.                                 next.setRight(prev, gtuType);
  593.                             }
  594.                             for (RollingLaneStructureRecord edgeRecord : edge)
  595.                             {
  596.                                 if (!edgeRecord.equals(prev)
  597.                                         && edgeRecord.getLane().getParentLink().equals(next.getLane().getParentLink()))
  598.                                 {
  599.                                     for (Lane adjLane : edgeRecord.getLane().accessibleAdjacentLanesPhysical(
  600.                                             LateralDirectionality.RIGHT, gtuType, edgeRecord.getDirection()))
  601.                                     {
  602.                                         if (adjLane.equals(next.getLane()))
  603.                                         {
  604.                                             edgeRecord.setRight(next, gtuType);
  605.                                             next.setLeft(edgeRecord, gtuType);
  606.                                         }
  607.                                     }
  608.                                 }
  609.                             }
  610.                         }
  611.                         else
  612.                         {
  613.                             prev.setRight(next, gtuType);
  614.                             if (nextLane
  615.                                     .accessibleAdjacentLanesPhysical(LateralDirectionality.LEFT, gtuType, prev.getDirection())
  616.                                     .contains(prev.getLane()))
  617.                             {
  618.                                 next.setLeft(prev, gtuType);
  619.                             }
  620.                             for (RollingLaneStructureRecord edgeRecord : edge)
  621.                             {
  622.                                 if (!edgeRecord.equals(prev)
  623.                                         && edgeRecord.getLane().getParentLink().equals(next.getLane().getParentLink()))
  624.                                 {
  625.                                     for (Lane adjLane : edgeRecord.getLane().accessibleAdjacentLanesPhysical(
  626.                                             LateralDirectionality.LEFT, gtuType, edgeRecord.getDirection()))
  627.                                     {
  628.                                         if (adjLane.equals(next.getLane()))
  629.                                         {
  630.                                             edgeRecord.setLeft(next, gtuType);
  631.                                             next.setRight(edgeRecord, gtuType);
  632.                                         }
  633.                                     }
  634.                                 }
  635.                             }
  636.                         }
  637.                         prev = next;
  638.                         adjacentLanes =
  639.                                 prev.getLane().accessibleAdjacentLanesPhysical(latDirection, gtuType, prev.getDirection());
  640.                     }
  641.                     else
  642.                     {
  643.                         break;
  644.                     }
  645.                 }
  646.             }
  647.         }
  648.         return nextSet;
  649.     }

  650.     /**
  651.      * This method makes sure that not all history is maintained forever, and the upstream edge moves with the GTU.
  652.      * @throws GTUException on exception
  653.      */
  654.     private void retreatUpstreamEdge() throws GTUException
  655.     {
  656.         boolean moved = true;
  657.         Set<RollingLaneStructureRecord> nexts = new LinkedHashSet<>();
  658.         while (moved)
  659.         {
  660.             moved = false;
  661.             nexts.clear();
  662.             Iterator<RollingLaneStructureRecord> iterator = this.upstreamEdge.iterator();
  663.             while (iterator.hasNext())
  664.             {
  665.                 RollingLaneStructureRecord prev = iterator.next();
  666.                 // nexts may contain 'prev' as two lanes are removed from the upstream edge that merge in to 1 downstream lane
  667.                 if (!nexts.contains(prev) && prev.getStartDistance().si + prev.getLane().getLength().si < this.up.si)
  668.                 {
  669.                     for (RollingLaneStructureRecord next : prev.getNext())
  670.                     {
  671.                         next.clearPrevList();
  672.                         next.setCutOffStart(this.up.minus(next.getStartDistance()));
  673.                         moved = true;
  674.                         nexts.add(next);
  675.                         RollingLaneStructureRecord lat = next.getLeft();
  676.                         while (lat != null && lat.getPrev().isEmpty())
  677.                         {
  678.                             nexts.add(lat);
  679.                             lat = lat.getLeft();
  680.                         }
  681.                         lat = next.getRight();
  682.                         while (lat != null && lat.getPrev().isEmpty())
  683.                         {
  684.                             nexts.add(lat);
  685.                             lat = lat.getRight();
  686.                         }
  687.                     }
  688.                     prev.clearNextList();
  689.                     removeRecord(prev);
  690.                     iterator.remove();
  691.                 }
  692.                 else
  693.                 {
  694.                     Length cutOff = this.up.minus(prev.getStartDistance());
  695.                     if (cutOff.si > 0)
  696.                     {
  697.                         prev.setCutOffStart(cutOff);
  698.                     }
  699.                 }
  700.             }
  701.             this.upstreamEdge.addAll(nexts);

  702.             // check adjacent lanes
  703.             for (RollingLaneStructureRecord record : nexts)
  704.             {
  705.                 RollingLaneStructureRecord prev = record;
  706.                 for (LateralDirectionality latDirection : new LateralDirectionality[] { LateralDirectionality.LEFT,
  707.                         LateralDirectionality.RIGHT })
  708.                 {
  709.                     while (prev != null)
  710.                     {
  711.                         RollingLaneStructureRecord next = latDirection.isLeft() ? prev.getLeft() : prev.getRight();
  712.                         if (next != null && !this.upstreamEdge.contains(next))
  713.                         {
  714.                             moved |= findUpstreamEdge(next);
  715.                         }
  716.                         prev = next;
  717.                     }
  718.                 }
  719.             }
  720.         }
  721.     }

  722.     /**
  723.      * Recursive method to find downstream record(s) on the upstream edge, as the edge was moved downstream and a laterally
  724.      * connected lane was not yet in the upstream edge. All edge records are added to the edge set.
  725.      * @param record LaneStructureRecord; newly found adjacent record after moving the upstream edge downstream
  726.      * @return boolean; whether a record was added to the edge, note that no record is added of the record is fully downstream
  727.      *         of the upstream view distance
  728.      * @throws GTUException on exception
  729.      */
  730.     private boolean findUpstreamEdge(final RollingLaneStructureRecord record) throws GTUException
  731.     {
  732.         Length cutOff = this.up.minus(record.getStartDistance());
  733.         boolean moved = false;
  734.         if (cutOff.gt0())
  735.         {
  736.             if (cutOff.lt(record.getLane().getLength()))
  737.             {
  738.                 record.clearPrevList();
  739.                 record.setCutOffStart(cutOff);
  740.                 this.upstreamEdge.add(record);
  741.                 moved = true;
  742.             }
  743.             else
  744.             {
  745.                 if (this.relativeLanes.containsKey(record))
  746.                 {
  747.                     // could have been removed from upstream already
  748.                     removeRecord(record);
  749.                 }
  750.                 for (RollingLaneStructureRecord next : record.getNext())
  751.                 {
  752.                     moved |= findUpstreamEdge(next);
  753.                 }
  754.             }
  755.         }
  756.         return moved;
  757.     }

  758.     /**
  759.      * Main downstream search for the map. Can be used at initial build and to update.
  760.      * @param gtuType GTUType; GTU type
  761.      * @param fractionalPosition double; fractional position on reference link
  762.      * @param route Route; route of the GTU
  763.      * @throws GTUException on exception
  764.      */
  765.     private void expandDownstreamEdge(final GTUType gtuType, final double fractionalPosition, final Route route)
  766.             throws GTUException
  767.     {
  768.         this.ignoreSet.clear();
  769.         for (LaneStructureRecord record : this.downstreamEdge)
  770.         {
  771.             this.ignoreSet.add(record.getLane());
  772.         }
  773.         Set<RollingLaneStructureRecord> nextSet = new LinkedHashSet<>();
  774.         Set<RollingLaneStructureRecord> splitSet = new LinkedHashSet<>();
  775.         boolean expand = true;
  776.         while (expand)
  777.         {
  778.             expand = false;

  779.             // longitudinal
  780.             Iterator<RollingLaneStructureRecord> iterator = this.downstreamEdge.iterator();
  781.             Set<RollingLaneStructureRecord> modifiedEdge = new LinkedHashSet<>(this.downstreamEdge);
  782.             while (iterator.hasNext())
  783.             {
  784.                 RollingLaneStructureRecord record = iterator.next();
  785.                 Map<Lane, GTUDirectionality> nexts = record.getLane().downstreamLanes(record.getDirection(), gtuType);
  786.                 if (record.getStartDistance().si + record.getLane().getLength().si > this.down.si)
  787.                 {
  788.                     // downstream search ends on this lane
  789.                     record.setCutOffEnd(this.down.minus(record.getStartDistance()));
  790.                     for (Lane nextLane : nexts.keySet())
  791.                     {
  792.                         this.ignoreSet.add(nextLane); // exclude in lateral search
  793.                     }
  794.                 }
  795.                 else
  796.                 {
  797.                     // downstream search goes further downstream

  798.                     // in case there are multiple lanes on the same link after a lane split, we need to choose one
  799.                     LaneDirection nextLaneDirection =
  800.                             new LaneDirection(record.getLane(), record.getDirection()).getNextLaneDirection(this.containingGtu);

  801.                     record.clearCutOffEnd();
  802.                     iterator.remove(); // can remove from edge, no algorithm needs it anymore in the downstream edge
  803.                     for (Lane nextLane : nexts.keySet())
  804.                     {
  805.                         if (nextLaneDirection != null
  806.                                 && nextLane.getParentLink().equals(nextLaneDirection.getLane().getParentLink())
  807.                                 && !nextLane.equals(nextLaneDirection.getLane()))
  808.                         {
  809.                             // skip this lane as its a not chosen lane on the next link after a lane split
  810.                             continue;
  811.                         }
  812.                         RelativeLane relativeLane = this.relativeLanes.get(record);
  813.                         GTUDirectionality dir = nexts.get(nextLane);
  814.                         RollingLaneStructureRecord next = constructRecord(nextLane, dir, record, RecordLink.DOWN, relativeLane);
  815.                         this.ignoreSet.add(nextLane);
  816.                         next.updateStartDistance(fractionalPosition, this);
  817.                         record.addNext(next);
  818.                         next.addPrev(record);
  819.                         connectLaterally(next, gtuType, modifiedEdge);
  820.                         // check route
  821.                         int from = route == null ? 0 : route.indexOf(next.getFromNode());
  822.                         int to = route == null ? 1 : route.indexOf(next.getToNode());
  823.                         if (to < 0 || to - from != 1)
  824.                         {
  825.                             // not on our route, add some distance and stop
  826.                             splitSet.add(next);
  827.                         }
  828.                         else
  829.                         {
  830.                             // regular edge
  831.                             nextSet.add(next);
  832.                         }
  833.                         modifiedEdge.add(next);
  834.                         // expand upstream over any possible other lane that merges in to this
  835.                         Set<RollingLaneStructureRecord> set = new HashSet<>();
  836.                         set.add(next);
  837.                         expandUpstreamMerge(set, gtuType, fractionalPosition, route);
  838.                     }
  839.                 }
  840.             }
  841.             this.downstreamEdge.addAll(nextSet);
  842.             expand |= !nextSet.isEmpty();
  843.             nextSet.clear();

  844.             // split
  845.             expandDownstreamSplit(splitSet, gtuType, fractionalPosition, route);
  846.             splitSet.clear();

  847.             // lateral
  848.             Set<RollingLaneStructureRecord> lateralSet =
  849.                     expandLateral(this.downstreamEdge, RecordLink.LATERAL_END, gtuType, fractionalPosition);
  850.             nextSet.addAll(lateralSet);
  851.             expandUpstreamMerge(lateralSet, gtuType, fractionalPosition, route);

  852.             // next iteration
  853.             this.downstreamEdge.addAll(nextSet);
  854.             expand |= !nextSet.isEmpty();
  855.             nextSet.clear();
  856.         }
  857.     }

  858.     /**
  859.      * Expand the map to include a limited section downstream of a split, regarding links not on the route.
  860.      * @param set Set&lt;LaneStructureRecord&gt;; set of lanes that have been laterally found
  861.      * @param gtuType GTUType; GTU type
  862.      * @param fractionalPosition double; fractional position on reference link
  863.      * @param route Route; route
  864.      * @throws GTUException on exception
  865.      */
  866.     private void expandDownstreamSplit(final Set<RollingLaneStructureRecord> set, final GTUType gtuType,
  867.             final double fractionalPosition, final Route route) throws GTUException
  868.     {
  869.         Map<RollingLaneStructureRecord, Length> prevs = new LinkedHashMap<>();
  870.         Map<RollingLaneStructureRecord, Length> nexts = new LinkedHashMap<>();
  871.         for (RollingLaneStructureRecord record : set)
  872.         {
  873.             prevs.put(record, record.getStartDistance().plus(this.downSplit));
  874.         }
  875.         while (!prevs.isEmpty())
  876.         {
  877.             for (RollingLaneStructureRecord prev : prevs.keySet())
  878.             {
  879.                 Map<Lane, GTUDirectionality> nextLanes = prev.getLane().downstreamLanes(prev.getDirection(), gtuType);
  880.                 RelativeLane relativeLane = this.relativeLanes.get(prev);
  881.                 for (Lane nextLane : nextLanes.keySet())
  882.                 {
  883.                     GTUDirectionality dir = nextLanes.get(nextLane);
  884.                     Node fromNode =
  885.                             dir.isPlus() ? nextLane.getParentLink().getStartNode() : nextLane.getParentLink().getEndNode();
  886.                     Node toNode =
  887.                             dir.isPlus() ? nextLane.getParentLink().getEndNode() : nextLane.getParentLink().getStartNode();
  888.                     int from = route.indexOf(fromNode);
  889.                     int to = route.indexOf(toNode);
  890.                     if (from == -1 || to == -1 || to - from != 1)
  891.                     {
  892.                         RollingLaneStructureRecord next = constructRecord(nextLane, dir, prev, RecordLink.DOWN, relativeLane);
  893.                         next.updateStartDistance(fractionalPosition, this);
  894.                         next.addPrev(prev);
  895.                         prev.addNext(next);
  896.                         connectLaterally(next, gtuType, nexts.keySet());
  897.                         Length downHere = prevs.get(prev);
  898.                         if (next.getStartDistance().si > downHere.si)
  899.                         {
  900.                             next.setCutOffEnd(downHere.minus(next.getStartDistance()));
  901.                         }
  902.                         else
  903.                         {
  904.                             nexts.put(next, downHere);
  905.                         }
  906.                     }
  907.                 }
  908.             }
  909.             prevs = nexts;
  910.             nexts = new LinkedHashMap<>();
  911.         }
  912.     }

  913.     /**
  914.      * Expand the map to include a limited section upstream of a merge that is downstream, regarding links not on the route.
  915.      * @param set Set&lt;LaneStructureRecord&gt;; set of lanes that have been laterally found
  916.      * @param gtuType GTUType; GTU type
  917.      * @param fractionalPosition double; fractional position on reference link
  918.      * @param route Route; route of the GTU
  919.      * @throws GTUException on exception
  920.      */
  921.     private void expandUpstreamMerge(final Set<RollingLaneStructureRecord> set, final GTUType gtuType,
  922.             final double fractionalPosition, final Route route) throws GTUException
  923.     {
  924.         Map<RollingLaneStructureRecord, Length> prevs = new LinkedHashMap<>();
  925.         Map<RollingLaneStructureRecord, Length> nexts = new LinkedHashMap<>();
  926.         for (RollingLaneStructureRecord record : set)
  927.         {
  928.             prevs.put(record, record.getStartDistance().plus(this.upMerge)); // upMerge is negative
  929.         }
  930.         while (!prevs.isEmpty())
  931.         {
  932.             for (RollingLaneStructureRecord prev : prevs.keySet())
  933.             {
  934.                 Map<Lane, GTUDirectionality> nextLanes = prev.getLane().upstreamLanes(prev.getDirection(), gtuType);
  935.                 boolean anyAdded = false;
  936.                 for (Lane nextLane : nextLanes.keySet())
  937.                 {
  938.                     GTUDirectionality dir = nextLanes.get(nextLane);
  939.                     Node fromNode =
  940.                             dir.isPlus() ? nextLane.getParentLink().getStartNode() : nextLane.getParentLink().getEndNode();
  941.                     Node toNode =
  942.                             dir.isPlus() ? nextLane.getParentLink().getEndNode() : nextLane.getParentLink().getStartNode();
  943.                     int from = route == null ? 0 : route.indexOf(fromNode);
  944.                     int to = route == null ? 1 : route.indexOf(toNode);
  945.                     // TODO we now assume everything is on the route, but merges could be ok without route
  946.                     // so, without a route we should be able to recognize which upstream 'nextLane' is on the other link
  947.                     if (from == -1 || to == -1 || to - from != 1)
  948.                     {
  949.                         anyAdded = true;
  950.                         RelativeLane relativeLane = this.relativeLanes.get(prev);
  951.                         RollingLaneStructureRecord next =
  952.                                 constructRecord(nextLane, nextLanes.get(nextLane), prev, RecordLink.UP, relativeLane);
  953.                         next.updateStartDistance(fractionalPosition, this);
  954.                         next.addNext(prev);
  955.                         prev.addPrev(next);
  956.                         connectLaterally(next, gtuType, nexts.keySet());
  957.                         Length upHere = prevs.get(prev);
  958.                         if (next.getStartDistance().si < upHere.si)
  959.                         {
  960.                             next.setCutOffStart(upHere.minus(next.getStartDistance()));
  961.                             this.upstreamEdge.add(next);
  962.                         }
  963.                         else
  964.                         {
  965.                             nexts.put(next, upHere);
  966.                         }
  967.                     }
  968.                 }
  969.                 if (!anyAdded && !set.contains(prev))
  970.                 {
  971.                     this.upstreamEdge.add(prev);
  972.                 }
  973.             }
  974.             prevs = nexts;
  975.             nexts = new LinkedHashMap<>();
  976.         }
  977.     }

  978.     /**
  979.      * Helper method of various other methods that laterally couples lanes that have been longitudinally found.
  980.      * @param record LaneStructureRecord; longitudinally found lane
  981.      * @param gtuType GTUType; GTU type
  982.      * @param nextSet Set&lt;RollingLaneStructureRecord&gt;; set of records on current build edge
  983.      */
  984.     private void connectLaterally(final RollingLaneStructureRecord record, final GTUType gtuType,
  985.             final Set<RollingLaneStructureRecord> nextSet)
  986.     {
  987.         for (RollingLaneStructureRecord other : nextSet)
  988.         {
  989.             for (LateralDirectionality latDirection : new LateralDirectionality[] { LateralDirectionality.LEFT,
  990.                     LateralDirectionality.RIGHT })
  991.             {
  992.                 if ((latDirection.isLeft() ? other.getLeft() : other.getRight()) == null)
  993.                 {
  994.                     for (Lane otherLane : other.getLane().accessibleAdjacentLanesPhysical(latDirection, gtuType,
  995.                             other.getDirection()))
  996.                     {
  997.                         if (otherLane.equals(record.getLane()))
  998.                         {
  999.                             if (latDirection.isLeft())
  1000.                             {
  1001.                                 other.setLeft(record, gtuType);
  1002.                                 record.setRight(other, gtuType);
  1003.                             }
  1004.                             else
  1005.                             {
  1006.                                 other.setRight(record, gtuType);
  1007.                                 record.setLeft(other, gtuType);
  1008.                             }
  1009.                         }
  1010.                     }
  1011.                 }
  1012.             }
  1013.         }
  1014.     }

  1015.     /**
  1016.      * Creates a lane structure record and adds it to relevant maps.
  1017.      * @param lane lane
  1018.      * @param direction direction
  1019.      * @param startDistanceSource source of the start distance
  1020.      * @param recordLink record link
  1021.      * @param relativeLane relative lane
  1022.      * @return created lane structure record
  1023.      */
  1024.     private RollingLaneStructureRecord constructRecord(final Lane lane, final GTUDirectionality direction,
  1025.             final RollingLaneStructureRecord startDistanceSource, final RecordLink recordLink, final RelativeLane relativeLane)
  1026.     {
  1027.         RollingLaneStructureRecord record = new RollingLaneStructureRecord(lane, direction, startDistanceSource, recordLink);
  1028.         if (!this.relativeLaneMap.containsKey(relativeLane))
  1029.         {
  1030.             this.relativeLaneMap.put(relativeLane, new LinkedHashSet<>());
  1031.         }
  1032.         this.relativeLaneMap.get(relativeLane).add(record);
  1033.         this.relativeLanes.put(record, relativeLane);
  1034.         return record;
  1035.     }

  1036.     /** {@inheritDoc} */
  1037.     @Override
  1038.     public final LaneStructureRecord getRootRecord()
  1039.     {
  1040.         return this.root.get();
  1041.     }

  1042.     /**
  1043.      * @param time Time; time to obtain the root at
  1044.      * @return rootRecord
  1045.      */
  1046.     public final LaneStructureRecord getRootRecord(final Time time)
  1047.     {
  1048.         return this.root.get(time);
  1049.     }

  1050.     /** {@inheritDoc} */
  1051.     @Override
  1052.     public final SortedSet<RelativeLane> getExtendedCrossSection()
  1053.     {
  1054.         return this.firstRecords.navigableKeySet();
  1055.     }

  1056.     /**
  1057.      * Returns the first record on the given lane. This is often a record in the current cross section, but it may be one
  1058.      * downstream for a lane that starts further downstream.
  1059.      * @param lane RelativeLane; lane
  1060.      * @return first record on the given lane, or {@code null} if no such record
  1061.      */
  1062.     public final RollingLaneStructureRecord getFirstRecord(final RelativeLane lane)
  1063.     {
  1064.         if (this.firstRecords.containsKey(lane))
  1065.         {
  1066.             return this.firstRecords.get(lane);
  1067.         }
  1068.         // not in current cross section, get first via downstream
  1069.         RelativeLane rel = RelativeLane.CURRENT;
  1070.         int dMin = Integer.MAX_VALUE;
  1071.         for (RelativeLane relLane : this.crossSectionRecords.keySet())
  1072.         {
  1073.             if (relLane.getLateralDirectionality().equals(lane.getLateralDirectionality()))
  1074.             {
  1075.                 int d = lane.getNumLanes() - relLane.getNumLanes();
  1076.                 if (d < dMin)
  1077.                 {
  1078.                     rel = relLane;
  1079.                     d = dMin;
  1080.                 }
  1081.             }
  1082.         }
  1083.         RollingLaneStructureRecord record = this.crossSectionRecords.get(rel);
  1084.         // move downstream until a lateral move is made to the right relative lane
  1085.         while (rel.getNumLanes() < lane.getNumLanes())
  1086.         {
  1087.             RollingLaneStructureRecord adj = lane.getLateralDirectionality().isLeft() ? record.getLeft() : record.getRight();
  1088.             if (adj != null)
  1089.             {
  1090.                 rel = lane.getLateralDirectionality().isLeft() ? rel.getLeft() : rel.getRight();
  1091.                 record = adj;
  1092.             }
  1093.             else if (!record.getNext().isEmpty())
  1094.             {
  1095.                 LaneDirection laneDir =
  1096.                         new LaneDirection(record.getLane(), record.getDirection()).getNextLaneDirection(this.containingGtu);
  1097.                 if (laneDir == null)
  1098.                 {
  1099.                     record = null;
  1100.                     break;
  1101.                 }
  1102.                 RollingLaneStructureRecord chosenNext = null;
  1103.                 for (RollingLaneStructureRecord next : record.getNext())
  1104.                 {
  1105.                     if (next.getLane().equals(laneDir.getLane()))
  1106.                     {
  1107.                         chosenNext = next;
  1108.                         break;
  1109.                     }
  1110.                 }
  1111.                 Throw.when(chosenNext == null, RuntimeException.class,
  1112.                         "Unexpected exception while deriving first record not on the cross-section.");
  1113.                 record = chosenNext;
  1114.             }
  1115.             else
  1116.             {
  1117.                 // reached a dead-end
  1118.                 record = null;
  1119.                 break;
  1120.             }
  1121.         }
  1122.         if (record != null)
  1123.         {
  1124.             // now move upstream until we are at x = 0
  1125.             while (record.getPrev().size() == 1 && record.getStartDistance().gt0())
  1126.             {
  1127.                 record = record.getPrev().get(0);
  1128.             }
  1129.             this.firstRecords.put(lane, record);
  1130.         }
  1131.         return record;
  1132.     }

  1133.     /**
  1134.      * Retrieve objects of a specific type. Returns objects over a maximum length of the look ahead distance downstream from the
  1135.      * relative position, or as far as the lane map goes.
  1136.      * @param clazz class of objects to find
  1137.      * @param gtu gtu
  1138.      * @param pos relative position to start search from
  1139.      * @param <T> type of objects to find
  1140.      * @return Sorted set of objects of requested type per lane
  1141.      * @throws GTUException if lane is not in current set
  1142.      */
  1143.     public final <T extends LaneBasedObject> Map<RelativeLane, SortedSet<Entry<T>>> getDownstreamObjects(final Class<T> clazz,
  1144.             final LaneBasedGTU gtu, final RelativePosition.TYPE pos) throws GTUException
  1145.     {
  1146.         Map<RelativeLane, SortedSet<Entry<T>>> out = new HashMap<>();
  1147.         for (RelativeLane relativeLane : this.relativeLaneMap.keySet())
  1148.         {
  1149.             out.put(relativeLane, getDownstreamObjects(relativeLane, clazz, gtu, pos));
  1150.         }
  1151.         return out;
  1152.     }

  1153.     /**
  1154.      * Retrieve objects on a lane of a specific type. Returns objects over a maximum length of the look ahead distance
  1155.      * downstream from the relative position, or as far as the lane map goes.
  1156.      * @param lane lane
  1157.      * @param clazz class of objects to find
  1158.      * @param gtu gtu
  1159.      * @param pos relative position to start search from
  1160.      * @param <T> type of objects to find
  1161.      * @return Sorted set of objects of requested type
  1162.      * @throws GTUException if lane is not in current set
  1163.      */
  1164.     @SuppressWarnings("unchecked")
  1165.     public final <T extends LaneBasedObject> SortedSet<Entry<T>> getDownstreamObjects(final RelativeLane lane,
  1166.             final Class<T> clazz, final LaneBasedGTU gtu, final RelativePosition.TYPE pos) throws GTUException
  1167.     {
  1168.         LaneStructureRecord record = getFirstRecord(lane);
  1169.         SortedSet<Entry<T>> set = new TreeSet<>();
  1170.         if (record != null)
  1171.         {
  1172.             double ds = gtu.getRelativePositions().get(pos).getDx().si - gtu.getReference().getDx().si;
  1173.             if (record.isDownstreamBranch())
  1174.             {
  1175.                 // the list is ordered, but only for DIR_PLUS, need to do our own ordering
  1176.                 Length minimumPosition;
  1177.                 Length maximumPosition;
  1178.                 if (record.getDirection().isPlus())
  1179.                 {
  1180.                     minimumPosition = Length.createSI(ds - record.getStartDistance().si);
  1181.                     maximumPosition = Length.createSI(record.getLane().getLength().si);
  1182.                 }
  1183.                 else
  1184.                 {
  1185.                     minimumPosition = Length.ZERO;
  1186.                     maximumPosition = Length.createSI(record.getLane().getLength().si + record.getStartDistance().si - ds);
  1187.                 }

  1188.                 for (LaneBasedObject object : record.getLane().getLaneBasedObjects(minimumPosition, maximumPosition))
  1189.                 {
  1190.                     if (clazz.isAssignableFrom(object.getClass())
  1191.                             && ((record.getDirection().isPlus() && object.getDirection().isForwardOrBoth())
  1192.                                     || (record.getDirection().isMinus() && object.getDirection().isBackwardOrBoth())))
  1193.                     {
  1194.                         // unchecked, but the above isAssignableFrom assures correctness
  1195.                         double distance = record.getDistanceToPosition(object.getLongitudinalPosition()).si - ds;
  1196.                         if (distance <= this.lookAhead.si)
  1197.                         {
  1198.                             set.add(new Entry<>(Length.createSI(distance), (T) object));
  1199.                         }
  1200.                     }
  1201.                 }
  1202.             }
  1203.             getDownstreamObjectsRecursive(set, record, clazz, ds);
  1204.         }
  1205.         return set;
  1206.     }

  1207.     /**
  1208.      * Retrieve objects on a lane of a specific type. Returns objects over a maximum length of the look ahead distance
  1209.      * downstream from the relative position, or as far as the lane map goes. Objects on links not on the route are ignored.
  1210.      * @param lane lane
  1211.      * @param clazz class of objects to find
  1212.      * @param gtu gtu
  1213.      * @param pos relative position to start search from
  1214.      * @param <T> type of objects to find
  1215.      * @param route the route
  1216.      * @return Sorted set of objects of requested type
  1217.      * @throws GTUException if lane is not in current set
  1218.      */
  1219.     public final <T extends LaneBasedObject> SortedSet<Entry<T>> getDownstreamObjectsOnRoute(final RelativeLane lane,
  1220.             final Class<T> clazz, final LaneBasedGTU gtu, final RelativePosition.TYPE pos, final Route route)
  1221.             throws GTUException
  1222.     {
  1223.         SortedSet<Entry<T>> set = getDownstreamObjects(lane, clazz, gtu, pos);
  1224.         if (route != null)
  1225.         {
  1226.             Iterator<Entry<T>> iterator = set.iterator();
  1227.             while (iterator.hasNext())
  1228.             {
  1229.                 Entry<T> entry = iterator.next();
  1230.                 CrossSectionLink link = entry.getLaneBasedObject().getLane().getParentLink();
  1231.                 if (!route.contains(link.getStartNode()) || !route.contains(link.getEndNode())
  1232.                         || Math.abs(route.indexOf(link.getStartNode()) - route.indexOf(link.getEndNode())) != 1)
  1233.                 {
  1234.                     iterator.remove();
  1235.                 }
  1236.             }
  1237.         }
  1238.         return set;
  1239.     }

  1240.     /**
  1241.      * Recursive search for lane based objects downstream.
  1242.      * @param set set to store entries into
  1243.      * @param record current record
  1244.      * @param clazz class of objects to find
  1245.      * @param ds distance from reference to chosen relative position
  1246.      * @param <T> type of objects to find
  1247.      */
  1248.     @SuppressWarnings("unchecked")
  1249.     private <T extends LaneBasedObject> void getDownstreamObjectsRecursive(final SortedSet<Entry<T>> set,
  1250.             final LaneStructureRecord record, final Class<T> clazz, final double ds)
  1251.     {
  1252.         if (record.getNext().isEmpty() || record.getNext().get(0).getStartDistance().gt(this.lookAhead))
  1253.         {
  1254.             return;
  1255.         }
  1256.         for (LaneStructureRecord next : record.getNext())
  1257.         {
  1258.             if (next.isDownstreamBranch())
  1259.             {
  1260.                 List<LaneBasedObject> list = next.getLane().getLaneBasedObjects();
  1261.                 int iStart, di;
  1262.                 if (record.getDirection().isPlus())
  1263.                 {
  1264.                     iStart = 0;
  1265.                     di = 1;
  1266.                 }
  1267.                 else
  1268.                 {
  1269.                     iStart = list.size() - 1;
  1270.                     di = -1;
  1271.                 }
  1272.                 for (int i = iStart; i >= 0 & i < list.size(); i += di)
  1273.                 {
  1274.                     LaneBasedObject object = list.get(i);
  1275.                     if (clazz.isAssignableFrom(object.getClass())
  1276.                             && ((record.getDirection().isPlus() && object.getDirection().isForwardOrBoth())
  1277.                                     || (record.getDirection().isMinus() && object.getDirection().isBackwardOrBoth())))
  1278.                     {
  1279.                         // unchecked, but the above isAssignableFrom assures correctness
  1280.                         double distance = next.getDistanceToPosition(object.getLongitudinalPosition()).si - ds;
  1281.                         if (distance <= this.lookAhead.si)
  1282.                         {
  1283.                             set.add(new Entry<>(Length.createSI(distance), (T) object));
  1284.                         }
  1285.                         else
  1286.                         {
  1287.                             return;
  1288.                         }
  1289.                     }
  1290.                 }
  1291.             }
  1292.             getDownstreamObjectsRecursive(set, next, clazz, ds);
  1293.         }
  1294.     }

  1295.     /**
  1296.      * Retrieve objects of a specific type. Returns objects over a maximum length of the look ahead distance downstream from the
  1297.      * relative position, or as far as the lane map goes. Objects on links not on the route are ignored.
  1298.      * @param clazz class of objects to find
  1299.      * @param gtu gtu
  1300.      * @param pos relative position to start search from
  1301.      * @param <T> type of objects to find
  1302.      * @param route the route
  1303.      * @return Sorted set of objects of requested type per lane
  1304.      * @throws GTUException if lane is not in current set
  1305.      */
  1306.     public final <T extends LaneBasedObject> Map<RelativeLane, SortedSet<Entry<T>>> getDownstreamObjectsOnRoute(
  1307.             final Class<T> clazz, final LaneBasedGTU gtu, final RelativePosition.TYPE pos, final Route route)
  1308.             throws GTUException
  1309.     {
  1310.         Map<RelativeLane, SortedSet<Entry<T>>> out = new HashMap<>();
  1311.         for (RelativeLane relativeLane : this.relativeLaneMap.keySet())
  1312.         {
  1313.             out.put(relativeLane, getDownstreamObjectsOnRoute(relativeLane, clazz, gtu, pos, route));
  1314.         }
  1315.         return out;
  1316.     }

  1317.     /**
  1318.      * Retrieve objects on a lane of a specific type. Returns upstream objects from the relative position for as far as the lane
  1319.      * map goes. Distances to upstream objects are given as positive values.
  1320.      * @param lane lane
  1321.      * @param clazz class of objects to find
  1322.      * @param gtu gtu
  1323.      * @param pos relative position to start search from
  1324.      * @param <T> type of objects to find
  1325.      * @return Sorted set of objects of requested type
  1326.      * @throws GTUException if lane is not in current set
  1327.      */
  1328.     @SuppressWarnings("unchecked")
  1329.     public final <T extends LaneBasedObject> SortedSet<Entry<T>> getUpstreamObjects(final RelativeLane lane,
  1330.             final Class<T> clazz, final LaneBasedGTU gtu, final RelativePosition.TYPE pos) throws GTUException
  1331.     {
  1332.         SortedSet<Entry<T>> set = new TreeSet<>();
  1333.         LaneStructureRecord record = this.getFirstRecord(lane);
  1334.         if (record.getStartDistance().gt0())
  1335.         {
  1336.             return set; // this lane is only downstream
  1337.         }
  1338.         Length ds = gtu.getReference().getDx().minus(gtu.getRelativePositions().get(pos).getDx());
  1339.         // the list is ordered, but only for DIR_PLUS, need to do our own ordering
  1340.         Length minimumPosition;
  1341.         Length maximumPosition;
  1342.         if (record.getDirection().isPlus())
  1343.         {
  1344.             minimumPosition = Length.ZERO;
  1345.             maximumPosition = record.getStartDistance().neg().minus(ds);
  1346.         }
  1347.         else
  1348.         {
  1349.             minimumPosition = record.getLane().getLength().plus(record.getStartDistance()).plus(ds);
  1350.             maximumPosition = record.getLane().getLength();
  1351.         }
  1352.         Length distance;
  1353.         for (LaneBasedObject object : record.getLane().getLaneBasedObjects(minimumPosition, maximumPosition))
  1354.         {
  1355.             if (clazz.isAssignableFrom(object.getClass())
  1356.                     && ((record.getDirection().isPlus() && object.getDirection().isForwardOrBoth())
  1357.                             || (record.getDirection().isMinus() && object.getDirection().isBackwardOrBoth())))
  1358.             {
  1359.                 distance = record.getDistanceToPosition(object.getLongitudinalPosition()).neg().minus(ds);
  1360.                 // unchecked, but the above isAssignableFrom assures correctness
  1361.                 set.add(new Entry<>(distance, (T) object));
  1362.             }
  1363.         }
  1364.         getUpstreamObjectsRecursive(set, record, clazz, ds);
  1365.         return set;
  1366.     }

  1367.     /**
  1368.      * Recursive search for lane based objects upstream.
  1369.      * @param set set to store entries into
  1370.      * @param record current record
  1371.      * @param clazz class of objects to find
  1372.      * @param ds distance from reference to chosen relative position
  1373.      * @param <T> type of objects to find
  1374.      */
  1375.     @SuppressWarnings("unchecked")
  1376.     private <T extends LaneBasedObject> void getUpstreamObjectsRecursive(final SortedSet<Entry<T>> set,
  1377.             final LaneStructureRecord record, final Class<T> clazz, final Length ds)
  1378.     {
  1379.         for (LaneStructureRecord prev : record.getPrev())
  1380.         {
  1381.             Length distance;
  1382.             for (LaneBasedObject object : prev.getLane().getLaneBasedObjects())
  1383.             {
  1384.                 if (clazz.isAssignableFrom(object.getClass())
  1385.                         && ((record.getDirection().isPlus() && object.getDirection().isForwardOrBoth())
  1386.                                 || (record.getDirection().isMinus() && object.getDirection().isBackwardOrBoth())))
  1387.                 {
  1388.                     distance = prev.getDistanceToPosition(object.getLongitudinalPosition()).neg().minus(ds);
  1389.                     // unchecked, but the above isAssignableFrom assures correctness
  1390.                     set.add(new Entry<>(distance, (T) object));
  1391.                 }
  1392.             }
  1393.             getUpstreamObjectsRecursive(set, prev, clazz, ds);
  1394.         }
  1395.     }

  1396.     /** {@inheritDoc} */
  1397.     @Override
  1398.     public final String toString()
  1399.     {
  1400.         return "LaneStructure [rootLSR=" + this.root + "]";
  1401.     }

  1402.     /**
  1403.      * Enables visualization of this lane structure. This is purely for debugging purposes.
  1404.      * @param gtu GTU to animate the LaneStructure off
  1405.      */
  1406.     public final void visualize(final GTU gtu)
  1407.     {
  1408.         /**
  1409.          * Locatable.
  1410.          */
  1411.         class LaneStructureLocatable implements Locatable
  1412.         {
  1413.             /** {@inheritDoc} */
  1414.             @Override
  1415.             public DirectedPoint getLocation() throws RemoteException
  1416.             {
  1417.                 LaneStructureRecord rt = getRootRecord();
  1418.                 if (rt == null)
  1419.                 {
  1420.                     return gtu.getLocation();
  1421.                 }
  1422.                 Length position = rt.getDirection().isPlus() ? rt.getStartDistance().neg()
  1423.                         : rt.getLane().getLength().plus(rt.getStartDistance());
  1424.                 position = position.lt0() ? Length.ZERO : position;
  1425.                 try
  1426.                 {
  1427.                     return rt.getLane().getCenterLine().getLocation(position);
  1428.                 }
  1429.                 catch (OTSGeometryException exception)
  1430.                 {
  1431.                     throw new RuntimeException("Unable to return location.", exception);
  1432.                 }
  1433.             }

  1434.             /** {@inheritDoc} */
  1435.             @Override
  1436.             public Bounds getBounds() throws RemoteException
  1437.             {
  1438.                 Point3d p1 = new Point3d(-1000000, -1000000, 0.0);
  1439.                 Point3d p2 = new Point3d(1000000, 1000000, 0.0);
  1440.                 return new BoundingBox(p1, p2);
  1441.             }
  1442.         }
  1443.         /**
  1444.          * Animation of lane structure.
  1445.          */
  1446.         class LaneStructureAnimation extends Renderable2D<LaneStructureLocatable>
  1447.         {

  1448.             /** Destroyed. */
  1449.             private boolean isDestroyed = false;

  1450.             /**
  1451.              * @param source LaneStructureLocatable; dummy locatable
  1452.              * @throws NamingException on naming exception
  1453.              * @throws RemoteException on remote exception
  1454.              */
  1455.             LaneStructureAnimation(final LaneStructureLocatable source) throws NamingException, RemoteException
  1456.             {
  1457.                 super(source, gtu.getSimulator());
  1458.                 this.setFlip(false);
  1459.                 this.setRotate(false);
  1460.             }

  1461.             /** {@inheritDoc} */
  1462.             @Override
  1463.             public void paint(final Graphics2D graphics, final ImageObserver observer) throws RemoteException
  1464.             {
  1465.                 if (!this.isDestroyed)
  1466.                 {
  1467.                     if (gtu.isDestroyed())
  1468.                     {
  1469.                         this.isDestroyed = true;
  1470.                         Try.execute(() -> destroy(), "Exception during deletion of LaneStructureAnimation.");
  1471.                         return;
  1472.                     }
  1473.                     else
  1474.                     {
  1475.                         LaneStructureRecord rt = getRootRecord();
  1476.                         if (rt != null)
  1477.                         {
  1478.                             paintRecord(rt, graphics);
  1479.                         }
  1480.                     }
  1481.                 }
  1482.             }

  1483.             /**
  1484.              * @param lsr LaneStructureRecord; record
  1485.              * @param graphics Graphics2D; graphics
  1486.              */
  1487.             @SuppressWarnings({ "unchecked", "synthetic-access" })
  1488.             private void paintRecord(final LaneStructureRecord lsr, final Graphics2D graphics)
  1489.             {
  1490.                 // line
  1491.                 DirectedPoint loc = Try.assign(() -> getSource().getLocation(), "Unable to return location.");
  1492.                 graphics.setStroke(
  1493.                         new BasicStroke(RollingLaneStructure.this.crossSectionRecords.containsValue(lsr) ? 1.0f : 0.5f));
  1494.                 graphics.setColor(RollingLaneStructure.this.crossSectionRecords.containsValue(lsr) ? Color.PINK
  1495.                         : RollingLaneStructure.this.upstreamEdge.contains(lsr) ? Color.MAGENTA
  1496.                                 : RollingLaneStructure.this.downstreamEdge.contains(lsr) ? Color.GREEN : Color.CYAN);
  1497.                 OTSLine3D line = Try.assign(() -> lsr.getLane().getCenterLine().extractFractional(0.1, 0.9),
  1498.                         "Exception while painting LaneStructures");
  1499.                 Path2D.Double path = new Path2D.Double();
  1500.                 boolean start = true;
  1501.                 for (OTSPoint3D point : line.getPoints())
  1502.                 {
  1503.                     if (start)
  1504.                     {
  1505.                         path.moveTo(point.x - loc.x, -(point.y - loc.y));
  1506.                         start = false;
  1507.                     }
  1508.                     else
  1509.                     {
  1510.                         path.lineTo(point.x - loc.x, -(point.y - loc.y));
  1511.                     }
  1512.                 }
  1513.                 graphics.draw(path);
  1514.                 // connection
  1515.                 Field sourceField = Try.assign(() -> RollingLaneStructureRecord.class.getDeclaredField("source"),
  1516.                         "Exception while painting LaneStructure");
  1517.                 sourceField.setAccessible(true);
  1518.                 LaneStructureRecord src =
  1519.                         Try.assign(() -> (LaneStructureRecord) sourceField.get(lsr), "Exception while painting LaneStructure");
  1520.                 if (src != null)
  1521.                 {
  1522.                     Field sourceLinkField = Try.assign(() -> RollingLaneStructureRecord.class.getDeclaredField("sourceLink"),
  1523.                             "Exception while painting LaneStructure");
  1524.                     sourceLinkField.setAccessible(true);
  1525.                     RecordLink link =
  1526.                             (RecordLink) Try.assign(() -> sourceLinkField.get(lsr), "Exception while painting LaneStructure");
  1527.                     float f1 = link.equals(RecordLink.DOWN) ? 0.9f : link.equals(RecordLink.UP) ? 0.1f : 0.5f;
  1528.                     float f2 = link.equals(RecordLink.DOWN) ? 0.0f : link.equals(RecordLink.UP) ? 1.0f : 0.5f;
  1529.                     f1 = src.getDirection().isPlus() ? f1 : 1.0f - f1;
  1530.                     f2 = lsr.getDirection().isPlus() ? f2 : 1.0f - f2;
  1531.                     float f3 = f1;
  1532.                     float f4 = f2;
  1533.                     DirectedPoint p1 = Try.assign(() -> src.getLane().getCenterLine().getLocationFraction(f3),
  1534.                             "Exception while painting LaneStructure");
  1535.                     DirectedPoint p2 = Try.assign(() -> line.getLocationFraction(f4), "Exception while painting LaneStructure");
  1536.                     path = new Path2D.Double();
  1537.                     path.moveTo(p1.x - loc.x, -(p1.y - loc.y));
  1538.                     path.lineTo(p2.x - loc.x, -(p2.y - loc.y));
  1539.                     graphics.setStroke(new BasicStroke(0.15f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 10f,
  1540.                             new float[] { .3f, 1.2f }, 0f));
  1541.                     graphics.setColor(Color.DARK_GRAY);
  1542.                     graphics.draw(path);
  1543.                 }
  1544.                 // left/right
  1545.                 paintLateralConnection(lsr, lsr.getLeft(), Color.RED, graphics, loc);
  1546.                 paintLateralConnection(lsr, lsr.getRight(), Color.BLUE, graphics, loc);
  1547.                 // recursion to depending records
  1548.                 Field dependentField = Try.assign(() -> RollingLaneStructureRecord.class.getDeclaredField("dependentRecords"),
  1549.                         "Exception while painting LaneStructure");
  1550.                 dependentField.setAccessible(true);
  1551.                 Set<LaneStructureRecord> dependables = (Set<LaneStructureRecord>) Try.assign(() -> dependentField.get(lsr),
  1552.                         "Exception while painting LaneStructure");
  1553.                 if (dependables != null)
  1554.                 {
  1555.                     for (LaneStructureRecord dependable : new LinkedHashSet<>(dependables)) // concurrency
  1556.                     {
  1557.                         paintRecord(dependable, graphics);
  1558.                     }
  1559.                 }
  1560.             }

  1561.             /**
  1562.              * Paint the connection to a lateral record.
  1563.              * @param main LaneStructureRecord; main record
  1564.              * @param adj LaneStructureRecord; adjacent record, can be {@code null}
  1565.              * @param color Color; color
  1566.              * @param graphics Graphics2D; graphics
  1567.              * @param loc DirectedPoint; location
  1568.              */
  1569.             private void paintLateralConnection(final LaneStructureRecord main, final LaneStructureRecord adj,
  1570.                     final Color color, final Graphics2D graphics, final DirectedPoint loc)
  1571.             {
  1572.                 if (adj == null)
  1573.                 {
  1574.                     return;
  1575.                 }
  1576.                 float f1 = main.getDirection().isPlus() ? 0.45f : 0.55f;
  1577.                 float f2 = adj.getDirection().isPlus() ? 0.55f : 0.45f;
  1578.                 DirectedPoint p1 = Try.assign(() -> main.getLane().getCenterLine().getLocationFraction(f1),
  1579.                         "Exception while painting LaneStructure");
  1580.                 DirectedPoint p2 = Try.assign(() -> adj.getLane().getCenterLine().getLocationFraction(f2),
  1581.                         "Exception while painting LaneStructure");
  1582.                 Path2D.Double path = new Path2D.Double();
  1583.                 path.moveTo(p1.x - loc.x, -(p1.y - loc.y));
  1584.                 path.lineTo(p2.x - loc.x, -(p2.y - loc.y));
  1585.                 graphics.setStroke(new BasicStroke(0.05f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 10f,
  1586.                         new float[] { .15f, 0.6f }, 0f));
  1587.                 graphics.setColor(color);
  1588.                 graphics.draw(path);
  1589.             }
  1590.         }
  1591.         Try.execute(() -> new LaneStructureAnimation(new LaneStructureLocatable()), "Could not create animation.");
  1592.     }

  1593. }