# FlightMonitor/controller/app_controller.py from queue import Queue, Empty as QueueEmpty from typing import List, Optional, Dict, Any, TYPE_CHECKING from tkinter import simpledialog 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 from ..data.area_profile_manager import AreaProfileManager 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.profile_manager: Optional[AreaProfileManager] = 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 try: self.profile_manager = AreaProfileManager() # NUOVA RIGA module_logger.info("AreaProfileManager initialized successfully.") except Exception as e: module_logger.error(f"Failed to initialize AreaProfileManager: {e}", exc_info=True) 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.") def get_profile_names(self) -> List[str]: """Returns the list of available profile names.""" if self.profile_manager: return self.profile_manager.get_profile_names() return [] def load_area_profile(self, profile_name: str): """Loads a profile and updates the GUI and map.""" if not self.profile_manager: module_logger.error("Profile manager not available.") return profile_data = self.profile_manager.get_profile_data(profile_name) if profile_data: module_logger.info(f"Loading profile: '{profile_name}'") # Aggiorna i campi della GUI if self.main_window: self.main_window.update_bbox_gui_fields(profile_data) # Aggiorna la mappa if self.main_window and self.main_window.map_manager_instance: self.main_window.map_manager_instance.set_target_bbox(profile_data) else: module_logger.warning(f"Profile '{profile_name}' not found.") def save_current_area_as_profile(self): """Saves the current BBox values as a new profile.""" if not self.profile_manager or not self.main_window: module_logger.error("Profile manager or main window not available.") return current_bbox = self.main_window.get_bounding_box_from_gui() if not current_bbox: self.main_window.show_error_message("Save Error", "The current Bounding Box values are invalid.") return # Chiedi all'utente un nome per il nuovo profilo profile_name = simpledialog.askstring( "Save Profile", "Enter a name for the new area profile:", parent=self.main_window.root ) if not profile_name or not profile_name.strip(): module_logger.info("Profile save cancelled by user.") return profile_name = profile_name.strip() if self.profile_manager.save_profile(profile_name, current_bbox): module_logger.info(f"Successfully saved profile '{profile_name}'.") # Aggiorna la ComboBox nella GUI if hasattr(self.main_window, "area_management_panel"): self.main_window.area_management_panel.update_profile_list() self.main_window.area_management_panel.set_selected_profile(profile_name) self.main_window.show_info_message("Success", f"Profile '{profile_name}' saved successfully.") else: self.main_window.show_error_message("Save Error", f"Could not save profile '{profile_name}'. The name might be invalid or reserved.") def delete_area_profile(self, profile_name: str): """Deletes a user-defined profile.""" if not self.profile_manager or not self.main_window: module_logger.error("Profile manager or main window not available.") return if self.profile_manager.delete_profile(profile_name): module_logger.info(f"Successfully deleted profile '{profile_name}'.") # Aggiorna la ComboBox nella GUI if hasattr(self.main_window, "area_management_panel"): self.main_window.area_management_panel.update_profile_list() self.main_window.show_info_message("Success", f"Profile '{profile_name}' deleted.") else: self.main_window.show_error_message("Delete Error", f"Could not delete profile '{profile_name}'. It might be the default profile or not exist.")