h3o/index/edge.rs
1use super::{IndexMode, bits};
2use crate::{
3 Boundary, CellIndex, Direction, EARTH_RADIUS_KM, coord::FaceIJK, error,
4 grid,
5};
6use core::{cmp::Ordering, fmt, num::NonZeroU64, str::FromStr};
7
8/// Minimum value for a cell edge.
9const MIN: u8 = 1;
10
11/// Maximum value for a cell edge.
12const MAX: u8 = 6;
13
14// -----------------------------------------------------------------------------
15
16/// Edge of an H3 cell.
17#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub struct Edge(u8);
20
21impl Edge {
22 /// Iterates over the valid directions.
23 ///
24 /// # Example
25 ///
26 /// ```
27 /// let edges = h3o::Edge::iter().collect::<Vec<_>>();
28 /// ```
29 pub fn iter() -> impl Iterator<Item = Self> {
30 // SAFETY: values from 0 to MAX are valid directions.
31 (MIN..=MAX).map(Self::new_unchecked)
32 }
33
34 /// Initializes a new cell edge using a value that may be out of range.
35 ///
36 /// # Safety
37 ///
38 /// The value must be a valid cell edge.
39 pub(crate) fn new_unchecked(value: u8) -> Self {
40 debug_assert!((MIN..=MAX).contains(&value), "cell edge out of range");
41 Self(value)
42 }
43}
44
45impl TryFrom<u8> for Edge {
46 type Error = error::InvalidEdge;
47
48 fn try_from(value: u8) -> Result<Self, Self::Error> {
49 if !(MIN..=MAX).contains(&value) {
50 return Err(Self::Error::new(value, "out of range"));
51 }
52 Ok(Self(value))
53 }
54}
55
56impl From<Edge> for u8 {
57 fn from(value: Edge) -> Self {
58 value.0
59 }
60}
61
62impl From<Edge> for u64 {
63 fn from(value: Edge) -> Self {
64 Self::from(value.0)
65 }
66}
67
68impl fmt::Display for Edge {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 write!(f, "{}", self.0)
71 }
72}
73
74// -----------------------------------------------------------------------------
75
76/// Represents a single directed edge between two cells (an "origin" cell and a
77/// neighboring "destination" cell).
78///
79/// The index is encoded on 64-bit with the following bit layout:
80///
81/// ```text
82/// ┏━┳━━━┳━━━┳━━━━━━━━━━━━━━━━━━━━━━┈┈┈┈┈┈┈┈━━━━━━━┓
83/// ┃U┃ M ┃ E ┃ O ┃
84/// ┗━┻━━━┻━━━┻━━━━━━━━━━━━━━━━━━━━━━┈┈┈┈┈┈┈┈━━━━━━━┛
85/// 64 63 59 56 0
86/// ```
87///
88/// Where:
89/// - `U` is an unused reserved bit, always set to 0 (bit 63).
90/// - `M` is the index mode, always set to 2, coded on 4 bits (59-62).
91/// - `E` is the edge of the origin cell, in [1; 6], coded on 3 bits (56-58).
92/// - `O` is the origin cell index, coded on 56 bits (0-55).
93///
94/// References:
95/// - [H3 Index Representations](https://h3geo.org/docs/core-library/h3Indexing)
96/// - [H3 Index Bit Layout](https://observablehq.com/@nrabinowitz/h3-index-bit-layout?collection=@nrabinowitz/h3)
97/// - [H3 Index Inspector](https://observablehq.com/@nrabinowitz/h3-index-inspector?collection=@nrabinowitz/h3)
98#[derive(Clone, Copy, Eq, PartialEq, Hash)]
99#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
100pub struct DirectedEdgeIndex(NonZeroU64);
101
102impl DirectedEdgeIndex {
103 /// Returns the cell edge.
104 ///
105 /// # Example
106 ///
107 /// ```
108 /// let index = h3o::DirectedEdgeIndex::try_from(0x13a194e699ab7fff)?;
109 /// assert_eq!(index.edge(), h3o::Edge::try_from(3)?);
110 /// # Ok::<(), Box<dyn std::error::Error>>(())
111 /// ```
112 #[must_use]
113 pub fn edge(self) -> Edge {
114 // SAFETY: `EdgeIndex` only contains valid cell edge (invariant).
115 Edge::new_unchecked(bits::get_edge(self.0.get()))
116 }
117
118 /// Returns the origin hexagon from the directed edge index.
119 ///
120 /// # Example
121 ///
122 /// ```
123 /// let index = h3o::DirectedEdgeIndex::try_from(0x13a194e699ab7fff)?;
124 /// assert_eq!(index.origin(), h3o::CellIndex::try_from(0x8a194e699ab7fff)?);
125 /// # Ok::<(), Box<dyn std::error::Error>>(())
126 /// ```
127 #[must_use]
128 pub fn origin(self) -> CellIndex {
129 let bits = bits::set_mode(self.0.get(), IndexMode::Cell);
130 CellIndex::new_unchecked(bits::clr_edge(bits))
131 }
132
133 /// Returns the destination hexagon from the directed edge index.
134 ///
135 /// # Example
136 ///
137 /// ```
138 /// let index = h3o::DirectedEdgeIndex::try_from(0x13a1_94e6_99ab_7fff)?;
139 /// assert_eq!(index.destination(), h3o::CellIndex::try_from(0x8a194e699a97fff)?);
140 /// # Ok::<(), Box<dyn std::error::Error>>(())
141 /// ```
142 #[must_use]
143 pub fn destination(self) -> CellIndex {
144 let direction = Direction::from(self.edge());
145 let origin = self.origin();
146 // Every edge has a destination.
147 grid::neighbor_rotations(origin, direction, 0)
148 .expect("destination cell index")
149 .0
150 }
151
152 /// Returns the `(origin, destination)` pair of cell index for this edge.
153 ///
154 /// # Example
155 ///
156 /// ```
157 /// let index = h3o::DirectedEdgeIndex::try_from(0x13a1_94e6_99ab_7fff)?;
158 /// assert_eq!(index.cells(), (
159 /// h3o::CellIndex::try_from(0x8a194e699ab7fff)?,
160 /// h3o::CellIndex::try_from(0x8a194e699a97fff)?,
161 /// ));
162 /// # Ok::<(), Box<dyn std::error::Error>>(())
163 /// ```
164 #[must_use]
165 pub fn cells(self) -> (CellIndex, CellIndex) {
166 (self.origin(), self.destination())
167 }
168
169 /// Returns the coordinates defining the directed edge.
170 ///
171 /// # Example
172 ///
173 /// ```
174 /// let index = h3o::DirectedEdgeIndex::try_from(0x13a194e699ab7fff)?;
175 /// let boundary = index.boundary();
176 /// # Ok::<(), Box<dyn std::error::Error>>(())
177 /// ```
178 #[must_use]
179 pub fn boundary(self) -> Boundary {
180 // Get the origin and neighbor direction from the edge.
181 let direction = Direction::from(self.edge());
182 let origin = self.origin();
183
184 // Get the start vertex for the edge.
185 let start_vertex = direction.vertex(origin);
186
187 // Get the geo boundary for the appropriate vertexes of the origin. Note
188 // that while there are always 2 topological vertexes per edge, the
189 // resulting edge boundary may have an additional distortion vertex if
190 // it crosses an edge of the icosahedron.
191 let fijk = FaceIJK::from(origin);
192 let resolution = origin.resolution();
193 if origin.is_pentagon() {
194 fijk.pentagon_boundary(resolution, start_vertex, 2)
195 } else {
196 fijk.hexagon_boundary(resolution, start_vertex, 2)
197 }
198 }
199
200 /// Computes the length of this directed edge, in radians.
201 ///
202 /// # Example
203 ///
204 /// ```
205 /// let index = h3o::DirectedEdgeIndex::try_from(0x13a194e699ab7fff)?;
206 /// float_eq::assert_float_eq!(
207 /// index.length_rads(),
208 /// 1.1795418098325597e-5,
209 /// abs <= 1e-11
210 /// );
211 /// # Ok::<(), Box<dyn std::error::Error>>(())
212 /// ```
213 #[must_use]
214 pub fn length_rads(self) -> f64 {
215 let boundary = self.boundary();
216
217 (0..boundary.len() - 1)
218 .map(|i| boundary[i].distance_rads(boundary[i + 1]))
219 .sum()
220 }
221
222 /// Computes the length of this directed edge, in kilometers.
223 ///
224 /// # Example
225 ///
226 /// ```
227 /// let index = h3o::DirectedEdgeIndex::try_from(0x13a194e699ab7fff)?;
228 /// float_eq::assert_float_eq!(
229 /// index.length_km(),
230 /// 0.07514869340636812,
231 /// abs <= 1e-11
232 /// );
233 /// # Ok::<(), Box<dyn std::error::Error>>(())
234 /// ```
235 #[must_use]
236 pub fn length_km(self) -> f64 {
237 self.length_rads() * EARTH_RADIUS_KM
238 }
239
240 /// Computes the length of this directed edge, in meters.
241 ///
242 /// # Example
243 ///
244 /// ```
245 /// let index = h3o::DirectedEdgeIndex::try_from(0x13a194e699ab7fff)?;
246 /// float_eq::assert_float_eq!(
247 /// index.length_m(),
248 /// 75.14869340636812,
249 /// abs <= 1e-8
250 /// );
251 /// # Ok::<(), Box<dyn std::error::Error>>(())
252 /// ```
253 #[must_use]
254 pub fn length_m(self) -> f64 {
255 self.length_km() * 1000.
256 }
257
258 /// Initializes a new edge index a value that may be invalid.
259 ///
260 /// # Safety
261 ///
262 /// The value must be a valid edge index.
263 pub(crate) fn new_unchecked(value: u64) -> Self {
264 debug_assert!(Self::try_from(value).is_ok(), "invalid edge index");
265 Self(NonZeroU64::new(value).expect("valid edge index"))
266 }
267}
268
269impl Ord for DirectedEdgeIndex {
270 fn cmp(&self, other: &Self) -> Ordering {
271 /// Bitmask to hide the resolution and edge.
272 const MASK: u64 = 0xf80f_ffff_ffff_ffff;
273
274 // Order by index first, then by edge.
275 (self.0.get() & MASK, self.edge())
276 .cmp(&(other.0.get() & MASK, other.edge()))
277 }
278}
279
280impl PartialOrd for DirectedEdgeIndex {
281 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
282 Some(self.cmp(other))
283 }
284}
285
286impl From<DirectedEdgeIndex> for u64 {
287 fn from(value: DirectedEdgeIndex) -> Self {
288 value.0.get()
289 }
290}
291
292impl TryFrom<u64> for DirectedEdgeIndex {
293 type Error = error::InvalidDirectedEdgeIndex;
294
295 fn try_from(value: u64) -> Result<Self, Self::Error> {
296 if bits::get_mode(value) != u8::from(IndexMode::DirectedEdge) {
297 return Err(Self::Error::new(Some(value), "invalid index mode"));
298 }
299
300 // Clear the highest byte and validate the index part.
301 let bits = bits::set_mode(value, IndexMode::Cell);
302 let bits = bits::clr_edge(bits);
303 CellIndex::try_from(bits)
304 .map_err(|err| Self::Error::new(Some(value), err.reason))?;
305
306 // An hexagon has 6 edges (1-6), while a pentagon only has 5 (2-6).
307 let min_edge =
308 1 + u8::from(CellIndex::new_unchecked(bits).is_pentagon());
309 if !(min_edge..=MAX).contains(&bits::get_edge(value)) {
310 return Err(Self::Error::new(Some(value), "invalid cell edge"));
311 }
312
313 // XXX: 0 is rejected by the mode check (mode cannot be 0).
314 Ok(Self(NonZeroU64::new(value).expect("non-zero edge index")))
315 }
316}
317
318impl FromStr for DirectedEdgeIndex {
319 type Err = error::InvalidDirectedEdgeIndex;
320
321 fn from_str(s: &str) -> Result<Self, Self::Err> {
322 u64::from_str_radix(s, 16)
323 .map_err(|_| Self::Err {
324 value: None,
325 reason: "invalid 64-bit hex number",
326 })
327 .and_then(Self::try_from)
328 }
329}
330
331impl fmt::Debug for DirectedEdgeIndex {
332 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333 write!(
334 f,
335 "{}-{:015o}_{} ({})",
336 self.origin().base_cell(),
337 u64::from(*self) & bits::DIRECTIONS_MASK,
338 self.edge(),
339 self
340 )
341 }
342}
343
344impl fmt::Display for DirectedEdgeIndex {
345 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
346 write!(f, "{self:x}")
347 }
348}
349
350impl fmt::Binary for DirectedEdgeIndex {
351 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
352 fmt::Binary::fmt(&self.0, f)
353 }
354}
355
356impl fmt::Octal for DirectedEdgeIndex {
357 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358 fmt::Octal::fmt(&self.0, f)
359 }
360}
361
362impl fmt::LowerHex for DirectedEdgeIndex {
363 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364 fmt::LowerHex::fmt(&self.0, f)
365 }
366}
367
368impl fmt::UpperHex for DirectedEdgeIndex {
369 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
370 fmt::UpperHex::fmt(&self.0, f)
371 }
372}
373
374#[cfg(feature = "geo")]
375impl From<DirectedEdgeIndex> for geo::Line {
376 fn from(value: DirectedEdgeIndex) -> Self {
377 let coords: Vec<geo::Coord> =
378 value.boundary().iter().copied().map(Into::into).collect();
379
380 // We only have two point (start and end) as boundary for an edge.
381 assert_eq!(coords.len(), 2);
382
383 Self::new(coords[0], coords[1])
384 }
385}
386
387#[cfg(feature = "arbitrary")]
388impl<'a> arbitrary::Arbitrary<'a> for DirectedEdgeIndex {
389 fn arbitrary(
390 data: &mut arbitrary::Unstructured<'a>,
391 ) -> arbitrary::Result<Self> {
392 u64::arbitrary(data).and_then(|byte| {
393 Self::try_from(byte).map_err(|_| arbitrary::Error::IncorrectFormat)
394 })
395 }
396}
397
398#[cfg(test)]
399#[path = "./edge_tests.rs"]
400mod tests;