tzf_rs/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use geometry_rs::{Point, Polygon};
4use std::collections::HashMap;
5use std::f64::consts::PI;
6use std::vec;
7use tzf_rel::{load_preindex, load_reduced};
8pub mod gen;
9
10struct Item {
11    polys: Vec<Polygon>,
12    name: String,
13}
14
15impl Item {
16    fn contains_point(&self, p: &Point) -> bool {
17        for poly in &self.polys {
18            if poly.contains_point(*p) {
19                return true;
20            }
21        }
22        false
23    }
24}
25
26/// Finder works anywhere.
27///
28/// Finder use a fine tuned Ray casting algorithm implement [geometry-rs]
29/// which is Rust port of [geometry] by [Josh Baker].
30///
31/// [geometry-rs]: https://github.com/ringsaturn/geometry-rs
32/// [geometry]: https://github.com/tidwall/geometry
33/// [Josh Baker]: https://github.com/tidwall
34pub struct Finder {
35    all: Vec<Item>,
36    data_version: String,
37}
38
39impl Finder {
40    /// `from_pb` is used when you can use your own timezone data, as long as
41    /// it's compatible with Proto's desc.
42    ///
43    /// # Arguments
44    ///
45    /// * `tzs` - Timezones data.
46    ///
47    /// # Returns
48    ///
49    /// * `Finder` - A Finder instance.
50    #[must_use]
51    pub fn from_pb(tzs: gen::Timezones) -> Self {
52        let mut f = Self {
53            all: vec![],
54            data_version: tzs.version,
55        };
56        for tz in &tzs.timezones {
57            let mut polys: Vec<Polygon> = vec![];
58
59            for pbpoly in &tz.polygons {
60                let mut exterior: Vec<Point> = vec![];
61                for pbpoint in &pbpoly.points {
62                    exterior.push(Point {
63                        x: f64::from(pbpoint.lng),
64                        y: f64::from(pbpoint.lat),
65                    });
66                }
67
68                let mut interior: Vec<Vec<Point>> = vec![];
69
70                for holepoly in &pbpoly.holes {
71                    let mut holeextr: Vec<Point> = vec![];
72                    for holepoint in &holepoly.points {
73                        holeextr.push(Point {
74                            x: f64::from(holepoint.lng),
75                            y: f64::from(holepoint.lat),
76                        });
77                    }
78                    interior.push(holeextr);
79                }
80
81                let geopoly = geometry_rs::Polygon::new(exterior, interior);
82                polys.push(geopoly);
83            }
84
85            let item: Item = Item {
86                name: tz.name.to_string(),
87                polys,
88            };
89
90            f.all.push(item);
91        }
92        f
93    }
94
95    /// Example:
96    ///
97    /// ```rust
98    /// use tzf_rs::Finder;
99    ///
100    /// let finder = Finder::new();
101    /// assert_eq!("Asia/Shanghai", finder.get_tz_name(116.3883, 39.9289));
102    /// ```
103    #[must_use]
104    pub fn get_tz_name(&self, lng: f64, lat: f64) -> &str {
105        let direct_res = self._get_tz_name(lng, lat);
106        if !direct_res.is_empty() {
107            return direct_res;
108        }
109
110        for &dx in &[0.0, -0.01, 0.01, -0.02, 0.02] {
111            for &dy in &[0.0, -0.01, 0.01, -0.02, 0.02] {
112                let dlng = dx + lng;
113                let dlat = dy + lat;
114                let name = self._get_tz_name(dlng, dlat);
115                if !name.is_empty() {
116                    return name;
117                }
118            }
119        }
120        ""
121    }
122
123    fn _get_tz_name(&self, lng: f64, lat: f64) -> &str {
124        let p = geometry_rs::Point { x: lng, y: lat };
125        for item in &self.all {
126            if item.contains_point(&p) {
127                return &item.name;
128            }
129        }
130        ""
131    }
132
133    /// ```rust
134    /// use tzf_rs::Finder;
135    /// let finder = Finder::new();
136    /// println!("{:?}", finder.get_tz_names(116.3883, 39.9289));
137    /// ```
138    #[must_use]
139    pub fn get_tz_names(&self, lng: f64, lat: f64) -> Vec<&str> {
140        let mut ret: Vec<&str> = vec![];
141        let p = geometry_rs::Point { x: lng, y: lat };
142        for item in &self.all {
143            if item.contains_point(&p) {
144                ret.push(&item.name);
145            }
146        }
147        ret
148    }
149
150    /// Example:
151    ///
152    /// ```rust
153    /// use tzf_rs::Finder;
154    ///
155    /// let finder = Finder::new();
156    /// println!("{:?}", finder.timezonenames());
157    /// ```
158    #[must_use]
159    pub fn timezonenames(&self) -> Vec<&str> {
160        let mut ret: Vec<&str> = vec![];
161        for item in &self.all {
162            ret.push(&item.name);
163        }
164        ret
165    }
166
167    /// Example:
168    ///
169    /// ```rust
170    /// use tzf_rs::Finder;
171    ///
172    /// let finder = Finder::new();
173    /// println!("{:?}", finder.data_version());
174    /// ```
175    #[must_use]
176    pub fn data_version(&self) -> &str {
177        &self.data_version
178    }
179
180    /// Creates a new, empty `Finder`.
181    ///
182    /// Example:
183    ///
184    /// ```rust
185    /// use tzf_rs::Finder;
186    ///
187    /// let finder = Finder::new();
188    /// ```
189    #[must_use]
190    pub fn new() -> Self {
191        Self::default()
192    }
193}
194
195/// Creates a new, empty `Finder`.
196///
197/// Example:
198///
199/// ```rust
200/// use tzf_rs::Finder;
201///
202/// let finder = Finder::default();
203/// ```
204impl Default for Finder {
205    fn default() -> Self {
206        // let file_bytes = include_bytes!("data/combined-with-oceans.reduce.pb").to_vec();
207        let file_bytes: Vec<u8> = load_reduced();
208        Self::from_pb(gen::Timezones::try_from(file_bytes).unwrap_or_default())
209    }
210}
211
212/// deg2num is used to convert longitude, latitude to [Slippy map tilenames]
213/// under specific zoom level.
214///
215/// [Slippy map tilenames]: https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
216///
217/// Example:
218///
219/// ```rust
220/// use tzf_rs::deg2num;
221/// let ret = deg2num(116.3883, 39.9289, 7);
222/// assert_eq!((105, 48), ret);
223/// ```
224#[must_use]
225#[allow(
226    clippy::cast_precision_loss,
227    clippy::cast_possible_truncation,
228    clippy::similar_names
229)]
230pub fn deg2num(lng: f64, lat: f64, zoom: i64) -> (i64, i64) {
231    let lat_rad = lat.to_radians();
232    let n = f64::powf(2.0, zoom as f64);
233    let xtile = (lng + 180.0) / 360.0 * n;
234    let ytile = (1.0 - lat_rad.tan().asinh() / PI) / 2.0 * n;
235
236    // Possible precision loss here
237    (xtile as i64, ytile as i64)
238}
239
240/// `FuzzyFinder` blazing fast for most places on earth, use a preindex data.
241/// Not work for places around borders.
242///
243/// `FuzzyFinder` store all preindex's tiles data in a `HashMap`,
244/// It iterate all zoom levels for input's longitude and latitude to build
245/// map key to to check if in map.
246///
247/// It's is very fast and use about 400ns to check if has preindex.
248/// It work for most places on earth and here is a quick loop of preindex data:
249/// ![](https://user-images.githubusercontent.com/13536789/200174943-7d40661e-bda5-4b79-a867-ec637e245a49.png)
250pub struct FuzzyFinder {
251    min_zoom: i64,
252    max_zoom: i64,
253    all: HashMap<(i64, i64, i64), Vec<String>>, // K: <x,y,z>
254    data_version: String,
255}
256
257impl Default for FuzzyFinder {
258    /// Creates a new, empty `FuzzyFinder`.
259    ///
260    /// ```rust
261    /// use tzf_rs::FuzzyFinder;
262    ///
263    /// let finder = FuzzyFinder::default();
264    /// ```
265    fn default() -> Self {
266        let file_bytes: Vec<u8> = load_preindex();
267        Self::from_pb(gen::PreindexTimezones::try_from(file_bytes).unwrap_or_default())
268    }
269}
270
271impl FuzzyFinder {
272    #[must_use]
273    pub fn from_pb(tzs: gen::PreindexTimezones) -> Self {
274        let mut f = Self {
275            min_zoom: i64::from(tzs.agg_zoom),
276            max_zoom: i64::from(tzs.idx_zoom),
277            all: HashMap::new(),
278            data_version: tzs.version,
279        };
280        for item in &tzs.keys {
281            let key = (i64::from(item.x), i64::from(item.y), i64::from(item.z));
282            f.all.entry(key).or_insert_with(std::vec::Vec::new);
283            f.all.get_mut(&key).unwrap().push(item.name.to_string());
284            f.all.get_mut(&key).unwrap().sort();
285        }
286        f
287    }
288
289    /// Retrieves the time zone name for the given longitude and latitude.
290    ///
291    /// # Arguments
292    ///
293    /// * `lng` - Longitude
294    /// * `lat` - Latitude
295    ///
296    /// # Example:
297    ///
298    /// ```rust
299    /// use tzf_rs::FuzzyFinder;
300    ///
301    /// let finder = FuzzyFinder::new();
302    /// assert_eq!("Asia/Shanghai", finder.get_tz_name(116.3883, 39.9289));
303    /// ```
304    ///
305    /// # Panics
306    ///
307    /// - Panics if `lng` or `lat` is out of range.
308    /// - Panics if `lng` or `lat` is not a number.
309    #[must_use]
310    pub fn get_tz_name(&self, lng: f64, lat: f64) -> &str {
311        for zoom in self.min_zoom..self.max_zoom {
312            let idx = deg2num(lng, lat, zoom);
313            let k = &(idx.0, idx.1, zoom);
314            let ret = self.all.get(k);
315            if ret.is_none() {
316                continue;
317            }
318            return ret.unwrap().first().unwrap();
319        }
320        ""
321    }
322
323    pub fn get_tz_names(&self, lng: f64, lat: f64) -> Vec<&str> {
324        let mut names: Vec<&str> = vec![];
325        for zoom in self.min_zoom..self.max_zoom {
326            let idx = deg2num(lng, lat, zoom);
327            let k = &(idx.0, idx.1, zoom);
328            let ret = self.all.get(k);
329            if ret.is_none() {
330                continue;
331            }
332            for item in ret.unwrap() {
333                names.push(item);
334            }
335        }
336        names
337    }
338
339    /// Gets the version of the data used by this `FuzzyFinder`.
340    ///
341    /// # Returns
342    ///
343    /// The version of the data used by this `FuzzyFinder` as a `&str`.
344    ///
345    /// # Example:
346    ///
347    /// ```rust
348    /// use tzf_rs::FuzzyFinder;
349    ///
350    /// let finder = FuzzyFinder::new();
351    /// println!("{:?}", finder.data_version());
352    /// ```
353    #[must_use]
354    pub fn data_version(&self) -> &str {
355        &self.data_version
356    }
357
358    /// Creates a new, empty `FuzzyFinder`.
359    ///
360    /// ```rust
361    /// use tzf_rs::FuzzyFinder;
362    ///
363    /// let finder = FuzzyFinder::default();
364    /// ```
365    #[must_use]
366    pub fn new() -> Self {
367        Self::default()
368    }
369}
370
371/// It's most recommend to use, combine both [`Finder`] and [`FuzzyFinder`],
372/// if [`FuzzyFinder`] got no data, then use [`Finder`].
373pub struct DefaultFinder {
374    finder: Finder,
375    fuzzy_finder: FuzzyFinder,
376}
377
378impl Default for DefaultFinder {
379    /// Creates a new, empty `DefaultFinder`.
380    ///
381    /// # Example
382    ///
383    /// ```rust
384    /// use tzf_rs::DefaultFinder;
385    /// let finder = DefaultFinder::new();
386    /// ```
387    fn default() -> Self {
388        let finder = Finder::default();
389        let fuzzy_finder = FuzzyFinder::default();
390
391        Self {
392            finder,
393            fuzzy_finder,
394        }
395    }
396}
397
398impl DefaultFinder {
399    /// ```rust
400    /// use tzf_rs::DefaultFinder;
401    /// let finder = DefaultFinder::new();
402    /// assert_eq!("Asia/Shanghai", finder.get_tz_name(116.3883, 39.9289));
403    /// ```
404    #[must_use]
405    pub fn get_tz_name(&self, lng: f64, lat: f64) -> &str {
406        // The simplified polygon data contains some empty areas where not covered by any timezone.
407        // It's not a bug but a limitation of the simplified algorithm.
408        //
409        // To handle this, auto shift the point a little bit to find the nearest timezone.
410        let res = self.get_tz_names(lng, lat);
411        if !res.is_empty() {
412            return res.first().unwrap();
413        }
414        ""
415    }
416
417    /// ```rust
418    /// use tzf_rs::DefaultFinder;
419    /// let finder = DefaultFinder::new();
420    /// println!("{:?}", finder.get_tz_names(116.3883, 39.9289));
421    /// ```
422    #[must_use]
423    pub fn get_tz_names(&self, lng: f64, lat: f64) -> Vec<&str> {
424        for &dx in &[0.0, -0.01, 0.01, -0.02, 0.02] {
425            for &dy in &[0.0, -0.01, 0.01, -0.02, 0.02] {
426                let dlng = dx + lng;
427                let dlat = dy + lat;
428                let fuzzy_names = self.fuzzy_finder.get_tz_names(dlng, dlat);
429                if !fuzzy_names.is_empty() {
430                    return fuzzy_names;
431                }
432                let names = self.finder.get_tz_names(dlng, dlat);
433                if !names.is_empty() {
434                    return names;
435                }
436            }
437        }
438        Vec::new() // Return empty vector if no timezone is found
439    }
440
441    /// Returns all time zone names as a `Vec<&str>`.
442    ///
443    /// ```rust
444    /// use tzf_rs::DefaultFinder;
445    /// let finder = DefaultFinder::new();
446    /// println!("{:?}", finder.timezonenames());
447    /// ```
448    #[must_use]
449    pub fn timezonenames(&self) -> Vec<&str> {
450        self.finder.timezonenames()
451    }
452
453    /// Returns the version of the data used by this `DefaultFinder` as a `&str`.
454    ///
455    /// Example:
456    ///
457    /// ```rust
458    /// use tzf_rs::DefaultFinder;
459    ///
460    /// let finder = DefaultFinder::new();
461    /// println!("{:?}", finder.data_version());
462    /// ```
463    #[must_use]
464    pub fn data_version(&self) -> &str {
465        &self.finder.data_version
466    }
467
468    /// Creates a new instance of `DefaultFinder`.
469    ///
470    /// ```rust
471    /// use tzf_rs::DefaultFinder;
472    /// let finder = DefaultFinder::new();
473    /// ```
474    #[must_use]
475    pub fn new() -> Self {
476        Self::default()
477    }
478}