93 lines
3.9 KiB
Python
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)
|