View Javadoc
1   package org.opentrafficsim.road.network;
2   
3   import java.util.Iterator;
4   import java.util.LinkedHashMap;
5   import java.util.List;
6   import java.util.Map;
7   import java.util.Set;
8   import java.util.SortedSet;
9   import java.util.TreeSet;
10  
11  import org.djunits.Throw;
12  import org.djunits.value.vdouble.scalar.Length;
13  import org.djutils.immutablecollections.ImmutableSortedSet;
14  import org.djutils.immutablecollections.ImmutableTreeSet;
15  import org.djutils.multikeymap.MultiKeyMap;
16  import org.jgrapht.GraphPath;
17  import org.jgrapht.alg.shortestpath.DijkstraShortestPath;
18  import org.jgrapht.graph.SimpleDirectedWeightedGraph;
19  import org.opentrafficsim.base.Identifiable;
20  import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
21  import org.opentrafficsim.core.gtu.GtuType;
22  import org.opentrafficsim.core.network.LateralDirectionality;
23  import org.opentrafficsim.core.network.Link;
24  import org.opentrafficsim.core.network.Network;
25  import org.opentrafficsim.core.network.NetworkException;
26  import org.opentrafficsim.core.network.Node;
27  import org.opentrafficsim.core.network.route.Route;
28  import org.opentrafficsim.road.network.lane.CrossSectionLink;
29  import org.opentrafficsim.road.network.lane.Lane;
30  
31  /**
32   * RoadNetwork adds the ability to retrieve lane change information.
33   * <p>
34   * Copyright (c) 2013-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
35   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
36   * </p>
37   * @author <a href="https://github.com/averbraeck" target="_blank">Alexander Verbraeck</a>
38   */
39  public class RoadNetwork extends Network
40  {
41      /** */
42      private static final long serialVersionUID = 1L;
43  
44      /** Cached lane graph for legal connections, per GTU type. */
45      private Map<GtuType, RouteWeightedGraph> legalLaneGraph = new LinkedHashMap<>();
46  
47      /** Cached lane graph for physical connections. */
48      private RouteWeightedGraph physicalLaneGraph = null;
49  
50      /** Cached legal lane change info, over complete length of route. */
51      private MultiKeyMap<SortedSet<LaneChangeInfo>> legalLaneChangeInfoCache =
52              new MultiKeyMap<>(GtuType.class, Route.class, Lane.class);
53  
54      /** Cached physical lane change info, over complete length of route. */
55      private MultiKeyMap<SortedSet<LaneChangeInfo>> physicalLaneChangeInfoCache = new MultiKeyMap<>(Route.class, Lane.class);
56  
57      /**
58       * Construction of an empty network.
59       * @param id String; the network id.
60       * @param simulator OtsSimulatorInterface; the DSOL simulator engine
61       */
62      public RoadNetwork(final String id, final OtsSimulatorInterface simulator)
63      {
64          super(id, simulator);
65      }
66  
67      /**
68       * Returns lane change info from the given lane. Distances are given from the start of the lane and will never exceed the
69       * given range. This method returns {@code null} if no valid path exists. If there are no reasons to change lane within
70       * range, an empty set is returned.
71       * @param lane Lane; from lane.
72       * @param route Route; route.
73       * @param gtuType GtuType; GTU Type.
74       * @param range Length; maximum range of info to consider, from the start of the given lane.
75       * @param laneAccessLaw LaneAccessLaw; lane access law.
76       * @return ImmutableSortedSet&lt;LaneChangeInfo&gt;; lane change info from the given lane, or {@code null} if no path
77       *         exists.
78       */
79      public ImmutableSortedSet<LaneChangeInfo> getLaneChangeInfo(final Lane lane, final Route route, final GtuType gtuType,
80              final Length range, final LaneAccessLaw laneAccessLaw)
81      {
82          Throw.whenNull(lane, "Lane may not be null.");
83          Throw.whenNull(route, "Route may not be null.");
84          Throw.whenNull(gtuType, "GTU type may not be null.");
85          Throw.whenNull(range, "Range may not be null.");
86          Throw.whenNull(laneAccessLaw, "Lane access law may not be null.");
87          Throw.when(range.le0(), IllegalArgumentException.class, "Range should be a positive value.");
88  
89          // get the complete info
90          SortedSet<LaneChangeInfo> info = getCompleteLaneChangeInfo(lane, route, gtuType, laneAccessLaw);
91          if (info == null)
92          {
93              return null;
94          }
95  
96          // find first LaneChangeInfo beyond range, if any
97          LaneChangeInfo lcInfoBeyondHorizon = null;
98          Iterator<LaneChangeInfo> iterator = info.iterator();
99          while (lcInfoBeyondHorizon == null && iterator.hasNext())
100         {
101             LaneChangeInfo lcInfo = iterator.next();
102             if (lcInfo.getRemainingDistance().gt(range))
103             {
104                 lcInfoBeyondHorizon = lcInfo;
105             }
106         }
107 
108         // return subset in range
109         if (lcInfoBeyondHorizon != null)
110         {
111             return new ImmutableTreeSet<>(info.headSet(lcInfoBeyondHorizon));
112         }
113         return new ImmutableTreeSet<>(info); // empty, or all in range
114     }
115 
116     /**
117      * Returns the complete (i.e. without range) lane change info from the given lane. It is either taken from cache, or
118      * created.
119      * @param lane Lane; from lane.
120      * @param route Route; route.
121      * @param gtuType GtuType; GTU Type.
122      * @param laneAccessLaw LaneAccessLaw; lane access law.
123      * @return SortedSet&lt;LaneChangeInfo&gt;; complete (i.e. without range) lane change info from the given lane, or
124      *         {@code null} if no path exists.
125      */
126     private SortedSet<LaneChangeInfo> getCompleteLaneChangeInfo(final Lane lane, final Route route, final GtuType gtuType,
127             final LaneAccessLaw laneAccessLaw)
128     {
129         // try to get info from the right cache
130         SortedSet<LaneChangeInfo> outputLaneChangeInfo;
131         if (laneAccessLaw.equals(LaneAccessLaw.LEGAL))
132         {
133             outputLaneChangeInfo = this.legalLaneChangeInfoCache.get(gtuType, route, lane);
134             // build info if required
135             if (outputLaneChangeInfo == null)
136             {
137                 // get the right lane graph for the GTU type, or build it
138                 RouteWeightedGraph graph = this.legalLaneGraph.get(gtuType);
139                 if (graph == null)
140                 {
141                     graph = new RouteWeightedGraph();
142                     this.legalLaneGraph.put(gtuType, graph);
143                     buildGraph(graph, gtuType, laneAccessLaw);
144                 }
145                 List<LaneChangeInfoEdge> path = findPath(lane, graph, gtuType, route);
146 
147                 if (path != null)
148                 {
149                     // derive lane change info from every lane along the path and cache it
150                     boolean originalPath = true;
151                     while (!path.isEmpty())
152                     {
153                         SortedSet<LaneChangeInfo> laneChangeInfo = extractLaneChangeInfo(path);
154                         if (originalPath)
155                         {
156                             outputLaneChangeInfo = laneChangeInfo;
157                             originalPath = false;
158                         }
159                         this.legalLaneChangeInfoCache.put(laneChangeInfo, gtuType, route, path.get(0).getFromLane());
160                         path.remove(0); // next lane
161                     }
162                 }
163             }
164         }
165         else if (laneAccessLaw.equals(LaneAccessLaw.PHYSICAL))
166         {
167             outputLaneChangeInfo = this.physicalLaneChangeInfoCache.get(route, lane);
168             // build info if required
169             if (outputLaneChangeInfo == null)
170             {
171                 // build the lane graph if required
172                 if (this.physicalLaneGraph == null)
173                 {
174                     this.physicalLaneGraph = new RouteWeightedGraph();
175                     // TODO: Is the GTU type actually relevant for physical? It is used still to find adjacent lanes.
176                     buildGraph(this.physicalLaneGraph, gtuType, laneAccessLaw);
177                 }
178                 List<LaneChangeInfoEdge> path = findPath(lane, this.physicalLaneGraph, gtuType, route);
179 
180                 if (path != null)
181                 {
182                     // derive lane change info from every lane along the path and cache it
183                     boolean originalPath = true;
184                     while (!path.isEmpty())
185                     {
186                         SortedSet<LaneChangeInfo> laneChangeInfo = extractLaneChangeInfo(path);
187                         if (originalPath)
188                         {
189                             outputLaneChangeInfo = laneChangeInfo;
190                             originalPath = false;
191                         }
192                         this.physicalLaneChangeInfoCache.put(laneChangeInfo, route, path.get(0).getFromLane());
193                         path.remove(0); // next lane
194                     }
195                 }
196             }
197         }
198         else
199         {
200             // in case it is inadvertently extended in the future
201             throw new RuntimeException(String.format("Unknown LaneChangeLaw %s", laneAccessLaw));
202         }
203         return outputLaneChangeInfo;
204     }
205 
206     /**
207      * Builds the graph.
208      * @param graph RouteWeightedGraph; empty graph to build.
209      * @param gtuType GtuType; GTU type.
210      * @param laneChangeLaw LaneChangeLaw; lane change law, legal or physical.
211      */
212     private void buildGraph(final RouteWeightedGraph graph, final GtuType gtuType, final LaneAccessLaw laneChangeLaw)
213     {
214         // add vertices
215         for (Link link : this.getLinkMap().values())
216         {
217             for (Lane lane : ((CrossSectionLink) link).getLanes())
218             {
219                 graph.addVertex(lane);
220             }
221             // each end node may be a destination for the shortest path search
222             graph.addVertex(link.getEndNode());
223         }
224 
225         // add edges
226         boolean legal = laneChangeLaw.equals(LaneAccessLaw.LEGAL);
227         for (Link link : this.getLinkMap().values())
228         {
229             for (Lane lane : ((CrossSectionLink) link).getLanes())
230             {
231                 // adjacent lanes
232                 for (LateralDirectionality lat : List.of(LateralDirectionality.LEFT, LateralDirectionality.RIGHT))
233                 {
234                     Set<Lane> adjacentLanes;
235                     if (legal)
236                     {
237                         adjacentLanes = lane.accessibleAdjacentLanesLegal(lat, gtuType);
238                     }
239                     else
240                     {
241                         adjacentLanes = lane.accessibleAdjacentLanesPhysical(lat, gtuType);
242                     }
243                     for (Lane adjacentLane : adjacentLanes)
244                     {
245                         LaneChangeInfoEdgeType type = lat.equals(LateralDirectionality.LEFT) ? LaneChangeInfoEdgeType.LEFT
246                                 : LaneChangeInfoEdgeType.RIGHT;
247                         // downstream link may be null for lateral edges
248                         LaneChangeInfoEdge edge = new LaneChangeInfoEdge(lane, type, null);
249                         graph.addEdge(lane, adjacentLane, edge);
250                     }
251                 }
252                 // next lanes
253                 Set<Lane> nextLanes = lane.nextLanes(legal ? gtuType : null);
254                 for (Lane nextLane : nextLanes)
255                 {
256                     LaneChangeInfoEdge edge =
257                             new LaneChangeInfoEdge(lane, LaneChangeInfoEdgeType.DOWNSTREAM, nextLane.getParentLink());
258                     graph.addEdge(lane, nextLane, edge);
259                 }
260                 // add edge towards end node so that it can be used as a destination in the shortest path search
261                 LaneChangeInfoEdge edge = new LaneChangeInfoEdge(lane, LaneChangeInfoEdgeType.DOWNSTREAM, null);
262                 graph.addEdge(lane, lane.getParentLink().getEndNode(), edge);
263             }
264         }
265     }
266 
267     /**
268      * Returns a set of lane change info, extracted from the graph.
269      * @param lane Lane; from lane.
270      * @param graph RouteWeightedGraph; graph.
271      * @param gtuType GtuType; GTU Type.
272      * @param route Route; route.
273      * @return List&lt;LaneChangeInfoEdge&gt;; path derived from the graph, or {@code null} if there is no path.
274      */
275     private List<LaneChangeInfoEdge> findPath(final Lane lane, final RouteWeightedGraph graph, final GtuType gtuType,
276             final Route route)
277     {
278         // if there is no route, find the destination node by moving down the links (no splits allowed)
279         Node destination = null;
280         Route routeForWeights = route;
281         if (route == null)
282         {
283             destination = graph.getNoRouteDestinationNode(gtuType);
284             try
285             {
286                 routeForWeights = getShortestRouteBetween(gtuType, lane.getParentLink().getStartNode(), destination);
287             }
288             catch (NetworkException exception)
289             {
290                 // this should not happen, as we obtained the destination by moving downstream towards the end of the network
291                 throw new RuntimeException("Could not find route to destination.", exception);
292             }
293         }
294         else
295         {
296             // otherwise, get destination node from route, which is the last node on a link with lanes (i.e. no connector)
297             List<Node> nodes = route.getNodes();
298             for (int i = nodes.size() - 1; i > 0; i--)
299             {
300                 Link link = getLink(nodes.get(i - 1), nodes.get(i));
301                 if (link instanceof CrossSectionLink && !((CrossSectionLink) link).getLanes().isEmpty())
302                 {
303                     destination = nodes.get(i);
304                     break; // found most downstream link with lanes, who's end node is the destination for lane changes
305                 }
306             }
307             Throw.whenNull(destination, "Route has no links with lanes, "
308                     + "unable to find a suitable destination node regarding lane change information.");
309         }
310 
311         // set the route on the path for route-dependent edge weights
312         graph.setRoute(routeForWeights);
313 
314         // find the shortest path
315         GraphPath<Identifiable, LaneChangeInfoEdge> path = DijkstraShortestPath.findPathBetween(graph, lane, destination);
316         return path == null ? null : path.getEdgeList();
317     }
318 
319     /**
320      * Extracts lane change info from a path.
321      * @param path List&lt;LaneChangeInfoEdge&gt;; path.
322      * @return SortedSet&lt;LaneChangeInfo&gt;; lane change info.
323      */
324     private SortedSet<LaneChangeInfo> extractLaneChangeInfo(final List<LaneChangeInfoEdge> path)
325     {
326         SortedSet<LaneChangeInfo> info = new TreeSet<>();
327         Length x = Length.ZERO; // cumulative longitudinal distance
328         int n = 0; // number of applied lane changes
329         boolean inLateralState = false; // consecutive lateral moves in the path create 1 LaneChangeInfo
330         for (LaneChangeInfoEdge edge : path)
331         {
332             LaneChangeInfoEdgeType lcType = edge.getLaneChangeInfoEdgeType();
333             int lat = lcType.equals(LaneChangeInfoEdgeType.LEFT) ? -1 : (lcType.equals(LaneChangeInfoEdgeType.RIGHT) ? 1 : 0);
334 
335             // check opposite lateral direction
336             if (n * lat < 0)
337             {
338                 /*
339                  * The required direction is opposite a former required direction, in which case all further lane change
340                  * information is not yet of concern. For example, we first need to make 1 right lane change for a lane drop,
341                  * and then later 2 lane changes to the left for a split. The latter information is pointless before the lane
342                  * drop; we are not going to stay on the lane longer as it won't affect the ease of the left lane changes later.
343                  */
344                 break;
345             }
346 
347             // increase n, x, and trigger (consecutive) lateral move start or stop
348             if (lat == 0)
349             {
350                 // lateral move stop
351                 if (inLateralState)
352                 {
353                     // TODO: isDeadEnd should be removed from LaneChangeInfo, behavior should consider legal vs. physical
354                     boolean isDeadEnd = false;
355                     info.add(new LaneChangeInfo(Math.abs(n), x, isDeadEnd,
356                             n < 0 ? LateralDirectionality.LEFT : LateralDirectionality.RIGHT));
357                     inLateralState = false;
358                     // don't add the length of the previous lane, that was already done for the first lane of all lateral moves
359                 }
360                 else
361                 {
362                     // longitudinal move, we need to add distance to x
363                     x = x.plus(edge.getFromLane().getLength());
364                 }
365             }
366             else
367             {
368                 // lateral move start
369                 if (!inLateralState)
370                 {
371                     x = x.plus(edge.getFromLane().getLength()); // need to add length of first lane of all lateral moves
372                     inLateralState = true;
373                 }
374                 // increase lane change count (negative for left)
375                 n += lat;
376             }
377         }
378         return info;
379     }
380 
381     /**
382      * Clears all lane change info graphs and cached sets. This method should be invoked on every network change that affects
383      * lane changes and the distances within which they need to be performed.
384      */
385     public void clearLaneChangeInfoCache()
386     {
387         this.legalLaneGraph.clear();
388         this.physicalLaneGraph = null;
389         this.legalLaneChangeInfoCache = new MultiKeyMap<>(GtuType.class, Route.class, Lane.class);
390         this.physicalLaneChangeInfoCache = new MultiKeyMap<>(Route.class, Lane.class);
391     }
392 
393     /**
394      * A {@code SimpleDirectedWeightedGraph} to search over the lanes, where the weight of an edge (movement between lanes) is
395      * tailored to providing lane change information. The vertex type is {@code Identifiable} such that both {@code Lane}'s and
396      * {@code Node}'s can be used. The latter is required to find paths towards a destination node.<br>
397      * <br>
398      * Copyright (c) 2022-2023 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
399      * See for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project
400      * is distributed under a three-clause BSD-style license, which can be found at
401      * <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</a>. <br>
402      * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
403      */
404     private class RouteWeightedGraph extends SimpleDirectedWeightedGraph<Identifiable, LaneChangeInfoEdge>
405     {
406 
407         /** */
408         private static final long serialVersionUID = 20220923L;
409 
410         /** Route. */
411         private Route route;
412 
413         /** Node in the network that is the destination if no route is used. */
414         private Node noRouteDestination = null;
415 
416         /**
417          * Constructor.
418          */
419         RouteWeightedGraph()
420         {
421             super(LaneChangeInfoEdge.class);
422         }
423 
424         /**
425          * Set the route.
426          * @param route Route; route.
427          */
428         public void setRoute(final Route route)
429         {
430             Throw.whenNull(route, "Route may not be null for lane change information.");
431             this.route = route;
432         }
433 
434         /**
435          * Returns the weight of moving from one lane to the next. In order to find the latest possible location at which lane
436          * changes may still be performed, the longitudinal weights are 1.0 while the lateral weights are 1.0 + 1/X, where X is
437          * the number (index) of the link within the route. This favors later lane changes for the shortest path algorithm, as
438          * we are interested in the distances within which the lane change have to be performed. In the case an edge is towards
439          * a link that is not in a given route, a positive infinite weight is returned. Finally, when the edge is towards a
440          * node, which may be the destination in a route, 0.0 is returned.
441          */
442         @Override
443         public double getEdgeWeight(final LaneChangeInfoEdge e)
444         {
445             if (e.getLaneChangeInfoEdgeType().equals(LaneChangeInfoEdgeType.LEFT)
446                     || e.getLaneChangeInfoEdgeType().equals(LaneChangeInfoEdgeType.RIGHT))
447             {
448                 int indexEndNode = this.route.indexOf(e.getFromLane().getParentLink().getEndNode());
449                 return 1.0 + 1.0 / indexEndNode; // lateral, reduce weight for further lane changes
450             }
451             Link toLink = e.getToLink();
452             if (toLink == null)
453             {
454                 return 0.0; // edge towards Node, which may be the destination in a Route
455             }
456             if (this.route.contains(toLink.getEndNode())
457                     && this.route.indexOf(toLink.getEndNode()) == this.route.indexOf(toLink.getStartNode()) + 1)
458             {
459                 return 1.0; // downstream, always 1.0 if the next lane is on the route
460             }
461             return Double.POSITIVE_INFINITY; // next lane not on the route, this is a dead-end branch for the route
462         }
463 
464         /**
465          * Returns the destination node to use when no route is available. This will be the last node found moving downstream.
466          * @param gtuType GtuType; GTU type.
467          * @return Node; destination node to use when no route is available.
468          */
469         public Node getNoRouteDestinationNode(final GtuType gtuType)
470         {
471             if (this.noRouteDestination == null)
472             {
473                 // get any lane from the network
474                 Lane lane = null;
475                 Iterator<Identifiable> iterator = this.vertexSet().iterator();
476                 while (lane == null && iterator.hasNext())
477                 {
478                     Identifiable next = iterator.next();
479                     if (next instanceof Lane)
480                     {
481                         lane = (Lane) next;
482                     }
483                 }
484                 Throw.when(lane == null, RuntimeException.class, "Requesting destination node on network without lanes.");
485                 // move to downstream link for as long as there is 1 downstream link
486                 try
487                 {
488                     Link link = lane.getParentLink();
489                     Set<Link> downstreamLinks = link.getEndNode().nextLinks(gtuType, link);
490                     while (downstreamLinks.size() == 1)
491                     {
492                         link = downstreamLinks.iterator().next();
493                         downstreamLinks = link.getEndNode().nextLinks(gtuType, link);
494                     }
495                     Throw.when(downstreamLinks.size() > 1, RuntimeException.class, "Using null route on network with split. "
496                             + "Unable to find a destination to find lane change info towards.");
497                     this.noRouteDestination = link.getEndNode();
498                 }
499                 catch (NetworkException ne)
500                 {
501                     throw new RuntimeException("Requesting lane change info from link that does not allow the GTU type.", ne);
502                 }
503             }
504             return this.noRouteDestination;
505         }
506     }
507 
508     /**
509      * Edge between two lanes, or between a lane and a node (to provide the shortest path algorithm with a suitable
510      * destination). From a list of these from a path, the lane change information along the path (distances and number of lane
511      * changes) can be derived.<br>
512      * <br>
513      * Copyright (c) 2022-2023 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
514      * See for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project
515      * is distributed under a three-clause BSD-style license, which can be found at
516      * <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</a>. <br>
517      * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
518      */
519     private static class LaneChangeInfoEdge
520     {
521         /** From lane, to allow construction of distances from a path. */
522         private final Lane fromLane;
523 
524         /** The type of lane to lane movement performed along this edge. */
525         private final LaneChangeInfoEdgeType laneChangeInfoEdgeType;
526 
527         /** To link (of the lane this edge moves to). */
528         private final Link toLink;
529 
530         /**
531          * Constructor.
532          * @param fromLane Lane; lane this edge is from.
533          * @param laneChangeInfoEdgeType LaneChangeInfoEdgeType; type of lane to lane movement performed along this edge.
534          * @param toLink Link; to link of target lane (if any, may be {@code null}).
535          */
536         LaneChangeInfoEdge(final Lane fromLane, final LaneChangeInfoEdgeType laneChangeInfoEdgeType, final Link toLink)
537         {
538             this.fromLane = fromLane;
539             this.laneChangeInfoEdgeType = laneChangeInfoEdgeType;
540             this.toLink = toLink;
541         }
542 
543         /**
544          * Returns the from lane to allow construction of distances from a path.
545          * @return Lane; from lane.
546          */
547         public Lane getFromLane()
548         {
549             return this.fromLane;
550         }
551 
552         /**
553          * Returns the type of lane to lane movement performed along this edge.
554          * @return LaneChangeInfoEdgeType; type of lane to lane movement performed along this edge.
555          */
556         public LaneChangeInfoEdgeType getLaneChangeInfoEdgeType()
557         {
558             return this.laneChangeInfoEdgeType;
559         }
560 
561         /**
562          * Returns the to link.
563          * @return Link; to link of target lane (if any, may be {@code null})
564          */
565         public Link getToLink()
566         {
567             return this.toLink;
568         }
569 
570         @Override
571         public String toString()
572         {
573             return "LaneChangeInfoEdge [fromLane=" + this.fromLane + "]";
574         }
575 
576     }
577 
578     /**
579      * Enum to provide information on the lane to lane movement in a path.<br>
580      * <br>
581      * Copyright (c) 2022-2023 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
582      * See for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project
583      * is distributed under a three-clause BSD-style license, which can be found at
584      * <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</a>. <br>
585      * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
586      */
587     private enum LaneChangeInfoEdgeType
588     {
589         /** Left lane change. */
590         LEFT,
591 
592         /** Right lane change. */
593         RIGHT,
594 
595         /** Downstream movement, either towards a lane, or towards a node (which may be the destination in a route). */
596         DOWNSTREAM;
597     }
598 
599 }