refactoring app_controller
This commit is contained in:
parent
4d212b5f6e
commit
c7127d8fe3
@ -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
|
||||
|
||||
@ -60,6 +65,12 @@ class AppController:
|
||||
|
||||
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."
|
||||
)
|
||||
module_logger.warning("Controller: MapCommandHandler not initialized for set_map_track_length.")
|
||||
155
flightmonitor/controller/cleanup_manager.py
Normal file
155
flightmonitor/controller/cleanup_manager.py
Normal file
@ -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."
|
||||
)
|
||||
389
flightmonitor/controller/map_command_handler.py
Normal file
389
flightmonitor/controller/map_command_handler.py
Normal file
@ -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.")
|
||||
@ -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"
|
||||
|
||||
@ -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,152 +418,94 @@ 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:
|
||||
@ -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.")
|
||||
Loading…
Reference in New Issue
Block a user