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/// ![](https://user-images.githubusercontent.com/13536789/200174943-7d40661e-bda5-4b79-a867-ec637e245a49.png)
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}