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
31 /**
32 * RoadNetwork adds the ability to retrieve lane change information.
33 * <p>
34 * Copyright (c) 2013-2026 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 Alexander Verbraeck
38 * @author Wouter Schakel
39 */
40 public class RoadNetwork extends Network
41 {
42 /** Cached lane graph for legal connections, per GTU type. */
43 private Map<GtuType, RouteWeightedGraph> legalLaneGraph = new LinkedHashMap<>();
44
45 /** Cached lane graph for physical connections. */
46 private RouteWeightedGraph physicalLaneGraph = null;
47
48 /** Cached legal lane change info, over complete length of route. */
49 private MultiKeyMap<SortedSet<LaneChangeInfo>> legalLaneChangeInfoCache =
50 new MultiKeyMap<>(GtuType.class, Route.class, Lane.class);
51
52 /** Cached physical lane change info, over complete length of route. */
53 private MultiKeyMap<SortedSet<LaneChangeInfo>> physicalLaneChangeInfoCache = new MultiKeyMap<>(Route.class, Lane.class);
54
55 /**
56 * Construction of an empty network.
57 * @param id the network id.
58 * @param simulator the DSOL simulator engine
59 */
60 public RoadNetwork(final String id, final OtsSimulatorInterface simulator)
61 {
62 super(id, simulator);
63 }
64
65 /**
66 * Returns lane change info from the given lane. Distances are given from the start of the lane and will never exceed the
67 * given range. This method returns {@code null} if no valid path exists. If there are no reasons to change lane within
68 * range, an empty set is returned.
69 * @param lane from lane.
70 * @param route route.
71 * @param gtuType GTU Type.
72 * @param range maximum range of info to consider, from the start of the given lane.
73 * @param laneAccessLaw lane access law.
74 * @return lane change info from the given lane, or empty if no path exists.
75 */
76 public ImmutableSortedSet<LaneChangeInfo> getLaneChangeInfo(final Lane lane, final Route route, final GtuType gtuType,
77 final Length range, final LaneAccessLaw laneAccessLaw)
78 {
79 Throw.whenNull(lane, "Lane may not be null.");
80 Throw.whenNull(route, "Route may not be null.");
81 Throw.whenNull(gtuType, "GTU type may not be null.");
82 Throw.whenNull(range, "Range may not be null.");
83 Throw.whenNull(laneAccessLaw, "Lane access law may not be null.");
84 Throw.when(range.le0(), IllegalArgumentException.class, "Range should be a positive value.");
85
86 // get the complete info
87 SortedSet<LaneChangeInfo> info = getCompleteLaneChangeInfo(lane, route, gtuType, laneAccessLaw);
88 if (info == null)
89 {
90 return new ImmutableTreeSet<>(Collections.emptySet());
91 }
92
93 // find first LaneChangeInfo beyond range, if any
94 LaneChangeInfo lcInfoBeyondHorizon = null;
95 Iterator<LaneChangeInfo> iterator = info.iterator();
96 while (lcInfoBeyondHorizon == null && iterator.hasNext())
97 {
98 LaneChangeInfo lcInfo = iterator.next();
99 if (lcInfo.remainingDistance().gt(range))
100 {
101 lcInfoBeyondHorizon = lcInfo;
102 }
103 }
104
105 // return subset in range
106 if (lcInfoBeyondHorizon != null)
107 {
108 return new ImmutableTreeSet<>(info.headSet(lcInfoBeyondHorizon));
109 }
110 return new ImmutableTreeSet<>(info); // empty, or all in range
111 }
112
113 /**
114 * Returns the complete (i.e. without range) lane change info from the given lane. It is either taken from cache, or
115 * created.
116 * @param lane from lane.
117 * @param route route.
118 * @param gtuType GTU Type.
119 * @param laneAccessLaw lane access law.
120 * @return complete (i.e. without range) lane change info from the given lane, or {@code null} if no path exists.
121 */
122 private SortedSet<LaneChangeInfo> getCompleteLaneChangeInfo(final Lane lane, final Route route, final GtuType gtuType,
123 final LaneAccessLaw laneAccessLaw)
124 {
125 // try to get info from the right cache
126 SortedSet<LaneChangeInfo> outputLaneChangeInfo;
127 if (laneAccessLaw.equals(LaneAccessLaw.LEGAL))
128 {
129 outputLaneChangeInfo = this.legalLaneChangeInfoCache.get(gtuType, route, lane);
130 // build info if required
131 if (outputLaneChangeInfo == null)
132 {
133 // get the right lane graph for the GTU type, or build it
134 RouteWeightedGraph graph = this.legalLaneGraph.get(gtuType);
135 if (graph == null)
136 {
137 graph = new RouteWeightedGraph();
138 this.legalLaneGraph.put(gtuType, graph);
139 buildGraph(graph, gtuType, laneAccessLaw);
140 }
141 List<LaneChangeInfoEdge> path = findPath(lane, graph, gtuType, route);
142
143 if (path != null)
144 {
145 // derive lane change info from every lane along the path and cache it
146 boolean originalPath = true;
147 while (!path.isEmpty())
148 {
149 SortedSet<LaneChangeInfo> laneChangeInfo = extractLaneChangeInfo(path);
150 if (originalPath)
151 {
152 outputLaneChangeInfo = laneChangeInfo;
153 originalPath = false;
154 }
155 this.legalLaneChangeInfoCache.put(laneChangeInfo, gtuType, route, path.get(0).fromLane());
156 path.remove(0); // next lane
157 }
158 }
159 }
160 }
161 else if (laneAccessLaw.equals(LaneAccessLaw.PHYSICAL))
162 {
163 outputLaneChangeInfo = this.physicalLaneChangeInfoCache.get(route, lane);
164 // build info if required
165 if (outputLaneChangeInfo == null)
166 {
167 // build the lane graph if required
168 if (this.physicalLaneGraph == null)
169 {
170 this.physicalLaneGraph = new RouteWeightedGraph();
171 // TODO: Is the GTU type actually relevant for physical? It is used still to find adjacent lanes.
172 buildGraph(this.physicalLaneGraph, gtuType, laneAccessLaw);
173 }
174 List<LaneChangeInfoEdge> path = findPath(lane, this.physicalLaneGraph, gtuType, route);
175
176 if (path != null)
177 {
178 // derive lane change info from every lane along the path and cache it
179 boolean originalPath = true;
180 while (!path.isEmpty())
181 {
182 SortedSet<LaneChangeInfo> laneChangeInfo = extractLaneChangeInfo(path);
183 if (originalPath)
184 {
185 outputLaneChangeInfo = laneChangeInfo;
186 originalPath = false;
187 }
188 this.physicalLaneChangeInfoCache.put(laneChangeInfo, route, path.get(0).fromLane());
189 path.remove(0); // next lane
190 }
191 }
192 }
193 }
194 else
195 {
196 // in case it is inadvertently extended in the future
197 throw new OtsRuntimeException(String.format("Unknown LaneChangeLaw %s", laneAccessLaw));
198 }
199 return outputLaneChangeInfo;
200 }
201
202 /**
203 * Builds the graph.
204 * @param graph empty graph to build.
205 * @param gtuType GTU type.
206 * @param laneChangeLaw lane change law, legal or physical.
207 */
208 private void buildGraph(final RouteWeightedGraph graph, final GtuType gtuType, final LaneAccessLaw laneChangeLaw)
209 {
210 // add vertices
211 boolean legal = laneChangeLaw.equals(LaneAccessLaw.LEGAL);
212 for (Link link : this.getLinkMap().values())
213 {
214 for (Lane lane : legal ? ((CrossSectionLink) link).getLanes() : ((CrossSectionLink) link).getLanesAndShoulders())
215 {
216 graph.addVertex(lane);
217 }
218 // each end node may be a destination for the shortest path search
219 graph.addVertex(link.getEndNode());
220 }
221
222 // add edges
223 for (Link link : this.getLinkMap().values())
224 {
225 if (link instanceof CrossSectionLink cLink)
226 {
227 for (Lane lane : legal ? cLink.getLanes() : cLink.getLanesAndShoulders())
228 {
229 // adjacent lanes
230 for (LateralDirectionality lat : List.of(LateralDirectionality.LEFT, LateralDirectionality.RIGHT))
231 {
232 Set<Lane> adjacentLanes;
233 if (legal)
234 {
235 adjacentLanes = lane.accessibleAdjacentLanesLegal(lat, gtuType);
236 }
237 else
238 {
239 adjacentLanes = lane.accessibleAdjacentLanesPhysical(lat, gtuType);
240 }
241 for (Lane adjacentLane : adjacentLanes)
242 {
243 LaneChangeInfoEdgeType type = lat.equals(LateralDirectionality.LEFT) ? LaneChangeInfoEdgeType.LEFT
244 : LaneChangeInfoEdgeType.RIGHT;
245 // downstream link may be null for lateral edges
246 LaneChangeInfoEdge edge = new LaneChangeInfoEdge(lane, type, null);
247 graph.addEdge(lane, adjacentLane, edge);
248 }
249 }
250 // next lanes
251 Set<Lane> nextLanes = lane.nextLanes(legal ? gtuType : null);
252 for (Lane nextLane : nextLanes)
253 {
254 LaneChangeInfoEdge edge =
255 new LaneChangeInfoEdge(lane, LaneChangeInfoEdgeType.DOWNSTREAM, nextLane.getLink());
256 graph.addEdge(lane, nextLane, edge);
257 }
258 // add edge towards end node so that it can be used as a destination in the shortest path search
259 LaneChangeInfoEdge edge = new LaneChangeInfoEdge(lane, LaneChangeInfoEdgeType.DOWNSTREAM, null);
260 graph.addEdge(lane, lane.getLink().getEndNode(), edge);
261 }
262 }
263 }
264 }
265
266 /**
267 * Returns a set of lane change info, extracted from the graph.
268 * @param lane from lane.
269 * @param graph graph.
270 * @param gtuType GTU Type.
271 * @param route route.
272 * @return path derived from the graph, or {@code null} if there is no path.
273 */
274 private List<LaneChangeInfoEdge> findPath(final Lane lane, final RouteWeightedGraph graph, final GtuType gtuType,
275 final Route route)
276 {
277 // if there is no route, find the destination node by moving down the links (no splits allowed)
278 Node destination = null;
279 Route routeForWeights = route;
280 if (route == null)
281 {
282 destination = graph.getNoRouteDestinationNode(gtuType);
283 try
284 {
285 routeForWeights = getShortestRouteBetween(gtuType, lane.getLink().getStartNode(), destination);
286 }
287 catch (NetworkException exception)
288 {
289 // this should not happen, as we obtained the destination by moving downstream towards the end of the network
290 throw new OtsRuntimeException("Could not find route to destination.", exception);
291 }
292 }
293 else
294 {
295 // otherwise, get destination node from route, which is the last node on a link with lanes (i.e. no connector)
296 List<Node> nodes = route.getNodes();
297 for (int i = nodes.size() - 1; i > 0; i--)
298 {
299 Link link = getLink(nodes.get(i - 1), nodes.get(i))
300 .orElseThrow(() -> new OtsRuntimeException("Unable to find link for two consecutive nodes in route."));
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 path.
322 * @return 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.laneChangeInfoEdgeType();
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.fromLane().getLength());
364 }
365 }
366 else
367 {
368 // lateral move start
369 if (!inLateralState)
370 {
371 x = x.plus(edge.fromLane().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.
397 * <p>
398 * Copyright (c) 2022-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
399 * <br>
400 * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
401 * </p>
402 * @author Wouter Schakel
403 */
404 private class RouteWeightedGraph extends SimpleDirectedWeightedGraph<Identifiable, LaneChangeInfoEdge>
405 {
406
407 /** Serialization version UID. */
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.
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.laneChangeInfoEdgeType().equals(LaneChangeInfoEdgeType.LEFT)
446 || e.laneChangeInfoEdgeType().equals(LaneChangeInfoEdgeType.RIGHT))
447 {
448 int indexEndNode = this.route.indexOf(e.fromLane().getLink().getEndNode());
449 return 1.0 + 1.0 / indexEndNode; // lateral, reduce weight for further lane changes
450 }
451 Link toLink = e.toLink();
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 GTU type.
467 * @return 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, OtsRuntimeException.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.getLink();
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, OtsRuntimeException.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 OtsRuntimeException("Requesting lane change info from link that does not allow the GTU type.",
502 ne);
503 }
504 }
505 return this.noRouteDestination;
506 }
507 }
508
509 /**
510 * Edge between two lanes, or between a lane and a node (to provide the shortest path algorithm with a suitable
511 * destination). From a list of these from a path, the lane change information along the path (distances and number of lane
512 * changes) can be derived.
513 * <p>
514 * Copyright (c) 2022-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
515 * <br>
516 * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
517 * </p>
518 * @author Wouter Schakel
519 * @param fromLane from lane, to allow construction of distances from a path.
520 * @param laneChangeInfoEdgeType the type of lane to lane movement performed along this edge.
521 * @param toLink to link (of the lane this edge moves to).
522 */
523 private record LaneChangeInfoEdge(Lane fromLane, LaneChangeInfoEdgeType laneChangeInfoEdgeType, Link toLink)
524 {
525 }
526
527 /**
528 * Enum to provide information on the lane to lane movement in a path.
529 * <p>
530 * Copyright (c) 2022-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
531 * <br>
532 * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
533 * </p>
534 * @author Wouter Schakel
535 */
536 private enum LaneChangeInfoEdgeType
537 {
538 /** Left lane change. */
539 LEFT,
540
541 /** Right lane change. */
542 RIGHT,
543
544 /** Downstream movement, either towards a lane, or towards a node (which may be the destination in a route). */
545 DOWNSTREAM;
546 }
547
548 }