Skip to main content

geo_hash/
codec.rs

1//Based on https://mmcloughlin.com/posts/geohash-assembly
2
3use core::{hash, fmt, ptr};
4use super::{Bbox, Coordinate, MAX_LAT, MAX_LON};
5
6const CHUNK_BITS_SIZE: usize = 5;
7const MAX_LEN: usize = 12;
8const BASE32_CHARS: [char; 32] = [
9    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
10    'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm',
11    'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
12];
13
14const fn build_reverse_table(chars: [char; 32]) -> [u8; 256] {
15    let mut table = [u8::MAX; 256];
16
17    let mut idx = 0;
18    while idx < chars.len() {
19        table[chars[idx] as usize] = idx as u8;
20        idx += 1
21    }
22
23    table
24}
25
26const REVERSE_BASE32: [u8; 256] = build_reverse_table(BASE32_CHARS);
27
28#[derive(Copy, Clone, Debug)]
29///Geohash decoding error
30pub struct DecodeError {
31    position: u8,
32    character: u8,
33}
34
35impl fmt::Display for DecodeError {
36    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
37        let Self { position, character } = self;
38        fmt.write_fmt(format_args!("Encountered invalid character byte='{character}' at position={position}"))
39    }
40}
41
42const fn encode(coord: Coordinate, len: usize) -> GeoHash {
43    //combines two u32 into single u64 with their bits interleaved
44    #[inline]
45    const fn interleave(x: u32, y: u32) -> u64 {
46        //spread takes a u32 and deposits its bits into the evenbit positions of a u64
47        #[inline]
48        const fn spread(x: u32) -> u64 {
49            let mut new_x = x as u64;
50            new_x = (new_x | (new_x << 16)) & 0x0000ffff0000ffff;
51            new_x = (new_x | (new_x << 8)) & 0x00ff00ff00ff00ff;
52            new_x = (new_x | (new_x << 4)) & 0x0f0f0f0f0f0f0f0f;
53            new_x = (new_x | (new_x << 2)) & 0x3333333333333333;
54            new_x = (new_x | (new_x << 1)) & 0x5555555555555555;
55
56            new_x
57        }
58
59        spread(x) | (spread(y) << 1)
60    }
61
62    let lat32 = (coord.latitude / (MAX_LAT * 2.0)) + 1.5;
63    let lon32 = (coord.longitude / (MAX_LON * 2.0)) + 1.5;
64
65    let lat_bits = lat32.to_bits() >> 20;
66    let lon_bits = lon32.to_bits() >> 20;
67
68    let mut idx = 0;
69    let mut buffer = [0u8; MAX_LEN];
70    let mut interleaved_bits = interleave(lat_bits as u32, lon_bits as u32);
71    while idx < len {
72        //iterate over chunks of 5 bits
73        let code = ((interleaved_bits >> 59) as u32) & 0x1f;
74        buffer[idx] = BASE32_CHARS[code as usize] as u8;
75
76        interleaved_bits <<= CHUNK_BITS_SIZE as u32;
77        idx += 1;
78    }
79
80    GeoHash {
81        buffer,
82        len: len as _
83    }
84}
85
86///Geohash codec
87///
88///Its `LEN` must be in range of `1..=12`.
89///This is validated at compile time to ensure you can only use [Codec::encode] when `LEN` is correct.
90pub struct Codec<const LEN: usize>;
91
92impl<const LEN: usize> Codec<LEN> {
93    const VALIDATE: () = {
94        if LEN < 1 {
95            panic!("Geohash len cannot less than 1")
96        } else if LEN > MAX_LEN {
97            panic!("Geohash len cannot greater than 12")
98        }
99    };
100
101    #[inline(always)]
102    ///Encodes geohash bits returning integer
103    pub const fn encode(coord: Coordinate) -> GeoHash {
104        let _ = Self::VALIDATE;
105        encode(coord, LEN)
106    }
107}
108
109#[derive(Copy, Clone)]
110///GeoHash value encoded as base32 string
111pub struct GeoHash {
112    buffer: [u8; MAX_LEN],
113    len: u8,
114}
115
116impl GeoHash {
117    ///Maximum possible length of the geohash
118    pub const MAX_LEN: usize = MAX_LEN;
119
120    #[inline(always)]
121    ///Creates `len` long geohash for specified coordinates `coord`.
122    ///
123    ///Returns `None` if `len` is not within `1..=12` range
124    ///
125    ///Prefer to use [Codec] to ensure `len` is valid at compile time when possible
126    pub const fn encode(coord: Coordinate, len: usize) -> Option<Self> {
127        if len >= 1 && len <= Self::MAX_LEN {
128            Some(encode(coord, len))
129        } else {
130            None
131        }
132    }
133
134    #[inline(always)]
135    ///Creates geohash from string, validating its length within `1..=12` with panic in case of failure
136    pub const fn from_str(text: &str) -> Self {
137        match Self::try_from_str(text) {
138            Some(result) => result,
139            None => panic!("'text' is not valid geohash")
140        }
141    }
142
143    ///Creates geohash from string, validating its length within `1..=12`
144    pub const fn try_from_str(text: &str) -> Option<Self> {
145        if text.len() >= 1 && text.len() <= 12 {
146            let mut buffer = [0u8; MAX_LEN];
147            unsafe {
148                ptr::copy_nonoverlapping(text.as_ptr(), buffer.as_mut_ptr(), text.len());
149            }
150            Some(Self {
151                buffer,
152                len: text.len() as _
153            })
154        } else {
155            None
156        }
157    }
158
159    #[inline(always)]
160    ///Returns geohash length
161    pub const fn len(&self) -> usize {
162        self.len as _
163    }
164
165    #[inline(always)]
166    ///Gets text representation of the hash
167    pub const fn as_str(&self) -> &str {
168        unsafe {
169            core::str::from_utf8_unchecked(
170                core::slice::from_raw_parts(self.buffer.as_ptr(), self.len as _)
171            )
172        }
173    }
174
175    ///Decodes geohash into its cell's coordinates
176    pub const fn decode_bbox(&self) -> Result<Bbox, DecodeError> {
177        let bits_len = self.len * CHUNK_BITS_SIZE as u8;
178
179        let mut position = 0;
180        let mut hash: u64 = 0;
181        let bytes = self.as_str().as_bytes();
182        while position < bytes.len() {
183            let character = bytes[position];
184            let ch_decoded = REVERSE_BASE32[character as usize];
185            if ch_decoded == u8::MAX {
186                return Err(DecodeError {
187                    position: position as _,
188                    character,
189                })
190            }
191
192            hash <<= 5;
193            hash |= ch_decoded as u64;
194
195            position += 1;
196        }
197
198        //squashes the even bit positions of a u64 into a u32
199        #[inline]
200        const fn squash(x: u64) -> u32 {
201            let mut new_x = x & 0x5555555555555555;
202            new_x = (new_x | (new_x >> 1)) & 0x3333333333333333;
203            new_x = (new_x | (new_x >> 2)) & 0x0f0f0f0f0f0f0f0f;
204            new_x = (new_x | (new_x >> 4)) & 0x00ff00ff00ff00ff;
205            new_x = (new_x | (new_x >> 8)) & 0x0000ffff0000ffff;
206            new_x = (new_x | (new_x >> 16)) & 0x00000000ffffffff;
207            new_x as u32
208        }
209
210        //Reverse interleave()
211        #[inline]
212        const fn deinterleave(x: u64) -> (u32, u32) {
213            (squash(x), squash(x >> 1))
214        }
215
216        let full_hash = hash << (64 - bits_len);
217        let (lat32, lon32) = deinterleave(full_hash);
218
219        //reverse integer conversion
220        const fn decode32(x: u32, r: f64) -> f64 {
221            let p = f64::from_bits(((x as u64) << 20) | (1023 << 52));
222            2.0 * r * (p - 1.0) - r
223        }
224        let min = Coordinate {
225            latitude: decode32(lat32, MAX_LAT),
226            longitude: decode32(lon32, MAX_LON),
227        };
228
229        const fn calc_geohash_diff(bits: u32) -> Coordinate {
230            let lat_bits = bits / 2;
231            let lon_bits = bits - lat_bits;
232
233            let latitude = crate::math::ldexp(MAX_LAT * 2.0, -(lat_bits as i32));
234            let longitude = crate::math::ldexp(MAX_LON * 2.0, -(lon_bits as i32));
235            Coordinate {
236                latitude,
237                longitude,
238            }
239        }
240
241        let max_diff = calc_geohash_diff(bits_len as u32);
242        let max = Coordinate {
243            latitude: min.latitude + max_diff.latitude,
244            longitude: min.longitude + max_diff.longitude,
245        };
246
247        Ok(Bbox {
248            min,
249            max
250        })
251    }
252}
253
254impl fmt::Debug for GeoHash {
255    #[inline(always)]
256    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
257        fmt::Debug::fmt(self.as_str(), fmt)
258    }
259}
260
261impl fmt::Display for GeoHash {
262    #[inline(always)]
263    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
264        fmt::Display::fmt(self.as_str(), fmt)
265    }
266}
267
268impl hash::Hash for GeoHash {
269    #[inline(always)]
270    fn hash<H: hash::Hasher>(&self, state: &mut H) {
271        state.write(self.as_str().as_bytes())
272    }
273}
274
275impl PartialEq for GeoHash {
276    #[inline(always)]
277    fn eq(&self, other: &Self) -> bool {
278        self.as_str() == other.as_str()
279    }
280}
281
282impl PartialEq<str> for GeoHash {
283    #[inline(always)]
284    fn eq(&self, other: &str) -> bool {
285        self.as_str() == other
286    }
287}
288
289impl PartialEq<&str> for GeoHash {
290    #[inline(always)]
291    fn eq(&self, other: &&str) -> bool {
292        self.as_str() == *other
293    }
294}