oxigdal_noalloc/
geohash.rs1use crate::BBox2D;
7
8pub const BASE32_CHARS: &[u8; 32] = b"0123456789bcdefghjkmnpqrstuvwxyz";
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct GeoHashFixed {
16 chars: [u8; 12],
17 len: u8,
18}
19
20impl GeoHashFixed {
21 #[must_use]
25 pub fn encode(lat: f64, lon: f64, precision: u8) -> Self {
26 let precision = precision.clamp(1, 12) as usize;
27
28 let mut chars = [0u8; 12];
29 let mut min_lat = -90.0_f64;
31 let mut max_lat = 90.0_f64;
32 let mut min_lon = -180.0_f64;
33 let mut max_lon = 180.0_f64;
34
35 let mut is_lon = true; let mut bits = 0u8; let mut bit_count = 0u8;
38 let mut char_idx = 0usize;
39
40 let total_bits = precision * 5;
42
43 for _ in 0..total_bits {
44 if is_lon {
45 let mid = (min_lon + max_lon) * 0.5;
46 if lon >= mid {
47 bits = (bits << 1) | 1;
48 min_lon = mid;
49 } else {
50 bits <<= 1;
51 max_lon = mid;
52 }
53 } else {
54 let mid = (min_lat + max_lat) * 0.5;
55 if lat >= mid {
56 bits = (bits << 1) | 1;
57 min_lat = mid;
58 } else {
59 bits <<= 1;
60 max_lat = mid;
61 }
62 }
63 is_lon = !is_lon;
64 bit_count += 1;
65
66 if bit_count == 5 {
67 chars[char_idx] = BASE32_CHARS[bits as usize];
68 char_idx += 1;
69 bits = 0;
70 bit_count = 0;
71 }
72 }
73
74 Self {
75 chars,
76 len: precision as u8,
77 }
78 }
79
80 #[must_use]
82 #[inline]
83 pub fn as_bytes(&self) -> &[u8] {
84 &self.chars[..self.len as usize]
85 }
86
87 #[must_use]
89 #[inline]
90 pub fn precision(&self) -> u8 {
91 self.len
92 }
93
94 #[must_use]
96 pub fn decode(&self) -> (f64, f64) {
97 let bbox = self.bbox();
98 let lat = (bbox.min_y + bbox.max_y) * 0.5;
99 let lon = (bbox.min_x + bbox.max_x) * 0.5;
100 (lat, lon)
101 }
102
103 #[must_use]
111 pub fn neighbours(&self) -> [GeoHashFixed; 8] {
112 let bbox = self.bbox();
113 let center_lat = (bbox.min_y + bbox.max_y) * 0.5;
114 let center_lon = (bbox.min_x + bbox.max_x) * 0.5;
115 let dlat = bbox.max_y - bbox.min_y;
116 let dlon = bbox.max_x - bbox.min_x;
117 let prec = self.len;
118
119 let offsets: [(f64, f64); 8] = [
122 (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.0), (-1.0, -1.0), (0.0, -1.0), (1.0, -1.0), ];
131
132 let mut result = [*self; 8];
133 let mut i = 0;
134 while i < 8 {
135 let (dy, dx) = offsets[i];
136 let mut nlat = center_lat + dy * dlat;
137 let mut nlon = center_lon + dx * dlon;
138
139 nlat = nlat.clamp(-90.0, 90.0);
141 if nlon > 180.0 {
143 nlon -= 360.0;
144 }
145 if nlon < -180.0 {
146 nlon += 360.0;
147 }
148
149 result[i] = GeoHashFixed::encode(nlat, nlon, prec);
150 i += 1;
151 }
152 result
153 }
154
155 #[must_use]
159 pub fn bbox(&self) -> BBox2D {
160 let mut min_lat = -90.0_f64;
161 let mut max_lat = 90.0_f64;
162 let mut min_lon = -180.0_f64;
163 let mut max_lon = 180.0_f64;
164
165 let bytes = self.as_bytes();
166 let mut is_lon = true;
167
168 for &byte in bytes {
169 let idx = base32_decode_char(byte);
171 for bit_pos in (0..5).rev() {
173 let bit = (idx >> bit_pos) & 1;
174 if is_lon {
175 let mid = (min_lon + max_lon) * 0.5;
176 if bit == 1 {
177 min_lon = mid;
178 } else {
179 max_lon = mid;
180 }
181 } else {
182 let mid = (min_lat + max_lat) * 0.5;
183 if bit == 1 {
184 min_lat = mid;
185 } else {
186 max_lat = mid;
187 }
188 }
189 is_lon = !is_lon;
190 }
191 }
192
193 BBox2D::new(min_lon, min_lat, max_lon, max_lat)
194 }
195}
196
197#[inline]
200fn base32_decode_char(c: u8) -> u8 {
201 for (i, &ch) in BASE32_CHARS.iter().enumerate() {
202 if ch == c {
203 return i as u8;
204 }
205 }
206 0
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn test_neighbours_count() {
215 let gh = GeoHashFixed::encode(48.858, 2.294, 6); let nbrs = gh.neighbours();
217 assert_eq!(nbrs.len(), 8);
218 }
219
220 #[test]
221 fn test_neighbours_different_from_center() {
222 let gh = GeoHashFixed::encode(48.858, 2.294, 6);
223 let nbrs = gh.neighbours();
224 for nbr in &nbrs {
225 assert_ne!(
226 nbr.as_bytes(),
227 gh.as_bytes(),
228 "neighbour should differ from center"
229 );
230 }
231 }
232
233 #[test]
234 fn test_neighbours_same_precision() {
235 let gh = GeoHashFixed::encode(35.68, 139.69, 5);
236 let nbrs = gh.neighbours();
237 for nbr in &nbrs {
238 assert_eq!(nbr.precision(), gh.precision());
239 }
240 }
241
242 #[test]
243 fn test_neighbours_adjacency() {
244 let gh = GeoHashFixed::encode(40.0, -74.0, 5);
247 let gh_bbox = gh.bbox();
248 let nbrs = gh.neighbours();
249 let north = &nbrs[0];
250 let n_bbox = north.bbox();
251 let diff = (n_bbox.min_y - gh_bbox.max_y).abs();
253 assert!(
254 diff < 0.01,
255 "North neighbour should be adjacent: diff={diff}"
256 );
257 }
258
259 #[test]
260 fn test_neighbours_south_adjacency() {
261 let gh = GeoHashFixed::encode(40.0, -74.0, 5);
262 let gh_bbox = gh.bbox();
263 let nbrs = gh.neighbours();
264 let south = &nbrs[4];
265 let s_bbox = south.bbox();
266 let diff = (s_bbox.max_y - gh_bbox.min_y).abs();
267 assert!(
268 diff < 0.01,
269 "South neighbour should be adjacent: diff={diff}"
270 );
271 }
272
273 #[test]
274 fn test_neighbours_east_adjacency() {
275 let gh = GeoHashFixed::encode(40.0, -74.0, 5);
276 let gh_bbox = gh.bbox();
277 let nbrs = gh.neighbours();
278 let east = &nbrs[2];
279 let e_bbox = east.bbox();
280 let diff = (e_bbox.min_x - gh_bbox.max_x).abs();
281 assert!(
282 diff < 0.01,
283 "East neighbour should be adjacent: diff={diff}"
284 );
285 }
286
287 #[test]
288 fn test_neighbours_wrap_antimeridian() {
289 let gh = GeoHashFixed::encode(0.0, 179.99, 3);
291 let nbrs = gh.neighbours();
292 let east = &nbrs[2];
294 let (_, east_lon) = east.decode();
295 assert!(
297 !(0.0..=170.0).contains(&east_lon),
298 "East of lon~180 should wrap or stay high, got {east_lon}"
299 );
300 }
301
302 #[test]
303 fn test_neighbours_pole_clamp() {
304 let gh = GeoHashFixed::encode(89.9, 0.0, 3);
306 let nbrs = gh.neighbours();
307 let north = &nbrs[0];
308 let (n_lat, _) = north.decode();
309 assert!(n_lat <= 90.0, "North of pole should be clamped to 90");
310 }
311
312 #[test]
313 fn test_neighbours_all_unique() {
314 let gh = GeoHashFixed::encode(51.5, -0.1, 6);
315 let nbrs = gh.neighbours();
316 let mut i = 0;
318 while i < 8 {
319 let mut j = i + 1;
320 while j < 8 {
321 assert_ne!(
322 nbrs[i].as_bytes(),
323 nbrs[j].as_bytes(),
324 "Neighbours {i} and {j} should be distinct"
325 );
326 j += 1;
327 }
328 i += 1;
329 }
330 }
331}