use std::f64::consts::PI;
pub const MAX_LAT: f64 = 85.05112878;
pub const EARTH_RADIUS: f64 = 6_378_137.0;
pub const TILE_SIZE: u32 = 256;
#[inline]
pub fn clamp_lat(lat: f64) -> f64 {
lat.clamp(-MAX_LAT, MAX_LAT)
}
#[inline]
pub fn project(lon: f64, lat: f64) -> (f64, f64) {
let lat = clamp_lat(lat);
let x = (lon + 180.0) / 360.0;
let sin = (lat * PI / 180.0).sin();
let y = 0.5 - ((1.0 + sin) / (1.0 - sin)).ln() / (4.0 * PI);
(x, y)
}
#[inline]
pub fn unproject(x: f64, y: f64) -> (f64, f64) {
let lon = x * 360.0 - 180.0;
let n = PI * (1.0 - 2.0 * y);
let lat = 180.0 / PI * (0.5 * (n.exp() - (-n).exp())).atan();
(lon, lat)
}
#[inline]
pub fn to_pixels(lon: f64, lat: f64, zoom: u8) -> (f64, f64) {
let scale = (TILE_SIZE as f64) * 2f64.powi(zoom as i32);
let (x, y) = project(lon, lat);
(x * scale, y * scale)
}
#[inline]
pub fn from_pixels(px: f64, py: f64, zoom: u8) -> (f64, f64) {
let scale = (TILE_SIZE as f64) * 2f64.powi(zoom as i32);
unproject(px / scale, py / scale)
}
#[inline]
pub fn meters_per_pixel(lat: f64, zoom: u8) -> f64 {
let lat_rad = clamp_lat(lat) * PI / 180.0;
2.0 * PI * EARTH_RADIUS * lat_rad.cos() / ((TILE_SIZE as f64) * 2f64.powi(zoom as i32))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn origin_projects_to_center() {
let (x, y) = project(0.0, 0.0);
assert!((x - 0.5).abs() < 1e-12);
assert!((y - 0.5).abs() < 1e-12);
}
#[test]
fn roundtrip() {
for &(lon, lat) in &[(105.85, 21.02), (-73.99, 40.73), (139.69, 35.68)] {
let (x, y) = project(lon, lat);
let (lon2, lat2) = unproject(x, y);
assert!((lon - lon2).abs() < 1e-9, "lon {lon}");
assert!((lat - lat2).abs() < 1e-9, "lat {lat}");
}
}
#[test]
fn clamps_extreme_latitude() {
let (_, y1) = project(0.0, 90.0);
let (_, y2) = project(0.0, MAX_LAT);
assert!((y1 - y2).abs() < 1e-12);
}
#[test]
fn pixel_roundtrip_at_zoom() {
let (px, py) = to_pixels(105.85, 21.02, 14);
let (lon, lat) = from_pixels(px, py, 14);
assert!((lon - 105.85).abs() < 1e-9);
assert!((lat - 21.02).abs() < 1e-9);
}
}