# FlightMonitor/controller/app_controller.py from queue import Queue, Empty as QueueEmpty import time import os import csv import copy from datetime import datetime, timezone 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.common_models import CanonicalFlightState from ..data.aircraft_database_manager import AircraftDatabaseManager from typing import List, Optional, Dict, Any, TYPE_CHECKING, Callable from ..utils.gui_utils import ( GUI_STATUS_OK, GUI_STATUS_WARNING, GUI_STATUS_ERROR, GUI_STATUS_FETCHING, GUI_STATUS_UNKNOWN, ) from .aircraft_db_importer import AircraftDBImporter from .live_data_processor import LiveDataProcessor # MODIFIED: Import MapCommandHandler # WHY: To delegate all map-related commands and information updates. # HOW: Added import statement. from .map_command_handler import MapCommandHandler from ..map.map_utils import _is_valid_bbox_dict if TYPE_CHECKING: from ..gui.main_window import MainWindow from ..gui.dialogs.import_progress_dialog import ImportProgressDialog from ..gui.dialogs.full_flight_details_window import FullFlightDetailsWindow from ..map.map_canvas_manager import MapCanvasManager module_logger = get_logger(__name__) ADAPTER_JOIN_TIMEOUT_SECONDS = 5.0 DEFAULT_CLICK_AREA_SIZE_KM = 50.0 class AppController: 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 # MODIFIED: Declare map_command_handler # WHY: This will hold an instance of our new map command processing class. # HOW: Added declaration, initialized to None. self.map_command_handler: Optional[MapCommandHandler] = None self.active_detail_window_icao: Optional[str] = None self.active_detail_window_ref: Optional["FullFlightDetailsWindow"] = None try: self.data_storage = DataStorage() module_logger.info("DataStorage initialized successfully by AppController.") 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 by AppController." ) 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 ({type(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 by AppController.") else: module_logger.warning("AircraftDBImporter could not be initialized due to missing dependencies.") self.flight_data_queue = Queue(maxsize=200) self.live_data_processor = LiveDataProcessor(self, self.flight_data_queue) module_logger.info("LiveDataProcessor initialized successfully by AppController.") # MODIFIED: Instantiate MapCommandHandler here # WHY: MapCommandHandler needs access to the AppController instance (self) to delegate to MainWindow/MapCanvasManager. # HOW: Created an instance, passing 'self'. self.map_command_handler = MapCommandHandler(self) module_logger.info("MapCommandHandler initialized successfully by AppController.") 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." module_logger.error(err_msg_ds) initial_status_msg = err_msg_ds initial_status_level = GUI_STATUS_ERROR if not self.aircraft_db_manager: err_msg_adb = "Aircraft DB init failed. Static details may be unavailable." module_logger.error(err_msg_adb) if initial_status_level == GUI_STATUS_OK: initial_status_msg = err_msg_adb initial_status_level = GUI_STATUS_WARNING else: initial_status_msg += f" {err_msg_adb}" initial_status_level = GUI_STATUS_ERROR if ( self.main_window and hasattr(self.main_window, "root") and self.main_window.root.winfo_exists() and hasattr(self.main_window, "update_semaphore_and_status") ): self.main_window.update_semaphore_and_status( initial_status_level, initial_status_msg ) else: module_logger.error( "Main window not set or lacks update_semaphore_and_status during set_main_window." ) def start_live_monitoring(self, bounding_box: Dict[str, float]): if not self.main_window: module_logger.error("Controller: Main window not set for live monitoring.") return if not bounding_box or not _is_valid_bbox_dict(bounding_box): err_msg = "Controller: Bounding box is required and must be valid." module_logger.error(err_msg) 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 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 or stop in progress." ) if hasattr(self.main_window, "_reset_gui_to_stopped_state"): self.main_window._reset_gui_to_stopped_state( "Monitoring stop in progress or already active." ) return module_logger.info( f"Controller: Starting live monitoring for bbox: {bounding_box}" ) self._active_bounding_box = bounding_box if ( hasattr(self.main_window, "map_manager_instance") and self.main_window.map_manager_instance and hasattr(self.main_window.map_manager_instance, "set_target_bbox") ): try: self.main_window.map_manager_instance.set_target_bbox(bounding_box) except Exception as e_map: module_logger.error(f"Error setting map BBox: {e_map}", exc_info=True) else: if hasattr(self.main_window, "clear_all_views_data"): self.main_window.clear_all_views_data() if self.flight_data_queue: while not self.flight_data_queue.empty(): try: self.flight_data_queue.get_nowait() self.flight_data_queue.task_done() except QueueEmpty: break except Exception: break if not self.live_data_processor: module_logger.critical("Controller: LiveDataProcessor not initialized. Cannot start monitoring.") if hasattr(self.main_window, "_reset_gui_to_stopped_state"): self.main_window._reset_gui_to_stopped_state("Start failed: Internal error (Processor N/A).") 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 and self.live_adapter_thread and self.live_adapter_thread.is_alive() ): if ( hasattr(self.main_window, "_reset_gui_to_stopped_state") and not from_error ): 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() else: module_logger.warning("Controller: LiveDataProcessor not initialized during stop_live_monitoring.") if self.live_adapter_thread and self.live_adapter_thread.is_alive(): try: self.live_adapter_thread.stop() if self.main_window and self.main_window.root.winfo_exists(): self.main_window.root.update_idletasks() self.live_adapter_thread.join(timeout=ADAPTER_JOIN_TIMEOUT_SECONDS) if self.live_adapter_thread.is_alive(): module_logger.warning( "Adapter thread did not stop in time on stop_live_monitoring." ) except Exception as e_join: module_logger.error(f"Error stopping adapter: {e_join}", exc_info=True) finally: self.live_adapter_thread = None if hasattr(self.main_window, "clear_all_views_data"): self.main_window.clear_all_views_data() if hasattr(self.main_window, "_reset_gui_to_stopped_state"): msg = ( "Monitoring stopped due to an error." if from_error else "Monitoring stopped." ) try: self.main_window._reset_gui_to_stopped_state(msg) except Exception as e_reset: module_logger.error(f"Error resetting GUI: {e_reset}", exc_info=False) self._active_bounding_box = None def on_application_exit(self): module_logger.info( "Controller: Application exit requested. Cleaning up resources." ) if ( self.active_detail_window_ref and self.active_detail_window_ref.winfo_exists() ): try: module_logger.info( f"Closing active detail window for {self.active_detail_window_icao} on app exit." ) self.active_detail_window_ref.destroy() except Exception as e_close_detail: module_logger.error( f"Error closing detail window on app exit: {e_close_detail}" ) finally: self.active_detail_window_ref = None self.active_detail_window_icao = None if ( self.main_window and hasattr(self.main_window, "map_manager_instance") and self.main_window.map_manager_instance is not None ): map_manager = self.main_window.map_manager_instance if hasattr(map_manager, "shutdown_worker") and callable( map_manager.shutdown_worker ): try: map_manager.shutdown_worker() module_logger.info( "Controller: Main MapCanvasManager worker shutdown requested." ) except Exception as e_map_shutdown: module_logger.error( f"Controller: Error during Main MapCanvasManager worker shutdown: {e_map_shutdown}", exc_info=True, ) if self.live_data_processor and self.is_live_monitoring_active: module_logger.info("Controller: Stopping LiveDataProcessor explicitly on app exit.") self.live_data_processor.stop_processing_queue() is_adapter_considered_running = ( self.live_adapter_thread and self.live_adapter_thread.is_alive() ) or self.is_live_monitoring_active if is_adapter_considered_running: self.stop_live_monitoring(from_error=False) if self.data_storage: try: self.data_storage.close_connection() module_logger.info("DataStorage connection closed.") except Exception as e_db_close: module_logger.error( f"Error closing DataStorage: {e_db_close}", exc_info=True ) finally: self.data_storage = None if self.aircraft_db_manager: try: self.aircraft_db_manager.close_connection() module_logger.info("AircraftDatabaseManager connection closed.") except Exception as e_ac_db_close: module_logger.error( f"Error closing AircraftDatabaseManager: {e_ac_db_close}", exc_info=True, ) finally: self.aircraft_db_manager = None module_logger.info("Controller: Cleanup on application exit finished.") def start_history_monitoring(self): if not self.main_window: module_logger.error("Main window not set for history monitoring.") return if not self.data_storage: err_msg = "DataStorage not initialized. Cannot use history features." if hasattr(self.main_window, "update_semaphore_and_status"): self.main_window.update_semaphore_and_status(GUI_STATUS_ERROR, err_msg) if hasattr(self.main_window, "_reset_gui_to_stopped_state"): self.main_window._reset_gui_to_stopped_state( f"History start failed: {err_msg}" ) return if hasattr(self.main_window, "update_semaphore_and_status"): self.main_window.update_semaphore_and_status( GUI_STATUS_OK, "History mode active (placeholder)." ) module_logger.info("History monitoring started (placeholder).") def stop_history_monitoring(self): if not self.main_window: return if hasattr(self.main_window, "update_semaphore_and_status"): self.main_window.update_semaphore_and_status( GUI_STATUS_OK, "History monitoring stopped." ) module_logger.info("History monitoring stopped (placeholder).") if hasattr(self.main_window, "_reset_gui_to_stopped_state"): self.main_window._reset_gui_to_stopped_state("History monitoring stopped.") # MODIFIED: Delegated on_map_left_click to MapCommandHandler # WHY: This method's logic is now handled by MapCommandHandler. # HOW: Changed implementation to delegate to self.map_command_handler. def on_map_left_click( self, latitude: float, longitude: float, screen_x: int, screen_y: int ): if self.map_command_handler: self.map_command_handler.on_map_left_click(latitude, longitude, screen_x, screen_y) else: module_logger.warning("Controller: MapCommandHandler not initialized for on_map_left_click.") 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( "AppController: AircraftDBImporter not initialized. Cannot perform 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 (controller issue)." )) elif self.main_window and hasattr(self.main_window, "show_error_message"): self.main_window.show_error_message( "Import Error", "Aircraft database importer not ready." ) return self.aircraft_db_importer.import_aircraft_database_with_progress( csv_filepath, progress_dialog_ref ) # MODIFIED: Re-added request_detailed_flight_info (ensuring it's here and correct) # WHY: This function is crucial for displaying selected flight details in the main window panel. # HOW: Re-inserted the function with its corrected logic for combining live and static data. def request_detailed_flight_info(self, icao24: str): normalized_icao24 = icao24.lower().strip() module_logger.info( f"Controller: Detailed info request for ICAO24: {normalized_icao24}" ) if not self.main_window: module_logger.error( "Controller: MainWindow not set, cannot update flight details panel." ) return if not normalized_icao24: if hasattr(self.main_window, "update_selected_flight_details"): self.main_window.update_selected_flight_details(None) return live_data_for_panel: Optional[Dict[str, Any]] = None static_data_for_panel: Optional[Dict[str, Any]] = None combined_details_for_panel: Dict[str, Any] = {"icao24": normalized_icao24} if ( self.main_window and hasattr(self.main_window, "map_manager_instance") and self.main_window.map_manager_instance and hasattr( self.main_window.map_manager_instance, "_current_flights_to_display_gui" ) and hasattr(self.main_window.map_manager_instance, "_map_data_lock") ): 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_for_panel = state.to_dict() break if live_data_for_panel: combined_details_for_panel.update(live_data_for_panel) module_logger.debug(f"AppController: Added live data to details for {normalized_icao24}.") if self.aircraft_db_manager: static_data_for_panel = self.aircraft_db_manager.get_aircraft_details( normalized_icao24 ) if static_data_for_panel: for k, v in static_data_for_panel.items(): if k not in combined_details_for_panel or combined_details_for_panel[k] is None: combined_details_for_panel[k] = v module_logger.debug(f"AppController: Added static data to details for {normalized_icao24}.") if hasattr(self.main_window, "update_selected_flight_details"): self.main_window.update_selected_flight_details(combined_details_for_panel) module_logger.debug(f"AppController: Called update_selected_flight_details with combined data for {normalized_icao24}.") 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() else: module_logger.info( f"Closing existing detail window for {self.active_detail_window_icao} before opening new one for {normalized_icao24}." ) try: self.active_detail_window_ref.destroy() except tk.TclError: pass 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 hasattr(self.main_window, "map_manager_instance") and self.main_window.map_manager_instance and hasattr( self.main_window.map_manager_instance, "_current_flights_to_display_gui" ) and hasattr(self.main_window.map_manager_instance, "_map_data_lock") ): 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: 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, ) try: from ..gui.dialogs.full_flight_details_window import FullFlightDetailsWindow if ( self.active_detail_window_ref and self.active_detail_window_icao == normalized_icao24 and self.active_detail_window_ref.winfo_exists() ): details_win = self.active_detail_window_ref else: details_win = FullFlightDetailsWindow( self.main_window.root, normalized_icao24, self ) self.active_detail_window_ref = details_win self.active_detail_window_icao = normalized_icao24 if self.main_window: self.main_window.full_flight_details_window = details_win 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 def details_window_closed(self, closed_icao24: str): normalized_closed_icao24 = closed_icao24.lower().strip() if self.active_detail_window_icao == normalized_closed_icao24: module_logger.info( f"AppController: Detail window for {normalized_closed_icao24} reported closed. Clearing references." ) self.active_detail_window_ref = None self.active_detail_window_icao = None if ( self.main_window and hasattr(self.main_window, "full_flight_details_window") and self.main_window.full_flight_details_window and not self.main_window.full_flight_details_window.winfo_exists() ): self.main_window.full_flight_details_window = None else: module_logger.debug( f"AppController: A detail window for {normalized_closed_icao24} closed, but it was not the currently tracked active one ({self.active_detail_window_icao}). No action on active_detail references." ) # MODIFIED: Delegated on_map_right_click to MapCommandHandler # WHY: This method's logic is now handled by MapCommandHandler. # HOW: Changed implementation to delegate to self.map_command_handler. 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.") # MODIFIED: Delegated on_map_context_menu_request to MapCommandHandler # WHY: This method's logic is now handled by MapCommandHandler. # HOW: Changed implementation to delegate to self.map_command_handler. def on_map_context_menu_request( self, latitude: float, longitude: float, screen_x: int, screen_y: int ): if self.map_command_handler: self.map_command_handler.on_map_context_menu_request(latitude, longitude, screen_x, screen_y) else: module_logger.warning("Controller: MapCommandHandler not initialized for on_map_context_menu_request.") # MODIFIED: Delegated recenter_map_at_coords to MapCommandHandler # WHY: This method's logic is now handled by MapCommandHandler. # HOW: Changed implementation to delegate to self.map_command_handler. 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.") # MODIFIED: Delegated set_bbox_around_coords to MapCommandHandler # WHY: This method's logic is now handled by MapCommandHandler. # HOW: Changed implementation to delegate to self.map_command_handler. def set_bbox_around_coords( self, center_lat: float, center_lon: float, area_size_km: float = DEFAULT_CLICK_AREA_SIZE_KM, ): 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.") # MODIFIED: Delegated update_bbox_gui_fields to MapCommandHandler # WHY: This method's logic is now handled by MapCommandHandler. # HOW: Changed implementation to delegate to self.map_command_handler. def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]): if self.map_command_handler: self.map_command_handler.update_bbox_gui_fields(bbox_dict) else: module_logger.warning("Controller: MapCommandHandler not initialized for update_bbox_gui_fields.") # MODIFIED: Delegated update_general_map_info to MapCommandHandler # WHY: This method's logic is now handled by MapCommandHandler. # HOW: Changed implementation to delegate to self.map_command_handler. 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.") # MODIFIED: Delegated map_zoom_in to MapCommandHandler # WHY: This method's logic is now handled by MapCommandHandler. # HOW: Changed implementation to delegate to self.map_command_handler. def map_zoom_in(self): if self.map_command_handler: self.map_command_handler.map_zoom_in() else: module_logger.warning("Controller: MapCommandHandler not initialized for map_zoom_in.") # MODIFIED: Delegated map_zoom_out to MapCommandHandler # WHY: This method's logic is now handled by MapCommandHandler. # HOW: Changed implementation to delegate to self.map_command_handler. def map_zoom_out(self): if self.map_command_handler: self.map_command_handler.map_zoom_out() else: module_logger.warning("Controller: MapCommandHandler not initialized for map_zoom_out.") # MODIFIED: Delegated map_pan_direction to MapCommandHandler # WHY: This method's logic is now handled by MapCommandHandler. # HOW: Changed implementation to delegate to self.map_command_handler. def map_pan_direction(self, direction: str): if self.map_command_handler: self.map_command_handler.map_pan_direction(direction) else: module_logger.warning("Controller: MapCommandHandler not initialized for map_pan_direction.") # MODIFIED: Delegated map_center_on_coords_and_fit_patch to MapCommandHandler # WHY: This method's logic is now handled by MapCommandHandler. # HOW: Changed implementation to delegate to self.map_command_handler. 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) else: module_logger.warning("Controller: MapCommandHandler not initialized for map_center_on_coords_and_fit_patch.") # MODIFIED: Delegated set_map_track_length to MapCommandHandler # WHY: This method's logic is now handled by MapCommandHandler. # HOW: Changed implementation to delegate to self.map_command_handler. def set_map_track_length(self, length: int): if self.map_command_handler: self.map_command_handler.set_map_track_length(length) else: module_logger.warning("Controller: MapCommandHandler not initialized for set_map_track_length.")