Skip to main content

elevator_core/
topology.rs

1//! Lazy-rebuilt connectivity graph for cross-line topology queries.
2//!
3//! [`TopologyGraph`](crate::topology::TopologyGraph) tracks which stops are reachable
4//! from which, via which groups and lines. It is rebuilt from
5//! [`ElevatorGroup`](crate::dispatch::ElevatorGroup) data on demand
6//! (when marked dirty) and supports BFS-based reachability, transfer-point
7//! detection, and shortest-route computation.
8
9use std::collections::hash_map::Entry;
10use std::collections::{HashMap, HashSet, VecDeque};
11
12use crate::components::route::{Route, RouteLeg, TransportMode};
13use crate::dispatch::ElevatorGroup;
14use crate::entity::EntityId;
15use crate::ids::GroupId;
16
17/// An edge in the topology graph connecting two stops.
18#[derive(Debug, Clone)]
19struct Edge {
20    /// Destination stop entity.
21    to: EntityId,
22    /// Group that connects these stops.
23    group: GroupId,
24    /// Line within the group (retained for future per-line routing).
25    #[allow(dead_code)]
26    line: EntityId,
27}
28
29/// Lazy-rebuilt connectivity graph for topology queries.
30///
31/// Tracks which stops are reachable from which, via which groups/lines.
32/// Rebuilt from [`ElevatorGroup`] data when marked dirty.
33#[derive(Debug)]
34pub struct TopologyGraph {
35    /// Whether the graph needs rebuild.
36    dirty: bool,
37    /// stop -> `Vec<Edge>` adjacency list.
38    adjacency: HashMap<EntityId, Vec<Edge>>,
39}
40
41impl TopologyGraph {
42    /// Create a new topology graph (starts dirty so the first query triggers a rebuild).
43    #[must_use]
44    pub fn new() -> Self {
45        Self {
46            dirty: true,
47            adjacency: HashMap::new(),
48        }
49    }
50
51    /// Mark the graph as needing rebuild.
52    pub const fn mark_dirty(&mut self) {
53        self.dirty = true;
54    }
55
56    /// Whether the graph needs rebuild.
57    #[must_use]
58    pub const fn is_dirty(&self) -> bool {
59        self.dirty
60    }
61
62    /// Rebuild the graph from current group/line topology.
63    pub fn rebuild(&mut self, groups: &[ElevatorGroup]) {
64        self.adjacency.clear();
65
66        for group in groups {
67            for line_info in group.lines() {
68                // Every pair of stops served by a line is connected.
69                for &from in line_info.serves() {
70                    for &to in line_info.serves() {
71                        if from != to {
72                            self.adjacency.entry(from).or_default().push(Edge {
73                                to,
74                                group: group.id(),
75                                line: line_info.entity(),
76                            });
77                        }
78                    }
79                }
80            }
81        }
82
83        self.dirty = false;
84    }
85
86    /// All stops reachable from a given stop (BFS).
87    ///
88    /// Returns every reachable stop except the origin. If the origin has no
89    /// outgoing edges the result is empty.
90    #[must_use]
91    pub fn reachable_stops_from(&self, stop: EntityId) -> Vec<EntityId> {
92        let mut visited = HashSet::new();
93        let mut queue = VecDeque::new();
94
95        visited.insert(stop);
96        queue.push_back(stop);
97
98        while let Some(current) = queue.pop_front() {
99            if let Some(edges) = self.adjacency.get(&current) {
100                for edge in edges {
101                    if visited.insert(edge.to) {
102                        queue.push_back(edge.to);
103                    }
104                }
105            }
106        }
107
108        // Remove the origin itself.
109        visited.remove(&stop);
110        visited.into_iter().collect()
111    }
112
113    /// Stops that appear in two or more groups (transfer points).
114    ///
115    /// This inspects group membership directly and does not require the
116    /// adjacency graph. Note: this checks group membership only, not whether
117    /// riders can actually transfer between groups at the shared stop. A stop
118    /// served by two disconnected groups would still appear here.
119    #[must_use]
120    pub fn transfer_points(groups: &[ElevatorGroup]) -> Vec<EntityId> {
121        let mut stop_groups: HashMap<EntityId, usize> = HashMap::new();
122        for group in groups {
123            for &stop in group.stop_entities() {
124                *stop_groups.entry(stop).or_insert(0) += 1;
125            }
126        }
127        stop_groups
128            .into_iter()
129            .filter(|&(_, count)| count >= 2)
130            .map(|(stop, _)| stop)
131            .collect()
132    }
133
134    /// Find the shortest route from one stop to another (BFS on groups).
135    ///
136    /// Returns `None` if `to` is unreachable from `from`, or if `from == to`.
137    /// Each edge traversal becomes one [`RouteLeg`]; consecutive stops sharing
138    /// the same group naturally coalesce into a single leg when the caller
139    /// chooses to, but the raw path is returned as individual per-hop legs so
140    /// the consumer can decide on merging strategy.
141    #[must_use]
142    pub fn shortest_route(&self, from: EntityId, to: EntityId) -> Option<Route> {
143        if from == to {
144            return None;
145        }
146
147        // BFS tracking the predecessor and the edge used to reach each node.
148        let mut visited: HashMap<EntityId, (EntityId, GroupId)> = HashMap::new();
149        let mut queue = VecDeque::new();
150
151        queue.push_back(from);
152        // Sentinel: from has no predecessor.
153        visited.insert(from, (from, GroupId(u32::MAX)));
154
155        while let Some(current) = queue.pop_front() {
156            if current == to {
157                break;
158            }
159            if let Some(edges) = self.adjacency.get(&current) {
160                for edge in edges {
161                    if let Entry::Vacant(e) = visited.entry(edge.to) {
162                        e.insert((current, edge.group));
163                        queue.push_back(edge.to);
164                    }
165                }
166            }
167        }
168
169        // If `to` was never reached, return None.
170        if !visited.contains_key(&to) {
171            return None;
172        }
173
174        // Reconstruct the path from `to` back to `from`.
175        let mut path: Vec<(EntityId, GroupId)> = Vec::new();
176        let mut current = to;
177        while current != from {
178            let &(prev, group) = visited.get(&current)?;
179            path.push((current, group));
180            current = prev;
181        }
182        path.reverse();
183
184        // Build legs from the path.
185        let mut legs = Vec::with_capacity(path.len());
186        let mut leg_from = from;
187        for (stop, group) in path {
188            legs.push(RouteLeg {
189                from: leg_from,
190                to: stop,
191                via: TransportMode::Group(group),
192            });
193            leg_from = stop;
194        }
195
196        Some(Route {
197            legs,
198            current_leg: 0,
199        })
200    }
201}
202
203impl Default for TopologyGraph {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209#[cfg(test)]
210#[allow(clippy::expect_used, clippy::panic)]
211mod tests {
212    use super::*;
213    use crate::dispatch::LineInfo;
214    use slotmap::KeyData;
215
216    /// Create a deterministic `EntityId` from a raw index (for tests only).
217    fn eid(n: u64) -> EntityId {
218        EntityId::from(KeyData::from_ffi(n))
219    }
220
221    /// Helper to build a simple group with one line serving the given stops.
222    fn group_with_line(
223        group_id: u32,
224        line_entity: EntityId,
225        stops: Vec<EntityId>,
226    ) -> ElevatorGroup {
227        ElevatorGroup::new(
228            GroupId(group_id),
229            format!("Group {group_id}"),
230            vec![LineInfo::new(line_entity, Vec::new(), stops)],
231        )
232    }
233
234    #[test]
235    fn rebuild_clears_dirty_flag() {
236        let mut graph = TopologyGraph::new();
237        assert!(graph.is_dirty());
238        graph.rebuild(&[]);
239        assert!(!graph.is_dirty());
240    }
241
242    #[test]
243    fn mark_dirty_sets_flag() {
244        let mut graph = TopologyGraph::new();
245        graph.rebuild(&[]);
246        graph.mark_dirty();
247        assert!(graph.is_dirty());
248    }
249
250    #[test]
251    fn reachable_within_single_line() {
252        let a = eid(1);
253        let b = eid(2);
254        let c = eid(3);
255        let line = eid(100);
256
257        let groups = vec![group_with_line(0, line, vec![a, b, c])];
258        let mut graph = TopologyGraph::new();
259        graph.rebuild(&groups);
260
261        let mut reachable = graph.reachable_stops_from(a);
262        reachable.sort();
263        let mut expected = vec![b, c];
264        expected.sort();
265        assert_eq!(reachable, expected);
266    }
267
268    #[test]
269    fn reachable_across_groups_via_transfer() {
270        let a = eid(1);
271        let b = eid(2); // transfer point
272        let c = eid(3);
273        let line0 = eid(100);
274        let line1 = eid(101);
275
276        let groups = vec![
277            group_with_line(0, line0, vec![a, b]),
278            group_with_line(1, line1, vec![b, c]),
279        ];
280        let mut graph = TopologyGraph::new();
281        graph.rebuild(&groups);
282
283        let mut reachable = graph.reachable_stops_from(a);
284        reachable.sort();
285        let mut expected = vec![b, c];
286        expected.sort();
287        assert_eq!(reachable, expected);
288    }
289
290    #[test]
291    fn unreachable_stop() {
292        let a = eid(1);
293        let b = eid(2);
294        let c = eid(3);
295        let line0 = eid(100);
296        let line1 = eid(101);
297
298        // Two disconnected groups, no shared stops.
299        let groups = vec![
300            group_with_line(0, line0, vec![a]),
301            group_with_line(1, line1, vec![b, c]),
302        ];
303        let mut graph = TopologyGraph::new();
304        graph.rebuild(&groups);
305
306        let reachable = graph.reachable_stops_from(a);
307        assert!(reachable.is_empty());
308    }
309
310    #[test]
311    fn transfer_points_detected() {
312        let a = eid(1);
313        let b = eid(2);
314        let c = eid(3);
315        let line0 = eid(100);
316        let line1 = eid(101);
317
318        let groups = vec![
319            group_with_line(0, line0, vec![a, b]),
320            group_with_line(1, line1, vec![b, c]),
321        ];
322
323        let transfers = TopologyGraph::transfer_points(&groups);
324        assert_eq!(transfers, vec![b]);
325    }
326
327    #[test]
328    fn shortest_route_direct() {
329        let a = eid(1);
330        let b = eid(2);
331        let line = eid(100);
332
333        let groups = vec![group_with_line(0, line, vec![a, b])];
334        let mut graph = TopologyGraph::new();
335        graph.rebuild(&groups);
336
337        let route = graph.shortest_route(a, b).expect("expected direct route");
338        assert_eq!(route.legs.len(), 1);
339        assert_eq!(route.legs[0].from, a);
340        assert_eq!(route.legs[0].to, b);
341        assert_eq!(route.legs[0].via, TransportMode::Group(GroupId(0)));
342    }
343
344    #[test]
345    fn shortest_route_with_transfer() {
346        let a = eid(1);
347        let b = eid(2);
348        let c = eid(3);
349        let line0 = eid(100);
350        let line1 = eid(101);
351
352        let groups = vec![
353            group_with_line(0, line0, vec![a, b]),
354            group_with_line(1, line1, vec![b, c]),
355        ];
356        let mut graph = TopologyGraph::new();
357        graph.rebuild(&groups);
358
359        let route = graph.shortest_route(a, c).expect("expected transfer route");
360        assert_eq!(route.legs.len(), 2);
361        assert_eq!(route.legs[0].from, a);
362        assert_eq!(route.legs[0].to, b);
363        assert_eq!(route.legs[0].via, TransportMode::Group(GroupId(0)));
364        assert_eq!(route.legs[1].from, b);
365        assert_eq!(route.legs[1].to, c);
366        assert_eq!(route.legs[1].via, TransportMode::Group(GroupId(1)));
367    }
368
369    #[test]
370    fn shortest_route_unreachable() {
371        let a = eid(1);
372        let b = eid(2);
373        let line0 = eid(100);
374        let line1 = eid(101);
375
376        let groups = vec![
377            group_with_line(0, line0, vec![a]),
378            group_with_line(1, line1, vec![b]),
379        ];
380        let mut graph = TopologyGraph::new();
381        graph.rebuild(&groups);
382
383        assert!(graph.shortest_route(a, b).is_none());
384    }
385
386    #[test]
387    fn shortest_route_same_stop() {
388        let a = eid(1);
389        let line = eid(100);
390
391        let groups = vec![group_with_line(0, line, vec![a])];
392        let mut graph = TopologyGraph::new();
393        graph.rebuild(&groups);
394
395        assert!(graph.shortest_route(a, a).is_none());
396    }
397}