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