1#![doc = include_str!("../README.md")]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3
4use geometry_rs::{
5 CoordStorage, I32Point, I32Polygon, I32RaycastMode, Point, Polygon, PolygonBuildOptions,
6};
7#[cfg(feature = "export-geojson")]
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::f64::consts::PI;
11use std::vec;
12#[cfg(all(feature = "bundled", feature = "full"))]
13compile_error!(
14 "features `bundled` and `full` are mutually exclusive; \
15 add `default-features = false` when enabling `full`"
16);
17
18#[cfg(feature = "bundled")]
19use tzf_dist::{load_preindex, load_topology_compress_topo};
20#[cfg(feature = "full")]
21use tzf_dist_git::{load_compress_topo, load_preindex, load_topology_compress_topo};
22pub mod pbgen;
23
24struct Item<T: CoordStorage> {
25 polys: Vec<Polygon<T>>,
26 name: String,
27}
28
29impl<T: CoordStorage> Item<T> {
30 fn contains_point(&self, p: &Point) -> bool {
31 for poly in &self.polys {
32 if poly.contains_point(*p) {
33 return true;
34 }
35 }
36 false
37 }
38}
39
40struct FinderCore<T: CoordStorage> {
44 all: Vec<Item<T>>,
45 data_version: String,
46 grid: Option<HashMap<(i16, i16), Vec<u32>>>,
50}
51
52enum FinderKind {
53 Float(FinderCore<f64>),
54 Scaled(FinderCore<i32>),
55}
56
57macro_rules! with_core {
60 ($finder:expr, $core:ident => $body:expr) => {
61 match &$finder.inner {
62 FinderKind::Float($core) => $body,
63 FinderKind::Scaled($core) => $body,
64 }
65 };
66}
67
68pub struct Finder {
77 inner: FinderKind,
78}
79
80const DEFAULT_RTREE_MIN_SEGMENTS: usize = 32;
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
105#[non_exhaustive]
106pub enum FinderOptions {
107 #[default]
112 NoIndex,
113 YStripes,
115 NoIndexFloatRaycast,
118 NoIndexIntegerRaycast,
122}
123
124impl FinderOptions {
125 #[must_use]
127 pub fn no_index() -> Self {
128 Self::NoIndex
129 }
130
131 #[must_use]
133 pub fn y_stripes() -> Self {
134 Self::YStripes
135 }
136
137 #[must_use]
138 pub fn no_index_float_raycast() -> Self {
139 Self::NoIndexFloatRaycast
140 }
141
142 #[must_use]
143 pub fn no_index_integer_raycast() -> Self {
144 Self::NoIndexIntegerRaycast
145 }
146
147 fn to_polygon_build_options(self) -> PolygonBuildOptions {
148 match self {
149 Self::YStripes => PolygonBuildOptions {
150 enable_rtree: false,
151 enable_compressed_quad: false,
152 enable_y_stripes: true,
153 rtree_min_segments: DEFAULT_RTREE_MIN_SEGMENTS,
154 },
155 Self::NoIndex | Self::NoIndexFloatRaycast | Self::NoIndexIntegerRaycast => {
156 PolygonBuildOptions {
157 enable_rtree: false,
158 enable_compressed_quad: false,
159 enable_y_stripes: false,
160 rtree_min_segments: DEFAULT_RTREE_MIN_SEGMENTS,
161 }
162 }
163 }
164 }
165
166 fn i32_raycast_mode(self) -> I32RaycastMode {
167 match self {
168 Self::NoIndexIntegerRaycast => I32RaycastMode::Integer,
169 Self::NoIndex | Self::NoIndexFloatRaycast | Self::YStripes => I32RaycastMode::Float,
170 }
171 }
172}
173
174#[allow(clippy::cast_possible_truncation)]
178fn decode_polyline(encoded: &[u8]) -> Vec<I32Point> {
179 let mut points = Vec::new();
180 let mut index = 0;
181 let mut lng: i64 = 0;
182 let mut lat: i64 = 0;
183
184 while index < encoded.len() {
185 let (dlng, next) = polyline_decode_value(encoded, index);
186 index = next;
187 let (dlat, next) = polyline_decode_value(encoded, index);
188 index = next;
189 lng += dlng;
190 lat += dlat;
191 points.push(I32Point {
192 x: i32::try_from(lng).expect("polyline longitude exceeds i32"),
193 y: i32::try_from(lat).expect("polyline latitude exceeds i32"),
194 });
195 }
196 points
197}
198
199fn polyline_decode_value(encoded: &[u8], start: usize) -> (i64, usize) {
200 let mut result: i64 = 0;
201 let mut shift = 0;
202 let mut index = start;
203
204 loop {
205 let byte = (encoded[index] as i64) - 63;
206 index += 1;
207 result |= (byte & 0x1F) << shift;
208 shift += 5;
209 if byte < 0x20 {
210 break;
211 }
212 }
213
214 let value = if result & 1 != 0 {
215 !(result >> 1)
216 } else {
217 result >> 1
218 };
219 (value, index)
220}
221
222fn expand_compressed_ring(
223 segs: &[pbgen::CompressedRingSegment],
224 edges: &[Vec<I32Point>],
225) -> Vec<I32Point> {
226 let mut pts = Vec::new();
227 for seg in segs {
228 match &seg.content {
229 Some(pbgen::compressed_ring_segment::Content::Inline(inline)) => {
230 pts.extend(decode_polyline(&inline.points));
231 }
232 Some(pbgen::compressed_ring_segment::Content::EdgeForward(idx)) => {
233 pts.extend_from_slice(&edges[*idx as usize]);
234 }
235 Some(pbgen::compressed_ring_segment::Content::EdgeReversed(idx)) => {
236 pts.extend(edges[*idx as usize].iter().rev().copied());
237 }
238 None => {}
239 }
240 }
241 pts
242}
243
244impl<T: CoordStorage> FinderCore<T> {
245 fn get_tz_name(&self, lng: f64, lat: f64) -> &str {
246 if let Some(ref grid) = self.grid {
247 let key = (lng.floor() as i16, lat.floor() as i16);
248 let indices = match grid.get(&key) {
249 Some(v) => v,
250 None => return "",
251 };
252 if indices.len() == 1 && (-179.0..179.0).contains(&lng) && (-89.0..89.0).contains(&lat)
255 {
256 return &self.all[indices[0] as usize].name;
257 }
258 let p = geometry_rs::Point { x: lng, y: lat };
259 for &idx in indices {
260 if self.all[idx as usize].contains_point(&p) {
261 return &self.all[idx as usize].name;
262 }
263 }
264 return "";
265 }
266 let p = geometry_rs::Point { x: lng, y: lat };
267 for item in &self.all {
268 if item.contains_point(&p) {
269 return &item.name;
270 }
271 }
272 ""
273 }
274
275 fn get_tz_names(&self, lng: f64, lat: f64) -> Vec<&str> {
276 let mut ret: Vec<&str> = vec![];
277 if let Some(ref grid) = self.grid {
278 let key = (lng.floor() as i16, lat.floor() as i16);
279 if let Some(indices) = grid.get(&key) {
280 let p = geometry_rs::Point { x: lng, y: lat };
281 for &idx in indices {
282 if self.all[idx as usize].contains_point(&p) {
283 ret.push(&self.all[idx as usize].name);
284 }
285 }
286 }
287 return ret;
288 }
289 let p = geometry_rs::Point { x: lng, y: lat };
290 for item in &self.all {
291 if item.contains_point(&p) {
292 ret.push(&item.name);
293 }
294 }
295 ret
296 }
297
298 fn timezonenames(&self) -> Vec<&str> {
299 let mut ret: Vec<&str> = vec![];
300 for item in &self.all {
301 ret.push(&item.name);
302 }
303 ret
304 }
305}
306
307impl Finder {
308 fn from_pb_with_polygon_options(tzs: pbgen::Timezones, options: PolygonBuildOptions) -> Self {
309 let mut all: Vec<Item<f64>> = vec![];
310 for tz in &tzs.timezones {
311 let mut polys: Vec<Polygon> = vec![];
312
313 for pbpoly in &tz.polygons {
314 let mut exterior: Vec<Point> = vec![];
315 for pbpoint in &pbpoly.points {
316 exterior.push(Point {
317 x: f64::from(pbpoint.lng),
318 y: f64::from(pbpoint.lat),
319 });
320 }
321
322 let mut interior: Vec<Vec<Point>> = vec![];
323
324 for holepoly in &pbpoly.holes {
325 let mut holeextr: Vec<Point> = vec![];
326 for holepoint in &holepoly.points {
327 holeextr.push(Point {
328 x: f64::from(holepoint.lng),
329 y: f64::from(holepoint.lat),
330 });
331 }
332 interior.push(holeextr);
333 }
334
335 polys.push(geometry_rs::Polygon::new(exterior, interior, Some(options)));
336 }
337
338 all.push(Item {
339 name: tz.name.to_string(),
340 polys,
341 });
342 }
343 Self {
344 inner: FinderKind::Float(FinderCore {
345 all,
346 data_version: tzs.version,
347 grid: None,
348 }),
349 }
350 }
351
352 fn from_compressed_topo_with_polygon_options(
353 tzs: pbgen::CompressedTopoTimezones,
354 options: PolygonBuildOptions,
355 raycast_mode: I32RaycastMode,
356 ) -> Self {
357 let mut edges: Vec<Vec<I32Point>> = vec![Vec::new(); tzs.shared_edges.len()];
358 for edge in &tzs.shared_edges {
359 edges[edge.id as usize] = decode_polyline(&edge.points);
360 }
361
362 let grid = tzs.grid_index.map(|gi| {
363 let mut m = HashMap::with_capacity(gi.cells.len());
364 for cell in gi.cells {
365 m.insert((cell.lng as i16, cell.lat as i16), cell.tz_indices);
366 }
367 m
368 });
369
370 let mut all: Vec<Item<i32>> = vec![];
371 for tz in &tzs.timezones {
372 let mut polys: Vec<I32Polygon> = vec![];
373 for poly in &tz.polygons {
374 let exterior = expand_compressed_ring(&poly.exterior, &edges);
375 let interior: Vec<Vec<I32Point>> = poly
376 .holes
377 .iter()
378 .map(|hole| expand_compressed_ring(&hole.exterior, &edges))
379 .collect();
380 polys.push(I32Polygon::new_with_options(
384 exterior,
385 interior,
386 1e5,
387 raycast_mode,
388 Some(options),
389 ));
390 }
391 all.push(Item {
392 name: tz.name.clone(),
393 polys,
394 });
395 }
396 Self {
397 inner: FinderKind::Scaled(FinderCore {
398 all,
399 data_version: tzs.version,
400 grid,
401 }),
402 }
403 }
404
405 #[must_use]
409 pub fn from_compressed_topo(tzs: pbgen::CompressedTopoTimezones) -> Self {
410 Self::from_compressed_topo_with_options(tzs, FinderOptions::default())
411 }
412
413 #[must_use]
415 pub fn from_compressed_topo_with_options(
416 tzs: pbgen::CompressedTopoTimezones,
417 options: FinderOptions,
418 ) -> Self {
419 Self::from_compressed_topo_with_polygon_options(
420 tzs,
421 options.to_polygon_build_options(),
422 options.i32_raycast_mode(),
423 )
424 }
425
426 #[must_use]
437 pub fn from_pb(tzs: pbgen::Timezones) -> Self {
438 Self::from_pb_with_options(tzs, FinderOptions::default())
439 }
440
441 #[must_use]
443 pub fn from_pb_with_options(tzs: pbgen::Timezones, options: FinderOptions) -> Self {
444 Self::from_pb_with_polygon_options(tzs, options.to_polygon_build_options())
445 }
446
447 #[must_use]
456 pub fn get_tz_name(&self, lng: f64, lat: f64) -> &str {
457 with_core!(self, core => core.get_tz_name(lng, lat))
458 }
459
460 #[must_use]
466 pub fn get_tz_names(&self, lng: f64, lat: f64) -> Vec<&str> {
467 with_core!(self, core => core.get_tz_names(lng, lat))
468 }
469
470 #[must_use]
479 pub fn timezonenames(&self) -> Vec<&str> {
480 with_core!(self, core => core.timezonenames())
481 }
482
483 #[must_use]
492 pub fn data_version(&self) -> &str {
493 with_core!(self, core => &core.data_version)
494 }
495
496 #[must_use]
506 pub fn new() -> Self {
507 Self::default()
508 }
509
510 #[must_use]
524 #[cfg(feature = "export-geojson")]
525 pub fn to_geojson(&self) -> BoundaryFile {
526 with_core!(self, core => core.to_geojson())
527 }
528
529 #[must_use]
554 #[cfg(feature = "export-geojson")]
555 pub fn get_tz_geojson(&self, timezone_name: &str) -> Option<BoundaryFile> {
556 with_core!(self, core => core.get_tz_geojson(timezone_name))
557 }
558}
559
560#[cfg(feature = "export-geojson")]
561impl<T: CoordStorage> FinderCore<T> {
562 fn item_to_feature(&self, item: &Item<T>) -> FeatureItem {
564 let mut pbpolys = Vec::new();
566 for poly in &item.polys {
567 let scale = poly.scale();
569 let mut pbpoly = pbgen::Polygon {
570 points: Vec::new(),
571 holes: Vec::new(),
572 };
573
574 pbpoly
575 .points
576 .extend(poly.exterior().iter().map(|point| pbgen::Point {
577 lng: (point.x.to_f64() / scale) as f32,
578 lat: (point.y.to_f64() / scale) as f32,
579 }));
580 for hole in poly.holes() {
581 pbpoly.holes.push(pbgen::Polygon {
582 points: hole
583 .iter()
584 .map(|point| pbgen::Point {
585 lng: (point.x.to_f64() / scale) as f32,
586 lat: (point.y.to_f64() / scale) as f32,
587 })
588 .collect(),
589 holes: Vec::new(),
590 });
591 }
592
593 pbpolys.push(pbpoly);
594 }
595
596 let pbtz = pbgen::Timezone {
597 polygons: pbpolys,
598 name: item.name.clone(),
599 };
600
601 revert_item(&pbtz)
602 }
603
604 fn to_geojson(&self) -> BoundaryFile {
605 let mut output = BoundaryFile {
606 collection_type: "FeatureCollection".to_string(),
607 features: Vec::new(),
608 };
609
610 for item in &self.all {
611 output.features.push(self.item_to_feature(item));
612 }
613
614 output
615 }
616
617 fn get_tz_geojson(&self, timezone_name: &str) -> Option<BoundaryFile> {
618 let mut output = BoundaryFile {
619 collection_type: "FeatureCollection".to_string(),
620 features: Vec::new(),
621 };
622 for item in &self.all {
623 if item.name == timezone_name {
624 output.features.push(self.item_to_feature(item));
625 }
626 }
627
628 if output.features.is_empty() {
629 None
630 } else {
631 Some(output)
632 }
633 }
634}
635
636impl Default for Finder {
646 fn default() -> Self {
647 let file_bytes = load_topology_compress_topo();
648 Self::from_compressed_topo(
649 pbgen::CompressedTopoTimezones::try_from(file_bytes).unwrap_or_default(),
650 )
651 }
652}
653
654#[must_use]
667#[allow(
668 clippy::cast_precision_loss,
669 clippy::cast_possible_truncation,
670 clippy::similar_names
671)]
672pub fn deg2num(lng: f64, lat: f64, zoom: i64) -> (i64, i64) {
673 let n = (1i64 << zoom) as f64;
674 let lat_rad = lat.to_radians();
675 let xtile = (lng / 360.0 + 0.5) * n;
676 let ytile = (1.0 - lat_rad.tan().asinh() / PI) / 2.0 * n;
677
678 (xtile as i64, ytile as i64)
680}
681
682#[cfg(feature = "export-geojson")]
684pub type PolygonCoordinates = Vec<Vec<[f64; 2]>>;
685#[cfg(feature = "export-geojson")]
686pub type MultiPolygonCoordinates = Vec<PolygonCoordinates>;
687
688#[cfg(feature = "export-geojson")]
689#[derive(Debug, Clone, Serialize, Deserialize)]
690pub struct GeometryDefine {
691 #[serde(rename = "type")]
692 pub geometry_type: String,
693 pub coordinates: MultiPolygonCoordinates,
694}
695
696#[cfg(feature = "export-geojson")]
697#[derive(Debug, Clone, Serialize, Deserialize)]
698pub struct PropertiesDefine {
699 pub tzid: String,
700}
701
702#[cfg(feature = "export-geojson")]
703#[derive(Debug, Clone, Serialize, Deserialize)]
704pub struct FeatureItem {
705 #[serde(rename = "type")]
706 pub feature_type: String,
707 pub properties: PropertiesDefine,
708 pub geometry: GeometryDefine,
709}
710
711#[cfg(feature = "export-geojson")]
712impl FeatureItem {
713 pub fn to_string(&self) -> String {
714 serde_json::to_string(self).unwrap_or_default()
715 }
716
717 pub fn to_string_pretty(&self) -> String {
718 serde_json::to_string_pretty(self).unwrap_or_default()
719 }
720}
721
722#[cfg(feature = "export-geojson")]
723#[derive(Debug, Clone, Serialize, Deserialize)]
724pub struct BoundaryFile {
725 #[serde(rename = "type")]
726 pub collection_type: String,
727 pub features: Vec<FeatureItem>,
728}
729
730#[cfg(feature = "export-geojson")]
731impl BoundaryFile {
732 pub fn to_string(&self) -> String {
733 serde_json::to_string(self).unwrap_or_default()
734 }
735
736 pub fn to_string_pretty(&self) -> String {
737 serde_json::to_string_pretty(self).unwrap_or_default()
738 }
739}
740
741#[cfg(feature = "export-geojson")]
743fn from_pb_polygon_to_geo_multipolygon(pbpoly: &[pbgen::Polygon]) -> MultiPolygonCoordinates {
744 let mut res = MultiPolygonCoordinates::new();
745 for poly in pbpoly {
746 let mut new_geo_poly = PolygonCoordinates::new();
747
748 let mut mainpoly = Vec::new();
750 for point in &poly.points {
751 mainpoly.push([f64::from(point.lng), f64::from(point.lat)]);
752 }
753 new_geo_poly.push(mainpoly);
754
755 for holepoly in &poly.holes {
757 let mut holepoly_coords = Vec::new();
758 for point in &holepoly.points {
759 holepoly_coords.push([f64::from(point.lng), f64::from(point.lat)]);
760 }
761 new_geo_poly.push(holepoly_coords);
762 }
763 res.push(new_geo_poly);
764 }
765 res
766}
767
768#[cfg(feature = "export-geojson")]
770fn revert_item(input: &pbgen::Timezone) -> FeatureItem {
771 FeatureItem {
772 feature_type: "Feature".to_string(),
773 properties: PropertiesDefine {
774 tzid: input.name.clone(),
775 },
776 geometry: GeometryDefine {
777 geometry_type: "MultiPolygon".to_string(),
778 coordinates: from_pb_polygon_to_geo_multipolygon(&input.polygons),
779 },
780 }
781}
782
783#[cfg(feature = "export-geojson")]
785pub fn revert_timezones(input: &pbgen::Timezones) -> BoundaryFile {
786 let mut output = BoundaryFile {
787 collection_type: "FeatureCollection".to_string(),
788 features: Vec::new(),
789 };
790 for timezone in &input.timezones {
791 let item = revert_item(timezone);
792 output.features.push(item);
793 }
794 output
795}
796
797const TILE_COORD_BITS: u32 = 28;
804const TILE_COORD_MASK: u64 = (1 << TILE_COORD_BITS) - 1;
805
806#[inline]
807#[allow(clippy::cast_sign_loss)]
808fn pack_tile_key(x: i64, y: i64, z: i64) -> u64 {
809 ((z as u64) << (2 * TILE_COORD_BITS))
810 | ((x as u64 & TILE_COORD_MASK) << TILE_COORD_BITS)
811 | (y as u64 & TILE_COORD_MASK)
812}
813
814#[cfg(feature = "export-geojson")]
815#[allow(clippy::cast_possible_wrap)]
816fn unpack_tile_key(key: u64) -> (i64, i64, i64) {
817 let x = ((key >> TILE_COORD_BITS) & TILE_COORD_MASK) as i64;
818 let y = (key & TILE_COORD_MASK) as i64;
819 let z = (key >> (2 * TILE_COORD_BITS)) as i64;
820 (x, y, z)
821}
822
823enum TileEntry {
826 One(u16),
827 Many(Box<[u16]>),
828}
829
830impl TileEntry {
831 fn indices(&self) -> &[u16] {
832 match self {
833 Self::One(idx) => std::slice::from_ref(idx),
834 Self::Many(idxs) => idxs,
835 }
836 }
837}
838
839pub struct FuzzyFinder {
850 min_zoom: i64,
851 max_zoom: i64,
852 names: Vec<String>,
855 all: HashMap<u64, TileEntry>, data_version: String,
857}
858
859impl Default for FuzzyFinder {
860 fn default() -> Self {
868 let file_bytes = load_preindex();
869 Self::from_pb(pbgen::PreindexTimezones::try_from(file_bytes.to_vec()).unwrap_or_default())
870 }
871}
872
873impl FuzzyFinder {
874 #[must_use]
878 pub fn from_pb(tzs: pbgen::PreindexTimezones) -> Self {
879 let mut names: Vec<String> = tzs.keys.iter().map(|item| item.name.clone()).collect();
882 names.sort();
883 names.dedup();
884 let name_idx: HashMap<&str, u16> = names
885 .iter()
886 .enumerate()
887 .map(|(i, name)| {
888 (
889 name.as_str(),
890 u16::try_from(i).expect("more than u16::MAX timezone names"),
891 )
892 })
893 .collect();
894
895 let mut all: HashMap<u64, TileEntry> = HashMap::new();
897 for item in &tzs.keys {
898 let idx = name_idx[item.name.as_str()];
899 let key = pack_tile_key(i64::from(item.x), i64::from(item.y), i64::from(item.z));
900 match all.entry(key) {
901 std::collections::hash_map::Entry::Vacant(entry) => {
902 entry.insert(TileEntry::One(idx));
903 }
904 std::collections::hash_map::Entry::Occupied(mut entry) => {
905 let mut idxs = entry.get().indices().to_vec();
906 idxs.push(idx);
907 idxs.sort_unstable();
908 *entry.get_mut() = TileEntry::Many(idxs.into_boxed_slice());
909 }
910 }
911 }
912
913 Self {
914 min_zoom: i64::from(tzs.agg_zoom),
915 max_zoom: i64::from(tzs.idx_zoom),
916 names,
917 all,
918 data_version: tzs.version,
919 }
920 }
921
922 #[must_use]
943 pub fn get_tz_name(&self, lng: f64, lat: f64) -> &str {
944 if self.max_zoom <= self.min_zoom {
945 return "";
946 }
947 let top_zoom = self.max_zoom - 1;
949 let (high_x, high_y) = deg2num(lng, lat, top_zoom);
950 for zoom in self.min_zoom..self.max_zoom {
951 let shift = (top_zoom - zoom) as u32;
952 if let Some(&idx) = self
953 .all
954 .get(&pack_tile_key(high_x >> shift, high_y >> shift, zoom))
955 .and_then(|entry| entry.indices().first())
956 {
957 return &self.names[usize::from(idx)];
958 }
959 }
960 ""
961 }
962
963 pub fn get_tz_names(&self, lng: f64, lat: f64) -> Vec<&str> {
964 let mut names: Vec<&str> = vec![];
965 if self.max_zoom <= self.min_zoom {
966 return names;
967 }
968 let top_zoom = self.max_zoom - 1;
969 let (high_x, high_y) = deg2num(lng, lat, top_zoom);
970 for zoom in self.min_zoom..self.max_zoom {
971 let shift = (top_zoom - zoom) as u32;
972 if let Some(entry) =
973 self.all
974 .get(&pack_tile_key(high_x >> shift, high_y >> shift, zoom))
975 {
976 for &idx in entry.indices() {
977 names.push(self.names[usize::from(idx)].as_str());
978 }
979 }
980 }
981 names
982 }
983
984 #[must_use]
999 pub fn data_version(&self) -> &str {
1000 &self.data_version
1001 }
1002
1003 #[must_use]
1011 pub fn new() -> Self {
1012 Self::default()
1013 }
1014
1015 #[must_use]
1032 #[cfg(feature = "export-geojson")]
1033 pub fn to_geojson(&self) -> BoundaryFile {
1034 let mut name_to_keys: HashMap<u16, Vec<(i64, i64, i64)>> = HashMap::new();
1035
1036 for (key, entry) in &self.all {
1038 for &idx in entry.indices() {
1039 name_to_keys
1040 .entry(idx)
1041 .or_default()
1042 .push(unpack_tile_key(*key));
1043 }
1044 }
1045
1046 let mut features = Vec::new();
1047
1048 for (idx, keys) in name_to_keys {
1049 let mut multi_polygon_coords = MultiPolygonCoordinates::new();
1050
1051 for (x, y, z) in keys {
1052 let tile_poly = tile_to_polygon(x, y, z);
1054 multi_polygon_coords.push(vec![tile_poly]);
1055 }
1056
1057 let feature = FeatureItem {
1058 feature_type: "Feature".to_string(),
1059 properties: PropertiesDefine {
1060 tzid: self.names[usize::from(idx)].clone(),
1061 },
1062 geometry: GeometryDefine {
1063 geometry_type: "MultiPolygon".to_string(),
1064 coordinates: multi_polygon_coords,
1065 },
1066 };
1067
1068 features.push(feature);
1069 }
1070
1071 BoundaryFile {
1072 collection_type: "FeatureCollection".to_string(),
1073 features,
1074 }
1075 }
1076
1077 #[must_use]
1097 #[cfg(feature = "export-geojson")]
1098 pub fn get_tz_geojson(&self, timezone_name: &str) -> Option<FeatureItem> {
1099 let target = u16::try_from(
1101 self.names
1102 .binary_search_by(|name| name.as_str().cmp(timezone_name))
1103 .ok()?,
1104 )
1105 .ok()?;
1106
1107 let mut keys = Vec::new();
1108
1109 for (key, entry) in &self.all {
1111 if entry.indices().contains(&target) {
1112 keys.push(unpack_tile_key(*key));
1113 }
1114 }
1115
1116 if keys.is_empty() {
1117 return None;
1118 }
1119
1120 let mut multi_polygon_coords = MultiPolygonCoordinates::new();
1121
1122 for (x, y, z) in keys {
1123 let tile_poly = tile_to_polygon(x, y, z);
1125 multi_polygon_coords.push(vec![tile_poly]);
1126 }
1127
1128 Some(FeatureItem {
1129 feature_type: "Feature".to_string(),
1130 properties: PropertiesDefine {
1131 tzid: timezone_name.to_string(),
1132 },
1133 geometry: GeometryDefine {
1134 geometry_type: "MultiPolygon".to_string(),
1135 coordinates: multi_polygon_coords,
1136 },
1137 })
1138 }
1139}
1140
1141#[cfg(feature = "export-geojson")]
1143#[allow(clippy::cast_precision_loss)]
1144fn tile_to_polygon(x: i64, y: i64, z: i64) -> Vec<[f64; 2]> {
1145 let n = f64::powf(2.0, z as f64);
1146
1147 let lng_min = (x as f64) / n * 360.0 - 180.0;
1149 let lat_min_rad = ((1.0 - ((y + 1) as f64) / n * 2.0) * PI).sinh().atan();
1150 let lat_min = lat_min_rad.to_degrees();
1151
1152 let lng_max = ((x + 1) as f64) / n * 360.0 - 180.0;
1154 let lat_max_rad = ((1.0 - (y as f64) / n * 2.0) * PI).sinh().atan();
1155 let lat_max = lat_max_rad.to_degrees();
1156
1157 vec![
1159 [lng_min, lat_min],
1160 [lng_max, lat_min],
1161 [lng_max, lat_max],
1162 [lng_min, lat_max],
1163 [lng_min, lat_min],
1164 ]
1165}
1166
1167pub struct DefaultFinder {
1170 pub finder: Finder,
1171 pub fuzzy_finder: FuzzyFinder,
1172}
1173
1174impl Default for DefaultFinder {
1175 fn default() -> Self {
1184 let options = FinderOptions::y_stripes();
1185 let topo_bytes = load_topology_compress_topo();
1186 let tzs = pbgen::CompressedTopoTimezones::try_from(topo_bytes).unwrap_or_default();
1187 let finder = Finder::from_compressed_topo_with_options(tzs, options);
1188
1189 let fuzzy_finder = FuzzyFinder::default();
1190
1191 Self {
1192 finder,
1193 fuzzy_finder,
1194 }
1195 }
1196}
1197
1198impl DefaultFinder {
1199 #[must_use]
1203 pub fn new_with_options(options: FinderOptions) -> Self {
1204 let topo_bytes = load_topology_compress_topo();
1205 let tzs = pbgen::CompressedTopoTimezones::try_from(topo_bytes).unwrap_or_default();
1206 Self {
1207 finder: Finder::from_compressed_topo_with_options(tzs, options),
1208 fuzzy_finder: FuzzyFinder::default(),
1209 }
1210 }
1211
1212 #[must_use]
1234 #[cfg(feature = "full")]
1235 #[cfg_attr(docsrs, doc(cfg(feature = "full")))]
1236 pub fn new_full() -> Self {
1237 Self::new_full_with_options(FinderOptions::y_stripes())
1238 }
1239
1240 #[must_use]
1242 #[cfg(feature = "full")]
1243 #[cfg_attr(docsrs, doc(cfg(feature = "full")))]
1244 pub fn new_full_with_options(options: FinderOptions) -> Self {
1245 let tzs =
1246 pbgen::CompressedTopoTimezones::try_from(load_compress_topo()).unwrap_or_default();
1247 Self {
1248 finder: Finder::from_compressed_topo_with_options(tzs, options),
1249 fuzzy_finder: FuzzyFinder::default(),
1250 }
1251 }
1252
1253 #[must_use]
1259 pub fn get_tz_name(&self, lng: f64, lat: f64) -> &str {
1260 let fuzzy = self.fuzzy_finder.get_tz_name(lng, lat);
1261 if !fuzzy.is_empty() {
1262 return fuzzy;
1263 }
1264 self.finder.get_tz_name(lng, lat)
1265 }
1266
1267 #[must_use]
1273 pub fn get_tz_names(&self, lng: f64, lat: f64) -> Vec<&str> {
1274 self.finder.get_tz_names(lng, lat)
1275 }
1276
1277 #[must_use]
1285 pub fn timezonenames(&self) -> Vec<&str> {
1286 self.finder.timezonenames()
1287 }
1288
1289 #[must_use]
1300 pub fn data_version(&self) -> &str {
1301 self.finder.data_version()
1302 }
1303
1304 #[must_use]
1311 pub fn new() -> Self {
1312 Self::default()
1313 }
1314
1315 #[must_use]
1331 #[cfg(feature = "export-geojson")]
1332 pub fn to_geojson(&self) -> BoundaryFile {
1333 self.finder.to_geojson()
1334 }
1335
1336 #[must_use]
1363 #[cfg(feature = "export-geojson")]
1364 pub fn get_tz_geojson(&self, timezone_name: &str) -> Option<BoundaryFile> {
1365 self.finder.get_tz_geojson(timezone_name)
1366 }
1367}