SXXXXXXX_FlightMonitor/flightmonitor/controller/app_controller.py

452 lines
20 KiB
Python

# FlightMonitor/controller/app_controller.py
from queue import Queue, Empty as QueueEmpty
from typing import List, Optional, Dict, Any, TYPE_CHECKING
from ..data.opensky_live_adapter import OpenSkyLiveAdapter, AdapterMessage
from ..data import config as app_config
from ..utils.logger import get_logger
from ..data.storage import DataStorage
from ..data.aircraft_database_manager import AircraftDatabaseManager
from ..utils.gui_utils import GUI_STATUS_OK, GUI_STATUS_WARNING, GUI_STATUS_ERROR
from .aircraft_db_importer import AircraftDBImporter
from .live_data_processor import LiveDataProcessor
from .map_command_handler import MapCommandHandler
from .cleanup_manager import CleanupManager
if TYPE_CHECKING:
from ..gui.main_window import MainWindow
from ..gui.dialogs.import_progress_dialog import ImportProgressDialog
module_logger = get_logger(__name__)
ADAPTER_JOIN_TIMEOUT_SECONDS = 5.0
class AppController:
"""
Orchestrates the application's logic, acting as an intermediary
between the GUI (View) and the data layer (Model).
"""
def __init__(self):
self.main_window: Optional["MainWindow"] = None
self.live_adapter_thread: Optional[OpenSkyLiveAdapter] = None
self.is_live_monitoring_active: bool = False
self.flight_data_queue: Optional[Queue[AdapterMessage]] = None
self._active_bounding_box: Optional[Dict[str, float]] = None
self.data_storage: Optional[DataStorage] = None
self.aircraft_db_manager: Optional[AircraftDatabaseManager] = None
self.aircraft_db_importer: Optional[AircraftDBImporter] = None
self.live_data_processor: Optional[LiveDataProcessor] = None
self.map_command_handler: Optional[MapCommandHandler] = None
self.cleanup_manager: Optional[CleanupManager] = None
self.active_detail_window_icao: Optional[str] = None
self.active_detail_window_ref: Optional["FullFlightDetailsWindow"] = None # type: ignore
try:
self.data_storage = DataStorage()
module_logger.info("DataStorage initialized successfully.")
except Exception as e:
module_logger.critical(f"CRITICAL: Failed to initialize DataStorage: {e}", exc_info=True)
self.data_storage = None
try:
self.aircraft_db_manager = AircraftDatabaseManager()
module_logger.info("AircraftDatabaseManager initialized successfully.")
except Exception as e:
module_logger.critical(f"CRITICAL: Failed to initialize AircraftDatabaseManager: {e}", exc_info=True)
self.aircraft_db_manager = None
module_logger.info("AppController initialized.")
def set_main_window(self, main_window_instance: "MainWindow"):
self.main_window = main_window_instance
module_logger.debug(f"Main window instance set in AppController.")
if self.aircraft_db_manager and self.main_window:
self.aircraft_db_importer = AircraftDBImporter(self.aircraft_db_manager, self.main_window)
module_logger.info("AircraftDBImporter initialized successfully.")
else:
module_logger.warning("AircraftDBImporter could not be initialized.")
self.flight_data_queue = Queue(maxsize=200)
self.live_data_processor = LiveDataProcessor(self, self.flight_data_queue)
self.map_command_handler = MapCommandHandler(self)
self.cleanup_manager = CleanupManager(self)
module_logger.info("Controller sub-components (Processor, Handler, Cleanup) initialized.")
initial_status_msg = "System Initialized. Ready."
initial_status_level = GUI_STATUS_OK
if not self.data_storage:
err_msg_ds = "Data storage init failed. History will not be saved."
initial_status_msg, initial_status_level = err_msg_ds, GUI_STATUS_ERROR
if not self.aircraft_db_manager:
err_msg_adb = "Aircraft DB init failed. Static details may be unavailable."
if initial_status_level == GUI_STATUS_OK:
initial_status_msg, initial_status_level = err_msg_adb, GUI_STATUS_WARNING
else:
initial_status_msg += f" {err_msg_adb}"
initial_status_level = GUI_STATUS_ERROR
if self.main_window and self.main_window.root.winfo_exists():
self.main_window.update_semaphore_and_status(initial_status_level, initial_status_msg)
def start_live_monitoring(self):
if not self.main_window:
module_logger.error("Controller: Main window not set for live monitoring.")
return
bounding_box = self.main_window.get_bounding_box_from_gui()
if not bounding_box:
err_msg = "Controller: Bounding box from GUI is invalid or missing."
if hasattr(self.main_window, "_reset_gui_to_stopped_state"):
self.main_window._reset_gui_to_stopped_state(f"Start failed: {err_msg}")
return
if hasattr(self.main_window, "clear_all_views_data"):
self.main_window.clear_all_views_data()
if hasattr(self.main_window, "set_monitoring_button_states"):
self.main_window.set_monitoring_button_states(True)
if not self.data_storage and hasattr(self.main_window, "update_semaphore_and_status"):
self.main_window.update_semaphore_and_status(
GUI_STATUS_WARNING, "DataStorage N/A. History will not be saved."
)
if self.is_live_monitoring_active:
module_logger.warning("Live monitoring requested but already active.")
return
module_logger.info(f"Controller: Starting live monitoring for bbox: {bounding_box}")
self._active_bounding_box = bounding_box
if self.main_window.map_manager_instance:
self.main_window.map_manager_instance.set_target_bbox(bounding_box)
if self.flight_data_queue:
while not self.flight_data_queue.empty():
try:
self.flight_data_queue.get_nowait()
except QueueEmpty:
break
if not self.live_data_processor:
module_logger.critical("Controller: LiveDataProcessor not initialized.")
if hasattr(self.main_window, "_reset_gui_to_stopped_state"):
self.main_window._reset_gui_to_stopped_state("Start failed: Internal error.")
return
self.live_adapter_thread = OpenSkyLiveAdapter(
output_queue=self.flight_data_queue,
bounding_box=self._active_bounding_box,
polling_interval=app_config.LIVE_POLLING_INTERVAL_SECONDS,
)
self.is_live_monitoring_active = True
self.live_adapter_thread.start()
self.live_data_processor.start_processing_queue()
def stop_live_monitoring(self, from_error: bool = False):
if not self.is_live_monitoring_active and not from_error:
if hasattr(self.main_window, "_reset_gui_to_stopped_state"):
self.main_window._reset_gui_to_stopped_state("Monitoring already stopped.")
return
module_logger.info(f"Controller: Stopping live monitoring (from_error={from_error}).")
self.is_live_monitoring_active = False
if self.live_data_processor:
self.live_data_processor.stop_processing_queue()
if self.live_adapter_thread and self.live_adapter_thread.is_alive():
self.live_adapter_thread.stop()
self.live_adapter_thread.join(timeout=ADAPTER_JOIN_TIMEOUT_SECONDS)
self.live_adapter_thread = None
if hasattr(self.main_window, "_reset_gui_to_stopped_state"):
msg = "Monitoring stopped due to an error." if from_error else "Monitoring stopped. Last view retained."
self.main_window._reset_gui_to_stopped_state(msg)
def on_application_exit(self):
if self.cleanup_manager:
self.cleanup_manager.on_application_exit()
else:
module_logger.critical("CleanupManager not initialized. Cannot perform proper cleanup.")
def on_function_tab_changed(self, tab_text: str):
module_logger.info(f"Controller notified of function tab change to: {tab_text}")
if self.main_window:
self.main_window.clear_all_views_data()
placeholder = "Select a mode to begin."
if "Live Monitor" in tab_text:
placeholder = "Define area and press Start Live Monitoring."
elif "Historical Download" in tab_text:
placeholder = "Historical Download - Coming Soon"
elif "Playback" in tab_text:
placeholder = "Playback - Coming Soon"
if hasattr(self.main_window, "_update_map_placeholder"):
self.main_window._update_map_placeholder(placeholder)
def import_aircraft_database_from_file_with_progress(
self, csv_filepath: str, progress_dialog_ref: "ImportProgressDialog"
):
if not self.aircraft_db_importer:
module_logger.error("AircraftDBImporter not initialized. Cannot import.")
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
if self.main_window and self.main_window.root.winfo_exists():
self.main_window.root.after(0, lambda: progress_dialog_ref.import_finished(
False, "Error: Import function not initialized correctly."
))
return
self.aircraft_db_importer.import_aircraft_database_with_progress(csv_filepath, progress_dialog_ref)
def get_historical_track_for_icao(self, icao24: str) -> List[Dict[str, Any]]:
"""
Retrieves the historical track for a given ICAO on the current day,
only considering points within the configured time window for continuity.
"""
if not self.data_storage or not icao24:
return []
try:
from datetime import datetime, timezone, timedelta
# Calcola il timestamp limite
max_age_hours = getattr(app_config, "TRACK_MAX_AGE_HOURS_FOR_CONTINUITY", 4)
current_utc_dt = datetime.now(timezone.utc)
since_dt = current_utc_dt - timedelta(hours=max_age_hours)
since_timestamp = since_dt.timestamp()
track_states = self.data_storage.get_flight_track_for_icao_on_date(
icao24,
current_utc_dt, # Passiamo la data corrente
since_timestamp=since_timestamp # Passiamo il nuovo filtro
)
if track_states:
return [state.to_dict() for state in track_states]
except Exception as e:
module_logger.error(f"Controller: Error retrieving historical track for {icao24}: {e}", exc_info=True)
return []
def request_detailed_flight_info(self, icao24: str):
if not self.main_window: return
normalized_icao24 = icao24.lower().strip()
if not normalized_icao24:
if hasattr(self.main_window, "update_selected_flight_details"):
self.main_window.update_selected_flight_details(None)
return
combined_details = {"icao24": normalized_icao24}
if self.main_window.map_manager_instance:
map_mgr = self.main_window.map_manager_instance
with map_mgr._map_data_lock:
for state in map_mgr._current_flights_to_display_gui:
if state.icao24 == normalized_icao24:
combined_details.update(state.to_dict())
break
if self.aircraft_db_manager:
static_data = self.aircraft_db_manager.get_aircraft_details(normalized_icao24)
if static_data:
for k, v in static_data.items():
if k not in combined_details or combined_details[k] is None:
combined_details[k] = v
if hasattr(self.main_window, "update_selected_flight_details"):
self.main_window.update_selected_flight_details(combined_details)
def request_and_show_full_flight_details(self, icao24: str):
normalized_icao24 = icao24.lower().strip()
module_logger.info(
f"Controller: Requesting to show full details for ICAO24: {normalized_icao24}"
)
if (
not self.main_window
or not hasattr(self.main_window, "root")
or not self.main_window.root.winfo_exists()
):
module_logger.error(
"Controller: MainWindow not available to show full flight details."
)
return
if not normalized_icao24:
module_logger.warning("Controller: Empty ICAO24 for full details request.")
if hasattr(self.main_window, "show_info_message"):
self.main_window.show_info_message(
"Flight Details", "No ICAO24 provided for full details."
)
return
if (
self.active_detail_window_ref
and self.active_detail_window_ref.winfo_exists()
):
if self.active_detail_window_icao == normalized_icao24:
module_logger.info(
f"Detail window for {normalized_icao24} already open. Re-focusing/Re-populating."
)
self.active_detail_window_ref.lift()
self.active_detail_window_ref.focus_set()
# Anche se la finestra esiste, la ripopoliamo con i dati più recenti
else:
module_logger.info(
f"Closing existing detail window for {self.active_detail_window_icao} before opening new one for {normalized_icao24}."
)
try:
# Usiamo destroy() per chiudere la vecchia finestra
self.active_detail_window_ref.destroy()
except tk.TclError:
pass # La finestra potrebbe essere già stata chiusa dall'utente
# Raccogliamo i dati necessari
static_data: Optional[Dict[str, Any]] = None
if self.aircraft_db_manager:
static_data = self.aircraft_db_manager.get_aircraft_details(
normalized_icao24
)
live_data: Optional[Dict[str, Any]] = None
if (
self.main_window
and self.main_window.map_manager_instance
and hasattr(
self.main_window.map_manager_instance, "_current_flights_to_display_gui"
)
):
map_mgr = self.main_window.map_manager_instance
with map_mgr._map_data_lock:
for state in map_mgr._current_flights_to_display_gui:
if state.icao24 == normalized_icao24:
live_data = state.to_dict()
break
full_track_data_list: List[Dict[str, Any]] = []
if self.data_storage:
try:
from datetime import datetime, timezone
current_utc_date = datetime.now(timezone.utc)
track_states = self.data_storage.get_flight_track_for_icao_on_date(
normalized_icao24, current_utc_date
)
if track_states:
full_track_data_list = [state.to_dict() for state in track_states]
except Exception as e_track:
module_logger.error(
f"FullDetails: Error retrieving historical track for {normalized_icao24}: {e_track}",
exc_info=True,
)
# Creiamo e mostriamo la finestra
try:
from ..gui.dialogs.full_flight_details_window import FullFlightDetailsWindow
# Creiamo sempre una nuova finestra dopo aver chiuso la vecchia se necessario
details_win = FullFlightDetailsWindow(
self.main_window.root, normalized_icao24, self
)
self.active_detail_window_ref = details_win
self.active_detail_window_icao = normalized_icao24
# Passiamo un riferimento anche alla main_window per coerenza
if self.main_window:
self.main_window.full_flight_details_window = details_win
# Popoliamo la finestra con i dati
details_win.update_details(static_data, live_data, full_track_data_list)
except ImportError:
module_logger.error(
"FullFlightDetailsWindow class not found. Cannot display full details."
)
if hasattr(self.main_window, "show_error_message"):
self.main_window.show_error_message(
"UI Error", "Could not open full details window (import error)."
)
except Exception as e_show_details:
module_logger.error(
f"Error showing full flight details window for {normalized_icao24}: {e_show_details}",
exc_info=True,
)
if hasattr(self.main_window, "show_error_message"):
self.main_window.show_error_message(
"Error", f"Could not display full details: {e_show_details}"
)
self.active_detail_window_ref = None
self.active_detail_window_icao = None
if self.main_window:
self.main_window.full_flight_details_window = None # La logica qui dentro è complessa e non sembra necessitare modifiche per ora
def details_window_closed(self, closed_icao24: str):
if self.cleanup_manager:
self.cleanup_manager.details_window_closed(closed_icao24)
else:
module_logger.warning("CleanupManager not initialized for details_window_closed.")
# --- Delegations to MapCommandHandler ---
def on_map_left_click(self, latitude: Optional[float], longitude: Optional[float], canvas_x: int, canvas_y: int, screen_x: int, screen_y: int):
if self.map_command_handler:
self.map_command_handler.on_map_left_click(latitude, longitude, canvas_x, canvas_y, screen_x, screen_y)
else:
module_logger.warning("Controller: MapCommandHandler not initialized for on_map_left_click.")
def on_map_right_click(self, latitude: float, longitude: float, screen_x: int, screen_y: int):
if self.map_command_handler:
self.map_command_handler.on_map_right_click(latitude, longitude, screen_x, screen_y)
else:
module_logger.warning("Controller: MapCommandHandler not initialized for on_map_right_click.")
def recenter_map_at_coords(self, lat: float, lon: float):
if self.map_command_handler:
self.map_command_handler.recenter_map_at_coords(lat, lon)
else:
module_logger.warning("Controller: MapCommandHandler not initialized for recenter_map_at_coords.")
def set_bbox_around_coords(self, center_lat: float, center_lon: float, area_size_km: float):
if self.map_command_handler:
self.map_command_handler.set_bbox_around_coords(center_lat, center_lon, area_size_km)
else:
module_logger.warning("Controller: MapCommandHandler not initialized for set_bbox_around_coords.")
def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]):
if self.main_window:
self.main_window.update_bbox_gui_fields(bbox_dict)
else:
module_logger.warning("Controller: MainWindow not set for update_bbox_gui_fields.")
def update_general_map_info(self):
if self.map_command_handler:
self.map_command_handler.update_general_map_info()
else:
module_logger.warning("Controller: MapCommandHandler not initialized for update_general_map_info.")
def map_zoom_in(self):
if self.map_command_handler:
self.map_command_handler.map_zoom_in()
def map_zoom_out(self):
if self.map_command_handler:
self.map_command_handler.map_zoom_out()
def map_pan_direction(self, direction: str):
if self.map_command_handler:
self.map_command_handler.map_pan_direction(direction)
def map_center_on_coords_and_fit_patch(self, lat: float, lon: float, patch_size_km: float):
if self.map_command_handler:
self.map_command_handler.map_center_on_coords_and_fit_patch(lat, lon, patch_size_km)
def set_map_track_length(self, length: int):
if self.main_window and self.main_window.map_manager_instance:
self.main_window.map_manager_instance.set_max_track_points(length)
else:
module_logger.warning("Controller: Cannot set track length, map manager not available.")