All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opentrafficsim.road.network.RoadNetwork Maven / Gradle / Ivy

The newest version!
package org.opentrafficsim.road.network;

import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.djunits.value.vdouble.scalar.Length;
import org.djutils.base.Identifiable;
import org.djutils.exceptions.Throw;
import org.djutils.immutablecollections.ImmutableSortedSet;
import org.djutils.immutablecollections.ImmutableTreeSet;
import org.djutils.multikeymap.MultiKeyMap;
import org.jgrapht.GraphPath;
import org.jgrapht.alg.shortestpath.DijkstraShortestPath;
import org.jgrapht.graph.SimpleDirectedWeightedGraph;
import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
import org.opentrafficsim.core.gtu.GtuType;
import org.opentrafficsim.core.network.LateralDirectionality;
import org.opentrafficsim.core.network.Link;
import org.opentrafficsim.core.network.Network;
import org.opentrafficsim.core.network.NetworkException;
import org.opentrafficsim.core.network.Node;
import org.opentrafficsim.core.network.route.Route;
import org.opentrafficsim.road.network.lane.CrossSectionLink;
import org.opentrafficsim.road.network.lane.Lane;

/**
 * RoadNetwork adds the ability to retrieve lane change information.
 * 

* Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
* BSD-style license. See OpenTrafficSim License. *

* @author Alexander Verbraeck * @author Wouter Schakel */ public class RoadNetwork extends Network { /** */ private static final long serialVersionUID = 1L; /** Cached lane graph for legal connections, per GTU type. */ private Map legalLaneGraph = new LinkedHashMap<>(); /** Cached lane graph for physical connections. */ private RouteWeightedGraph physicalLaneGraph = null; /** Cached legal lane change info, over complete length of route. */ private MultiKeyMap> legalLaneChangeInfoCache = new MultiKeyMap<>(GtuType.class, Route.class, Lane.class); /** Cached physical lane change info, over complete length of route. */ private MultiKeyMap> physicalLaneChangeInfoCache = new MultiKeyMap<>(Route.class, Lane.class); /** * Construction of an empty network. * @param id String; the network id. * @param simulator OtsSimulatorInterface; the DSOL simulator engine */ public RoadNetwork(final String id, final OtsSimulatorInterface simulator) { super(id, simulator); } /** * Returns lane change info from the given lane. Distances are given from the start of the lane and will never exceed the * given range. This method returns {@code null} if no valid path exists. If there are no reasons to change lane within * range, an empty set is returned. * @param lane Lane; from lane. * @param route Route; route. * @param gtuType GtuType; GTU Type. * @param range Length; maximum range of info to consider, from the start of the given lane. * @param laneAccessLaw LaneAccessLaw; lane access law. * @return ImmutableSortedSet<LaneChangeInfo>; lane change info from the given lane, or {@code null} if no path * exists. */ public ImmutableSortedSet getLaneChangeInfo(final Lane lane, final Route route, final GtuType gtuType, final Length range, final LaneAccessLaw laneAccessLaw) { Throw.whenNull(lane, "Lane may not be null."); Throw.whenNull(route, "Route may not be null."); Throw.whenNull(gtuType, "GTU type may not be null."); Throw.whenNull(range, "Range may not be null."); Throw.whenNull(laneAccessLaw, "Lane access law may not be null."); Throw.when(range.le0(), IllegalArgumentException.class, "Range should be a positive value."); // get the complete info SortedSet info = getCompleteLaneChangeInfo(lane, route, gtuType, laneAccessLaw); if (info == null) { return null; } // find first LaneChangeInfo beyond range, if any LaneChangeInfo lcInfoBeyondHorizon = null; Iterator iterator = info.iterator(); while (lcInfoBeyondHorizon == null && iterator.hasNext()) { LaneChangeInfo lcInfo = iterator.next(); if (lcInfo.remainingDistance().gt(range)) { lcInfoBeyondHorizon = lcInfo; } } // return subset in range if (lcInfoBeyondHorizon != null) { return new ImmutableTreeSet<>(info.headSet(lcInfoBeyondHorizon)); } return new ImmutableTreeSet<>(info); // empty, or all in range } /** * Returns the complete (i.e. without range) lane change info from the given lane. It is either taken from cache, or * created. * @param lane Lane; from lane. * @param route Route; route. * @param gtuType GtuType; GTU Type. * @param laneAccessLaw LaneAccessLaw; lane access law. * @return SortedSet<LaneChangeInfo>; complete (i.e. without range) lane change info from the given lane, or * {@code null} if no path exists. */ private SortedSet getCompleteLaneChangeInfo(final Lane lane, final Route route, final GtuType gtuType, final LaneAccessLaw laneAccessLaw) { // try to get info from the right cache SortedSet outputLaneChangeInfo; if (laneAccessLaw.equals(LaneAccessLaw.LEGAL)) { outputLaneChangeInfo = this.legalLaneChangeInfoCache.get(gtuType, route, lane); // build info if required if (outputLaneChangeInfo == null) { // get the right lane graph for the GTU type, or build it RouteWeightedGraph graph = this.legalLaneGraph.get(gtuType); if (graph == null) { graph = new RouteWeightedGraph(); this.legalLaneGraph.put(gtuType, graph); buildGraph(graph, gtuType, laneAccessLaw); } List path = findPath(lane, graph, gtuType, route); if (path != null) { // derive lane change info from every lane along the path and cache it boolean originalPath = true; while (!path.isEmpty()) { SortedSet laneChangeInfo = extractLaneChangeInfo(path); if (originalPath) { outputLaneChangeInfo = laneChangeInfo; originalPath = false; } this.legalLaneChangeInfoCache.put(laneChangeInfo, gtuType, route, path.get(0).fromLane()); path.remove(0); // next lane } } } } else if (laneAccessLaw.equals(LaneAccessLaw.PHYSICAL)) { outputLaneChangeInfo = this.physicalLaneChangeInfoCache.get(route, lane); // build info if required if (outputLaneChangeInfo == null) { // build the lane graph if required if (this.physicalLaneGraph == null) { this.physicalLaneGraph = new RouteWeightedGraph(); // TODO: Is the GTU type actually relevant for physical? It is used still to find adjacent lanes. buildGraph(this.physicalLaneGraph, gtuType, laneAccessLaw); } List path = findPath(lane, this.physicalLaneGraph, gtuType, route); if (path != null) { // derive lane change info from every lane along the path and cache it boolean originalPath = true; while (!path.isEmpty()) { SortedSet laneChangeInfo = extractLaneChangeInfo(path); if (originalPath) { outputLaneChangeInfo = laneChangeInfo; originalPath = false; } this.physicalLaneChangeInfoCache.put(laneChangeInfo, route, path.get(0).fromLane()); path.remove(0); // next lane } } } } else { // in case it is inadvertently extended in the future throw new RuntimeException(String.format("Unknown LaneChangeLaw %s", laneAccessLaw)); } return outputLaneChangeInfo; } /** * Builds the graph. * @param graph RouteWeightedGraph; empty graph to build. * @param gtuType GtuType; GTU type. * @param laneChangeLaw LaneChangeLaw; lane change law, legal or physical. */ private void buildGraph(final RouteWeightedGraph graph, final GtuType gtuType, final LaneAccessLaw laneChangeLaw) { // add vertices boolean legal = laneChangeLaw.equals(LaneAccessLaw.LEGAL); for (Link link : this.getLinkMap().values()) { for (Lane lane : legal ? ((CrossSectionLink) link).getLanes() : ((CrossSectionLink) link).getLanesAndShoulders()) { graph.addVertex(lane); } // each end node may be a destination for the shortest path search graph.addVertex(link.getEndNode()); } // add edges for (Link link : this.getLinkMap().values()) { if (link instanceof CrossSectionLink cLink) { for (Lane lane : legal ? cLink.getLanes() : cLink.getLanesAndShoulders()) { // adjacent lanes for (LateralDirectionality lat : List.of(LateralDirectionality.LEFT, LateralDirectionality.RIGHT)) { Set adjacentLanes; if (legal) { adjacentLanes = lane.accessibleAdjacentLanesLegal(lat, gtuType); } else { adjacentLanes = lane.accessibleAdjacentLanesPhysical(lat, gtuType); } for (Lane adjacentLane : adjacentLanes) { LaneChangeInfoEdgeType type = lat.equals(LateralDirectionality.LEFT) ? LaneChangeInfoEdgeType.LEFT : LaneChangeInfoEdgeType.RIGHT; // downstream link may be null for lateral edges LaneChangeInfoEdge edge = new LaneChangeInfoEdge(lane, type, null); graph.addEdge(lane, adjacentLane, edge); } } // next lanes Set nextLanes = lane.nextLanes(legal ? gtuType : null); for (Lane nextLane : nextLanes) { LaneChangeInfoEdge edge = new LaneChangeInfoEdge(lane, LaneChangeInfoEdgeType.DOWNSTREAM, nextLane.getLink()); graph.addEdge(lane, nextLane, edge); } // add edge towards end node so that it can be used as a destination in the shortest path search LaneChangeInfoEdge edge = new LaneChangeInfoEdge(lane, LaneChangeInfoEdgeType.DOWNSTREAM, null); graph.addEdge(lane, lane.getLink().getEndNode(), edge); } } } } /** * Returns a set of lane change info, extracted from the graph. * @param lane Lane; from lane. * @param graph RouteWeightedGraph; graph. * @param gtuType GtuType; GTU Type. * @param route Route; route. * @return List<LaneChangeInfoEdge>; path derived from the graph, or {@code null} if there is no path. */ private List findPath(final Lane lane, final RouteWeightedGraph graph, final GtuType gtuType, final Route route) { // if there is no route, find the destination node by moving down the links (no splits allowed) Node destination = null; Route routeForWeights = route; if (route == null) { destination = graph.getNoRouteDestinationNode(gtuType); try { routeForWeights = getShortestRouteBetween(gtuType, lane.getLink().getStartNode(), destination); } catch (NetworkException exception) { // this should not happen, as we obtained the destination by moving downstream towards the end of the network throw new RuntimeException("Could not find route to destination.", exception); } } else { // otherwise, get destination node from route, which is the last node on a link with lanes (i.e. no connector) List nodes = route.getNodes(); for (int i = nodes.size() - 1; i > 0; i--) { Link link = getLink(nodes.get(i - 1), nodes.get(i)); if (link instanceof CrossSectionLink && !((CrossSectionLink) link).getLanes().isEmpty()) { destination = nodes.get(i); break; // found most downstream link with lanes, who's end node is the destination for lane changes } } Throw.whenNull(destination, "Route has no links with lanes, " + "unable to find a suitable destination node regarding lane change information."); } // set the route on the path for route-dependent edge weights graph.setRoute(routeForWeights); // find the shortest path GraphPath path = DijkstraShortestPath.findPathBetween(graph, lane, destination); return path == null ? null : path.getEdgeList(); } /** * Extracts lane change info from a path. * @param path List<LaneChangeInfoEdge>; path. * @return SortedSet<LaneChangeInfo>; lane change info. */ private SortedSet extractLaneChangeInfo(final List path) { SortedSet info = new TreeSet<>(); Length x = Length.ZERO; // cumulative longitudinal distance int n = 0; // number of applied lane changes boolean inLateralState = false; // consecutive lateral moves in the path create 1 LaneChangeInfo for (LaneChangeInfoEdge edge : path) { LaneChangeInfoEdgeType lcType = edge.laneChangeInfoEdgeType(); int lat = lcType.equals(LaneChangeInfoEdgeType.LEFT) ? -1 : (lcType.equals(LaneChangeInfoEdgeType.RIGHT) ? 1 : 0); // check opposite lateral direction if (n * lat < 0) { /* * The required direction is opposite a former required direction, in which case all further lane change * information is not yet of concern. For example, we first need to make 1 right lane change for a lane drop, * and then later 2 lane changes to the left for a split. The latter information is pointless before the lane * drop; we are not going to stay on the lane longer as it won't affect the ease of the left lane changes later. */ break; } // increase n, x, and trigger (consecutive) lateral move start or stop if (lat == 0) { // lateral move stop if (inLateralState) { // TODO: isDeadEnd should be removed from LaneChangeInfo, behavior should consider legal vs. physical boolean isDeadEnd = false; info.add(new LaneChangeInfo(Math.abs(n), x, isDeadEnd, n < 0 ? LateralDirectionality.LEFT : LateralDirectionality.RIGHT)); inLateralState = false; // don't add the length of the previous lane, that was already done for the first lane of all lateral moves } else { // longitudinal move, we need to add distance to x x = x.plus(edge.fromLane().getLength()); } } else { // lateral move start if (!inLateralState) { x = x.plus(edge.fromLane().getLength()); // need to add length of first lane of all lateral moves inLateralState = true; } // increase lane change count (negative for left) n += lat; } } return info; } /** * Clears all lane change info graphs and cached sets. This method should be invoked on every network change that affects * lane changes and the distances within which they need to be performed. */ public void clearLaneChangeInfoCache() { this.legalLaneGraph.clear(); this.physicalLaneGraph = null; this.legalLaneChangeInfoCache = new MultiKeyMap<>(GtuType.class, Route.class, Lane.class); this.physicalLaneChangeInfoCache = new MultiKeyMap<>(Route.class, Lane.class); } /** * A {@code SimpleDirectedWeightedGraph} to search over the lanes, where the weight of an edge (movement between lanes) is * tailored to providing lane change information. The vertex type is {@code Identifiable} such that both {@code Lane}'s and * {@code Node}'s can be used. The latter is required to find paths towards a destination node. *

* Copyright (c) 2022-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. *
* BSD-style license. See OpenTrafficSim License. *

* @author Wouter Schakel */ private class RouteWeightedGraph extends SimpleDirectedWeightedGraph { /** */ private static final long serialVersionUID = 20220923L; /** Route. */ private Route route; /** Node in the network that is the destination if no route is used. */ private Node noRouteDestination = null; /** * Constructor. */ RouteWeightedGraph() { super(LaneChangeInfoEdge.class); } /** * Set the route. * @param route Route; route. */ public void setRoute(final Route route) { Throw.whenNull(route, "Route may not be null for lane change information."); this.route = route; } /** * Returns the weight of moving from one lane to the next. In order to find the latest possible location at which lane * changes may still be performed, the longitudinal weights are 1.0 while the lateral weights are 1.0 + 1/X, where X is * the number (index) of the link within the route. This favors later lane changes for the shortest path algorithm, as * we are interested in the distances within which the lane change have to be performed. In the case an edge is towards * a link that is not in a given route, a positive infinite weight is returned. Finally, when the edge is towards a * node, which may be the destination in a route, 0.0 is returned. */ @Override public double getEdgeWeight(final LaneChangeInfoEdge e) { if (e.laneChangeInfoEdgeType().equals(LaneChangeInfoEdgeType.LEFT) || e.laneChangeInfoEdgeType().equals(LaneChangeInfoEdgeType.RIGHT)) { int indexEndNode = this.route.indexOf(e.fromLane().getLink().getEndNode()); return 1.0 + 1.0 / indexEndNode; // lateral, reduce weight for further lane changes } Link toLink = e.toLink(); if (toLink == null) { return 0.0; // edge towards Node, which may be the destination in a Route } if (this.route.contains(toLink.getEndNode()) && this.route.indexOf(toLink.getEndNode()) == this.route.indexOf(toLink.getStartNode()) + 1) { return 1.0; // downstream, always 1.0 if the next lane is on the route } return Double.POSITIVE_INFINITY; // next lane not on the route, this is a dead-end branch for the route } /** * Returns the destination node to use when no route is available. This will be the last node found moving downstream. * @param gtuType GtuType; GTU type. * @return Node; destination node to use when no route is available. */ public Node getNoRouteDestinationNode(final GtuType gtuType) { if (this.noRouteDestination == null) { // get any lane from the network Lane lane = null; Iterator iterator = this.vertexSet().iterator(); while (lane == null && iterator.hasNext()) { Identifiable next = iterator.next(); if (next instanceof Lane) { lane = (Lane) next; } } Throw.when(lane == null, RuntimeException.class, "Requesting destination node on network without lanes."); // move to downstream link for as long as there is 1 downstream link try { Link link = lane.getLink(); Set downstreamLinks = link.getEndNode().nextLinks(gtuType, link); while (downstreamLinks.size() == 1) { link = downstreamLinks.iterator().next(); downstreamLinks = link.getEndNode().nextLinks(gtuType, link); } Throw.when(downstreamLinks.size() > 1, RuntimeException.class, "Using null route on network with split. " + "Unable to find a destination to find lane change info towards."); this.noRouteDestination = link.getEndNode(); } catch (NetworkException ne) { throw new RuntimeException("Requesting lane change info from link that does not allow the GTU type.", ne); } } return this.noRouteDestination; } } /** * Edge between two lanes, or between a lane and a node (to provide the shortest path algorithm with a suitable * destination). From a list of these from a path, the lane change information along the path (distances and number of lane * changes) can be derived. *

* Copyright (c) 2022-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. *
* BSD-style license. See OpenTrafficSim License. *

* @author Wouter Schakel * @param fromLane Lane; from lane, to allow construction of distances from a path. * @param laneChangeInfoEdgeType LaneChangeInfoEdgeType; the type of lane to lane movement performed along this edge. * @param toLink Link; to link (of the lane this edge moves to). */ private static record LaneChangeInfoEdge(Lane fromLane, LaneChangeInfoEdgeType laneChangeInfoEdgeType, Link toLink) { } /** * Enum to provide information on the lane to lane movement in a path. *

* Copyright (c) 2022-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. *
* BSD-style license. See OpenTrafficSim License. *

* @author Wouter Schakel */ private enum LaneChangeInfoEdgeType { /** Left lane change. */ LEFT, /** Right lane change. */ RIGHT, /** Downstream movement, either towards a lane, or towards a node (which may be the destination in a route). */ DOWNSTREAM; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy