//! Local OSM road routing using Overpass API and petgraph. //! //! Downloads OpenStreetMap road network data via Overpass API, //! builds a graph locally, and computes shortest paths with Dijkstra. //! Results are cached in memory (per-process) and `.osm_cache/` (persistent). use ordered_float::OrderedFloat; use petgraph::algo::{astar, dijkstra}; use petgraph::graph::{DiGraph, NodeIndex}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::Path; use std::sync::{Arc, OnceLock}; use tokio::sync::RwLock; use tracing::{debug, error, info}; /// In-memory cache of road networks, keyed by bbox cache key. /// First request downloads, subsequent requests reuse the cached network. static NETWORK_CACHE: OnceLock>>> = OnceLock::new(); fn network_cache() -> &'static RwLock>> { NETWORK_CACHE.get_or_init(|| RwLock::new(HashMap::new())) } /// Overpass API URL. const OVERPASS_URL: &str = "https://overpass-api.de/api/interpreter"; /// Cache directory for downloaded OSM data. const CACHE_DIR: &str = ".osm_cache"; /// Default driving speed in m/s (50 km/h = 13.89 m/s). const DEFAULT_SPEED_MPS: f64 = 50.0 * 1000.0 / 3600.0; /// Error type for routing operations. #[derive(Debug)] pub enum RoutingError { /// Network request failed. Network(String), /// Failed to parse OSM data. Parse(String), /// I/O error. Io(std::io::Error), /// No route found. NoRoute, } impl std::fmt::Display for RoutingError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { RoutingError::Network(msg) => write!(f, "Network error: {}", msg), RoutingError::Parse(msg) => write!(f, "Parse error: {}", msg), RoutingError::Io(e) => write!(f, "I/O error: {}", e), RoutingError::NoRoute => write!(f, "No route found"), } } } impl std::error::Error for RoutingError {} impl From for RoutingError { fn from(e: std::io::Error) -> Self { RoutingError::Io(e) } } /// Bounding box for OSM queries. #[derive(Debug, Clone, Copy)] pub struct BoundingBox { pub min_lat: f64, pub min_lng: f64, pub max_lat: f64, pub max_lng: f64, } impl BoundingBox { /// Creates a new bounding box. pub fn new(min_lat: f64, min_lng: f64, max_lat: f64, max_lng: f64) -> Self { Self { min_lat, min_lng, max_lat, max_lng, } } /// Expands the bounding box by a factor (e.g., 0.1 = 10% on each side). pub fn expand(&self, factor: f64) -> Self { let lat_range = self.max_lat - self.min_lat; let lng_range = self.max_lng - self.min_lng; let lat_pad = lat_range * factor; let lng_pad = lng_range * factor; Self { min_lat: self.min_lat - lat_pad, min_lng: self.min_lng - lng_pad, max_lat: self.max_lat + lat_pad, max_lng: self.max_lng + lng_pad, } } /// Returns a cache key for this bounding box. fn cache_key(&self) -> String { format!( "{:.4}_{:.4}_{:.4}_{:.4}", self.min_lat, self.min_lng, self.max_lat, self.max_lng ) } } /// Node data in the road graph. #[derive(Debug, Clone)] struct NodeData { lat: f64, lng: f64, } /// Edge data in the road graph. #[derive(Debug, Clone)] struct EdgeData { /// Travel time in seconds. travel_time_s: f64, /// Distance in meters. distance_m: f64, /// Intermediate geometry points (for future full path reconstruction). #[allow(dead_code)] geometry: Vec<(f64, f64)>, } /// Result of a route computation. #[derive(Debug, Clone)] pub struct RouteResult { /// Travel time in seconds. pub duration_seconds: i64, /// Distance in meters. pub distance_meters: f64, /// Full route geometry (lat, lng pairs). pub geometry: Vec<(f64, f64)>, } /// Road network graph built from OSM data. pub struct RoadNetwork { /// Directed graph with travel times as edge weights. graph: DiGraph, /// Map from (lat_e7, lng_e7) to node index. coord_to_node: HashMap<(i64, i64), NodeIndex>, } impl RoadNetwork { /// Creates an empty road network. pub fn new() -> Self { Self { graph: DiGraph::new(), coord_to_node: HashMap::new(), } } /// Loads or fetches road network for a bounding box. /// /// Uses three-tier caching: /// 1. In-memory cache (instant, per-process) /// 2. File cache (fast, persists across restarts) /// 3. Overpass API download (slow, ~5-30s) /// /// Thread-safe: concurrent requests for the same bbox will wait for /// the first download to complete rather than downloading multiple times. pub async fn load_or_fetch(bbox: &BoundingBox) -> Result, RoutingError> { let cache_key = bbox.cache_key(); // 1. Check in-memory cache (fast path, read lock) { let cache = network_cache().read().await; if let Some(network) = cache.get(&cache_key) { info!("Using in-memory cached road network for {}", cache_key); return Ok(Arc::clone(network)); } } // 2. Acquire write lock and double-check (another request may have loaded it) let mut cache = network_cache().write().await; if let Some(network) = cache.get(&cache_key) { info!("Using in-memory cached road network for {}", cache_key); return Ok(Arc::clone(network)); } // 3. Try loading from file cache tokio::fs::create_dir_all(CACHE_DIR).await?; let cache_path = Path::new(CACHE_DIR).join(format!("{}.json", cache_key)); let network = if tokio::fs::try_exists(&cache_path).await.unwrap_or(false) { info!("Loading road network from file cache: {:?}", cache_path); match Self::load_from_cache(&cache_path).await { Ok(n) => n, Err(e) => { // File cache failed (corrupted/old version), download fresh info!("File cache invalid ({}), downloading fresh", e); let n = Self::from_bbox(bbox).await?; n.save_to_cache(&cache_path).await?; info!("Saved road network to file cache: {:?}", cache_path); n } } } else { // 4. Download from Overpass API info!("Downloading road network from Overpass API"); let n = Self::from_bbox(bbox).await?; n.save_to_cache(&cache_path).await?; info!("Saved road network to file cache: {:?}", cache_path); n }; // Store in memory cache let network = Arc::new(network); cache.insert(cache_key, Arc::clone(&network)); Ok(network) } /// Downloads and builds road network from Overpass API. pub async fn from_bbox(bbox: &BoundingBox) -> Result { let query = format!( r#"[out:json][timeout:120]; ( way["highway"~"^(motorway|trunk|primary|secondary|tertiary|residential|unclassified|service|living_street)$"] ({},{},{},{}); ); (._;>;); out body;"#, bbox.min_lat, bbox.min_lng, bbox.max_lat, bbox.max_lng ); debug!("Overpass query:\n{}", query); info!("Preparing Overpass query for bbox: {:.4},{:.4} to {:.4},{:.4}", bbox.min_lat, bbox.min_lng, bbox.max_lat, bbox.max_lng); let client = reqwest::Client::builder() .connect_timeout(std::time::Duration::from_secs(30)) .read_timeout(std::time::Duration::from_secs(180)) .timeout(std::time::Duration::from_secs(180)) .user_agent("SolverForge/0.4.0") .build() .map_err(|e| RoutingError::Network(e.to_string()))?; info!("Sending request to Overpass API..."); let response = client .post(OVERPASS_URL) .body(query) .header("Content-Type", "text/plain") .send() .await .map_err(|e| { error!("Overpass request failed: {}", e); RoutingError::Network(e.to_string()) })?; info!("Received response: status={}", response.status()); if !response.status().is_success() { return Err(RoutingError::Network(format!( "Overpass API returned status {}", response.status() ))); } let osm_data: OverpassResponse = response .json() .await .map_err(|e| RoutingError::Parse(e.to_string()))?; info!( "Downloaded {} OSM elements", osm_data.elements.len() ); Self::build_from_osm(&osm_data) } /// Builds the road network from parsed OSM data. fn build_from_osm(osm: &OverpassResponse) -> Result { let mut network = Self::new(); // First pass: collect all nodes let mut nodes: HashMap = HashMap::new(); for elem in &osm.elements { if elem.elem_type == "node" { if let (Some(lat), Some(lon)) = (elem.lat, elem.lon) { nodes.insert(elem.id, (lat, lon)); } } } info!("Parsed {} nodes", nodes.len()); // Second pass: process ways and build graph let mut way_count = 0; for elem in &osm.elements { if elem.elem_type == "way" { if let Some(ref node_ids) = elem.nodes { let highway = elem.tags.as_ref().and_then(|t| t.highway.as_deref()); let oneway = elem.tags.as_ref().and_then(|t| t.oneway.as_deref()); let speed = get_speed_for_highway(highway.unwrap_or("residential")); let is_oneway = matches!(oneway, Some("yes") | Some("1")); // Process consecutive node pairs for window in node_ids.windows(2) { let n1_id = window[0]; let n2_id = window[1]; let Some(&(lat1, lng1)) = nodes.get(&n1_id) else { continue; }; let Some(&(lat2, lng2)) = nodes.get(&n2_id) else { continue; }; // Get or create node indices let idx1 = network.get_or_create_node(lat1, lng1); let idx2 = network.get_or_create_node(lat2, lng2); // Calculate edge properties let distance = haversine_distance(lat1, lng1, lat2, lng2); let travel_time = distance / speed; let edge_data = EdgeData { travel_time_s: travel_time, distance_m: distance, geometry: vec![(lat1, lng1), (lat2, lng2)], }; // Add forward edge network.graph.add_edge(idx1, idx2, edge_data.clone()); // Add reverse edge if not oneway if !is_oneway { network.graph.add_edge(idx2, idx1, edge_data); } } way_count += 1; } } } info!( "Built graph with {} nodes and {} edges from {} ways", network.graph.node_count(), network.graph.edge_count(), way_count ); Ok(network) } /// Gets or creates a node for the given coordinates. fn get_or_create_node(&mut self, lat: f64, lng: f64) -> NodeIndex { let key = coord_key(lat, lng); if let Some(&idx) = self.coord_to_node.get(&key) { idx } else { let idx = self.graph.add_node(NodeData { lat, lng }); self.coord_to_node.insert(key, idx); idx } } /// Finds the nearest road node to the given coordinates. pub fn snap_to_road(&self, lat: f64, lng: f64) -> Option { self.coord_to_node .iter() .min_by_key(|((lat_e7, lng_e7), _)| { let node_lat = *lat_e7 as f64 / 1e7; let node_lng = *lng_e7 as f64 / 1e7; OrderedFloat(haversine_distance(lat, lng, node_lat, node_lng)) }) .map(|(_, &idx)| idx) } /// Computes shortest path between two coordinates. /// /// Returns the route with full geometry following roads. pub fn route(&self, from: (f64, f64), to: (f64, f64)) -> Option { let start = self.snap_to_road(from.0, from.1)?; let end = self.snap_to_road(to.0, to.1)?; if start == end { return Some(RouteResult { duration_seconds: 0, distance_meters: 0.0, geometry: vec![from, to], }); } // Use A* with zero heuristic (equivalent to Dijkstra, but returns full path) let (cost, path) = astar( &self.graph, start, |n| n == end, |e| OrderedFloat(e.weight().travel_time_s), |_| OrderedFloat(0.0), )?; let total_time = cost.0; // Build geometry from path nodes let geometry: Vec<(f64, f64)> = path .iter() .filter_map(|&idx| self.graph.node_weight(idx).map(|n| (n.lat, n.lng))) .collect(); // Sum actual edge distances along the path let mut distance = 0.0; for window in path.windows(2) { if let Some(edge) = self.graph.find_edge(window[0], window[1]) { if let Some(weight) = self.graph.edge_weight(edge) { distance += weight.distance_m; } } } Some(RouteResult { duration_seconds: total_time.round() as i64, distance_meters: distance, geometry, }) } /// Computes route geometries for all location pairs. /// /// Returns a map from `(from_idx, to_idx)` to the route geometry. pub fn compute_all_geometries( &self, locations: &[(f64, f64)], ) -> HashMap<(usize, usize), Vec<(f64, f64)>> { self.compute_all_geometries_with_progress(locations, |_, _| {}) } /// Computes route geometries with row-level progress callback. /// /// The callback receives `(completed_row, total_rows)` after each source row is computed. /// For n locations, this computes n*(n-1) routes, calling the callback n times. /// /// # Example /// /// ``` /// # use vehicle_routing::routing::RoadNetwork; /// let network = RoadNetwork::new(); /// let locations = vec![(39.95, -75.16), (39.96, -75.17)]; /// let mut progress_calls = 0; /// let geometries = network.compute_all_geometries_with_progress(&locations, |row, total| { /// progress_calls += 1; /// assert!(row < total); /// }); /// assert_eq!(progress_calls, 2); // One call per source location /// ``` pub fn compute_all_geometries_with_progress( &self, locations: &[(f64, f64)], mut on_row_complete: F, ) -> HashMap<(usize, usize), Vec<(f64, f64)>> where F: FnMut(usize, usize), { let n = locations.len(); let mut geometries = HashMap::new(); for i in 0..n { for j in 0..n { if i == j { continue; } if let Some(result) = self.route(locations[i], locations[j]) { geometries.insert((i, j), result.geometry); } } // Report progress after each source row on_row_complete(i, n); } geometries } /// Computes all-pairs travel time matrix for given locations. /// /// Returns a matrix where `result[i][j]` is the travel time from location i to j. pub fn compute_matrix(&self, locations: &[(f64, f64)]) -> Vec> { self.compute_matrix_with_progress(locations, |_, _| {}) } /// Computes all-pairs travel time matrix with row-level progress callback. /// /// The callback receives `(completed_row, total_rows)` after each row is computed. /// This enables progress reporting during the O(n) Dijkstra runs. /// /// # Example /// /// ``` /// # use vehicle_routing::routing::RoadNetwork; /// let network = RoadNetwork::new(); /// let locations = vec![(39.95, -75.16), (39.96, -75.17)]; /// let mut progress_calls = 0; /// let matrix = network.compute_matrix_with_progress(&locations, |row, total| { /// progress_calls += 1; /// assert!(row < total); /// }); /// assert_eq!(progress_calls, 2); // One call per row /// assert_eq!(matrix.len(), 2); /// ``` pub fn compute_matrix_with_progress( &self, locations: &[(f64, f64)], mut on_row_complete: F, ) -> Vec> where F: FnMut(usize, usize), { let n = locations.len(); let mut matrix = vec![vec![0i64; n]; n]; // Snap all locations to nodes let nodes: Vec> = locations .iter() .map(|&(lat, lng)| self.snap_to_road(lat, lng)) .collect(); // Compute travel times row by row for i in 0..n { if let Some(from_node) = nodes[i] { // Run Dijkstra from this node let costs = dijkstra(&self.graph, from_node, None, |e| { OrderedFloat(e.weight().travel_time_s) }); for j in 0..n { if i == j { continue; } if let Some(to_node) = nodes[j] { if let Some(cost) = costs.get(&to_node) { matrix[i][j] = cost.0.round() as i64; } else { // No route found, use haversine estimate let dist = haversine_distance( locations[i].0, locations[i].1, locations[j].0, locations[j].1, ); matrix[i][j] = (dist / DEFAULT_SPEED_MPS).round() as i64; } } } } else { // Location couldn't be snapped, use haversine for all for j in 0..n { if i == j { continue; } let dist = haversine_distance( locations[i].0, locations[i].1, locations[j].0, locations[j].1, ); matrix[i][j] = (dist / DEFAULT_SPEED_MPS).round() as i64; } } // Report progress after each row on_row_complete(i, n); } matrix } /// Returns the number of nodes in the graph. pub fn node_count(&self) -> usize { self.graph.node_count() } /// Returns the number of edges in the graph. pub fn edge_count(&self) -> usize { self.graph.edge_count() } /// Loads road network from cache file. async fn load_from_cache(path: &Path) -> Result { let data = tokio::fs::read_to_string(path).await?; // Parse cached data, handling corrupted files let cached: CachedNetwork = match serde_json::from_str(&data) { Ok(c) => c, Err(e) => { info!("Cache file corrupted, will re-download: {}", e); let _ = tokio::fs::remove_file(path).await; return Err(RoutingError::Parse(e.to_string())); } }; // Check version - delete old format and re-download if cached.version != CACHE_VERSION { info!( "Cache version mismatch (got {}, need {}), will re-download", cached.version, CACHE_VERSION ); let _ = tokio::fs::remove_file(path).await; return Err(RoutingError::Parse("cache version mismatch".into())); } let mut network = Self::new(); // Rebuild graph from cached data for node in &cached.nodes { let idx = network.graph.add_node(NodeData { lat: node.lat, lng: node.lng, }); let key = coord_key(node.lat, node.lng); network.coord_to_node.insert(key, idx); } for edge in &cached.edges { let from = NodeIndex::new(edge.from); let to = NodeIndex::new(edge.to); network.graph.add_edge( from, to, EdgeData { travel_time_s: edge.travel_time_s, distance_m: edge.distance_m, geometry: vec![], }, ); } Ok(network) } /// Saves road network to cache file. async fn save_to_cache(&self, path: &Path) -> Result<(), RoutingError> { let nodes: Vec = self .graph .node_indices() .filter_map(|idx| { self.graph.node_weight(idx).map(|n| CachedNode { lat: n.lat, lng: n.lng, }) }) .collect(); let edges: Vec = self .graph .edge_indices() .filter_map(|idx| { let (from, to) = self.graph.edge_endpoints(idx)?; let weight = self.graph.edge_weight(idx)?; Some(CachedEdge { from: from.index(), to: to.index(), travel_time_s: weight.travel_time_s, distance_m: weight.distance_m, }) }) .collect(); let cached = CachedNetwork { version: CACHE_VERSION, nodes, edges, }; let data = serde_json::to_string(&cached).map_err(|e| RoutingError::Parse(e.to_string()))?; tokio::fs::write(path, data).await?; Ok(()) } } impl Default for RoadNetwork { fn default() -> Self { Self::new() } } // ============================================================================ // OSM Data Structures (Overpass API) // ============================================================================ #[derive(Debug, Deserialize)] struct OverpassResponse { elements: Vec, } #[derive(Debug, Deserialize)] struct OsmElement { #[serde(rename = "type")] elem_type: String, id: i64, lat: Option, lon: Option, nodes: Option>, tags: Option, } #[derive(Debug, Deserialize)] struct OsmTags { highway: Option, oneway: Option, /// Maxspeed tag (for future use with dynamic speed calculation). #[allow(dead_code)] maxspeed: Option, } // ============================================================================ // Cache Data Structures // ============================================================================ /// Cache format version. Bump this when changing the cache structure. const CACHE_VERSION: u32 = 1; #[derive(Debug, Serialize, Deserialize)] struct CachedNetwork { /// Cache format version for automatic invalidation. version: u32, nodes: Vec, edges: Vec, } #[derive(Debug, Serialize, Deserialize)] struct CachedNode { lat: f64, lng: f64, } #[derive(Debug, Serialize, Deserialize)] struct CachedEdge { from: usize, to: usize, travel_time_s: f64, distance_m: f64, } // ============================================================================ // Helper Functions // ============================================================================ /// Converts coordinates to a hash key (7 decimal places precision). fn coord_key(lat: f64, lng: f64) -> (i64, i64) { ((lat * 1e7).round() as i64, (lng * 1e7).round() as i64) } /// Returns speed in m/s for a highway type. fn get_speed_for_highway(highway: &str) -> f64 { let kmh = match highway { "motorway" | "motorway_link" => 100.0, "trunk" | "trunk_link" => 80.0, "primary" | "primary_link" => 60.0, "secondary" | "secondary_link" => 50.0, "tertiary" | "tertiary_link" => 40.0, "residential" => 30.0, "unclassified" => 30.0, "service" => 20.0, "living_street" => 10.0, _ => 30.0, }; kmh * 1000.0 / 3600.0 } /// Haversine distance between two points in meters. fn haversine_distance(lat1: f64, lng1: f64, lat2: f64, lng2: f64) -> f64 { const R: f64 = 6_371_000.0; // Earth radius in meters let lat1_rad = lat1.to_radians(); let lat2_rad = lat2.to_radians(); let dlat = (lat2 - lat1).to_radians(); let dlng = (lng2 - lng1).to_radians(); let a = (dlat / 2.0).sin().powi(2) + lat1_rad.cos() * lat2_rad.cos() * (dlng / 2.0).sin().powi(2); let c = 2.0 * a.sqrt().asin(); R * c } #[cfg(test)] mod tests { use super::*; #[test] fn test_haversine_distance() { // Philadelphia City Hall to Liberty Bell (~500m) let dist = haversine_distance(39.9526, -75.1635, 39.9496, -75.1503); assert!((dist - 1200.0).abs() < 100.0); // Approximately 1.2 km } #[test] fn test_coord_key() { let key = coord_key(39.9526, -75.1635); assert_eq!(key, (399526000, -751635000)); } #[test] fn test_bbox_expand() { let bbox = BoundingBox::new(39.9, -75.2, 40.0, -75.1); let expanded = bbox.expand(0.1); assert!(expanded.min_lat < bbox.min_lat); assert!(expanded.max_lat > bbox.max_lat); } #[test] fn test_empty_network() { let network = RoadNetwork::new(); assert_eq!(network.node_count(), 0); assert_eq!(network.edge_count(), 0); } #[test] fn test_snap_to_road_empty() { let network = RoadNetwork::new(); assert!(network.snap_to_road(39.95, -75.16).is_none()); } }