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}