use crate::bounds::GeoBounds;
use crate::coord::{GeoCoord, WorldCoord, MAX_MERCATOR_LAT};
use crate::ellipsoid::Ellipsoid;
use crate::projection::Projection;
use std::f64::consts::PI;
const EARTH_RADIUS: f64 = Ellipsoid::WGS84.a;
pub struct WebMercator;
impl WebMercator {
#[inline]
pub fn project(geo: &GeoCoord) -> WorldCoord {
<Self as Projection>::project(&Self, geo)
}
#[inline]
pub fn project_checked(geo: &GeoCoord) -> Option<WorldCoord> {
if !geo.is_web_mercator_valid() {
return None;
}
Some(Self::project(geo))
}
#[inline]
pub fn project_clamped(geo: &GeoCoord) -> WorldCoord {
Self::project(&geo.clamped_mercator())
}
#[inline]
pub fn unproject(world: &WorldCoord) -> GeoCoord {
<Self as Projection>::unproject(&Self, world)
}
#[inline]
pub fn max_extent() -> f64 {
EARTH_RADIUS * PI
}
#[inline]
pub fn world_size() -> f64 {
2.0 * Self::max_extent()
}
}
impl Projection for WebMercator {
fn project(&self, geo: &GeoCoord) -> WorldCoord {
let x = EARTH_RADIUS * geo.lon.to_radians();
let lat_rad = geo.lat.to_radians();
let y = EARTH_RADIUS * ((PI / 4.0 + lat_rad / 2.0).tan()).ln();
WorldCoord::new(x, y, geo.alt)
}
fn unproject(&self, world: &WorldCoord) -> GeoCoord {
let mut lon = (world.position.x / EARTH_RADIUS).to_degrees();
lon = ((lon + 180.0).rem_euclid(360.0)) - 180.0;
let lat = (2.0 * (world.position.y / EARTH_RADIUS).exp().atan() - PI / 2.0).to_degrees();
GeoCoord::new(lat, lon, world.position.z)
}
fn scale_factor(&self, geo: &GeoCoord) -> f64 {
1.0 / geo.lat.to_radians().cos()
}
fn projection_bounds(&self) -> GeoBounds {
GeoBounds::new(
GeoCoord::from_lat_lon(-MAX_MERCATOR_LAT, -180.0),
GeoCoord::from_lat_lon(MAX_MERCATOR_LAT, 180.0),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_origin() {
let geo = GeoCoord::from_lat_lon(0.0, 0.0);
let world = WebMercator::project(&geo);
let back = WebMercator::unproject(&world);
assert!((back.lat - geo.lat).abs() < 1e-10);
assert!((back.lon - geo.lon).abs() < 1e-10);
}
#[test]
fn roundtrip_nonzero() {
let geo = GeoCoord::from_lat_lon(51.09916, 17.03664);
let world = WebMercator::project(&geo);
let back = WebMercator::unproject(&world);
assert!((back.lat - geo.lat).abs() < 1e-8);
assert!((back.lon - geo.lon).abs() < 1e-8);
}
#[test]
fn project_checked_rejects_invalid_lat() {
let geo = GeoCoord::from_lat_lon(89.0, 0.0);
assert!(WebMercator::project_checked(&geo).is_none());
}
#[test]
fn project_clamped_accepts_invalid_lat() {
let geo = GeoCoord::from_lat_lon(89.0, 0.0);
let world = WebMercator::project_clamped(&geo);
assert!(world.position.y.is_finite());
}
#[test]
fn world_size_is_double_extent() {
assert!((WebMercator::world_size() - 2.0 * WebMercator::max_extent()).abs() < 1e-10);
}
#[test]
fn scale_factor_equator() {
let sf = WebMercator.scale_factor(&GeoCoord::from_lat_lon(0.0, 0.0));
assert!((sf - 1.0).abs() < 1e-10);
}
#[test]
fn scale_factor_60_degrees() {
let sf = WebMercator.scale_factor(&GeoCoord::from_lat_lon(60.0, 0.0));
assert!((sf - 2.0).abs() < 1e-10);
}
#[test]
fn projection_bounds_mercator() {
let bounds = WebMercator.projection_bounds();
assert!((bounds.sw().lat - (-MAX_MERCATOR_LAT)).abs() < 1e-10);
assert!((bounds.ne().lat - MAX_MERCATOR_LAT).abs() < 1e-10);
}
#[test]
fn project_clamped_wraps_longitude() {
let a = GeoCoord {
lat: 0.0,
lon: 190.0,
alt: 0.0,
};
let b = GeoCoord::from_lat_lon(0.0, -170.0);
let wa = WebMercator::project_clamped(&a);
let wb = WebMercator::project_clamped(&b);
assert!((wa.position.x - wb.position.x).abs() < 1e-10);
}
#[test]
fn altitude_passthrough() {
let geo = GeoCoord::new(45.0, 12.0, 1234.5);
let world = WebMercator::project(&geo);
let back = WebMercator::unproject(&world);
assert!((world.position.z - 1234.5).abs() < 1e-12);
assert!((back.alt - 1234.5).abs() < 1e-12);
}
#[test]
fn unproject_wraps_longitude_back_into_valid_range() {
let extent = WebMercator::max_extent();
let world = WorldCoord::new(extent + 1000.0, 0.0, 0.0);
let geo = WebMercator::unproject(&world);
assert!(geo.lon >= -180.0 && geo.lon <= 180.0);
}
}