1use core::fmt;
48
49pub const MAX_PRECISION: u8 = 7;
51
52#[derive(Clone, Copy, PartialEq, Eq, Hash)]
60pub struct NodeLocation {
61 len: u8,
62 bytes: [u8; MAX_PRECISION as usize],
63}
64
65impl NodeLocation {
66 pub const UNSPECIFIED: NodeLocation = NodeLocation {
68 len: 0,
69 bytes: [0; MAX_PRECISION as usize],
70 };
71
72 pub fn from_bytes(bytes: &[u8]) -> Self {
75 let len = bytes.len().min(MAX_PRECISION as usize) as u8;
76 let mut buf = [0u8; MAX_PRECISION as usize];
77 buf[..len as usize].copy_from_slice(&bytes[..len as usize]);
78 Self { len, bytes: buf }
79 }
80
81 pub fn from_lat_lon(lat: f32, lon: f32, precision: u8) -> Self {
89 let precision = precision.min(MAX_PRECISION);
90 if precision == 0 {
91 return Self::UNSPECIFIED;
92 }
93 let lat = lat.clamp(-90.0, 90.0);
94 let lon = lon.clamp(-180.0, 180.0);
95 let (lat_idx, lon_idx) = encode_indices(lat, lon, precision as u32);
96
97 let mut bytes = [0u8; MAX_PRECISION as usize];
98 for k in 0..precision as usize {
99 let shift = 4 * (precision as usize - 1 - k);
100 let hi = ((lat_idx >> shift) & 0xF) as u8;
101 let lo = ((lon_idx >> shift) & 0xF) as u8;
102 bytes[k] = (hi << 4) | lo;
103 }
104 Self {
105 len: precision,
106 bytes,
107 }
108 }
109
110 #[cfg(feature = "f64")]
116 pub fn from_lat_lon_f64(lat: f64, lon: f64, precision: u8) -> Self {
117 Self::from_lat_lon(lat as f32, lon as f32, precision)
118 }
119
120 pub fn from_e7(lat_e7: i32, lon_e7: i32, precision: u8) -> Self {
133 const LAT_SPAN_E7: i64 = 1_800_000_000;
134 const LON_SPAN_E7: i64 = 3_600_000_000;
135
136 let precision = precision.min(MAX_PRECISION);
137 if precision == 0 {
138 return Self::UNSPECIFIED;
139 }
140 let lat = i64::from(lat_e7).clamp(-LAT_SPAN_E7 / 2, LAT_SPAN_E7 / 2);
141 let lon = i64::from(lon_e7).clamp(-LON_SPAN_E7 / 2, LON_SPAN_E7 / 2);
142 let cells = 1i64 << (4 * precision as u32);
144 let max_index = (cells - 1) as u32;
145 let lat_idx = (((lat + LAT_SPAN_E7 / 2) * cells) / LAT_SPAN_E7).min(i64::from(max_index));
146 let lon_idx = (((lon + LON_SPAN_E7 / 2) * cells) / LON_SPAN_E7).min(i64::from(max_index));
147
148 let mut bytes = [0u8; MAX_PRECISION as usize];
149 for k in 0..precision as usize {
150 let shift = 4 * (precision as usize - 1 - k);
151 let hi = ((lat_idx >> shift) & 0xF) as u8;
152 let lo = ((lon_idx >> shift) & 0xF) as u8;
153 bytes[k] = (hi << 4) | lo;
154 }
155 Self {
156 len: precision,
157 bytes,
158 }
159 }
160
161 pub fn as_bytes(&self) -> &[u8] {
163 &self.bytes[..self.len as usize]
164 }
165
166 pub fn len(&self) -> usize {
168 self.len as usize
169 }
170
171 pub fn is_unspecified(&self) -> bool {
173 self.len == 0
174 }
175
176 pub fn precision(&self) -> u8 {
178 self.len
179 }
180
181 pub fn clamped(&self, precision: u8) -> Self {
186 let len = self.len.min(precision.min(MAX_PRECISION));
187 let mut bytes = [0u8; MAX_PRECISION as usize];
195 bytes[..len as usize].copy_from_slice(&self.bytes[..len as usize]);
196 Self { len, bytes }
197 }
198
199 pub fn bounds(&self) -> ((f32, f32), (f32, f32)) {
204 if self.len == 0 {
205 return ((-90.0, -180.0), (90.0, 180.0));
206 }
207 let (lat_idx, lon_idx) = self.decode_indices();
208 let n = self.len as u32;
209 let (lat_lo, lat_hi) = decode_range(lat_idx, 180.0, -90.0, n);
210 let (lon_lo, lon_hi) = decode_range(lon_idx, 360.0, -180.0, n);
211 ((lat_lo, lon_lo), (lat_hi, lon_hi))
212 }
213
214 pub fn center(&self) -> (f32, f32) {
216 let ((lat_lo, lon_lo), (lat_hi, lon_hi)) = self.bounds();
217 ((lat_lo + lat_hi) * 0.5, (lon_lo + lon_hi) * 0.5)
218 }
219
220 pub fn contains(&self, lat: f32, lon: f32) -> bool {
224 let ((lat_lo, lon_lo), (lat_hi, lon_hi)) = self.bounds();
225 lat >= lat_lo && lat < lat_hi && lon >= lon_lo && lon < lon_hi
226 }
227
228 pub fn contains_location(&self, other: &Self) -> bool {
233 if self.len == 0 {
234 return true;
235 }
236 if other.len < self.len {
237 return false;
238 }
239 other.bytes[..self.len as usize] == self.bytes[..self.len as usize]
240 }
241
242 fn decode_indices(&self) -> (u32, u32) {
244 let mut lat = 0u32;
245 let mut lon = 0u32;
246 for &b in &self.bytes[..self.len as usize] {
247 lat = (lat << 4) | ((b >> 4) as u32);
248 lon = (lon << 4) | ((b & 0xF) as u32);
249 }
250 (lat, lon)
251 }
252}
253
254#[inline]
258fn encode_indices(lat: f32, lon: f32, n: u32) -> (u32, u32) {
259 #[cfg(feature = "f64")]
260 {
261 let scale = (1u64 << (4 * n)) as f64;
262 let lat_idx = ((lat as f64 + 90.0) * scale / 180.0) as u32;
263 let lon_idx = ((lon as f64 + 180.0) * scale / 360.0) as u32;
264 let max_idx = (scale as u32).saturating_sub(1);
265 (lat_idx.min(max_idx), lon_idx.min(max_idx))
266 }
267 #[cfg(not(feature = "f64"))]
268 {
269 let scale = (1u64 << (4 * n)) as f32;
270 let lat_idx = ((lat + 90.0) * scale / 180.0) as u32;
271 let lon_idx = ((lon + 180.0) * scale / 360.0) as u32;
272 let max_idx = (scale as u32).saturating_sub(1);
273 (lat_idx.min(max_idx), lon_idx.min(max_idx))
274 }
275}
276
277#[inline]
279fn decode_range(idx: u32, range: f32, offset: f32, n: u32) -> (f32, f32) {
280 #[cfg(feature = "f64")]
281 {
282 let scale = (1u64 << (4 * n)) as f64;
283 let lo = (idx as f64 * range as f64 / scale + offset as f64) as f32;
284 let hi = ((idx as f64 + 1.0) * range as f64 / scale + offset as f64) as f32;
285 (lo, hi)
286 }
287 #[cfg(not(feature = "f64"))]
288 {
289 let scale = (1u64 << (4 * n)) as f32;
290 let lo = idx as f32 * range / scale + offset;
291 let hi = (idx as f32 + 1.0) * range / scale + offset;
292 (lo, hi)
293 }
294}
295
296impl Default for NodeLocation {
299 fn default() -> Self {
300 Self::UNSPECIFIED
301 }
302}
303
304impl fmt::Display for NodeLocation {
308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309 if self.len == 0 {
310 return f.write_str("(unspecified)");
311 }
312 let (lat, lon) = self.center();
313 let dp = self.len.saturating_sub(1) as usize;
314 write!(f, "{:.*}, {:.*}", dp, lat, dp, lon)
315 }
316}
317
318impl fmt::Debug for NodeLocation {
319 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320 if self.len == 0 {
321 return write!(f, "NodeLocation(unspecified)");
322 }
323 write!(f, "NodeLocation({} @ precision {})", self, self.len)
324 }
325}
326
327impl From<NodeLocation> for (f32, f32) {
329 fn from(loc: NodeLocation) -> Self {
330 loc.center()
331 }
332}
333
334impl From<(f32, f32)> for NodeLocation {
336 fn from((lat, lon): (f32, f32)) -> Self {
337 Self::from_lat_lon(lat, lon, MAX_PRECISION)
338 }
339}
340
341#[cfg(feature = "f64")]
345impl From<(f64, f64)> for NodeLocation {
346 fn from((lat, lon): (f64, f64)) -> Self {
347 Self::from_lat_lon(lat as f32, lon as f32, MAX_PRECISION)
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 #[test]
358 fn from_bytes_roundtrips() {
359 let src = [0xB2, 0x59, 0x15];
360 let loc = NodeLocation::from_bytes(&src);
361 assert_eq!(loc.as_bytes(), &src);
362 assert_eq!(loc.len(), 3);
363 }
364
365 #[test]
366 fn from_bytes_truncates_to_max_precision() {
367 let loc = NodeLocation::from_bytes(&[0u8; 10]);
368 assert_eq!(loc.len(), MAX_PRECISION as usize);
369 }
370
371 #[test]
372 fn from_bytes_empty_is_unspecified() {
373 let loc = NodeLocation::from_bytes(&[]);
374 assert!(loc.is_unspecified());
375 assert_eq!(loc, NodeLocation::UNSPECIFIED);
376 }
377
378 #[test]
381 fn san_jose_3_byte() {
382 let loc = NodeLocation::from_lat_lon(37.331, -121.883, 3);
384 assert_eq!(loc.as_bytes(), &[0xB2, 0x59, 0x15]);
385 }
386
387 #[test]
390 fn from_e7_matches_the_spec_worked_example() {
391 let loc = NodeLocation::from_e7(373_310_000, -1_218_830_000, 3);
393 assert_eq!(loc.as_bytes(), &[0xB2, 0x59, 0x15]);
394 }
395
396 #[test]
400 fn from_e7_agrees_with_the_float_path_where_that_path_is_sound() {
401 let places = [
402 (373_310_000i32, -1_218_830_000i32),
403 (525_200_000, 134_050_000),
404 (0, 0),
405 (-410_000_000, 1_746_000_000),
406 (-330_000_000, -700_000_000),
407 ];
408 for (lat_e7, lon_e7) in places {
409 for precision in 1..=5u8 {
410 let integer = NodeLocation::from_e7(lat_e7, lon_e7, precision);
411 let float =
412 NodeLocation::from_lat_lon(lat_e7 as f32 / 1e7, lon_e7 as f32 / 1e7, precision);
413 assert_eq!(
414 integer.as_bytes(),
415 float.as_bytes(),
416 "disagreement at ({lat_e7}, {lon_e7}) precision {precision}"
417 );
418 }
419 }
420 }
421
422 #[test]
425 fn from_e7_is_prefix_truncation_safe_at_every_precision() {
426 let (lat_e7, lon_e7) = (373_310_456, -1_218_830_123);
427 let full = NodeLocation::from_e7(lat_e7, lon_e7, MAX_PRECISION);
428 for precision in 1..=MAX_PRECISION {
429 let short = NodeLocation::from_e7(lat_e7, lon_e7, precision);
430 assert_eq!(
431 short.as_bytes(),
432 &full.as_bytes()[..precision as usize],
433 "precision {precision} is not a prefix of the full encoding"
434 );
435 assert_eq!(short, full.clamped(precision));
436 }
437 }
438
439 #[test]
443 fn a_clamped_location_equals_the_same_cell_built_directly() {
444 let full = NodeLocation::from_e7(373_310_456, -1_218_830_123, MAX_PRECISION);
445 for precision in 0..=MAX_PRECISION {
446 let clamped = full.clamped(precision);
447 let direct = NodeLocation::from_bytes(&full.as_bytes()[..precision as usize]);
448 assert_eq!(clamped, direct, "at precision {precision}");
449 }
450 }
451
452 #[test]
453 fn from_e7_clamps_rather_than_wrapping_at_the_extremes() {
454 let corner = NodeLocation::from_e7(900_000_000, 1_800_000_000, 2);
458 assert_eq!(corner.as_bytes(), &[0xFF, 0xFF]);
459 let opposite = NodeLocation::from_e7(-900_000_000, -1_800_000_000, 2);
460 assert_eq!(opposite.as_bytes(), &[0x00, 0x00]);
461 assert_eq!(
463 NodeLocation::from_e7(i32::MAX, i32::MAX, 2).as_bytes(),
464 corner.as_bytes()
465 );
466 assert_eq!(
467 NodeLocation::from_e7(i32::MIN, i32::MIN, 2).as_bytes(),
468 opposite.as_bytes()
469 );
470 }
471
472 #[test]
473 fn from_e7_at_zero_precision_is_unspecified() {
474 assert!(NodeLocation::from_e7(525_200_000, 134_050_000, 0).is_unspecified());
475 assert_eq!(
477 NodeLocation::from_e7(525_200_000, 134_050_000, 20).len(),
478 MAX_PRECISION as usize
479 );
480 }
481
482 #[test]
483 fn encode_contains_source_point() {
484 let (lat, lon) = (52.52f32, 13.405f32); for precision in 1..=5u8 {
489 let loc = NodeLocation::from_lat_lon(lat, lon, precision);
490 assert!(loc.contains(lat, lon), "failed at precision={precision}");
491 }
492 }
493
494 #[cfg(feature = "f64")]
498 #[test]
499 fn f64_decode_cell_width_precision_5() {
500 let loc = NodeLocation::from_lat_lon(52.52, 13.405, 5);
501 let ((_, lon_lo), (_, lon_hi)) = loc.bounds();
502 let expected = 360.0f64 / (1u64 << 20) as f64;
503 let actual = (lon_hi - lon_lo) as f64;
504 assert!(
505 (actual - expected).abs() < 1e-7,
506 "cell width {actual} != {expected}"
507 );
508 }
509
510 #[test]
511 fn antimeridian_does_not_panic() {
512 let _ = NodeLocation::from_lat_lon(0.0, 180.0, 7);
513 let _ = NodeLocation::from_lat_lon(0.0, -180.0, 7);
514 }
515
516 #[test]
517 fn poles_do_not_panic() {
518 let _ = NodeLocation::from_lat_lon(90.0, 0.0, 7);
519 let _ = NodeLocation::from_lat_lon(-90.0, 0.0, 7);
520 }
521
522 #[test]
523 fn zero_precision_gives_unspecified() {
524 assert_eq!(
525 NodeLocation::from_lat_lon(0.0, 0.0, 0),
526 NodeLocation::UNSPECIFIED
527 );
528 }
529
530 #[test]
531 fn excess_precision_clamped_to_max() {
532 assert_eq!(
533 NodeLocation::from_lat_lon(0.0, 0.0, 255).len(),
534 MAX_PRECISION as usize
535 );
536 }
537
538 #[test]
541 fn truncation_matches_direct_lower_precision() {
542 let (lat, lon) = (51.509f32, -0.118f32); let full = NodeLocation::from_lat_lon(lat, lon, 7);
544 for k in 1..=7u8 {
545 let direct = NodeLocation::from_lat_lon(lat, lon, k);
546 let truncated = full.clamped(k);
547 assert_eq!(
548 direct.as_bytes(),
549 truncated.as_bytes(),
550 "mismatch at precision={k}"
551 );
552 }
553 }
554
555 #[test]
558 fn center_is_within_bounds() {
559 let loc = NodeLocation::from_lat_lon(48.864, 2.349, 5); let (lat_c, lon_c) = loc.center();
561 assert!(loc.contains(lat_c, lon_c));
562 }
563
564 #[test]
565 fn unspecified_bounds_is_whole_globe() {
566 let ((lat_lo, lon_lo), (lat_hi, lon_hi)) = NodeLocation::UNSPECIFIED.bounds();
567 assert_eq!(
568 (lat_lo, lon_lo, lat_hi, lon_hi),
569 (-90.0, -180.0, 90.0, 180.0)
570 );
571 }
572
573 #[test]
574 fn bounds_span_shrinks_by_16_per_byte() {
575 let (lat, lon) = (0.0f32, 0.0f32);
576 let loc1 = NodeLocation::from_lat_lon(lat, lon, 1);
577 let loc2 = NodeLocation::from_lat_lon(lat, lon, 2);
578 let ((lo1, _), (hi1, _)) = loc1.bounds();
579 let ((lo2, _), (hi2, _)) = loc2.bounds();
580 let ratio = (hi1 - lo1) / (hi2 - lo2);
581 assert!((ratio - 16.0).abs() < 1e-4, "expected 16×, got {ratio}");
582 }
583
584 #[test]
587 fn contains_source_point() {
588 let loc = NodeLocation::from_lat_lon(41.878, -87.629, 4); assert!(loc.contains(41.878, -87.629));
590 }
591
592 #[test]
593 fn contains_location_coarser_contains_finer() {
594 let coarse = NodeLocation::from_lat_lon(35.689, 139.691, 3); let fine = NodeLocation::from_lat_lon(35.689, 139.691, 6);
596 assert!(coarse.contains_location(&fine));
597 assert!(!fine.contains_location(&coarse));
598 }
599
600 #[test]
601 fn unspecified_contains_everything() {
602 let anywhere = NodeLocation::from_lat_lon(28.614, 77.209, 7); assert!(NodeLocation::UNSPECIFIED.contains_location(&anywhere));
604 }
605
606 #[test]
609 fn from_f32_tuple_roundtrips_approximately() {
610 let (lat, lon) = (-33.868f32, 151.209f32); let loc = NodeLocation::from((lat, lon));
612 let (out_lat, out_lon): (f32, f32) = loc.into();
613 assert!((out_lat - lat).abs() < 0.001, "lat drift={}", out_lat - lat);
614 assert!((out_lon - lon).abs() < 0.001, "lon drift={}", out_lon - lon);
615 }
616
617 #[test]
620 fn display_unspecified() {
621 assert_eq!(NodeLocation::UNSPECIFIED.to_string(), "(unspecified)");
622 }
623
624 #[test]
625 fn display_precision_one_no_decimal_point() {
626 let loc = NodeLocation::from_lat_lon(0.0, 0.0, 1);
627 let s = loc.to_string();
628 assert!(!s.contains('.'), "unexpected decimal in '{s}'");
629 }
630
631 #[test]
632 fn display_precision_four_has_three_decimal_places() {
633 let loc = NodeLocation::from_lat_lon(0.0, 0.0, 4);
634 let s = loc.to_string();
635 for part in s.split(", ") {
636 let dp = part.find('.').map(|i| part.len() - i - 1).unwrap_or(0);
637 assert_eq!(dp, 3, "wrong decimal places in '{s}'");
638 }
639 }
640}