step 1: lock public api and pure core

This commit is contained in:
2026-05-22 18:21:01 +02:00
parent 11fc1bb9bd
commit bd1ce7abff
14 changed files with 885 additions and 6 deletions

View File

@@ -1 +1,92 @@
"""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)