use fast_hilbert::{h2xy, xy2h};
use crate::{PmtError, PmtResult};
#[expect(clippy::unreadable_literal)]
pub const PYRAMID_SIZE_BY_ZOOM: [u64; 32] = [
0,
1,
5,
21,
85,
341,
1365,
5461,
21845,
87381,
349525,
1398101,
5592405,
22369621,
89478485,
357913941,
1431655765,
5726623061,
22906492245,
91625968981,
366503875925,
1466015503701,
5864062014805,
23456248059221,
93824992236885,
375299968947541,
1501199875790165,
6004799503160661,
24019198012642645,
96076792050570581,
384307168202282325,
1537228672809129301,
];
#[expect(clippy::cast_possible_truncation)]
pub const MAX_ZOOM: u8 = PYRAMID_SIZE_BY_ZOOM.len() as u8 - 1;
pub const MAX_TILE_ID: u64 =
PYRAMID_SIZE_BY_ZOOM[PYRAMID_SIZE_BY_ZOOM.len() - 1] + 4_u64.pow(MAX_ZOOM as u32) - 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TileCoord {
z: u8,
x: u32,
y: u32,
}
impl TileCoord {
pub fn new(z: u8, x: u32, y: u32) -> PmtResult<Self> {
if z > MAX_ZOOM || x >= (1 << z) || y >= (1 << z) {
Err(PmtError::InvalidCoordinate(z, x, y))
} else {
Ok(Self { z, x, y })
}
}
#[must_use]
pub fn z(&self) -> u8 {
self.z
}
#[must_use]
pub fn x(&self) -> u32 {
self.x
}
#[must_use]
pub fn y(&self) -> u32 {
self.y
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct TileId(u64);
impl TileId {
#[must_use]
pub fn new(id: u64) -> Option<Self> {
if id <= MAX_TILE_ID {
Some(Self(id))
} else {
None
}
}
#[must_use]
pub fn value(self) -> u64 {
self.0
}
}
impl From<TileId> for u64 {
fn from(tile_id: TileId) -> Self {
tile_id.0
}
}
impl From<TileId> for TileCoord {
#[expect(clippy::cast_possible_truncation)]
fn from(id: TileId) -> Self {
let id = id.value();
let mut z = 0;
let mut size = 0;
for (idx, &val) in PYRAMID_SIZE_BY_ZOOM.iter().enumerate() {
if id < val {
break;
}
z = idx as u8;
size = val;
}
if z > 0 {
let (x, y) = h2xy::<u32>(id - size, z);
TileCoord { z, x, y }
} else {
TileCoord { z: 0, x: 0, y: 0 }
}
}
}
impl From<TileCoord> for TileId {
fn from(coord: TileCoord) -> Self {
let TileCoord { z, x, y } = coord;
if z == 0 {
TileId(0)
} else {
let base = PYRAMID_SIZE_BY_ZOOM
.get(usize::from(z))
.expect("TileCoord should be valid"); let tile_id = xy2h(x, y, z);
TileId(base + tile_id)
}
}
}
#[cfg(test)]
pub(crate) mod test {
use crate::{MAX_TILE_ID, PYRAMID_SIZE_BY_ZOOM, TileCoord, TileId};
pub fn coord(z: u8, x: u32, y: u32) -> TileCoord {
TileCoord::new(z, x, y).unwrap()
}
pub fn coord_to_id(z: u8, x: u32, y: u32) -> u64 {
TileId::from(coord(z, x, y)).value()
}
pub fn id_to_coord(id: u64) -> (u8, u32, u32) {
let coord = TileCoord::from(TileId::new(id).unwrap());
(coord.z(), coord.x(), coord.y())
}
#[test]
#[expect(clippy::unreadable_literal)]
fn test_tile_id() {
assert_eq!(TileId::new(0).unwrap().value(), 0);
assert_eq!(TileId::new(MAX_TILE_ID + 1), None);
assert_eq!(TileId::new(MAX_TILE_ID).unwrap().value(), MAX_TILE_ID);
assert_eq!(coord_to_id(0, 0, 0), 0);
assert_eq!(coord_to_id(1, 1, 0), 4);
assert_eq!(coord_to_id(2, 1, 3), 11);
assert_eq!(coord_to_id(3, 3, 0), 26);
assert_eq!(coord_to_id(20, 0, 0), 366503875925);
assert_eq!(coord_to_id(21, 0, 0), 1466015503701);
assert_eq!(coord_to_id(22, 0, 0), 5864062014805);
assert_eq!(coord_to_id(23, 0, 0), 23456248059221);
assert_eq!(coord_to_id(24, 0, 0), 93824992236885);
assert_eq!(coord_to_id(25, 0, 0), 375299968947541);
assert_eq!(coord_to_id(26, 0, 0), 1501199875790165);
assert_eq!(coord_to_id(27, 0, 0), 6004799503160661);
assert_eq!(coord_to_id(28, 0, 0), 24019198012642645);
assert_eq!(coord_to_id(31, 0, 0), 1537228672809129301);
let max_v = (1 << 31) - 1;
assert_eq!(coord_to_id(31, max_v, max_v), 4611686018427387903);
assert_eq!(coord_to_id(31, 0, max_v), 3074457345618258602);
assert_eq!(coord_to_id(31, max_v, 0), 6148914691236517204);
}
#[test]
fn round_trip_ids() {
const LAST_PYRAMID_IDX: usize = PYRAMID_SIZE_BY_ZOOM.len() - 1;
for id in [
0,
1,
2,
3,
4,
5,
6,
PYRAMID_SIZE_BY_ZOOM[LAST_PYRAMID_IDX],
PYRAMID_SIZE_BY_ZOOM[LAST_PYRAMID_IDX] - 1,
PYRAMID_SIZE_BY_ZOOM[LAST_PYRAMID_IDX] + 1,
MAX_TILE_ID - 1,
MAX_TILE_ID,
] {
test_id(id);
}
for id in 0..1000 {
test_id(id);
}
}
fn test_id(id: u64) {
let id1 = TileId::new(id).unwrap();
let coord1 = TileCoord::from(id1);
let coord2 = TileCoord::new(coord1.z, coord1.x, coord1.y).unwrap();
let id2 = TileId::from(coord2);
assert_eq!(id, id2.value(), "Failed round-trip for id={id}");
}
#[test]
fn test_calc_tile_coords() {
let test_cases = [
(0, 0, 0),
(1, 1, 0),
(2, 1, 3),
(3, 3, 0),
(20, 0, 0),
(21, 0, 0),
(22, 0, 0),
(23, 0, 0),
(24, 0, 0),
(25, 0, 0),
(26, 0, 0),
(27, 0, 0),
(28, 0, 0),
];
for (z, x, y) in test_cases {
let (z2, x2, y2) = id_to_coord(coord_to_id(z, x, y));
assert_eq!(
(z, x, y),
(z2, x2, y2),
"Failed round-trip for z={z}, x={x}, y={y}",
);
}
}
}