python-map-manager/map_manager/engine.py
2025-12-02 09:09:22 +01:00

93 lines
3.9 KiB
Python

"""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)