SXXXXXXX_FlightMonitor/flightmonitor/controller/map_command_handler.py
2025-06-13 11:48:49 +02:00

584 lines
24 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 flightmonitor.utils.logger import get_logger
from flightmonitor.data import config as app_config # For map size decimal places, etc.
from flightmonitor.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 flightmonitor.controller.app_controller import (
AppController,
) # For type hinting AppController
from flightmonitor.gui.main_window import MainWindow
from flightmonitor.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."
)