solverforge_maps/routing/
fetch.rs1use 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 network.add_edge(idx2, idx1, edge_data);
295 } else {
296 network.add_edge(idx1, idx2, edge_data.clone());
298 if !is_oneway_forward {
299 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 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 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}