use std::cmp::Ordering;
use std::collections::BinaryHeap;
use rustsim_modes::{AllowedModes, TravelMode};
use rustsim_traffic::{TransportLinkMetadata, TransportLinkOps, TransportLinkSpace};
use thiserror::Error;
pub type NodeId = u64;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ModalEdge {
pub from: NodeId,
pub to: NodeId,
pub cost: f64,
pub modes: AllowedModes,
}
#[derive(Debug, Clone, Default)]
pub struct ModalGraph {
adjacency: hashbrown::HashMap<NodeId, Vec<ModalEdge>>,
}
#[derive(Debug, Clone, PartialEq, Error)]
pub enum ModalGraphBuildError {
#[error("transport metadata references missing link {0}")]
MissingLink(u64),
#[error("transport metadata endpoints for link {link_id} are ({metadata_from}, {metadata_to}) but link space has ({space_from}, {space_to})")]
EndpointMismatch {
link_id: u64,
metadata_from: u64,
metadata_to: u64,
space_from: u64,
space_to: u64,
},
#[error("transport link {link_id} has invalid routing cost {cost}")]
InvalidCost {
link_id: u64,
cost: f64,
},
}
impl ModalGraph {
pub fn new() -> Self {
Self::default()
}
pub fn add_edge(&mut self, edge: ModalEdge) {
self.adjacency.entry(edge.from).or_default().push(edge);
}
pub fn neighbours(&self, node: NodeId) -> &[ModalEdge] {
self.adjacency
.get(&node)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn from_transport_links(
space: &TransportLinkSpace,
metadata: impl IntoIterator<Item = TransportLinkMetadata>,
) -> Result<Self, ModalGraphBuildError> {
let mut graph = Self::new();
for link in metadata {
let link_id = link.edge_id;
let (space_from, space_to) = space
.link_endpoints(link_id)
.ok_or(ModalGraphBuildError::MissingLink(link_id as u64))?;
if (link.from_node, link.to_node) != (space_from, space_to) {
return Err(ModalGraphBuildError::EndpointMismatch {
link_id: link_id as u64,
metadata_from: link.from_node as u64,
metadata_to: link.to_node as u64,
space_from: space_from as u64,
space_to: space_to as u64,
});
}
let cost = space.link_travel_time(link_id);
if !cost.is_finite() || cost < 0.0 {
return Err(ModalGraphBuildError::InvalidCost {
link_id: link_id as u64,
cost,
});
}
graph.add_edge(ModalEdge {
from: space_from as NodeId,
to: space_to as NodeId,
cost,
modes: link.allowed_modes,
});
}
Ok(graph)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ModalRoute {
pub nodes: Vec<NodeId>,
pub total_cost: f64,
}
pub fn shortest_path(
graph: &ModalGraph,
start: NodeId,
goal: NodeId,
available_modes: AllowedModes,
) -> Option<ModalRoute> {
#[derive(Copy, Clone, PartialEq)]
struct State {
cost: f64,
node: NodeId,
}
impl Eq for State {}
impl Ord for State {
fn cmp(&self, other: &Self) -> Ordering {
other
.cost
.partial_cmp(&self.cost)
.unwrap_or(Ordering::Equal)
}
}
impl PartialOrd for State {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
let mut dist: hashbrown::HashMap<NodeId, f64> = hashbrown::HashMap::new();
let mut came_from: hashbrown::HashMap<NodeId, NodeId> = hashbrown::HashMap::new();
let mut heap = BinaryHeap::new();
dist.insert(start, 0.0);
heap.push(State {
cost: 0.0,
node: start,
});
while let Some(State { cost, node }) = heap.pop() {
if node == goal {
return Some(reconstruct(&came_from, start, goal, cost));
}
if let Some(&best) = dist.get(&node) {
if cost > best + 1e-9 {
continue;
}
}
for edge in graph.neighbours(node) {
if !modes_intersect(edge.modes, available_modes) {
continue;
}
let next_cost = cost + edge.cost;
let better = dist
.get(&edge.to)
.map(|&d| next_cost < d - 1e-12)
.unwrap_or(true);
if better {
dist.insert(edge.to, next_cost);
came_from.insert(edge.to, node);
heap.push(State {
cost: next_cost,
node: edge.to,
});
}
}
}
None
}
fn reconstruct(
came_from: &hashbrown::HashMap<NodeId, NodeId>,
start: NodeId,
goal: NodeId,
cost: f64,
) -> ModalRoute {
let mut nodes = vec![goal];
let mut cur = goal;
while cur != start {
if let Some(&prev) = came_from.get(&cur) {
nodes.push(prev);
cur = prev;
} else {
break;
}
}
nodes.reverse();
ModalRoute {
nodes,
total_cost: cost,
}
}
pub fn only(mode: TravelMode) -> AllowedModes {
AllowedModes::none().with_mode(mode)
}
fn modes_intersect(a: AllowedModes, b: AllowedModes) -> bool {
[
TravelMode::Pedestrian,
TravelMode::Vehicle,
TravelMode::Bicycle,
TravelMode::Transit,
]
.iter()
.any(|m| a.allows(*m) && b.allows(*m))
}
#[cfg(test)]
mod tests {
use super::*;
use rustsim_traffic::{LinkClass, LinkProperties};
use rustsim_traffic::{TrafficControlType, TransportLinkMetadata, TransportLinkSpace};
fn walk_plus_transit_graph() -> ModalGraph {
let mut g = ModalGraph::new();
let walk_only = only(TravelMode::Pedestrian);
g.add_edge(ModalEdge {
from: 1,
to: 2,
cost: 600.0,
modes: walk_only,
});
g.add_edge(ModalEdge {
from: 2,
to: 3,
cost: 600.0,
modes: walk_only,
});
g.add_edge(ModalEdge {
from: 3,
to: 4,
cost: 300.0,
modes: walk_only,
});
let transit_only = only(TravelMode::Transit);
g.add_edge(ModalEdge {
from: 2,
to: 3,
cost: 120.0,
modes: transit_only,
});
g
}
#[test]
fn walk_only_takes_slow_path() {
let g = walk_plus_transit_graph();
let r = shortest_path(&g, 1, 4, only(TravelMode::Pedestrian)).unwrap();
assert_eq!(r.nodes, vec![1, 2, 3, 4]);
assert!((r.total_cost - 1500.0).abs() < 1e-9);
}
#[test]
fn walk_plus_transit_takes_transit_shortcut() {
let g = walk_plus_transit_graph();
let modes = AllowedModes::none()
.with_mode(TravelMode::Pedestrian)
.with_mode(TravelMode::Transit);
let r = shortest_path(&g, 1, 4, modes).unwrap();
assert_eq!(r.nodes, vec![1, 2, 3, 4]);
assert!((r.total_cost - 1020.0).abs() < 1e-9);
}
#[test]
fn disconnected_returns_none() {
let g = walk_plus_transit_graph();
let r = shortest_path(&g, 1, 99, only(TravelMode::Pedestrian));
assert!(r.is_none());
}
#[test]
fn builds_graph_from_transport_link_space() {
let mut space = TransportLinkSpace::new();
let a = space.add_node();
let b = space.add_node();
let c = space.add_node();
let (walk_geom, walk_props) = LinkProperties::pedestrian(60.0, 3.0).unwrap();
let walk = space.add_link(a, b, walk_geom, walk_props).unwrap();
let (road_geom, road_props) = LinkProperties::urban(500.0, 40.0, 1).unwrap();
let road = space.add_link(b, c, road_geom, road_props).unwrap();
let metadata = vec![
TransportLinkMetadata::new(
walk,
a,
b,
LinkClass::Walkway,
AllowedModes::pedestrian_only(),
),
TransportLinkMetadata::new(
road,
b,
c,
LinkClass::Transitway,
AllowedModes::vehicular(),
)
.with_traffic_control(TrafficControlType::Signal),
];
let graph = ModalGraph::from_transport_links(&space, metadata).unwrap();
assert_eq!(graph.neighbours(a as NodeId).len(), 1);
assert_eq!(graph.neighbours(b as NodeId).len(), 1);
assert!(shortest_path(
&graph,
a as NodeId,
c as NodeId,
AllowedModes::none()
.with_mode(TravelMode::Pedestrian)
.with_mode(TravelMode::Transit),
)
.is_some());
}
}