use crate::error::{Error, Result};
use geo_types::Point;
#[cfg(feature = "projection")]
use proj::Proj;
#[cfg(feature = "projection")]
use std::sync::OnceLock;
#[cfg(feature = "projection")]
static WGS84_TO_WEB_MERCATOR: OnceLock<Proj> = OnceLock::new();
#[cfg(feature = "projection")]
static WEB_MERCATOR_TO_WGS84: OnceLock<Proj> = OnceLock::new();
#[cfg(feature = "projection")]
fn wgs84_to_web_mercator() -> &'static Proj {
WGS84_TO_WEB_MERCATOR.get_or_init(|| {
Proj::new_known_crs("EPSG:4326", "EPSG:3857", None)
.expect("Failed to initialize WGS84 to Web Mercator projection")
})
}
#[cfg(feature = "projection")]
fn web_mercator_to_wgs84() -> &'static Proj {
WEB_MERCATOR_TO_WGS84.get_or_init(|| {
Proj::new_known_crs("EPSG:3857", "EPSG:4326", None)
.expect("Failed to initialize Web Mercator to WGS84 projection")
})
}
pub fn lonlat_to_tile(lon: f64, lat: f64, zoom: u8) -> Result<(u32, u32)> {
if !(-180.0..=180.0).contains(&lon) {
return Err(Error::InvalidCoordinates(zoom, 0, 0));
}
if !(-85.0511..=85.0511).contains(&lat) {
return Err(Error::InvalidCoordinates(zoom, 0, 0));
}
let n = 1u32 << zoom;
let x = ((lon + 180.0) / 360.0 * n as f64).floor() as u32;
let lat_rad = lat.to_radians();
let y = ((1.0 - lat_rad.tan().asinh() / std::f64::consts::PI) / 2.0 * n as f64).floor() as u32;
Ok((x.min(n - 1), y.min(n - 1)))
}
pub fn lonlat_to_tile_coords(
lon: f64,
lat: f64,
zoom: u8,
tile_x: u32,
tile_y: u32,
extent: u32,
) -> (i32, i32) {
let n = 1u32 << zoom;
let world_x = (lon + 180.0) / 360.0 * n as f64;
let lat_rad = lat.to_radians();
let world_y = (1.0 - lat_rad.tan().asinh() / std::f64::consts::PI) / 2.0 * n as f64;
let tile_rel_x = (world_x - tile_x as f64) * extent as f64;
let tile_rel_y = (world_y - tile_y as f64) * extent as f64;
(tile_rel_x.round() as i32, tile_rel_y.round() as i32)
}
pub fn tile_to_lonlat(tile_x: u32, tile_y: u32, zoom: u8) -> Point<f64> {
let n = (1u32 << zoom) as f64;
let lon = (tile_x as f64 / n) * 360.0 - 180.0;
let lat_rad = (std::f64::consts::PI * (1.0 - 2.0 * tile_y as f64 / n))
.sinh()
.atan();
let lat = lat_rad.to_degrees();
Point::new(lon, lat)
}
pub fn tile_geo_bounds(zoom: u8, tile_x: u32, tile_y: u32) -> (f64, f64, f64, f64) {
let nw = tile_to_lonlat(tile_x, tile_y, zoom);
let se = tile_to_lonlat(tile_x + 1, tile_y + 1, zoom);
(nw.x(), se.y(), se.x(), nw.y())
}
pub fn tile_coords_to_lonlat(
x: i32,
y: i32,
zoom: u8,
tile_x: u32,
tile_y: u32,
extent: u32,
) -> Point<f64> {
let n = (1u32 << zoom) as f64;
let world_x = tile_x as f64 + (x as f64 / extent as f64);
let world_y = tile_y as f64 + (y as f64 / extent as f64);
let lon = (world_x / n) * 360.0 - 180.0;
let lat_rad = (std::f64::consts::PI * (1.0 - 2.0 * world_y / n))
.sinh()
.atan();
let lat = lat_rad.to_degrees();
Point::new(lon, lat)
}
#[cfg(feature = "projection")]
pub fn wgs84_to_meters(point: Point<f64>) -> Result<Point<f64>> {
let proj = wgs84_to_web_mercator();
let (x, y) = proj
.convert((point.x(), point.y()))
.map_err(|e| Error::Other(format!("Projection error: {}", e)))?;
Ok(Point::new(x, y))
}
#[cfg(not(feature = "projection"))]
pub fn wgs84_to_meters(point: Point<f64>) -> Result<Point<f64>> {
const R: f64 = 6378137.0; let x = R * point.x().to_radians();
let y = R
* (std::f64::consts::FRAC_PI_4 + point.y().to_radians() / 2.0)
.tan()
.ln();
Ok(Point::new(x, y))
}
#[cfg(feature = "projection")]
pub fn meters_to_wgs84(point: Point<f64>) -> Result<Point<f64>> {
let proj = web_mercator_to_wgs84();
let (x, y) = proj
.convert((point.x(), point.y()))
.map_err(|e| Error::Other(format!("Projection error: {}", e)))?;
Ok(Point::new(x, y))
}
#[cfg(not(feature = "projection"))]
pub fn meters_to_wgs84(point: Point<f64>) -> Result<Point<f64>> {
const R: f64 = 6378137.0; let lon = point.x().to_degrees() / R;
let lat = (2.0 * (point.y() / R).exp().atan() - std::f64::consts::FRAC_PI_2).to_degrees();
Ok(Point::new(lon, lat))
}
pub fn tile_bounds(tile_x: u32, tile_y: u32, zoom: u8) -> crate::types::BoundingBox {
let nw = tile_to_lonlat(tile_x, tile_y, zoom);
let se = tile_to_lonlat(tile_x + 1, tile_y + 1, zoom);
crate::types::BoundingBox::new(
nw.x(), se.y(), se.x(), nw.y(), )
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lonlat_to_tile() {
let (x, y) = lonlat_to_tile(0.0, 0.0, 0).unwrap();
assert_eq!(x, 0);
assert_eq!(y, 0);
let (x, y) = lonlat_to_tile(-122.4194, 37.7749, 10).unwrap();
assert_eq!(x, 163);
assert_eq!(y, 395);
}
#[test]
fn test_tile_to_lonlat() {
let point = tile_to_lonlat(163, 395, 10);
assert!((point.x() + 122.695).abs() < 0.01, "lon = {}", point.x());
assert!((point.y() - 37.996).abs() < 0.01, "lat = {}", point.y());
let (tx, ty) = lonlat_to_tile(-122.4194, 37.7749, 10).unwrap();
assert_eq!((tx, ty), (163, 395));
}
#[test]
fn test_lonlat_to_tile_coords() {
let (x, y) = lonlat_to_tile_coords(-122.4194, 37.7749, 10, 163, 395, 4096);
assert!(x > 0 && x < 4096);
assert!(y > 0 && y < 4096);
}
#[test]
fn test_tile_coords_roundtrip() {
let original_lon = -122.4194;
let original_lat = 37.7749;
let zoom = 10;
let extent = 4096;
let (tile_x, tile_y) = lonlat_to_tile(original_lon, original_lat, zoom).unwrap();
let (x, y) =
lonlat_to_tile_coords(original_lon, original_lat, zoom, tile_x, tile_y, extent);
let point = tile_coords_to_lonlat(x, y, zoom, tile_x, tile_y, extent);
assert!((point.x() - original_lon).abs() < 0.001);
assert!((point.y() - original_lat).abs() < 0.001);
}
#[test]
fn test_wgs84_to_meters() {
let point = Point::new(-122.4194, 37.7749);
let meters = wgs84_to_meters(point).unwrap();
assert!(meters.x().abs() > 10_000_000.0);
assert!(meters.y().abs() > 4_000_000.0);
let back = meters_to_wgs84(meters).unwrap();
assert!((back.x() - point.x()).abs() < 0.01); assert!((back.y() - point.y()).abs() < 0.01);
}
#[test]
fn test_tile_bounds() {
let bounds = tile_bounds(163, 395, 10);
assert!(bounds.contains(-122.4194, 37.7749));
}
}