SXXXXXXX_FlightMonitor/flightmonitor/map/map_render_manager.py
2025-06-17 10:09:57 +02:00

216 lines
8.9 KiB
Python

# 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()