use crate::error::{RenderError, Result};
use crate::types::TileCoordinate;
use geo_types::{Coord, Polygon};
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BoundingBox {
pub min_x: f64,
pub min_y: f64,
pub max_x: f64,
pub max_y: f64,
}
impl BoundingBox {
pub const fn new(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Self {
Self {
min_x,
min_y,
max_x,
max_y,
}
}
pub fn width(&self) -> f64 {
self.max_x - self.min_x
}
pub fn height(&self) -> f64 {
self.max_y - self.min_y
}
}
pub fn lat_lon_to_web_mercator(lon: f64, lat: f64) -> Result<Coord<f64>> {
if !lon.is_finite() || !lat.is_finite() {
return Err(RenderError::InvalidCoordinate(format!(
"Coordinate values must be finite: lon={}, lat={}",
lon, lat
)));
}
let x = lon;
let clamped_lat = lat.clamp(-85.05112878, 85.05112878);
let y = ((PI / 4.0 + (clamped_lat * PI / 180.0) / 2.0).tan().ln()) * 180.0 / PI;
Ok(Coord { x, y })
}
pub fn tile_to_bounds(tile: TileCoordinate) -> Polygon<f64> {
let n = 2f64.powi(tile.z as i32);
let lon_deg = |x: f64| (x / n) * 360.0 - 180.0;
let lat_rad = |y: f64| (PI * (1.0 - 2.0 * y / n)).sinh().atan();
let lat_deg = |y: f64| (lat_rad(y) * 180.0) / PI;
let min_lon = lon_deg(tile.x as f64);
let max_lon = lon_deg(tile.x as f64 + 1.0);
let min_lat = lat_deg(tile.y as f64 + 1.0); let max_lat = lat_deg(tile.y as f64);
Polygon::new(
vec![
Coord { x: min_lon, y: min_lat },
Coord { x: max_lon, y: min_lat },
Coord { x: max_lon, y: max_lat },
Coord { x: min_lon, y: max_lat },
Coord { x: min_lon, y: min_lat }, ]
.into(),
vec![], )
}
pub fn transform_coordinates_to_pixels(
coords: &[Coord<f64>],
bbox: &BoundingBox,
size: f64,
x_scaling_factor: f64,
y_scaling_factor: f64,
offset: f64,
) -> Result<Vec<Coord<f64>>> {
coords
.iter()
.map(|&coord| {
if !coord.x.is_finite() || !coord.y.is_finite() {
return Err(RenderError::InvalidCoordinate(format!(
"Coordinate values must be finite: x={}, y={}",
coord.x, coord.y
)));
}
let merc = lat_lon_to_web_mercator(coord.x, coord.y)?;
let x = (merc.x - bbox.min_x) * x_scaling_factor + offset;
let y = size - (merc.y - bbox.min_y) * y_scaling_factor + offset;
Ok(Coord { x, y })
})
.collect()
}
pub fn transform_polygon_coordinates_to_pixels(
rings: &[Vec<Coord<f64>>],
bbox: &BoundingBox,
size: f64,
x_scaling_factor: f64,
y_scaling_factor: f64,
offset: f64,
) -> Result<Vec<Vec<Coord<f64>>>> {
rings
.iter()
.map(|ring| {
transform_coordinates_to_pixels(ring, bbox, size, x_scaling_factor, y_scaling_factor, offset)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_lat_lon_to_web_mercator_basic() {
let coord = lat_lon_to_web_mercator(0.0, 0.0).unwrap();
assert_abs_diff_eq!(coord.x, 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(coord.y, 0.0, epsilon = 1e-10);
let coord = lat_lon_to_web_mercator(45.0, 0.0).unwrap();
assert_abs_diff_eq!(coord.x, 45.0, epsilon = 1e-10);
}
#[test]
fn test_lat_lon_to_web_mercator_hemispheres() {
let north = lat_lon_to_web_mercator(0.0, 45.0).unwrap();
assert!(north.y > 0.0);
let south = lat_lon_to_web_mercator(0.0, -45.0).unwrap();
assert!(south.y < 0.0);
assert_abs_diff_eq!(north.y, -south.y, epsilon = 1e-10);
}
#[test]
fn test_lat_lon_to_web_mercator_clamping() {
let max_lat = lat_lon_to_web_mercator(0.0, 85.05112878).unwrap();
let beyond_max = lat_lon_to_web_mercator(0.0, 90.0).unwrap();
assert_abs_diff_eq!(max_lat.y, beyond_max.y, epsilon = 1e-6);
let min_lat = lat_lon_to_web_mercator(0.0, -85.05112878).unwrap();
let beyond_min = lat_lon_to_web_mercator(0.0, -90.0).unwrap();
assert_abs_diff_eq!(min_lat.y, beyond_min.y, epsilon = 1e-6);
}
#[test]
fn test_lat_lon_to_web_mercator_invalid() {
assert!(lat_lon_to_web_mercator(f64::NAN, 0.0).is_err());
assert!(lat_lon_to_web_mercator(0.0, f64::NAN).is_err());
assert!(lat_lon_to_web_mercator(f64::INFINITY, 0.0).is_err());
}
#[test]
fn test_tile_to_bounds_zoom_0() {
let tile = TileCoordinate::new(0, 0, 0).unwrap();
let polygon = tile_to_bounds(tile);
let exterior = polygon.exterior();
let coords: Vec<_> = exterior.coords().collect();
assert_abs_diff_eq!(coords[0].x, -180.0, epsilon = 1e-6);
assert_abs_diff_eq!(coords[1].x, 180.0, epsilon = 1e-6);
assert_eq!(coords.first().unwrap(), coords.last().unwrap());
}
#[test]
fn test_tile_to_bounds_polygon_closed() {
let tile = TileCoordinate::new(5, 10, 12).unwrap();
let polygon = tile_to_bounds(tile);
let exterior = polygon.exterior();
let coords: Vec<_> = exterior.coords().collect();
assert_eq!(coords.first().unwrap(), coords.last().unwrap());
assert_eq!(coords.len(), 5);
}
#[test]
fn test_transform_coordinates_to_pixels_basic() {
let coords = vec![Coord { x: 0.0, y: 0.0 }];
let bbox = BoundingBox::new(-10.0, -10.0, 10.0, 10.0);
let size = 256.0;
let x_scaling = size / bbox.width();
let y_scaling = size / bbox.height();
let pixels = transform_coordinates_to_pixels(&coords, &bbox, size, x_scaling, y_scaling, 0.0).unwrap();
assert_eq!(pixels.len(), 1);
assert_abs_diff_eq!(pixels[0].x, 128.0, epsilon = 1e-6);
assert_abs_diff_eq!(pixels[0].y, 128.0, epsilon = 1e-6);
}
#[test]
fn test_transform_coordinates_to_pixels_offset() {
let coords = vec![Coord { x: 0.0, y: 0.0 }];
let bbox = BoundingBox::new(-10.0, -10.0, 10.0, 10.0);
let size = 256.0;
let x_scaling = size / bbox.width();
let y_scaling = size / bbox.height();
let offset = 10.0;
let pixels = transform_coordinates_to_pixels(&coords, &bbox, size, x_scaling, y_scaling, offset).unwrap();
assert_eq!(pixels.len(), 1);
assert_abs_diff_eq!(pixels[0].x, 138.0, epsilon = 1e-6);
assert_abs_diff_eq!(pixels[0].y, 138.0, epsilon = 1e-6);
}
#[test]
fn test_transform_coordinates_to_pixels_invalid() {
let coords = vec![Coord { x: f64::NAN, y: 0.0 }];
let bbox = BoundingBox::new(-10.0, -10.0, 10.0, 10.0);
let size = 256.0;
assert!(transform_coordinates_to_pixels(&coords, &bbox, size, 1.0, 1.0, 0.0).is_err());
}
#[test]
fn test_transform_polygon_coordinates_to_pixels() {
let rings = vec![vec![
Coord { x: -1.0, y: -1.0 },
Coord { x: 1.0, y: -1.0 },
Coord { x: 1.0, y: 1.0 },
Coord { x: -1.0, y: 1.0 },
Coord { x: -1.0, y: -1.0 },
]];
let bbox = BoundingBox::new(-10.0, -10.0, 10.0, 10.0);
let size = 256.0;
let x_scaling = size / bbox.width();
let y_scaling = size / bbox.height();
let pixel_rings = transform_polygon_coordinates_to_pixels(&rings, &bbox, size, x_scaling, y_scaling, 0.0).unwrap();
assert_eq!(pixel_rings.len(), 1);
assert_eq!(pixel_rings[0].len(), 5);
}
#[test]
fn test_bounding_box() {
let bbox = BoundingBox::new(0.0, 0.0, 100.0, 50.0);
assert_eq!(bbox.width(), 100.0);
assert_eq!(bbox.height(), 50.0);
}
}