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/// 
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}