# 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, canvas_x: int, canvas_y: int, 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"): # Use canvas_x, canvas_y for flight identification clicked_flight_icao = map_manager.get_clicked_flight_icao(canvas_x, canvas_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 # 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.")