use crate::{TileBBox, TileCoord};
use anyhow::{Result, bail};
pub trait HilbertIndex {
fn get_hilbert_index(&self) -> Result<u64>;
fn from_hilbert_index(index: u64) -> Result<Self>
where
Self: Sized;
}
impl HilbertIndex for TileBBox {
fn get_hilbert_index(&self) -> Result<u64> {
coord_to_index(self.level, self.x_min()?, self.y_min()?)
}
fn from_hilbert_index(index: u64) -> Result<Self> {
let coord = index_to_coord(index)?;
TileBBox::from_min_and_max(coord.level, coord.x, coord.y, coord.x, coord.y)
}
}
impl HilbertIndex for TileCoord {
fn get_hilbert_index(&self) -> Result<u64> {
coord_to_index(self.level, self.x, self.y)
}
fn from_hilbert_index(index: u64) -> Result<Self> {
index_to_coord(index)
}
}
fn coord_to_index(level: u8, x: u32, y: u32) -> Result<u64> {
let x = i64::from(x);
let y = i64::from(y);
let level = i64::from(level);
if level >= 32 {
bail!("tile zoom exceeds 64-bit limit");
}
let size = 1i64 << level;
if x >= size || y >= size {
bail!("tile x/y outside zoom level bounds");
}
let mut acc = 0i64;
for t_z in 0..level {
acc += 1i64 << (t_z * 2);
}
let mut tx = x;
let mut ty = y;
let mut d = 0i64;
let mut s = size / 2;
while s > 0 {
let rx = i64::from((tx & s) > 0);
let ry = i64::from((ty & s) > 0);
d += s * s * ((3 * rx) ^ ry);
rotate(s, &mut tx, &mut ty, rx, ry);
s /= 2;
}
Ok(u64::try_from(acc + d)?)
}
#[doc(hidden)]
fn rotate(s: i64, tx: &mut i64, ty: &mut i64, rx: i64, ry: i64) {
if ry == 0 {
if rx == 1 {
*tx = s - 1 - *tx;
*ty = s - 1 - *ty;
}
std::mem::swap(tx, ty);
}
}
fn index_to_coord(index: u64) -> Result<TileCoord> {
let index = i64::try_from(index)?;
let mut acc = 0;
for t_z in 0..32 {
let num_tiles = (1 << t_z) * (1 << t_z);
if acc + num_tiles > index {
let n = 1 << t_z;
let mut t = index - acc;
let mut tx = 0i64;
let mut ty = 0i64;
let mut s = 1i64;
while s < n {
let rx = (t / 2) & 1;
let ry = (t ^ rx) & 1;
rotate(s, &mut tx, &mut ty, rx, ry);
if rx == 1 {
tx += s;
}
if ry == 1 {
ty += s;
}
t /= 4;
s *= 2;
}
return TileCoord::new(t_z, u32::try_from(tx)?, u32::try_from(ty)?);
}
acc += num_tiles;
}
bail!("tile zoom exceeds 64-bit limit".to_string())
}
#[cfg(test)]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
mod tests {
use super::*;
use crate::MAX_ZOOM_LEVEL;
#[test]
fn test_coord_to_tile_id_basic_inputs() -> Result<()> {
assert_eq!(coord_to_index(1, 1, 1)?, 3);
assert_eq!(coord_to_index(0, 0, 0)?, 0);
assert_eq!(coord_to_index(2, 2, 2)?, 13);
assert_eq!(coord_to_index(3, 5, 3)?, 73);
assert_eq!(coord_to_index(3, 7, 7)?, 63);
assert_eq!(coord_to_index(31, 0, 0)?, 1537228672809129301);
assert_eq!(coord_to_index(31, (1 << 31) - 1, (1 << 31) - 1)?, 4611686018427387903);
Ok(())
}
#[test]
fn test_coord_to_tile_id_invalid_zoom() {
assert_eq!(
coord_to_index(32, 1, 1).unwrap_err().to_string(),
"tile zoom exceeds 64-bit limit"
);
}
#[test]
fn test_coord_to_tile_id_out_of_bounds() {
assert_eq!(
coord_to_index(0, 1, 0).unwrap_err().to_string(),
"tile x/y outside zoom level bounds"
);
}
#[test]
fn test_tile_id_to_coord() -> Result<()> {
let mut f = 0f64;
loop {
let id0 = f as u64;
let coord = index_to_coord(id0).unwrap();
let id1 = coord_to_index(coord.level, coord.x, coord.y)?;
assert_eq!(id0, id1);
if coord.level >= 30 {
break;
}
f = f * 1.1 + 1.0;
}
Ok(())
}
#[test]
fn test_tile_id_to_coord_edge_cases() -> Result<()> {
assert_eq!(index_to_coord(0)?, TileCoord::new(0, 0, 0)?);
assert_eq!(coord_to_index(0, 0, 0)?, 0);
let z = MAX_ZOOM_LEVEL;
let max_index = (1..=u32::from(z)).map(|l| 4u64.pow(l)).sum::<u64>();
let max_n = (1 << z) - 1;
assert_eq!(index_to_coord(max_index)?, TileCoord::new(z, max_n, 0)?);
assert_eq!(coord_to_index(z, max_n, 0)?, max_index);
assert!(index_to_coord(max_index + 1).is_err());
Ok(())
}
#[test]
fn test_tile_id_to_coord_invalid_index() {
assert_eq!(
index_to_coord(u64::MAX / 2).unwrap_err().to_string(),
"tile zoom exceeds 64-bit limit"
);
}
#[test]
fn test_hilbert_index_trait_tile_bbox() -> Result<()> {
let bbox = TileBBox::from_min_and_max(3, 5, 3, 5, 3)?;
let index = bbox.get_hilbert_index()?;
let reconstructed_bbox = TileBBox::from_hilbert_index(index)?;
assert_eq!(bbox, reconstructed_bbox);
Ok(())
}
#[test]
fn test_hilbert_index_trait_tile_coord() -> Result<()> {
let coord = TileCoord::new(5, 3, 3)?;
let index = coord.get_hilbert_index()?;
let reconstructed_coord = TileCoord::from_hilbert_index(index)?;
assert_eq!(coord, reconstructed_coord);
Ok(())
}
#[test]
fn test_tile_id_to_coord_random() -> Result<()> {
fn pseudo_random(r: &mut f64) -> f64 {
*r = ((*r * 2000.0 + 0.2).sin() + 1.1) * 1000.0 % 1.0;
*r
}
let mut r = 0.1;
for z in 0u8..31 {
let n = 1u32 << z;
let x = (pseudo_random(&mut r) * f64::from(n)) as u32;
let y = (pseudo_random(&mut r) * f64::from(n)) as u32;
let coord = index_to_coord(coord_to_index(z, x, y)?)?;
assert_eq!(coord.x, x);
assert_eq!(coord.y, y);
assert_eq!(coord.level, z);
let coord = index_to_coord(coord_to_index(z, 0, 0)?)?;
assert_eq!(coord.x, 0);
assert_eq!(coord.y, 0);
assert_eq!(coord.level, z);
let coord = index_to_coord(coord_to_index(z, n - 1, n - 1)?)?;
assert_eq!(coord.x, n - 1);
assert_eq!(coord.y, n - 1);
assert_eq!(coord.level, z);
}
Ok(())
}
}