const MERCATOR_LAT_LIMIT: f64 = 85.051_128_78;
const QUADBIN_HEADER: u64 = 0x4800_0000_0000_0000;
pub fn lonlat_to_tile(lon: f64, lat: f64, z: u8) -> (u32, u32) {
let n = 1u64 << z;
let n_f = n as f64;
let lat = lat.clamp(-MERCATOR_LAT_LIMIT, MERCATOR_LAT_LIMIT);
let lon = lon.clamp(-180.0, 180.0);
let x = ((lon + 180.0) / 360.0 * n_f).floor();
let lat_rad = lat.to_radians();
let y = ((1.0 - lat_rad.tan().asinh() / std::f64::consts::PI) / 2.0 * n_f).floor();
let max = (n - 1) as f64;
let x = x.clamp(0.0, max) as u32;
let y = y.clamp(0.0, max) as u32;
(x, y)
}
fn interleave(v: u32) -> u64 {
let mut x = v as u64 & 0x0000_0000_03ff_ffff; x = (x | (x << 16)) & 0x0000_ffff_0000_ffff;
x = (x | (x << 8)) & 0x00ff_00ff_00ff_00ff;
x = (x | (x << 4)) & 0x0f0f_0f0f_0f0f_0f0f;
x = (x | (x << 2)) & 0x3333_3333_3333_3333;
x = (x | (x << 1)) & 0x5555_5555_5555_5555;
x
}
pub fn tile_to_quadbin(z: u8, x: u32, y: u32) -> u64 {
debug_assert!(z <= 26, "Quadbin zoom out of band: {z}");
let z = z as u64;
let interleaved = interleave(x) | (interleave(y) << 1);
let morton_shift = 52 - 2 * z;
let payload = (interleaved << morton_shift) & 0x000f_ffff_ffff_ffff;
let fill = if morton_shift == 0 {
0
} else {
(1u64 << morton_shift) - 1
};
QUADBIN_HEADER | (z << 52) | payload | fill
}
pub fn lonlat_to_quadbin(lon: f64, lat: f64, z: u8) -> u64 {
let (x, y) = lonlat_to_tile(lon, lat, z);
tile_to_quadbin(z, x, y)
}
pub fn quadbin_to_tile(q: u64) -> (u8, u32, u32) {
let z = ((q >> 52) & 0x1f) as u8;
let morton_shift = 52u32.saturating_sub(2 * z as u32);
let interleaved = (q & 0x000f_ffff_ffff_ffff) >> morton_shift;
let x = deinterleave(interleaved);
let y = deinterleave(interleaved >> 1);
(z, x, y)
}
fn deinterleave(coded: u64) -> u32 {
let mut v = coded & 0x5555_5555_5555_5555;
v = (v | (v >> 1)) & 0x3333_3333_3333_3333;
v = (v | (v >> 2)) & 0x0f0f_0f0f_0f0f_0f0f;
v = (v | (v >> 4)) & 0x00ff_00ff_00ff_00ff;
v = (v | (v >> 8)) & 0x0000_ffff_0000_ffff;
v = (v | (v >> 16)) & 0x0000_0000_ffff_ffff;
v as u32
}
pub fn quadbin_centroid(z: u8, x: u32, y: u32) -> [f64; 2] {
let n = (1u64 << z) as f64;
let lon = ((x as f64 + 0.5) / n) * 360.0 - 180.0;
let lat_rad = (std::f64::consts::PI * (1.0 - 2.0 * (y as f64 + 0.5) / n))
.sinh()
.atan();
[lon, lat_rad.to_degrees()]
}
#[cfg(test)]
mod tests {
use super::*;
fn decode_from_ts_layout(q: u64) -> (u8, u32, u32) {
fn deint(coded: u64) -> u32 {
let mut v = coded & 0x5555_5555_5555_5555;
v = (v | (v >> 1)) & 0x3333_3333_3333_3333;
v = (v | (v >> 2)) & 0x0f0f_0f0f_0f0f_0f0f;
v = (v | (v >> 4)) & 0x00ff_00ff_00ff_00ff;
v = (v | (v >> 8)) & 0x0000_ffff_0000_ffff;
v = (v | (v >> 16)) & 0x0000_0000_ffff_ffff;
v as u32
}
let z = ((q >> 52) & 0x1f) as u8;
let morton_shift = (52 - 2 * z as u32) as u64;
let interleaved = (q & ((1u64 << 52) - 1)) >> morton_shift;
let x = deint(interleaved);
let y = deint(interleaved >> 1);
(z, x, y)
}
#[test]
fn carto_reference_value() {
assert_eq!(tile_to_quadbin(0, 0, 0), 0x480f_ffff_ffff_ffff);
}
#[test]
fn header_and_zoom_bits_are_well_formed() {
for z in 0u8..=26 {
let q = tile_to_quadbin(z, 0, 0);
assert_eq!(q >> 60, 0b100, "header nibble wrong at z={z}");
assert_eq!((q >> 59) & 1, 1, "mode bit wrong at z={z}");
assert_eq!(((q >> 52) & 0x1f) as u8, z, "zoom field wrong at z={z}");
}
}
#[test]
fn round_trips_through_decode() {
let cases: &[(u8, u32, u32)] = &[
(0, 0, 0),
(1, 0, 0),
(1, 1, 1),
(2, 3, 1),
(10, 163, 395), (14, 8191, 5000),
(25, (1u32 << 25) - 1, 0),
(25, 0, (1u32 << 25) - 1),
(26, (1u32 << 26) - 1, (1u32 << 26) - 1),
(26, 12_345_678, 9_876_543),
];
for &(z, x, y) in cases {
let q = tile_to_quadbin(z, x, y);
assert_eq!(
decode_from_ts_layout(q),
(z, x, y),
"TS-layout round-trip failed for (z={z}, x={x}, y={y}) -> {q:#018x}"
);
assert_eq!(
quadbin_to_tile(q),
(z, x, y),
"module decode round-trip failed for (z={z}, x={x}, y={y}) -> {q:#018x}"
);
}
}
#[test]
fn lonlat_to_tile_sanity() {
assert_eq!(lonlat_to_tile(0.0, 0.0, 1), (1, 1));
assert_eq!(lonlat_to_tile(-122.4194, 37.7749, 10), (163, 395));
let n = (1u32 << 5) - 1;
let (x, y) = lonlat_to_tile(180.0, 89.0, 5);
assert!(x <= n && y <= n, "clamped tile out of band: ({x},{y})");
let (x, y) = lonlat_to_tile(-180.0, -89.0, 5);
assert!(x <= n && y <= n, "clamped tile out of band: ({x},{y})");
}
#[test]
fn lonlat_to_quadbin_matches_two_step() {
let (x, y) = lonlat_to_tile(-122.4194, 37.7749, 10);
assert_eq!(
lonlat_to_quadbin(-122.4194, 37.7749, 10),
tile_to_quadbin(10, x, y)
);
}
#[test]
fn centroid_lands_inside_its_tile() {
for &(z, x, y) in &[(2u8, 1u32, 2u32), (10, 163, 395), (14, 8191, 5000)] {
let [lon, lat] = quadbin_centroid(z, x, y);
assert_eq!(lonlat_to_tile(lon, lat, z), (x, y));
}
}
}