rtzlib/geo/tz/
osm.rs

1//! The [OpenStreetMap](https://www.openstreetmap.org/) timezone lookup module.
2
3use std::{collections::HashMap, sync::OnceLock};
4
5use geo::{Contains, Coord};
6use rtz_core::{
7    base::types::Float,
8    geo::{
9        shared::{ConcreteVec, EncodableIds, HasGeometry, RoundLngLat},
10        tz::osm::OsmTimezone,
11    },
12};
13
14use crate::{
15    geo::shared::{HasItemData, HasLookupData},
16    CanPerformGeoLookup,
17};
18
19#[cfg(feature = "self-contained")]
20use include_bytes_aligned::include_bytes_aligned;
21
22// Trait impls.
23
24impl HasItemData for OsmTimezone {
25    fn get_mem_items() -> &'static ConcreteVec<OsmTimezone> {
26        static TIMEZONES: OnceLock<ConcreteVec<OsmTimezone>> = OnceLock::new();
27
28        #[cfg(feature = "self-contained")]
29        {
30            TIMEZONES.get_or_init(|| crate::geo::shared::decode_binary_data(TZ_BINCODE))
31        }
32
33        #[cfg(not(feature = "self-contained"))]
34        {
35            use rtz_core::geo::{shared::get_items_from_features, tz::osm::get_geojson_features_from_source};
36
37            TIMEZONES.get_or_init(|| {
38                let features = get_geojson_features_from_source();
39
40                get_items_from_features(features)
41            })
42        }
43    }
44}
45
46impl HasLookupData for OsmTimezone {
47    type Lookup = EncodableIds;
48
49    fn get_mem_lookup() -> &'static HashMap<RoundLngLat, Self::Lookup> {
50        static CACHE: OnceLock<HashMap<RoundLngLat, EncodableIds>> = OnceLock::new();
51
52        #[cfg(feature = "self-contained")]
53        {
54            CACHE.get_or_init(|| crate::geo::shared::decode_binary_data(LOOKUP_BINCODE))
55        }
56
57        #[cfg(not(feature = "self-contained"))]
58        {
59            use rtz_core::geo::shared::get_lookup_from_geometries;
60
61            CACHE.get_or_init(|| {
62                let cache = get_lookup_from_geometries(OsmTimezone::get_mem_items());
63
64                cache
65            })
66        }
67    }
68}
69
70// Special implementation of this for timezones since our timezone data covers the whole world.
71// Therefore, we can use the special optimization.
72impl CanPerformGeoLookup for OsmTimezone {
73    fn lookup(xf: Float, yf: Float) -> Vec<&'static Self> {
74        let x = xf.floor() as i16;
75        let y = yf.floor() as i16;
76
77        let Some(suggestions) = Self::get_lookup_suggestions(x, y) else {
78            return Vec::new();
79        };
80
81        // [ARoney] Optimization: If there is only one item, we can skip the more expensive
82        // intersection check.  Edges are weird, so we still need to check if the point is in the
83        // polygon at thg edges of the polar space.
84        if suggestions.len() == 1 && xf > -179. && xf < 179. && yf > -89. && yf < 89. {
85            return suggestions;
86        }
87
88        suggestions.into_iter().filter(|&i| i.geometry().contains(&Coord { x: xf, y: yf })).collect()
89    }
90}
91
92// Statics.
93
94#[cfg(all(host_family_unix, feature = "self-contained"))]
95static TZ_BINCODE: &[u8] = include_bytes_aligned!(8, "../../../../assets/osm_time_zones.bincode");
96#[cfg(all(host_family_windows, feature = "self-contained"))]
97static TZ_BINCODE: &[u8] = include_bytes_aligned!(8, "..\\..\\..\\..\\assets\\osm_time_zones.bincode");
98
99#[cfg(all(host_family_unix, feature = "self-contained"))]
100static LOOKUP_BINCODE: &[u8] = include_bytes_aligned!(8, "../../../../assets/osm_time_zone_lookup.bincode");
101#[cfg(all(host_family_windows, feature = "self-contained"))]
102static LOOKUP_BINCODE: &[u8] = include_bytes_aligned!(8, "..\\..\\..\\..\\assets\\osm_time_zone_lookup.bincode");
103
104// Tests.
105
106#[cfg(test)]
107mod tests {
108    use crate::geo::shared::{CanPerformGeoLookup, HasItemData, MapIntoItems};
109
110    use super::*;
111    use pretty_assertions::assert_eq;
112    use rayon::prelude::{IntoParallelIterator, ParallelIterator};
113    use rtz_core::base::types::Float;
114
115    #[test]
116    fn can_get_timezones() {
117        let timezones = OsmTimezone::get_mem_items();
118        assert_eq!(timezones.len(), 444);
119    }
120
121    #[test]
122    fn can_get_lookup() {
123        let cache = OsmTimezone::get_mem_lookup();
124        assert_eq!(cache.len(), 64_800);
125    }
126
127    #[test]
128    fn can_get_from_lookup() {
129        let cache = OsmTimezone::get_lookup_suggestions(-121, 46).unwrap();
130        assert_eq!(cache.len(), 1);
131    }
132
133    #[test]
134    fn can_perform_exact_lookup() {
135        assert_eq!(OsmTimezone::lookup_slow(-177.0, -15.0).len(), 1);
136        assert_eq!(OsmTimezone::lookup_slow(-121.0, 46.0)[0].identifier.as_ref(), "America/Los_Angeles");
137
138        assert_eq!(OsmTimezone::lookup_slow(179.9968, -67.0959).len(), 1);
139    }
140
141    #[test]
142    fn can_access_lookup() {
143        let cache = OsmTimezone::get_mem_lookup();
144
145        let tzs = cache.get(&(-177, -15)).map_into_items().unwrap() as Vec<&OsmTimezone>;
146        assert_eq!(tzs.len(), 1);
147
148        let tzs = cache.get(&(-121, 46)).map_into_items().unwrap() as Vec<&OsmTimezone>;
149        assert_eq!(tzs.len(), 1);
150
151        let tz = cache.get(&(-121, 46)).map_into_items().unwrap()[0] as &OsmTimezone;
152        assert_eq!(tz.identifier.as_ref(), "America/Los_Angeles");
153
154        let tzs = cache.get(&(-87, 38)).map_into_items().unwrap() as Vec<&OsmTimezone>;
155        assert_eq!(tzs.len(), 7);
156    }
157
158    #[test]
159    fn can_verify_lookup_assisted_accuracy() {
160        let x = rand::random::<Float>() * 360.0 - 180.0;
161
162        (0..100).into_par_iter().for_each(|_| {
163            let y = rand::random::<Float>() * 180.0 - 90.0;
164            let full = OsmTimezone::lookup_slow(x, y);
165            let lookup_assisted = OsmTimezone::lookup(x, y);
166
167            assert_eq!(
168                full.into_iter().map(|t| t.id).collect::<Vec<_>>(),
169                lookup_assisted.into_iter().map(|t| t.id).collect::<Vec<_>>(),
170                "({}, {})",
171                x,
172                y
173            );
174        });
175    }
176}