bambam_osm/algorithm/
connected_components.rs

1use crate::model::osm::{
2    graph::{OsmGraph, OsmNodeId},
3    OsmError,
4};
5use itertools::Itertools;
6use kdam::{tqdm, Bar, BarExt};
7use std::collections::{HashMap, HashSet, VecDeque};
8
9pub type UndirectedAdjacencyList = HashMap<OsmNodeId, HashSet<OsmNodeId>>;
10
11pub fn to_undirected(graph: &OsmGraph) -> UndirectedAdjacencyList {
12    let mut undirected: HashMap<OsmNodeId, HashSet<OsmNodeId>> = HashMap::new();
13    let adjacencies_iter = tqdm!(
14        graph.connected_node_pair_iterator(false),
15        desc = "create undirected graph"
16    );
17    for (src, dst) in adjacencies_iter {
18        add_undirected_edge(src, dst, &mut undirected);
19    }
20
21    eprintln!();
22    undirected
23}
24
25/// helper that creates the relations src->dst, dst->src from a directed edge src->dst
26/// guards against self-loops.
27fn add_undirected_edge(src: &OsmNodeId, dst: &OsmNodeId, g: &mut UndirectedAdjacencyList) {
28    if src == dst {
29        return;
30    }
31    g.entry(*src)
32        .and_modify(|h| {
33            h.insert(*dst);
34        })
35        .or_insert(HashSet::from([*dst]));
36    g.entry(*dst)
37        .and_modify(|h| {
38            h.insert(*src);
39        })
40        .or_insert(HashSet::from([*src]));
41}
42
43/// runs a synchronous weakly-connected components algorithm over the directed graph.
44///
45/// # Arguments
46///
47/// * `fwd` - forward traversal segments, the "out-edges" of the nodes
48/// * `rev` - reverse traversal segments, the "in-edges" of the nodes
49/// * `nodes` - the graph nodes included to find components.
50///              this can either be the complete set or a subset.
51///
52/// # Result
53///
54/// a vector of each found component as a node list
55pub fn weakly_connected_components(
56    graph: &OsmGraph,
57    nodes: &[OsmNodeId],
58) -> Result<Vec<Vec<OsmNodeId>>, OsmError> {
59    let undirected = to_undirected(graph);
60
61    let n_unassigned = nodes.len();
62    let mut assigned: HashSet<OsmNodeId> = HashSet::new();
63    let mut solution: Vec<Vec<OsmNodeId>> = vec![];
64    let mut bar = Bar::builder()
65        .total(n_unassigned)
66        .desc("weakly connected components search")
67        .build()
68        .map_err(OsmError::InternalError)?;
69
70    // create a new component any time we find an unattached node
71    for node_id in nodes.iter() {
72        if !assigned.contains(node_id) {
73            let cluster = bfs_undirected(node_id, &undirected)?;
74            for cluster_node_id in cluster.iter() {
75                assigned.insert(*cluster_node_id);
76            }
77            solution.push(cluster);
78        }
79        let _ = bar.update(1);
80    }
81    eprintln!();
82    log::info!("found {} weakly-connected components", solution.len());
83    Ok(solution)
84}
85
86/// runs an undirected breadth-first search from some source
87/// to find all weakly-connected nodes.
88pub fn bfs_undirected(
89    source: &OsmNodeId,
90    graph: &UndirectedAdjacencyList,
91) -> Result<Vec<OsmNodeId>, OsmError> {
92    // the solution set, beginning with the source node
93    let mut visited: HashSet<OsmNodeId> = HashSet::from([*source]);
94
95    // initialize the search frontier with the neighbors of source
96    let mut frontier: VecDeque<OsmNodeId> = VecDeque::new();
97    for n in graph.get(source).into_iter().flatten() {
98        frontier.push_back(*n);
99    }
100
101    let mut bar = Bar::builder()
102        .desc("connected components")
103        .build()
104        .map_err(|e| OsmError::InternalError(e.to_string()))?;
105
106    // recurse through graph until all weakly connected neighbors are found
107    while let Some(next_id) = frontier.pop_front() {
108        if !visited.contains(&next_id) {
109            visited.insert(next_id);
110            if visited.len() == graph.len() {
111                // all nodes have been visited
112                return Ok(visited.into_iter().collect_vec());
113            }
114            for n in graph.get(&next_id).into_iter().flatten() {
115                if !visited.contains(n) {
116                    frontier.push_back(*n);
117                }
118            }
119        }
120
121        let _ = bar.update(1);
122    }
123
124    Ok(visited.into_iter().collect_vec())
125}
126
127#[cfg(test)]
128mod tests {
129    use crate::model::osm::graph::{OsmGraph, OsmNodeData, OsmNodeId, OsmWayData, OsmWayId};
130    use std::collections::HashMap;
131
132    #[test]
133    fn test_bfs_circle_with_dot() {
134        let source = OsmNodeId(0);
135        let n_connected_nodes: usize = 20;
136
137        let mut nodes = create_nodes_in_circle(n_connected_nodes);
138        // set up bogie, a lonely and unattached node
139        nodes.insert(OsmNodeId(999), OsmNodeData::default());
140
141        let mut ways: HashMap<OsmWayId, OsmWayData> = HashMap::new();
142        let n_iters = (n_connected_nodes - 1) as i64;
143        for i in 0..n_iters {
144            let reversed = i % 2 == 0;
145            let src = if reversed {
146                OsmNodeId(i + 1)
147            } else {
148                OsmNodeId(i)
149            };
150            let dst = if reversed {
151                OsmNodeId(i)
152            } else {
153                OsmNodeId(i + 1)
154            };
155            let way_id = OsmWayId(i);
156            let mut way = OsmWayData::default();
157            way.osmid = way_id;
158            way.nodes = vec![src, dst];
159
160            ways.insert(way.osmid, way);
161        }
162
163        // we expect two components, one for the circle, and one for the dot
164        let graph = OsmGraph::new(nodes, ways).unwrap();
165        let undirected_graph = super::to_undirected(&graph);
166        let result = super::bfs_undirected(&source, &undirected_graph).unwrap();
167        assert_eq!(result.len(), n_connected_nodes);
168    }
169
170    #[test]
171    fn test_bfs_complete_graph_5() {
172        let source = OsmNodeId(0);
173        let n_connected_nodes: usize = 5;
174        let nodes = create_nodes_in_circle(n_connected_nodes);
175        let mut ways: HashMap<OsmWayId, OsmWayData> = HashMap::new();
176
177        let n_iters = n_connected_nodes as i64;
178        let mut way_id = 0;
179        for i in 0..n_iters {
180            let src = OsmNodeId(i);
181            for j in 0..n_iters {
182                if i == j {
183                    continue;
184                }
185                let dst = OsmNodeId(j);
186                let this_way_id = OsmWayId(way_id);
187                let mut way = OsmWayData::default();
188                way.osmid = this_way_id;
189                way.nodes = vec![src, dst];
190                ways.insert(way.osmid, way);
191                way_id += 1;
192            }
193        }
194        let graph = OsmGraph::new(nodes, ways).unwrap();
195        let undirected_graph = super::to_undirected(&graph);
196
197        eprintln!(
198            "{}",
199            serde_json::to_string_pretty(&undirected_graph).unwrap()
200        );
201
202        let result = super::bfs_undirected(&source, &undirected_graph).unwrap();
203        assert_eq!(result.len(), n_connected_nodes);
204        eprintln!("{result:?}");
205    }
206
207    // helper function that creates nodes with coordinates evenly spaced in a circle
208    fn create_nodes_in_circle(n_connected_nodes: usize) -> HashMap<OsmNodeId, OsmNodeData> {
209        (0..n_connected_nodes)
210            .map(|n| {
211                let node_id = OsmNodeId(n.try_into().unwrap());
212                let mut node = OsmNodeData::default();
213                node.osmid = node_id.clone();
214                let angle = 2.0 * std::f32::consts::PI * (n as f32) / (n_connected_nodes as f32);
215                let x = angle.cos();
216                let y = angle.sin();
217                node.x = x;
218                node.y = y;
219                (node_id, node)
220            })
221            .collect()
222    }
223}