# FlightMonitor/map/map_render_manager.py """ Manages the background worker thread responsible for rendering map images. Handles communication (requests, results) with the worker thread. """ import threading import queue import time import os from typing import Optional, Dict, Any, Callable, Tuple, TYPE_CHECKING try: from PIL import Image, ImageTk ImageType = Image.Image PhotoImageType = ImageTk.PhotoImage except ImportError: ImageType = Any PhotoImageType = Any from flightmonitor.utils.logger import get_logger from flightmonitor.data import config as app_config if TYPE_CHECKING: from flightmonitor.map.map_canvas_manager import MapCanvasManager module_logger = get_logger(__name__) MAP_WORKER_QUEUE_TIMEOUT_S = 0.1 # MODIFICATO: Sostituiti i tipi di richiesta RENDER_REQUEST_BASE_MAP = "render_base_map" # Richiesta per mappa di base + overlay RENDER_REQUEST_OVERLAY = "render_overlay" # Richiesta per solo overlay RENDER_REQUEST_TYPE_SHUTDOWN = "shutdown_worker" # Cartella per il debug delle immagini renderizzate DEBUG_IMAGE_SAVE_DIR = "map_render_debug" class MapRenderManager: """ Manages a dedicated worker thread for processing map rendering requests. Handles queuing of requests, retrieving rendered results, and worker shutdown. """ def __init__(self): self._map_render_request_queue: queue.Queue[Dict[str, Any]] = queue.Queue(maxsize=5) self._map_render_result_queue: queue.Queue[Dict[str, Any]] = queue.Queue(maxsize=5) self._map_worker_stop_event: threading.Event = threading.Event() self._map_worker_thread: Optional[threading.Thread] = None self._last_render_request_id: int = 0 self._expected_render_id_gui: int = 0 self._render_pipeline_callable: Optional[ Callable[..., Tuple[Optional[PhotoImageType], Optional[PhotoImageType], Optional[Tuple[float, ...]], Optional[str]]] ] = None # Crea la cartella di debug se necessario if getattr(app_config, "DEBUG_SAVE_RENDERED_IMAGES", False): try: os.makedirs(DEBUG_IMAGE_SAVE_DIR, exist_ok=True) module_logger.info(f"Debug image save directory is '{DEBUG_IMAGE_SAVE_DIR}'") except OSError as e: module_logger.error(f"Could not create debug image directory: {e}") module_logger.debug("MapRenderManager initialized.") def set_render_pipeline_callable( self, render_pipeline_callable: Callable[ ..., Tuple[Optional[PhotoImageType], Optional[PhotoImageType], Optional[Tuple[float, ...]], Optional[str]] ], ): self._render_pipeline_callable = render_pipeline_callable module_logger.debug("MapRenderManager: Render pipeline callable set.") def start_worker(self): if self._map_worker_thread is not None and self._map_worker_thread.is_alive(): module_logger.warning("MapRenderManager: Map worker thread already running.") return self._map_worker_stop_event.clear() self._map_worker_thread = threading.Thread( target=self._map_render_worker_target, name="MapRenderWorker", daemon=True, ) self._map_worker_thread.start() module_logger.info("MapRenderManager: Map rendering worker thread started successfully.") def shutdown_worker(self): module_logger.info("MapRenderManager: Shutdown worker requested.") self._map_worker_stop_event.set() try: self._map_render_request_queue.put_nowait( {"type": RENDER_REQUEST_TYPE_SHUTDOWN, "request_id": -999} ) except queue.Full: pass if self._map_worker_thread and self._map_worker_thread.is_alive(): self._map_worker_thread.join(timeout=2.0) self._map_worker_thread = None while not self._map_render_request_queue.empty(): try: self._map_render_request_queue.get_nowait() except Exception: break while not self._map_render_result_queue.empty(): try: self._map_render_result_queue.get_nowait() except Exception: break module_logger.info("MapRenderManager: Worker shutdown sequence complete.") def _save_debug_image(self, image: ImageType, request_id: int, image_type: str): """Helper to save images for debugging.""" if not getattr(app_config, "DEBUG_SAVE_RENDERED_IMAGES", False) or image is None: return try: filename = f"{DEBUG_IMAGE_SAVE_DIR}/req_{request_id}_{image_type}_{int(time.time())}.png" image.save(filename, "PNG") except Exception as e: module_logger.error(f"Failed to save debug image {filename}: {e}") def _map_render_worker_target(self): thread_name = threading.current_thread().name module_logger.debug(f"{thread_name}: Worker thread target loop started.") if not self._render_pipeline_callable: module_logger.critical(f"{thread_name}: Render pipeline callable not set. Worker cannot function. Exiting.") return while not self._map_worker_stop_event.is_set(): try: request_data = self._map_render_request_queue.get(timeout=MAP_WORKER_QUEUE_TIMEOUT_S) request_type = request_data.get("type") request_id = request_data.get("request_id", -1) if request_type == RENDER_REQUEST_TYPE_SHUTDOWN: break base_map_pil, overlay_pil, map_bounds_result, error_msg = None, None, None, None # Unpack common parameters render_params = request_data.get("params", {}) if request_type == RENDER_REQUEST_BASE_MAP: base_map_pil, overlay_pil, map_bounds_result, error_msg = self._render_pipeline_callable( render_params, render_base=True, render_overlay=True ) # Now we save the PIL images, which have the .save() method self._save_debug_image(base_map_pil, request_id, "base") self._save_debug_image(overlay_pil, request_id, "overlay_full") elif request_type == RENDER_REQUEST_OVERLAY: # For overlay-only, we don't need to re-render the base map _, overlay_pil, _, error_msg = self._render_pipeline_callable( render_params, render_base=False, render_overlay=True ) self._save_debug_image(overlay_pil, request_id, "overlay_only") else: error_msg = f"Unknown request type: {request_type}" if self._map_worker_stop_event.is_set(): break # Convert to PhotoImage for the GUI thread base_map_photo_result = ImageTk.PhotoImage(base_map_pil) if base_map_pil else None overlay_photo_result = ImageTk.PhotoImage(overlay_pil) if overlay_pil else None result_payload = { "request_id": request_id, "type": request_type, "base_map_photo": base_map_photo_result, "overlay_photo": overlay_photo_result, "map_geo_bounds": map_bounds_result, "error": error_msg, } self._map_render_result_queue.put_nowait(result_payload) except queue.Empty: continue except Exception as e: module_logger.exception(f"{thread_name}: Unhandled exception in worker loop: {e}") time.sleep(0.5) module_logger.info(f"{thread_name}: Worker thread target loop finished.") def put_render_request(self, request_type: str, render_params: Dict[str, Any]) -> Optional[int]: self._last_render_request_id += 1 current_request_id = self._last_render_request_id request_payload = { "request_id": current_request_id, "type": request_type, "params": render_params } try: self._map_render_request_queue.put_nowait(request_payload) return current_request_id except queue.Full: module_logger.warning(f"MapRenderManager: Render request queue FULL. Request ID {current_request_id} was DROPPED.") return None def get_render_result(self) -> Optional[Dict[str, Any]]: try: return self._map_render_result_queue.get_nowait() except queue.Empty: return None def get_expected_gui_render_id(self) -> int: return self._expected_render_id_gui def set_expected_gui_render_id(self, request_id: int): self._expected_render_id_gui = request_id def is_worker_alive(self) -> bool: return self._map_worker_thread is not None and self._map_worker_thread.is_alive()