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