1use std::fmt::Display;
2use std::ops::{Index, Range};
3
4use ecow::EcoString;
5use geo::{Coord, Distance, Haversine, Intersects, LineString, Point, Polygon};
6use geohash::{Direction, GeohashError, decode, decode_bbox, encode};
7use itertools::Itertools;
8use ordered_float::OrderedFloat;
9
10use crate::segment::common::operation_error::{OperationError, OperationResult};
11use crate::segment::types::{GeoBoundingBox, GeoPoint, GeoPolygon, GeoRadius};
12
13#[derive(Default, Clone, Copy, Debug, PartialEq, Hash, Ord, PartialOrd, Eq)]
34pub struct GeoHash(u64);
35
36#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
41#[repr(C)]
42pub struct GeoHashRaw(pub u64);
43
44impl GeoHashRaw {
45 pub fn normalize(self) -> GeoHash {
47 GeoHash::new_from_parts(self.0, self.0 & GeoHash::LEN_MASK)
48 }
49}
50
51impl From<GeoHash> for GeoHashRaw {
52 fn from(hash: GeoHash) -> GeoHashRaw {
53 GeoHashRaw(hash.0)
54 }
55}
56
57const LON_RANGE: Range<f64> = -180.0..180.0;
58const LAT_RANGE: Range<f64> = -90.0..90.0;
59const COORD_EPS: f64 = 1e-12;
60
61impl Index<usize> for GeoHash {
62 type Output = u8;
63
64 fn index(&self, i: usize) -> &Self::Output {
65 assert!(i < self.len());
66 let index = (self.0 >> Self::shift_value(i)) & ((1 << GeoHash::CHAR_BITS) - 1);
67 &GeoHash::BASE32[index as usize]
68 }
69}
70
71impl TryFrom<EcoString> for GeoHash {
72 type Error = GeohashError;
73
74 fn try_from(hash: EcoString) -> Result<Self, Self::Error> {
75 Self::new(hash.as_bytes())
76 }
77}
78
79impl TryFrom<String> for GeoHash {
80 type Error = GeohashError;
81
82 fn try_from(hash: String) -> Result<Self, Self::Error> {
83 Self::new(hash.as_bytes())
84 }
85}
86
87impl From<GeoHash> for EcoString {
88 fn from(hash: GeoHash) -> Self {
89 hash.iter().map(char::from).collect()
90 }
91}
92
93impl Display for GeoHash {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 EcoString::from(*self).fmt(f)
96 }
97}
98
99pub struct GeoHashIterator(u64);
100
101impl Iterator for GeoHashIterator {
102 type Item = u8;
103
104 fn next(&mut self) -> Option<Self::Item> {
105 let len = self.0 & GeoHash::LEN_MASK;
106 if len > 0 {
107 let char_index = self.0 >> (GeoHash::BITS - GeoHash::CHAR_BITS);
109
110 self.0 = (self.0 << GeoHash::CHAR_BITS) | (len - 1);
112
113 Some(GeoHash::BASE32[char_index as usize])
115 } else {
116 None
117 }
118 }
119}
120
121impl GeoHash {
122 const BITS: u32 = u64::BITS;
123
124 const MAX_LEN: usize = 12;
126
127 const LEN_MASK: u64 = 0b1111;
130
131 const LEN_BITS: u32 = 4;
132
133 const CHAR_BITS: u32 = 5;
134
135 const BASE32: [u8; 32] = *b"0123456789bcdefghjkmnpqrstuvwxyz";
137
138 fn new<H>(s: H) -> Result<Self, GeohashError>
139 where
140 H: AsRef<[u8]>,
141 {
142 let s = s.as_ref();
143 if s.len() > GeoHash::MAX_LEN {
144 return Err(GeohashError::InvalidLength(s.len()));
145 }
146 let mut packed: u64 = 0;
147 for (i, c) in s.iter().enumerate() {
148 let index = GeoHash::BASE32.iter().position(|x| x == c).unwrap() as u64;
149 packed |= index << Self::shift_value(i);
150 }
151 packed |= s.len() as u64;
152 Ok(Self(packed))
153 }
154
155 fn new_from_parts(characters: u64, len: u64) -> GeoHash {
156 let len = len.min(GeoHash::MAX_LEN as u64);
157 let characters_mask = !GeoHash::LEN_MASK
158 << (GeoHash::BITS - GeoHash::LEN_BITS - GeoHash::CHAR_BITS * len as u32);
159 GeoHash((characters & characters_mask) | len)
160 }
161
162 pub fn iter(&self) -> GeoHashIterator {
163 GeoHashIterator(self.0)
164 }
165
166 pub fn is_empty(&self) -> bool {
167 self.len() == 0
168 }
169
170 pub fn len(&self) -> usize {
171 (self.0 & GeoHash::LEN_MASK) as usize
172 }
173
174 pub fn truncate(&self, new_len: usize) -> Self {
175 assert!(new_len <= self.len());
176 GeoHash::new_from_parts(self.0, new_len as u64)
177 }
178
179 pub fn starts_with(&self, other: GeoHash) -> bool {
180 if self.len() < other.len() {
181 return false;
183 }
184 if other.is_empty() {
185 return true;
187 }
188
189 let self_shifted = self.0 >> Self::shift_value(other.len() - 1);
190 let other_shifted = other.0 >> Self::shift_value(other.len() - 1);
191 self_shifted == other_shifted
192 }
193
194 fn shift_value(i: usize) -> u32 {
196 assert!(i < GeoHash::MAX_LEN);
197 GeoHash::LEN_BITS + GeoHash::CHAR_BITS * (GeoHash::MAX_LEN as u32 - 1 - i as u32)
198 }
199}
200
201impl From<GeoPoint> for Coord<f64> {
202 fn from(point: GeoPoint) -> Self {
203 Self {
204 x: point.lon.0,
205 y: point.lat.0,
206 }
207 }
208}
209
210pub fn common_hash_prefix(geo_hashes: &[GeoHash]) -> Option<GeoHash> {
211 if geo_hashes.is_empty() {
212 return None;
213 }
214 let first = &geo_hashes[0];
215 let mut prefix: usize = first.len();
216 for geo_hash in geo_hashes.iter().skip(1) {
217 for i in 0..prefix {
218 if first[i] != geo_hash[i] {
219 prefix = i;
220 break;
221 }
222 }
223 }
224 Some(first.truncate(prefix))
225}
226
227fn sphere_lon(lon: f64) -> f64 {
230 let mut res_lon = lon;
231 if res_lon > LON_RANGE.end {
232 res_lon = LON_RANGE.start + res_lon - LON_RANGE.end;
233 }
234 if res_lon < LON_RANGE.start {
235 res_lon = LON_RANGE.end + res_lon - LON_RANGE.start;
236 }
237 res_lon
238}
239
240fn sphere_lat(lat: f64) -> f64 {
242 let mut res_lat = lat;
243 if res_lat > LAT_RANGE.end {
244 res_lat = LAT_RANGE.end - COORD_EPS;
245 }
246 if res_lat < LAT_RANGE.start {
247 res_lat = LAT_RANGE.start + COORD_EPS;
248 }
249 res_lat
250}
251
252fn sphere_neighbor(hash: GeoHash, direction: Direction) -> Result<GeoHash, GeohashError> {
254 let hash_str = EcoString::from(hash);
255 let (coord, lon_err, lat_err) = decode(hash_str.as_str())?;
256 let (dlat, dlng) = direction.to_tuple();
257 let lon = sphere_lon(coord.x + 2f64 * lon_err.abs() * dlng);
258 let lat = sphere_lat(coord.y + 2f64 * lat_err.abs() * dlat);
259
260 let neighbor_coord = Coord { x: lon, y: lat };
261 let encoded_string = encode(neighbor_coord, hash_str.len())?;
262 GeoHash::try_from(encoded_string)
263}
264
265pub fn encode_max_precision(lon: f64, lat: f64) -> Result<GeoHash, GeohashError> {
266 let encoded_string = encode((lon, lat).into(), GeoHash::MAX_LEN)?;
267 GeoHash::try_from(encoded_string)
268}
269
270pub fn geo_hash_to_box(geo_hash: GeoHash) -> GeoBoundingBox {
271 let rectangle = decode_bbox(EcoString::from(geo_hash).as_str()).unwrap();
272 let top_left = GeoPoint {
273 lon: OrderedFloat(rectangle.min().x),
274 lat: OrderedFloat(rectangle.max().y),
275 };
276 let bottom_right = GeoPoint {
277 lon: OrderedFloat(rectangle.max().x),
278 lat: OrderedFloat(rectangle.min().y),
279 };
280
281 GeoBoundingBox {
282 top_left,
283 bottom_right,
284 }
285}
286
287#[derive(Debug)]
288struct GeohashBoundingBox {
289 north_west: GeoHash,
290 south_west: GeoHash,
291 #[cfg_attr(not(test), expect(dead_code))]
292 south_east: GeoHash, north_east: GeoHash,
294}
295
296impl GeohashBoundingBox {
297 fn geohash_regions(&self, precision: usize, max_regions: usize) -> Option<Vec<GeoHash>> {
310 let mut seen: Vec<GeoHash> = Vec::new();
311
312 let mut from_row: GeoHash = self.north_west.truncate(precision);
313 let mut to_row: GeoHash = self.north_east.truncate(precision);
314
315 let to_column = self.south_west.truncate(precision);
316
317 loop {
318 let mut current = from_row;
319 loop {
320 seen.push(current);
321
322 if seen.len() > max_regions {
323 return None;
324 }
325
326 if current == to_row {
327 break;
328 }
329 current = sphere_neighbor(current, Direction::E).unwrap();
330 }
331 if from_row == to_column {
332 break;
333 }
334
335 from_row = sphere_neighbor(from_row, Direction::S).unwrap();
336 to_row = sphere_neighbor(to_row, Direction::S).unwrap();
337 }
338
339 Some(seen)
340 }
341}
342
343impl From<GeoBoundingBox> for GeohashBoundingBox {
344 fn from(bounding_box: GeoBoundingBox) -> Self {
345 let GeoPoint {
346 lat: OrderedFloat(max_lat),
347 lon: OrderedFloat(min_lon),
348 } = bounding_box.top_left;
349 let GeoPoint {
350 lat: OrderedFloat(min_lat),
351 lon: OrderedFloat(max_lon),
352 } = bounding_box.bottom_right;
353
354 let north_west = encode_max_precision(min_lon, max_lat).unwrap();
356 let south_west = encode_max_precision(min_lon, min_lat).unwrap();
357 let south_east = encode_max_precision(max_lon, min_lat).unwrap();
358 let north_east = encode_max_precision(max_lon, max_lat).unwrap();
359
360 Self {
361 north_west,
362 south_west,
363 south_east,
364 north_east,
365 }
366 }
367}
368
369fn check_circle_intersection(geohash: &str, circle: &GeoRadius) -> bool {
371 let precision = geohash.len();
372 if precision == 0 {
373 return true;
374 }
375 let rect = decode_bbox(geohash).unwrap();
376 let c0 = rect.min();
377 let c1 = rect.max();
378
379 let bbox_center = Point::new((c0.x + c1.x) / 2f64, (c0.y + c1.y) / 2f64);
380 let half_diagonal = Haversine.distance(bbox_center, Point(c0));
381
382 half_diagonal + circle.radius.0
383 > Haversine.distance(
384 bbox_center,
385 Point::new(circle.center.lon.0, circle.center.lat.0),
386 )
387}
388
389fn check_polygon_intersection(geohash: &str, polygon: &Polygon) -> bool {
391 let precision = geohash.len();
392 if precision == 0 {
393 return true;
394 }
395 let rect = decode_bbox(geohash).unwrap();
396
397 rect.intersects(polygon)
398}
399
400fn create_hashes(
401 mapping_fn: impl Fn(usize) -> Option<Vec<GeoHash>>,
402) -> OperationResult<Vec<GeoHash>> {
403 (0..=GeoHash::MAX_LEN)
404 .map(mapping_fn)
405 .take_while(|hashes| hashes.is_some())
406 .last()
407 .ok_or_else(|| OperationError::service_error("no hash coverage for any precision"))?
408 .ok_or_else(|| OperationError::service_error("geo-hash coverage is empty"))
409}
410
411pub fn circle_hashes(circle: &GeoRadius, max_regions: usize) -> OperationResult<Vec<GeoHash>> {
414 if max_regions == 0 {
415 return Err(OperationError::service_error(
416 "max_regions cannot be equal to zero",
417 ));
418 }
419
420 let geo_bounding_box = minimum_bounding_rectangle_for_circle(circle);
421 if geo_bounding_box.top_left.lat.is_nan()
422 || geo_bounding_box.top_left.lon.is_nan()
423 || geo_bounding_box.bottom_right.lat.is_nan()
424 || geo_bounding_box.bottom_right.lon.is_nan()
425 {
426 return Err(OperationError::service_error("Invalid circle"));
427 }
428 let full_geohash_bounding_box: GeohashBoundingBox = geo_bounding_box.into();
429
430 let mapping_fn = |precision| {
431 full_geohash_bounding_box
432 .geohash_regions(precision, max_regions)
433 .map(|hashes| {
434 hashes
435 .into_iter()
436 .filter(|hash| {
437 check_circle_intersection(EcoString::from(*hash).as_str(), circle)
438 })
439 .collect_vec()
440 })
441 };
442 create_hashes(mapping_fn)
443}
444
445pub fn rectangle_hashes(
448 rectangle: &GeoBoundingBox,
449 max_regions: usize,
450) -> OperationResult<Vec<GeoHash>> {
451 if max_regions == 0 {
452 return Err(OperationError::service_error(
453 "max_regions cannot be equal to zero",
454 ));
455 }
456 let full_geohash_bounding_box: GeohashBoundingBox = (*rectangle).into();
457
458 let mapping_fn = |precision| full_geohash_bounding_box.geohash_regions(precision, max_regions);
459 create_hashes(mapping_fn)
460}
461
462fn boundary_hashes(boundary: &LineString, max_regions: usize) -> OperationResult<Vec<GeoHash>> {
465 let geo_bounding_box = minimum_bounding_rectangle_for_boundary(boundary);
466 let full_geohash_bounding_box: GeohashBoundingBox = geo_bounding_box.into();
467 let polygon = Polygon::new(boundary.clone(), vec![]);
468
469 let mapping_fn = |precision| {
470 full_geohash_bounding_box
471 .geohash_regions(precision, max_regions)
472 .map(|hashes| {
473 hashes
474 .into_iter()
475 .filter(|hash| {
476 check_polygon_intersection(EcoString::from(*hash).as_str(), &polygon)
477 })
478 .collect_vec()
479 })
480 };
481 create_hashes(mapping_fn)
482}
483
484pub fn polygon_hashes_estimation(
491 polygon: &GeoPolygon,
492 max_regions: usize,
493) -> (Vec<GeoHash>, Vec<Vec<GeoHash>>) {
494 assert_ne!(max_regions, 0, "max_regions cannot be equal to zero");
495 let polygon_wrapper = polygon.convert().polygon;
496 let exterior_hashes = boundary_hashes(&polygon_wrapper.exterior().clone(), max_regions);
497 let interiors_hashes = polygon_wrapper
498 .interiors()
499 .iter()
500 .map(|interior| boundary_hashes(interior, max_regions).unwrap())
501 .collect_vec();
502
503 (exterior_hashes.unwrap(), interiors_hashes)
504}
505
506pub fn polygon_hashes(polygon: &GeoPolygon, max_regions: usize) -> OperationResult<Vec<GeoHash>> {
509 if max_regions == 0 {
510 return Err(OperationError::service_error(
511 "max_regions cannot be equal to zero",
512 ));
513 }
514 let polygon_wrapper = polygon.convert().polygon;
515 let geo_bounding_box = minimum_bounding_rectangle_for_boundary(polygon_wrapper.exterior());
516 let full_geohash_bounding_box: GeohashBoundingBox = geo_bounding_box.into();
517
518 let mapping_fn = |precision| {
519 full_geohash_bounding_box
520 .geohash_regions(precision, max_regions)
521 .map(|hashes| {
522 hashes
523 .into_iter()
524 .filter(|hash| {
525 check_polygon_intersection(
526 EcoString::from(*hash).as_str(),
527 &polygon_wrapper,
528 )
529 })
530 .collect_vec()
531 })
532 };
533 create_hashes(mapping_fn)
534}
535
536const EARTH_RADIUS_METERS: f64 = 6371.0 * 1000.;
539
540fn minimum_bounding_rectangle_for_circle(circle: &GeoRadius) -> GeoBoundingBox {
543 let angular_radius: f64 = circle.radius.0 / EARTH_RADIUS_METERS;
545
546 let angular_lat = circle.center.lat.to_radians();
547 let mut min_lat = (angular_lat - angular_radius).to_degrees();
548 let mut max_lat = (angular_lat + angular_radius).to_degrees();
549
550 let (min_lon, max_lon) = if LAT_RANGE.start < min_lat && max_lat < LAT_RANGE.end {
551 let angular_lon = circle.center.lon.to_radians();
553 let delta_lon = (angular_radius.sin() / angular_lat.cos()).asin();
554
555 let min_lon = (angular_lon - delta_lon).to_degrees();
556 let max_lon = (angular_lon + delta_lon).to_degrees();
557
558 (min_lon, max_lon)
559 } else {
560 if LAT_RANGE.start > min_lat {
562 min_lat = LAT_RANGE.start + COORD_EPS;
563 }
564 if max_lat > LAT_RANGE.end {
565 max_lat = LAT_RANGE.end - COORD_EPS;
566 }
567
568 (LON_RANGE.start + COORD_EPS, LON_RANGE.end - COORD_EPS)
569 };
570
571 let top_left = GeoPoint {
572 lat: OrderedFloat(max_lat),
573 lon: OrderedFloat(sphere_lon(min_lon)),
574 };
575 let bottom_right = GeoPoint {
576 lat: OrderedFloat(min_lat),
577 lon: OrderedFloat(sphere_lon(max_lon)),
578 };
579
580 GeoBoundingBox {
581 top_left,
582 bottom_right,
583 }
584}
585
586fn minimum_bounding_rectangle_for_boundary(boundary: &LineString) -> GeoBoundingBox {
587 let mut min_lon = f64::MAX;
588 let mut max_lon = f64::MIN;
589 let mut min_lat = f64::MAX;
590 let mut max_lat = f64::MIN;
591
592 for point in boundary.coords() {
593 if point.x < min_lon {
594 min_lon = point.x;
595 }
596 if point.x > max_lon {
597 max_lon = point.x;
598 }
599 if point.y < min_lat {
600 min_lat = point.y;
601 }
602 if point.y > max_lat {
603 max_lat = point.y;
604 }
605 }
606
607 let top_left = GeoPoint {
608 lon: OrderedFloat(min_lon),
609 lat: OrderedFloat(max_lat),
610 };
611 let bottom_right = GeoPoint {
612 lon: OrderedFloat(max_lon),
613 lat: OrderedFloat(min_lat),
614 };
615
616 GeoBoundingBox {
617 top_left,
618 bottom_right,
619 }
620}
621
622#[cfg(test)]
623mod tests {
624 use rand::rngs::StdRng;
625 use rand::{RngExt, SeedableRng};
626
627 use super::*;
628 use crate::segment::types::CheckGeoPoint;
629 use crate::segment::types::test_utils::{build_polygon, build_polygon_with_interiors};
630
631 const BERLIN: GeoPoint = GeoPoint {
632 lat: OrderedFloat(52.52437),
633 lon: OrderedFloat(13.41053),
634 };
635
636 const NYC: GeoPoint = GeoPoint {
637 lat: OrderedFloat(40.75798),
638 lon: OrderedFloat(-73.991516),
639 };
640
641 #[test]
642 fn geohash_ordering() {
643 let mut v: Vec<&[u8]> = vec![
644 b"dr5ru",
645 b"uft56",
646 b"hhbcd",
647 b"uft560000000",
648 b"h",
649 b"hbcd",
650 b"887hh1234567",
651 b"",
652 b"hwx98",
653 b"hbc",
654 b"dr5rukz",
655 ];
656 let mut hashes = v.iter().map(|s| GeoHash::new(s).unwrap()).collect_vec();
657 hashes.sort_unstable();
658 v.sort_unstable();
659 for (a, b) in hashes.iter().zip(v) {
660 assert_eq!(a.to_string().as_bytes(), b);
661 }
662
663 assert_eq!(
666 GeoHash::new(b"uft5600")
667 .unwrap()
668 .cmp(&GeoHash::new(b"uft560000000").unwrap()),
669 "uft5600".cmp("uft560000000"),
670 );
671 assert_eq!(
672 GeoHash::new(b"")
673 .unwrap()
674 .cmp(&GeoHash::new(b"000000000000").unwrap()),
675 "".cmp("000000000000"),
676 );
677 }
678
679 #[test]
680 fn geohash_starts_with() {
681 let samples: [&[u8]; 6] = [
682 b"",
683 b"uft5601",
684 b"uft560100000",
685 b"uft56010000r",
686 b"uft5602",
687 b"uft560200000",
688 ];
689 for a in &samples {
690 let a_hash = GeoHash::new(a).unwrap();
691 for b in &samples {
692 let b_hash = GeoHash::new(b).unwrap();
693 if a.starts_with(b) {
694 assert!(
695 a_hash.starts_with(b_hash),
696 "{a:?} expected to start with {b:?}",
697 );
698 } else {
699 assert!(
700 !a_hash.starts_with(b_hash),
701 "{a:?} expected to not start with {b:?}",
702 );
703 }
704 }
705 }
706 }
707
708 #[test]
709 #[expect(clippy::unusual_byte_groupings)]
710 fn geohash_normalize() {
711 let valid_samples: [&[u8]; _] = [
712 b"dr5ru",
713 b"uft56",
714 b"hhbcd",
715 b"uft560000000",
716 b"h",
717 b"hbcd",
718 b"887hh1234567",
719 b"",
720 b"hwx98",
721 b"hbc",
722 b"dr5rukz",
723 ];
724 for s in valid_samples {
726 let hash = GeoHash::new(s).unwrap();
727 assert_eq!(hash, GeoHashRaw::from(hash).normalize());
728 }
729
730 let raw = 0b_00001_00010_00011_00100_00101_00110_00111_01000_01001_01010_01100_01101__0011;
731 let fxd = 0b_00001_00010_00011_00000_00000_00000_00000_00000_00000_00000_00000_00000__0011;
732 assert_eq!(GeoHashRaw(raw).normalize().0, fxd);
734
735 let raw = 0b_00001_00010_00011_00100_00101_00110_00111_01000_01001_01010_01100_01101__1111;
736 let fxd = 0b_00001_00010_00011_00100_00101_00110_00111_01000_01001_01010_01100_01101__1100;
737 assert_eq!(GeoHashRaw(raw).normalize().0, fxd);
739 }
740
741 #[test]
742 fn geohash_encode_longitude_first() {
743 let center_hash = GeoHash::new(encode(Coord::from(NYC), GeoHash::MAX_LEN).unwrap());
744 assert_eq!(center_hash.ok(), GeoHash::new(b"dr5ru7c02wnv").ok());
745 let center_hash = GeoHash::new(encode(Coord::from(NYC), 6).unwrap());
746 assert_eq!(center_hash.ok(), GeoHash::new(b"dr5ru7").ok());
747 let center_hash = GeoHash::new(encode(Coord::from(BERLIN), GeoHash::MAX_LEN).unwrap());
748 assert_eq!(center_hash.ok(), GeoHash::new(b"u33dc1v0xupz").ok());
749 let center_hash = GeoHash::new(encode(Coord::from(BERLIN), 6).unwrap());
750 assert_eq!(center_hash.ok(), GeoHash::new(b"u33dc1").ok());
751 }
752
753 #[test]
754 fn rectangle_geo_hash_nyc() {
755 let near_nyc_circle = GeoRadius {
757 center: NYC,
758 radius: OrderedFloat(800.0),
759 };
760
761 let bounding_box = minimum_bounding_rectangle_for_circle(&near_nyc_circle);
762 let rectangle: GeohashBoundingBox = bounding_box.into();
763 assert_eq!(rectangle.north_west, GeoHash::new(b"dr5ruj4477kd").unwrap());
764 assert_eq!(rectangle.south_west, GeoHash::new(b"dr5ru46ne2ux").unwrap());
765 assert_eq!(rectangle.south_east, GeoHash::new(b"dr5ru6ryw0cp").unwrap());
766 assert_eq!(rectangle.north_east, GeoHash::new(b"dr5rumpfq534").unwrap());
767 }
768
769 #[test]
770 fn top_level_rectangle_geo_area() {
771 let rect = GeohashBoundingBox {
772 north_west: GeoHash::new(b"u").unwrap(),
773 south_west: GeoHash::new(b"s").unwrap(),
774 south_east: GeoHash::new(b"t").unwrap(),
775 north_east: GeoHash::new(b"v").unwrap(),
776 };
777 let mut geo_area = rect.geohash_regions(1, 100).unwrap();
778 let mut expected = vec![
779 GeoHash::new(b"u").unwrap(),
780 GeoHash::new(b"s").unwrap(),
781 GeoHash::new(b"v").unwrap(),
782 GeoHash::new(b"t").unwrap(),
783 ];
784
785 geo_area.sort_unstable();
786 expected.sort_unstable();
787 assert_eq!(geo_area, expected);
788 }
789
790 #[test]
791 fn nyc_rectangle_geo_area_high_precision() {
792 let rect = GeohashBoundingBox {
793 north_west: GeoHash::new(b"dr5ruj4477kd").unwrap(),
794 south_west: GeoHash::new(b"dr5ru46ne2ux").unwrap(),
795 south_east: GeoHash::new(b"dr5ru6ryw0cp").unwrap(),
796 north_east: GeoHash::new(b"dr5rumpfq534").unwrap(),
797 };
798
799 assert!(rect.geohash_regions(12, 100).is_none());
801 }
802
803 #[test]
804 fn nyc_rectangle_geo_area_medium_precision() {
805 let rect = GeohashBoundingBox {
806 north_west: GeoHash::new(b"dr5ruj4").unwrap(),
807 south_west: GeoHash::new(b"dr5ru46").unwrap(),
808 south_east: GeoHash::new(b"dr5ru6r").unwrap(),
809 north_east: GeoHash::new(b"dr5rump").unwrap(),
810 };
811
812 let geo_area = rect.geohash_regions(7, 1000).unwrap();
813 assert_eq!(14 * 12, geo_area.len());
814 }
815
816 #[test]
817 fn nyc_rectangle_geo_area_low_precision() {
818 let rect = GeohashBoundingBox {
819 north_west: GeoHash::new(b"dr5ruj").unwrap(),
820 south_west: GeoHash::new(b"dr5ru4").unwrap(),
821 south_east: GeoHash::new(b"dr5ru6").unwrap(),
822 north_east: GeoHash::new(b"dr5rum").unwrap(),
823 };
824
825 let mut geo_area = rect.geohash_regions(6, 100).unwrap();
826 let mut expected = vec![
827 GeoHash::new(b"dr5ru4").unwrap(),
828 GeoHash::new(b"dr5ru5").unwrap(),
829 GeoHash::new(b"dr5ru6").unwrap(),
830 GeoHash::new(b"dr5ru7").unwrap(),
831 GeoHash::new(b"dr5ruh").unwrap(),
832 GeoHash::new(b"dr5ruj").unwrap(),
833 GeoHash::new(b"dr5rum").unwrap(),
834 GeoHash::new(b"dr5ruk").unwrap(),
835 ];
836
837 expected.sort_unstable();
838 geo_area.sort_unstable();
839 assert_eq!(geo_area, expected);
840 }
841
842 #[test]
843 fn rectangle_hashes_nyc() {
844 let top_left = GeoPoint {
847 lon: OrderedFloat(-74.00101399),
848 lat: OrderedFloat(40.76517460),
849 };
850
851 let bottom_right = GeoPoint {
853 lon: OrderedFloat(-73.98201792),
854 lat: OrderedFloat(40.75078539),
855 };
856
857 let near_nyc_rectangle = GeoBoundingBox {
858 top_left,
859 bottom_right,
860 };
861
862 let nyc_hashes_result = rectangle_hashes(&near_nyc_rectangle, 200);
863 let nyc_hashes = nyc_hashes_result.unwrap();
864 assert_eq!(nyc_hashes.len(), 168);
865 assert!(nyc_hashes.iter().all(|h| h.len() == 7)); let mut nyc_hashes_result = rectangle_hashes(&near_nyc_rectangle, 10);
868 nyc_hashes_result.as_mut().unwrap().sort_unstable();
869 let mut expected = vec![
870 GeoHash::new(b"dr5ruj").unwrap(),
871 GeoHash::new(b"dr5ruh").unwrap(),
872 GeoHash::new(b"dr5ru5").unwrap(),
873 GeoHash::new(b"dr5ru4").unwrap(),
874 GeoHash::new(b"dr5rum").unwrap(),
875 GeoHash::new(b"dr5ruk").unwrap(),
876 GeoHash::new(b"dr5ru7").unwrap(),
877 GeoHash::new(b"dr5ru6").unwrap(),
878 ];
879 expected.sort_unstable();
880
881 assert_eq!(nyc_hashes_result.unwrap(), expected);
882
883 let nyc_hashes_result = rectangle_hashes(&near_nyc_rectangle, 7);
901 assert_eq!(
902 nyc_hashes_result.unwrap(),
903 [GeoHash::new(b"dr5ru").unwrap()],
904 );
905 }
906
907 #[test]
908 fn rectangle_hashes_crossing_antimeridian() {
909 let top_left = GeoPoint {
912 lat: OrderedFloat(74.071028),
913 lon: OrderedFloat(167.0),
914 };
915
916 let bottom_right = GeoPoint {
918 lat: OrderedFloat(40.75798),
919 lon: OrderedFloat(-73.991516),
920 };
921
922 let crossing_usa_rectangle = GeoBoundingBox {
923 top_left,
924 bottom_right,
925 };
926
927 let usa_hashes_result = rectangle_hashes(&crossing_usa_rectangle, 200);
928 let usa_hashes = usa_hashes_result.unwrap();
929 assert_eq!(usa_hashes.len(), 84);
930 assert!(usa_hashes.iter().all(|h| h.len() == 2)); let mut usa_hashes_result = rectangle_hashes(&crossing_usa_rectangle, 10);
933 usa_hashes_result.as_mut().unwrap().sort_unstable();
934 let mut expected = vec![
935 GeoHash::new(b"8").unwrap(),
936 GeoHash::new(b"9").unwrap(),
937 GeoHash::new(b"b").unwrap(),
938 GeoHash::new(b"c").unwrap(),
939 GeoHash::new(b"d").unwrap(),
940 GeoHash::new(b"f").unwrap(),
941 GeoHash::new(b"x").unwrap(),
942 GeoHash::new(b"z").unwrap(),
943 ];
944 expected.sort_unstable();
945
946 assert_eq!(usa_hashes_result.unwrap(), expected);
947
948 }
960
961 #[test]
962 fn polygon_hashes_nyc() {
963 let near_nyc_polygon = build_polygon(vec![
966 (-74.00101399, 40.76517460),
967 (-73.98201792, 40.76517460),
968 (-73.98201792, 40.75078539),
969 (-74.00101399, 40.75078539),
970 (-74.00101399, 40.76517460),
971 ]);
972
973 let nyc_hashes_result = polygon_hashes(&near_nyc_polygon, 200);
974 let nyc_hashes = nyc_hashes_result.unwrap();
975 assert_eq!(nyc_hashes.len(), 168);
976 assert!(nyc_hashes.iter().all(|h| h.len() == 7)); let mut nyc_hashes_result = polygon_hashes(&near_nyc_polygon, 10);
979 nyc_hashes_result.as_mut().unwrap().sort_unstable();
980 let mut expected = vec![
981 GeoHash::new(b"dr5ruj").unwrap(),
982 GeoHash::new(b"dr5ruh").unwrap(),
983 GeoHash::new(b"dr5ru5").unwrap(),
984 GeoHash::new(b"dr5ru4").unwrap(),
985 GeoHash::new(b"dr5rum").unwrap(),
986 GeoHash::new(b"dr5ruk").unwrap(),
987 GeoHash::new(b"dr5ru7").unwrap(),
988 GeoHash::new(b"dr5ru6").unwrap(),
989 ];
990 expected.sort_unstable();
991
992 assert_eq!(nyc_hashes_result.unwrap(), expected);
993
994 let nyc_hashes_result = polygon_hashes(&near_nyc_polygon, 7);
996 assert_eq!(
997 nyc_hashes_result.unwrap(),
998 [GeoHash::new(b"dr5ru").unwrap()],
999 );
1000 }
1001
1002 #[test]
1003 fn random_circles() {
1004 let mut rnd = StdRng::seed_from_u64(42);
1005 for _ in 0..1000 {
1006 let r_meters = rnd.random_range(1.0..10000.0);
1007 let query = GeoRadius {
1008 center: GeoPoint::new_unchecked(
1009 rnd.random_range(LON_RANGE),
1010 rnd.random_range(LAT_RANGE),
1011 ),
1012 radius: OrderedFloat(r_meters),
1013 };
1014 let max_hashes = rnd.random_range(1..32);
1015 let hashes = circle_hashes(&query, max_hashes);
1016 assert!(hashes.unwrap().len() <= max_hashes);
1017 }
1018 }
1019
1020 #[test]
1021 fn test_check_polygon_intersection() {
1022 fn check_intersection(geohash: &str, polygon: &GeoPolygon, expected: bool) {
1023 let intersect = check_polygon_intersection(geohash, &polygon.convert().polygon);
1024 assert_eq!(intersect, expected);
1025 }
1026
1027 let geohash = encode(Coord { x: -50.0, y: 35.0 }, 2).unwrap();
1029
1030 check_intersection(
1032 &geohash,
1033 &build_polygon(vec![
1034 (-60.0, 37.0),
1035 (-60.0, 45.0),
1036 (-50.0, 45.0),
1037 (-50.0, 37.0),
1038 (-60.0, 37.0),
1039 ]),
1040 true,
1041 );
1042
1043 check_intersection(
1045 &geohash,
1046 &build_polygon(vec![
1047 (-70.2, 50.8),
1048 (-70.2, 55.9),
1049 (-65.6, 55.9),
1050 (-65.6, 50.8),
1051 (-70.2, 50.8),
1052 ]),
1053 false,
1054 );
1055
1056 check_intersection(
1058 &geohash,
1059 &build_polygon(vec![
1060 (-56.2, 33.75),
1061 (-56.2, 39.375),
1062 (-45.0, 39.375),
1063 (-45.0, 33.75),
1064 (-56.2, 33.75),
1065 ]),
1066 true,
1067 );
1068
1069 check_intersection(
1071 &geohash,
1072 &build_polygon(vec![
1073 (-45.0, 39.375),
1074 (-45.0, 45.0),
1075 (-30.9, 45.0),
1076 (-30.9, 39.375),
1077 (-45.0, 39.375),
1078 ]),
1079 true,
1080 );
1081
1082 check_intersection(
1084 &geohash,
1085 &build_polygon(vec![
1086 (-55.7, 34.3),
1087 (-55.7, 38.0),
1088 (-46.8, 38.0),
1089 (-46.8, 34.3),
1090 (-55.7, 34.3),
1091 ]),
1092 true,
1093 );
1094
1095 check_intersection(
1097 &geohash,
1098 &build_polygon(vec![
1099 (-60.0, 33.0),
1100 (-60.0, 40.0),
1101 (-44.0, 40.0),
1102 (-44.0, 33.0),
1103 (-60.0, 33.0),
1104 ]),
1105 true,
1106 );
1107
1108 check_intersection(
1110 &geohash,
1111 &build_polygon_with_interiors(
1112 vec![
1113 (-70.0, 13.0),
1114 (-70.0, 50.0),
1115 (-34.0, 50.0),
1116 (-34.0, 13.0),
1117 (-70.0, 13.0),
1118 ],
1119 vec![vec![
1120 (-60.0, 33.0),
1121 (-60.0, 40.0),
1122 (-44.0, 40.0),
1123 (-44.0, 33.0),
1124 (-60.0, 33.0),
1125 ]],
1126 ),
1127 false,
1128 );
1129 }
1130
1131 #[test]
1132 fn test_lon_threshold() {
1133 let query = GeoRadius {
1134 center: GeoPoint {
1135 lon: OrderedFloat(179.987181),
1136 lat: OrderedFloat(44.9811609411936),
1137 },
1138 radius: OrderedFloat(100000.),
1139 };
1140
1141 let max_hashes = 10;
1142 let hashes = circle_hashes(&query, max_hashes);
1143 assert_eq!(
1144 hashes.unwrap(),
1145 vec![
1146 GeoHash::new(b"zbp").unwrap(),
1147 GeoHash::new(b"b00").unwrap(),
1148 GeoHash::new(b"xzz").unwrap(),
1149 GeoHash::new(b"8pb").unwrap(),
1150 ],
1151 );
1152 }
1153
1154 #[test]
1155 fn wide_circle_meridian() {
1156 let query = GeoRadius {
1157 center: GeoPoint {
1158 lon: OrderedFloat(-17.81718188959701),
1159 lat: OrderedFloat(89.9811609411936),
1160 },
1161 radius: OrderedFloat(9199.481636468849),
1162 };
1163
1164 let max_hashes = 10;
1165 let hashes = circle_hashes(&query, max_hashes);
1166 let vec = hashes.unwrap();
1167 assert!(vec.len() <= max_hashes);
1168 assert_eq!(
1169 vec,
1170 [
1171 GeoHash::new(b"b").unwrap(),
1172 GeoHash::new(b"c").unwrap(),
1173 GeoHash::new(b"f").unwrap(),
1174 GeoHash::new(b"g").unwrap(),
1175 GeoHash::new(b"u").unwrap(),
1176 GeoHash::new(b"v").unwrap(),
1177 GeoHash::new(b"y").unwrap(),
1178 GeoHash::new(b"z").unwrap(),
1179 ],
1180 );
1181 }
1182
1183 #[test]
1184 fn tight_circle_meridian() {
1185 let query = GeoRadius {
1186 center: GeoPoint {
1187 lon: OrderedFloat(-17.81718188959701),
1188 lat: OrderedFloat(89.9811609411936),
1189 },
1190 radius: OrderedFloat(1000.0),
1191 };
1192
1193 let max_hashes = 10;
1194 let hashes_result = circle_hashes(&query, max_hashes);
1195 let hashes = hashes_result.unwrap();
1196 assert!(hashes.len() <= max_hashes);
1197 assert_eq!(
1198 hashes,
1199 [
1200 GeoHash::new(b"fz").unwrap(),
1201 GeoHash::new(b"gp").unwrap(),
1202 GeoHash::new(b"gr").unwrap(),
1203 GeoHash::new(b"gx").unwrap(),
1204 GeoHash::new(b"gz").unwrap(),
1205 GeoHash::new(b"up").unwrap(),
1206 ],
1207 );
1208 }
1209
1210 #[test]
1211 fn wide_circle_south_pole() {
1212 let query = GeoRadius {
1213 center: GeoPoint {
1214 lon: OrderedFloat(155.85591760141335),
1215 lat: OrderedFloat(-74.19418872656166),
1216 },
1217 radius: OrderedFloat(7133.775526733084),
1218 };
1219 let max_hashes = 10;
1220 let hashes_result = circle_hashes(&query, max_hashes);
1221 let hashes = hashes_result.unwrap();
1222 assert!(hashes.len() <= max_hashes);
1223 assert_eq!(
1224 hashes,
1225 [
1226 GeoHash::new(b"p6yd").unwrap(),
1227 GeoHash::new(b"p6yf").unwrap(),
1228 GeoHash::new(b"p6y9").unwrap(),
1229 GeoHash::new(b"p6yc").unwrap(),
1230 ],
1231 );
1232 }
1233
1234 #[test]
1235 fn tight_circle_south_pole() {
1236 let query = GeoRadius {
1237 center: GeoPoint {
1238 lon: OrderedFloat(155.85591760141335),
1239 lat: OrderedFloat(-74.19418872656166),
1240 },
1241 radius: OrderedFloat(1000.0),
1242 };
1243 let max_hashes = 10;
1244 let hashes_result = circle_hashes(&query, max_hashes);
1245 let hashes = hashes_result.unwrap();
1246 assert!(hashes.len() <= max_hashes);
1247 assert_eq!(
1248 hashes,
1249 [
1250 GeoHash::new(b"p6ycc").unwrap(),
1251 GeoHash::new(b"p6ycf").unwrap(),
1252 GeoHash::new(b"p6ycg").unwrap(),
1253 ],
1254 );
1255 }
1256
1257 #[test]
1258 fn circle_hashes_nyc() {
1259 let near_nyc_circle = GeoRadius {
1260 center: NYC,
1261 radius: OrderedFloat(800.0),
1262 };
1263
1264 let nyc_hashes_result = circle_hashes(&near_nyc_circle, 200).unwrap();
1265 assert!(nyc_hashes_result.iter().all(|h| h.len() == 7)); let mut nyc_hashes_result = circle_hashes(&near_nyc_circle, 10);
1268 nyc_hashes_result.as_mut().unwrap().sort_unstable();
1269 let mut expected = [
1270 GeoHash::new(b"dr5ruj").unwrap(),
1271 GeoHash::new(b"dr5ruh").unwrap(),
1272 GeoHash::new(b"dr5ru5").unwrap(),
1273 GeoHash::new(b"dr5ru4").unwrap(),
1274 GeoHash::new(b"dr5rum").unwrap(),
1275 GeoHash::new(b"dr5ruk").unwrap(),
1276 GeoHash::new(b"dr5ru7").unwrap(),
1277 GeoHash::new(b"dr5ru6").unwrap(),
1278 ];
1279 expected.sort_unstable();
1280 assert_eq!(nyc_hashes_result.unwrap(), expected);
1281
1282 let nyc_hashes_result = circle_hashes(&near_nyc_circle, 7);
1284 assert_eq!(
1285 nyc_hashes_result.unwrap(),
1286 [GeoHash::new(b"dr5ru").unwrap()],
1287 );
1288 }
1289
1290 #[test]
1291 fn go_north() {
1292 let mut geohash = sphere_neighbor(GeoHash::new(b"ww8p").unwrap(), Direction::N).unwrap();
1293 for _ in 0..1000 {
1294 geohash = sphere_neighbor(geohash, Direction::N).unwrap();
1295 }
1296 }
1297
1298 #[test]
1299 fn go_west() {
1300 let starting_hash = GeoHash::new(b"ww8").unwrap();
1301 let mut geohash = sphere_neighbor(starting_hash, Direction::W).unwrap();
1302 let mut is_earth_round = false;
1303 for _ in 0..1000 {
1304 geohash = sphere_neighbor(geohash, Direction::W).unwrap();
1305 if geohash == starting_hash {
1306 is_earth_round = true;
1307 }
1308 }
1309 assert!(is_earth_round)
1310 }
1311
1312 #[test]
1313 fn sphere_neighbor_corner_cases() {
1314 assert_eq!(
1315 &EcoString::from(sphere_neighbor(GeoHash::new(b"z").unwrap(), Direction::NE).unwrap()),
1316 "b",
1317 );
1318 assert_eq!(
1319 &EcoString::from(sphere_neighbor(GeoHash::new(b"zz").unwrap(), Direction::NE).unwrap()),
1320 "bp",
1321 );
1322 assert_eq!(
1323 &EcoString::from(sphere_neighbor(GeoHash::new(b"0").unwrap(), Direction::SW).unwrap()),
1324 "p",
1325 );
1326 assert_eq!(
1327 &EcoString::from(sphere_neighbor(GeoHash::new(b"00").unwrap(), Direction::SW).unwrap()),
1328 "pb",
1329 );
1330
1331 assert_eq!(
1332 &EcoString::from(sphere_neighbor(GeoHash::new(b"8").unwrap(), Direction::W).unwrap()),
1333 "x",
1334 );
1335 assert_eq!(
1336 &EcoString::from(sphere_neighbor(GeoHash::new(b"8h").unwrap(), Direction::W).unwrap()),
1337 "xu",
1338 );
1339 assert_eq!(
1340 &EcoString::from(sphere_neighbor(GeoHash::new(b"r").unwrap(), Direction::E).unwrap()),
1341 "2",
1342 );
1343 assert_eq!(
1344 &EcoString::from(sphere_neighbor(GeoHash::new(b"ru").unwrap(), Direction::E).unwrap()),
1345 "2h",
1346 );
1347
1348 assert_eq!(
1349 EcoString::from(
1350 sphere_neighbor(GeoHash::new(b"ww8p1r4t8").unwrap(), Direction::SE).unwrap()
1351 ),
1352 EcoString::from(&geohash::neighbor("ww8p1r4t8", Direction::SE).unwrap()),
1353 );
1354 }
1355
1356 #[test]
1357 fn long_overflow_distance() {
1358 let dist = Haversine.distance(Point::new(-179.999, 66.0), Point::new(179.999, 66.0));
1359 eprintln!("dist` = {dist:#?}");
1360 assert_eq!(dist, 90.45422731917998);
1361 let dist = Haversine.distance(Point::new(0.99, 90.), Point::new(0.99, -90.0));
1362 assert_eq!(dist, 20015114.442035925);
1363 }
1364
1365 #[test]
1366 fn turn_geo_hash_to_box() {
1367 let geo_box = geo_hash_to_box(GeoHash::new(b"dr5ruj4477kd").unwrap());
1368 let center = GeoPoint {
1369 lat: OrderedFloat(40.76517460),
1370 lon: OrderedFloat(-74.00101399),
1371 };
1372 assert!(geo_box.check_point(¢er));
1373 }
1374
1375 #[test]
1376 fn common_prefix() {
1377 let geo_hashes = vec![
1378 GeoHash::new(b"zbcd123").unwrap(),
1379 GeoHash::new(b"zbcd2233").unwrap(),
1380 GeoHash::new(b"zbcd3213").unwrap(),
1381 GeoHash::new(b"zbcd533").unwrap(),
1382 ];
1383
1384 let common_prefix = common_hash_prefix(&geo_hashes).unwrap();
1385 println!("common_prefix = {:?}", EcoString::from(common_prefix));
1386
1387 let geo_hashes = vec![
1390 GeoHash::new(b"zbcd123").unwrap(),
1391 GeoHash::new(b"bbcd2233").unwrap(),
1392 GeoHash::new(b"cbcd3213").unwrap(),
1393 GeoHash::new(b"dbcd533").unwrap(),
1394 ];
1395
1396 let common_prefix = common_hash_prefix(&geo_hashes).unwrap();
1397 println!("common_prefix = {:?}", EcoString::from(common_prefix));
1398
1399 assert_eq!(common_prefix, GeoHash::new(b"").unwrap());
1400 }
1401
1402 #[test]
1403 fn max_regions_cannot_be_equal_to_zero() {
1404 let invalid_max_hashes = 0;
1405
1406 let sample_circle = GeoRadius {
1408 center: GeoPoint {
1409 lon: OrderedFloat(179.987181),
1410 lat: OrderedFloat(44.9811609411936),
1411 },
1412 radius: OrderedFloat(100000.),
1413 };
1414 let circle_hashes = circle_hashes(&sample_circle, invalid_max_hashes);
1415 assert!(circle_hashes.is_err());
1416
1417 let top_left = GeoPoint {
1419 lon: OrderedFloat(-74.00101399),
1420 lat: OrderedFloat(40.76517460),
1421 };
1422
1423 let bottom_right = GeoPoint {
1424 lon: OrderedFloat(-73.98201792),
1425 lat: OrderedFloat(40.75078539),
1426 };
1427
1428 let sample_rectangle = GeoBoundingBox {
1429 top_left,
1430 bottom_right,
1431 };
1432 let rectangle_hashes = rectangle_hashes(&sample_rectangle, invalid_max_hashes);
1433 assert!(rectangle_hashes.is_err());
1434
1435 let sample_polygon = build_polygon(vec![
1437 (-74.00101399, 40.76517460),
1438 (-73.98201792, 40.75078539),
1439 ]);
1440
1441 let polygon_hashes = polygon_hashes(&sample_polygon, invalid_max_hashes);
1442 assert!(polygon_hashes.is_err());
1443 }
1444
1445 #[test]
1446 fn geo_radius_zero_division() {
1447 let circle = GeoRadius {
1448 center: GeoPoint {
1449 lon: OrderedFloat(45.0),
1450 lat: OrderedFloat(80.0),
1451 },
1452 radius: OrderedFloat(1000.0),
1453 };
1454 let hashes = circle_hashes(&circle, GeoHash::MAX_LEN);
1455 assert!(hashes.is_ok());
1456
1457 let circle2 = GeoRadius {
1458 center: GeoPoint {
1459 lon: OrderedFloat(45.0),
1460 lat: OrderedFloat(90.0),
1461 },
1462 radius: OrderedFloat(-1.0),
1463 };
1464 let hashes2 = circle_hashes(&circle2, GeoHash::MAX_LEN);
1465 assert!(hashes2.is_err());
1466 }
1467}