1#![doc = include_str!("../README.md")]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3
4use geometry_rs::{Point, Polygon, PolygonBuildOptions};
5#[cfg(feature = "export-geojson")]
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::f64::consts::PI;
9use std::vec;
10#[cfg(all(feature = "bundled", feature = "full"))]
11compile_error!(
12 "features `bundled` and `full` are mutually exclusive; \
13 add `default-features = false` when enabling `full`"
14);
15
16#[cfg(feature = "bundled")]
17use tzf_dist::{load_preindex, load_topology_compress_topo};
18#[cfg(feature = "full")]
19use tzf_dist_git::{load_compress_topo, load_preindex, load_topology_compress_topo};
20pub mod pbgen;
21
22struct Item {
23 polys: Vec<Polygon>,
24 name: String,
25}
26
27impl Item {
28 fn contains_point(&self, p: &Point) -> bool {
29 for poly in &self.polys {
30 if poly.contains_point(*p) {
31 return true;
32 }
33 }
34 false
35 }
36}
37
38pub struct Finder {
47 all: Vec<Item>,
48 data_version: String,
49}
50
51const DEFAULT_RTREE_MIN_SEGMENTS: usize = 64;
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58#[non_exhaustive]
59pub enum FinderOptions {
60 #[default]
62 NoIndex,
63 YStripes,
65}
66
67impl FinderOptions {
68 #[must_use]
70 pub fn no_index() -> Self {
71 Self::NoIndex
72 }
73
74 #[must_use]
76 pub fn y_stripes() -> Self {
77 Self::YStripes
78 }
79
80 fn to_polygon_build_options(self) -> PolygonBuildOptions {
81 match self {
82 Self::YStripes => PolygonBuildOptions {
83 enable_rtree: false,
84 enable_compressed_quad: false,
85 enable_y_stripes: true,
86 rtree_min_segments: DEFAULT_RTREE_MIN_SEGMENTS,
87 },
88 Self::NoIndex => PolygonBuildOptions {
89 enable_rtree: false,
90 enable_compressed_quad: false,
91 enable_y_stripes: false,
92 rtree_min_segments: DEFAULT_RTREE_MIN_SEGMENTS,
93 },
94 }
95 }
96}
97
98#[allow(clippy::cast_possible_truncation)]
102fn decode_polyline(encoded: &[u8]) -> Vec<Point> {
103 let mut points = Vec::new();
104 let mut index = 0;
105 let mut lng: i64 = 0;
106 let mut lat: i64 = 0;
107
108 while index < encoded.len() {
109 let (dlng, next) = polyline_decode_value(encoded, index);
110 index = next;
111 let (dlat, next) = polyline_decode_value(encoded, index);
112 index = next;
113 lng += dlng;
114 lat += dlat;
115 points.push(Point {
116 x: lng as f64 / 1e5,
117 y: lat as f64 / 1e5,
118 });
119 }
120 points
121}
122
123fn polyline_decode_value(encoded: &[u8], start: usize) -> (i64, usize) {
124 let mut result: i64 = 0;
125 let mut shift = 0;
126 let mut index = start;
127
128 loop {
129 let byte = (encoded[index] as i64) - 63;
130 index += 1;
131 result |= (byte & 0x1F) << shift;
132 shift += 5;
133 if byte < 0x20 {
134 break;
135 }
136 }
137
138 let value = if result & 1 != 0 {
139 !(result >> 1)
140 } else {
141 result >> 1
142 };
143 (value, index)
144}
145
146fn expand_compressed_ring(
147 segs: &[pbgen::CompressedRingSegment],
148 edges: &[Vec<Point>],
149) -> Vec<Point> {
150 let mut pts = Vec::new();
151 for seg in segs {
152 match &seg.content {
153 Some(pbgen::compressed_ring_segment::Content::Inline(inline)) => {
154 pts.extend(decode_polyline(&inline.points));
155 }
156 Some(pbgen::compressed_ring_segment::Content::EdgeForward(idx)) => {
157 pts.extend_from_slice(&edges[*idx as usize]);
158 }
159 Some(pbgen::compressed_ring_segment::Content::EdgeReversed(idx)) => {
160 pts.extend(edges[*idx as usize].iter().rev().copied());
161 }
162 None => {}
163 }
164 }
165 pts
166}
167
168impl Finder {
169 fn from_pb_with_polygon_options(tzs: pbgen::Timezones, options: PolygonBuildOptions) -> Self {
170 let mut f = Self {
171 all: vec![],
172 data_version: tzs.version,
173 };
174 for tz in &tzs.timezones {
175 let mut polys: Vec<Polygon> = vec![];
176
177 for pbpoly in &tz.polygons {
178 let mut exterior: Vec<Point> = vec![];
179 for pbpoint in &pbpoly.points {
180 exterior.push(Point {
181 x: f64::from(pbpoint.lng),
182 y: f64::from(pbpoint.lat),
183 });
184 }
185
186 let mut interior: Vec<Vec<Point>> = vec![];
187
188 for holepoly in &pbpoly.holes {
189 let mut holeextr: Vec<Point> = vec![];
190 for holepoint in &holepoly.points {
191 holeextr.push(Point {
192 x: f64::from(holepoint.lng),
193 y: f64::from(holepoint.lat),
194 });
195 }
196 interior.push(holeextr);
197 }
198
199 let geopoly = geometry_rs::Polygon::new(exterior, interior, Some(options));
200 polys.push(geopoly);
201 }
202
203 let item: Item = Item {
204 name: tz.name.to_string(),
205 polys,
206 };
207
208 f.all.push(item);
209 }
210 f
211 }
212
213 fn from_compressed_topo_with_polygon_options(
214 tzs: pbgen::CompressedTopoTimezones,
215 options: PolygonBuildOptions,
216 ) -> Self {
217 let mut edges: Vec<Vec<Point>> = vec![Vec::new(); tzs.shared_edges.len()];
218 for edge in &tzs.shared_edges {
219 edges[edge.id as usize] = decode_polyline(&edge.points);
220 }
221
222 let mut f = Self {
223 all: vec![],
224 data_version: tzs.version,
225 };
226
227 for tz in &tzs.timezones {
228 let mut polys: Vec<Polygon> = vec![];
229 for poly in &tz.polygons {
230 let exterior = expand_compressed_ring(&poly.exterior, &edges);
231 let interior: Vec<Vec<Point>> = poly
232 .holes
233 .iter()
234 .map(|hole| expand_compressed_ring(&hole.exterior, &edges))
235 .collect();
236 polys.push(geometry_rs::Polygon::new(exterior, interior, Some(options)));
237 }
238 f.all.push(Item {
239 name: tz.name.clone(),
240 polys,
241 });
242 }
243 f
244 }
245
246 #[must_use]
250 pub fn from_compressed_topo(tzs: pbgen::CompressedTopoTimezones) -> Self {
251 Self::from_compressed_topo_with_options(tzs, FinderOptions::default())
252 }
253
254 #[must_use]
256 pub fn from_compressed_topo_with_options(
257 tzs: pbgen::CompressedTopoTimezones,
258 options: FinderOptions,
259 ) -> Self {
260 Self::from_compressed_topo_with_polygon_options(tzs, options.to_polygon_build_options())
261 }
262
263 #[must_use]
274 pub fn from_pb(tzs: pbgen::Timezones) -> Self {
275 Self::from_pb_with_options(tzs, FinderOptions::default())
276 }
277
278 #[must_use]
280 pub fn from_pb_with_options(tzs: pbgen::Timezones, options: FinderOptions) -> Self {
281 Self::from_pb_with_polygon_options(tzs, options.to_polygon_build_options())
282 }
283
284 #[must_use]
293 pub fn get_tz_name(&self, lng: f64, lat: f64) -> &str {
294 let direct_res = self._get_tz_name(lng, lat);
295 if !direct_res.is_empty() {
296 return direct_res;
297 }
298 ""
299 }
300
301 fn _get_tz_name(&self, lng: f64, lat: f64) -> &str {
302 let p = geometry_rs::Point { x: lng, y: lat };
303 for item in &self.all {
304 if item.contains_point(&p) {
305 return &item.name;
306 }
307 }
308 ""
309 }
310
311 #[must_use]
317 pub fn get_tz_names(&self, lng: f64, lat: f64) -> Vec<&str> {
318 let mut ret: Vec<&str> = vec![];
319 let p = geometry_rs::Point { x: lng, y: lat };
320 for item in &self.all {
321 if item.contains_point(&p) {
322 ret.push(&item.name);
323 }
324 }
325 ret
326 }
327
328 #[must_use]
337 pub fn timezonenames(&self) -> Vec<&str> {
338 let mut ret: Vec<&str> = vec![];
339 for item in &self.all {
340 ret.push(&item.name);
341 }
342 ret
343 }
344
345 #[must_use]
354 pub fn data_version(&self) -> &str {
355 &self.data_version
356 }
357
358 #[must_use]
368 pub fn new() -> Self {
369 Self::default()
370 }
371
372 #[cfg(feature = "export-geojson")]
374 fn item_to_feature(&self, item: &Item) -> FeatureItem {
375 let mut pbpolys = Vec::new();
377 for poly in &item.polys {
378 let mut pbpoly = pbgen::Polygon {
379 points: Vec::new(),
380 holes: Vec::new(),
381 };
382
383 for point in poly.exterior() {
385 pbpoly.points.push(pbgen::Point {
386 lng: point.x as f32,
387 lat: point.y as f32,
388 });
389 }
390
391 for hole in poly.holes() {
393 let mut hole_poly = pbgen::Polygon {
394 points: Vec::new(),
395 holes: Vec::new(),
396 };
397 for point in hole {
398 hole_poly.points.push(pbgen::Point {
399 lng: point.x as f32,
400 lat: point.y as f32,
401 });
402 }
403 pbpoly.holes.push(hole_poly);
404 }
405
406 pbpolys.push(pbpoly);
407 }
408
409 let pbtz = pbgen::Timezone {
410 polygons: pbpolys,
411 name: item.name.clone(),
412 };
413
414 revert_item(&pbtz)
415 }
416
417 #[must_use]
431 #[cfg(feature = "export-geojson")]
432 pub fn to_geojson(&self) -> BoundaryFile {
433 let mut output = BoundaryFile {
434 collection_type: "FeatureCollection".to_string(),
435 features: Vec::new(),
436 };
437
438 for item in &self.all {
439 output.features.push(self.item_to_feature(item));
440 }
441
442 output
443 }
444
445 #[must_use]
470 #[cfg(feature = "export-geojson")]
471 pub fn get_tz_geojson(&self, timezone_name: &str) -> Option<BoundaryFile> {
472 let mut output = BoundaryFile {
473 collection_type: "FeatureCollection".to_string(),
474 features: Vec::new(),
475 };
476 for item in &self.all {
477 if item.name == timezone_name {
478 output.features.push(self.item_to_feature(item));
479 }
480 }
481
482 if output.features.is_empty() {
483 None
484 } else {
485 Some(output)
486 }
487 }
488}
489
490impl Default for Finder {
500 fn default() -> Self {
501 let file_bytes = load_topology_compress_topo();
502 Self::from_compressed_topo(
503 pbgen::CompressedTopoTimezones::try_from(file_bytes).unwrap_or_default(),
504 )
505 }
506}
507
508#[must_use]
521#[allow(
522 clippy::cast_precision_loss,
523 clippy::cast_possible_truncation,
524 clippy::similar_names
525)]
526pub fn deg2num(lng: f64, lat: f64, zoom: i64) -> (i64, i64) {
527 let lat_rad = lat.to_radians();
528 let n = f64::powf(2.0, zoom as f64);
529 let xtile = (lng + 180.0) / 360.0 * n;
530 let ytile = (1.0 - lat_rad.tan().asinh() / PI) / 2.0 * n;
531
532 (xtile as i64, ytile as i64)
534}
535
536#[cfg(feature = "export-geojson")]
538pub type PolygonCoordinates = Vec<Vec<[f64; 2]>>;
539#[cfg(feature = "export-geojson")]
540pub type MultiPolygonCoordinates = Vec<PolygonCoordinates>;
541
542#[cfg(feature = "export-geojson")]
543#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct GeometryDefine {
545 #[serde(rename = "type")]
546 pub geometry_type: String,
547 pub coordinates: MultiPolygonCoordinates,
548}
549
550#[cfg(feature = "export-geojson")]
551#[derive(Debug, Clone, Serialize, Deserialize)]
552pub struct PropertiesDefine {
553 pub tzid: String,
554}
555
556#[cfg(feature = "export-geojson")]
557#[derive(Debug, Clone, Serialize, Deserialize)]
558pub struct FeatureItem {
559 #[serde(rename = "type")]
560 pub feature_type: String,
561 pub properties: PropertiesDefine,
562 pub geometry: GeometryDefine,
563}
564
565#[cfg(feature = "export-geojson")]
566impl FeatureItem {
567 pub fn to_string(&self) -> String {
568 serde_json::to_string(self).unwrap_or_default()
569 }
570
571 pub fn to_string_pretty(&self) -> String {
572 serde_json::to_string_pretty(self).unwrap_or_default()
573 }
574}
575
576#[cfg(feature = "export-geojson")]
577#[derive(Debug, Clone, Serialize, Deserialize)]
578pub struct BoundaryFile {
579 #[serde(rename = "type")]
580 pub collection_type: String,
581 pub features: Vec<FeatureItem>,
582}
583
584#[cfg(feature = "export-geojson")]
585impl BoundaryFile {
586 pub fn to_string(&self) -> String {
587 serde_json::to_string(self).unwrap_or_default()
588 }
589
590 pub fn to_string_pretty(&self) -> String {
591 serde_json::to_string_pretty(self).unwrap_or_default()
592 }
593}
594
595#[cfg(feature = "export-geojson")]
597fn from_pb_polygon_to_geo_multipolygon(pbpoly: &[pbgen::Polygon]) -> MultiPolygonCoordinates {
598 let mut res = MultiPolygonCoordinates::new();
599 for poly in pbpoly {
600 let mut new_geo_poly = PolygonCoordinates::new();
601
602 let mut mainpoly = Vec::new();
604 for point in &poly.points {
605 mainpoly.push([f64::from(point.lng), f64::from(point.lat)]);
606 }
607 new_geo_poly.push(mainpoly);
608
609 for holepoly in &poly.holes {
611 let mut holepoly_coords = Vec::new();
612 for point in &holepoly.points {
613 holepoly_coords.push([f64::from(point.lng), f64::from(point.lat)]);
614 }
615 new_geo_poly.push(holepoly_coords);
616 }
617 res.push(new_geo_poly);
618 }
619 res
620}
621
622#[cfg(feature = "export-geojson")]
624fn revert_item(input: &pbgen::Timezone) -> FeatureItem {
625 FeatureItem {
626 feature_type: "Feature".to_string(),
627 properties: PropertiesDefine {
628 tzid: input.name.clone(),
629 },
630 geometry: GeometryDefine {
631 geometry_type: "MultiPolygon".to_string(),
632 coordinates: from_pb_polygon_to_geo_multipolygon(&input.polygons),
633 },
634 }
635}
636
637#[cfg(feature = "export-geojson")]
639pub fn revert_timezones(input: &pbgen::Timezones) -> BoundaryFile {
640 let mut output = BoundaryFile {
641 collection_type: "FeatureCollection".to_string(),
642 features: Vec::new(),
643 };
644 for timezone in &input.timezones {
645 let item = revert_item(timezone);
646 output.features.push(item);
647 }
648 output
649}
650
651pub struct FuzzyFinder {
662 min_zoom: i64,
663 max_zoom: i64,
664 all: HashMap<(i64, i64, i64), Vec<String>>, data_version: String,
666}
667
668impl Default for FuzzyFinder {
669 fn default() -> Self {
677 let file_bytes = load_preindex();
678 Self::from_pb(pbgen::PreindexTimezones::try_from(file_bytes.to_vec()).unwrap_or_default())
679 }
680}
681
682impl FuzzyFinder {
683 #[must_use]
684 pub fn from_pb(tzs: pbgen::PreindexTimezones) -> Self {
685 let mut f = Self {
686 min_zoom: i64::from(tzs.agg_zoom),
687 max_zoom: i64::from(tzs.idx_zoom),
688 all: HashMap::new(),
689 data_version: tzs.version,
690 };
691 for item in &tzs.keys {
692 let key = (i64::from(item.x), i64::from(item.y), i64::from(item.z));
693 let names = f.all.entry(key).or_default();
694 names.push(item.name.to_string());
695 names.sort();
696 }
697 f
698 }
699
700 #[must_use]
721 pub fn get_tz_name(&self, lng: f64, lat: f64) -> &str {
722 for zoom in self.min_zoom..self.max_zoom {
723 let idx = deg2num(lng, lat, zoom);
724 let k = &(idx.0, idx.1, zoom);
725 let ret = self.all.get(k);
726 if ret.is_none() {
727 continue;
728 }
729 return ret.unwrap().first().unwrap();
730 }
731 ""
732 }
733
734 pub fn get_tz_names(&self, lng: f64, lat: f64) -> Vec<&str> {
735 let mut names: Vec<&str> = vec![];
736 for zoom in self.min_zoom..self.max_zoom {
737 let idx = deg2num(lng, lat, zoom);
738 let k = &(idx.0, idx.1, zoom);
739 let ret = self.all.get(k);
740 if ret.is_none() {
741 continue;
742 }
743 for item in ret.unwrap() {
744 names.push(item);
745 }
746 }
747 names
748 }
749
750 #[must_use]
765 pub fn data_version(&self) -> &str {
766 &self.data_version
767 }
768
769 #[must_use]
777 pub fn new() -> Self {
778 Self::default()
779 }
780
781 #[must_use]
798 #[cfg(feature = "export-geojson")]
799 pub fn to_geojson(&self) -> BoundaryFile {
800 let mut name_to_keys: HashMap<&String, Vec<(i64, i64, i64)>> = HashMap::new();
801
802 for (key, names) in &self.all {
804 for name in names {
805 name_to_keys.entry(name).or_insert_with(Vec::new).push(*key);
806 }
807 }
808
809 let mut features = Vec::new();
810
811 for (name, keys) in name_to_keys {
812 let mut multi_polygon_coords = MultiPolygonCoordinates::new();
813
814 for (x, y, z) in keys {
815 let tile_poly = tile_to_polygon(x, y, z);
817 multi_polygon_coords.push(vec![tile_poly]);
818 }
819
820 let feature = FeatureItem {
821 feature_type: "Feature".to_string(),
822 properties: PropertiesDefine { tzid: name.clone() },
823 geometry: GeometryDefine {
824 geometry_type: "MultiPolygon".to_string(),
825 coordinates: multi_polygon_coords,
826 },
827 };
828
829 features.push(feature);
830 }
831
832 BoundaryFile {
833 collection_type: "FeatureCollection".to_string(),
834 features,
835 }
836 }
837
838 #[must_use]
858 #[cfg(feature = "export-geojson")]
859 pub fn get_tz_geojson(&self, timezone_name: &str) -> Option<FeatureItem> {
860 let mut keys = Vec::new();
861
862 for (key, names) in &self.all {
864 if names.iter().any(|n| n == timezone_name) {
865 keys.push(*key);
866 }
867 }
868
869 if keys.is_empty() {
870 return None;
871 }
872
873 let mut multi_polygon_coords = MultiPolygonCoordinates::new();
874
875 for (x, y, z) in keys {
876 let tile_poly = tile_to_polygon(x, y, z);
878 multi_polygon_coords.push(vec![tile_poly]);
879 }
880
881 Some(FeatureItem {
882 feature_type: "Feature".to_string(),
883 properties: PropertiesDefine {
884 tzid: timezone_name.to_string(),
885 },
886 geometry: GeometryDefine {
887 geometry_type: "MultiPolygon".to_string(),
888 coordinates: multi_polygon_coords,
889 },
890 })
891 }
892}
893
894#[cfg(feature = "export-geojson")]
896#[allow(clippy::cast_precision_loss)]
897fn tile_to_polygon(x: i64, y: i64, z: i64) -> Vec<[f64; 2]> {
898 let n = f64::powf(2.0, z as f64);
899
900 let lng_min = (x as f64) / n * 360.0 - 180.0;
902 let lat_min_rad = ((1.0 - ((y + 1) as f64) / n * 2.0) * PI).sinh().atan();
903 let lat_min = lat_min_rad.to_degrees();
904
905 let lng_max = ((x + 1) as f64) / n * 360.0 - 180.0;
907 let lat_max_rad = ((1.0 - (y as f64) / n * 2.0) * PI).sinh().atan();
908 let lat_max = lat_max_rad.to_degrees();
909
910 vec![
912 [lng_min, lat_min],
913 [lng_max, lat_min],
914 [lng_max, lat_max],
915 [lng_min, lat_max],
916 [lng_min, lat_min],
917 ]
918}
919
920pub struct DefaultFinder {
923 pub finder: Finder,
924 pub fuzzy_finder: FuzzyFinder,
925}
926
927impl Default for DefaultFinder {
928 fn default() -> Self {
937 let options = FinderOptions::y_stripes();
938 let topo_bytes = load_topology_compress_topo();
939 let tzs = pbgen::CompressedTopoTimezones::try_from(topo_bytes).unwrap_or_default();
940 let finder = Finder::from_compressed_topo_with_options(tzs, options);
941
942 let fuzzy_finder = FuzzyFinder::default();
943
944 Self {
945 finder,
946 fuzzy_finder,
947 }
948 }
949}
950
951impl DefaultFinder {
952 #[must_use]
956 pub fn new_with_options(options: FinderOptions) -> Self {
957 let topo_bytes = load_topology_compress_topo();
958 let tzs = pbgen::CompressedTopoTimezones::try_from(topo_bytes).unwrap_or_default();
959 Self {
960 finder: Finder::from_compressed_topo_with_options(tzs, options),
961 fuzzy_finder: FuzzyFinder::default(),
962 }
963 }
964
965 #[must_use]
987 #[cfg(feature = "full")]
988 #[cfg_attr(docsrs, doc(cfg(feature = "full")))]
989 pub fn new_full() -> Self {
990 Self::new_full_with_options(FinderOptions::y_stripes())
991 }
992
993 #[must_use]
995 #[cfg(feature = "full")]
996 #[cfg_attr(docsrs, doc(cfg(feature = "full")))]
997 pub fn new_full_with_options(options: FinderOptions) -> Self {
998 let tzs =
999 pbgen::CompressedTopoTimezones::try_from(load_compress_topo()).unwrap_or_default();
1000 Self {
1001 finder: Finder::from_compressed_topo_with_options(tzs, options),
1002 fuzzy_finder: FuzzyFinder::default(),
1003 }
1004 }
1005
1006 #[must_use]
1012 pub fn get_tz_name(&self, lng: f64, lat: f64) -> &str {
1013 let res = self.get_tz_names(lng, lat);
1018 if !res.is_empty() {
1019 return res.first().unwrap();
1020 }
1021 ""
1022 }
1023
1024 #[must_use]
1030 pub fn get_tz_names(&self, lng: f64, lat: f64) -> Vec<&str> {
1031 let fuzzy_names = self.fuzzy_finder.get_tz_names(lng, lat);
1032 if !fuzzy_names.is_empty() {
1033 return fuzzy_names;
1034 }
1035 let names = self.finder.get_tz_names(lng, lat);
1036 if !names.is_empty() {
1037 return names;
1038 }
1039 Vec::new() }
1041
1042 #[must_use]
1050 pub fn timezonenames(&self) -> Vec<&str> {
1051 self.finder.timezonenames()
1052 }
1053
1054 #[must_use]
1065 pub fn data_version(&self) -> &str {
1066 &self.finder.data_version
1067 }
1068
1069 #[must_use]
1076 pub fn new() -> Self {
1077 Self::default()
1078 }
1079
1080 #[must_use]
1096 #[cfg(feature = "export-geojson")]
1097 pub fn to_geojson(&self) -> BoundaryFile {
1098 self.finder.to_geojson()
1099 }
1100
1101 #[must_use]
1128 #[cfg(feature = "export-geojson")]
1129 pub fn get_tz_geojson(&self, timezone_name: &str) -> Option<BoundaryFile> {
1130 self.finder.get_tz_geojson(timezone_name)
1131 }
1132}