use std::f64::consts::PI;
use crate::tile::{TileBounds, TileCoord};
pub const WORLD_SCALE: u64 = 1_u64 << 32;
pub const WORLD_HALF: u32 = 1_u32 << 31;
pub const MAX_LATITUDE: f64 = 85.05112878;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WorldCoord {
pub x: u32,
pub y: u32,
}
impl WorldCoord {
#[inline]
pub const fn new(x: u32, y: u32) -> Self {
Self { x, y }
}
#[inline]
pub fn from_lng_lat(lng: f64, lat: f64) -> Self {
lng_lat_to_world(lng, lat)
}
#[inline]
pub fn to_tile(&self, zoom: u8) -> TileCoord {
if zoom == 0 {
return TileCoord::new(0, 0, 0);
}
let shift = 32 - zoom as u32;
let x = self.x >> shift;
let y = self.y >> shift;
TileCoord::new(x, y, zoom)
}
#[inline]
pub fn to_tile_local(&self, tile: &TileCoord, extent: u32) -> (i32, i32) {
if tile.z == 0 {
let local_x = ((self.x as u64) * extent as u64 / WORLD_SCALE) as i32;
let local_y = ((self.y as u64) * extent as u64 / WORLD_SCALE) as i32;
return (local_x, local_y);
}
let shift = 32 - tile.z as u32;
let tile_size = 1_u64 << shift;
let tile_x = (tile.x as u64) << shift;
let tile_y = (tile.y as u64) << shift;
let local_x = ((self.x as i64 - tile_x as i64) * extent as i64 / tile_size as i64) as i32;
let local_y = ((self.y as i64 - tile_y as i64) * extent as i64 / tile_size as i64) as i32;
(local_x, local_y)
}
}
pub fn lng_lat_to_world(lng: f64, lat: f64) -> WorldCoord {
let scale = WORLD_SCALE as f64;
let lng_normalized = ((lng + 180.0) / 360.0).clamp(0.0, 0.9999999999);
let x = (lng_normalized * scale) as u32;
let lat_clamped = lat.clamp(-MAX_LATITUDE, MAX_LATITUDE);
let lat_rad = lat_clamped.to_radians();
let mercator_y = (lat_rad.tan() + 1.0 / lat_rad.cos()).ln();
let y_normalized = ((1.0 - mercator_y / PI) / 2.0).clamp(0.0, 0.9999999999);
let y = (y_normalized * scale) as u32;
WorldCoord::new(x, y)
}
pub fn world_to_lng_lat(coord: WorldCoord) -> (f64, f64) {
let scale = WORLD_SCALE as f64;
let lng = (coord.x as f64) / scale * 360.0 - 180.0;
let mercator_y = (1.0 - 2.0 * (coord.y as f64) / scale) * PI;
let lat = (mercator_y.sinh()).atan().to_degrees();
(lng, lat)
}
pub fn tile_local_to_world(
tile: &TileCoord,
local_x: i32,
local_y: i32,
extent: u32,
) -> WorldCoord {
if tile.z == 0 {
let world_x = ((local_x as i64) * WORLD_SCALE as i64 / extent as i64) as u32;
let world_y = ((local_y as i64) * WORLD_SCALE as i64 / extent as i64) as u32;
return WorldCoord::new(world_x, world_y);
}
let shift = 32 - tile.z as u32;
let tile_size = 1_u64 << shift;
let tile_world_x = (tile.x as u64) << shift;
let tile_world_y = (tile.y as u64) << shift;
let world_x =
(tile_world_x as i64 + (local_x as i64) * tile_size as i64 / extent as i64) as u32;
let world_y =
(tile_world_y as i64 + (local_y as i64) * tile_size as i64 / extent as i64) as u32;
WorldCoord::new(world_x, world_y)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WorldBounds {
pub x_min: u32,
pub y_min: u32,
pub x_max: u32,
pub y_max: u32,
}
impl WorldBounds {
#[inline]
pub const fn new(x_min: u32, y_min: u32, x_max: u32, y_max: u32) -> Self {
Self {
x_min,
y_min,
x_max,
y_max,
}
}
pub fn from_tile(tile: &TileCoord) -> Self {
if tile.z == 0 {
return Self::new(0, 0, u32::MAX, u32::MAX);
}
let shift = 32 - tile.z as u32;
let tile_size = 1_u64 << shift;
let x_min = ((tile.x as u64) << shift) as u32;
let y_min = ((tile.y as u64) << shift) as u32;
let x_max = ((tile.x as u64 + 1) * tile_size - 1).min(u32::MAX as u64) as u32;
let y_max = ((tile.y as u64 + 1) * tile_size - 1).min(u32::MAX as u64) as u32;
Self::new(x_min, y_min, x_max, y_max)
}
pub fn from_tile_with_buffer(tile: &TileCoord, buffer_pixels: u32, extent: u32) -> Self {
let base = Self::from_tile(tile);
if buffer_pixels == 0 {
return base;
}
let tile_size_world: u64 = if tile.z == 0 {
WORLD_SCALE
} else {
1_u64 << (32 - tile.z as u32)
};
let buffer_world = (tile_size_world * buffer_pixels as u64 / extent as u64) as u32;
Self::new(
base.x_min.saturating_sub(buffer_world),
base.y_min.saturating_sub(buffer_world),
base.x_max.saturating_add(buffer_world),
base.y_max.saturating_add(buffer_world),
)
}
#[inline]
pub fn contains(&self, coord: &WorldCoord) -> bool {
coord.x >= self.x_min
&& coord.x <= self.x_max
&& coord.y >= self.y_min
&& coord.y <= self.y_max
}
#[inline]
pub fn intersects(&self, other: &WorldBounds) -> bool {
self.x_max >= other.x_min
&& self.x_min <= other.x_max
&& self.y_max >= other.y_min
&& self.y_min <= other.y_max
}
#[inline]
pub fn contains_bounds(&self, other: &WorldBounds) -> bool {
other.x_min >= self.x_min
&& other.x_max <= self.x_max
&& other.y_min >= self.y_min
&& other.y_max <= self.y_max
}
#[inline]
pub fn width(&self) -> u32 {
self.x_max - self.x_min
}
#[inline]
pub fn height(&self) -> u32 {
self.y_max - self.y_min
}
pub fn to_tile_bounds(&self) -> TileBounds {
let (lng_min, lat_max) = world_to_lng_lat(WorldCoord::new(self.x_min, self.y_min));
let (lng_max, lat_min) = world_to_lng_lat(WorldCoord::new(self.x_max, self.y_max));
TileBounds::new(lng_min, lat_min, lng_max, lat_max)
}
pub fn from_tile_bounds(bounds: &TileBounds) -> Self {
let nw = lng_lat_to_world(bounds.lng_min, bounds.lat_max);
let se = lng_lat_to_world(bounds.lng_max, bounds.lat_min);
Self::new(nw.x, nw.y, se.x, se.y)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_null_island_conversion() {
let coord = lng_lat_to_world(0.0, 0.0);
assert_eq!(coord.x, WORLD_HALF, "x should be 2^31 for lng=0");
assert_eq!(coord.y, WORLD_HALF, "y should be 2^31 for lat=0");
}
#[test]
fn test_northwest_corner() {
let coord = lng_lat_to_world(-180.0, MAX_LATITUDE);
assert_eq!(coord.x, 0, "x should be 0 for lng=-180");
assert!(
coord.y < 1000,
"y should be near 0 for max latitude, got {}",
coord.y
);
}
#[test]
fn test_southeast_corner() {
let coord = lng_lat_to_world(179.999, -MAX_LATITUDE);
assert!(
coord.x > u32::MAX - 100000,
"x should be near max for lng=179.999, got {}",
coord.x
);
assert!(
coord.y > u32::MAX - 100000,
"y should be near max for lat=-85.05, got {}",
coord.y
);
}
#[test]
fn test_round_trip_null_island() {
let original = (0.0, 0.0);
let coord = lng_lat_to_world(original.0, original.1);
let (lng, lat) = world_to_lng_lat(coord);
assert!(
(lng - original.0).abs() < 0.0001,
"Longitude round-trip failed: {} vs {}",
lng,
original.0
);
assert!(
(lat - original.1).abs() < 0.0001,
"Latitude round-trip failed: {} vs {}",
lat,
original.1
);
}
#[test]
fn test_round_trip_new_york() {
let original = (-73.985428, 40.748817);
let coord = lng_lat_to_world(original.0, original.1);
let (lng, lat) = world_to_lng_lat(coord);
assert!(
(lng - original.0).abs() < 0.0001,
"Longitude round-trip failed: {} vs {}",
lng,
original.0
);
assert!(
(lat - original.1).abs() < 0.0001,
"Latitude round-trip failed: {} vs {}",
lat,
original.1
);
}
#[test]
fn test_round_trip_sydney() {
let original = (151.215256, -33.856784);
let coord = lng_lat_to_world(original.0, original.1);
let (lng, lat) = world_to_lng_lat(coord);
assert!(
(lng - original.0).abs() < 0.0001,
"Longitude round-trip failed: {} vs {}",
lng,
original.0
);
assert!(
(lat - original.1).abs() < 0.0001,
"Latitude round-trip failed: {} vs {}",
lat,
original.1
);
}
#[test]
fn test_to_tile_zoom_0() {
let coord = lng_lat_to_world(0.0, 0.0);
let tile = coord.to_tile(0);
assert_eq!(tile.x, 0);
assert_eq!(tile.y, 0);
assert_eq!(tile.z, 0);
}
#[test]
fn test_to_tile_zoom_1() {
let coord = lng_lat_to_world(0.0, 0.0);
let tile = coord.to_tile(1);
assert_eq!(tile.x, 1, "Null Island should be in eastern half at z1");
assert_eq!(tile.y, 1, "Null Island should be in southern half at z1");
assert_eq!(tile.z, 1);
}
#[test]
fn test_to_tile_northwest_quadrant() {
let coord = lng_lat_to_world(-90.0, 45.0);
let tile = coord.to_tile(1);
assert_eq!(tile.x, 0, "Western hemisphere should have x=0 at z1");
assert_eq!(tile.y, 0, "Northern hemisphere should have y=0 at z1");
}
#[test]
fn test_to_tile_southeast_quadrant() {
let coord = lng_lat_to_world(90.0, -45.0);
let tile = coord.to_tile(1);
assert_eq!(tile.x, 1, "Eastern hemisphere should have x=1 at z1");
assert_eq!(tile.y, 1, "Southern hemisphere should have y=1 at z1");
}
#[test]
fn test_to_tile_local_center() {
let coord = lng_lat_to_world(0.0, 0.0);
let tile = TileCoord::new(0, 0, 0);
let extent = 4096;
let (local_x, local_y) = coord.to_tile_local(&tile, extent);
assert!(
(local_x - 2048).abs() <= 1,
"Center should be near extent/2, got {}",
local_x
);
assert!(
(local_y - 2048).abs() <= 1,
"Center should be near extent/2, got {}",
local_y
);
}
#[test]
fn test_to_tile_local_origin() {
let coord = lng_lat_to_world(-180.0, MAX_LATITUDE);
let tile = TileCoord::new(0, 0, 0);
let extent = 4096;
let (local_x, local_y) = coord.to_tile_local(&tile, extent);
assert!(
(0..10).contains(&local_x),
"Northwest should be near x=0, got {}",
local_x
);
assert!(
(0..10).contains(&local_y),
"Northwest should be near y=0, got {}",
local_y
);
}
#[test]
fn test_tile_local_round_trip() {
let tile = TileCoord::new(1234, 5678, 14);
let extent = 4096;
let original_local = (1000, 2000);
let world = tile_local_to_world(&tile, original_local.0, original_local.1, extent);
let (local_x, local_y) = world.to_tile_local(&tile, extent);
assert_eq!(
local_x, original_local.0,
"Local X round-trip failed: {} vs {}",
local_x, original_local.0
);
assert_eq!(
local_y, original_local.1,
"Local Y round-trip failed: {} vs {}",
local_y, original_local.1
);
}
#[test]
fn test_latitude_clamping() {
let coord_north = lng_lat_to_world(0.0, 90.0);
let coord_max = lng_lat_to_world(0.0, MAX_LATITUDE);
assert!(
(coord_north.y as i64 - coord_max.y as i64).abs() < 100,
"Latitude 90 should be clamped to max: {} vs {}",
coord_north.y,
coord_max.y
);
let coord_south = lng_lat_to_world(0.0, -90.0);
let coord_min = lng_lat_to_world(0.0, -MAX_LATITUDE);
assert!(
(coord_south.y as i64 - coord_min.y as i64).abs() < 100,
"Latitude -90 should be clamped to min: {} vs {}",
coord_south.y,
coord_min.y
);
}
#[test]
fn test_antimeridian_east() {
let coord = lng_lat_to_world(179.9, 0.0);
assert!(coord.x > WORLD_HALF, "x should be > 2^31 near 180°");
let (lng, _) = world_to_lng_lat(coord);
assert!(
(lng - 179.9).abs() < 0.001,
"Longitude near antimeridian: {} vs 179.9",
lng
);
}
#[test]
fn test_antimeridian_west() {
let coord = lng_lat_to_world(-179.9, 0.0);
assert!(coord.x < WORLD_HALF, "x should be < 2^31 near -180°");
let (lng, _) = world_to_lng_lat(coord);
assert!(
(lng - (-179.9)).abs() < 0.001,
"Longitude near antimeridian: {} vs -179.9",
lng
);
}
#[test]
fn test_consistency_with_tile_coord_bounds() {
use crate::tile::lng_lat_to_tile;
let test_points = [
(0.0, 0.0),
(-73.985428, 40.748817), (151.215256, -33.856784), (-122.4194, 37.7749), (139.6917, 35.6895), ];
for (lng, lat) in test_points {
for zoom in [0, 5, 10, 14, 18] {
let world_coord = lng_lat_to_world(lng, lat);
let tile_from_world = world_coord.to_tile(zoom);
let tile_direct = lng_lat_to_tile(lng, lat, zoom);
assert_eq!(
tile_from_world, tile_direct,
"Tile mismatch for ({}, {}) at z{}: {:?} vs {:?}",
lng, lat, zoom, tile_from_world, tile_direct
);
}
}
}
#[test]
fn test_precision_at_high_zoom() {
let lng1 = -73.985428;
let lat1 = 40.748817;
let lng2 = lng1 + 0.00001;
let coord1 = lng_lat_to_world(lng1, lat1);
let coord2 = lng_lat_to_world(lng2, lat1);
assert_ne!(
coord1.x, coord2.x,
"Points 1m apart should have different x coordinates"
);
let tile = coord1.to_tile(20);
let (local1, _) = coord1.to_tile_local(&tile, 4096);
let (local2, _) = coord2.to_tile_local(&tile, 4096);
let _ = (local1, local2); }
#[test]
fn test_world_coord_equality() {
let c1 = WorldCoord::new(100, 200);
let c2 = WorldCoord::new(100, 200);
let c3 = WorldCoord::new(100, 201);
assert_eq!(c1, c2);
assert_ne!(c1, c3);
}
#[test]
fn test_world_coord_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(WorldCoord::new(100, 200));
set.insert(WorldCoord::new(100, 200)); set.insert(WorldCoord::new(100, 201));
assert_eq!(set.len(), 2);
}
#[test]
fn test_world_bounds_from_tile_zoom0() {
let tile = TileCoord::new(0, 0, 0);
let bounds = WorldBounds::from_tile(&tile);
assert_eq!(bounds.x_min, 0);
assert_eq!(bounds.y_min, 0);
assert_eq!(bounds.x_max, u32::MAX);
assert_eq!(bounds.y_max, u32::MAX);
}
#[test]
fn test_world_bounds_from_tile_zoom1() {
let tile = TileCoord::new(0, 0, 1);
let bounds = WorldBounds::from_tile(&tile);
assert_eq!(bounds.x_min, 0);
assert_eq!(bounds.y_min, 0);
assert_eq!(bounds.x_max, (1u64 << 31) as u32 - 1);
assert_eq!(bounds.y_max, (1u64 << 31) as u32 - 1);
}
#[test]
fn test_world_bounds_from_tile_zoom1_se() {
let tile = TileCoord::new(1, 1, 1);
let bounds = WorldBounds::from_tile(&tile);
assert_eq!(bounds.x_min, 1u32 << 31);
assert_eq!(bounds.y_min, 1u32 << 31);
assert_eq!(bounds.x_max, u32::MAX);
assert_eq!(bounds.y_max, u32::MAX);
}
#[test]
fn test_world_bounds_contains_point() {
let bounds = WorldBounds::new(100, 100, 200, 200);
assert!(bounds.contains(&WorldCoord::new(150, 150)));
assert!(bounds.contains(&WorldCoord::new(100, 100))); assert!(bounds.contains(&WorldCoord::new(200, 200))); assert!(!bounds.contains(&WorldCoord::new(99, 150)));
assert!(!bounds.contains(&WorldCoord::new(201, 150)));
}
#[test]
fn test_world_bounds_intersects() {
let a = WorldBounds::new(0, 0, 100, 100);
let b = WorldBounds::new(50, 50, 150, 150);
let c = WorldBounds::new(200, 200, 300, 300);
assert!(a.intersects(&b));
assert!(b.intersects(&a));
assert!(!a.intersects(&c));
assert!(!c.intersects(&a));
}
#[test]
fn test_world_bounds_contains_bounds() {
let outer = WorldBounds::new(0, 0, 1000, 1000);
let inner = WorldBounds::new(100, 100, 500, 500);
let partial = WorldBounds::new(500, 500, 1500, 1500);
assert!(outer.contains_bounds(&inner));
assert!(!inner.contains_bounds(&outer));
assert!(!outer.contains_bounds(&partial));
}
#[test]
fn test_world_bounds_with_buffer() {
let tile = TileCoord::new(1, 1, 2);
let base = WorldBounds::from_tile(&tile);
let buffered = WorldBounds::from_tile_with_buffer(&tile, 8, 4096);
assert!(
buffered.x_min < base.x_min,
"buffered x_min {} should be < base x_min {}",
buffered.x_min,
base.x_min
);
assert!(
buffered.y_min < base.y_min,
"buffered y_min {} should be < base y_min {}",
buffered.y_min,
base.y_min
);
assert!(
buffered.x_max > base.x_max,
"buffered x_max {} should be > base x_max {}",
buffered.x_max,
base.x_max
);
assert!(
buffered.y_max > base.y_max,
"buffered y_max {} should be > base y_max {}",
buffered.y_max,
base.y_max
);
let expected_buffer = ((1u64 << 30) * 8 / 4096) as u32;
assert_eq!(base.x_min - buffered.x_min, expected_buffer);
}
#[test]
fn test_world_bounds_buffer_saturates_at_edges() {
let tile = TileCoord::new(0, 0, 1);
let buffered = WorldBounds::from_tile_with_buffer(&tile, 8, 4096);
assert_eq!(buffered.x_min, 0); assert_eq!(buffered.y_min, 0); }
#[test]
fn test_world_bounds_round_trip_via_tile_bounds() {
let tile = TileCoord::new(4, 3, 3);
let world_bounds = WorldBounds::from_tile(&tile);
let tile_bounds = world_bounds.to_tile_bounds();
let world_bounds_rt = WorldBounds::from_tile_bounds(&tile_bounds);
let x_diff = (world_bounds.x_min as i64 - world_bounds_rt.x_min as i64).unsigned_abs();
let y_diff = (world_bounds.y_min as i64 - world_bounds_rt.y_min as i64).unsigned_abs();
assert!(
x_diff < 1000,
"x_min round-trip drift too large: {} vs {} (diff={})",
world_bounds.x_min,
world_bounds_rt.x_min,
x_diff
);
assert!(
y_diff < 1000,
"y_min round-trip drift too large: {} vs {} (diff={})",
world_bounds.y_min,
world_bounds_rt.y_min,
y_diff
);
}
}