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