Files
gps-denied-onboard/h06_web_mercator_utils.py
Denys Zaitsev d7e1066c60 Initial commit
2026-04-03 23:25:54 +03:00

53 lines
2.3 KiB
Python

import math
from typing import Tuple, Dict, Any
from pydantic import BaseModel
from abc import ABC, abstractmethod
class TileBounds(BaseModel):
nw: Tuple[float, float]
ne: Tuple[float, float]
sw: Tuple[float, float]
se: Tuple[float, float]
center: Tuple[float, float]
gsd: float
class IWebMercatorUtils(ABC):
@abstractmethod
def latlon_to_tile(self, lat: float, lon: float, zoom: int) -> Tuple[int, int]: pass
@abstractmethod
def tile_to_latlon(self, x: int, y: int, zoom: int) -> Tuple[float, float]: pass
@abstractmethod
def compute_tile_bounds(self, x: int, y: int, zoom: int) -> TileBounds: pass
@abstractmethod
def get_zoom_gsd(self, lat: float, zoom: int) -> float: pass
class WebMercatorUtils(IWebMercatorUtils):
"""H06: Web Mercator projection (EPSG:3857) for tile coordinates."""
def latlon_to_tile(self, lat: float, lon: float, zoom: int) -> Tuple[int, int]:
lat_rad = math.radians(lat)
n = 2.0 ** zoom
return int((lon + 180.0) / 360.0 * n), int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)
def tile_to_latlon(self, x: int, y: int, zoom: int) -> Tuple[float, float]:
n = 2.0 ** zoom
lat_rad = math.atan(math.sinh(math.pi * (1.0 - 2.0 * y / n)))
return math.degrees(lat_rad), x / n * 360.0 - 180.0
def get_zoom_gsd(self, lat: float, zoom: int) -> float:
return 156543.03392 * math.cos(math.radians(lat)) / (2.0 ** zoom)
def compute_tile_bounds(self, x: int, y: int, zoom: int) -> TileBounds:
center = self.tile_to_latlon(x + 0.5, y + 0.5, zoom)
return TileBounds(
nw=self.tile_to_latlon(x, y, zoom), ne=self.tile_to_latlon(x + 1, y, zoom),
sw=self.tile_to_latlon(x, y + 1, zoom), se=self.tile_to_latlon(x + 1, y + 1, zoom),
center=center, gsd=self.get_zoom_gsd(center[0], zoom)
)
# Module-level proxies for backward compatibility with F04
_instance = WebMercatorUtils()
def latlon_to_tile(lat, lon, zoom): return _instance.latlon_to_tile(lat, lon, zoom)
def tile_to_latlon(x, y, zoom): return _instance.tile_to_latlon(x, y, zoom)
def compute_tile_bounds(x, y, zoom):
b = _instance.compute_tile_bounds(x, y, zoom)
return {"nw": b.nw, "ne": b.ne, "sw": b.sw, "se": b.se, "center": b.center, "gsd": b.gsd}