SXXXXXXX_FlightMonitor/flightmonitor/controller/playback_data_processor.py

153 lines
5.7 KiB
Python

# 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