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_allow_on_edge(*p) {
38 return true;
39 }
40 }
41 false
42 }
43}
44
45struct FinderCore<T: CoordStorage> {
49 all: Vec<Item<T>>,
50 data_version: String,
51 grid: Option<HashMap<(i16, i16), Vec<u32>>>,
55}
56
57enum FinderKind {
58 Float(FinderCore<f64>),
59 Scaled(FinderCore<i32>),
60}
61
62macro_rules! with_core {
65 ($finder:expr, $core:ident => $body:expr) => {
66 match &$finder.inner {
67 FinderKind::Float($core) => $body,
68 FinderKind::Scaled($core) => $body,
69 }
70 };
71}
72
73pub struct Finder {
82 inner: FinderKind,
83}
84
85const DEFAULT_RTREE_MIN_SEGMENTS: usize = 32;
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
110#[non_exhaustive]
111pub enum FinderOptions {
112 #[default]
117 NoIndex,
118 YStripes,
120 NoIndexFloatRaycast,
123 NoIndexIntegerRaycast,
127}
128
129impl FinderOptions {
130 #[must_use]
132 pub fn no_index() -> Self {
133 Self::NoIndex
134 }
135
136 #[must_use]
138 pub fn y_stripes() -> Self {
139 Self::YStripes
140 }
141
142 #[must_use]
143 pub fn no_index_float_raycast() -> Self {
144 Self::NoIndexFloatRaycast
145 }
146
147 #[must_use]
148 pub fn no_index_integer_raycast() -> Self {
149 Self::NoIndexIntegerRaycast
150 }
151
152 fn to_polygon_build_options(self) -> PolygonBuildOptions {
153 match self {
154 Self::YStripes => PolygonBuildOptions {
155 enable_rtree: false,
156 enable_compressed_quad: false,
157 enable_y_stripes: true,
158 rtree_min_segments: DEFAULT_RTREE_MIN_SEGMENTS,
159 },
160 Self::NoIndex | Self::NoIndexFloatRaycast | Self::NoIndexIntegerRaycast => {
161 PolygonBuildOptions {
162 enable_rtree: false,
163 enable_compressed_quad: false,
164 enable_y_stripes: false,
165 rtree_min_segments: DEFAULT_RTREE_MIN_SEGMENTS,
166 }
167 }
168 }
169 }
170
171 fn i32_raycast_mode(self) -> I32RaycastMode {
172 match self {
173 Self::NoIndexIntegerRaycast => I32RaycastMode::Integer,
174 Self::NoIndex | Self::NoIndexFloatRaycast | Self::YStripes => I32RaycastMode::Float,
175 }
176 }
177}
178
179#[allow(clippy::cast_possible_truncation)]
183fn decode_polyline(encoded: &[u8]) -> Vec<I32Point> {
184 let mut points = Vec::new();
185 let mut index = 0;
186 let mut lng: i64 = 0;
187 let mut lat: i64 = 0;
188
189 while index < encoded.len() {
190 let (dlng, next) = polyline_decode_value(encoded, index);
191 index = next;
192 let (dlat, next) = polyline_decode_value(encoded, index);
193 index = next;
194 lng += dlng;
195 lat += dlat;
196 points.push(I32Point {
197 x: i32::try_from(lng).expect("polyline longitude exceeds i32"),
198 y: i32::try_from(lat).expect("polyline latitude exceeds i32"),
199 });
200 }
201 points
202}
203
204fn polyline_decode_value(encoded: &[u8], start: usize) -> (i64, usize) {
205 let mut result: i64 = 0;
206 let mut shift = 0;
207 let mut index = start;
208
209 loop {
210 let byte = (encoded[index] as i64) - 63;
211 index += 1;
212 result |= (byte & 0x1F) << shift;
213 shift += 5;
214 if byte < 0x20 {
215 break;
216 }
217 }
218
219 let value = if result & 1 != 0 {
220 !(result >> 1)
221 } else {
222 result >> 1
223 };
224 (value, index)
225}
226
227fn expand_compressed_ring(
228 segs: &[pbgen::CompressedRingSegment],
229 edges: &[Vec<I32Point>],
230) -> Vec<I32Point> {
231 let mut pts = Vec::new();
232 for seg in segs {
233 match &seg.content {
234 Some(pbgen::compressed_ring_segment::Content::Inline(inline)) => {
235 pts.extend(decode_polyline(&inline.points));
236 }
237 Some(pbgen::compressed_ring_segment::Content::EdgeForward(idx)) => {
238 pts.extend_from_slice(&edges[*idx as usize]);
239 }
240 Some(pbgen::compressed_ring_segment::Content::EdgeReversed(idx)) => {
241 pts.extend(edges[*idx as usize].iter().rev().copied());
242 }
243 None => {}
244 }
245 }
246 pts
247}
248
249impl<T: CoordStorage> FinderCore<T> {
250 fn get_tz_name(&self, lng: f64, lat: f64) -> &str {
251 if let Some(ref grid) = self.grid {
252 let key = (lng.floor() as i16, lat.floor() as i16);
253 let indices = match grid.get(&key) {
254 Some(v) => v,
255 None => return "",
256 };
257 if indices.len() == 1 && (-179.0..179.0).contains(&lng) && (-89.0..89.0).contains(&lat)
260 {
261 return &self.all[indices[0] as usize].name;
262 }
263 let p = geometry_rs::Point { x: lng, y: lat };
264 for &idx in indices {
265 if self.all[idx as usize].contains_point(&p) {
266 return &self.all[idx as usize].name;
267 }
268 }
269 return "";
270 }
271 let p = geometry_rs::Point { x: lng, y: lat };
272 for item in &self.all {
273 if item.contains_point(&p) {
274 return &item.name;
275 }
276 }
277 ""
278 }
279
280 fn get_tz_names(&self, lng: f64, lat: f64) -> Vec<&str> {
281 let mut ret: Vec<&str> = vec![];
282 if let Some(ref grid) = self.grid {
283 let key = (lng.floor() as i16, lat.floor() as i16);
284 if let Some(indices) = grid.get(&key) {
285 let p = geometry_rs::Point { x: lng, y: lat };
286 for &idx in indices {
287 if self.all[idx as usize].contains_point(&p) {
288 ret.push(&self.all[idx as usize].name);
289 }
290 }
291 }
292 return ret;
293 }
294 let p = geometry_rs::Point { x: lng, y: lat };
295 for item in &self.all {
296 if item.contains_point(&p) {
297 ret.push(&item.name);
298 }
299 }
300 ret
301 }
302
303 fn timezonenames(&self) -> Vec<&str> {
304 let mut ret: Vec<&str> = vec![];
305 for item in &self.all {
306 ret.push(&item.name);
307 }
308 ret
309 }
310}
311
312impl Finder {
313 fn from_pb_with_polygon_options(tzs: pbgen::Timezones, options: PolygonBuildOptions) -> Self {
314 let mut all: Vec<Item<f64>> = vec![];
315 for tz in &tzs.timezones {
316 let mut polys: Vec<Polygon> = vec![];
317
318 for pbpoly in &tz.polygons {
319 let mut exterior: Vec<Point> = vec![];
320 for pbpoint in &pbpoly.points {
321 exterior.push(Point {
322 x: f64::from(pbpoint.lng),
323 y: f64::from(pbpoint.lat),
324 });
325 }
326
327 let mut interior: Vec<Vec<Point>> = vec![];
328
329 for holepoly in &pbpoly.holes {
330 let mut holeextr: Vec<Point> = vec![];
331 for holepoint in &holepoly.points {
332 holeextr.push(Point {
333 x: f64::from(holepoint.lng),
334 y: f64::from(holepoint.lat),
335 });
336 }
337 interior.push(holeextr);
338 }
339
340 polys.push(geometry_rs::Polygon::new(exterior, interior, Some(options)));
341 }
342
343 all.push(Item {
344 name: tz.name.to_string(),
345 polys,
346 });
347 }
348 Self {
349 inner: FinderKind::Float(FinderCore {
350 all,
351 data_version: tzs.version,
352 grid: None,
353 }),
354 }
355 }
356
357 fn from_compressed_topo_with_polygon_options(
358 tzs: pbgen::CompressedTopoTimezones,
359 options: PolygonBuildOptions,
360 raycast_mode: I32RaycastMode,
361 ) -> Self {
362 let mut edges: Vec<Vec<I32Point>> = vec![Vec::new(); tzs.shared_edges.len()];
363 for edge in &tzs.shared_edges {
364 edges[edge.id as usize] = decode_polyline(&edge.points);
365 }
366
367 let grid = tzs.grid_index.map(|gi| {
368 let mut m = HashMap::with_capacity(gi.cells.len());
369 for cell in gi.cells {
370 m.insert((cell.lng as i16, cell.lat as i16), cell.tz_indices);
371 }
372 m
373 });
374
375 let mut all: Vec<Item<i32>> = vec![];
376 for tz in &tzs.timezones {
377 let mut polys: Vec<I32Polygon> = vec![];
378 for poly in &tz.polygons {
379 let exterior = expand_compressed_ring(&poly.exterior, &edges);
380 let interior: Vec<Vec<I32Point>> = poly
381 .holes
382 .iter()
383 .map(|hole| expand_compressed_ring(&hole.exterior, &edges))
384 .collect();
385 polys.push(I32Polygon::new_with_options(
389 exterior,
390 interior,
391 1e5,
392 raycast_mode,
393 Some(options),
394 ));
395 }
396 all.push(Item {
397 name: tz.name.clone(),
398 polys,
399 });
400 }
401 Self {
402 inner: FinderKind::Scaled(FinderCore {
403 all,
404 data_version: tzs.version,
405 grid,
406 }),
407 }
408 }
409
410 #[must_use]
414 pub fn from_compressed_topo(tzs: pbgen::CompressedTopoTimezones) -> Self {
415 Self::from_compressed_topo_with_options(tzs, FinderOptions::default())
416 }
417
418 #[must_use]
420 pub fn from_compressed_topo_with_options(
421 tzs: pbgen::CompressedTopoTimezones,
422 options: FinderOptions,
423 ) -> Self {
424 Self::from_compressed_topo_with_polygon_options(
425 tzs,
426 options.to_polygon_build_options(),
427 options.i32_raycast_mode(),
428 )
429 }
430
431 #[must_use]
442 pub fn from_pb(tzs: pbgen::Timezones) -> Self {
443 Self::from_pb_with_options(tzs, FinderOptions::default())
444 }
445
446 #[must_use]
448 pub fn from_pb_with_options(tzs: pbgen::Timezones, options: FinderOptions) -> Self {
449 Self::from_pb_with_polygon_options(tzs, options.to_polygon_build_options())
450 }
451
452 #[must_use]
461 pub fn get_tz_name(&self, lng: f64, lat: f64) -> &str {
462 with_core!(self, core => core.get_tz_name(lng, lat))
463 }
464
465 #[must_use]
471 pub fn get_tz_names(&self, lng: f64, lat: f64) -> Vec<&str> {
472 with_core!(self, core => core.get_tz_names(lng, lat))
473 }
474
475 #[must_use]
484 pub fn timezonenames(&self) -> Vec<&str> {
485 with_core!(self, core => core.timezonenames())
486 }
487
488 #[must_use]
497 pub fn data_version(&self) -> &str {
498 with_core!(self, core => &core.data_version)
499 }
500
501 #[must_use]
511 pub fn new() -> Self {
512 Self::default()
513 }
514
515 #[must_use]
529 #[cfg(feature = "export-geojson")]
530 pub fn to_geojson(&self) -> BoundaryFile {
531 with_core!(self, core => core.to_geojson())
532 }
533
534 #[must_use]
559 #[cfg(feature = "export-geojson")]
560 pub fn get_tz_geojson(&self, timezone_name: &str) -> Option<BoundaryFile> {
561 with_core!(self, core => core.get_tz_geojson(timezone_name))
562 }
563}
564
565#[cfg(feature = "export-geojson")]
566impl<T: CoordStorage> FinderCore<T> {
567 fn item_to_feature(&self, item: &Item<T>) -> FeatureItem {
569 let mut pbpolys = Vec::new();
571 for poly in &item.polys {
572 let scale = poly.scale();
574 let mut pbpoly = pbgen::Polygon {
575 points: Vec::new(),
576 holes: Vec::new(),
577 };
578
579 pbpoly
580 .points
581 .extend(poly.exterior().iter().map(|point| pbgen::Point {
582 lng: (point.x.to_f64() / scale) as f32,
583 lat: (point.y.to_f64() / scale) as f32,
584 }));
585 for hole in poly.holes() {
586 pbpoly.holes.push(pbgen::Polygon {
587 points: hole
588 .iter()
589 .map(|point| pbgen::Point {
590 lng: (point.x.to_f64() / scale) as f32,
591 lat: (point.y.to_f64() / scale) as f32,
592 })
593 .collect(),
594 holes: Vec::new(),
595 });
596 }
597
598 pbpolys.push(pbpoly);
599 }
600
601 let pbtz = pbgen::Timezone {
602 polygons: pbpolys,
603 name: item.name.clone(),
604 };
605
606 revert_item(&pbtz)
607 }
608
609 fn to_geojson(&self) -> BoundaryFile {
610 let mut output = BoundaryFile {
611 collection_type: "FeatureCollection".to_string(),
612 features: Vec::new(),
613 };
614
615 for item in &self.all {
616 output.features.push(self.item_to_feature(item));
617 }
618
619 output
620 }
621
622 fn get_tz_geojson(&self, timezone_name: &str) -> Option<BoundaryFile> {
623 let mut output = BoundaryFile {
624 collection_type: "FeatureCollection".to_string(),
625 features: Vec::new(),
626 };
627 for item in &self.all {
628 if item.name == timezone_name {
629 output.features.push(self.item_to_feature(item));
630 }
631 }
632
633 if output.features.is_empty() {
634 None
635 } else {
636 Some(output)
637 }
638 }
639}
640
641impl Default for Finder {
651 fn default() -> Self {
652 let file_bytes = load_topology_compress_topo();
653 Self::from_compressed_topo(
654 pbgen::CompressedTopoTimezones::try_from(file_bytes).unwrap_or_default(),
655 )
656 }
657}
658
659#[must_use]
672#[allow(
673 clippy::cast_precision_loss,
674 clippy::cast_possible_truncation,
675 clippy::similar_names
676)]
677pub fn deg2num(lng: f64, lat: f64, zoom: i64) -> (i64, i64) {
678 let n = (1i64 << zoom) as f64;
679 let lat_rad = lat.to_radians();
680 let xtile = (lng / 360.0 + 0.5) * n;
681 let ytile = (1.0 - lat_rad.tan().asinh() / PI) / 2.0 * n;
682
683 (xtile as i64, ytile as i64)
685}
686
687#[cfg(feature = "export-geojson")]
689pub type PolygonCoordinates = Vec<Vec<[f64; 2]>>;
690#[cfg(feature = "export-geojson")]
691pub type MultiPolygonCoordinates = Vec<PolygonCoordinates>;
692
693#[cfg(feature = "export-geojson")]
694#[derive(Debug, Clone, Serialize, Deserialize)]
695pub struct GeometryDefine {
696 #[serde(rename = "type")]
697 pub geometry_type: String,
698 pub coordinates: MultiPolygonCoordinates,
699}
700
701#[cfg(feature = "export-geojson")]
702#[derive(Debug, Clone, Serialize, Deserialize)]
703pub struct PropertiesDefine {
704 pub tzid: String,
705}
706
707#[cfg(feature = "export-geojson")]
708#[derive(Debug, Clone, Serialize, Deserialize)]
709pub struct FeatureItem {
710 #[serde(rename = "type")]
711 pub feature_type: String,
712 pub properties: PropertiesDefine,
713 pub geometry: GeometryDefine,
714}
715
716#[cfg(feature = "export-geojson")]
717impl FeatureItem {
718 pub fn to_string(&self) -> String {
719 serde_json::to_string(self).unwrap_or_default()
720 }
721
722 pub fn to_string_pretty(&self) -> String {
723 serde_json::to_string_pretty(self).unwrap_or_default()
724 }
725}
726
727#[cfg(feature = "export-geojson")]
728#[derive(Debug, Clone, Serialize, Deserialize)]
729pub struct BoundaryFile {
730 #[serde(rename = "type")]
731 pub collection_type: String,
732 pub features: Vec<FeatureItem>,
733}
734
735#[cfg(feature = "export-geojson")]
736impl BoundaryFile {
737 pub fn to_string(&self) -> String {
738 serde_json::to_string(self).unwrap_or_default()
739 }
740
741 pub fn to_string_pretty(&self) -> String {
742 serde_json::to_string_pretty(self).unwrap_or_default()
743 }
744}
745
746#[cfg(feature = "export-geojson")]
748fn from_pb_polygon_to_geo_multipolygon(pbpoly: &[pbgen::Polygon]) -> MultiPolygonCoordinates {
749 let mut res = MultiPolygonCoordinates::new();
750 for poly in pbpoly {
751 let mut new_geo_poly = PolygonCoordinates::new();
752
753 let mut mainpoly = Vec::new();
755 for point in &poly.points {
756 mainpoly.push([f64::from(point.lng), f64::from(point.lat)]);
757 }
758 new_geo_poly.push(mainpoly);
759
760 for holepoly in &poly.holes {
762 let mut holepoly_coords = Vec::new();
763 for point in &holepoly.points {
764 holepoly_coords.push([f64::from(point.lng), f64::from(point.lat)]);
765 }
766 new_geo_poly.push(holepoly_coords);
767 }
768 res.push(new_geo_poly);
769 }
770 res
771}
772
773#[cfg(feature = "export-geojson")]
775fn revert_item(input: &pbgen::Timezone) -> FeatureItem {
776 FeatureItem {
777 feature_type: "Feature".to_string(),
778 properties: PropertiesDefine {
779 tzid: input.name.clone(),
780 },
781 geometry: GeometryDefine {
782 geometry_type: "MultiPolygon".to_string(),
783 coordinates: from_pb_polygon_to_geo_multipolygon(&input.polygons),
784 },
785 }
786}
787
788#[cfg(feature = "export-geojson")]
790pub fn revert_timezones(input: &pbgen::Timezones) -> BoundaryFile {
791 let mut output = BoundaryFile {
792 collection_type: "FeatureCollection".to_string(),
793 features: Vec::new(),
794 };
795 for timezone in &input.timezones {
796 let item = revert_item(timezone);
797 output.features.push(item);
798 }
799 output
800}
801
802const TILE_COORD_BITS: u32 = 28;
809const TILE_COORD_MASK: u64 = (1 << TILE_COORD_BITS) - 1;
810
811#[inline]
812#[allow(clippy::cast_sign_loss)]
813fn pack_tile_key(x: i64, y: i64, z: i64) -> u64 {
814 ((z as u64) << (2 * TILE_COORD_BITS))
815 | ((x as u64 & TILE_COORD_MASK) << TILE_COORD_BITS)
816 | (y as u64 & TILE_COORD_MASK)
817}
818
819#[cfg(feature = "export-geojson")]
820#[allow(clippy::cast_possible_wrap)]
821fn unpack_tile_key(key: u64) -> (i64, i64, i64) {
822 let x = ((key >> TILE_COORD_BITS) & TILE_COORD_MASK) as i64;
823 let y = (key & TILE_COORD_MASK) as i64;
824 let z = (key >> (2 * TILE_COORD_BITS)) as i64;
825 (x, y, z)
826}
827
828enum TileEntry {
831 One(u16),
832 Many(Box<[u16]>),
833}
834
835impl TileEntry {
836 fn indices(&self) -> &[u16] {
837 match self {
838 Self::One(idx) => std::slice::from_ref(idx),
839 Self::Many(idxs) => idxs,
840 }
841 }
842}
843
844pub struct FuzzyFinder {
855 min_zoom: i64,
856 max_zoom: i64,
857 names: Vec<String>,
860 all: HashMap<u64, TileEntry>, data_version: String,
862}
863
864impl Default for FuzzyFinder {
865 fn default() -> Self {
873 let file_bytes = load_preindex();
874 Self::from_pb(pbgen::PreindexTimezones::try_from(file_bytes.to_vec()).unwrap_or_default())
875 }
876}
877
878impl FuzzyFinder {
879 #[must_use]
883 pub fn from_pb(tzs: pbgen::PreindexTimezones) -> Self {
884 let mut names: Vec<String> = tzs.keys.iter().map(|item| item.name.clone()).collect();
887 names.sort();
888 names.dedup();
889 let name_idx: HashMap<&str, u16> = names
890 .iter()
891 .enumerate()
892 .map(|(i, name)| {
893 (
894 name.as_str(),
895 u16::try_from(i).expect("more than u16::MAX timezone names"),
896 )
897 })
898 .collect();
899
900 let mut all: HashMap<u64, TileEntry> = HashMap::new();
902 for item in &tzs.keys {
903 let idx = name_idx[item.name.as_str()];
904 let key = pack_tile_key(i64::from(item.x), i64::from(item.y), i64::from(item.z));
905 match all.entry(key) {
906 std::collections::hash_map::Entry::Vacant(entry) => {
907 entry.insert(TileEntry::One(idx));
908 }
909 std::collections::hash_map::Entry::Occupied(mut entry) => {
910 let mut idxs = entry.get().indices().to_vec();
911 idxs.push(idx);
912 idxs.sort_unstable();
913 *entry.get_mut() = TileEntry::Many(idxs.into_boxed_slice());
914 }
915 }
916 }
917
918 Self {
919 min_zoom: i64::from(tzs.agg_zoom),
920 max_zoom: i64::from(tzs.idx_zoom),
921 names,
922 all,
923 data_version: tzs.version,
924 }
925 }
926
927 #[must_use]
948 pub fn get_tz_name(&self, lng: f64, lat: f64) -> &str {
949 if self.max_zoom <= self.min_zoom {
950 return "";
951 }
952 let top_zoom = self.max_zoom - 1;
954 let (high_x, high_y) = deg2num(lng, lat, top_zoom);
955 for zoom in self.min_zoom..self.max_zoom {
956 let shift = (top_zoom - zoom) as u32;
957 if let Some(&idx) = self
958 .all
959 .get(&pack_tile_key(high_x >> shift, high_y >> shift, zoom))
960 .and_then(|entry| entry.indices().first())
961 {
962 return &self.names[usize::from(idx)];
963 }
964 }
965 ""
966 }
967
968 pub fn get_tz_names(&self, lng: f64, lat: f64) -> Vec<&str> {
969 let mut names: Vec<&str> = vec![];
970 if self.max_zoom <= self.min_zoom {
971 return names;
972 }
973 let top_zoom = self.max_zoom - 1;
974 let (high_x, high_y) = deg2num(lng, lat, top_zoom);
975 for zoom in self.min_zoom..self.max_zoom {
976 let shift = (top_zoom - zoom) as u32;
977 if let Some(entry) =
978 self.all
979 .get(&pack_tile_key(high_x >> shift, high_y >> shift, zoom))
980 {
981 for &idx in entry.indices() {
982 names.push(self.names[usize::from(idx)].as_str());
983 }
984 }
985 }
986 names
987 }
988
989 #[must_use]
1004 pub fn data_version(&self) -> &str {
1005 &self.data_version
1006 }
1007
1008 #[must_use]
1016 pub fn new() -> Self {
1017 Self::default()
1018 }
1019
1020 #[must_use]
1037 #[cfg(feature = "export-geojson")]
1038 pub fn to_geojson(&self) -> BoundaryFile {
1039 let mut name_to_keys: HashMap<u16, Vec<(i64, i64, i64)>> = HashMap::new();
1040
1041 for (key, entry) in &self.all {
1043 for &idx in entry.indices() {
1044 name_to_keys
1045 .entry(idx)
1046 .or_default()
1047 .push(unpack_tile_key(*key));
1048 }
1049 }
1050
1051 let mut features = Vec::new();
1052
1053 for (idx, keys) in name_to_keys {
1054 let mut multi_polygon_coords = MultiPolygonCoordinates::new();
1055
1056 for (x, y, z) in keys {
1057 let tile_poly = tile_to_polygon(x, y, z);
1059 multi_polygon_coords.push(vec![tile_poly]);
1060 }
1061
1062 let feature = FeatureItem {
1063 feature_type: "Feature".to_string(),
1064 properties: PropertiesDefine {
1065 tzid: self.names[usize::from(idx)].clone(),
1066 },
1067 geometry: GeometryDefine {
1068 geometry_type: "MultiPolygon".to_string(),
1069 coordinates: multi_polygon_coords,
1070 },
1071 };
1072
1073 features.push(feature);
1074 }
1075
1076 BoundaryFile {
1077 collection_type: "FeatureCollection".to_string(),
1078 features,
1079 }
1080 }
1081
1082 #[must_use]
1102 #[cfg(feature = "export-geojson")]
1103 pub fn get_tz_geojson(&self, timezone_name: &str) -> Option<FeatureItem> {
1104 let target = u16::try_from(
1106 self.names
1107 .binary_search_by(|name| name.as_str().cmp(timezone_name))
1108 .ok()?,
1109 )
1110 .ok()?;
1111
1112 let mut keys = Vec::new();
1113
1114 for (key, entry) in &self.all {
1116 if entry.indices().contains(&target) {
1117 keys.push(unpack_tile_key(*key));
1118 }
1119 }
1120
1121 if keys.is_empty() {
1122 return None;
1123 }
1124
1125 let mut multi_polygon_coords = MultiPolygonCoordinates::new();
1126
1127 for (x, y, z) in keys {
1128 let tile_poly = tile_to_polygon(x, y, z);
1130 multi_polygon_coords.push(vec![tile_poly]);
1131 }
1132
1133 Some(FeatureItem {
1134 feature_type: "Feature".to_string(),
1135 properties: PropertiesDefine {
1136 tzid: timezone_name.to_string(),
1137 },
1138 geometry: GeometryDefine {
1139 geometry_type: "MultiPolygon".to_string(),
1140 coordinates: multi_polygon_coords,
1141 },
1142 })
1143 }
1144}
1145
1146#[cfg(feature = "export-geojson")]
1148#[allow(clippy::cast_precision_loss)]
1149fn tile_to_polygon(x: i64, y: i64, z: i64) -> Vec<[f64; 2]> {
1150 let n = f64::powf(2.0, z as f64);
1151
1152 let lng_min = (x as f64) / n * 360.0 - 180.0;
1154 let lat_min_rad = ((1.0 - ((y + 1) as f64) / n * 2.0) * PI).sinh().atan();
1155 let lat_min = lat_min_rad.to_degrees();
1156
1157 let lng_max = ((x + 1) as f64) / n * 360.0 - 180.0;
1159 let lat_max_rad = ((1.0 - (y as f64) / n * 2.0) * PI).sinh().atan();
1160 let lat_max = lat_max_rad.to_degrees();
1161
1162 vec![
1164 [lng_min, lat_min],
1165 [lng_max, lat_min],
1166 [lng_max, lat_max],
1167 [lng_min, lat_max],
1168 [lng_min, lat_min],
1169 ]
1170}
1171
1172pub struct DefaultFinder {
1175 pub finder: Finder,
1176 pub fuzzy_finder: FuzzyFinder,
1177}
1178
1179impl Default for DefaultFinder {
1180 fn default() -> Self {
1189 let options = FinderOptions::y_stripes();
1190 let topo_bytes = load_topology_compress_topo();
1191 let tzs = pbgen::CompressedTopoTimezones::try_from(topo_bytes).unwrap_or_default();
1192 let finder = Finder::from_compressed_topo_with_options(tzs, options);
1193
1194 let fuzzy_finder = FuzzyFinder::default();
1195
1196 Self {
1197 finder,
1198 fuzzy_finder,
1199 }
1200 }
1201}
1202
1203impl DefaultFinder {
1204 #[must_use]
1208 pub fn new_with_options(options: FinderOptions) -> Self {
1209 let topo_bytes = load_topology_compress_topo();
1210 let tzs = pbgen::CompressedTopoTimezones::try_from(topo_bytes).unwrap_or_default();
1211 Self {
1212 finder: Finder::from_compressed_topo_with_options(tzs, options),
1213 fuzzy_finder: FuzzyFinder::default(),
1214 }
1215 }
1216
1217 #[must_use]
1239 #[cfg(feature = "full")]
1240 #[cfg_attr(docsrs, doc(cfg(feature = "full")))]
1241 pub fn new_full() -> Self {
1242 Self::new_full_with_options(FinderOptions::y_stripes())
1243 }
1244
1245 #[must_use]
1247 #[cfg(feature = "full")]
1248 #[cfg_attr(docsrs, doc(cfg(feature = "full")))]
1249 pub fn new_full_with_options(options: FinderOptions) -> Self {
1250 let tzs =
1251 pbgen::CompressedTopoTimezones::try_from(load_compress_topo()).unwrap_or_default();
1252 Self {
1253 finder: Finder::from_compressed_topo_with_options(tzs, options),
1254 fuzzy_finder: FuzzyFinder::default(),
1255 }
1256 }
1257
1258 #[must_use]
1264 pub fn get_tz_name(&self, lng: f64, lat: f64) -> &str {
1265 let fuzzy = self.fuzzy_finder.get_tz_name(lng, lat);
1266 if !fuzzy.is_empty() {
1267 return fuzzy;
1268 }
1269 self.finder.get_tz_name(lng, lat)
1270 }
1271
1272 #[must_use]
1278 pub fn get_tz_names(&self, lng: f64, lat: f64) -> Vec<&str> {
1279 self.finder.get_tz_names(lng, lat)
1280 }
1281
1282 #[must_use]
1290 pub fn timezonenames(&self) -> Vec<&str> {
1291 self.finder.timezonenames()
1292 }
1293
1294 #[must_use]
1305 pub fn data_version(&self) -> &str {
1306 self.finder.data_version()
1307 }
1308
1309 #[must_use]
1316 pub fn new() -> Self {
1317 Self::default()
1318 }
1319
1320 #[must_use]
1336 #[cfg(feature = "export-geojson")]
1337 pub fn to_geojson(&self) -> BoundaryFile {
1338 self.finder.to_geojson()
1339 }
1340
1341 #[must_use]
1368 #[cfg(feature = "export-geojson")]
1369 pub fn get_tz_geojson(&self, timezone_name: &str) -> Option<BoundaryFile> {
1370 self.finder.get_tz_geojson(timezone_name)
1371 }
1372}