Skip to main content

rtz_core/geo/admin/
osm.rs

1//! All of the geo-specific functions for OSM admin lookups.
2
3use geo::Geometry;
4use serde_json::{Map, Value};
5use std::borrow::Cow;
6
7#[cfg(feature = "self-contained")]
8use bincode::{
9    de::{BorrowDecoder, Decoder},
10    error::DecodeError,
11    BorrowDecode, Decode, Encode,
12};
13
14use crate::{
15    base::types::Float,
16    geo::shared::{get_geojson_feature_from_string, simplify_geometry, CanGetGeoJsonFeaturesFromSource, EncodableGeometry, EncodableString, HasGeometry, HasProperties, IdFeaturePair},
17};
18
19use super::shared::IsAdmin;
20
21// Constants.
22
23#[cfg(not(feature = "extrasimplified"))]
24const SIMPLIFICATION_EPSILON: Float = 0.001;
25#[cfg(feature = "extrasimplified")]
26const SIMPLIFICATION_EPSILON: Float = 0.1;
27
28// Helpers.
29
30/// Get the GeoJSON [`geojson::Feature`]s from the source.
31#[cfg(not(target_family = "wasm"))]
32#[cfg_attr(coverage_nightly, coverage(off))]
33pub fn get_geojson_features_from_source() -> geojson::FeatureCollection {
34    use rayon::prelude::{IntoParallelIterator, ParallelIterator};
35
36    let admin_dirs = std::env::var("RTZ_OSM_ADMIN_DIRS")
37        .expect("RTZ_OSM_ADMIN_DIRS must be set (semicolon-separated admin GeoJSON directories) to regenerate OSM admin data");
38    let paths = admin_dirs.split(';').collect::<Vec<_>>();
39    let mut files = Vec::new();
40
41    for path in paths {
42        let mut path_files = std::fs::read_dir(path)
43            .unwrap()
44            .filter(|f| f.as_ref().unwrap().file_name().to_str().unwrap().ends_with(".geojson"))
45            .map(|f| f.unwrap())
46            .collect::<Vec<_>>();
47
48        files.append(&mut path_files);
49    }
50
51    let features = files
52        .into_par_iter()
53        .filter(|f| {
54            let md = f.metadata().unwrap();
55
56            md.len() != 0
57        })
58        .map(|f| {
59            let json = std::fs::read_to_string(f.path()).unwrap();
60            get_geojson_feature_from_string(&json)
61        })
62        .collect::<Vec<_>>();
63
64    geojson::FeatureCollection {
65        bbox: None,
66        features,
67        foreign_members: None,
68    }
69}
70
71/// The name of the timezone bincode file.
72pub static ADMIN_BINCODE_DESTINATION_NAME: &str = "osm_admins.bincode";
73/// The name of the cache bincode file.
74pub static LOOKUP_BINCODE_DESTINATION_NAME: &str = "osm_admin_lookup.bincode";
75
76// Types.
77
78/// A representation of the [OpenStreetMap](https://www.openstreetmap.org/)
79/// [geojson](https://github.com/evansiroky/timezone-boundary-builder)
80/// [`geojson::Feature`]s for administrative areas.
81#[derive(Debug)]
82#[cfg_attr(feature = "self-contained", derive(Encode))]
83pub struct OsmAdmin {
84    /// The index of the [`OsmAdmin`] in the global static cache.
85    ///
86    /// This is is not stable across builds or new data sets.  It is merely unique during a single build.
87    pub id: usize,
88
89    /// The OSM relation id of the admin area (e.g., `1473947`), or `0` if the source boundary was
90    /// not relation-backed. Unlike [`OsmAdmin::id`], this is stable across builds and data sets.
91    pub relation_id: u64,
92
93    /// The `name` of the [`OsmAdmin`] (e.g., `Burkina Faso`).
94    pub name: EncodableString,
95    /// The `level` of the [`OsmAdmin`] (e.g., `3`).
96    pub level: usize,
97
98    /// The geometry of the [`OsmAdmin`].
99    pub geometry: EncodableGeometry,
100}
101
102#[cfg(feature = "self-contained")]
103impl<Context> Decode<Context> for OsmAdmin {
104    fn decode<D>(decoder: &mut D) -> Result<Self, DecodeError>
105    where
106        D: Decoder<Context = Context>,
107    {
108        let id = usize::decode(decoder)?;
109        let relation_id = u64::decode(decoder)?;
110        let name = EncodableString::decode(decoder)?;
111        let level = usize::decode(decoder)?;
112        let geometry = EncodableGeometry::decode(decoder)?;
113
114        Ok(OsmAdmin { id, relation_id, name, level, geometry })
115    }
116}
117
118#[cfg(feature = "self-contained")]
119impl<'de, Context> BorrowDecode<'de, Context> for OsmAdmin
120where
121    'de: 'static,
122{
123    fn borrow_decode<D>(decoder: &mut D) -> Result<Self, DecodeError>
124    where
125        D: BorrowDecoder<'de, Context = Context>,
126    {
127        let id = usize::decode(decoder)?;
128        let relation_id = u64::decode(decoder)?;
129        let name = EncodableString::borrow_decode(decoder)?;
130        let level = usize::decode(decoder)?;
131        let geometry = EncodableGeometry::borrow_decode(decoder)?;
132
133        Ok(OsmAdmin { id, relation_id, name, level, geometry })
134    }
135}
136
137impl PartialEq for OsmAdmin {
138    fn eq(&self, other: &Self) -> bool {
139        self.id == other.id
140    }
141}
142
143impl From<IdFeaturePair> for OsmAdmin {
144    fn from(value: IdFeaturePair) -> OsmAdmin {
145        let id = value.0;
146        let properties = value.1.properties.as_ref().unwrap();
147        let geometry = value.1.geometry.as_ref().unwrap();
148
149        // Read defensively: a way-backed boundary can lack `relation_id`, and a single missing
150        // value would otherwise panic the whole regen. `0` marks "unknown" (no OSM id is ever 0).
151        let relation_id = properties.get("relation_id").and_then(|v| v.as_u64()).unwrap_or(0);
152        let name = EncodableString(Cow::Owned(properties.get("name").unwrap().as_str().unwrap().to_string()));
153        let level = properties.get("admin_level").unwrap().as_u64().unwrap() as usize;
154
155        let geometry: Geometry<Float> = geometry.value.clone().try_into().unwrap();
156        let geometry = EncodableGeometry(simplify_geometry(geometry, SIMPLIFICATION_EPSILON));
157
158        OsmAdmin { id, relation_id, name, level, geometry }
159    }
160}
161
162impl IsAdmin for OsmAdmin {
163    fn name(&self) -> &str {
164        self.name.as_ref()
165    }
166}
167
168impl HasGeometry for OsmAdmin {
169    fn id(&self) -> usize {
170        self.id
171    }
172
173    fn geometry(&self) -> &Geometry<Float> {
174        &self.geometry.0
175    }
176}
177
178impl HasProperties for OsmAdmin {
179    fn properties(&self) -> Map<String, Value> {
180        let mut properties = Map::new();
181
182        properties.insert("name".to_string(), Value::String(self.name.to_string()));
183        properties.insert("level".to_string(), Value::String(self.level.to_string()));
184
185        properties
186    }
187}
188
189#[cfg(not(target_family = "wasm"))]
190impl CanGetGeoJsonFeaturesFromSource for OsmAdmin {
191    fn get_geojson_features_from_source() -> geojson::FeatureCollection {
192        get_geojson_features_from_source()
193    }
194}