Skip to main content

pysochrone/
isochrone.rs

1use crate::graph::{self, SpatialGraph};
2use crate::overpass;
3use crate::cache;
4use crate::error::OsmGraphError;
5use crate::overpass::NetworkType;
6
7use geo::{ConcaveHull, ConvexHull, KNearestConcaveHull, MultiPoint, Polygon};
8use petgraph::algo::dijkstra;
9use petgraph::prelude::*;
10use std::collections::HashSet;
11use std::sync::Arc;
12
13#[derive(Debug, Copy, Clone)]
14pub enum HullType {
15    FastConcave,
16    Concave,
17    Convex,
18}
19
20pub fn calculate_isochrones(
21    graph: &DiGraph<graph::XmlNode, graph::XmlWay>,
22    start_node: NodeIndex,
23    time_limits: Vec<f64>,
24    hull_type: HullType,
25) -> Vec<Polygon> {
26    let mut isochrones = Vec::new();
27
28    // Compute shortest paths from start_node
29    let shortest_paths = dijkstra(graph, start_node, None, |e| {
30        let edge_weight = graph.edge_weight(e.id()).unwrap();
31        edge_weight.drive_travel_time
32    });
33
34    // For each time limit, find unique nodes that are within that limit
35    for &time_limit in &time_limits {
36        let isochrone_nodes = shortest_paths
37            .iter()
38            .filter_map(|(&node, &time)| if time <= time_limit { Some(node) } else { None })
39            .collect::<HashSet<_>>();
40
41        // Convert each node index in the isochrone to latitude/longitude
42        let isochrone_lat_lons = isochrone_nodes
43            .into_iter()
44            .map(|node_index| graph::node_to_latlon(graph, node_index))
45            .collect::<Vec<_>>();
46
47        let points: MultiPoint<f64> = isochrone_lat_lons.into();
48
49        let hull = match hull_type {
50            HullType::FastConcave => points.concave_hull(2.0),
51            HullType::Concave => points.k_nearest_concave_hull(3),
52            HullType::Convex => points.convex_hull(),
53        };
54
55        isochrones.push(hull);
56    }
57
58    isochrones
59}
60
61pub fn calculate_isochrones_concurrently(
62    graph: std::sync::Arc<DiGraph<graph::XmlNode, graph::XmlWay>>,
63    start_node: NodeIndex,
64    time_limits: Vec<f64>,
65    network_type: overpass::NetworkType,
66    hull_type: HullType,
67) -> Vec<Polygon> {
68    // Run Dijkstra once — results cover all time limits
69    let shortest_paths = dijkstra(&*graph, start_node, None, |e| {
70        let way = graph.edge_weight(e.id()).unwrap();
71        match network_type {
72            NetworkType::Walk => way.walk_travel_time,
73            NetworkType::Bike => way.bike_travel_time,
74            _ => way.drive_travel_time,
75        }
76    });
77
78    // Collect all (node, time) pairs once, then filter per time limit in parallel
79    let node_times: Vec<(NodeIndex, f64)> = shortest_paths.into_iter().collect();
80    let node_times = std::sync::Arc::new(node_times);
81
82    let mut handles = vec![];
83    for time_limit in time_limits {
84        let graph_clone = std::sync::Arc::clone(&graph);
85        let node_times_clone = std::sync::Arc::clone(&node_times);
86        let handle = std::thread::spawn(move || {
87            let points: MultiPoint<f64> = node_times_clone
88                .iter()
89                .filter(|(_, t)| *t <= time_limit)
90                .map(|(node, _)| graph::node_to_latlon(&*graph_clone, *node))
91                .collect::<Vec<_>>()
92                .into();
93
94            match hull_type {
95                HullType::FastConcave => points.concave_hull(2.0),
96                HullType::Concave => points.k_nearest_concave_hull(3),
97                HullType::Convex => points.convex_hull(),
98            }
99        });
100        handles.push(handle);
101    }
102
103    handles.into_iter().map(|h| h.join().unwrap()).collect()
104}
105
106
107pub async fn calculate_isochrones_from_point(
108    lat: f64,
109    lon: f64,
110    max_dist: Option<f64>,
111    time_limits: Vec<f64>,
112    network_type: overpass::NetworkType,
113    hull_type: HullType,
114    retain_all: bool,
115) -> Result<(Vec<Polygon>, SpatialGraph), OsmGraphError> {
116
117    // Auto-size bounding box if not provided.
118    // Use max time limit * a generous speed + 20% buffer to ensure the
119    // isochrone never saturates into a square at the bbox boundary.
120    let max_speed_m_per_s = match network_type {
121        NetworkType::Walk => 5.0 / 3.6,
122        NetworkType::Bike => 25.0 / 3.6,
123        _ => 120.0 / 3.6,
124    };
125    let max_time = time_limits.iter().cloned().fold(0.0_f64, f64::max);
126    let computed_dist = max_dist.unwrap_or_else(|| max_time * max_speed_m_per_s * 1.2);
127
128    let polygon_coord_str = overpass::bbox_from_point(lat, lon, computed_dist);
129    let query = overpass::create_overpass_query(&polygon_coord_str, network_type);
130    let graph_key = format!("{}:{}", query, retain_all);
131
132    let sg = if let Some(cached) = cache::check_cache(&graph_key)? {
133        cached
134    } else {
135        let xml = if let Some(cached_xml) = cache::check_xml_cache(&query)? {
136            cached_xml                                      // in-memory hit
137        } else if let Some(disk_xml) = cache::check_disk_xml_cache(&query) {
138            cache::insert_into_xml_cache(query.clone(), disk_xml.clone())?; // promote to memory
139            disk_xml                                        // disk hit
140        } else {
141            let fetched = overpass::make_request("https://overpass-api.de/api/interpreter", &query).await?;
142            cache::write_disk_xml_cache(&query, &fetched); // persist to disk (best-effort)
143            cache::insert_into_xml_cache(query.clone(), fetched.clone())?;
144            fetched                                         // network fetch
145        };
146
147        let parsed = graph::parse_xml(&xml)?;
148        if parsed.nodes.is_empty() {
149            return Err(OsmGraphError::EmptyGraph);
150        }
151        let g = graph::create_graph(parsed.nodes, parsed.ways, retain_all, false);
152        let sg = SpatialGraph::new(g);
153        cache::insert_into_cache(graph_key, sg.clone())?;
154        sg
155    };
156
157    let node_index = sg.nearest_node(lat, lon).ok_or(OsmGraphError::NodeNotFound)?;
158    let shared_graph = Arc::clone(&sg.graph); // O(1) refcount bump — no graph copy
159    let isochrones = calculate_isochrones_concurrently(
160        shared_graph,
161        node_index,
162        time_limits,
163        network_type,
164        hull_type,
165    );
166
167    Ok((isochrones, sg))
168}