1use std::cmp::Ordering;
10use std::collections::BinaryHeap;
11
12use rustsim_modes::{AllowedModes, TravelMode};
13use rustsim_traffic::{TransportLinkMetadata, TransportLinkOps, TransportLinkSpace};
14use thiserror::Error;
15
16pub type NodeId = u64;
18
19#[derive(Debug, Clone, Copy, PartialEq)]
21pub struct ModalEdge {
22 pub from: NodeId,
24 pub to: NodeId,
26 pub cost: f64,
28 pub modes: AllowedModes,
30}
31
32#[derive(Debug, Clone, Default)]
34pub struct ModalGraph {
35 adjacency: hashbrown::HashMap<NodeId, Vec<ModalEdge>>,
37}
38
39#[derive(Debug, Clone, PartialEq, Error)]
41pub enum ModalGraphBuildError {
42 #[error("transport metadata references missing link {0}")]
44 MissingLink(u64),
45 #[error("transport metadata endpoints for link {link_id} are ({metadata_from}, {metadata_to}) but link space has ({space_from}, {space_to})")]
47 EndpointMismatch {
48 link_id: u64,
50 metadata_from: u64,
52 metadata_to: u64,
54 space_from: u64,
56 space_to: u64,
58 },
59 #[error("transport link {link_id} has invalid routing cost {cost}")]
61 InvalidCost {
62 link_id: u64,
64 cost: f64,
66 },
67}
68
69impl ModalGraph {
70 pub fn new() -> Self {
72 Self::default()
73 }
74
75 pub fn add_edge(&mut self, edge: ModalEdge) {
77 self.adjacency.entry(edge.from).or_default().push(edge);
78 }
79
80 pub fn neighbours(&self, node: NodeId) -> &[ModalEdge] {
82 self.adjacency
83 .get(&node)
84 .map(|v| v.as_slice())
85 .unwrap_or(&[])
86 }
87
88 pub fn from_transport_links(
96 space: &TransportLinkSpace,
97 metadata: impl IntoIterator<Item = TransportLinkMetadata>,
98 ) -> Result<Self, ModalGraphBuildError> {
99 let mut graph = Self::new();
100 for link in metadata {
101 let link_id = link.edge_id;
102 let (space_from, space_to) = space
103 .link_endpoints(link_id)
104 .ok_or(ModalGraphBuildError::MissingLink(link_id as u64))?;
105 if (link.from_node, link.to_node) != (space_from, space_to) {
106 return Err(ModalGraphBuildError::EndpointMismatch {
107 link_id: link_id as u64,
108 metadata_from: link.from_node as u64,
109 metadata_to: link.to_node as u64,
110 space_from: space_from as u64,
111 space_to: space_to as u64,
112 });
113 }
114
115 let cost = space.link_travel_time(link_id);
116 if !cost.is_finite() || cost < 0.0 {
117 return Err(ModalGraphBuildError::InvalidCost {
118 link_id: link_id as u64,
119 cost,
120 });
121 }
122
123 graph.add_edge(ModalEdge {
124 from: space_from as NodeId,
125 to: space_to as NodeId,
126 cost,
127 modes: link.allowed_modes,
128 });
129 }
130 Ok(graph)
131 }
132}
133
134#[derive(Debug, Clone, PartialEq)]
136pub struct ModalRoute {
137 pub nodes: Vec<NodeId>,
139 pub total_cost: f64,
141}
142
143pub fn shortest_path(
147 graph: &ModalGraph,
148 start: NodeId,
149 goal: NodeId,
150 available_modes: AllowedModes,
151) -> Option<ModalRoute> {
152 #[derive(Copy, Clone, PartialEq)]
153 struct State {
154 cost: f64,
155 node: NodeId,
156 }
157 impl Eq for State {}
158 impl Ord for State {
159 fn cmp(&self, other: &Self) -> Ordering {
160 other
161 .cost
162 .partial_cmp(&self.cost)
163 .unwrap_or(Ordering::Equal)
164 }
165 }
166 impl PartialOrd for State {
167 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
168 Some(self.cmp(other))
169 }
170 }
171
172 let mut dist: hashbrown::HashMap<NodeId, f64> = hashbrown::HashMap::new();
173 let mut came_from: hashbrown::HashMap<NodeId, NodeId> = hashbrown::HashMap::new();
174 let mut heap = BinaryHeap::new();
175
176 dist.insert(start, 0.0);
177 heap.push(State {
178 cost: 0.0,
179 node: start,
180 });
181
182 while let Some(State { cost, node }) = heap.pop() {
183 if node == goal {
184 return Some(reconstruct(&came_from, start, goal, cost));
185 }
186 if let Some(&best) = dist.get(&node) {
187 if cost > best + 1e-9 {
188 continue;
189 }
190 }
191 for edge in graph.neighbours(node) {
192 if !modes_intersect(edge.modes, available_modes) {
193 continue;
194 }
195 let next_cost = cost + edge.cost;
196 let better = dist
197 .get(&edge.to)
198 .map(|&d| next_cost < d - 1e-12)
199 .unwrap_or(true);
200 if better {
201 dist.insert(edge.to, next_cost);
202 came_from.insert(edge.to, node);
203 heap.push(State {
204 cost: next_cost,
205 node: edge.to,
206 });
207 }
208 }
209 }
210
211 None
212}
213
214fn reconstruct(
215 came_from: &hashbrown::HashMap<NodeId, NodeId>,
216 start: NodeId,
217 goal: NodeId,
218 cost: f64,
219) -> ModalRoute {
220 let mut nodes = vec![goal];
221 let mut cur = goal;
222 while cur != start {
223 if let Some(&prev) = came_from.get(&cur) {
224 nodes.push(prev);
225 cur = prev;
226 } else {
227 break;
228 }
229 }
230 nodes.reverse();
231 ModalRoute {
232 nodes,
233 total_cost: cost,
234 }
235}
236
237pub fn only(mode: TravelMode) -> AllowedModes {
239 AllowedModes::none().with_mode(mode)
240}
241
242fn modes_intersect(a: AllowedModes, b: AllowedModes) -> bool {
244 [
245 TravelMode::Pedestrian,
246 TravelMode::Vehicle,
247 TravelMode::Bicycle,
248 TravelMode::Transit,
249 ]
250 .iter()
251 .any(|m| a.allows(*m) && b.allows(*m))
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use rustsim_traffic::{LinkClass, LinkProperties};
258 use rustsim_traffic::{TrafficControlType, TransportLinkMetadata, TransportLinkSpace};
259
260 fn walk_plus_transit_graph() -> ModalGraph {
261 let mut g = ModalGraph::new();
262 let walk_only = only(TravelMode::Pedestrian);
264 g.add_edge(ModalEdge {
265 from: 1,
266 to: 2,
267 cost: 600.0,
268 modes: walk_only,
269 });
270 g.add_edge(ModalEdge {
271 from: 2,
272 to: 3,
273 cost: 600.0,
274 modes: walk_only,
275 });
276 g.add_edge(ModalEdge {
277 from: 3,
278 to: 4,
279 cost: 300.0,
280 modes: walk_only,
281 });
282 let transit_only = only(TravelMode::Transit);
284 g.add_edge(ModalEdge {
285 from: 2,
286 to: 3,
287 cost: 120.0,
288 modes: transit_only,
289 });
290 g
291 }
292
293 #[test]
294 fn walk_only_takes_slow_path() {
295 let g = walk_plus_transit_graph();
296 let r = shortest_path(&g, 1, 4, only(TravelMode::Pedestrian)).unwrap();
297 assert_eq!(r.nodes, vec![1, 2, 3, 4]);
298 assert!((r.total_cost - 1500.0).abs() < 1e-9);
299 }
300
301 #[test]
302 fn walk_plus_transit_takes_transit_shortcut() {
303 let g = walk_plus_transit_graph();
304 let modes = AllowedModes::none()
305 .with_mode(TravelMode::Pedestrian)
306 .with_mode(TravelMode::Transit);
307 let r = shortest_path(&g, 1, 4, modes).unwrap();
308 assert_eq!(r.nodes, vec![1, 2, 3, 4]);
309 assert!((r.total_cost - 1020.0).abs() < 1e-9);
311 }
312
313 #[test]
314 fn disconnected_returns_none() {
315 let g = walk_plus_transit_graph();
316 let r = shortest_path(&g, 1, 99, only(TravelMode::Pedestrian));
317 assert!(r.is_none());
318 }
319
320 #[test]
321 fn builds_graph_from_transport_link_space() {
322 let mut space = TransportLinkSpace::new();
323 let a = space.add_node();
324 let b = space.add_node();
325 let c = space.add_node();
326 let (walk_geom, walk_props) = LinkProperties::pedestrian(60.0, 3.0).unwrap();
327 let walk = space.add_link(a, b, walk_geom, walk_props).unwrap();
328 let (road_geom, road_props) = LinkProperties::urban(500.0, 40.0, 1).unwrap();
329 let road = space.add_link(b, c, road_geom, road_props).unwrap();
330
331 let metadata = vec![
332 TransportLinkMetadata::new(
333 walk,
334 a,
335 b,
336 LinkClass::Walkway,
337 AllowedModes::pedestrian_only(),
338 ),
339 TransportLinkMetadata::new(
340 road,
341 b,
342 c,
343 LinkClass::Transitway,
344 AllowedModes::vehicular(),
345 )
346 .with_traffic_control(TrafficControlType::Signal),
347 ];
348
349 let graph = ModalGraph::from_transport_links(&space, metadata).unwrap();
350 assert_eq!(graph.neighbours(a as NodeId).len(), 1);
351 assert_eq!(graph.neighbours(b as NodeId).len(), 1);
352 assert!(shortest_path(
353 &graph,
354 a as NodeId,
355 c as NodeId,
356 AllowedModes::none()
357 .with_mode(TravelMode::Pedestrian)
358 .with_mode(TravelMode::Transit),
359 )
360 .is_some());
361 }
362}