tzf_rs/lib.rs
1#![doc = include_str!("../README.md")]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3
4use geometry_rs::{Point, Polygon};
5#[cfg(feature = "export-geojson")]
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::f64::consts::PI;
9use std::vec;
10use tzf_rel::{load_preindex, load_reduced};
11pub mod pbgen;
12
13struct Item {
14 polys: Vec<Polygon>,
15 name: String,
16}
17
18impl Item {
19 fn contains_point(&self, p: &Point) -> bool {
20 for poly in &self.polys {
21 if poly.contains_point(*p) {
22 return true;
23 }
24 }
25 false
26 }
27}
28
29/// Finder works anywhere.
30///
31/// Finder use a fine tuned Ray casting algorithm implement [geometry-rs]
32/// which is Rust port of [geometry] by [Josh Baker].
33///
34/// [geometry-rs]: https://github.com/ringsaturn/geometry-rs
35/// [geometry]: https://github.com/tidwall/geometry
36/// [Josh Baker]: https://github.com/tidwall
37pub struct Finder {
38 all: Vec<Item>,
39 data_version: String,
40}
41
42impl Finder {
43 /// `from_pb` is used when you can use your own timezone data, as long as
44 /// it's compatible with Proto's desc.
45 ///
46 /// # Arguments
47 ///
48 /// * `tzs` - Timezones data.
49 ///
50 /// # Returns
51 ///
52 /// * `Finder` - A Finder instance.
53 #[must_use]
54 pub fn from_pb(tzs: pbgen::Timezones) -> Self {
55 let mut f = Self {
56 all: vec![],
57 data_version: tzs.version,
58 };
59 for tz in &tzs.timezones {
60 let mut polys: Vec<Polygon> = vec![];
61
62 for pbpoly in &tz.polygons {
63 let mut exterior: Vec<Point> = vec![];
64 for pbpoint in &pbpoly.points {
65 exterior.push(Point {
66 x: f64::from(pbpoint.lng),
67 y: f64::from(pbpoint.lat),
68 });
69 }
70
71 let mut interior: Vec<Vec<Point>> = vec![];
72
73 for holepoly in &pbpoly.holes {
74 let mut holeextr: Vec<Point> = vec![];
75 for holepoint in &holepoly.points {
76 holeextr.push(Point {
77 x: f64::from(holepoint.lng),
78 y: f64::from(holepoint.lat),
79 });
80 }
81 interior.push(holeextr);
82 }
83
84 let geopoly = geometry_rs::Polygon::new(exterior, interior);
85 polys.push(geopoly);
86 }
87
88 let item: Item = Item {
89 name: tz.name.to_string(),
90 polys,
91 };
92
93 f.all.push(item);
94 }
95 f
96 }
97
98 /// Example:
99 ///
100 /// ```rust
101 /// use tzf_rs::Finder;
102 ///
103 /// let finder = Finder::new();
104 /// assert_eq!("Asia/Shanghai", finder.get_tz_name(116.3883, 39.9289));
105 /// ```
106 #[must_use]
107 pub fn get_tz_name(&self, lng: f64, lat: f64) -> &str {
108 let direct_res = self._get_tz_name(lng, lat);
109 if !direct_res.is_empty() {
110 return direct_res;
111 }
112
113 for &dx in &[0.0, -0.01, 0.01, -0.02, 0.02] {
114 for &dy in &[0.0, -0.01, 0.01, -0.02, 0.02] {
115 let dlng = dx + lng;
116 let dlat = dy + lat;
117 let name = self._get_tz_name(dlng, dlat);
118 if !name.is_empty() {
119 return name;
120 }
121 }
122 }
123 ""
124 }
125
126 fn _get_tz_name(&self, lng: f64, lat: f64) -> &str {
127 let p = geometry_rs::Point { x: lng, y: lat };
128 for item in &self.all {
129 if item.contains_point(&p) {
130 return &item.name;
131 }
132 }
133 ""
134 }
135
136 /// ```rust
137 /// use tzf_rs::Finder;
138 /// let finder = Finder::new();
139 /// println!("{:?}", finder.get_tz_names(116.3883, 39.9289));
140 /// ```
141 #[must_use]
142 pub fn get_tz_names(&self, lng: f64, lat: f64) -> Vec<&str> {
143 let mut ret: Vec<&str> = vec![];
144 let p = geometry_rs::Point { x: lng, y: lat };
145 for item in &self.all {
146 if item.contains_point(&p) {
147 ret.push(&item.name);
148 }
149 }
150 ret
151 }
152
153 /// Example:
154 ///
155 /// ```rust
156 /// use tzf_rs::Finder;
157 ///
158 /// let finder = Finder::new();
159 /// println!("{:?}", finder.timezonenames());
160 /// ```
161 #[must_use]
162 pub fn timezonenames(&self) -> Vec<&str> {
163 let mut ret: Vec<&str> = vec![];
164 for item in &self.all {
165 ret.push(&item.name);
166 }
167 ret
168 }
169
170 /// Example:
171 ///
172 /// ```rust
173 /// use tzf_rs::Finder;
174 ///
175 /// let finder = Finder::new();
176 /// println!("{:?}", finder.data_version());
177 /// ```
178 #[must_use]
179 pub fn data_version(&self) -> &str {
180 &self.data_version
181 }
182
183 /// Creates a new, empty `Finder`.
184 ///
185 /// Example:
186 ///
187 /// ```rust
188 /// use tzf_rs::Finder;
189 ///
190 /// let finder = Finder::new();
191 /// ```
192 #[must_use]
193 pub fn new() -> Self {
194 Self::default()
195 }
196
197 /// Helper method to convert an Item to a FeatureItem.
198 #[cfg(feature = "export-geojson")]
199 fn item_to_feature(&self, item: &Item) -> FeatureItem {
200 // Convert internal Item to pbgen::Timezone format
201 let mut pbpolys = Vec::new();
202 for poly in &item.polys {
203 let mut pbpoly = pbgen::Polygon {
204 points: Vec::new(),
205 holes: Vec::new(),
206 };
207
208 // Convert exterior points
209 for point in &poly.exterior {
210 pbpoly.points.push(pbgen::Point {
211 lng: point.x as f32,
212 lat: point.y as f32,
213 });
214 }
215
216 // Convert holes
217 for hole in &poly.holes {
218 let mut hole_poly = pbgen::Polygon {
219 points: Vec::new(),
220 holes: Vec::new(),
221 };
222 for point in hole {
223 hole_poly.points.push(pbgen::Point {
224 lng: point.x as f32,
225 lat: point.y as f32,
226 });
227 }
228 pbpoly.holes.push(hole_poly);
229 }
230
231 pbpolys.push(pbpoly);
232 }
233
234 let pbtz = pbgen::Timezone {
235 polygons: pbpolys,
236 name: item.name.clone(),
237 };
238
239 revert_item(&pbtz)
240 }
241
242 /// Convert the Finder's data to GeoJSON format.
243 ///
244 /// Returns a `BoundaryFile` (FeatureCollection) containing all timezone polygons.
245 ///
246 /// # Example
247 ///
248 /// ```rust
249 /// use tzf_rs::Finder;
250 ///
251 /// let finder = Finder::new();
252 /// let geojson = finder.to_geojson();
253 /// let json_string = geojson.to_string();
254 /// ```
255 #[must_use]
256 #[cfg(feature = "export-geojson")]
257 pub fn to_geojson(&self) -> BoundaryFile {
258 let mut output = BoundaryFile {
259 collection_type: "FeatureCollection".to_string(),
260 features: Vec::new(),
261 };
262
263 for item in &self.all {
264 output.features.push(self.item_to_feature(item));
265 }
266
267 output
268 }
269
270 /// Convert a specific timezone to GeoJSON format.
271 ///
272 /// Returns `Some(BoundaryFile)` containing a FeatureCollection with all features
273 /// for the timezone if found, `None` otherwise. The returned FeatureCollection
274 /// may contain multiple features if the timezone has multiple geographic boundaries.
275 ///
276 /// # Arguments
277 ///
278 /// * `timezone_name` - The timezone name to export (e.g., "Asia/Tokyo")
279 ///
280 /// # Example
281 ///
282 /// ```rust
283 /// use tzf_rs::Finder;
284 ///
285 /// let finder = Finder::new();
286 /// if let Some(collection) = finder.get_tz_geojson("Asia/Tokyo") {
287 /// let json_string = collection.to_string();
288 /// println!("Found {} feature(s)", collection.features.len());
289 /// if let Some(first_feature) = collection.features.first() {
290 /// println!("Timezone ID: {}", first_feature.properties.tzid);
291 /// }
292 /// }
293 /// ```
294 #[must_use]
295 #[cfg(feature = "export-geojson")]
296 pub fn get_tz_geojson(&self, timezone_name: &str) -> Option<BoundaryFile> {
297 let mut output = BoundaryFile {
298 collection_type: "FeatureCollection".to_string(),
299 features: Vec::new(),
300 };
301 for item in &self.all {
302 if item.name == timezone_name {
303 output.features.push(self.item_to_feature(item));
304 }
305 }
306
307 if output.features.is_empty() {
308 None
309 } else {
310 Some(output)
311 }
312 }
313}
314
315/// Creates a new, empty `Finder`.
316///
317/// Example:
318///
319/// ```rust
320/// use tzf_rs::Finder;
321///
322/// let finder = Finder::default();
323/// ```
324impl Default for Finder {
325 fn default() -> Self {
326 // let file_bytes = include_bytes!("data/combined-with-oceans.reduce.pb").to_vec();
327 let file_bytes: Vec<u8> = load_reduced();
328 Self::from_pb(pbgen::Timezones::try_from(file_bytes).unwrap_or_default())
329 }
330}
331
332/// deg2num is used to convert longitude, latitude to [Slippy map tilenames]
333/// under specific zoom level.
334///
335/// [Slippy map tilenames]: https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
336///
337/// Example:
338///
339/// ```rust
340/// use tzf_rs::deg2num;
341/// let ret = deg2num(116.3883, 39.9289, 7);
342/// assert_eq!((105, 48), ret);
343/// ```
344#[must_use]
345#[allow(
346 clippy::cast_precision_loss,
347 clippy::cast_possible_truncation,
348 clippy::similar_names
349)]
350pub fn deg2num(lng: f64, lat: f64, zoom: i64) -> (i64, i64) {
351 let lat_rad = lat.to_radians();
352 let n = f64::powf(2.0, zoom as f64);
353 let xtile = (lng + 180.0) / 360.0 * n;
354 let ytile = (1.0 - lat_rad.tan().asinh() / PI) / 2.0 * n;
355
356 // Possible precision loss here
357 (xtile as i64, ytile as i64)
358}
359
360/// GeoJSON type definitions for conversion
361#[cfg(feature = "export-geojson")]
362pub type PolygonCoordinates = Vec<Vec<[f64; 2]>>;
363#[cfg(feature = "export-geojson")]
364pub type MultiPolygonCoordinates = Vec<PolygonCoordinates>;
365
366#[cfg(feature = "export-geojson")]
367#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct GeometryDefine {
369 #[serde(rename = "type")]
370 pub geometry_type: String,
371 pub coordinates: MultiPolygonCoordinates,
372}
373
374#[cfg(feature = "export-geojson")]
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct PropertiesDefine {
377 pub tzid: String,
378}
379
380#[cfg(feature = "export-geojson")]
381#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct FeatureItem {
383 #[serde(rename = "type")]
384 pub feature_type: String,
385 pub properties: PropertiesDefine,
386 pub geometry: GeometryDefine,
387}
388
389#[cfg(feature = "export-geojson")]
390impl FeatureItem {
391 pub fn to_string(&self) -> String {
392 serde_json::to_string(self).unwrap_or_default()
393 }
394
395 pub fn to_string_pretty(&self) -> String {
396 serde_json::to_string_pretty(self).unwrap_or_default()
397 }
398}
399
400#[cfg(feature = "export-geojson")]
401#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct BoundaryFile {
403 #[serde(rename = "type")]
404 pub collection_type: String,
405 pub features: Vec<FeatureItem>,
406}
407
408#[cfg(feature = "export-geojson")]
409impl BoundaryFile {
410 pub fn to_string(&self) -> String {
411 serde_json::to_string(self).unwrap_or_default()
412 }
413
414 pub fn to_string_pretty(&self) -> String {
415 serde_json::to_string_pretty(self).unwrap_or_default()
416 }
417}
418
419/// Convert protobuf Polygon array to GeoJSON MultiPolygon coordinates
420#[cfg(feature = "export-geojson")]
421fn from_pb_polygon_to_geo_multipolygon(pbpoly: &[pbgen::Polygon]) -> MultiPolygonCoordinates {
422 let mut res = MultiPolygonCoordinates::new();
423 for poly in pbpoly {
424 let mut new_geo_poly = PolygonCoordinates::new();
425
426 // Main polygon (exterior ring)
427 let mut mainpoly = Vec::new();
428 for point in &poly.points {
429 mainpoly.push([f64::from(point.lng), f64::from(point.lat)]);
430 }
431 new_geo_poly.push(mainpoly);
432
433 // Holes (interior rings)
434 for holepoly in &poly.holes {
435 let mut holepoly_coords = Vec::new();
436 for point in &holepoly.points {
437 holepoly_coords.push([f64::from(point.lng), f64::from(point.lat)]);
438 }
439 new_geo_poly.push(holepoly_coords);
440 }
441 res.push(new_geo_poly);
442 }
443 res
444}
445
446/// Convert a protobuf Timezone to a GeoJSON FeatureItem
447#[cfg(feature = "export-geojson")]
448fn revert_item(input: &pbgen::Timezone) -> FeatureItem {
449 FeatureItem {
450 feature_type: "Feature".to_string(),
451 properties: PropertiesDefine {
452 tzid: input.name.clone(),
453 },
454 geometry: GeometryDefine {
455 geometry_type: "MultiPolygon".to_string(),
456 coordinates: from_pb_polygon_to_geo_multipolygon(&input.polygons),
457 },
458 }
459}
460
461/// Convert protobuf Timezones to GeoJSON BoundaryFile (FeatureCollection)
462#[cfg(feature = "export-geojson")]
463pub fn revert_timezones(input: &pbgen::Timezones) -> BoundaryFile {
464 let mut output = BoundaryFile {
465 collection_type: "FeatureCollection".to_string(),
466 features: Vec::new(),
467 };
468 for timezone in &input.timezones {
469 let item = revert_item(timezone);
470 output.features.push(item);
471 }
472 output
473}
474
475/// `FuzzyFinder` blazing fast for most places on earth, use a preindex data.
476/// Not work for places around borders.
477///
478/// `FuzzyFinder` store all preindex's tiles data in a `HashMap`,
479/// It iterate all zoom levels for input's longitude and latitude to build
480/// map key to to check if in map.
481///
482/// It's is very fast and use about 400ns to check if has preindex.
483/// It work for most places on earth and here is a quick loop of preindex data:
484/// 
485pub struct FuzzyFinder {
486 min_zoom: i64,
487 max_zoom: i64,
488 all: HashMap<(i64, i64, i64), Vec<String>>, // K: <x,y,z>
489 data_version: String,
490}
491
492impl Default for FuzzyFinder {
493 /// Creates a new, empty `FuzzyFinder`.
494 ///
495 /// ```rust
496 /// use tzf_rs::FuzzyFinder;
497 ///
498 /// let finder = FuzzyFinder::default();
499 /// ```
500 fn default() -> Self {
501 let file_bytes: Vec<u8> = load_preindex();
502 Self::from_pb(pbgen::PreindexTimezones::try_from(file_bytes).unwrap_or_default())
503 }
504}
505
506impl FuzzyFinder {
507 #[must_use]
508 pub fn from_pb(tzs: pbgen::PreindexTimezones) -> Self {
509 let mut f = Self {
510 min_zoom: i64::from(tzs.agg_zoom),
511 max_zoom: i64::from(tzs.idx_zoom),
512 all: HashMap::new(),
513 data_version: tzs.version,
514 };
515 for item in &tzs.keys {
516 let key = (i64::from(item.x), i64::from(item.y), i64::from(item.z));
517 f.all.entry(key).or_insert_with(std::vec::Vec::new);
518 f.all.get_mut(&key).unwrap().push(item.name.to_string());
519 f.all.get_mut(&key).unwrap().sort();
520 }
521 f
522 }
523
524 /// Retrieves the time zone name for the given longitude and latitude.
525 ///
526 /// # Arguments
527 ///
528 /// * `lng` - Longitude
529 /// * `lat` - Latitude
530 ///
531 /// # Example:
532 ///
533 /// ```rust
534 /// use tzf_rs::FuzzyFinder;
535 ///
536 /// let finder = FuzzyFinder::new();
537 /// assert_eq!("Asia/Shanghai", finder.get_tz_name(116.3883, 39.9289));
538 /// ```
539 ///
540 /// # Panics
541 ///
542 /// - Panics if `lng` or `lat` is out of range.
543 /// - Panics if `lng` or `lat` is not a number.
544 #[must_use]
545 pub fn get_tz_name(&self, lng: f64, lat: f64) -> &str {
546 for zoom in self.min_zoom..self.max_zoom {
547 let idx = deg2num(lng, lat, zoom);
548 let k = &(idx.0, idx.1, zoom);
549 let ret = self.all.get(k);
550 if ret.is_none() {
551 continue;
552 }
553 return ret.unwrap().first().unwrap();
554 }
555 ""
556 }
557
558 pub fn get_tz_names(&self, lng: f64, lat: f64) -> Vec<&str> {
559 let mut names: Vec<&str> = vec![];
560 for zoom in self.min_zoom..self.max_zoom {
561 let idx = deg2num(lng, lat, zoom);
562 let k = &(idx.0, idx.1, zoom);
563 let ret = self.all.get(k);
564 if ret.is_none() {
565 continue;
566 }
567 for item in ret.unwrap() {
568 names.push(item);
569 }
570 }
571 names
572 }
573
574 /// Gets the version of the data used by this `FuzzyFinder`.
575 ///
576 /// # Returns
577 ///
578 /// The version of the data used by this `FuzzyFinder` as a `&str`.
579 ///
580 /// # Example:
581 ///
582 /// ```rust
583 /// use tzf_rs::FuzzyFinder;
584 ///
585 /// let finder = FuzzyFinder::new();
586 /// println!("{:?}", finder.data_version());
587 /// ```
588 #[must_use]
589 pub fn data_version(&self) -> &str {
590 &self.data_version
591 }
592
593 /// Creates a new, empty `FuzzyFinder`.
594 ///
595 /// ```rust
596 /// use tzf_rs::FuzzyFinder;
597 ///
598 /// let finder = FuzzyFinder::default();
599 /// ```
600 #[must_use]
601 pub fn new() -> Self {
602 Self::default()
603 }
604
605 /// Convert the FuzzyFinder's preindex data to GeoJSON format.
606 ///
607 /// This method generates polygons for each tile in the preindex,
608 /// representing the geographic bounds of each tile.
609 ///
610 /// Returns a `BoundaryFile` (FeatureCollection) containing all timezone tile polygons.
611 ///
612 /// # Example
613 ///
614 /// ```rust
615 /// use tzf_rs::FuzzyFinder;
616 ///
617 /// let finder = FuzzyFinder::new();
618 /// let geojson = finder.to_geojson();
619 /// let json_string = geojson.to_string();
620 /// ```
621 #[must_use]
622 #[cfg(feature = "export-geojson")]
623 pub fn to_geojson(&self) -> BoundaryFile {
624 let mut name_to_keys: HashMap<&String, Vec<(i64, i64, i64)>> = HashMap::new();
625
626 // Group tiles by timezone name
627 for (key, names) in &self.all {
628 for name in names {
629 name_to_keys.entry(name).or_insert_with(Vec::new).push(*key);
630 }
631 }
632
633 let mut features = Vec::new();
634
635 for (name, keys) in name_to_keys {
636 let mut multi_polygon_coords = MultiPolygonCoordinates::new();
637
638 for (x, y, z) in keys {
639 // Convert tile coordinates to lat/lng bounds
640 let tile_poly = tile_to_polygon(x, y, z);
641 multi_polygon_coords.push(vec![tile_poly]);
642 }
643
644 let feature = FeatureItem {
645 feature_type: "Feature".to_string(),
646 properties: PropertiesDefine { tzid: name.clone() },
647 geometry: GeometryDefine {
648 geometry_type: "MultiPolygon".to_string(),
649 coordinates: multi_polygon_coords,
650 },
651 };
652
653 features.push(feature);
654 }
655
656 BoundaryFile {
657 collection_type: "FeatureCollection".to_string(),
658 features,
659 }
660 }
661
662 /// Convert a specific timezone's preindex data to GeoJSON format.
663 ///
664 /// Returns `Some(FeatureItem)` if the timezone is found in the preindex, `None` otherwise.
665 ///
666 /// # Arguments
667 ///
668 /// * `timezone_name` - The timezone name to export (e.g., "Asia/Tokyo")
669 ///
670 /// # Example
671 ///
672 /// ```rust
673 /// use tzf_rs::FuzzyFinder;
674 ///
675 /// let finder = FuzzyFinder::new();
676 /// if let Some(feature) = finder.get_tz_geojson("Asia/Tokyo") {
677 /// let json_string = feature.to_string();
678 /// println!("Found {} tiles for timezone", feature.geometry.coordinates.len());
679 /// }
680 /// ```
681 #[must_use]
682 #[cfg(feature = "export-geojson")]
683 pub fn get_tz_geojson(&self, timezone_name: &str) -> Option<FeatureItem> {
684 let mut keys = Vec::new();
685
686 // Find all tiles that contain this timezone
687 for (key, names) in &self.all {
688 if names.iter().any(|n| n == timezone_name) {
689 keys.push(*key);
690 }
691 }
692
693 if keys.is_empty() {
694 return None;
695 }
696
697 let mut multi_polygon_coords = MultiPolygonCoordinates::new();
698
699 for (x, y, z) in keys {
700 // Convert tile coordinates to lat/lng bounds
701 let tile_poly = tile_to_polygon(x, y, z);
702 multi_polygon_coords.push(vec![tile_poly]);
703 }
704
705 Some(FeatureItem {
706 feature_type: "Feature".to_string(),
707 properties: PropertiesDefine {
708 tzid: timezone_name.to_string(),
709 },
710 geometry: GeometryDefine {
711 geometry_type: "MultiPolygon".to_string(),
712 coordinates: multi_polygon_coords,
713 },
714 })
715 }
716}
717
718/// Convert tile coordinates (x, y, z) to a polygon representing the tile bounds.
719#[cfg(feature = "export-geojson")]
720#[allow(clippy::cast_precision_loss)]
721fn tile_to_polygon(x: i64, y: i64, z: i64) -> Vec<[f64; 2]> {
722 let n = f64::powf(2.0, z as f64);
723
724 // Calculate min (west, south) corner
725 let lng_min = (x as f64) / n * 360.0 - 180.0;
726 let lat_min_rad = ((1.0 - ((y + 1) as f64) / n * 2.0) * PI).sinh().atan();
727 let lat_min = lat_min_rad.to_degrees();
728
729 // Calculate max (east, north) corner
730 let lng_max = ((x + 1) as f64) / n * 360.0 - 180.0;
731 let lat_max_rad = ((1.0 - (y as f64) / n * 2.0) * PI).sinh().atan();
732 let lat_max = lat_max_rad.to_degrees();
733
734 // Create a closed polygon (5 points, first == last)
735 vec![
736 [lng_min, lat_min],
737 [lng_max, lat_min],
738 [lng_max, lat_max],
739 [lng_min, lat_max],
740 [lng_min, lat_min],
741 ]
742}
743
744/// It's most recommend to use, combine both [`Finder`] and [`FuzzyFinder`],
745/// if [`FuzzyFinder`] got no data, then use [`Finder`].
746pub struct DefaultFinder {
747 pub finder: Finder,
748 pub fuzzy_finder: FuzzyFinder,
749}
750
751impl Default for DefaultFinder {
752 /// Creates a new, empty `DefaultFinder`.
753 ///
754 /// # Example
755 ///
756 /// ```rust
757 /// use tzf_rs::DefaultFinder;
758 /// let finder = DefaultFinder::new();
759 /// ```
760 fn default() -> Self {
761 let finder = Finder::default();
762 let fuzzy_finder = FuzzyFinder::default();
763
764 Self {
765 finder,
766 fuzzy_finder,
767 }
768 }
769}
770
771impl DefaultFinder {
772 /// ```rust
773 /// use tzf_rs::DefaultFinder;
774 /// let finder = DefaultFinder::new();
775 /// assert_eq!("Asia/Shanghai", finder.get_tz_name(116.3883, 39.9289));
776 /// ```
777 #[must_use]
778 pub fn get_tz_name(&self, lng: f64, lat: f64) -> &str {
779 // The simplified polygon data contains some empty areas where not covered by any timezone.
780 // It's not a bug but a limitation of the simplified algorithm.
781 //
782 // To handle this, auto shift the point a little bit to find the nearest timezone.
783 let res = self.get_tz_names(lng, lat);
784 if !res.is_empty() {
785 return res.first().unwrap();
786 }
787 ""
788 }
789
790 /// ```rust
791 /// use tzf_rs::DefaultFinder;
792 /// let finder = DefaultFinder::new();
793 /// println!("{:?}", finder.get_tz_names(116.3883, 39.9289));
794 /// ```
795 #[must_use]
796 pub fn get_tz_names(&self, lng: f64, lat: f64) -> Vec<&str> {
797 for &dx in &[0.0, -0.01, 0.01, -0.02, 0.02] {
798 for &dy in &[0.0, -0.01, 0.01, -0.02, 0.02] {
799 let dlng = dx + lng;
800 let dlat = dy + lat;
801 let fuzzy_names = self.fuzzy_finder.get_tz_names(dlng, dlat);
802 if !fuzzy_names.is_empty() {
803 return fuzzy_names;
804 }
805 let names = self.finder.get_tz_names(dlng, dlat);
806 if !names.is_empty() {
807 return names;
808 }
809 }
810 }
811 Vec::new() // Return empty vector if no timezone is found
812 }
813
814 /// Returns all time zone names as a `Vec<&str>`.
815 ///
816 /// ```rust
817 /// use tzf_rs::DefaultFinder;
818 /// let finder = DefaultFinder::new();
819 /// println!("{:?}", finder.timezonenames());
820 /// ```
821 #[must_use]
822 pub fn timezonenames(&self) -> Vec<&str> {
823 self.finder.timezonenames()
824 }
825
826 /// Returns the version of the data used by this `DefaultFinder` as a `&str`.
827 ///
828 /// Example:
829 ///
830 /// ```rust
831 /// use tzf_rs::DefaultFinder;
832 ///
833 /// let finder = DefaultFinder::new();
834 /// println!("{:?}", finder.data_version());
835 /// ```
836 #[must_use]
837 pub fn data_version(&self) -> &str {
838 &self.finder.data_version
839 }
840
841 /// Creates a new instance of `DefaultFinder`.
842 ///
843 /// ```rust
844 /// use tzf_rs::DefaultFinder;
845 /// let finder = DefaultFinder::new();
846 /// ```
847 #[must_use]
848 pub fn new() -> Self {
849 Self::default()
850 }
851
852 /// Convert the DefaultFinder's data to GeoJSON format.
853 ///
854 /// This uses the underlying `Finder`'s data for the GeoJSON conversion.
855 ///
856 /// Returns a `BoundaryFile` (FeatureCollection) containing all timezone polygons.
857 ///
858 /// # Example
859 ///
860 /// ```rust
861 /// use tzf_rs::DefaultFinder;
862 ///
863 /// let finder = DefaultFinder::new();
864 /// let geojson = finder.to_geojson();
865 /// let json_string = geojson.to_string();
866 /// ```
867 #[must_use]
868 #[cfg(feature = "export-geojson")]
869 pub fn to_geojson(&self) -> BoundaryFile {
870 self.finder.to_geojson()
871 }
872
873 /// Convert a specific timezone to GeoJSON format.
874 ///
875 /// This uses the underlying `Finder`'s data for the GeoJSON conversion.
876 ///
877 /// Returns `Some(BoundaryFile)` containing a FeatureCollection with all features
878 /// for the timezone if found, `None` otherwise. The returned FeatureCollection
879 /// may contain multiple features if the timezone has multiple geographic boundaries.
880 ///
881 /// # Arguments
882 ///
883 /// * `timezone_name` - The timezone name to export (e.g., "Asia/Tokyo")
884 ///
885 /// # Example
886 ///
887 /// ```rust
888 /// use tzf_rs::DefaultFinder;
889 ///
890 /// let finder = DefaultFinder::new();
891 /// if let Some(collection) = finder.get_tz_geojson("Asia/Tokyo") {
892 /// let json_string = collection.to_string();
893 /// println!("Found {} feature(s)", collection.features.len());
894 /// if let Some(first_feature) = collection.features.first() {
895 /// println!("Timezone ID: {}", first_feature.properties.tzid);
896 /// }
897 /// }
898 /// ```
899 #[must_use]
900 #[cfg(feature = "export-geojson")]
901 pub fn get_tz_geojson(&self, timezone_name: &str) -> Option<BoundaryFile> {
902 self.finder.get_tz_geojson(timezone_name)
903 }
904}