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.
117    #[must_use]
118    pub fn transfer_points(groups: &[ElevatorGroup]) -> Vec<EntityId> {
119        let mut stop_groups: HashMap<EntityId, usize> = HashMap::new();
120        for group in groups {
121            for &stop in group.stop_entities() {
122                *stop_groups.entry(stop).or_insert(0) += 1;
123            }
124        }
125        stop_groups
126            .into_iter()
127            .filter(|&(_, count)| count >= 2)
128            .map(|(stop, _)| stop)
129            .collect()
130    }
131
132    /// Find the shortest route from one stop to another (BFS on groups).
133    ///
134    /// Returns `None` if `to` is unreachable from `from`, or if `from == to`.
135    /// Each edge traversal becomes one [`RouteLeg`]; consecutive stops sharing
136    /// the same group naturally coalesce into a single leg when the caller
137    /// chooses to, but the raw path is returned as individual per-hop legs so
138    /// the consumer can decide on merging strategy.
139    #[must_use]
140    pub fn shortest_route(&self, from: EntityId, to: EntityId) -> Option<Route> {
141        if from == to {
142            return None;
143        }
144
145        // BFS tracking the predecessor and the edge used to reach each node.
146        let mut visited: HashMap<EntityId, (EntityId, GroupId)> = HashMap::new();
147        let mut queue = VecDeque::new();
148
149        queue.push_back(from);
150        // Sentinel: from has no predecessor.
151        visited.insert(from, (from, GroupId(u32::MAX)));
152
153        while let Some(current) = queue.pop_front() {
154            if current == to {
155                break;
156            }
157            if let Some(edges) = self.adjacency.get(&current) {
158                for edge in edges {
159                    if let Entry::Vacant(e) = visited.entry(edge.to) {
160                        e.insert((current, edge.group));
161                        queue.push_back(edge.to);
162                    }
163                }
164            }
165        }
166
167        // If `to` was never reached, return None.
168        if !visited.contains_key(&to) {
169            return None;
170        }
171
172        // Reconstruct the path from `to` back to `from`.
173        let mut path: Vec<(EntityId, GroupId)> = Vec::new();
174        let mut current = to;
175        while current != from {
176            let &(prev, group) = visited.get(&current)?;
177            path.push((current, group));
178            current = prev;
179        }
180        path.reverse();
181
182        // Build legs from the path.
183        let mut legs = Vec::with_capacity(path.len());
184        let mut leg_from = from;
185        for (stop, group) in path {
186            legs.push(RouteLeg {
187                from: leg_from,
188                to: stop,
189                via: TransportMode::Group(group),
190            });
191            leg_from = stop;
192        }
193
194        Some(Route {
195            legs,
196            current_leg: 0,
197        })
198    }
199}
200
201impl Default for TopologyGraph {
202    fn default() -> Self {
203        Self::new()
204    }
205}
206
207#[cfg(test)]
208#[allow(clippy::expect_used, clippy::panic)]
209mod tests {
210    use super::*;
211    use crate::dispatch::LineInfo;
212    use slotmap::KeyData;
213
214    /// Create a deterministic `EntityId` from a raw index (for tests only).
215    fn eid(n: u64) -> EntityId {
216        EntityId::from(KeyData::from_ffi(n))
217    }
218
219    /// Helper to build a simple group with one line serving the given stops.
220    fn group_with_line(
221        group_id: u32,
222        line_entity: EntityId,
223        stops: Vec<EntityId>,
224    ) -> ElevatorGroup {
225        ElevatorGroup::new(
226            GroupId(group_id),
227            format!("Group {group_id}"),
228            vec![LineInfo::new(line_entity, Vec::new(), stops)],
229        )
230    }
231
232    #[test]
233    fn rebuild_clears_dirty_flag() {
234        let mut graph = TopologyGraph::new();
235        assert!(graph.is_dirty());
236        graph.rebuild(&[]);
237        assert!(!graph.is_dirty());
238    }
239
240    #[test]
241    fn mark_dirty_sets_flag() {
242        let mut graph = TopologyGraph::new();
243        graph.rebuild(&[]);
244        graph.mark_dirty();
245        assert!(graph.is_dirty());
246    }
247
248    #[test]
249    fn reachable_within_single_line() {
250        let a = eid(1);
251        let b = eid(2);
252        let c = eid(3);
253        let line = eid(100);
254
255        let groups = vec![group_with_line(0, line, vec![a, b, c])];
256        let mut graph = TopologyGraph::new();
257        graph.rebuild(&groups);
258
259        let mut reachable = graph.reachable_stops_from(a);
260        reachable.sort();
261        let mut expected = vec![b, c];
262        expected.sort();
263        assert_eq!(reachable, expected);
264    }
265
266    #[test]
267    fn reachable_across_groups_via_transfer() {
268        let a = eid(1);
269        let b = eid(2); // transfer point
270        let c = eid(3);
271        let line0 = eid(100);
272        let line1 = eid(101);
273
274        let groups = vec![
275            group_with_line(0, line0, vec![a, b]),
276            group_with_line(1, line1, vec![b, c]),
277        ];
278        let mut graph = TopologyGraph::new();
279        graph.rebuild(&groups);
280
281        let mut reachable = graph.reachable_stops_from(a);
282        reachable.sort();
283        let mut expected = vec![b, c];
284        expected.sort();
285        assert_eq!(reachable, expected);
286    }
287
288    #[test]
289    fn unreachable_stop() {
290        let a = eid(1);
291        let b = eid(2);
292        let c = eid(3);
293        let line0 = eid(100);
294        let line1 = eid(101);
295
296        // Two disconnected groups, no shared stops.
297        let groups = vec![
298            group_with_line(0, line0, vec![a]),
299            group_with_line(1, line1, vec![b, c]),
300        ];
301        let mut graph = TopologyGraph::new();
302        graph.rebuild(&groups);
303
304        let reachable = graph.reachable_stops_from(a);
305        assert!(reachable.is_empty());
306    }
307
308    #[test]
309    fn transfer_points_detected() {
310        let a = eid(1);
311        let b = eid(2);
312        let c = eid(3);
313        let line0 = eid(100);
314        let line1 = eid(101);
315
316        let groups = vec![
317            group_with_line(0, line0, vec![a, b]),
318            group_with_line(1, line1, vec![b, c]),
319        ];
320
321        let transfers = TopologyGraph::transfer_points(&groups);
322        assert_eq!(transfers, vec![b]);
323    }
324
325    #[test]
326    fn shortest_route_direct() {
327        let a = eid(1);
328        let b = eid(2);
329        let line = eid(100);
330
331        let groups = vec![group_with_line(0, line, vec![a, b])];
332        let mut graph = TopologyGraph::new();
333        graph.rebuild(&groups);
334
335        let route = graph.shortest_route(a, b).expect("expected direct route");
336        assert_eq!(route.legs.len(), 1);
337        assert_eq!(route.legs[0].from, a);
338        assert_eq!(route.legs[0].to, b);
339        assert_eq!(route.legs[0].via, TransportMode::Group(GroupId(0)));
340    }
341
342    #[test]
343    fn shortest_route_with_transfer() {
344        let a = eid(1);
345        let b = eid(2);
346        let c = eid(3);
347        let line0 = eid(100);
348        let line1 = eid(101);
349
350        let groups = vec![
351            group_with_line(0, line0, vec![a, b]),
352            group_with_line(1, line1, vec![b, c]),
353        ];
354        let mut graph = TopologyGraph::new();
355        graph.rebuild(&groups);
356
357        let route = graph.shortest_route(a, c).expect("expected transfer route");
358        assert_eq!(route.legs.len(), 2);
359        assert_eq!(route.legs[0].from, a);
360        assert_eq!(route.legs[0].to, b);
361        assert_eq!(route.legs[0].via, TransportMode::Group(GroupId(0)));
362        assert_eq!(route.legs[1].from, b);
363        assert_eq!(route.legs[1].to, c);
364        assert_eq!(route.legs[1].via, TransportMode::Group(GroupId(1)));
365    }
366
367    #[test]
368    fn shortest_route_unreachable() {
369        let a = eid(1);
370        let b = eid(2);
371        let line0 = eid(100);
372        let line1 = eid(101);
373
374        let groups = vec![
375            group_with_line(0, line0, vec![a]),
376            group_with_line(1, line1, vec![b]),
377        ];
378        let mut graph = TopologyGraph::new();
379        graph.rebuild(&groups);
380
381        assert!(graph.shortest_route(a, b).is_none());
382    }
383
384    #[test]
385    fn shortest_route_same_stop() {
386        let a = eid(1);
387        let line = eid(100);
388
389        let groups = vec![group_with_line(0, line, vec![a])];
390        let mut graph = TopologyGraph::new();
391        graph.rebuild(&groups);
392
393        assert!(graph.shortest_route(a, a).is_none());
394    }
395}