use super::{CoordIJ, CoordIJK};
use crate::{
BaseCell, CCW, CW, CellIndex, DEFAULT_CELL_INDEX, Direction, Resolution,
error::{HexGridError, LocalIjError},
index::bits,
};
use core::{fmt, num::NonZeroU8};
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct LocalIJK {
pub anchor: CellIndex,
pub coord: CoordIJK,
}
impl LocalIJK {
pub const fn coord(&self) -> &CoordIJK {
&self.coord
}
}
impl TryFrom<LocalIJK> for CellIndex {
type Error = LocalIjError;
fn try_from(value: LocalIJK) -> Result<Self, Self::Error> {
let resolution = value.anchor.resolution();
let origin_base_cell = value.anchor.base_cell();
let origin_on_pent = origin_base_cell.is_pentagon();
let mut bits = bits::set_resolution(DEFAULT_CELL_INDEX, resolution);
if resolution == Resolution::Zero {
let dir = Direction::try_from(value.coord)?;
let new_base_cell = origin_base_cell
.neighbor(dir)
.ok_or(Self::Error::Pentagon)?;
return Ok(Self::new_unchecked(h3o_bit::set_base_cell(
bits,
new_base_cell.into(),
)));
}
let ijk = checked_directions_bits_from_ijk(
value.coord,
&mut bits,
resolution,
)
.ok_or_else(|| HexGridError::new("IJ coordinates overflow"))?;
let mut dir = Direction::try_from(ijk)?;
let mut base_cell = origin_base_cell.neighbor(dir);
let index_on_pent = base_cell.is_some_and(BaseCell::is_pentagon);
if dir != Direction::Center {
let mut pentagon_rotations = 0;
if origin_on_pent {
let leading_direction = bits::first_axe(value.anchor.into())
.map_or_else(|| 0, NonZeroU8::get);
pentagon_rotations = PENTAGON_ROTATIONS_REVERSE
[usize::from(leading_direction)][usize::from(dir)];
assert_ne!(pentagon_rotations, 0xff);
dir = dir.rotate60::<CCW>(pentagon_rotations.into());
let fixed_base_cell = origin_base_cell
.neighbor(dir)
.ok_or(Self::Error::Pentagon)?;
base_cell = Some(fixed_base_cell);
debug_assert!(!fixed_base_cell.is_pentagon());
}
let fixed_base_cell = base_cell.expect("fixed base cell");
let base_cell_rotations = origin_base_cell.neighbor_rotation(dir);
if index_on_pent {
let rev_dir = usize::from(
fixed_base_cell
.direction(origin_base_cell)
.expect("reverse direction"),
);
bits = bits::rotate60::<CCW>(bits, base_cell_rotations.into());
let leading_direction = usize::from(
bits::first_axe(bits).map_or_else(|| 0, NonZeroU8::get),
);
let pentagon_rotations = if fixed_base_cell.is_polar_pentagon()
{
PENTAGON_ROTATIONS_REVERSE_POLAR[rev_dir][leading_direction]
} else {
PENTAGON_ROTATIONS_REVERSE_NONPOLAR[rev_dir]
[leading_direction]
};
assert_ne!(pentagon_rotations, 0xff);
bits = (0..pentagon_rotations)
.fold(bits, |acc, _| bits::pentagon_rotate60::<CCW>(acc));
} else {
assert!(pentagon_rotations != 0xff);
let count =
usize::from(pentagon_rotations + base_cell_rotations);
bits = bits::rotate60::<CCW>(bits, count);
}
} else if origin_on_pent && index_on_pent {
let origin_leading_dir = usize::from(
bits::first_axe(value.anchor.into())
.map_or_else(|| 0, NonZeroU8::get),
);
let index_leading_dir = usize::from(
bits::first_axe(bits).map_or_else(|| 0, NonZeroU8::get),
);
let rotations = PENTAGON_ROTATIONS_REVERSE[origin_leading_dir]
[index_leading_dir];
assert!(rotations != 0xff, "invalid K axis digit");
bits = bits::rotate60::<CCW>(bits, rotations.into());
}
if index_on_pent {
if bits::first_axe(bits) == Direction::K.axe() {
return Err(Self::Error::Pentagon);
}
}
let base_cell = base_cell
.ok_or_else(|| HexGridError::new("cannot resolve base cell"))?;
Ok(Self::new_unchecked(h3o_bit::set_base_cell(
bits,
base_cell.into(),
)))
}
}
#[expect(
clippy::inline_always,
reason = "4-5% boost, up to 13% at resolution 1"
)]
#[inline(always)]
pub fn checked_directions_bits_from_ijk(
mut ijk: CoordIJK,
bits: &mut u64,
resolution: Resolution,
) -> Option<CoordIJK> {
for res in Resolution::range(Resolution::One, resolution).rev() {
let last_ijk = ijk;
let last_center = if res.is_class3() {
ijk = ijk.checked_up_aperture7::<{ CCW }>()?;
ijk.down_aperture7::<{ CCW }>()
} else {
ijk = ijk.checked_up_aperture7::<{ CW }>()?;
ijk.down_aperture7::<{ CW }>()
};
let diff = (last_ijk - last_center).normalize();
let direction = Direction::try_from(diff).expect("unit IJK coordinate");
*bits = bits::set_direction(*bits, direction.into(), res);
}
Some(ijk)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LocalIJ {
pub anchor: CellIndex,
pub coord: CoordIJ,
}
impl LocalIJ {
#[must_use]
pub const fn new(anchor: CellIndex, coord: CoordIJ) -> Self {
Self { anchor, coord }
}
}
impl TryFrom<LocalIJ> for CellIndex {
type Error = LocalIjError;
fn try_from(value: LocalIJ) -> Result<Self, Self::Error> {
let local_ijk = LocalIJK {
anchor: value.anchor,
coord: CoordIJK::try_from(value.coord)?,
};
Self::try_from(local_ijk)
}
}
impl fmt::Display for LocalIJ {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.anchor, self.coord)
}
}
#[rustfmt::skip]
const PENTAGON_ROTATIONS_REVERSE: [[u8; 7]; 7] = [
[ 0, 0, 0, 0, 0, 0, 0], [ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], [ 0, 1, 0, 0, 0, 0, 0], [ 0, 1, 0, 0, 0, 1, 0], [ 0, 5, 0, 0, 0, 0, 0], [ 0, 5, 0, 5, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0], ];
#[rustfmt::skip]
const PENTAGON_ROTATIONS_REVERSE_NONPOLAR: [[u8; 7]; 7] = [
[ 0, 0, 0, 0, 0, 0, 0], [ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], [ 0, 1, 0, 0, 0, 0, 0], [ 0, 1, 0, 0, 0, 1, 0], [ 0, 5, 0, 0, 0, 0, 0], [ 0, 1, 0, 5, 1, 1, 0], [ 0, 0, 0, 0, 0, 0, 0], ];
#[rustfmt::skip]
const PENTAGON_ROTATIONS_REVERSE_POLAR: [[u8; 7]; 7] = [
[ 0, 0, 0, 0, 0, 0, 0], [ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], [ 0, 1, 1, 1, 1, 1, 1], [ 0, 1, 0, 0, 0, 1, 0], [ 0, 1, 0, 0, 1, 1, 1], [ 0, 1, 0, 5, 1, 1, 0], [ 0, 1, 1, 0, 1, 1, 1], ];
#[cfg(test)]
#[path = "./localij_tests.rs"]
mod tests;