RoadNetwork.java

  1. package org.opentrafficsim.road.network;

  2. import java.util.Iterator;
  3. import java.util.LinkedHashMap;
  4. import java.util.List;
  5. import java.util.Map;
  6. import java.util.Set;
  7. import java.util.SortedSet;
  8. import java.util.TreeSet;

  9. import org.djunits.value.vdouble.scalar.Length;
  10. import org.djutils.base.Identifiable;
  11. import org.djutils.exceptions.Throw;
  12. import org.djutils.immutablecollections.ImmutableSortedSet;
  13. import org.djutils.immutablecollections.ImmutableTreeSet;
  14. import org.djutils.multikeymap.MultiKeyMap;
  15. import org.jgrapht.GraphPath;
  16. import org.jgrapht.alg.shortestpath.DijkstraShortestPath;
  17. import org.jgrapht.graph.SimpleDirectedWeightedGraph;
  18. import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
  19. import org.opentrafficsim.core.gtu.GtuType;
  20. import org.opentrafficsim.core.network.LateralDirectionality;
  21. import org.opentrafficsim.core.network.Link;
  22. import org.opentrafficsim.core.network.Network;
  23. import org.opentrafficsim.core.network.NetworkException;
  24. import org.opentrafficsim.core.network.Node;
  25. import org.opentrafficsim.core.network.route.Route;
  26. import org.opentrafficsim.road.network.lane.CrossSectionLink;
  27. import org.opentrafficsim.road.network.lane.Lane;

  28. /**
  29.  * RoadNetwork adds the ability to retrieve lane change information.
  30.  * <p>
  31.  * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
  32.  * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  33.  * </p>
  34.  * @author <a href="https://github.com/averbraeck" target="_blank">Alexander Verbraeck</a>
  35.  * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
  36.  */
  37. public class RoadNetwork extends Network
  38. {
  39.     /** */
  40.     private static final long serialVersionUID = 1L;

  41.     /** Cached lane graph for legal connections, per GTU type. */
  42.     private Map<GtuType, RouteWeightedGraph> legalLaneGraph = new LinkedHashMap<>();

  43.     /** Cached lane graph for physical connections. */
  44.     private RouteWeightedGraph physicalLaneGraph = null;

  45.     /** Cached legal lane change info, over complete length of route. */
  46.     private MultiKeyMap<SortedSet<LaneChangeInfo>> legalLaneChangeInfoCache =
  47.             new MultiKeyMap<>(GtuType.class, Route.class, Lane.class);

  48.     /** Cached physical lane change info, over complete length of route. */
  49.     private MultiKeyMap<SortedSet<LaneChangeInfo>> physicalLaneChangeInfoCache = new MultiKeyMap<>(Route.class, Lane.class);

  50.     /**
  51.      * Construction of an empty network.
  52.      * @param id the network id.
  53.      * @param simulator the DSOL simulator engine
  54.      */
  55.     public RoadNetwork(final String id, final OtsSimulatorInterface simulator)
  56.     {
  57.         super(id, simulator);
  58.     }

  59.     /**
  60.      * Returns lane change info from the given lane. Distances are given from the start of the lane and will never exceed the
  61.      * given range. This method returns {@code null} if no valid path exists. If there are no reasons to change lane within
  62.      * range, an empty set is returned.
  63.      * @param lane from lane.
  64.      * @param route route.
  65.      * @param gtuType GTU Type.
  66.      * @param range maximum range of info to consider, from the start of the given lane.
  67.      * @param laneAccessLaw lane access law.
  68.      * @return lane change info from the given lane, or {@code null} if no path exists.
  69.      */
  70.     public ImmutableSortedSet<LaneChangeInfo> getLaneChangeInfo(final Lane lane, final Route route, final GtuType gtuType,
  71.             final Length range, final LaneAccessLaw laneAccessLaw)
  72.     {
  73.         Throw.whenNull(lane, "Lane may not be null.");
  74.         Throw.whenNull(route, "Route may not be null.");
  75.         Throw.whenNull(gtuType, "GTU type may not be null.");
  76.         Throw.whenNull(range, "Range may not be null.");
  77.         Throw.whenNull(laneAccessLaw, "Lane access law may not be null.");
  78.         Throw.when(range.le0(), IllegalArgumentException.class, "Range should be a positive value.");

  79.         // get the complete info
  80.         SortedSet<LaneChangeInfo> info = getCompleteLaneChangeInfo(lane, route, gtuType, laneAccessLaw);
  81.         if (info == null)
  82.         {
  83.             return null;
  84.         }

  85.         // find first LaneChangeInfo beyond range, if any
  86.         LaneChangeInfo lcInfoBeyondHorizon = null;
  87.         Iterator<LaneChangeInfo> iterator = info.iterator();
  88.         while (lcInfoBeyondHorizon == null && iterator.hasNext())
  89.         {
  90.             LaneChangeInfo lcInfo = iterator.next();
  91.             if (lcInfo.remainingDistance().gt(range))
  92.             {
  93.                 lcInfoBeyondHorizon = lcInfo;
  94.             }
  95.         }

  96.         // return subset in range
  97.         if (lcInfoBeyondHorizon != null)
  98.         {
  99.             return new ImmutableTreeSet<>(info.headSet(lcInfoBeyondHorizon));
  100.         }
  101.         return new ImmutableTreeSet<>(info); // empty, or all in range
  102.     }

  103.     /**
  104.      * Returns the complete (i.e. without range) lane change info from the given lane. It is either taken from cache, or
  105.      * created.
  106.      * @param lane from lane.
  107.      * @param route route.
  108.      * @param gtuType GTU Type.
  109.      * @param laneAccessLaw lane access law.
  110.      * @return complete (i.e. without range) lane change info from the given lane, or {@code null} if no path exists.
  111.      */
  112.     private SortedSet<LaneChangeInfo> getCompleteLaneChangeInfo(final Lane lane, final Route route, final GtuType gtuType,
  113.             final LaneAccessLaw laneAccessLaw)
  114.     {
  115.         // try to get info from the right cache
  116.         SortedSet<LaneChangeInfo> outputLaneChangeInfo;
  117.         if (laneAccessLaw.equals(LaneAccessLaw.LEGAL))
  118.         {
  119.             outputLaneChangeInfo = this.legalLaneChangeInfoCache.get(gtuType, route, lane);
  120.             // build info if required
  121.             if (outputLaneChangeInfo == null)
  122.             {
  123.                 // get the right lane graph for the GTU type, or build it
  124.                 RouteWeightedGraph graph = this.legalLaneGraph.get(gtuType);
  125.                 if (graph == null)
  126.                 {
  127.                     graph = new RouteWeightedGraph();
  128.                     this.legalLaneGraph.put(gtuType, graph);
  129.                     buildGraph(graph, gtuType, laneAccessLaw);
  130.                 }
  131.                 List<LaneChangeInfoEdge> path = findPath(lane, graph, gtuType, route);

  132.                 if (path != null)
  133.                 {
  134.                     // derive lane change info from every lane along the path and cache it
  135.                     boolean originalPath = true;
  136.                     while (!path.isEmpty())
  137.                     {
  138.                         SortedSet<LaneChangeInfo> laneChangeInfo = extractLaneChangeInfo(path);
  139.                         if (originalPath)
  140.                         {
  141.                             outputLaneChangeInfo = laneChangeInfo;
  142.                             originalPath = false;
  143.                         }
  144.                         this.legalLaneChangeInfoCache.put(laneChangeInfo, gtuType, route, path.get(0).fromLane());
  145.                         path.remove(0); // next lane
  146.                     }
  147.                 }
  148.             }
  149.         }
  150.         else if (laneAccessLaw.equals(LaneAccessLaw.PHYSICAL))
  151.         {
  152.             outputLaneChangeInfo = this.physicalLaneChangeInfoCache.get(route, lane);
  153.             // build info if required
  154.             if (outputLaneChangeInfo == null)
  155.             {
  156.                 // build the lane graph if required
  157.                 if (this.physicalLaneGraph == null)
  158.                 {
  159.                     this.physicalLaneGraph = new RouteWeightedGraph();
  160.                     // TODO: Is the GTU type actually relevant for physical? It is used still to find adjacent lanes.
  161.                     buildGraph(this.physicalLaneGraph, gtuType, laneAccessLaw);
  162.                 }
  163.                 List<LaneChangeInfoEdge> path = findPath(lane, this.physicalLaneGraph, gtuType, route);

  164.                 if (path != null)
  165.                 {
  166.                     // derive lane change info from every lane along the path and cache it
  167.                     boolean originalPath = true;
  168.                     while (!path.isEmpty())
  169.                     {
  170.                         SortedSet<LaneChangeInfo> laneChangeInfo = extractLaneChangeInfo(path);
  171.                         if (originalPath)
  172.                         {
  173.                             outputLaneChangeInfo = laneChangeInfo;
  174.                             originalPath = false;
  175.                         }
  176.                         this.physicalLaneChangeInfoCache.put(laneChangeInfo, route, path.get(0).fromLane());
  177.                         path.remove(0); // next lane
  178.                     }
  179.                 }
  180.             }
  181.         }
  182.         else
  183.         {
  184.             // in case it is inadvertently extended in the future
  185.             throw new RuntimeException(String.format("Unknown LaneChangeLaw %s", laneAccessLaw));
  186.         }
  187.         return outputLaneChangeInfo;
  188.     }

  189.     /**
  190.      * Builds the graph.
  191.      * @param graph empty graph to build.
  192.      * @param gtuType GTU type.
  193.      * @param laneChangeLaw lane change law, legal or physical.
  194.      */
  195.     private void buildGraph(final RouteWeightedGraph graph, final GtuType gtuType, final LaneAccessLaw laneChangeLaw)
  196.     {
  197.         // add vertices
  198.         boolean legal = laneChangeLaw.equals(LaneAccessLaw.LEGAL);
  199.         for (Link link : this.getLinkMap().values())
  200.         {
  201.             for (Lane lane : legal ? ((CrossSectionLink) link).getLanes() : ((CrossSectionLink) link).getLanesAndShoulders())
  202.             {
  203.                 graph.addVertex(lane);
  204.             }
  205.             // each end node may be a destination for the shortest path search
  206.             graph.addVertex(link.getEndNode());
  207.         }

  208.         // add edges
  209.         for (Link link : this.getLinkMap().values())
  210.         {
  211.             if (link instanceof CrossSectionLink cLink)
  212.             {
  213.                 for (Lane lane : legal ? cLink.getLanes() : cLink.getLanesAndShoulders())
  214.                 {
  215.                     // adjacent lanes
  216.                     for (LateralDirectionality lat : List.of(LateralDirectionality.LEFT, LateralDirectionality.RIGHT))
  217.                     {
  218.                         Set<Lane> adjacentLanes;
  219.                         if (legal)
  220.                         {
  221.                             adjacentLanes = lane.accessibleAdjacentLanesLegal(lat, gtuType);
  222.                         }
  223.                         else
  224.                         {
  225.                             adjacentLanes = lane.accessibleAdjacentLanesPhysical(lat, gtuType);
  226.                         }
  227.                         for (Lane adjacentLane : adjacentLanes)
  228.                         {
  229.                             LaneChangeInfoEdgeType type = lat.equals(LateralDirectionality.LEFT) ? LaneChangeInfoEdgeType.LEFT
  230.                                     : LaneChangeInfoEdgeType.RIGHT;
  231.                             // downstream link may be null for lateral edges
  232.                             LaneChangeInfoEdge edge = new LaneChangeInfoEdge(lane, type, null);
  233.                             graph.addEdge(lane, adjacentLane, edge);
  234.                         }
  235.                     }
  236.                     // next lanes
  237.                     Set<Lane> nextLanes = lane.nextLanes(legal ? gtuType : null);
  238.                     for (Lane nextLane : nextLanes)
  239.                     {
  240.                         LaneChangeInfoEdge edge =
  241.                                 new LaneChangeInfoEdge(lane, LaneChangeInfoEdgeType.DOWNSTREAM, nextLane.getLink());
  242.                         graph.addEdge(lane, nextLane, edge);
  243.                     }
  244.                     // add edge towards end node so that it can be used as a destination in the shortest path search
  245.                     LaneChangeInfoEdge edge = new LaneChangeInfoEdge(lane, LaneChangeInfoEdgeType.DOWNSTREAM, null);
  246.                     graph.addEdge(lane, lane.getLink().getEndNode(), edge);
  247.                 }
  248.             }
  249.         }
  250.     }

  251.     /**
  252.      * Returns a set of lane change info, extracted from the graph.
  253.      * @param lane from lane.
  254.      * @param graph graph.
  255.      * @param gtuType GTU Type.
  256.      * @param route route.
  257.      * @return path derived from the graph, or {@code null} if there is no path.
  258.      */
  259.     private List<LaneChangeInfoEdge> findPath(final Lane lane, final RouteWeightedGraph graph, final GtuType gtuType,
  260.             final Route route)
  261.     {
  262.         // if there is no route, find the destination node by moving down the links (no splits allowed)
  263.         Node destination = null;
  264.         Route routeForWeights = route;
  265.         if (route == null)
  266.         {
  267.             destination = graph.getNoRouteDestinationNode(gtuType);
  268.             try
  269.             {
  270.                 routeForWeights = getShortestRouteBetween(gtuType, lane.getLink().getStartNode(), destination);
  271.             }
  272.             catch (NetworkException exception)
  273.             {
  274.                 // this should not happen, as we obtained the destination by moving downstream towards the end of the network
  275.                 throw new RuntimeException("Could not find route to destination.", exception);
  276.             }
  277.         }
  278.         else
  279.         {
  280.             // otherwise, get destination node from route, which is the last node on a link with lanes (i.e. no connector)
  281.             List<Node> nodes = route.getNodes();
  282.             for (int i = nodes.size() - 1; i > 0; i--)
  283.             {
  284.                 Link link = getLink(nodes.get(i - 1), nodes.get(i));
  285.                 if (link instanceof CrossSectionLink && !((CrossSectionLink) link).getLanes().isEmpty())
  286.                 {
  287.                     destination = nodes.get(i);
  288.                     break; // found most downstream link with lanes, who's end node is the destination for lane changes
  289.                 }
  290.             }
  291.             Throw.whenNull(destination, "Route has no links with lanes, "
  292.                     + "unable to find a suitable destination node regarding lane change information.");
  293.         }

  294.         // set the route on the path for route-dependent edge weights
  295.         graph.setRoute(routeForWeights);

  296.         // find the shortest path
  297.         GraphPath<Identifiable, LaneChangeInfoEdge> path = DijkstraShortestPath.findPathBetween(graph, lane, destination);
  298.         return path == null ? null : path.getEdgeList();
  299.     }

  300.     /**
  301.      * Extracts lane change info from a path.
  302.      * @param path path.
  303.      * @return lane change info.
  304.      */
  305.     private SortedSet<LaneChangeInfo> extractLaneChangeInfo(final List<LaneChangeInfoEdge> path)
  306.     {
  307.         SortedSet<LaneChangeInfo> info = new TreeSet<>();
  308.         Length x = Length.ZERO; // cumulative longitudinal distance
  309.         int n = 0; // number of applied lane changes
  310.         boolean inLateralState = false; // consecutive lateral moves in the path create 1 LaneChangeInfo
  311.         for (LaneChangeInfoEdge edge : path)
  312.         {
  313.             LaneChangeInfoEdgeType lcType = edge.laneChangeInfoEdgeType();
  314.             int lat = lcType.equals(LaneChangeInfoEdgeType.LEFT) ? -1 : (lcType.equals(LaneChangeInfoEdgeType.RIGHT) ? 1 : 0);

  315.             // check opposite lateral direction
  316.             if (n * lat < 0)
  317.             {
  318.                 /*
  319.                  * The required direction is opposite a former required direction, in which case all further lane change
  320.                  * information is not yet of concern. For example, we first need to make 1 right lane change for a lane drop,
  321.                  * and then later 2 lane changes to the left for a split. The latter information is pointless before the lane
  322.                  * drop; we are not going to stay on the lane longer as it won't affect the ease of the left lane changes later.
  323.                  */
  324.                 break;
  325.             }

  326.             // increase n, x, and trigger (consecutive) lateral move start or stop
  327.             if (lat == 0)
  328.             {
  329.                 // lateral move stop
  330.                 if (inLateralState)
  331.                 {
  332.                     // TODO: isDeadEnd should be removed from LaneChangeInfo, behavior should consider legal vs. physical
  333.                     boolean isDeadEnd = false;
  334.                     info.add(new LaneChangeInfo(Math.abs(n), x, isDeadEnd,
  335.                             n < 0 ? LateralDirectionality.LEFT : LateralDirectionality.RIGHT));
  336.                     inLateralState = false;
  337.                     // don't add the length of the previous lane, that was already done for the first lane of all lateral moves
  338.                 }
  339.                 else
  340.                 {
  341.                     // longitudinal move, we need to add distance to x
  342.                     x = x.plus(edge.fromLane().getLength());
  343.                 }
  344.             }
  345.             else
  346.             {
  347.                 // lateral move start
  348.                 if (!inLateralState)
  349.                 {
  350.                     x = x.plus(edge.fromLane().getLength()); // need to add length of first lane of all lateral moves
  351.                     inLateralState = true;
  352.                 }
  353.                 // increase lane change count (negative for left)
  354.                 n += lat;
  355.             }
  356.         }
  357.         return info;
  358.     }

  359.     /**
  360.      * Clears all lane change info graphs and cached sets. This method should be invoked on every network change that affects
  361.      * lane changes and the distances within which they need to be performed.
  362.      */
  363.     public void clearLaneChangeInfoCache()
  364.     {
  365.         this.legalLaneGraph.clear();
  366.         this.physicalLaneGraph = null;
  367.         this.legalLaneChangeInfoCache = new MultiKeyMap<>(GtuType.class, Route.class, Lane.class);
  368.         this.physicalLaneChangeInfoCache = new MultiKeyMap<>(Route.class, Lane.class);
  369.     }

  370.     /**
  371.      * A {@code SimpleDirectedWeightedGraph} to search over the lanes, where the weight of an edge (movement between lanes) is
  372.      * tailored to providing lane change information. The vertex type is {@code Identifiable} such that both {@code Lane}'s and
  373.      * {@code Node}'s can be used. The latter is required to find paths towards a destination node.
  374.      * <p>
  375.      * Copyright (c) 2022-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
  376.      * <br>
  377.      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  378.      * </p>
  379.      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
  380.      */
  381.     private class RouteWeightedGraph extends SimpleDirectedWeightedGraph<Identifiable, LaneChangeInfoEdge>
  382.     {

  383.         /** */
  384.         private static final long serialVersionUID = 20220923L;

  385.         /** Route. */
  386.         private Route route;

  387.         /** Node in the network that is the destination if no route is used. */
  388.         private Node noRouteDestination = null;

  389.         /**
  390.          * Constructor.
  391.          */
  392.         RouteWeightedGraph()
  393.         {
  394.             super(LaneChangeInfoEdge.class);
  395.         }

  396.         /**
  397.          * Set the route.
  398.          * @param route route.
  399.          */
  400.         public void setRoute(final Route route)
  401.         {
  402.             Throw.whenNull(route, "Route may not be null for lane change information.");
  403.             this.route = route;
  404.         }

  405.         /**
  406.          * Returns the weight of moving from one lane to the next. In order to find the latest possible location at which lane
  407.          * changes may still be performed, the longitudinal weights are 1.0 while the lateral weights are 1.0 + 1/X, where X is
  408.          * the number (index) of the link within the route. This favors later lane changes for the shortest path algorithm, as
  409.          * we are interested in the distances within which the lane change have to be performed. In the case an edge is towards
  410.          * a link that is not in a given route, a positive infinite weight is returned. Finally, when the edge is towards a
  411.          * node, which may be the destination in a route, 0.0 is returned.
  412.          */
  413.         @Override
  414.         public double getEdgeWeight(final LaneChangeInfoEdge e)
  415.         {
  416.             if (e.laneChangeInfoEdgeType().equals(LaneChangeInfoEdgeType.LEFT)
  417.                     || e.laneChangeInfoEdgeType().equals(LaneChangeInfoEdgeType.RIGHT))
  418.             {
  419.                 int indexEndNode = this.route.indexOf(e.fromLane().getLink().getEndNode());
  420.                 return 1.0 + 1.0 / indexEndNode; // lateral, reduce weight for further lane changes
  421.             }
  422.             Link toLink = e.toLink();
  423.             if (toLink == null)
  424.             {
  425.                 return 0.0; // edge towards Node, which may be the destination in a Route
  426.             }
  427.             if (this.route.contains(toLink.getEndNode())
  428.                     && this.route.indexOf(toLink.getEndNode()) == this.route.indexOf(toLink.getStartNode()) + 1)
  429.             {
  430.                 return 1.0; // downstream, always 1.0 if the next lane is on the route
  431.             }
  432.             return Double.POSITIVE_INFINITY; // next lane not on the route, this is a dead-end branch for the route
  433.         }

  434.         /**
  435.          * Returns the destination node to use when no route is available. This will be the last node found moving downstream.
  436.          * @param gtuType GTU type.
  437.          * @return destination node to use when no route is available.
  438.          */
  439.         public Node getNoRouteDestinationNode(final GtuType gtuType)
  440.         {
  441.             if (this.noRouteDestination == null)
  442.             {
  443.                 // get any lane from the network
  444.                 Lane lane = null;
  445.                 Iterator<Identifiable> iterator = this.vertexSet().iterator();
  446.                 while (lane == null && iterator.hasNext())
  447.                 {
  448.                     Identifiable next = iterator.next();
  449.                     if (next instanceof Lane)
  450.                     {
  451.                         lane = (Lane) next;
  452.                     }
  453.                 }
  454.                 Throw.when(lane == null, RuntimeException.class, "Requesting destination node on network without lanes.");
  455.                 // move to downstream link for as long as there is 1 downstream link
  456.                 try
  457.                 {
  458.                     Link link = lane.getLink();
  459.                     Set<Link> downstreamLinks = link.getEndNode().nextLinks(gtuType, link);
  460.                     while (downstreamLinks.size() == 1)
  461.                     {
  462.                         link = downstreamLinks.iterator().next();
  463.                         downstreamLinks = link.getEndNode().nextLinks(gtuType, link);
  464.                     }
  465.                     Throw.when(downstreamLinks.size() > 1, RuntimeException.class, "Using null route on network with split. "
  466.                             + "Unable to find a destination to find lane change info towards.");
  467.                     this.noRouteDestination = link.getEndNode();
  468.                 }
  469.                 catch (NetworkException ne)
  470.                 {
  471.                     throw new RuntimeException("Requesting lane change info from link that does not allow the GTU type.", ne);
  472.                 }
  473.             }
  474.             return this.noRouteDestination;
  475.         }
  476.     }

  477.     /**
  478.      * Edge between two lanes, or between a lane and a node (to provide the shortest path algorithm with a suitable
  479.      * destination). From a list of these from a path, the lane change information along the path (distances and number of lane
  480.      * changes) can be derived.
  481.      * <p>
  482.      * Copyright (c) 2022-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
  483.      * <br>
  484.      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  485.      * </p>
  486.      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
  487.      * @param fromLane from lane, to allow construction of distances from a path.
  488.      * @param laneChangeInfoEdgeType the type of lane to lane movement performed along this edge.
  489.      * @param toLink to link (of the lane this edge moves to).
  490.      */
  491.     private static record LaneChangeInfoEdge(Lane fromLane, LaneChangeInfoEdgeType laneChangeInfoEdgeType, Link toLink)
  492.     {
  493.     }

  494.     /**
  495.      * Enum to provide information on the lane to lane movement in a path.
  496.      * <p>
  497.      * Copyright (c) 2022-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
  498.      * <br>
  499.      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  500.      * </p>
  501.      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
  502.      */
  503.     private enum LaneChangeInfoEdgeType
  504.     {
  505.         /** Left lane change. */
  506.         LEFT,

  507.         /** Right lane change. */
  508.         RIGHT,

  509.         /** Downstream movement, either towards a lane, or towards a node (which may be the destination in a route). */
  510.         DOWNSTREAM;
  511.     }

  512. }