# FlightMonitor/gui/panels/map_tools_panel.py """ Panel for controlling map interactions (zoom, pan, center & fit). """ import tkinter as tk from tkinter import ttk from typing import Any, Optional, Tuple from flightmonitor.utils.logger import get_logger from flightmonitor.data import ( config as app_config, ) # Used for default patch size for center & fit module_logger = get_logger(__name__) # Constants for map tools, if any specific to this panel DEFAULT_CENTER_PATCH_SIZE_KM = 100 # Default value for the "Patch (km)" entry class MapToolsPanel: """ Manages the GUI elements for map control, including zoom, pan, and centering. """ def __init__(self, parent_frame: ttk.Frame, controller: Any): """ Initializes the MapToolsPanel. Args: parent_frame: The parent ttk.Frame where this panel will be placed. controller: The application controller instance to delegate map actions. """ self.parent_frame = parent_frame self.controller = controller # --- Zoom Controls --- # Crea un frame per i controlli di zoom e i relativi bottoni. self.zoom_frame = ttk.Frame(self.parent_frame) self.zoom_frame.pack(side=tk.TOP, pady=(0, 10)) ttk.Button(self.zoom_frame, text="Zoom In (+)", command=self._on_zoom_in).pack( side=tk.LEFT, padx=2 ) ttk.Button( self.zoom_frame, text="Zoom Out (-)", command=self._on_zoom_out ).pack(side=tk.LEFT, padx=2) # --- Pan Controls --- # Crea un frame per i controlli di pan (spostamento) e i relativi bottoni, disposti a croce. self.pan_frame = ttk.Frame(self.parent_frame) self.pan_frame.pack(side=tk.TOP, pady=(0, 10)) ttk.Button(self.pan_frame, text="Up", command=lambda: self._on_pan("up")).grid( row=0, column=1, padx=2, pady=2 ) ttk.Button( self.pan_frame, text="Left", command=lambda: self._on_pan("left") ).grid(row=1, column=0, padx=2, pady=2) ttk.Button( self.pan_frame, text="Right", command=lambda: self._on_pan("right") ).grid(row=1, column=2, padx=2, pady=2) ttk.Button( self.pan_frame, text="Down", command=lambda: self._on_pan("down") ).grid(row=2, column=1, padx=2, pady=2) # Configura le colonne per garantire un allineamento corretto dei bottoni di pan. self.pan_frame.columnconfigure(0, weight=1) self.pan_frame.columnconfigure(2, weight=1) # --- Center Map Controls --- # Crea un LabelFrame per i controlli di centramento mappa, inclusi input per lat, lon e dimensione patch. self.center_frame = ttk.LabelFrame( self.parent_frame, text="Center Map", padding=5 ) self.center_frame.pack(side=tk.TOP, fill=tk.X, pady=(5, 0)) # Configura le colonne per gli input di lat/lon/patch. self.center_frame.columnconfigure(1, weight=1) self.center_frame.columnconfigure(3, weight=1) ttk.Label(self.center_frame, text="Lat:").grid( row=0, column=0, padx=(0, 2), pady=2, sticky=tk.W ) self.center_lat_var = tk.StringVar() self.center_lat_entry = ttk.Entry( self.center_frame, textvariable=self.center_lat_var, width=10 ) self.center_lat_entry.grid(row=0, column=1, padx=(0, 5), pady=2, sticky=tk.EW) ttk.Label(self.center_frame, text="Lon:").grid( row=0, column=2, padx=(5, 2), pady=2, sticky=tk.W ) self.center_lon_var = tk.StringVar() self.center_lon_entry = ttk.Entry( self.center_frame, textvariable=self.center_lon_var, width=10 ) self.center_lon_entry.grid(row=0, column=3, padx=(0, 0), pady=2, sticky=tk.EW) ttk.Label(self.center_frame, text="Patch (km):").grid( row=1, column=0, padx=(0, 2), pady=2, sticky=tk.W ) self.center_patch_size_var = tk.StringVar( value=str(DEFAULT_CENTER_PATCH_SIZE_KM) ) self.center_patch_size_entry = ttk.Entry( self.center_frame, textvariable=self.center_patch_size_var, width=7 ) self.center_patch_size_entry.grid( row=1, column=1, padx=(0, 5), pady=2, sticky=tk.W ) self.center_map_button = ttk.Button( self.center_frame, text="Center & Fit Patch", command=self._on_center_and_fit, ) self.center_map_button.grid( row=1, column=2, columnspan=2, padx=5, pady=5, sticky=tk.E ) module_logger.debug("MapToolsPanel initialized.") def _on_zoom_in(self): """Delegates the 'zoom in' action to the application controller.""" if self.controller and hasattr(self.controller, "map_zoom_in"): self.controller.map_zoom_in() else: module_logger.warning("Controller or map_zoom_in method not available.") def _on_zoom_out(self): """Delegates the 'zoom out' action to the application controller.""" if self.controller and hasattr(self.controller, "map_zoom_out"): self.controller.map_zoom_out() else: module_logger.warning("Controller or map_zoom_out method not available.") def _on_pan(self, direction: str): """ Delegates a 'pan' action in a specified direction to the application controller. Args: direction: The direction to pan ("up", "down", "left", "right"). """ if self.controller and hasattr(self.controller, "map_pan_direction"): self.controller.map_pan_direction(direction) else: module_logger.warning( f"Controller or map_pan_direction method not available for pan '{direction}'." ) def _on_center_and_fit(self): """ Retrieves latitude, longitude, and patch size from GUI entries and delegates the 'center and fit' action to the application controller. Includes input validation and error reporting via MainWindow. """ try: # Retrieve string values from Tkinter variables lat_str = self.center_lat_var.get() lon_str = self.center_lon_var.get() patch_str = self.center_patch_size_var.get() # Input validation: check for empty strings if not lat_str or not lon_str or not patch_str: self._show_error_message( "Input Error", "Latitude, Longitude, and Patch size are required." ) return # Convert string values to floats lat = float(lat_str) lon = float(lon_str) patch = float(patch_str) # Validate numerical ranges for geographic coordinates and patch size if not (-90 <= lat <= 90 and -180 <= lon <= 180 and patch > 0): self._show_error_message( "Input Error", "Invalid latitude (-90 to 90), longitude (-180 to 180), or patch size (>0).", ) return # Delegate the action to the controller if self.controller and hasattr( self.controller, "map_center_on_coords_and_fit_patch" ): self.controller.map_center_on_coords_and_fit_patch(lat, lon, patch) else: module_logger.warning( "Controller or map_center_on_coords_and_fit_patch method not available." ) self._show_error_message( "Error", "Map center function not available (controller issue)." ) except ValueError: # Catch errors if float conversion fails (e.g., non-numeric input) self._show_error_message( "Input Error", "Latitude, Longitude, and Patch size must be valid numbers.", ) except Exception as e: # Catch any other unexpected errors module_logger.error(f"Error in _on_center_and_fit: {e}", exc_info=True) self._show_error_message("Error", f"An unexpected error occurred: {e}") def _show_error_message(self, title: str, message: str): """ Helper method to show an error message, typically delegated to MainWindow. This avoids direct dependency on tkinter.messagebox here. """ if ( self.controller and hasattr(self.controller, "main_window") and self.controller.main_window ): main_window = self.controller.main_window if hasattr(main_window, "show_error_message"): main_window.show_error_message(title, message) else: module_logger.error( f"MainWindow.show_error_message not found. Error: {title} - {message}" ) else: module_logger.error( f"Controller or MainWindow not available to show error: {title} - {message}" )