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

104 lines
5.5 KiB
Python

"""Drawing helpers migrated from original project; adapted to new package layout.
"""
import logging
from typing import Optional, Tuple, List, Dict
try:
from PIL import Image, ImageDraw, ImageFont
PIL_LIB_AVAILABLE_DRAWING = True
except ImportError:
Image = None # type: ignore
ImageDraw = None # type: ignore
ImageFont = None # type: ignore
PIL_LIB_AVAILABLE_DRAWING = False
logging.error("MapDrawing: Pillow (PIL) library not found. Drawing operations will fail.")
try:
import cv2
import numpy as np
CV2_NUMPY_LIBS_AVAILABLE_DRAWING = True
except ImportError:
cv2 = None # type: ignore
np = None # type: ignore
CV2_NUMPY_LIBS_AVAILABLE_DRAWING = False
logging.warning("MapDrawing: OpenCV or NumPy not found. Some drawing operations (markers) will be disabled.")
try:
import mercantile
MERCANTILE_LIB_AVAILABLE_DRAWING = True
except ImportError:
mercantile = None # type: ignore
MERCANTILE_LIB_AVAILABLE_DRAWING = False
logging.error("MapDrawing: 'mercantile' library not found. Coordinate conversions will fail.")
logger = logging.getLogger(__name__)
# Prefer local utils import name
from .utils import get_hgt_tile_geographic_bounds
from .utils import MapCalculationError
# Fallback style constants
DEM_BOUNDARY_COLOR = "red"
DEM_BOUNDARY_THICKNESS_PX = 3
AREA_BOUNDARY_COLOR = "blue"
AREA_BOUNDARY_THICKNESS_PX = 2
TILE_TEXT_COLOR = "white"
TILE_TEXT_BG_COLOR = "rgba(0, 0, 0, 150)"
DEM_TILE_LABEL_BASE_FONT_SIZE = 12
DEM_TILE_LABEL_BASE_ZOOM = 10
_DEFAULT_FONT_FOR_LABELS = None
def _geo_to_pixel_on_unscaled_map(latitude_deg: float, longitude_deg: float, current_map_geo_bounds: Optional[Tuple[float, float, float, float]], current_stitched_map_pixel_shape: Optional[Tuple[int, int]]) -> Optional[Tuple[int, int]]:
if not MERCANTILE_LIB_AVAILABLE_DRAWING or current_map_geo_bounds is None or current_stitched_map_pixel_shape is None:
logger.warning("Map context incomplete or mercantile missing for geo_to_pixel_on_unscaled_map conversion.")
return None
unscaled_height, unscaled_width = current_stitched_map_pixel_shape
map_west_lon, map_south_lat, map_east_lon, map_north_lat = current_map_geo_bounds
try:
map_ul_merc_x, map_ul_merc_y = mercantile.xy(map_west_lon, map_north_lat) # type: ignore
map_lr_merc_x, map_lr_merc_y = mercantile.xy(map_east_lon, map_south_lat) # type: ignore
total_map_width_merc = abs(map_lr_merc_x - map_ul_merc_x)
total_map_height_merc = abs(map_ul_merc_y - map_lr_merc_y)
if total_map_width_merc <= 0 or total_map_height_merc <= 0:
logger.warning("Map Mercator extent is zero, cannot convert geo to pixel on unscaled map.")
return None
target_merc_x, target_merc_y = mercantile.xy(longitude_deg, latitude_deg) # type: ignore
relative_merc_x_in_map = (target_merc_x - map_ul_merc_x) / total_map_width_merc if total_map_width_merc > 0 else 0.0
relative_merc_y_in_map = (map_ul_merc_y - target_merc_y) / total_map_height_merc if total_map_height_merc > 0 else 0.0
pixel_x_on_unscaled = int(round(relative_merc_x_in_map * unscaled_width))
pixel_y_on_unscaled = int(round(relative_merc_y_in_map * unscaled_height))
px_clamped = max(0, min(pixel_x_on_unscaled, unscaled_width - 1))
py_clamped = max(0, min(pixel_y_on_unscaled, unscaled_height - 1))
return (px_clamped, py_clamped)
except Exception as e_geo_to_px_unscaled:
logger.exception(f"Error during geo_to_pixel_on_unscaled_map conversion: {e_geo_to_px_unscaled}")
return None
def draw_point_marker(pil_image_to_draw_on, latitude_deg: float, longitude_deg: float, current_map_geo_bounds: Optional[Tuple[float, float, float, float]], current_stitched_map_pixel_shape: Optional[Tuple[int, int]]):
if not PIL_LIB_AVAILABLE_DRAWING or pil_image_to_draw_on is None or current_map_geo_bounds is None or current_stitched_map_pixel_shape is None:
logger.warning("Cannot draw point marker: PIL image or map context missing.")
return pil_image_to_draw_on
pixel_coords_on_unscaled = _geo_to_pixel_on_unscaled_map(latitude_deg, longitude_deg, current_map_geo_bounds, current_stitched_map_pixel_shape)
if pixel_coords_on_unscaled:
px_clamped, py_clamped = pixel_coords_on_unscaled
logger.debug(f"Drawing point marker at unscaled pixel ({px_clamped},{py_clamped}) for geo ({latitude_deg:.5f},{longitude_deg:.5f})")
if CV2_NUMPY_LIBS_AVAILABLE_DRAWING and cv2 and np:
try:
if pil_image_to_draw_on.mode != 'RGB':
map_cv_bgr = cv2.cvtColor(np.array(pil_image_to_draw_on.convert('RGB')), cv2.COLOR_RGB2BGR) # type: ignore
else:
map_cv_bgr = cv2.cvtColor(np.array(pil_image_to_draw_on), cv2.COLOR_RGB2BGR) # type: ignore
cv2.drawMarker(map_cv_bgr, (px_clamped, py_clamped), (0, 0, 255), cv2.MARKER_CROSS, markerSize=15, thickness=2) # type: ignore
return Image.fromarray(cv2.cvtColor(map_cv_bgr, cv2.COLOR_BGR2RGB)) # type: ignore
except Exception as e_draw_click_cv:
logger.exception(f"Error drawing point marker with OpenCV: {e_draw_click_cv}")
return pil_image_to_draw_on
else:
logger.warning("CV2 or NumPy not available, cannot draw point marker using OpenCV.")
return pil_image_to_draw_on
else:
logger.warning(f"Geo-to-pixel conversion failed for ({latitude_deg:.5f},{longitude_deg:.5f}), cannot draw point marker.")
return pil_image_to_draw_on