use std::fmt;
use std::str::FromStr;
pub const CURVE_BITS_CAP: u32 = 21;
pub const ORDERING_BALANCE_BITS: u32 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BlobOrdering {
SpatialMajor,
TimeMajor,
#[default]
Hilbert3,
Morton3,
Auto,
}
impl BlobOrdering {
pub fn choose(space_bits: u32, time_bits: u32) -> BlobOrdering {
if time_bits <= 1 {
BlobOrdering::SpatialMajor
} else if time_bits > space_bits + ORDERING_BALANCE_BITS {
BlobOrdering::SpatialMajor
} else {
BlobOrdering::Hilbert3
}
}
}
impl BlobOrdering {
pub fn as_str(self) -> &'static str {
match self {
BlobOrdering::SpatialMajor => "spatial",
BlobOrdering::TimeMajor => "time-major",
BlobOrdering::Hilbert3 => "hilbert3",
BlobOrdering::Morton3 => "morton3",
BlobOrdering::Auto => "auto",
}
}
}
impl fmt::Display for BlobOrdering {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for BlobOrdering {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"spatial" | "spatial-major" | "hilbert" | "hilbert2" => Ok(BlobOrdering::SpatialMajor),
"time" | "time-major" | "timemajor" => Ok(BlobOrdering::TimeMajor),
"hilbert3" | "h3" | "3d-hilbert" => Ok(BlobOrdering::Hilbert3),
"morton3" | "morton" | "z-order" | "zorder" => Ok(BlobOrdering::Morton3),
"auto" => Ok(BlobOrdering::Auto),
other => Err(format!(
"unknown blob ordering '{other}' (want: spatial|time-major|hilbert3|morton3|auto)"
)),
}
}
}
pub fn bits_for(n: u64) -> u32 {
if n <= 1 {
0
} else {
64 - (n - 1).leading_zeros()
}
}
pub fn morton3(x: u32, y: u32, t: u32, bits: u32) -> u64 {
if bits == 0 {
return 0; }
let mut key: u64 = 0;
for i in 0..bits {
key |= (((x >> i) & 1) as u64) << (3 * i)
| (((y >> i) & 1) as u64) << (3 * i + 1)
| (((t >> i) & 1) as u64) << (3 * i + 2);
}
key
}
pub fn hilbert3(x: u32, y: u32, t: u32, bits: u32) -> u64 {
if bits == 0 {
return 0; }
let n = 3usize;
let mut coords = [x, y, t];
let mut q: u32 = 1 << (bits - 1);
while q > 1 {
let p = q - 1;
for i in 0..n {
if coords[i] & q != 0 {
coords[0] ^= p; } else {
let s = (coords[0] ^ coords[i]) & p;
coords[0] ^= s;
coords[i] ^= s; }
}
q >>= 1;
}
for i in 1..n {
coords[i] ^= coords[i - 1];
}
let mut t_acc: u32 = 0;
let mut q2: u32 = 1 << (bits - 1);
while q2 > 1 {
if coords[n - 1] & q2 != 0 {
t_acc ^= q2 - 1;
}
q2 >>= 1;
}
for c in coords.iter_mut() {
*c ^= t_acc;
}
let mut key: u64 = 0;
let mut bit = bits;
while bit > 0 {
bit -= 1;
for i in 0..n {
key = (key << 1) | (((coords[i] >> bit) & 1) as u64);
}
}
key
}
pub fn curve_bits(zoom: u8, tb_span: i64) -> u32 {
let tbits = bits_for((tb_span.max(0) + 1) as u64);
(zoom as u32).max(tbits).min(CURVE_BITS_CAP)
}
fn scale_axes(zoom: u8, x: u32, y: u32, tb: i64, tb_min: i64, bits: u32) -> (u32, u32, u32) {
let sdrop = (zoom as u32).saturating_sub(bits);
let xs = x.checked_shr(sdrop).unwrap_or(0);
let ys = y.checked_shr(sdrop).unwrap_or(0);
let rel = (tb - tb_min).max(0) as u64;
let tbits = bits_for(rel + 1);
let tdrop = tbits.saturating_sub(bits);
let qt = rel.checked_shr(tdrop).unwrap_or(0) as u32;
(xs, ys, qt)
}
#[allow(clippy::too_many_arguments)]
pub fn space_time_key(
ordering: BlobOrdering,
zoom: u8,
x: u32,
y: u32,
hilbert: u64,
time_start: i64,
tb: i64,
tb_min: i64,
tb_span: i64,
) -> (u8, u64, u64, u64) {
match ordering {
BlobOrdering::SpatialMajor => (zoom, hilbert, time_start.max(0) as u64, 0),
BlobOrdering::TimeMajor => (zoom, tb.max(0) as u64, hilbert, 0),
BlobOrdering::Hilbert3 => {
let bits = curve_bits(zoom, tb_span);
let (xs, ys, qt) = scale_axes(zoom, x, y, tb, tb_min, bits);
(zoom, hilbert3(xs, ys, qt, bits), 0, 0)
}
BlobOrdering::Morton3 => {
let bits = curve_bits(zoom, tb_span);
let (xs, ys, qt) = scale_axes(zoom, x, y, tb, tb_min, bits);
(zoom, morton3(xs, ys, qt, bits), 0, 0)
}
BlobOrdering::Auto => {
let bits = curve_bits(zoom, tb_span);
let (xs, ys, qt) = scale_axes(zoom, x, y, tb, tb_min, bits);
(zoom, hilbert3(xs, ys, qt, bits), 0, 0)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn morton3_roundtrip() {
fn de(key: u64, bits: u32) -> (u32, u32, u32) {
let (mut x, mut y, mut t) = (0u32, 0u32, 0u32);
for i in 0..bits {
x |= (((key >> (3 * i)) & 1) as u32) << i;
y |= (((key >> (3 * i + 1)) & 1) as u32) << i;
t |= (((key >> (3 * i + 2)) & 1) as u32) << i;
}
(x, y, t)
}
for &(x, y, t) in &[(0u32, 0u32, 0u32), (1, 2, 3), (7, 7, 7), (5, 0, 6)] {
assert_eq!(de(morton3(x, y, t, 3), 3), (x, y, t));
}
}
#[test]
fn hilbert3_is_bijection() {
let mut seen = vec![false; 512];
for x in 0..8 {
for y in 0..8 {
for t in 0..8 {
let h = hilbert3(x, y, t, 3) as usize;
assert!(h < 512, "index {h} out of range");
assert!(!seen[h], "collision at {h}");
seen[h] = true;
}
}
}
assert!(seen.iter().all(|&b| b));
}
#[test]
fn hilbert3_locality() {
let bits = 4u32;
let n = 1u32 << bits;
let mut pts: Vec<(u64, (u32, u32, u32))> = Vec::new();
for x in 0..n {
for y in 0..n {
for t in 0..n {
pts.push((hilbert3(x, y, t, bits), (x, y, t)));
}
}
}
pts.sort_by_key(|p| p.0);
for w in pts.windows(2) {
let (a, b) = (w[0].1, w[1].1);
let d = (a.0 as i64 - b.0 as i64).abs()
+ (a.1 as i64 - b.1 as i64).abs()
+ (a.2 as i64 - b.2 as i64).abs();
assert_eq!(d, 1, "consecutive Hilbert cells must be adjacent (got {d})");
}
}
#[test]
fn curve_bits_balances_native_resolution() {
assert_eq!(curve_bits(10, 23), 10);
assert_eq!(bits_for(24), 5);
assert_eq!(curve_bits(4, 2280), 12);
assert_eq!(bits_for(2281), 12);
}
#[test]
fn scale_axes_keeps_native_ranges() {
let bits = curve_bits(10, 23);
let (_, _, qt) = scale_axes(10, 512, 300, 10, 0, bits);
assert!(qt < (1 << bits_for(24)), "time axis must stay native ~5-bit, got {qt}");
let (xs, ys, _) = scale_axes(10, 777, 123, 0, 0, bits);
assert_eq!((xs, ys), (777, 123));
}
#[test]
fn bits_zero_is_safe() {
assert_eq!(curve_bits(0, 0), 0);
assert_eq!(hilbert3(0, 0, 0, 0), 0);
assert_eq!(morton3(0, 0, 0, 0), 0);
let k = space_time_key(BlobOrdering::Hilbert3, 0, 0, 0, 0, 0, 0, 0, 0);
assert_eq!(k, (0, 0, 0, 0));
let _ = space_time_key(BlobOrdering::Hilbert3, 60, 1 << 30, 1 << 30, 0, 0, 0, 0, 0);
}
#[test]
fn ordering_parse_roundtrip() {
for o in [
BlobOrdering::SpatialMajor,
BlobOrdering::TimeMajor,
BlobOrdering::Hilbert3,
BlobOrdering::Morton3,
BlobOrdering::Auto,
] {
assert_eq!(o.as_str().parse::<BlobOrdering>().unwrap(), o);
}
assert!("garbage".parse::<BlobOrdering>().is_err());
assert_eq!(BlobOrdering::default(), BlobOrdering::Hilbert3);
}
#[test]
fn choose_matches_measured_winners() {
assert_eq!(
BlobOrdering::choose(10, bits_for(24)),
BlobOrdering::Hilbert3
);
assert_eq!(
BlobOrdering::choose(4, bits_for(2281)),
BlobOrdering::SpatialMajor
);
assert_eq!(
BlobOrdering::choose(10, 10 + ORDERING_BALANCE_BITS),
BlobOrdering::Hilbert3
);
assert_eq!(
BlobOrdering::choose(10, 10 + ORDERING_BALANCE_BITS + 1),
BlobOrdering::SpatialMajor
);
assert_eq!(BlobOrdering::choose(10, 0), BlobOrdering::SpatialMajor);
assert_eq!(BlobOrdering::choose(10, 1), BlobOrdering::SpatialMajor);
}
#[test]
fn keys_distinguish_orderings() {
let k0 = space_time_key(BlobOrdering::SpatialMajor, 10, 5, 5, 42, 0, 0, 0, 100);
let k1 = space_time_key(BlobOrdering::SpatialMajor, 10, 5, 5, 42, 1, 1, 0, 100);
assert_eq!(k0.0, k1.0);
assert_eq!(k0.1, k1.1); let t0 = space_time_key(BlobOrdering::TimeMajor, 10, 5, 5, 42, 0, 0, 0, 100);
let t1 = space_time_key(BlobOrdering::TimeMajor, 10, 5, 5, 42, 1, 1, 0, 100);
assert_ne!(t0.1, t1.1); }
}