389 lines
21 KiB
Python
389 lines
21 KiB
Python
# 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.") |