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