use geo_types::Coord;
use crate::encoder::model::CurveParams;
#[must_use]
pub fn hilbert_xy_to_index(level: u32, coord: Coord<u32>) -> u32 {
debug_assert!((1..=16).contains(&level), "level must be in [1, 16]");
debug_assert!(coord.x < (1 << level), "x out of range for level");
debug_assert!(coord.y < (1 << level), "y out of range for level");
hilbert_2d::u32::xy2h_discrete(coord.x, coord.y, level, hilbert_2d::Variant::Hilbert)
}
#[must_use]
pub fn hilbert_sort_key(c: Coord<i32>, params: CurveParams) -> u32 {
debug_assert!((1..=16).contains(¶ms.bits));
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "shift brings value into [0, extent]; masked to 16 bits immediately after"
)]
let sx = ((i64::from(c.x) + i64::from(params.shift)) as u32) & 0xFFFF;
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "shift brings value into [0, extent]; masked to 16 bits immediately after"
)]
let sy = ((i64::from(c.y) + i64::from(params.shift)) as u32) & 0xFFFF;
hilbert_xy_to_index(params.bits, (sx, sy).into())
}
#[must_use]
pub fn hilbert_curve_params_from_bounds(min_val: i32, max_val: i32) -> CurveParams {
if min_val > max_val {
return CurveParams { shift: 0, bits: 1 };
}
let shift: u32 = if min_val < 0 {
min_val.unsigned_abs()
} else {
0
};
let extent = (i64::from(max_val) + i64::from(shift)).unsigned_abs();
let bits = if extent == 0 {
1
} else {
(u64::BITS - extent.leading_zeros()).min(16)
};
CurveParams { shift, bits }
}
#[cfg(test)]
mod tests {
use usize_cast::IntoUsize as _;
use super::*;
use crate::codecs::morton::interleave_bits;
const fn c(x: i32, y: i32) -> Coord<i32> {
Coord::<i32> { x, y }
}
const fn p(shift: u32, bits: u32) -> CurveParams {
CurveParams { shift, bits }
}
fn hilbert_position_to_xy(level: u32, pos: u32) -> Coord<u32> {
debug_assert!((1..=16).contains(&level), "level must be in [1, 16]");
debug_assert!(u64::from(pos) < (1u64 << (2 * level)), "pos out of range");
hilbert_2d::u32::h2xy_discrete(pos, level, hilbert_2d::Variant::Hilbert).into()
}
#[test]
fn hilbert_origin_always_zero() {
for level in 1u32..=8 {
let idx = hilbert_xy_to_index(level, (0, 0).into());
assert_eq!(idx, 0, "origin should be 0 at level {level}");
}
}
#[test]
fn hilbert_round_trip_level1() {
for x in 0u32..2 {
for y in 0u32..2 {
let idx = hilbert_xy_to_index(1, (x, y).into());
let (rx, ry) = hilbert_position_to_xy(1, idx).into();
assert_eq!((rx, ry), (x, y), "round-trip failed at level=1 ({x},{y})");
}
}
}
#[test]
fn hilbert_round_trip_level2() {
for x in 0u32..4 {
for y in 0u32..4 {
let idx = hilbert_xy_to_index(2, (x, y).into());
let (rx, ry) = hilbert_position_to_xy(2, idx).into();
assert_eq!((rx, ry), (x, y), "round-trip failed at level=2 ({x},{y})");
}
}
}
#[test]
fn hilbert_round_trip_level4() {
for x in 0u32..16 {
for y in 0u32..16 {
let idx = hilbert_xy_to_index(4, (x, y).into());
let (rx, ry) = hilbert_position_to_xy(4, idx).into();
assert_eq!((rx, ry), (x, y), "round-trip failed at level=4 ({x},{y})");
}
}
}
#[test]
fn hilbert_indices_are_a_bijection_at_level2() {
let mut seen = [false; 16];
for x in 0u32..4 {
for y in 0u32..4 {
let idx = hilbert_xy_to_index(2, (x, y).into()).into_usize();
assert!(!seen[idx], "duplicate index {idx} at ({x},{y})");
seen[idx] = true;
}
}
assert!(seen.iter().all(|&v| v), "some index was never produced");
}
#[test]
fn hilbert_indices_are_a_bijection_at_level4() {
let mut seen = vec![false; 256];
for x in 0u32..16 {
for y in 0u32..16 {
let idx = hilbert_xy_to_index(4, (x, y).into()).into_usize();
assert!(!seen[idx], "duplicate index {idx} at ({x},{y})");
seen[idx] = true;
}
}
assert!(seen.iter().all(|&v| v));
}
#[test]
fn hilbert_level1_covers_indices_0_to_3() {
let mut indices: Vec<u32> = (0u32..2)
.flat_map(|x| (0u32..2).map(move |y| hilbert_xy_to_index(1, (x, y).into())))
.collect();
indices.sort_unstable();
assert_eq!(indices, [0, 1, 2, 3]);
}
#[test]
fn hilbert_sort_key_origin_zero() {
assert_eq!(hilbert_sort_key(c(0, 0), p(0, 1)), 0);
}
#[test]
fn hilbert_sort_key_negative_coords_shift_correctly() {
assert_eq!(hilbert_sort_key(c(-1, -1), p(1, 1)), 0);
}
#[test]
fn hilbert_sort_key_matches_xy_to_index() {
let shift = 5u32;
let bits = 4u32;
for raw_x in -5i32..11 {
for raw_y in -5i32..11 {
let expected = hilbert_xy_to_index(
bits,
(
u32::try_from(i64::from(raw_x) + i64::from(shift)).unwrap(),
u32::try_from(i64::from(raw_y) + i64::from(shift)).unwrap(),
)
.into(),
);
let actual = hilbert_sort_key(c(raw_x, raw_y), p(shift, bits));
assert_eq!(actual, expected, "mismatch at ({raw_x},{raw_y})");
}
}
}
#[test]
fn curve_params_empty_bounds() {
let CurveParams { shift, bits } = hilbert_curve_params_from_bounds(i32::MAX, i32::MIN);
assert_eq!(shift, 0);
assert_eq!(bits, 1);
}
#[test]
fn curve_params_all_zero() {
let CurveParams { shift, bits } = hilbert_curve_params_from_bounds(0, 0);
assert_eq!(shift, 0);
assert_eq!(bits, 1);
}
#[test]
fn curve_params_positive_only() {
let CurveParams { shift, bits } = hilbert_curve_params_from_bounds(0, 3);
assert_eq!(shift, 0);
assert_eq!(bits, 2);
}
#[test]
fn curve_params_negative_min() {
let CurveParams { shift, bits } = hilbert_curve_params_from_bounds(-4, 4);
assert_eq!(shift, 4);
assert_eq!(bits, 4);
}
#[test]
fn curve_params_power_of_two_extent() {
let CurveParams { shift, bits } = hilbert_curve_params_from_bounds(0, 8);
assert_eq!(shift, 0);
assert_eq!(bits, 4);
}
#[test]
fn curve_params_single_axis_negative() {
let CurveParams { shift, bits } = hilbert_curve_params_from_bounds(-2, 5);
assert_eq!(shift, 2);
assert_eq!(bits, 3);
}
#[test]
fn curve_params_clamped_at_16_bits() {
let CurveParams { shift, bits } = hilbert_curve_params_from_bounds(0, 65535);
assert_eq!(shift, 0);
assert_eq!(bits, 16);
}
#[test]
fn hilbert_and_morton_both_sort_nearby_points_close_together() {
let points: Vec<Coord<u32>> = (0u32..8)
.flat_map(|x| (0u32..8).map(move |y| (x, y).into()))
.collect();
let h_keys: Vec<u32> = points.iter().map(|&c| hilbert_xy_to_index(3, c)).collect();
let h_max = h_keys.iter().copied().max().unwrap();
let m_keys: Vec<u32> = points.iter().map(|&c| interleave_bits(c)).collect();
let m_max = m_keys.iter().copied().max().unwrap();
assert_eq!(h_max, 63, "Hilbert should produce indices 0..63 for 8×8");
assert_eq!(m_max, interleave_bits((7, 7).into()));
let h_00 = hilbert_xy_to_index(3, (0, 0).into());
let h_10 = hilbert_xy_to_index(3, (1, 0).into());
assert!(
h_00.abs_diff(h_10) <= 4,
"adjacent points should have close Hilbert indices"
);
}
}