# FlightMonitor/controller/playback_data_processor.py """ Manages the processing of playback data from the PlaybackAdapter's output queue. It updates the GUI's virtual clock, timeline, and dispatches flight states to the map. """ from queue import Queue, Empty as QueueEmpty from typing import Optional, TYPE_CHECKING import time from flightmonitor.utils.logger import get_logger from flightmonitor.data.data_constants import ( MSG_TYPE_FLIGHT_DATA, MSG_TYPE_ADAPTER_STATUS, STATUS_STOPPED, ) from flightmonitor.data.common_models import CanonicalFlightState if TYPE_CHECKING: from flightmonitor.controller.app_controller import AppController module_logger = get_logger(__name__) GUI_QUEUE_CHECK_INTERVAL_MS = 100 class PlaybackDataProcessor: """ Processes flight data snapshots from a queue for playback, updating the GUI (virtual clock, map, timeline slider) for each snapshot. """ def __init__( self, app_controller: "AppController", data_queue: Queue ): """ Initializes the PlaybackDataProcessor. Args: app_controller: The main AppController instance. data_queue: The queue from which to read playback data snapshots. """ self.app_controller = app_controller self.data_queue = data_queue self._gui_after_id: Optional[str] = None module_logger.debug("PlaybackDataProcessor initialized.") def start_processing(self): """Starts the periodic processing of the playback data queue.""" main_window = self.app_controller.main_window if not (main_window and main_window.root and main_window.root.winfo_exists()): module_logger.error( "PlaybackDataProcessor: Cannot start queue processing, MainWindow or root is missing." ) return if self._gui_after_id: try: main_window.root.after_cancel(self._gui_after_id) except Exception: pass module_logger.info( "PlaybackDataProcessor: Starting playback data queue processing loop." ) self._gui_after_id = main_window.root.after( GUI_QUEUE_CHECK_INTERVAL_MS, self._process_queue_cycle ) def stop_processing(self): """Stops the periodic processing loop.""" main_window = self.app_controller.main_window if self._gui_after_id and main_window and main_window.root.winfo_exists(): try: main_window.root.after_cancel(self._gui_after_id) module_logger.info( "PlaybackDataProcessor: Stopped queue processing loop." ) except Exception: pass finally: self._gui_after_id = None if self.data_queue: while not self.data_queue.empty(): try: self.data_queue.get_nowait() except QueueEmpty: break def _process_queue_cycle(self): """ The core processing cycle that runs on the GUI thread. It retrieves a data snapshot, updates the clock, and updates the map. """ main_window = self.app_controller.main_window if not (main_window and main_window.root and main_window.root.winfo_exists()): self._gui_after_id = None return try: message = self.data_queue.get_nowait() message_type = message.get("type") if message_type == MSG_TYPE_FLIGHT_DATA: snapshot_timestamp = message.get("timestamp") payload_dict = message.get("payload", {}) flight_states: List[CanonicalFlightState] = payload_dict.get( "canonical", [] ) playback_panel = ( main_window.function_notebook_panel.playback_panel if hasattr(main_window.function_notebook_panel, "playback_panel") else None ) if snapshot_timestamp and playback_panel: playback_panel.update_virtual_clock(snapshot_timestamp) if self.app_controller.playback_adapter_thread: start_ts = self.app_controller.playback_adapter_thread.start_ts end_ts = self.app_controller.playback_adapter_thread.end_ts playback_panel.update_timeline(snapshot_timestamp, start_ts, end_ts) if main_window.map_manager_instance: main_window.map_manager_instance.update_playback_frame( flight_states, snapshot_timestamp ) elif message_type == MSG_TYPE_ADAPTER_STATUS: if message.get("status_code") == STATUS_STOPPED: module_logger.info( "Playback adapter finished. Stopping playback from processor." ) self.app_controller.stop_playback(finished_normally=True) self.data_queue.task_done() except QueueEmpty: pass except Exception as e: module_logger.error(f"Error in PlaybackDataProcessor cycle: {e}", exc_info=True) self.app_controller.stop_playback(from_error=True) if self.app_controller.is_playback_active: if main_window and main_window.root and main_window.root.winfo_exists(): self._gui_after_id = main_window.root.after( GUI_QUEUE_CHECK_INTERVAL_MS, self._process_queue_cycle ) else: self._gui_after_id = None else: self._gui_after_id = None