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}