solverforge_maps/routing/
fetch.rs

1//! Overpass API fetching and caching for road networks.
2
3use std::collections::HashMap;
4use std::path::Path;
5
6use tokio::sync::mpsc::Sender;
7use tracing::{debug, info};
8
9use super::bbox::BoundingBox;
10use super::cache::{
11    cache, record_hit, record_miss, CachedEdge, CachedNetwork, CachedNode, NetworkRef,
12    CACHE_VERSION,
13};
14use super::config::NetworkConfig;
15use super::coord::Coord;
16use super::error::RoutingError;
17use super::network::{EdgeData, RoadNetwork};
18use super::osm::OverpassResponse;
19use super::progress::RoutingProgress;
20
21impl RoadNetwork {
22    pub async fn load_or_fetch(
23        bbox: &BoundingBox,
24        config: &NetworkConfig,
25        progress: Option<&Sender<RoutingProgress>>,
26    ) -> Result<NetworkRef, RoutingError> {
27        let cache_key = bbox.cache_key();
28
29        if let Some(tx) = progress {
30            let _ = tx.send(RoutingProgress::CheckingCache { percent: 0 }).await;
31        }
32
33        {
34            let cache_guard = cache().read().await;
35            if cache_guard.contains_key(&cache_key) {
36                record_hit();
37                info!("Using in-memory cached road network for {}", cache_key);
38                if let Some(tx) = progress {
39                    let _ = tx
40                        .send(RoutingProgress::CheckingCache { percent: 10 })
41                        .await;
42                }
43                return Ok(NetworkRef::new(cache_guard, cache_key));
44            }
45        }
46        record_miss();
47
48        if let Some(tx) = progress {
49            let _ = tx.send(RoutingProgress::CheckingCache { percent: 5 }).await;
50        }
51
52        {
53            let mut cache_guard = cache().write().await;
54            if !cache_guard.contains_key(&cache_key) {
55                tokio::fs::create_dir_all(&config.cache_dir).await?;
56                let cache_path = config.cache_dir.join(format!("{}.json", cache_key));
57
58                let network = if tokio::fs::try_exists(&cache_path).await.unwrap_or(false) {
59                    info!("Loading road network from file cache: {:?}", cache_path);
60                    if let Some(tx) = progress {
61                        let _ = tx.send(RoutingProgress::CheckingCache { percent: 8 }).await;
62                    }
63                    match Self::load_from_file(&cache_path).await {
64                        Ok(n) => {
65                            if let Some(tx) = progress {
66                                let _ = tx
67                                    .send(RoutingProgress::BuildingGraph { percent: 50 })
68                                    .await;
69                            }
70                            n
71                        }
72                        Err(e) => {
73                            info!("File cache invalid ({}), downloading fresh", e);
74                            let n = Self::fetch_from_api(bbox, config, progress).await?;
75                            n.save_to_file(&cache_path).await?;
76                            info!("Saved road network to file cache: {:?}", cache_path);
77                            n
78                        }
79                    }
80                } else {
81                    info!("Downloading road network from Overpass API");
82                    let n = Self::fetch_from_api(bbox, config, progress).await?;
83                    n.save_to_file(&cache_path).await?;
84                    info!("Saved road network to file cache: {:?}", cache_path);
85                    n
86                };
87
88                cache_guard.insert(cache_key.clone(), network);
89            }
90        }
91
92        let cache_guard = cache().read().await;
93        Ok(NetworkRef::new(cache_guard, cache_key))
94    }
95
96    pub async fn fetch(
97        bbox: &BoundingBox,
98        config: &NetworkConfig,
99        progress: Option<&Sender<RoutingProgress>>,
100    ) -> Result<Self, RoutingError> {
101        Self::fetch_from_api(bbox, config, progress).await
102    }
103
104    async fn fetch_from_api(
105        bbox: &BoundingBox,
106        config: &NetworkConfig,
107        progress: Option<&Sender<RoutingProgress>>,
108    ) -> Result<Self, RoutingError> {
109        let highway_regex = config.highway_regex();
110        let query = format!(
111            r#"[out:json][timeout:120];
112(
113  way["highway"~"{}"]
114    ({},{},{},{});
115);
116(._;>;);
117out body;"#,
118            highway_regex, bbox.min_lat, bbox.min_lng, bbox.max_lat, bbox.max_lng
119        );
120
121        debug!("Overpass query:\n{}", query);
122        info!(
123            "Preparing Overpass query for bbox: {:.4},{:.4} to {:.4},{:.4}",
124            bbox.min_lat, bbox.min_lng, bbox.max_lat, bbox.max_lng
125        );
126
127        if let Some(tx) = progress {
128            let _ = tx
129                .send(RoutingProgress::DownloadingNetwork {
130                    percent: 10,
131                    bytes: 0,
132                })
133                .await;
134        }
135
136        let client = reqwest::Client::builder()
137            .connect_timeout(config.connect_timeout)
138            .read_timeout(config.read_timeout)
139            .timeout(config.read_timeout)
140            .user_agent("SolverForge/0.5.0")
141            .build()
142            .map_err(|e| RoutingError::Network(e.to_string()))?;
143
144        info!("Sending request to Overpass API...");
145
146        if let Some(tx) = progress {
147            let _ = tx
148                .send(RoutingProgress::DownloadingNetwork {
149                    percent: 15,
150                    bytes: 0,
151                })
152                .await;
153        }
154
155        let response = client
156            .post(&config.overpass_url)
157            .body(query)
158            .header("Content-Type", "text/plain")
159            .send()
160            .await
161            .map_err(|e| RoutingError::Network(e.to_string()))?;
162
163        info!("Received response: status={}", response.status());
164
165        if !response.status().is_success() {
166            return Err(RoutingError::Network(format!(
167                "Overpass API returned status {}",
168                response.status()
169            )));
170        }
171
172        if let Some(tx) = progress {
173            let _ = tx
174                .send(RoutingProgress::DownloadingNetwork {
175                    percent: 25,
176                    bytes: 0,
177                })
178                .await;
179        }
180
181        let bytes = response
182            .bytes()
183            .await
184            .map_err(|e| RoutingError::Network(e.to_string()))?;
185
186        let bytes_len = bytes.len();
187        if let Some(tx) = progress {
188            let _ = tx
189                .send(RoutingProgress::DownloadingNetwork {
190                    percent: 30,
191                    bytes: bytes_len,
192                })
193                .await;
194        }
195
196        if let Some(tx) = progress {
197            let _ = tx
198                .send(RoutingProgress::ParsingOsm {
199                    percent: 32,
200                    nodes: 0,
201                    edges: 0,
202                })
203                .await;
204        }
205
206        let osm_data: OverpassResponse =
207            serde_json::from_slice(&bytes).map_err(|e| RoutingError::Parse(e.to_string()))?;
208
209        info!("Downloaded {} OSM elements", osm_data.elements.len());
210
211        if let Some(tx) = progress {
212            let _ = tx
213                .send(RoutingProgress::ParsingOsm {
214                    percent: 35,
215                    nodes: osm_data.elements.len(),
216                    edges: 0,
217                })
218                .await;
219        }
220
221        if let Some(tx) = progress {
222            let _ = tx
223                .send(RoutingProgress::BuildingGraph { percent: 40 })
224                .await;
225        }
226
227        let network = Self::build_from_osm(&osm_data, config)?;
228
229        if let Some(tx) = progress {
230            let _ = tx
231                .send(RoutingProgress::BuildingGraph { percent: 50 })
232                .await;
233        }
234
235        Ok(network)
236    }
237
238    pub(super) fn build_from_osm(
239        osm: &OverpassResponse,
240        config: &NetworkConfig,
241    ) -> Result<Self, RoutingError> {
242        let mut network = Self::new();
243
244        let mut nodes: HashMap<i64, (f64, f64)> = HashMap::new();
245        for elem in &osm.elements {
246            if elem.elem_type == "node" {
247                if let (Some(lat), Some(lon)) = (elem.lat, elem.lon) {
248                    nodes.insert(elem.id, (lat, lon));
249                }
250            }
251        }
252
253        info!("Parsed {} nodes", nodes.len());
254
255        let mut way_count = 0;
256        for elem in &osm.elements {
257            if elem.elem_type == "way" {
258                if let Some(ref node_ids) = elem.nodes {
259                    let highway = elem.tags.as_ref().and_then(|t| t.highway.as_deref());
260                    let oneway = elem.tags.as_ref().and_then(|t| t.oneway.as_deref());
261                    let maxspeed = elem.tags.as_ref().and_then(|t| t.maxspeed.as_deref());
262                    let speed = config
263                        .speed_profile
264                        .speed_mps(maxspeed, highway.unwrap_or("residential"));
265                    let is_oneway_forward = matches!(oneway, Some("yes") | Some("1"));
266                    let is_oneway_reverse = matches!(oneway, Some("-1"));
267
268                    for window in node_ids.windows(2) {
269                        let n1_id = window[0];
270                        let n2_id = window[1];
271
272                        let Some(&(lat1, lng1)) = nodes.get(&n1_id) else {
273                            continue;
274                        };
275                        let Some(&(lat2, lng2)) = nodes.get(&n2_id) else {
276                            continue;
277                        };
278
279                        let idx1 = network.get_or_create_node(lat1, lng1);
280                        let idx2 = network.get_or_create_node(lat2, lng2);
281
282                        let coord1 = Coord::new(lat1, lng1);
283                        let coord2 = Coord::new(lat2, lng2);
284                        let distance = super::geo::haversine_distance(coord1, coord2);
285                        let travel_time = distance / speed;
286
287                        let edge_data = EdgeData {
288                            travel_time_s: travel_time,
289                            distance_m: distance,
290                        };
291
292                        if is_oneway_reverse {
293                            // oneway=-1 means traffic flows opposite to way direction
294                            network.add_edge(idx2, idx1, edge_data);
295                        } else {
296                            // Forward direction (always added unless reverse-only)
297                            network.add_edge(idx1, idx2, edge_data.clone());
298                            if !is_oneway_forward {
299                                // Bidirectional road
300                                network.add_edge(idx2, idx1, edge_data);
301                            }
302                        }
303                    }
304
305                    way_count += 1;
306                }
307            }
308        }
309
310        info!(
311            "Built graph with {} nodes and {} edges from {} ways",
312            network.node_count(),
313            network.edge_count(),
314            way_count
315        );
316
317        // Filter to largest strongly connected component to ensure all nodes are reachable
318        let scc_count = network.strongly_connected_components();
319        if scc_count > 1 {
320            info!(
321                "Road network has {} SCCs, filtering to largest component",
322                scc_count
323            );
324            network.filter_to_largest_scc();
325            info!(
326                "After SCC filter: {} nodes, {} edges",
327                network.node_count(),
328                network.edge_count()
329            );
330        }
331
332        network.build_spatial_index();
333
334        Ok(network)
335    }
336
337    async fn load_from_file(path: &Path) -> Result<Self, RoutingError> {
338        let data = tokio::fs::read_to_string(path).await?;
339
340        let cached: CachedNetwork = match serde_json::from_str(&data) {
341            Ok(c) => c,
342            Err(e) => {
343                info!("Cache file corrupted, will re-download: {}", e);
344                let _ = tokio::fs::remove_file(path).await;
345                return Err(RoutingError::Parse(e.to_string()));
346            }
347        };
348
349        if cached.version != CACHE_VERSION {
350            info!(
351                "Cache version mismatch (got {}, need {}), will re-download",
352                cached.version, CACHE_VERSION
353            );
354            let _ = tokio::fs::remove_file(path).await;
355            return Err(RoutingError::Parse("cache version mismatch".into()));
356        }
357
358        let mut network = Self::new();
359
360        for node in &cached.nodes {
361            network.add_node_at(node.lat, node.lng);
362        }
363
364        for edge in &cached.edges {
365            network.add_edge_by_index(edge.from, edge.to, edge.travel_time_s, edge.distance_m);
366        }
367
368        // Filter to largest SCC (cached networks from older versions may not be filtered)
369        let scc_count = network.strongly_connected_components();
370        if scc_count > 1 {
371            info!(
372                "Cached network has {} SCCs, filtering to largest component",
373                scc_count
374            );
375            network.filter_to_largest_scc();
376            info!(
377                "After SCC filter: {} nodes, {} edges",
378                network.node_count(),
379                network.edge_count()
380            );
381        }
382
383        network.build_spatial_index();
384
385        Ok(network)
386    }
387
388    async fn save_to_file(&self, path: &Path) -> Result<(), RoutingError> {
389        let nodes: Vec<CachedNode> = self
390            .nodes_iter()
391            .map(|(lat, lng)| CachedNode { lat, lng })
392            .collect();
393
394        let edges: Vec<CachedEdge> = self
395            .edges_iter()
396            .map(|(from, to, travel_time_s, distance_m)| CachedEdge {
397                from,
398                to,
399                travel_time_s,
400                distance_m,
401            })
402            .collect();
403
404        let cached = CachedNetwork {
405            version: CACHE_VERSION,
406            nodes,
407            edges,
408        };
409        let data =
410            serde_json::to_string(&cached).map_err(|e| RoutingError::Parse(e.to_string()))?;
411        tokio::fs::write(path, data).await?;
412
413        Ok(())
414    }
415}
416
417impl RoadNetwork {
418    #[doc(hidden)]
419    pub async fn load_or_fetch_simple(bbox: &BoundingBox) -> Result<NetworkRef, RoutingError> {
420        Self::load_or_fetch(bbox, &NetworkConfig::default(), None).await
421    }
422}