summaryrefslogtreecommitdiffstats
path: root/flood_api/src/osm.rs
blob: f410357c7ffe280fef88402dfb62e19e2de92297 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
//! OpenStreetMap data for a region via Overpass: rivers and water bodies (stand-in for the CEMS
//! hydrography layers the model was trained on) and emergency facilities.
use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{path::Path, time::Duration};

#[path = "pbf.rs"]
mod pbf;
#[path = "osm_store.rs"]
pub mod store;
pub use pbf::Road;
/// The extracts directly, bypassing the store (for check-osm-store)
pub use pbf::{read as pbf_read, read_roads as pbf_read_roads};

/// Roads and shelters for the bbox: from the OSM store if it covers it, else the extracts
pub fn read_roads(dir: &Path, w: f64, s: f64, e: f64, n: f64) -> Result<Option<(Vec<Road>, Vec<Facility>)>> {
    if let Some(r) = store::get().map(|st| st.roads(w, s, e, n)).transpose()?.flatten() {
        return Ok(Some(r));
    }
    pbf::read_roads(dir, w, s, e, n)
}

const MIRRORS: [&str; 3] = [
    "https://overpass-api.de/api/interpreter",
    "https://overpass.private.coffee/api/interpreter",
    "https://maps.mail.ru/osm/tools/overpass/api/interpreter",
];
const USER_AGENT: &str = "eu-ai-challenge-flood-api/0.1";
/// Bump when the extracted OSM content changes, so old per-bbox caches are not reused
const CACHE_VERSION: u32 = 2;

/// Places people can be brought to. `shelter`: OSM-mapped shelters and assembly points.
/// `candidate_shelter`: schools and sports halls, often used as emergency shelters (an assumption:
/// OSM does not say which ones are designated, nor their capacity).
pub fn shelter_kind(k: &str, v: &str) -> Option<&'static str> {
    match (k, v) {
        ("social_facility", "shelter") | ("emergency", "assembly_point") => Some("shelter"),
        ("amenity", "school") | ("leisure", "sports_hall" | "sports_centre") | ("building", "sports_hall") => Some("candidate_shelter"),
        _ => None,
    }
}

#[derive(Serialize, Deserialize, Clone)]
pub struct Facility {
    pub kind: String,
    pub name: Option<String>,
    pub lon: f64,
    pub lat: f64,
}

#[derive(Serialize, Deserialize)]
pub struct Osm {
    /// River / stream / canal centrelines, lon/lat
    pub rivers: Vec<Vec<(f64, f64)>>,
    /// Closed outer rings of lakes, reservoirs, river areas and wetlands, lon/lat
    pub water: Vec<Vec<(f64, f64)>>,
    pub facilities: Vec<Facility>,
}

/// OSM data for the bbox: the OSM store if it covers it (fast, nothing cached per bbox), else the
/// local Geofabrik extracts in `pbf_dir`, else Overpass; those two are cached on disk by bbox.
pub fn fetch(w: f64, s: f64, e: f64, n: f64, pbf_dir: &Path, cache_dir: &Path) -> Result<Osm> {
    if let Some(osm) = store::get().map(|st| st.osm(w, s, e, n)).transpose()?.flatten() {
        return Ok(dedup_facilities(osm));
    }
    let bb = format!("{s:.4},{w:.4},{n:.4},{e:.4}");
    let cache = cache_dir.join(format!("osm_v{CACHE_VERSION}_{}.json", bb.replace(',', "_")));
    if cache.exists() {
        return Ok(serde_json::from_str(&std::fs::read_to_string(&cache)?)?);
    }
    let osm = match pbf_dir.is_dir().then(|| pbf::read(pbf_dir, w, s, e, n)).transpose()?.flatten() {
        Some(osm) => osm,
        None => parse(&serde_json::from_str(&overpass(&bb)?)?)?,
    };
    let osm = dedup_facilities(osm);
    std::fs::create_dir_all(cache_dir)?;
    std::fs::write(&cache, serde_json::to_string(&osm)?)?;
    Ok(osm)
}

/// Extracts overlap at borders: drop facilities found twice
fn dedup_facilities(mut osm: Osm) -> Osm {
    let mut seen = std::collections::HashSet::new();
    osm.facilities.retain(|f| seen.insert((f.kind.clone(), (f.lon * 1e5) as i64, (f.lat * 1e5) as i64)));
    osm
}

