584 lines
24 KiB
Python
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."
|
|
)
|