diff --git a/flightmonitor/controller/app_controller.py b/flightmonitor/controller/app_controller.py index a073919..fd0288f 100644 --- a/flightmonitor/controller/app_controller.py +++ b/flightmonitor/controller/app_controller.py @@ -29,6 +29,11 @@ from ..utils.gui_utils import ( 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 @@ -59,6 +64,12 @@ class AppController: 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 @@ -103,6 +114,12 @@ class AppController: 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 @@ -371,50 +388,16 @@ class AppController: 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 ): - module_logger.debug( - f"Controller: Map left-clicked at Geo ({latitude:.5f}, {longitude:.5f})" - ) - if self.main_window and hasattr(self.main_window, "update_clicked_map_info"): - lat_dms_str, lon_dms_str = "N/A", "N/A" - try: - from ..map.map_utils import deg_to_dms_string - - lat_dms_str = ( - deg_to_dms_string(latitude, "lat") - if latitude is not None - else "N/A" - ) - lon_dms_str = ( - deg_to_dms_string(longitude, "lon") - if longitude is not None - else "N/A" - ) - except ImportError: - module_logger.warning("map_utils.deg_to_dms_string N/A for left click.") - except Exception as e_dms: - module_logger.warning( - f"Error DMS for left click: {e_dms}", exc_info=False - ) - lat_dms_str = lon_dms_str = "N/A (CalcErr)" - - try: - self.main_window.update_clicked_map_info( - lat_deg=latitude, - lon_deg=longitude, - lat_dms=lat_dms_str, - lon_dms=lon_dms_str, - ) - except tk.TclError as e_tcl: - module_logger.warning( - f"TclError updating map clicked info panel: {e_tcl}. GUI closing." - ) - except Exception as e_update: - module_logger.error( - f"Error updating map clicked info panel: {e_update}", exc_info=False - ) + 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" @@ -438,9 +421,8 @@ class AppController: csv_filepath, progress_dialog_ref ) - # MODIFIED: Re-added request_detailed_flight_info (this was the missing function) + # 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. - # It was inadvertently removed during the refactoring process. # 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() @@ -461,10 +443,8 @@ class AppController: live_data_for_panel: Optional[Dict[str, Any]] = None static_data_for_panel: Optional[Dict[str, Any]] = None - # Initialize combined_details_for_panel with icao24. combined_details_for_panel: Dict[str, Any] = {"icao24": normalized_icao24} - # Process live data first if ( self.main_window and hasattr(self.main_window, "map_manager_instance") @@ -482,23 +462,19 @@ class AppController: break if live_data_for_panel: - # IMPORTANT: Use .update() to ensure live data fields overwrite existing ones combined_details_for_panel.update(live_data_for_panel) module_logger.debug(f"AppController: Added live data to details for {normalized_icao24}.") - # Process static data second, only if key not already set by live data 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(): - # Only add static data if the key is not already populated (e.g., by live data) 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}.") - # Final call to update the GUI panel 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}.") @@ -646,369 +622,112 @@ class AppController: 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 ): - module_logger.debug( - f"Controller: Map right-clicked at Geo ({latitude:.5f}, {longitude:.5f})" - ) - if self.main_window: - if hasattr(self.main_window, "update_clicked_map_info"): - lat_dms_str, lon_dms_str = "N/A", "N/A" - try: - from ..map.map_utils import deg_to_dms_string - - lat_dms_str = ( - deg_to_dms_string(latitude, "lat") - if latitude is not None - else "N/A" - ) - lon_dms_str = ( - deg_to_dms_string(longitude, "lon") - if longitude is not None - else "N/A" - ) - except ImportError: - module_logger.warning( - "map_utils.deg_to_dms_string N/A for right click." - ) - except Exception as e_dms: - module_logger.warning( - f"Error DMS for right click: {e_dms}", exc_info=False - ) - lat_dms_str = lon_dms_str = "N/A (CalcErr)" - try: - self.main_window.update_clicked_map_info( - lat_deg=latitude, - lon_deg=longitude, - lat_dms=lat_dms_str, - lon_dms=lon_dms_str, - ) - except tk.TclError as e_tcl: - module_logger.warning( - f"TclError updating map clicked info (right): {e_tcl}. GUI closing." - ) - except Exception as e_update: - module_logger.error( - f"Error updating map clicked info (right): {e_update}", - exc_info=False, - ) - - if hasattr(self.main_window, "show_map_context_menu"): - try: - self.main_window.show_map_context_menu( - latitude, longitude, screen_x, screen_y - ) - except tk.TclError as e_tcl_menu: - module_logger.warning( - f"TclError showing map context menu: {e_tcl_menu}. GUI closing." - ) - except Exception as e_menu: - module_logger.error( - f"Error showing map context menu: {e_menu}", exc_info=False - ) + if self.map_command_handler: + self.map_command_handler.on_map_right_click(latitude, longitude, screen_x, screen_y) else: - module_logger.warning("Main window N/A for right click handling.") + 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 ): - module_logger.debug( - f"Controller received context menu request for ({latitude:.5f}, {longitude:.5f}) at screen ({screen_x}, {screen_y})." - ) - if self.main_window and hasattr(self.main_window, "show_map_context_menu"): - try: - self.main_window.show_map_context_menu( - latitude, longitude, screen_x, screen_y - ) - except tk.TclError as e_tcl: - module_logger.warning( - f"TclError delegating show_map_context_menu: {e_tcl}. GUI closing." - ) - except Exception as e_menu: - module_logger.error( - f"Error delegating show_map_context_menu: {e_menu}", exc_info=False - ) + if self.map_command_handler: + self.map_command_handler.on_map_context_menu_request(latitude, longitude, screen_x, screen_y) else: - module_logger.warning("Main window N/A to show context menu.") + 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): - module_logger.info( - f"Controller request: recenter map at ({lat:.5f}, {lon:.5f})." - ) - if ( - self.main_window - and hasattr(self.main_window, "map_manager_instance") - and self.main_window.map_manager_instance - ): - map_manager: "MapCanvasManager" = self.main_window.map_manager_instance - if hasattr(map_manager, "recenter_map_at_coords"): - try: - map_manager.recenter_map_at_coords(lat, lon) - except Exception as e: - module_logger.error( - f"Error map_manager.recenter_map_at_coords: {e}", exc_info=False - ) - else: - module_logger.warning( - "MapCanvasManager missing 'recenter_map_at_coords' method." - ) + if self.map_command_handler: + self.map_command_handler.recenter_map_at_coords(lat, lon) else: - module_logger.warning( - "Main window or MapCanvasManager N/A to recenter map." - ) + 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, ): - module_logger.info( - f"Controller request: set BBox ({area_size_km:.1f}km) around ({center_lat:.5f}, {center_lon:.5f})." - ) - if ( - self.main_window - and hasattr(self.main_window, "map_manager_instance") - and self.main_window.map_manager_instance - ): - map_manager: "MapCanvasManager" = self.main_window.map_manager_instance - if hasattr(map_manager, "set_bbox_around_coords"): - try: - map_manager.set_bbox_around_coords( - center_lat, center_lon, area_size_km - ) - except Exception as e: - module_logger.error( - f"Error map_manager.set_bbox_around_coords: {e}", - exc_info=False, - ) - else: - module_logger.warning( - "MapCanvasManager missing 'set_bbox_around_coords' method." - ) + if self.map_command_handler: + self.map_command_handler.set_bbox_around_coords(center_lat, center_lon, area_size_km) else: - module_logger.warning("Main window or MapCanvasManager N/A to set BBox.") + 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]): - module_logger.debug( - f"Controller request: update BBox GUI fields with: {bbox_dict}" - ) - if self.main_window and hasattr(self.main_window, "update_bbox_gui_fields"): - try: - self.main_window.update_bbox_gui_fields(bbox_dict) - except tk.TclError as e_tcl: - module_logger.warning( - f"TclError updating BBox GUI fields: {e_tcl}. GUI closing." - ) - except Exception as e_update: - module_logger.error( - f"Error updating BBox GUI fields: {e_update}", exc_info=False - ) + if self.map_command_handler: + self.map_command_handler.update_bbox_gui_fields(bbox_dict) else: - module_logger.warning("Main window N/A to update BBox GUI fields.") + 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): - module_logger.debug("Controller: Request to update general map info.") - if not ( - self.main_window - and hasattr(self.main_window, "map_manager_instance") - and self.main_window.map_manager_instance - ): - if self.main_window and hasattr( - self.main_window, "update_general_map_info_display" - ): - try: - self.main_window.update_general_map_info_display( - zoom=None, - map_size_str="N/A", - map_geo_bounds=None, - target_bbox_input=None, - flight_count=None, - ) - except Exception: - pass - return - - map_manager: "MapCanvasManager" = self.main_window.map_manager_instance - if hasattr(map_manager, "get_current_map_info"): - map_info = {} - try: - map_info = map_manager.get_current_map_info() - zoom = map_info.get("zoom") - map_geo_bounds = map_info.get("map_geo_bounds") - target_bbox_input_from_map_info = map_info.get("target_bbox_input") - flight_count = map_info.get("flight_count") - map_size_km_w = map_info.get("map_size_km_w") - map_size_km_h = map_info.get("map_size_km_h") - map_size_str = "N/A" - if map_size_km_w is not None and map_size_km_h is not None: - try: - decimals = getattr(app_config, "MAP_SIZE_KM_DECIMAL_PLACES", 1) - map_size_str = f"{map_size_km_w:.{decimals}f}km x {map_size_km_h:.{decimals}f}km" - except Exception as e_format: - module_logger.warning( - f"Error formatting map size: {e_format}. Using N/A.", - exc_info=False, - ) - map_size_str = "N/A (FormatErr)" - - if hasattr(self.main_window, "update_general_map_info_display"): - try: - self.main_window.update_general_map_info_display( - zoom=zoom, - map_size_str=map_size_str, - map_geo_bounds=map_geo_bounds, - target_bbox_input=target_bbox_input_from_map_info, - flight_count=flight_count, - ) - except tk.TclError as e_tcl: - module_logger.warning( - f"TclError updating general map info panel: {e_tcl}. GUI closing." - ) - except Exception as e_update: - module_logger.error( - f"Error updating general map info panel: {e_update}", - exc_info=False, - ) - except Exception as e_get_info: - module_logger.error( - f"Error getting map info from map manager in AppController: {e_get_info}", - exc_info=True, - ) - if hasattr(self.main_window, "update_general_map_info_display"): - try: - self.main_window.update_general_map_info_display( - zoom=None, - map_size_str="N/A", - map_geo_bounds=None, - target_bbox_input=None, - flight_count=None, - ) - except Exception: - pass + if self.map_command_handler: + self.map_command_handler.update_general_map_info() else: - module_logger.warning( - "MapCanvasManager missing 'get_current_map_info' method." - ) + 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): - module_logger.debug("Controller: Map Zoom In requested.") - if ( - self.main_window - and hasattr(self.main_window, "map_manager_instance") - and self.main_window.map_manager_instance - ): - map_manager: "MapCanvasManager" = self.main_window.map_manager_instance - if hasattr(map_manager, "zoom_in_at_center"): - try: - map_manager.zoom_in_at_center() - except Exception as e: - module_logger.error( - f"Error map_manager.zoom_in_at_center: {e}", exc_info=False - ) - else: - module_logger.warning( - "MapCanvasManager missing 'zoom_in_at_center' method." - ) + if self.map_command_handler: + self.map_command_handler.map_zoom_in() else: - module_logger.warning("Map manager N/A for zoom in.") + 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): - module_logger.debug("Controller: Map Zoom Out requested.") - if ( - self.main_window - and hasattr(self.main_window, "map_manager_instance") - and self.main_window.map_manager_instance - ): - map_manager: "MapCanvasManager" = self.main_window.map_manager_instance - if hasattr(map_manager, "zoom_out_at_center"): - try: - map_manager.zoom_out_at_center() - except Exception as e: - module_logger.error( - f"Error map_manager.zoom_out_at_center: {e}", exc_info=False - ) - else: - module_logger.warning( - "MapCanvasManager missing 'zoom_out_at_center' method." - ) + if self.map_command_handler: + self.map_command_handler.map_zoom_out() else: - module_logger.warning("Map manager N/A for zoom out.") + 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): - module_logger.debug(f"Controller: Map Pan '{direction}' requested.") - if ( - self.main_window - and hasattr(self.main_window, "map_manager_instance") - and self.main_window.map_manager_instance - ): - map_manager: "MapCanvasManager" = self.main_window.map_manager_instance - if hasattr(map_manager, "pan_map_fixed_step"): - try: - map_manager.pan_map_fixed_step(direction) - except Exception as e: - module_logger.error( - f"Error map_manager.pan_map_fixed_step('{direction}'): {e}", - exc_info=False, - ) - else: - module_logger.warning( - "MapCanvasManager missing 'pan_map_fixed_step' method." - ) + if self.map_command_handler: + self.map_command_handler.map_pan_direction(direction) else: - module_logger.warning(f"Map manager N/A for pan {direction}.") + 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 ): - module_logger.debug( - f"Controller: Center map at ({lat:.4f},{lon:.4f}) and fit {patch_size_km}km patch." - ) - if ( - self.main_window - and hasattr(self.main_window, "map_manager_instance") - and self.main_window.map_manager_instance - ): - map_manager: "MapCanvasManager" = self.main_window.map_manager_instance - if hasattr(map_manager, "center_map_and_fit_patch"): - try: - map_manager.center_map_and_fit_patch(lat, lon, patch_size_km) - except Exception as e: - module_logger.error( - f"Error map_manager.center_map_and_fit_patch: {e}", - exc_info=False, - ) - else: - module_logger.warning( - "MapCanvasManager missing 'center_map_and_fit_patch' method." - ) + 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("Map manager N/A for center and fit patch.") + 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): - module_logger.info( - f"Controller: Request to set map track length to {length} points." - ) - if ( - self.main_window - and hasattr(self.main_window, "map_manager_instance") - and self.main_window.map_manager_instance is not None - and hasattr(self.main_window.map_manager_instance, "set_max_track_points") - ): - try: - self.main_window.map_manager_instance.set_max_track_points(length) - except Exception as e: - module_logger.error( - f"Error calling map_manager.set_max_track_points({length}): {e}", - exc_info=True, - ) - if hasattr(self.main_window, "show_error_message"): - self.main_window.show_error_message( - "Map Configuration Error", - f"Failed to set track length on map: {e}", - ) + if self.map_command_handler: + self.map_command_handler.set_map_track_length(length) else: - module_logger.warning( - "MapCanvasManager or 'set_max_track_points' N/A to set track length." - ) \ No newline at end of file + module_logger.warning("Controller: MapCommandHandler not initialized for set_map_track_length.") \ No newline at end of file diff --git a/flightmonitor/controller/cleanup_manager.py b/flightmonitor/controller/cleanup_manager.py new file mode 100644 index 0000000..48f100c --- /dev/null +++ b/flightmonitor/controller/cleanup_manager.py @@ -0,0 +1,155 @@ +# FlightMonitor/controller/cleanup_manager.py +""" +Manages the orderly shutdown and cleanup of application resources, +including closing database connections, stopping background threads, +and destroying secondary GUI windows upon application exit or specific events. +""" +from typing import Any, Optional, TYPE_CHECKING + +from ..utils.logger import get_logger + +# Type checking imports to avoid circular dependencies at runtime +if TYPE_CHECKING: + from .app_controller import AppController + from ..data.storage import DataStorage + from ..data.aircraft_database_manager import AircraftDatabaseManager + from ..gui.main_window import MainWindow + from ..gui.dialogs.full_flight_details_window import FullFlightDetailsWindow + from ..map.map_canvas_manager import MapCanvasManager + + +module_logger = get_logger(__name__) + + +class CleanupManager: + """ + Handles all application shutdown and resource cleanup operations. + It orchestrates the closing of various modules and components. + """ + + def __init__(self, app_controller: "AppController"): + """ + Initializes the CleanupManager. + + Args: + app_controller: The main AppController instance, providing access + to all application components that need cleanup. + """ + self.app_controller = app_controller + module_logger.debug("CleanupManager initialized.") + + def on_application_exit(self): + """ + Performs a coordinated shutdown of all application resources. + This method is called when the main application window is closing. + """ + module_logger.info("CleanupManager: Application exit requested. Cleaning up resources.") + + # --- Close active Full Flight Details window (if any) --- + # Access active_detail_window_ref and active_detail_window_icao via app_controller + if ( + self.app_controller.active_detail_window_ref + and self.app_controller.active_detail_window_ref.winfo_exists() + ): + try: + module_logger.info( + f"CleanupManager: Closing active detail window for {self.app_controller.active_detail_window_icao} on app exit." + ) + self.app_controller.active_detail_window_ref.destroy() + except Exception as e_close_detail: + module_logger.error( + f"CleanupManager: Error closing detail window on app exit: {e_close_detail}" + ) + finally: + # Clear references in AppController directly or via a method + self.app_controller.active_detail_window_ref = None + self.app_controller.active_detail_window_icao = None + + # --- Shutdown MapCanvasManager worker (if active) --- + # Access map_manager_instance via app_controller.main_window + main_window = self.app_controller.main_window + if ( + main_window + and hasattr(main_window, "map_manager_instance") + and main_window.map_manager_instance is not None + ): + map_manager: "MapCanvasManager" = 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("CleanupManager: Main MapCanvasManager worker shutdown requested.") + except Exception as e_map_shutdown: + module_logger.error( + f"CleanupManager: Error during Main MapCanvasManager worker shutdown: {e_map_shutdown}", + exc_info=True, + ) + + # --- Stop live monitoring (adapter thread and data processor) --- + # Access live_adapter_thread, is_live_monitoring_active via app_controller + # Access stop_live_monitoring via app_controller + is_adapter_considered_running = ( + self.app_controller.live_adapter_thread and self.app_controller.live_adapter_thread.is_alive() + ) or self.app_controller.is_live_monitoring_active + if is_adapter_considered_running: + module_logger.info("CleanupManager: Live monitoring is active, requesting AppController to stop it.") + self.app_controller.stop_live_monitoring(from_error=False) + + # --- Close DataStorage connection --- + # Access data_storage via app_controller + if self.app_controller.data_storage: + try: + self.app_controller.data_storage.close_connection() + module_logger.info("CleanupManager: DataStorage connection closed.") + except Exception as e_db_close: + module_logger.error( + f"CleanupManager: Error closing DataStorage: {e_db_close}", exc_info=True + ) + finally: + self.app_controller.data_storage = None # Clear reference in AppController + + # --- Close AircraftDatabaseManager connection --- + # Access aircraft_db_manager via app_controller + if self.app_controller.aircraft_db_manager: + try: + self.app_controller.aircraft_db_manager.close_connection() + module_logger.info("CleanupManager: AircraftDatabaseManager connection closed.") + except Exception as e_ac_db_close: + module_logger.error( + f"CleanupManager: Error closing AircraftDatabaseManager: {e_ac_db_close}", + exc_info=True, + ) + finally: + self.app_controller.aircraft_db_manager = None # Clear reference in AppController + + module_logger.info("CleanupManager: Cleanup on application exit finished.") + + def details_window_closed(self, closed_icao24: str): + """ + Handles the event when a full flight details window is closed. + Clears the AppController's references to the closed window if it was the active one. + + Args: + closed_icao24: The ICAO24 of the flight whose details window was closed. + """ + normalized_closed_icao24 = closed_icao24.lower().strip() + + # Access active_detail_window_icao and active_detail_window_ref via app_controller + if self.app_controller.active_detail_window_icao == normalized_closed_icao24: + module_logger.info( + f"CleanupManager: Detail window for {normalized_closed_icao24} reported closed. Clearing references in AppController." + ) + self.app_controller.active_detail_window_ref = None + self.app_controller.active_detail_window_icao = None + # Also clear MainWindow's convenience reference if it matches (accessed via app_controller) + main_window = self.app_controller.main_window + if ( + main_window + and hasattr(main_window, "full_flight_details_window") + and main_window.full_flight_details_window + and not main_window.full_flight_details_window.winfo_exists() + ): # Check if already destroyed by Tkinter + main_window.full_flight_details_window = None + else: + module_logger.debug( + f"CleanupManager: A detail window for {normalized_closed_icao24} closed, but it was not the currently tracked active one ({self.app_controller.active_detail_window_icao}). No action on active_detail references." + ) \ No newline at end of file diff --git a/flightmonitor/controller/map_command_handler.py b/flightmonitor/controller/map_command_handler.py new file mode 100644 index 0000000..04135c0 --- /dev/null +++ b/flightmonitor/controller/map_command_handler.py @@ -0,0 +1,389 @@ +# FlightMonitor/controller/map_command_handler.py +""" +Handles map-related commands and information updates, delegating to MapCanvasManager +and updating MainWindow's map info panels. +""" +import tkinter as tk # For TkinterError in specific callbacks, though ideally abstracted +import math # For geographic calculations, e.g., in deg_to_dms_string +from typing import Optional, Dict, Any, Tuple, TYPE_CHECKING + +from ..utils.logger import get_logger +from ..data import config as app_config # For map size decimal places, etc. +from ..map.map_utils import _is_valid_bbox_dict, deg_to_dms_string, calculate_geographic_bbox_size_km, calculate_meters_per_pixel, calculate_zoom_level_for_geographic_size, get_bounding_box_from_center_size + +# Type checking imports to avoid circular dependencies at runtime +if TYPE_CHECKING: + from .app_controller import AppController # For type hinting AppController + from ..gui.main_window import MainWindow + from ..map.map_canvas_manager import MapCanvasManager + +module_logger = get_logger(__name__) + +DEFAULT_CLICK_AREA_SIZE_KM = 50.0 # Default for context menu 'set bbox' + + +class MapCommandHandler: + """ + Manages all map-related commands (zoom, pan, recenter, set bbox) and + updates to the map information display in the GUI. + It acts as an intermediary between the AppController and the MapCanvasManager/MainWindow. + """ + + def __init__(self, app_controller: "AppController"): + """ + Initializes the MapCommandHandler. + + Args: + app_controller: The main AppController instance. This handler accesses + MainWindow and MapCanvasManager via the controller. + """ + self.app_controller = app_controller + module_logger.debug("MapCommandHandler initialized.") + + def on_map_left_click(self, latitude: float, longitude: float, screen_x: int, screen_y: int): + """ + Handles a left-click event on the map canvas. + Updates the clicked map info panel and attempts to identify/select a flight. + """ + module_logger.debug(f"MapCommandHandler: Map left-clicked at Geo ({latitude:.5f}, {longitude:.5f})") + + main_window = self.app_controller.main_window + if not main_window: + module_logger.warning("MapCommandHandler: MainWindow not available for left click handling.") + return + + # Update clicked map info panel (Lat/Lon in degrees and DMS) + if hasattr(main_window, "update_clicked_map_info"): + lat_dms_str = deg_to_dms_string(latitude, "lat") if latitude is not None else "N/A" + lon_dms_str = deg_to_dms_string(longitude, "lon") if longitude is not None else "N/A" + try: + main_window.update_clicked_map_info( + lat_deg=latitude, + lon_deg=longitude, + lat_dms=lat_dms_str, + lon_dms=lon_dms_str, + ) + except tk.TclError as e_tcl: + module_logger.warning(f"MapCommandHandler: TclError updating map clicked info panel: {e_tcl}. GUI closing.") + except Exception as e_update: + module_logger.error(f"MapCommandHandler: Error updating map clicked info panel: {e_update}", exc_info=False) + + # Attempt to select a flight near the clicked coordinates + if hasattr(main_window, "map_manager_instance") and main_window.map_manager_instance: + map_manager: "MapCanvasManager" = main_window.map_manager_instance + if hasattr(map_manager, "get_clicked_flight_icao"): # Assuming MapCanvasManager has this helper + clicked_flight_icao = map_manager.get_clicked_flight_icao(screen_x, screen_y) # Use screen_x, screen_y or canvas_x,y + if clicked_flight_icao: + module_logger.info(f"MapCommandHandler: Flight selected by click: {clicked_flight_icao}") + if hasattr(self.app_controller, "request_detailed_flight_info"): + self.app_controller.request_detailed_flight_info(clicked_flight_icao) + elif not map_manager.is_detail_map: # Only clear for main map if no flight selected + module_logger.info(f"MapCommandHandler: No specific flight selected by click on MAIN map. Clearing details.") + if hasattr(self.app_controller, "request_detailed_flight_info"): + self.app_controller.request_detailed_flight_info("") # Request clearing details + + + def on_map_right_click(self, latitude: float, longitude: float, screen_x: int, screen_y: int): + """ + Handles a right-click event on the map canvas. + Updates the clicked map info panel and shows a context menu. + """ + module_logger.debug(f"MapCommandHandler: Map right-clicked at Geo ({latitude:.5f}, {longitude:.5f})") + + main_window = self.app_controller.main_window + if not main_window: + module_logger.warning("MapCommandHandler: MainWindow not available for right click handling.") + return + + # Update clicked map info panel (Lat/Lon in degrees and DMS) + if hasattr(main_window, "update_clicked_map_info"): + lat_dms_str = deg_to_dms_string(latitude, "lat") if latitude is not None else "N/A" + lon_dms_str = deg_to_dms_string(longitude, "lon") if longitude is not None else "N/A" + try: + main_window.update_clicked_map_info( + lat_deg=latitude, + lon_deg=longitude, + lat_dms=lat_dms_str, + lon_dms=lon_dms_str, + ) + except tk.TclError as e_tcl: + module_logger.warning(f"MapCommandHandler: TclError updating map clicked info (right): {e_tcl}. GUI closing.") + except Exception as e_update: + module_logger.error(f"MapCommandHandler: Error updating map clicked info (right): {e_update}", exc_info=False) + + # Show map context menu + if hasattr(main_window, "show_map_context_menu"): + try: + main_window.show_map_context_menu(latitude, longitude, screen_x, screen_y) + except tk.TclError as e_tcl_menu: + module_logger.warning(f"MapCommandHandler: TclError showing map context menu: {e_tcl_menu}. GUI closing.") + except Exception as e_menu: + module_logger.error(f"MapCommandHandler: Error showing map context menu: {e_menu}", exc_info=False) + + + def on_map_context_menu_request(self, latitude: float, longitude: float, screen_x: int, screen_y: int): + """ + Delegates a request to show the map context menu. This might be called directly by MapCanvasManager. + """ + module_logger.debug(f"MapCommandHandler: Received context menu request for ({latitude:.5f}, {longitude:.5f}) at screen ({screen_x}, {screen_y}).") + + main_window = self.app_controller.main_window + if main_window and hasattr(main_window, "show_map_context_menu"): + try: + main_window.show_map_context_menu(latitude, longitude, screen_x, screen_y) + except tk.TclError as e_tcl: + module_logger.warning(f"MapCommandHandler: TclError delegating show_map_context_menu: {e_tcl}. GUI closing.") + except Exception as e_menu: + module_logger.error(f"MapCommandHandler: Error delegating show_map_context_menu: {e_menu}", exc_info=False) + else: + module_logger.warning("MapCommandHandler: Main window N/A to show context menu.") + + def recenter_map_at_coords(self, lat: float, lon: float): + """ + Requests the MapCanvasManager to recenter the map at the given coordinates. + """ + module_logger.info(f"MapCommandHandler: Request: recenter map at ({lat:.5f}, {lon:.5f}).") + + main_window = self.app_controller.main_window + if ( + main_window and hasattr(main_window, "map_manager_instance") + and main_window.map_manager_instance is not None + ): + map_manager: "MapCanvasManager" = main_window.map_manager_instance + if hasattr(map_manager, "recenter_map_at_coords"): + try: + map_manager.recenter_map_at_coords(lat, lon) + except Exception as e_recenter: + module_logger.error(f"MapCommandHandler: Error map_manager.recenter_map_at_coords: {e_recenter}", exc_info=False) + else: + module_logger.warning("MapCommandHandler: MapCanvasManager missing 'recenter_map_at_coords' method.") + else: + module_logger.warning("MapCommandHandler: Main window or MapCanvasManager N/A to recenter map.") + + def set_bbox_around_coords(self, center_lat: float, center_lon: float, area_size_km: float = DEFAULT_CLICK_AREA_SIZE_KM): + """ + Requests the MapCanvasManager to set the map's bounding box around given coordinates with a specified area size. + This is typically used for map context menu actions on the main map. + """ + module_logger.info(f"MapCommandHandler: Request: set BBox ({area_size_km:.1f}km) around ({center_lat:.5f}, {center_lon:.5f}).") + + main_window = self.app_controller.main_window + if ( + main_window and hasattr(main_window, "map_manager_instance") + and main_window.map_manager_instance is not None + ): + map_manager: "MapCanvasManager" = main_window.map_manager_instance + # Ensure this is only called on the main map (not a detail map) + if hasattr(map_manager, "is_detail_map") and map_manager.is_detail_map: + module_logger.warning("MapCommandHandler: Attempt to set BBox on detail map. Ignoring.") + if hasattr(main_window, "show_info_message"): + main_window.show_info_message("Map Action", "Cannot set monitoring area on detail map.") + return + + if hasattr(map_manager, "set_bbox_around_coords"): + try: + map_manager.set_bbox_around_coords(center_lat, center_lon, area_size_km) + except Exception as e_set_bbox: + module_logger.error(f"MapCommandHandler: Error map_manager.set_bbox_around_coords: {e_set_bbox}", exc_info=False) + if hasattr(main_window, "show_error_message"): + main_window.show_error_message("Map Error", f"Failed to set BBox: {e_set_bbox}") + else: + module_logger.warning("MapCommandHandler: MapCanvasManager missing 'set_bbox_around_coords' method.") + if hasattr(main_window, "show_error_message"): + main_window.show_error_message("Map Error", "Map function missing to set BBox.") + else: + module_logger.warning("MapCommandHandler: Main window or MapCanvasManager N/A to set BBox.") + if hasattr(main_window, "show_error_message"): + main_window.show_error_message("Map Error", "Map not ready to set BBox.") + + def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]): + """ + Requests the MainWindow to update the GUI input fields for the bounding box. + This is typically called by MapCanvasManager when its internal bbox changes. + """ + module_logger.debug(f"MapCommandHandler: Request: update BBox GUI fields with: {bbox_dict}") + + main_window = self.app_controller.main_window + if main_window and hasattr(main_window, "update_bbox_gui_fields"): + try: + main_window.update_bbox_gui_fields(bbox_dict) + except tk.TclError as e_tcl: + module_logger.warning(f"MapCommandHandler: TclError updating BBox GUI fields: {e_tcl}. GUI closing.") + except Exception as e_update: + module_logger.error(f"MapCommandHandler: Error updating BBox GUI fields: {e_update}", exc_info=False) + else: + module_logger.warning("MapCommandHandler: Main window N/A to update BBox GUI fields.") + + def update_general_map_info(self): + """ + Requests the MapCanvasManager for current map information and + then updates the MainWindow's general map info display panel. + """ + module_logger.debug("MapCommandHandler: Request to update general map info.") + + main_window = self.app_controller.main_window + if not (main_window and hasattr(main_window, "map_manager_instance") and main_window.map_manager_instance): + # If map manager is not available, ensure info display is cleared + if main_window and hasattr(main_window, "update_general_map_info_display"): + try: + main_window.update_general_map_info_display( + zoom=None, map_size_str="N/A", map_geo_bounds=None, target_bbox_input=None, flight_count=None, + ) + except Exception: + pass + return + + map_manager: "MapCanvasManager" = main_window.map_manager_instance + if hasattr(map_manager, "get_current_map_info"): + map_info = {} + try: + map_info = map_manager.get_current_map_info() + zoom = map_info.get("zoom") + map_geo_bounds = map_info.get("map_geo_bounds") + target_bbox_input_from_map_info = map_info.get("target_bbox_input") + flight_count = map_info.get("flight_count") + map_size_km_w = map_info.get("map_size_km_w") + map_size_km_h = map_info.get("map_size_km_h") + + map_size_str = "N/A" + if map_size_km_w is not None and map_size_km_h is not None: + try: + decimals = getattr(app_config, "MAP_SIZE_KM_DECIMAL_PLACES", 1) + map_size_str = f"{map_size_km_w:.{decimals}f}km x {map_size_km_h:.{decimals}f}km" + except Exception as e_format: + module_logger.warning(f"MapCommandHandler: Error formatting map size: {e_format}. Using N/A.", exc_info=False) + map_size_str = "N/A (FormatErr)" + + if hasattr(main_window, "update_general_map_info_display"): + try: + main_window.update_general_map_info_display( + zoom=zoom, + map_size_str=map_size_str, + map_geo_bounds=map_geo_bounds, + target_bbox_input=target_bbox_input_from_map_info, + flight_count=flight_count, + ) + except tk.TclError as e_tcl: + module_logger.warning(f"MapCommandHandler: TclError updating general map info panel: {e_tcl}. GUI closing.") + except Exception as e_update: + module_logger.error(f"MapCommandHandler: Error updating general map info panel: {e_update}", exc_info=False) + except Exception as e_get_info: + module_logger.error(f"MapCommandHandler: Error getting map info from map manager: {e_get_info}", exc_info=True) + if main_window and hasattr(main_window, "update_general_map_info_display"): + try: + main_window.update_general_map_info_display( + zoom=None, map_size_str="N/A", map_geo_bounds=None, target_bbox_input=None, flight_count=None, + ) + except Exception: + pass + else: + module_logger.warning("MapCommandHandler: MapCanvasManager missing 'get_current_map_info' method.") + + + def map_zoom_in(self): + """Requests MapCanvasManager to zoom in at the current map center.""" + module_logger.debug("MapCommandHandler: Map Zoom In requested.") + + main_window = self.app_controller.main_window + if ( + main_window and hasattr(main_window, "map_manager_instance") + and main_window.map_manager_instance is not None + ): + map_manager: "MapCanvasManager" = main_window.map_manager_instance + if hasattr(map_manager, "zoom_in_at_center"): + try: + map_manager.zoom_in_at_center() + except Exception as e: + module_logger.error(f"MapCommandHandler: Error map_manager.zoom_in_at_center: {e}", exc_info=False) + else: + module_logger.warning("MapCommandHandler: MapCanvasManager missing 'zoom_in_at_center' method.") + else: + module_logger.warning("MapCommandHandler: Map manager N/A for zoom in.") + + def map_zoom_out(self): + """Requests MapCanvasManager to zoom out at the current map center.""" + module_logger.debug("MapCommandHandler: Map Zoom Out requested.") + + main_window = self.app_controller.main_window + if ( + main_window and hasattr(main_window, "map_manager_instance") + and main_window.map_manager_instance is not None + ): + map_manager: "MapCanvasManager" = main_window.map_manager_instance + if hasattr(map_manager, "zoom_out_at_center"): + try: + map_manager.zoom_out_at_center() + except Exception as e: + module_logger.error(f"MapCommandHandler: Error map_manager.zoom_out_at_center: {e}", exc_info=False) + else: + module_logger.warning("MapCommandHandler: MapCanvasManager missing 'zoom_out_at_center' method.") + else: + module_logger.warning("MapCommandHandler: Map manager N/A for zoom out.") + + def map_pan_direction(self, direction: str): + """ + Requests MapCanvasManager to pan the map in a specified direction. + + Args: + direction: The direction to pan ("up", "down", "left", "right"). + """ + module_logger.debug(f"MapCommandHandler: Map Pan '{direction}' requested.") + + main_window = self.app_controller.main_window + if ( + main_window and hasattr(main_window, "map_manager_instance") + and main_window.map_manager_instance is not None + ): + map_manager: "MapCanvasManager" = main_window.map_manager_instance + if hasattr(map_manager, "pan_map_fixed_step"): + try: + map_manager.pan_map_fixed_step(direction) + except Exception as e: + module_logger.error(f"MapCommandHandler: Error map_manager.pan_map_fixed_step('{direction}'): {e}", exc_info=False) + else: + module_logger.warning("MapCommandHandler: MapCanvasManager missing 'pan_map_fixed_step' method.") + else: + module_logger.warning(f"MapCommandHandler: Map manager N/A for pan {direction}.") + + def map_center_on_coords_and_fit_patch(self, lat: float, lon: float, patch_size_km: float): + """ + Requests MapCanvasManager to center the map at given coordinates and adjust zoom to fit a patch size. + """ + module_logger.debug(f"MapCommandHandler: Center map at ({lat:.4f},{lon:.4f}) and fit {patch_size_km}km patch.") + + main_window = self.app_controller.main_window + if ( + main_window and hasattr(main_window, "map_manager_instance") + and main_window.map_manager_instance is not None + ): + map_manager: "MapCanvasManager" = main_window.map_manager_instance + if hasattr(map_manager, "center_map_and_fit_patch"): + try: + map_manager.center_map_and_fit_patch(lat, lon, patch_size_km) + except Exception as e: + module_logger.error(f"MapCommandHandler: Error map_manager.center_map_and_fit_patch: {e}", exc_info=False) + else: + module_logger.warning("MapCommandHandler: MapCanvasManager missing 'center_map_and_fit_patch' method.") + else: + module_logger.warning("MapCommandHandler: Map manager N/A for center and fit patch.") + + def set_map_track_length(self, length: int): + """ + Requests MapCanvasManager to set the maximum number of track points to display. + """ + module_logger.info(f"MapCommandHandler: Request to set map track length to {length} points.") + + main_window = self.app_controller.main_window + if ( + main_window and hasattr(main_window, "map_manager_instance") + and main_window.map_manager_instance is not None + and hasattr(main_window.map_manager_instance, "set_max_track_points") + ): + try: + main_window.map_manager_instance.set_max_track_points(length) + except Exception as e: + module_logger.error(f"MapCommandHandler: Error calling map_manager.set_max_track_points({length}): {e}", exc_info=True) + if hasattr(main_window, "show_error_message"): + main_window.show_error_message("Map Configuration Error", f"Failed to set track length on map: {e}") + else: + module_logger.warning("MapCommandHandler: MapCanvasManager or 'set_max_track_points' N/A to set track length.") \ No newline at end of file diff --git a/flightmonitor/data/config.py b/flightmonitor/data/config.py index 3825397..3af8d61 100644 --- a/flightmonitor/data/config.py +++ b/flightmonitor/data/config.py @@ -169,3 +169,21 @@ LAYOUT_START_MAXIMIZED: bool = True # Per ora, le lasciamo come valori fissi, ma potremmo renderle più dinamiche. LAYOUT_WINDOW_MIN_WIDTH: int = 700 LAYOUT_WINDOW_MIN_HEIGHT: int = 500 + +# --- OpenSky API Authentication Configuration --- +# Set to True to attempt authentication with OpenSky Network API. +# If False, or if credentials are not provided, anonymous access will be used. +USE_OPENSKY_CREDENTIALS: bool = False # MODIFIED: Set to True to use your credentials + +# Your OpenSky Network API username and password. +# IMPORTANT: It's recommended to store sensitive information like passwords +# as environment variables (e.g., OPENSKY_USERNAME, OPENSKY_PASSWORD) +# and retrieve them using os.getenv() for security. +# For testing, you can uncomment and set them directly, but REMOVE THEM +# before sharing your code publicly. +OPENSKY_USERNAME: Optional[str] = "luca.vallongo@gmail.com" # os.getenv("OPENSKY_USERNAME", None) +OPENSKY_PASSWORD: Optional[str] = "Emanuela76@#Opensky" # os.getenv("OPENSKY_PASSWORD", None) + +# Example of setting directly for testing (DO NOT USE IN PRODUCTION OR PUBLIC REPOS): +# OPENSKY_USERNAME: Optional[str] = "YOUR_OPENSKY_USERNAME_HERE" +# OPENSKY_PASSWORD: Optional[str] = "YOUR_OPENSKY_PASSWORD_HERE" diff --git a/flightmonitor/data/opensky_live_adapter.py b/flightmonitor/data/opensky_live_adapter.py index 8dc53cb..86c820c 100644 --- a/flightmonitor/data/opensky_live_adapter.py +++ b/flightmonitor/data/opensky_live_adapter.py @@ -4,14 +4,14 @@ Adapter for fetching live flight data from the OpenSky Network API using the off This adapter polls the API periodically, transforms the raw data into CanonicalFlightState objects, and puts structured messages into an output queue. """ -import requests # La libreria opensky-api usa requests, quindi potremmo dover gestire le sue eccezioni +import requests import time import threading from queue import ( Queue, Empty as QueueEmpty, Full as QueueFull, -) # Manteniamo QueueFull per la gestione della coda di output +) import random from typing import Dict, Any, List, Tuple, Optional @@ -22,20 +22,14 @@ from typing import Dict, Any, List, Tuple, Optional # COME: Aggiunte le importazioni necessarie dalla libreria opensky_api. from opensky_api import OpenSkyApi -# Dalla documentazione, sembra che la libreria gestisca il rate-limiting internamente -# e sollevi eccezioni di requests per altri problemi. Non vedo eccezioni specifiche -# di OpenSkyApi da importare per la gestione degli errori di base qui. - -from . import config as app_config +from . import config as app_config # Assicurati che config sia importato per accedere alle credenziali from ..utils.logger import get_logger from .common_models import CanonicalFlightState module_logger = get_logger(__name__) # --- Adapter Specific Constants --- -PROVIDER_NAME = ( - "OpenSkyNetwork" # Rimane lo stesso, identifica la fonte per CanonicalFlightState -) +PROVIDER_NAME = "OpenSkyNetwork" INITIAL_BACKOFF_DELAY_SECONDS = 20.0 MAX_BACKOFF_DELAY_SECONDS = 300.0 BACKOFF_FACTOR = 1.8 @@ -51,9 +45,7 @@ MSG_TYPE_ADAPTER_STATUS = "adapter_status" STATUS_STARTING = "STARTING" STATUS_FETCHING = "FETCHING" STATUS_RECOVERED = "RECOVERED" -STATUS_RATE_LIMITED = ( - "RATE_LIMITED" # Potremmo non usarlo più se la libreria gestisce internamente -) +STATUS_RATE_LIMITED = "RATE_LIMITED" STATUS_API_ERROR_TEMPORARY = "API_ERROR_TEMPORARY" STATUS_PERMANENT_FAILURE = "PERMANENT_FAILURE" STATUS_STOPPING = "STOPPING" @@ -73,9 +65,8 @@ class OpenSkyLiveAdapter(threading.Thread): output_queue: Queue[AdapterMessage], bounding_box: Dict[ str, float - ], # Formato: {"lat_min": ..., "lon_min": ..., "lat_max": ..., "lon_max": ...} + ], polling_interval: int = app_config.LIVE_POLLING_INTERVAL_SECONDS, - # api_timeout non è più direttamente usato da noi, la libreria potrebbe averne uno suo o usare quello di requests daemon: bool = True, ): @@ -89,7 +80,7 @@ class OpenSkyLiveAdapter(threading.Thread): raise ValueError("Output queue must be provided to OpenSkyLiveAdapter.") self.output_queue = output_queue - self.bounding_box_dict = bounding_box # Il nostro formato standard + self.bounding_box_dict = bounding_box self.base_polling_interval = float(polling_interval) self._stop_event = threading.Event() @@ -97,21 +88,38 @@ class OpenSkyLiveAdapter(threading.Thread): self._current_backoff_delay: float = 0.0 self._in_backoff_mode: bool = False - # MODIFICATO: Inizializzazione del client opensky-api - # PERCHÉ: Necessario per utilizzare la libreria. + # MODIFICATO: Inizializzazione del client opensky-api con gestione credenziali/flag + # PERCHÉ: Permette di usare credenziali se fornite e la flag USE_OPENSKY_CREDENTIALS è True. # DOVE: Nel metodo __init__. - # COME: Creata un'istanza di OpenSkyApi. + # COME: Controllo della flag e delle credenziali per inizializzare OpenSkyApi. try: - self.api_client = OpenSkyApi() - module_logger.info( - f"{self.name}: OpenSkyApi client initialized successfully." - ) + username = app_config.OPENSKY_USERNAME + password = app_config.OPENSKY_PASSWORD + use_credentials_flag = getattr(app_config, "USE_OPENSKY_CREDENTIALS", False) # Default a False per retrocompatibilità + + module_logger.info(f"{self.name}: DEBUG - Inizializzazione OpenSkyApi:") + module_logger.info(f"{self.name}: DEBUG - USE_OPENSKY_CREDENTIALS flag: {use_credentials_flag}") + module_logger.info(f"{self.name}: DEBUG - Username from config: {'Presente' if username else 'Assente'}") + module_logger.info(f"{self.name}: DEBUG - Password from config: {'Presente' if password else 'Assente'}") + + if use_credentials_flag and username and password: + self.api_client = OpenSkyApi(username=username, password=password) + module_logger.info(f"{self.name}: OpenSkyApi client initialized with provided credentials.") + else: + self.api_client = OpenSkyApi() + log_msg = f"{self.name}: OpenSkyApi client initialized without credentials (anonymous access). " + if use_credentials_flag: + log_msg += "USE_OPENSKY_CREDENTIALS is True but credentials not found/provided." + else: + log_msg += "USE_OPENSKY_CREDENTIALS is False." + module_logger.warning(log_msg) + + except Exception as e: module_logger.critical( f"{self.name}: Failed to initialize OpenSkyApi client: {e}", exc_info=True, ) - # Potremmo voler sollevare l'eccezione o gestire lo stato in modo che il thread non parta raise RuntimeError(f"Failed to initialize OpenSkyApi client: {e}") from e module_logger.debug( @@ -135,8 +143,8 @@ class OpenSkyLiveAdapter(threading.Thread): icao24=f"mock{icao_suffix:02x}", callsign=f"MOCK{icao_suffix:02X}", origin_country="Mockland", - timestamp=now, # Primary timestamp (posizione) - last_contact_timestamp=now, # Timestamp ultimo contatto + timestamp=now, + last_contact_timestamp=now, latitude=round( lat_center + random.uniform(-lat_span / 2.1, lat_span / 2.1), 4 ), @@ -145,14 +153,14 @@ class OpenSkyLiveAdapter(threading.Thread): ), baro_altitude_m=random.uniform(1000, 12000), geo_altitude_m=random.uniform(1000, 12000) - + random.uniform(-200, 200), # Aggiunto geo_altitude fittizio + + random.uniform(-200, 200), on_ground=random.choice([True, False]), velocity_mps=random.uniform(50, 250), true_track_deg=random.uniform(0, 360), vertical_rate_mps=random.uniform(-10, 10), squawk=str(random.randint(1000, 7777)), spi=random.choice([True, False]), - position_source=0, # ADS-B (fittizio) + position_source=0, raw_data_provider=f"{PROVIDER_NAME}-Mock", ) @@ -187,51 +195,33 @@ class OpenSkyLiveAdapter(threading.Thread): ) self._stop_event.set() - # MODIFICATO: Il metodo _parse_state_vector è stato rimosso. - # PERCHÉ: La libreria opensky-api restituisce oggetti StateVector già parsati. - # DOVE: L'intero metodo è stato eliminato. - # COME: Rimozione del codice del metodo. - - # MODIFICATO: Nuovo metodo per convertire StateVector in CanonicalFlightState - # PERCHÉ: Necessario per mappare i dati dalla libreria al nostro modello canonico. - # DOVE: Aggiunto come nuovo metodo helper. - # COME: Implementata la logica di mapping campo per campo. def _convert_state_vector_to_canonical( self, sv: Any ) -> Optional[CanonicalFlightState]: """Converts an OpenSkyApi StateVector object to a CanonicalFlightState object.""" try: - if sv.icao24 is None: # icao24 è un campo obbligatorio + if sv.icao24 is None: module_logger.warning(f"Skipping StateVector with None icao24: {sv}") return None - # Gestione timestamp: CanonicalFlightState.timestamp è il timestamp della posizione/stato. - # StateVector ha time_position e last_contact. - # Diamo priorità a time_position per il nostro timestamp primario se disponibile. primary_ts = ( sv.time_position if sv.time_position is not None else sv.last_contact ) last_contact_ts = sv.last_contact - if ( - primary_ts is None - ): # Se anche last_contact è None, usiamo il tempo corrente + if primary_ts is None: primary_ts = time.time() module_logger.warning( f"ICAO {sv.icao24}: Using current time as primary_timestamp due to missing API timestamps (last_contact={sv.last_contact}, time_position={sv.time_position})." ) if last_contact_ts is None: - last_contact_ts = primary_ts # Se last_contact è None, impostalo al primary_ts determinato - - # La libreria dovrebbe già fornire i valori nelle unità corrette (metri, m/s) - # ma è bene verificarlo nella documentazione della libreria o con test. - # Ad esempio, velocity è in m/s, altitude in metri. + last_contact_ts = primary_ts return CanonicalFlightState( icao24=sv.icao24, callsign=( sv.callsign.strip() if sv.callsign else None - ), # Assicurati che il callsign sia pulito + ), origin_country=sv.origin_country, timestamp=float(primary_ts), last_contact_timestamp=float(last_contact_ts), @@ -247,7 +237,7 @@ class OpenSkyLiveAdapter(threading.Thread): spi=sv.spi, position_source=( str(sv.position_source) if sv.position_source is not None else None - ), # Converti in stringa per coerenza, se necessario + ), raw_data_provider=PROVIDER_NAME, ) except AttributeError as e_attr: @@ -268,26 +258,14 @@ class OpenSkyLiveAdapter(threading.Thread): Performs API request using opensky-api library or generates mock data. Returns a structured result dictionary. """ - # --- MOCK API Logic (invariato) --- if app_config.USE_MOCK_OPENSKY_API: - # ... (logica mock esistente, non la ripeto per brevità ma è identica a prima) ... - module_logger.info( - f"{self.name}: Using MOCK API data as per configuration." - ) - self._send_status_to_queue( - STATUS_FETCHING, "Generating mock flight data..." - ) - if ( - app_config.MOCK_API_ERROR_SIMULATION == "RATE_LIMITED" - ): # Simula rate limit + module_logger.info(f"{self.name}: Using MOCK API data as per configuration.") + self._send_status_to_queue(STATUS_FETCHING, "Generating mock flight data...") + if app_config.MOCK_API_ERROR_SIMULATION == "RATE_LIMITED": self._consecutive_api_errors += 1 self._in_backoff_mode = True - mock_retry_after = str( - getattr(app_config, "MOCK_RETRY_AFTER_SECONDS", 60) - ) - delay = self._calculate_next_backoff_delay( - provided_retry_after=mock_retry_after - ) + mock_retry_after = str(getattr(app_config, "MOCK_RETRY_AFTER_SECONDS", 60)) + delay = self._calculate_next_backoff_delay(provided_retry_after=mock_retry_after) err_msg = f"MOCK: Rate limited. Errors: {self._consecutive_api_errors}. Retrying in {delay:.1f}s." module_logger.warning(f"{self.name}: {err_msg}") return { @@ -297,9 +275,7 @@ class OpenSkyLiveAdapter(threading.Thread): "consecutive_errors": self._consecutive_api_errors, "status_code": 429, } - if ( - app_config.MOCK_API_ERROR_SIMULATION == "HTTP_ERROR" - ): # Simula errore HTTP generico + if app_config.MOCK_API_ERROR_SIMULATION == "HTTP_ERROR": self._consecutive_api_errors += 1 self._in_backoff_mode = True delay = self._calculate_next_backoff_delay() @@ -318,15 +294,11 @@ class OpenSkyLiveAdapter(threading.Thread): mock_count = getattr(app_config, "MOCK_API_FLIGHT_COUNT", 5) for i in range(mock_count): mock_states.append(self._generate_mock_flight_state(i + 1)) - module_logger.info( - f"{self.name}: Generated {len(mock_states)} mock flight states." - ) - self._reset_error_state() # Resetta errori se il mock ha successo + module_logger.info(f"{self.name}: Generated {len(mock_states)} mock flight states.") + self._reset_error_state() return {"data": mock_states} - # --- END MOCK API Logic --- - # --- REAL API Call Logic (MODIFICATO per usare opensky-api) --- - if not self.bounding_box_dict: # Usa il nostro formato standard per il check + if not self.bounding_box_dict: err_msg = "Bounding box not set for REAL API request." module_logger.error(f"{self.name}: {err_msg}") return { @@ -337,10 +309,6 @@ class OpenSkyLiveAdapter(threading.Thread): "consecutive_errors": self._consecutive_api_errors, } - # MODIFICATO: Conversione del BBox al formato richiesto dalla libreria (tupla) - # PERCHÉ: La libreria opensky-api si aspetta una tupla (min_lat, max_lat, min_lon, max_lon). - # DOVE: Prima della chiamata API. - # COME: Creata una tupla dal dizionario self.bounding_box_dict. try: api_bbox_tuple = ( self.bounding_box_dict["lat_min"], @@ -359,22 +327,11 @@ class OpenSkyLiveAdapter(threading.Thread): "consecutive_errors": self._consecutive_api_errors, } - self._send_status_to_queue( - STATUS_FETCHING, f"Requesting REAL data for bbox (tuple): {api_bbox_tuple}" - ) + self._send_status_to_queue(STATUS_FETCHING, f"Requesting REAL data for bbox (tuple): {api_bbox_tuple}") try: - # MODIFICATO: Chiamata API tramite la libreria opensky-api - # PERCHÉ: Sostituisce la chiamata HTTP manuale. - # DOVE: All'interno del blocco try per la chiamata API. - # COME: Utilizzato self.api_client.get_states(). states_response = self.api_client.get_states(bbox=api_bbox_tuple) - - # La libreria gestisce il rate limiting internamente, quindi non dovremmo vedere 429 qui - # a meno che la libreria non lo propaghi, cosa che non sembra fare per default. - # Se get_states() ritorna, assumiamo che sia andato a buon fine o che il rate limit sia stato gestito. - - self._reset_error_state() # Chiamata API riuscita (o rate limit gestito dalla libreria) + self._reset_error_state() canonical_states: List[CanonicalFlightState] = [] if states_response and states_response.states: @@ -382,29 +339,18 @@ class OpenSkyLiveAdapter(threading.Thread): canonical_sv = self._convert_state_vector_to_canonical(sv) if canonical_sv: canonical_states.append(canonical_sv) - module_logger.info( - f"{self.name}: Fetched and parsed {len(canonical_states)} flight states via opensky-api." - ) + module_logger.info(f"{self.name}: Fetched and parsed {len(canonical_states)} flight states via opensky-api.") else: - module_logger.info( - f"{self.name}: API returned no flight states (states_response or .states is null/empty)." - ) + module_logger.info(f"{self.name}: API returned no flight states (states_response or .states is null/empty).") return {"data": canonical_states} - # MODIFICATO: Gestione delle eccezioni - # PERCHÉ: La libreria opensky-api potrebbe sollevare eccezioni di 'requests' per errori di rete/HTTP. - # DOVE: Nel blocco except per la chiamata API. - # COME: Catturate eccezioni generiche di 'requests' e attivata la nostra logica di backoff. except ( requests.exceptions.HTTPError - ) as http_err: # Cattura errori HTTP specifici + ) as http_err: self._consecutive_api_errors += 1 self._in_backoff_mode = True - delay = ( - self._calculate_next_backoff_delay() - ) # Non c'è Retry-After header da passare qui + delay = self._calculate_next_backoff_delay() status_code = http_err.response.status_code if http_err.response else "N/A" - # Se è 429, la libreria dovrebbe averlo gestito. Se lo vediamo qui, è inaspettato. if status_code == 429: err_msg = f"UNEXPECTED Rate limit (429) propagated by library. Errors: {self._consecutive_api_errors}. Retrying in {delay:.1f}s." module_logger.error(f"{self.name}: {err_msg}") @@ -417,9 +363,7 @@ class OpenSkyLiveAdapter(threading.Thread): } else: err_msg = f"HTTP error {status_code} from opensky-api: {http_err}. Errors: {self._consecutive_api_errors}. Retrying in {delay:.1f}s." - module_logger.error( - f"{self.name}: {err_msg}", exc_info=False - ) # exc_info=False per non loggare lo stacktrace di requests + module_logger.error(f"{self.name}: {err_msg}", exc_info=False) return { "error_type": STATUS_API_ERROR_TEMPORARY, "status_code": status_code, @@ -444,14 +388,12 @@ class OpenSkyLiveAdapter(threading.Thread): except ( requests.exceptions.RequestException - ) as req_err: # Cattura altre eccezioni di requests (es. ConnectionError) + ) as req_err: self._consecutive_api_errors += 1 self._in_backoff_mode = True delay = self._calculate_next_backoff_delay() err_msg = f"Request error via opensky-api: {req_err}. Errors: {self._consecutive_api_errors}. Retrying in {delay:.1f}s." - module_logger.error( - f"{self.name}: {err_msg}", exc_info=True - ) # exc_info=True qui può essere utile + module_logger.error(f"{self.name}: {err_msg}", exc_info=True) return { "error_type": STATUS_API_ERROR_TEMPORARY, "status_code": "REQUEST_EXCEPTION", @@ -460,7 +402,7 @@ class OpenSkyLiveAdapter(threading.Thread): "consecutive_errors": self._consecutive_api_errors, } - except Exception as e: # Cattura eccezioni impreviste + except Exception as e: self._consecutive_api_errors += 1 self._in_backoff_mode = True delay = self._calculate_next_backoff_delay() @@ -476,153 +418,95 @@ class OpenSkyLiveAdapter(threading.Thread): def _calculate_next_backoff_delay( self, - provided_retry_after: Optional[ - str - ] = None, # Mantenuto per il MOCK, ma la libreria dovrebbe gestire per le chiamate reali + provided_retry_after: Optional[str] = None, ) -> float: calculated_delay = INITIAL_BACKOFF_DELAY_SECONDS * ( BACKOFF_FACTOR ** (self._consecutive_api_errors - 1) ) api_delay_from_header = 0.0 - # Questa logica per Retry-After potrebbe non essere più necessaria se la libreria gestisce il 429. - # La manteniamo per il mock e se la libreria dovesse propagare un 429 in modo inaspettato. if provided_retry_after is not None and provided_retry_after.isdigit(): try: api_delay_from_header = float(provided_retry_after) - module_logger.debug( - f"{self.name}: Found Retry-After header value: {provided_retry_after}s" - ) + module_logger.debug(f"{self.name}: Found Retry-After header value: {provided_retry_after}s") except ValueError: - module_logger.warning( - f"{self.name}: Could not parse Retry-After header value: '{provided_retry_after}'" - ) + module_logger.warning(f"{self.name}: Could not parse Retry-After header value: '{provided_retry_after}'") api_delay_from_header = 0.0 self._current_backoff_delay = max(api_delay_from_header, calculated_delay) - self._current_backoff_delay = min( - self._current_backoff_delay, MAX_BACKOFF_DELAY_SECONDS - ) - self._current_backoff_delay = max( - self._current_backoff_delay, 1.0 - ) # Assicura un delay minimo - module_logger.debug( - f"{self.name}: Calculated next backoff delay: {self._current_backoff_delay:.1f}s" - ) + self._current_backoff_delay = min(self._current_backoff_delay, MAX_BACKOFF_DELAY_SECONDS) + self._current_backoff_delay = max(self._current_backoff_delay, 1.0) + module_logger.debug(f"{self.name}: Calculated next backoff delay: {self._current_backoff_delay:.1f}s") return self._current_backoff_delay def _reset_error_state(self): if self._in_backoff_mode or self._consecutive_api_errors > 0: - module_logger.info( - f"{self.name}: API connection successful after {self._consecutive_api_errors} error(s). Resetting backoff." - ) + module_logger.info(f"{self.name}: API connection successful after {self._consecutive_api_errors} error(s). Resetting backoff.") self._send_status_to_queue(STATUS_RECOVERED, "API connection recovered.") self._consecutive_api_errors = 0 self._current_backoff_delay = 0.0 self._in_backoff_mode = False def run(self): - initial_settle_delay_seconds = ( - 0.1 # Breve ritardo per permettere alla GUI di stabilizzarsi se necessario - ) - module_logger.debug( - f"{self.name} thread starting initial settle delay ({initial_settle_delay_seconds}s)..." - ) + initial_settle_delay_seconds = 0.1 + module_logger.debug(f"{self.name} thread starting initial settle delay ({initial_settle_delay_seconds}s)...") if self._stop_event.wait(timeout=initial_settle_delay_seconds): - module_logger.info( - f"{self.name} thread received stop signal during initial settle. Terminating." - ) + module_logger.info(f"{self.name} thread received stop signal during initial settle. Terminating.") return - module_logger.info( - f"{self.name} thread is fully operational. Base polling interval: {self.base_polling_interval:.1f}s." - ) - self._send_status_to_queue( - STATUS_STARTING, "Adapter thread started, preparing initial fetch." - ) + module_logger.info(f"{self.name} thread is fully operational. Base polling interval: {self.base_polling_interval:.1f}s.") + self._send_status_to_queue(STATUS_STARTING, "Adapter thread started, preparing initial fetch.") while not self._stop_event.is_set(): if self._consecutive_api_errors >= MAX_CONSECUTIVE_ERRORS_THRESHOLD: perm_fail_msg = f"Reached max ({self._consecutive_api_errors}) consecutive API errors. Stopping live updates." module_logger.critical(f"{self.name}: {perm_fail_msg}") self._send_status_to_queue(STATUS_PERMANENT_FAILURE, perm_fail_msg) - break # Esce dal loop principale + break api_result = self._perform_api_request() - if ( - self._stop_event.is_set() - ): # Controlla di nuovo dopo la chiamata API (potrebbe essere lunga) - module_logger.info( - f"{self.name}: Stop event detected after API request. Exiting loop." - ) + if self._stop_event.is_set(): + module_logger.info(f"{self.name}: Stop event detected after API request. Exiting loop.") break if "data" in api_result: flight_data_payload: List[CanonicalFlightState] = api_result["data"] try: - self.output_queue.put_nowait( - {"type": MSG_TYPE_FLIGHT_DATA, "payload": flight_data_payload} - ) - module_logger.debug( - f"{self.name}: Sent {len(flight_data_payload)} flight states to queue." - ) + self.output_queue.put_nowait({"type": MSG_TYPE_FLIGHT_DATA, "payload": flight_data_payload}) + module_logger.debug(f"{self.name}: Sent {len(flight_data_payload)} flight states to queue.") except QueueFull: - module_logger.warning( - f"{self.name}: Output queue full. Discarding {len(flight_data_payload)} flight states." - ) - except Exception as e: # Altre eccezioni mettendo in coda - module_logger.error( - f"{self.name}: Error putting flight data into queue: {e}", - exc_info=True, - ) + module_logger.warning(f"{self.name}: Output queue full. Discarding {len(flight_data_payload)} flight states.") + except Exception as e: + module_logger.error(f"{self.name}: Error putting flight data into queue: {e}", exc_info=True) - elif ( - "error_type" in api_result - ): # Se _perform_api_request ha restituito un errore strutturato + elif ("error_type" in api_result): error_details_for_controller = api_result.copy() - # Assicurati che il tipo sia corretto per il messaggio di stato error_details_for_controller["type"] = MSG_TYPE_ADAPTER_STATUS - # status_code è già l'error_type - # message dovrebbe già essere presente - # delay e consecutive_errors sono utili per il controller self._send_status_to_queue( - status_code=api_result[ - "error_type" - ], # es. STATUS_API_ERROR_TEMPORARY + status_code=api_result["error_type"], message=api_result.get("message", "An API error occurred."), - details=api_result, # Passa tutti i dettagli dell'errore - ) - else: # Struttura risultato sconosciuta - module_logger.error( - f"{self.name}: Unknown result structure from _perform_api_request: {api_result}" + details=api_result, ) + else: + module_logger.error(f"{self.name}: Unknown result structure from _perform_api_request: {api_result}") time_to_wait_seconds: float if self._in_backoff_mode: time_to_wait_seconds = self._current_backoff_delay - module_logger.debug( - f"{self.name}: In backoff, next attempt in {time_to_wait_seconds:.1f}s." - ) + module_logger.debug(f"{self.name}: In backoff, next attempt in {time_to_wait_seconds:.1f}s.") else: time_to_wait_seconds = self.base_polling_interval - module_logger.debug( - f"{self.name}: Next fetch cycle in {time_to_wait_seconds:.1f}s." - ) + module_logger.debug(f"{self.name}: Next fetch cycle in {time_to_wait_seconds:.1f}s.") if time_to_wait_seconds > 0: if self._stop_event.wait(timeout=time_to_wait_seconds): - print( - f"{self.name}: Stop event received during wait period. Exiting loop." - ) + print(f"{self.name}: Stop event received during wait period. Exiting loop.") break - else: # Se il tempo di attesa è zero o negativo, controlla comunque lo stop event + else: if self._stop_event.is_set(): - print( - f"{self.name}: Stop event detected before waiting (wait time <= 0). Exiting loop." - ) + print(f"{self.name}: Stop event detected before waiting (wait time <= 0). Exiting loop.") break - # --- End Main Adapter Loop --- - + print(f"{self.name}: Exited main loop. Sending final STOPPED status.") try: final_status_payload = { @@ -633,13 +517,8 @@ class OpenSkyLiveAdapter(threading.Thread): self.output_queue.put_nowait(final_status_payload) print(f"{self.name}: Successfully sent final STATUS_STOPPED to queue.") except QueueFull: - print( - f"{self.name}: Output queue full. Could not send final STATUS_STOPPED message." - ) + print(f"{self.name}: Output queue full. Could not send final STATUS_STOPPED message.") except Exception as e_final_put: - print( - f"{self.name}: Error sending final STATUS_STOPPED message: {e_final_put}", - exc_info=True, - ) + print(f"{self.name}: Error sending final STATUS_STOPPED message: {e_final_put}", exc_info=True) - print(f"{self.name}: RUN method is terminating now.") + print(f"{self.name}: RUN method is terminating now.") \ No newline at end of file