fn overpass(bb: &str) -> Result<String> {
    {
        let q = format!(
            r#"[out:json][timeout:180];
(way["waterway"~"^(river|stream|canal|riverbank)$"]({bb});
 way["natural"~"^(water|wetland)$"]({bb});
 relation["natural"~"^(water|wetland)$"]({bb});
 relation["waterway"="riverbank"]({bb}););
out geom;
(nwr["amenity"~"^(fire_station|hospital|police)$"]({bb});
 nwr["emergency"~"^(ambulance_station|assembly_point)$"]({bb});
 nwr["social_facility"="shelter"]({bb});
 nwr["amenity"="school"]({bb});
 nwr["leisure"~"^(sports_hall|sports_centre)$"]({bb});
 nwr["building"="sports_hall"]({bb}););
out center tags;"#
        );
        let client = reqwest::blocking::Client::builder()
            .user_agent(USER_AGENT)
            .timeout(Duration::from_secs(200))
            .build()?;
        let mut last = String::new();
        let mut got = None;
        for url in MIRRORS {
            match client.post(url).form(&[("data", &q)]).send() {
                Ok(r) if r.status().is_success() => {
                    got = Some(r.text()?);
                    break;
                }
                Ok(r) => last = format!("{url}: HTTP {}", r.status()),
                Err(err) => last = format!("{url}: {err}"),
            }
        }
        let Some(text) = got else { bail!("all Overpass mirrors failed, last: {last}") };
        Ok(text)
    }
}

fn line(geom: &Value) -> Vec<(f64, f64)> {
    geom.as_array()
        .map(|pts| pts.iter().filter_map(|p| Some((p["lon"].as_f64()?, p["lat"].as_f64()?))).collect())
        .unwrap_or_default()
}

/// Join open member ways end to end into closed rings (multipolygon outers are often split).
fn rings(mut parts: Vec<Vec<(f64, f64)>>) -> Vec<Vec<(f64, f64)>> {
    let mut out = Vec::new();
    while let Some(mut ring) = parts.pop() {
        while ring.first() != ring.last() {
            let end = *ring.last().unwrap();
            let Some(i) = parts.iter().position(|p| p.first() == Some(&end) || p.last() == Some(&end)) else { break };
            let mut next = parts.swap_remove(i);
            if next.first() != Some(&end) {
                next.reverse();
            }
            ring.extend(next.into_iter().skip(1));
        }
        if ring.len() >= 4 && ring.first() == ring.last() {
            out.push(ring);
        }
    }
    out
}

fn parse(v: &Value) -> Result<Osm> {
    let mut osm = Osm { rivers: vec![], water: vec![], facilities: vec![] };
    for el in v["elements"].as_array().into_iter().flatten() {
        let tags = &el["tags"];
        let tag = |k: &str| tags[k].as_str().unwrap_or("");
        let kind = match (tag("amenity"), tag("emergency")) {
            (a @ ("fire_station" | "hospital" | "police"), _) => Some(a),
            (_, "ambulance_station") => Some("ambulance_station"),
            _ => tags.as_object().into_iter().flatten().find_map(|(k, v)| shelter_kind(k, v.as_str()?)),
        };
        if let Some(kind) = kind {
            let (lon, lat) = match el["type"].as_str() {
                Some("node") => (el["lon"].as_f64(), el["lat"].as_f64()),
                _ => (el["center"]["lon"].as_f64(), el["center"]["lat"].as_f64()),
            };
            if let (Some(lon), Some(lat)) = (lon, lat) {
                let name = tags["name"].as_str().map(str::to_owned);
                osm.facilities.push(Facility { kind: kind.into(), name, lon, lat });
            }
            continue;
        }
        match el["type"].as_str() {
            Some("way") if matches!(tag("waterway"), "river" | "stream" | "canal") => osm.rivers.push(line(&el["geometry"])),
            Some("way") => osm.water.extend(rings(vec![line(&el["geometry"])])),
            Some("relation") => {
                let outers = el["members"].as_array().into_iter().flatten()
                    .filter(|m| m["role"] == "outer")
                    .map(|m| line(&m["geometry"]))
                    .collect();
                osm.water.extend(rings(outers));
            }
            _ => {}
        }
    }
    Ok(osm)
}