216 lines
8.9 KiB
Python
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() |