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