rtzlib/geo/tz/
ned.rs

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