Skip to main content

geo_hash/
lib.rs

1//!Geohash library
2//!
3//!In simple terms geohash converts point into hash that can uniquely identify cell on the map of the world
4//!Hash length will determines size of the cell with following table illustrating approximate sizes
5//!
6//! | Hash Len | Width    | Height  |
7//! |----------|:-------: |--------:|
8//! | 1        | <=5km    | 5km     |
9//! | 2        | <=1.250km| 625km   |
10//! | 3        | <=156km  | 156km   |
11//! | 4        | <=39.1km | 19.5km  |
12//! | 5        | <=4.89km | 4.89km  |
13//! | 6        | <=1.22km | 0.61km  |
14//! | 7        | <=153m   | 153m    |
15//! | 8        | <=38.2m  | 19.1m   |
16//! | 9        | <=4.77m  | 4.77m   |
17//! | 10       | <=1.19m  | 0.596m  |
18//! | 11       | <=149mm  | 149mm   |
19//! | 12       | <=37.2mm | 18.6mm  |
20//!
21//! Note, width becomes smaller depending on how far coordinate is from equator
22//!
23//! The important property of the resulting hash is that the closer coordinates are, the bigger common prefix is between two hashes.
24//!
25//! ## Features
26//!
27//! `serde` - Implements serde interface on `GeoHash`
28//!
29//! ## Usage
30//!
31//! ### Encode with static hash size
32//!
33//! You can use [Codec] to ensure encoding will never fail at compile time by supplying it with
34//! length of the hash you desire
35//!
36//! ```rust
37//! use geo_hash::Coordinate;
38//! type CODEC = geo_hash::Codec::<9>;
39//!
40//! let position = Coordinate::try_new(43.2203, 142.8635).expect("valid GPS coordinates");
41//! let hash = CODEC::encode(position);
42//! assert_eq!(hash, "xpttfun02");
43//! ```
44//!
45//!### Encode with dynamic hash size
46//!
47//! You can use [GeoHash::encode] when you cannot know size of the hash at compile time, which will fail if hash length is not within range of `1..=12`
48//!
49//! ```rust
50//! use geo_hash::{GeoHash, Coordinate};
51//!
52//! let position = Coordinate::try_new(43.2203, 142.8635).expect("valid GPS coordinates");
53//! let hash = GeoHash::encode(position, 9).expect("to encode");
54//! assert_eq!(hash, "xpttfun02");
55//! ```
56//!### Decode hash to determine approximate position
57//!
58//! When you only have textual hash, you can determine its [bounding box](Bbox) or as approximation [position](GeoHashPosition) within this bounding box
59//!
60//! ```rust
61//! use geo_hash::{GeoHash, Coordinate};
62//!
63//! const COORD: Coordinate =  Coordinate::new(43.22027921676636, 142.86348581314087);
64//!
65//! //This function only checks length
66//! let hash = GeoHash::try_from_str("xpttfun02").expect("valid geohash");
67//! assert_eq!(hash, "xpttfun02");
68//! //This function will check validity of hash itself
69//! let bbox = hash.decode_bbox().expect("we should have valid hash here");
70//! assert_eq!(bbox.min(), Coordinate::new(43.22025775909424, 142.86346435546875));
71//! assert_eq!(bbox.max(), Coordinate::new(43.22030067443848, 142.863507270813));
72//! let position = bbox.position();
73//! assert_eq!(position.coordinates(), COORD);
74//! ```
75
76
77#![no_std]
78#![warn(missing_docs)]
79#![allow(clippy::style)]
80
81use core::fmt;
82
83const MAX_LAT: f64 = 90.0;
84const MIN_LAT: f64 = -90.0;
85const MAX_LON: f64 = 180.0;
86const MIN_LON: f64 = -180.0;
87
88#[cfg(feature = "serde")]
89mod serde;
90mod codec;
91mod math;
92pub use codec::{Codec, GeoHash, DecodeError};
93
94#[derive(Debug, Clone, Copy, PartialEq)]
95///Possible errors when creating [Coordinate]
96pub enum CoordinateError {
97    ///Indicates invalid latitude value outside of allowed bounds
98    InvalidLatitude(f64),
99    ///Indicates invalid longitude value outside of allowed bounds
100    InvalidLongitude(f64),
101}
102
103impl fmt::Display for CoordinateError {
104    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
105        match self {
106            Self::InvalidLatitude(value) => fmt.write_fmt(format_args!("Invalid latitude='{value}'. Allowed values are {MIN_LAT}..={MAX_LAT}")),
107            Self::InvalidLongitude(value) => fmt.write_fmt(format_args!("Invalid longitude='{value}'. Allowed values are {MIN_LON}..={MAX_LON}")),
108        }
109    }
110}
111
112#[derive(Debug, Copy, Clone, PartialEq)]
113///Coordinate
114pub struct Coordinate {
115    latitude: f64,
116    longitude: f64,
117}
118
119impl Coordinate {
120    #[inline(always)]
121    ///Creates new instance with panic in case of error
122    pub const fn new(latitude: f64, longitude: f64) -> Self {
123        match Self::try_new(latitude, longitude) {
124            Ok(result) => result,
125            Err(_) => panic!("Invalid coordinates"),
126        }
127    }
128
129    #[inline(always)]
130    ///Creates new instance verifying coordinates are valid
131    pub const fn try_new(latitude: f64, longitude: f64) -> Result<Self, CoordinateError> {
132        if latitude.is_nan() || latitude < MIN_LAT || latitude > MAX_LAT {
133            Err(CoordinateError::InvalidLatitude(latitude))
134        } else if longitude.is_nan() || longitude < MIN_LON || longitude > MAX_LON {
135            Err(CoordinateError::InvalidLongitude(latitude))
136        } else {
137            Ok(Self {
138                latitude,
139                longitude
140            })
141        }
142    }
143
144    #[inline(always)]
145    ///Returns latitude
146    pub const fn latitude(&self) -> f64 {
147        self.latitude
148    }
149
150    #[inline(always)]
151    ///Returns longitude
152    pub const fn longitude(&self) -> f64 {
153        self.longitude
154    }
155}
156
157//Convenience impls to handle automatic deref on references
158impl PartialEq<Coordinate> for &'_ Coordinate {
159    #[inline(always)]
160    fn eq(&self, other: &Coordinate) -> bool {
161        PartialEq::eq(*self, other)
162    }
163}
164
165impl PartialEq<&Coordinate> for Coordinate {
166    #[inline(always)]
167    fn eq(&self, other: &&Coordinate) -> bool {
168        PartialEq::eq(self, *other)
169    }
170}
171
172#[derive(Copy, Clone, Debug, PartialEq)]
173///Geohash position with margins of error
174pub struct GeoHashPosition {
175    ///Position itself
176    pub coord: Coordinate,
177    ///Latitude error
178    pub lat_err: f64,
179    ///Longitude error
180    pub lon_err: f64,
181}
182
183impl GeoHashPosition {
184    #[inline(always)]
185    ///Returns self's coordinates
186    pub const fn coordinates(&self) -> Coordinate {
187        self.coord
188    }
189
190    ///Retrieves neighboring coordinates in specified `direction`
191    pub const fn neighbor(&self, direction: Direction) -> Coordinate {
192        let (direction_lat, direction_lon) = direction.to_lat_lon();
193        Coordinate {
194            longitude: math::rem_euclid((self.coord.longitude + 2f64 * self.lon_err.abs() * direction_lon) + MAX_LON, MAX_LON * 2.0) - MAX_LON,
195            latitude: math::rem_euclid((self.coord.latitude + 2f64 * self.lat_err.abs() * direction_lat) + MAX_LAT, MAX_LAT * 2.0) - MAX_LAT,
196        }
197    }
198
199    ///Retrieves neighbors for specified `directions`
200    pub const fn neighbors<const N: usize>(&self, directions: [Direction; N]) -> [Coordinate; N] {
201        let mut result = [Coordinate::new(0.0, 0.0); N];
202
203        let mut idx = 0;
204        while idx < directions.len() {
205            result[idx] = self.neighbor(directions[idx]);
206            idx += 1;
207        }
208
209        result
210    }
211}
212
213#[derive(Copy, Clone, Debug, PartialEq)]
214///Geo Bounded box describing geohash cell
215pub struct Bbox {
216    ///Min position of the box (top left)
217    pub min: Coordinate,
218    ///Max position of the box (bottom right)
219    pub max: Coordinate,
220}
221
222impl Bbox {
223    #[inline(always)]
224    ///Returns min position of the box (top left)
225    pub const fn min(&self) -> &Coordinate {
226        &self.min
227    }
228
229    #[inline(always)]
230    ///Returns max position of the box (bottom right)
231    pub const fn max(&self) -> &Coordinate {
232        &self.max
233    }
234
235    #[inline(always)]
236    ///Calculates [GeoHashPosition]
237    pub const fn position(&self) -> GeoHashPosition {
238        let min = self.min;
239        let max = self.max;
240        GeoHashPosition {
241            coord: Coordinate {
242                latitude: (min.latitude + max.latitude) / 2.0,
243                longitude: (min.longitude + max.longitude) / 2.0,
244            },
245            lat_err: (max.latitude - min.latitude) / 2.0,
246            lon_err: (max.longitude - min.longitude) / 2.0,
247        }
248    }
249}
250
251#[derive(Clone, Copy, Debug, PartialEq, Eq)]
252///Direction to select neighbor
253pub enum Direction {
254    ///North
255    N,
256    ///North East
257    NE,
258    ///East
259    E,
260    ///South East
261    SE,
262    ///South
263    S,
264    ///South West
265    SW,
266    ///West
267    W,
268    ///North West
269    NW,
270}
271
272impl Direction {
273    ///All directions in single array
274    pub const ALL: [Self; 8] = [Self::N, Self::NE, Self::E, Self::SE, Self::S, Self::SW, Self::W, Self::NW];
275
276    const fn to_lat_lon(self) -> (f64, f64) {
277        match self {
278            Direction::SW => (-1.0, -1.0),
279            Direction::S => (-1.0, 0.0),
280            Direction::SE => (-1.0, 1.0),
281            Direction::W => (0.0, -1.0),
282            Direction::E => (0.0, 1.0),
283            Direction::NW => (1.0, -1.0),
284            Direction::N => (1.0, 0.0),
285            Direction::NE => (1.0, 1.0),
286        }
287    }
288}