Skip to main content

pysochrone/
overpass.rs

1// Define an enum for network types
2#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3pub enum NetworkType {
4    Drive,
5    DriveService,
6    Walk,
7    Bike,
8    All,
9    AllPrivate,
10}
11
12// Custom error type for better error messages
13#[derive(Debug)]
14pub enum OverpassError {
15    RequestError(reqwest::Error),
16    InvalidNetworkType,
17}
18
19impl std::fmt::Display for OverpassError {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        match self {
22            OverpassError::RequestError(err) => write!(f, "Request Error: {}", err),
23            OverpassError::InvalidNetworkType => write!(f, "Invalid Network Type"),
24        }
25    }
26}
27
28// Function to get OSM filter
29pub fn get_osm_filter(network_type: NetworkType) -> Result<&'static str, OverpassError> {
30    match network_type {
31        NetworkType::Drive => Ok(
32            "[\"highway\"][\"area\"!~\"yes\"][\"highway\"!~\"abandoned|bridleway|bus_guideway|construction|corridor|cycleway|elevator|escalator|footway|no|path|pedestrian|planned|platform|proposed|raceway|razed|service|steps|track\"][\"motor_vehicle\"!~\"no\"][\"motorcar\"!~\"no\"][\"service\"!~\"alley|driveway|emergency_access|parking|parking_aisle|private\"]"
33        ),
34        NetworkType::DriveService => Ok(
35            "[\"highway\"][\"area\"!~\"yes\"][\"highway\"!~\"abandoned|bridleway|bus_guideway|construction|corridor|cycleway|elevator|escalator|footway|no|path|pedestrian|planned|platform|proposed|raceway|razed|steps|track\"][\"motor_vehicle\"!~\"no\"][\"motorcar\"!~\"no\"][\"service\"!~\"emergency_access|parking|parking_aisle|private\"]"
36        ),
37        NetworkType::Walk => Ok(
38            "[\"highway\"][\"area\"!~\"yes\"][\"highway\"!~\"abandoned|bus_guideway|construction|corridor|elevator|escalator|motor|no|planned|platform|proposed|raceway|razed\"][\"foot\"!~\"no\"][\"service\"!~\"private\"]"
39        ),
40        NetworkType::Bike => Ok(
41            "[\"highway\"][\"area\"!~\"yes\"][\"highway\"!~\"abandoned|bus_guideway|construction|corridor|elevator|escalator|footway|motor|no|planned|platform|proposed|raceway|razed|steps\"][\"bicycle\"!~\"no\"][\"service\"!~\"private\"]"
42        ),
43        NetworkType::All => Ok(
44            "[\"highway\"][\"area\"!~\"yes\"][\"highway\"!~\"abandoned|construction|no|planned|platform|proposed|raceway|razed\"][\"service\"!~\"private\"]"
45        ),
46        NetworkType::AllPrivate => Ok(
47            "[\"highway\"][\"area\"!~\"yes\"][\"highway\"!~\"abandoned|construction|no|planned|platform|proposed|raceway|razed\"]"
48        ),
49    }
50}
51
52// Function to create the Overpass query string
53pub fn create_overpass_query(polygon_coord_str: &str, network_type: NetworkType) -> String {
54    let filter = get_osm_filter(network_type).unwrap_or("");
55    format!("[out:xml][timeout:50];(way{}({});>;);out;", filter, polygon_coord_str)
56}
57
58// Reuse a single reqwest::Client across all HTTP calls in the library
59lazy_static::lazy_static! {
60    pub(crate) static ref CLIENT: reqwest::Client = reqwest::Client::builder()
61        .user_agent("layover_planner/0.1 (https://github.com/kyleloving/osm_graph)")
62        .build()
63        .expect("failed to build HTTP client");
64}
65
66// Function to make request to Overpass API
67pub async fn make_request(url: &str, query: &str) -> Result<String, reqwest::Error> {
68    let response = CLIENT
69        .post(url)
70        .form(&[("data", query)])
71        .send()
72        .await?;
73
74    if response.status().is_success() {
75        let response_text = response.text().await?;
76        Ok(response_text)
77    } else {
78        Err(response.error_for_status().unwrap_err())
79    }
80}
81
82/// Construct a `south,west,north,east` bounding box string from a point and radius.
83pub fn bbox_from_point(lat: f64, lon: f64, dist: f64) -> String {
84    const EARTH_RADIUS_M: f64 = 6_371_009.0;
85
86    // Calculate deltas
87    let delta_lat = (dist / EARTH_RADIUS_M) * (180.0 / std::f64::consts::PI);
88    let delta_lon = (dist / EARTH_RADIUS_M) * (180.0 / std::f64::consts::PI)
89        / (lat * std::f64::consts::PI / 180.0).cos();
90
91    // Calculate bounding box
92    let north = lat + delta_lat;
93    let south = lat - delta_lat;
94    let east = lon + delta_lon;
95    let west = lon - delta_lon;
96
97    // Construct polygon_coord_str for Overpass API query
98    format!("{},{},{},{}", south, west, north, east)
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_bbox_is_symmetric() {
107        let bbox = bbox_from_point(48.0, 11.0, 1000.0);
108        let parts: Vec<f64> = bbox.split(',').map(|s| s.parse().unwrap()).collect();
109        let (south, west, north, east) = (parts[0], parts[1], parts[2], parts[3]);
110        assert!((48.0 - south - (north - 48.0)).abs() < 1e-6);
111        assert!((11.0 - west - (east - 11.0)).abs() < 1e-6);
112    }
113
114    #[test]
115    fn test_bbox_larger_dist_gives_larger_box() {
116        let small = bbox_from_point(48.0, 11.0, 1_000.0);
117        let large = bbox_from_point(48.0, 11.0, 10_000.0);
118        let small_parts: Vec<f64> = small.split(',').map(|s| s.parse().unwrap()).collect();
119        let large_parts: Vec<f64> = large.split(',').map(|s| s.parse().unwrap()).collect();
120        assert!(large_parts[2] > small_parts[2]);
121    }
122}