//! Coordinate transforms — WGS84, UTM, ENU, tile math. use crate::types::{GeoBBox, GeoPoint, TileCoord}; const WGS84_A: f64 = 6_378_137.0; #[allow(dead_code)] const WGS84_F: f64 = 1.0 / 298.257_223_563; #[allow(dead_code)] const WGS84_E2: f64 = 2.0 * WGS84_F - WGS84_F * WGS84_F; /// Haversine distance in meters. pub fn haversine(a: &GeoPoint, b: &GeoPoint) -> f64 { let dlat = (b.lat - a.lat).to_radians(); let dlon = (b.lon - a.lon).to_radians(); let lat1 = a.lat.to_radians(); let lat2 = b.lat.to_radians(); let h = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2); 2.0 * WGS84_A * h.sqrt().asin() } /// WGS84 to local ENU (East-North-Up) relative to origin, in meters. pub fn wgs84_to_enu(point: &GeoPoint, origin: &GeoPoint) -> [f64; 3] { let dlat = (point.lat - origin.lat).to_radians(); let dlon = (point.lon - origin.lon).to_radians(); let lat = origin.lat.to_radians(); let east = dlon * WGS84_A * lat.cos(); let north = dlat * WGS84_A; let up = point.alt - origin.alt; [east, north, up] } /// Local ENU to WGS84. pub fn enu_to_wgs84(enu: &[f64; 3], origin: &GeoPoint) -> GeoPoint { let lat = origin.lat.to_radians(); let dlat = enu[1] / WGS84_A; let dlon = enu[0] / (WGS84_A * lat.cos()); GeoPoint { lat: origin.lat + dlat.to_degrees(), lon: origin.lon + dlon.to_degrees(), alt: origin.alt + enu[2], } } /// WGS84 to XYZ tile coordinates (Slippy Map). pub fn wgs84_to_tile(lat: f64, lon: f64, zoom: u8) -> TileCoord { let n = 2f64.powi(zoom as i32); let x = ((lon + 180.0) / 360.0 * n).floor() as u32; let lat_rad = lat.to_radians(); let y = ((1.0 - lat_rad.tan().asinh() / std::f64::consts::PI) / 2.0 * n).floor() as u32; TileCoord { z: zoom, x, y } } /// Tile bounds in WGS84. pub fn tile_bounds(coord: &TileCoord) -> GeoBBox { let n = 2f64.powi(coord.z as i32); let west = coord.x as f64 / n * 360.0 - 180.0; let east = (coord.x + 1) as f64 / n * 360.0 - 180.0; let north = (std::f64::consts::PI * (1.0 - 2.0 * coord.y as f64 / n)) .sinh() .atan() .to_degrees(); let south = (std::f64::consts::PI * (1.0 - 2.0 * (coord.y + 1) as f64 / n)) .sinh() .atan() .to_degrees(); GeoBBox { south, west, north, east, } } /// Get all tile coordinates covering a bounding box at a zoom level. pub fn tiles_for_bbox(bbox: &GeoBBox, zoom: u8) -> Vec { let tl = wgs84_to_tile(bbox.north, bbox.west, zoom); let br = wgs84_to_tile(bbox.south, bbox.east, zoom); let mut tiles = Vec::new(); for y in tl.y..=br.y { for x in tl.x..=br.x { tiles.push(TileCoord { z: zoom, x, y }); } } tiles }