pub const TILE_SIZE_DEFAULT: f64 = 256.0;
pub const MAX_LATITUDE: f64 = 85.051_128_779_806_59;
#[inline]
pub fn lonlat_to_tile(longitude: f64, latitude: f64, zoom: f64) -> (f64, f64) {
let n = 2f64.powf(zoom);
let x = (longitude + 180.0) / 360.0 * n;
let lat_rad = latitude.clamp(-MAX_LATITUDE, MAX_LATITUDE).to_radians();
let y = (1.0 - (lat_rad.tan() + 1.0 / lat_rad.cos()).ln() / std::f64::consts::PI) / 2.0 * n;
(x, y)
}
#[inline]
pub fn tile_to_lonlat(tile_x: f64, tile_y: f64, zoom: f64) -> (f64, f64) {
let n = 2f64.powf(zoom);
let longitude = tile_x / n * 360.0 - 180.0;
let lat_rad = (std::f64::consts::PI * (1.0 - 2.0 * tile_y / n))
.sinh()
.atan();
(longitude, lat_rad.to_degrees())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_zero_meridian() {
for z in 0..=18 {
let (x, y) = lonlat_to_tile(0.0, 0.0, z as f64);
let (lon, lat) = tile_to_lonlat(x, y, z as f64);
assert!((lon - 0.0).abs() < 1e-9, "z={z} lon drift");
assert!((lat - 0.0).abs() < 1e-9, "z={z} lat drift");
}
}
#[test]
fn roundtrip_known_cities() {
let cases = [
("NYC", -74.0060, 40.7128),
("Sydney", 151.2093, -33.8688),
("Reykjavik", -21.9426, 64.1466),
];
for (name, lon, lat) in cases {
for z in [0.0, 5.0, 10.0, 17.0] {
let (x, y) = lonlat_to_tile(lon, lat, z);
let (lon2, lat2) = tile_to_lonlat(x, y, z);
assert!((lon - lon2).abs() < 1e-9, "{name}@z{z} lon drift");
assert!((lat - lat2).abs() < 1e-9, "{name}@z{z} lat drift");
}
}
}
#[test]
fn z0_covers_world_in_one_tile() {
for lon in [-179.9, -90.0, 0.0, 90.0, 179.9] {
for lat in [-80.0, -45.0, 0.0, 45.0, 80.0] {
let (x, y) = lonlat_to_tile(lon, lat, 0.0);
assert!((0.0..1.0).contains(&x), "x out of range: {x}");
assert!((0.0..1.0).contains(&y), "y out of range: {y}");
}
}
}
}