93 lines
2.7 KiB
Python
93 lines
2.7 KiB
Python
"""Web Mercator projection helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
from .exceptions import ProjectionError
|
|
from .types import LatLon, Point
|
|
|
|
WEB_MERCATOR_MAX_LAT = 85.05112878
|
|
DEFAULT_TILE_SIZE = 256
|
|
|
|
|
|
def clamp_latitude(lat: float) -> float:
|
|
"""Clamp latitude to the Web Mercator representable range."""
|
|
|
|
return min(max(lat, -WEB_MERCATOR_MAX_LAT), WEB_MERCATOR_MAX_LAT)
|
|
|
|
|
|
def map_size(zoom: int, tile_size: int = DEFAULT_TILE_SIZE) -> int:
|
|
"""Return square pixel size of the full world at a zoom level."""
|
|
|
|
if zoom < 0:
|
|
raise ProjectionError("zoom must be >= 0")
|
|
if tile_size <= 0:
|
|
raise ProjectionError("tile_size must be > 0")
|
|
return tile_size * (2**zoom)
|
|
|
|
|
|
def latlon_to_world(lat: float, lon: float, zoom: int, tile_size: int = DEFAULT_TILE_SIZE) -> Point:
|
|
"""Project latitude/longitude to world pixel coordinates."""
|
|
|
|
size = map_size(zoom, tile_size)
|
|
lat = clamp_latitude(lat)
|
|
lon = ((lon + 180.0) % 360.0) - 180.0
|
|
|
|
sin_lat = math.sin(math.radians(lat))
|
|
x = (lon + 180.0) / 360.0 * size
|
|
y = (0.5 - math.log((1.0 + sin_lat) / (1.0 - sin_lat)) / (4.0 * math.pi)) * size
|
|
return (x, y)
|
|
|
|
|
|
def world_to_latlon(x: float, y: float, zoom: int, tile_size: int = DEFAULT_TILE_SIZE) -> LatLon:
|
|
"""Unproject world pixel coordinates to latitude/longitude."""
|
|
|
|
size = map_size(zoom, tile_size)
|
|
lon = x / size * 360.0 - 180.0
|
|
n = math.pi - 2.0 * math.pi * y / size
|
|
lat = math.degrees(math.atan(math.sinh(n)))
|
|
return (lat, lon)
|
|
|
|
|
|
def latlon_to_tile(lat: float, lon: float, zoom: int) -> tuple[int, int, int]:
|
|
"""Return the XYZ tile coordinate containing a latitude/longitude point."""
|
|
|
|
x, y = latlon_to_world(lat, lon, zoom)
|
|
scale = 2**zoom
|
|
tile_x = min(max(int(x // DEFAULT_TILE_SIZE), 0), scale - 1)
|
|
tile_y = min(max(int(y // DEFAULT_TILE_SIZE), 0), scale - 1)
|
|
return (tile_x, tile_y, zoom)
|
|
|
|
|
|
def world_to_screen(
|
|
world_x: float,
|
|
world_y: float,
|
|
*,
|
|
center: LatLon,
|
|
zoom: int,
|
|
width: int,
|
|
height: int,
|
|
tile_size: int = DEFAULT_TILE_SIZE,
|
|
) -> Point:
|
|
"""Convert world pixels to screen pixels for a centered viewport."""
|
|
|
|
center_x, center_y = latlon_to_world(center[0], center[1], zoom, tile_size)
|
|
return (world_x - center_x + width / 2.0, world_y - center_y + height / 2.0)
|
|
|
|
|
|
def screen_to_world(
|
|
screen_x: float,
|
|
screen_y: float,
|
|
*,
|
|
center: LatLon,
|
|
zoom: int,
|
|
width: int,
|
|
height: int,
|
|
tile_size: int = DEFAULT_TILE_SIZE,
|
|
) -> Point:
|
|
"""Convert screen pixels to world pixels for a centered viewport."""
|
|
|
|
center_x, center_y = latlon_to_world(center[0], center[1], zoom, tile_size)
|
|
return (screen_x + center_x - width / 2.0, screen_y + center_y - height / 2.0)
|