# flightmonitor/map/map_drawing.py import logging import math from typing import Optional, Tuple, List, Dict, Any from collections import deque try: from PIL import Image, ImageDraw, ImageFont, ImageColor PIL_LIB_AVAILABLE_DRAWING = True except ImportError: Image, ImageDraw, ImageFont, ImageColor = None, None, None, None PIL_LIB_AVAILABLE_DRAWING = False import logging logging.error("MapDrawing: Pillow not found. Drawing disabled.") try: from .map_utils import geo_to_pixel_on_unscaled_map from ..data.common_models import CanonicalFlightState from . import map_constants from ..data import config as app_config logger = logging.getLogger(__name__) TRACK_LINE_WIDTH = getattr(app_config, "DEFAULT_TRACK_LINE_WIDTH", 2) except ImportError as e: logger = logging.getLogger(__name__) logger.error(f"MapDrawing failed to import dependencies: {e}. Using fallbacks.") # Fallback classes and constants if imports fail class CanonicalFlightState: pass class MockMapConstants: AREA_BOUNDARY_COLOR = "blue" AREA_BOUNDARY_THICKNESS_PX = 2 TILE_TEXT_BG_COLOR = "rgba(0, 0, 0, 150)" TILE_TEXT_COLOR = "white" DEFAULT_LABEL_FONT_PATH = None TRACK_COLOR_PALETTE = ["#FF0000", "#00FF00", "#0000FF"] map_constants = MockMapConstants() TRACK_LINE_WIDTH = 2 def geo_to_pixel_on_unscaled_map(*args, **kwargs): return None def draw_area_bounding_box( pil_image_to_draw_on: Any, area_geo_bbox: Tuple[float, float, float, float], current_map_geo_bounds: Optional[Tuple[float, float, float, float]], current_stitched_map_pixel_shape: Optional[Tuple[int, int]], color: str = "blue", thickness: int = 2, ) -> Any: if not (PIL_LIB_AVAILABLE_DRAWING and ImageDraw and pil_image_to_draw_on and current_map_geo_bounds and current_stitched_map_pixel_shape): return pil_image_to_draw_on west, south, east, north = area_geo_bbox corners_geo = [(west, north), (east, north), (east, south), (west, south)] pixel_corners: List[Tuple[int, int]] = [] for lon, lat in corners_geo: pixel_coords = geo_to_pixel_on_unscaled_map(lat, lon, current_map_geo_bounds, current_stitched_map_pixel_shape) if pixel_coords: pixel_corners.append(pixel_coords) else: logger.warning(f"Could not convert BBox corner {lat},{lon} to pixel. Aborting draw.") return pil_image_to_draw_on if len(pixel_corners) == 4: draw = ImageDraw.Draw(pil_image_to_draw_on) # Close the polygon by adding the first point at the end draw.line(pixel_corners + [pixel_corners[0]], fill=color, width=thickness) return pil_image_to_draw_on def _load_label_font(scaled_font_size: int) -> Optional[Any]: if not (PIL_LIB_AVAILABLE_DRAWING and ImageFont): return None scaled_font_size = max(1, scaled_font_size) font_path = getattr(map_constants, "DEFAULT_LABEL_FONT_PATH", None) if font_path: try: return ImageFont.truetype(font_path, scaled_font_size) except (IOError, OSError): logger.warning(f"Font file not found at {font_path}. Falling back.") except Exception as e: logger.error(f"Error loading TrueType font {font_path}: {e}") try: return ImageFont.load_default() except Exception as e: logger.error(f"Could not load default PIL font: {e}") return None def _calculate_faded_color(base_color_hex: str, index_from_end: int, total_visible_points: int, min_intensity: float = 0.1) -> Optional[Tuple[int, int, int, int]]: if not (0 <= index_from_end < total_visible_points and ImageColor): return None try: r, g, b = ImageColor.getrgb(base_color_hex) if total_visible_points <= 1: intensity = 1.0 else: ratio = (total_visible_points - 1 - index_from_end) / (total_visible_points - 1) intensity = min_intensity + (1.0 - min_intensity) * ratio return (r, g, b, int(max(0, min(255, intensity * 255)))) except Exception: return (128, 128, 128, 255) def _draw_single_flight( draw: Any, pixel_coords: Tuple[int, int], flight_state: CanonicalFlightState, label_font: Optional[Any], track_deque: Optional[deque], 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 and draw and pixel_coords and flight_state): return px, py = pixel_coords base_length, side_length = 8, 12 label_text = flight_state.callsign.strip() if flight_state.callsign and flight_state.callsign.strip() else flight_state.icao24 color_index = hash(flight_state.icao24) % len(map_constants.TRACK_COLOR_PALETTE) base_color = map_constants.TRACK_COLOR_PALETTE[color_index] if track_deque and len(track_deque) > 1: # Usiamo list() per creare una copia su cui iterare in sicurezza points_to_draw = list(track_deque) pixel_points = [geo_to_pixel_on_unscaled_map(p[0], p[1], current_map_geo_bounds, current_stitched_map_pixel_shape) for p in points_to_draw] pixel_points = [p for p in pixel_points if p is not None] # Filtra i punti non validi # Log di debug per la traccia if flight_state.icao24 == '300557': # Mantieni un ICAO di test logger.info(f"--- Drawing track for {flight_state.icao24} with {len(points_to_draw)} points ---") for i, point in enumerate(points_to_draw): logger.info(f" Point {i}: ts={int(point[2])}, lat={point[0]:.4f}, lon={point[1]:.4f}") logger.info("------------------------------------") if len(pixel_points) > 1: for i in range(len(pixel_points) - 1): start_point = pixel_points[i] end_point = pixel_points[i+1] faded_color = _calculate_faded_color(base_color, len(pixel_points) - 2 - i, len(pixel_points)) if faded_color: draw.line([start_point, end_point], fill=faded_color, width=TRACK_LINE_WIDTH) angle_rad = math.radians(90 - (flight_state.true_track_deg or 0.0)) tip = (px + side_length * math.cos(angle_rad), py - side_length * math.sin(angle_rad)) base1 = (px + (base_length / 2) * math.cos(angle_rad + math.pi/2), py - (base_length / 2) * math.sin(angle_rad + math.pi/2)) base2 = (px + (base_length / 2) * math.cos(angle_rad - math.pi/2), py - (base_length / 2) * math.sin(angle_rad - math.pi/2)) draw.polygon([tip, base1, base2], fill=base_color, outline="black") if label_font: try: anchor_x, anchor_y = px + 15, py bbox = draw.textbbox((anchor_x, anchor_y), label_text, font=label_font, anchor="lm") padding = 2 bg_bbox = (bbox[0] - padding, bbox[1] - padding, bbox[2] + padding, bbox[3] + padding) draw.rectangle(bg_bbox, fill=map_constants.TILE_TEXT_BG_COLOR) draw.text((anchor_x, anchor_y), label_text, fill=map_constants.TILE_TEXT_COLOR, font=label_font, anchor="lm") except Exception as e: logger.error(f"Error drawing label for '{label_text}': {e}", exc_info=False) def _draw_text_on_placeholder(draw: Any, image_size: Tuple[int, int], text: str): if not (PIL_LIB_AVAILABLE_DRAWING and draw): return img_w, img_h = image_size font = _load_label_font(12) if not font: draw.text((10, 10), text, fill="black") return try: bbox = draw.textbbox((0, 0), text, font=font, anchor="lt", spacing=4) text_w, text_h = bbox[2] - bbox[0], bbox[3] - bbox[1] pos_x, pos_y = (img_w - text_w) / 2, (img_h - text_h) / 2 draw.text((pos_x, pos_y), text, fill="black", font=font, spacing=4) except Exception as e: logger.warning(f"Error drawing placeholder text: {e}. Falling back.") draw.text((10, 10), text, fill="black")