"""MapEngine: facade for map retrieval operations (inside package). Minimal implementation: constructs a MapTileManager and exposes `get_image_for_area` and `get_image_for_point` helper methods. """ from typing import Optional, Tuple from .tile_manager import MapTileManager from .services import get_map_service_instance class MapEngine: """Facade for map retrieval operations. Features: - can compute an appropriate zoom level for a bounding box based on a `max_size` in pixels and the tile size - exposes `get_image_for_area(bbox, zoom=None, max_size=None)` where zoom or max_size (or both) can be provided """ def __init__(self, service_name: str = 'osm', cache_dir: Optional[str] = None, enable_online: bool = True): service = get_map_service_instance(service_name) if service is None: raise ValueError(f"Unknown map service: {service_name}") self.tile_manager = MapTileManager(service, cache_root_directory=cache_dir, enable_online_tile_fetching=enable_online) def _choose_zoom_for_bbox(self, bbox: Tuple[float, float, float, float], max_size_pixels: int) -> Optional[int]: """Choose the highest zoom level that results in a stitched image not exceeding `max_size_pixels` in both dimensions. Strategy: iterate from service max zoom down to 0 and return the first zoom where the pixel width and height (num_tiles * tile_size) fit. """ try: from .utils import get_tile_ranges_for_bbox except Exception: return None tile_size = self.tile_manager.tile_size max_zoom = self.tile_manager.map_service.max_zoom for z in range(max_zoom, -1, -1): ranges = get_tile_ranges_for_bbox(bbox, z) if not ranges: continue x_range, y_range = ranges num_w = (x_range[1] - x_range[0]) + 1 num_h = (y_range[1] - y_range[0]) + 1 px_w = num_w * tile_size px_h = num_h * tile_size if px_w <= max_size_pixels and px_h <= max_size_pixels: return z # If no zoom fits the requested max size, return the lowest zoom (0) return 0 def get_image_for_area(self, bbox: Tuple[float, float, float, float], zoom: Optional[int] = None, max_size: Optional[int] = None, progress_callback=None) -> Optional[object]: """Return a stitched PIL image covering `bbox`. Parameters: - bbox: (west, south, east, north) - zoom: explicit zoom level (if provided, used as-is) - max_size: maximum pixel size (width and height) for the returned image; if `zoom` is None, engine will compute a suitable zoom. """ if zoom is None and max_size is not None: zoom = self._choose_zoom_for_bbox(bbox, max_size) if zoom is None: # Default to a conservative zoom (middle range) zoom = min(12, self.tile_manager.map_service.max_zoom) from .utils import get_tile_ranges_for_bbox tile_ranges = get_tile_ranges_for_bbox(bbox, zoom) if not tile_ranges: return None return self.tile_manager.stitch_map_image(zoom, tile_ranges[0], tile_ranges[1], progress_callback=progress_callback) def get_image_for_point(self, lat: float, lon: float, zoom: int, tiles_radius: int = 1, progress_callback=None) -> Optional[object]: """Return a stitched image centered on (lat,lon) using a small tile window. `tiles_radius=1` returns a 3x3 tile image. """ try: import mercantile except Exception: return None center_tile = mercantile.tile(lon, lat, zoom) x, y = center_tile.x, center_tile.y range_x = (x - tiles_radius, x + tiles_radius) range_y = (y - tiles_radius, y + tiles_radius) return self.tile_manager.stitch_map_image(zoom, range_x, range_y, progress_callback=progress_callback)