1use h3o::{CellIndex, LatLng, Resolution};
12use nodedb_types::geometry::Geometry;
13
14pub fn h3_encode(lng: f64, lat: f64, resolution: u8) -> Option<u64> {
19 let res = Resolution::try_from(resolution).ok()?;
20 let ll = LatLng::new(lat, lng).ok()?;
21 let cell = ll.to_cell(res);
22 Some(u64::from(cell))
23}
24
25pub fn h3_encode_string(lng: f64, lat: f64, resolution: u8) -> Option<String> {
27 let res = Resolution::try_from(resolution).ok()?;
28 let ll = LatLng::new(lat, lng).ok()?;
29 let cell = ll.to_cell(res);
30 Some(cell.to_string())
31}
32
33pub fn h3_to_center(h3_index: u64) -> Option<(f64, f64)> {
35 let cell = CellIndex::try_from(h3_index).ok()?;
36 let ll = LatLng::from(cell);
37 Some((ll.lng(), ll.lat()))
38}
39
40pub fn h3_to_boundary(h3_index: u64) -> Option<Geometry> {
44 let cell = CellIndex::try_from(h3_index).ok()?;
45 let boundary = cell.boundary();
46 let mut ring: Vec<[f64; 2]> = boundary.iter().map(|ll| [ll.lng(), ll.lat()]).collect();
47 if let Some(&first) = ring.first() {
49 ring.push(first);
50 }
51 Some(Geometry::Polygon {
52 coordinates: vec![ring],
53 })
54}
55
56pub fn h3_resolution(h3_index: u64) -> Option<u8> {
58 let cell = CellIndex::try_from(h3_index).ok()?;
59 Some(cell.resolution() as u8)
60}
61
62pub fn h3_parent(h3_index: u64, parent_resolution: u8) -> Option<u64> {
64 let cell = CellIndex::try_from(h3_index).ok()?;
65 let res = Resolution::try_from(parent_resolution).ok()?;
66 cell.parent(res).map(u64::from)
67}
68
69pub fn h3_neighbors(h3_index: u64) -> Vec<u64> {
71 let Ok(cell) = CellIndex::try_from(h3_index) else {
72 return Vec::new();
73 };
74 cell.grid_disk::<Vec<_>>(1)
75 .into_iter()
76 .filter(|&c| c != cell)
77 .map(u64::from)
78 .collect()
79}
80
81pub fn h3_is_valid(h3_index: u64) -> bool {
83 CellIndex::try_from(h3_index).is_ok()
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn encode_nyc() {
92 let idx = h3_encode(-73.9857, 40.7484, 7).unwrap();
93 assert!(h3_is_valid(idx));
94 assert_eq!(h3_resolution(idx).unwrap(), 7);
95 }
96
97 #[test]
98 fn encode_string_roundtrip() {
99 let hex = h3_encode_string(0.0, 0.0, 5).unwrap();
100 assert!(!hex.is_empty());
101 }
102
103 #[test]
104 fn center_roundtrip() {
105 let idx = h3_encode(10.0, 50.0, 9).unwrap();
106 let (lng, lat) = h3_to_center(idx).unwrap();
107 assert!((lng - 10.0).abs() < 0.01, "lng={lng}");
108 assert!((lat - 50.0).abs() < 0.01, "lat={lat}");
109 }
110
111 #[test]
112 fn boundary_is_polygon() {
113 let idx = h3_encode(0.0, 0.0, 5).unwrap();
114 let poly = h3_to_boundary(idx).unwrap();
115 assert_eq!(poly.geometry_type(), "Polygon");
116 if let Geometry::Polygon { coordinates } = &poly {
117 assert!(coordinates[0].len() >= 7, "len={}", coordinates[0].len());
119 }
120 }
121
122 #[test]
123 fn resolution_accessor() {
124 for res in 0..=15 {
125 let idx = h3_encode(0.0, 0.0, res).unwrap();
126 assert_eq!(h3_resolution(idx).unwrap(), res);
127 }
128 }
129
130 #[test]
131 fn parent_is_coarser() {
132 let child = h3_encode(0.0, 0.0, 9).unwrap();
133 let parent = h3_parent(child, 7).unwrap();
134 assert_eq!(h3_resolution(parent).unwrap(), 7);
135 }
136
137 #[test]
138 fn neighbors_count() {
139 let idx = h3_encode(0.0, 0.0, 7).unwrap();
140 let nbrs = h3_neighbors(idx);
141 assert_eq!(nbrs.len(), 6, "got {} neighbors", nbrs.len());
143 }
144
145 #[test]
146 fn invalid_index() {
147 assert!(!h3_is_valid(0));
148 assert!(h3_to_center(0).is_none());
149 }
150
151 #[test]
152 fn nearby_points_same_cell() {
153 let a = h3_encode(-73.985, 40.758, 9).unwrap();
154 let b = h3_encode(-73.9851, 40.7581, 9).unwrap();
155 assert_eq!(a, b);
157 }
158
159 #[test]
160 fn different_resolutions_different_cells() {
161 let coarse = h3_encode(0.0, 0.0, 3).unwrap();
162 let fine = h3_encode(0.0, 0.0, 9).unwrap();
163 assert_ne!(coarse, fine);
164 }
165}