# FlightMonitor/gui/main_window.py """ Main window of the Flight Monitor application. Handles the layout with multiple notebooks for functions and views, user interactions, status display including a semaphore, and logging. """ import tkinter as tk from tkinter import ttk from tkinter import messagebox from tkinter.scrolledtext import ScrolledText from tkinter import font as tkFont from typing import List, Dict, Optional, Tuple, Any # Relative imports from ..data import config as fm_config # Rinominiamo per evitare conflitto con il 'config' del modulo tk from ..utils.logger import get_logger, shutdown_gui_logging from ..data.common_models import CanonicalFlightState try: from ..map.map_canvas_manager import MapCanvasManager MAP_CANVAS_MANAGER_AVAILABLE = True except ImportError as e_map_import: MapCanvasManager = None # type: ignore MAP_CANVAS_MANAGER_AVAILABLE = False # Il logger potrebbe non essere ancora pronto, quindi usiamo print per questo errore critico print(f"CRITICAL ERROR in MainWindow: Failed to import MapCanvasManager: {e_map_import}. Map functionality will be disabled.") module_logger = get_logger(__name__) # flightmonitor.gui.main_window # --- Constants for Semaphore --- SEMAPHORE_SIZE = 12 SEMAPHORE_PAD = 3 SEMAPHORE_BORDER_WIDTH = 1 SEMAPHORE_TOTAL_SIZE = SEMAPHORE_SIZE + 2 * (SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH) SEMAPHORE_COLOR_OK = "green3" SEMAPHORE_COLOR_WARNING = "gold" SEMAPHORE_COLOR_ERROR = "red2" SEMAPHORE_COLOR_UNKNOWN = "gray70" SEMAPHORE_COLOR_FETCHING = "sky blue" class MainWindow: """ Main window of the Flight Monitor application. Handles layout with function and view notebooks, interactions, status, and logging. """ def __init__(self, root: tk.Tk, controller: Any): # controller type can be AppController """ Initializes the main window. Args: root (tk.Tk): The root Tkinter object. controller: The application controller instance. """ self.root = root self.controller = controller self.root.title("Flight Monitor") # Stima e calcolo dimensioni finestra controls_estimated_height = 50 bbox_estimated_height = 70 log_area_min_height = 80 map_min_height = 300 # Minima altezza ragionevole per la mappa status_bar_actual_height = 30 info_panel_min_width = 200 # Larghezza minima per il pannello info # Altezza per il pannello superiore che contiene Function Notebook e Info Panel min_function_notebook_height = controls_estimated_height + bbox_estimated_height + 40 # Aggiunto padding # Il pannello superiore ora è un PanedWindow orizzontale, la sua altezza è dettata dal Function Notebook min_paned_top_horizontal_panel_height = min_function_notebook_height # Altezza per il pannello che contiene il Views Notebook (mappa) min_views_notebook_panel_height = map_min_height + 20 # Tab per Views Notebook min_paned_bottom_panel_height = log_area_min_height + status_bar_actual_height + 10 # Altezza totale minima: somma delle altezze dei pannelli nel PanedWindow verticale min_total_height = min_paned_top_horizontal_panel_height + \ min_views_notebook_panel_height + \ min_paned_bottom_panel_height + \ 30 # Spazio per i separatori dei PanedWindow e padding generale # Larghezza totale minima # Larghezza Function Notebook + Info Panel + padding # Assumiamo che il Function Notebook abbia bisogno di almeno 350-400px min_func_notebook_width = 350 min_total_width = min_func_notebook_width + info_panel_min_width + 30 # padding e separatore if min_total_width < 650: min_total_width = 650 # Un valore minimo assoluto self.root.minsize(min_total_width, min_total_height) module_logger.debug(f"Minimum window size set to: {min_total_width}x{min_total_height}") # Calcolo dimensioni iniziali finestra # Larghezza: abbastanza per Function Notebook, Info Panel, e un po' di mappa initial_func_nb_width = 400 initial_info_panel_width = 250 initial_map_canvas_width = getattr(fm_config, 'DEFAULT_CANVAS_WIDTH', 800) # La larghezza del ViewsNotebook (e quindi del canvas) sarà gestita dal PanedWindow # La larghezza totale sarà la somma o il massimo necessario. # Per ora, facciamo in modo che il canvas abbia una buona larghezza. # La larghezza iniziale del PanedWindow orizzontale sarà data dalla somma. # Poi l'utente può ridimensionare. initial_width = initial_func_nb_width + initial_info_panel_width + 20 # per il separatore if initial_width < min_total_width : initial_width = min_total_width # Se il canvas della mappa è più largo, la finestra si adatterà # Questa logica di larghezza è un po' complessa con i paned window, # affidiamoci ai pesi e al minsize. # Usiamo una larghezza iniziale che permetta di vedere bene i controlli. initial_width = 1200 # Un valore fisso per iniziare, poi l'utente ridimensiona. # Altezza iniziale: simile al calcolo del min_total_height ma con valori desiderati initial_function_notebook_height = controls_estimated_height + bbox_estimated_height + 50 initial_views_notebook_height = getattr(fm_config, 'DEFAULT_CANVAS_HEIGHT', 600) + 40 initial_log_height = 150 initial_total_height = max(initial_function_notebook_height, 150) + \ initial_views_notebook_height + \ initial_log_height + status_bar_actual_height + 40 if initial_total_height < min_total_height: initial_total_height = min_total_height self.root.geometry(f"{initial_width}x{initial_total_height}") module_logger.debug(f"Initial window size set to: {initial_width}x{initial_total_height}") # --- Main Layout: PanedWindow Verticale --- self.main_paned_window = ttk.PanedWindow(self.root, orient=tk.VERTICAL) self.main_paned_window.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # Aggiunto padding # --- 1. Pannello Superiore Orizzontale (per Controlli e Info Mappa) --- self.top_horizontal_paned_window = ttk.PanedWindow(self.main_paned_window, orient=tk.HORIZONTAL) self.main_paned_window.add(self.top_horizontal_paned_window, weight=0) # Non espandere verticalmente, altezza fissa basata sul contenuto # --- 1a. Pannello Sinistro del Top Horizontal Paned Window (Function Notebook) --- self.left_control_panel_frame = ttk.Frame(self.top_horizontal_paned_window, padding=(0,0,5,0)) # Padding a destra self.top_horizontal_paned_window.add(self.left_control_panel_frame, weight=1) # Più spazio ai controlli self.function_notebook = ttk.Notebook(self.left_control_panel_frame) self.function_notebook.pack(fill=tk.BOTH, expand=True) # Lascia che riempia il suo pane # --- Schede del Function Notebook --- self.live_bbox_tab_frame = ttk.Frame(self.function_notebook, padding=5) self.function_notebook.add(self.live_bbox_tab_frame, text="Live: Area Monitor") # Contenuto di live_bbox_tab_frame (Controls e BBox input) self.controls_frame = ttk.Frame(self.live_bbox_tab_frame) self.controls_frame.pack(side=tk.TOP, fill=tk.X, pady=(0,5)) self.control_frame = ttk.LabelFrame(self.controls_frame, text="Controls", padding=(10, 5)) self.control_frame.pack(side=tk.TOP, fill=tk.X) self.mode_var = tk.StringVar(value="Live") self.live_radio = ttk.Radiobutton(self.control_frame, text="Live", variable=self.mode_var, value="Live", command=self._on_mode_change) self.live_radio.pack(side=tk.LEFT, padx=(0,5)) self.history_radio = ttk.Radiobutton(self.control_frame, text="History", variable=self.mode_var, value="History", command=self._on_mode_change) self.history_radio.pack(side=tk.LEFT, padx=5) self.start_button = ttk.Button(self.control_frame, text="Start Monitoring", command=self._start_monitoring) self.start_button.pack(side=tk.LEFT, padx=5) self.stop_button = ttk.Button(self.control_frame, text="Stop Monitoring", command=self._stop_monitoring, state=tk.DISABLED) self.stop_button.pack(side=tk.LEFT, padx=5) self.bbox_frame = ttk.LabelFrame(self.controls_frame, text="Geographic Area (Bounding Box)", padding=(10,5)) self.bbox_frame.pack(side=tk.TOP, fill=tk.X, pady=5) self.bbox_frame.columnconfigure(1, weight=1) self.bbox_frame.columnconfigure(3, weight=1) ttk.Label(self.bbox_frame, text="Lat Min:").grid(row=0, column=0, padx=(0,2), pady=2, sticky=tk.W) self.lat_min_var = tk.StringVar(value=str(fm_config.DEFAULT_BBOX_LAT_MIN)) self.lat_min_entry = ttk.Entry(self.bbox_frame, textvariable=self.lat_min_var, width=10) self.lat_min_entry.grid(row=0, column=1, padx=(0,5), pady=2, sticky=tk.EW) ttk.Label(self.bbox_frame, text="Lon Min:").grid(row=0, column=2, padx=(5,2), pady=2, sticky=tk.W) self.lon_min_var = tk.StringVar(value=str(fm_config.DEFAULT_BBOX_LON_MIN)) self.lon_min_entry = ttk.Entry(self.bbox_frame, textvariable=self.lon_min_var, width=10) self.lon_min_entry.grid(row=0, column=3, padx=(0,0), pady=2, sticky=tk.EW) ttk.Label(self.bbox_frame, text="Lat Max:").grid(row=1, column=0, padx=(0,2), pady=2, sticky=tk.W) self.lat_max_var = tk.StringVar(value=str(fm_config.DEFAULT_BBOX_LAT_MAX)) self.lat_max_entry = ttk.Entry(self.bbox_frame, textvariable=self.lat_max_var, width=10) self.lat_max_entry.grid(row=1, column=1, padx=(0,5), pady=2, sticky=tk.EW) ttk.Label(self.bbox_frame, text="Lon Max:").grid(row=1, column=2, padx=(5,2), pady=2, sticky=tk.W) self.lon_max_var = tk.StringVar(value=str(fm_config.DEFAULT_BBOX_LON_MAX)) self.lon_max_entry = ttk.Entry(self.bbox_frame, textvariable=self.lon_max_var, width=10) self.lon_max_entry.grid(row=1, column=3, padx=(0,0), pady=2, sticky=tk.EW) self.live_airport_tab_frame = ttk.Frame(self.function_notebook, padding=5) self.function_notebook.add(self.live_airport_tab_frame, text="Live: Airport") ttk.Label(self.live_airport_tab_frame, text="Live from Airport - Coming Soon", font=("Arial", 10)).pack(expand=True) self.history_tab_frame = ttk.Frame(self.function_notebook, padding=5) self.function_notebook.add(self.history_tab_frame, text="History") ttk.Label(self.history_tab_frame, text="History Analysis - Coming Soon", font=("Arial", 10)).pack(expand=True) self.function_notebook.bind("<>", self._on_function_tab_change) # --- 1b. Pannello Destro del Top Horizontal Paned Window (Info Mappa) --- self.map_info_panel_frame = ttk.LabelFrame(self.top_horizontal_paned_window, text="Map Information", padding=10) self.top_horizontal_paned_window.add(self.map_info_panel_frame, weight=0) # Meno peso, larghezza fissa o minima self.info_lat_label = ttk.Label(self.map_info_panel_frame, text="Lat (Click):") self.info_lat_label.grid(row=0, column=0, sticky=tk.W, pady=2, padx=(0,2)) self.info_lat_value = ttk.Label(self.map_info_panel_frame, text="N/A", width=22) self.info_lat_value.grid(row=0, column=1, sticky=tk.W, pady=2) self.info_lon_label = ttk.Label(self.map_info_panel_frame, text="Lon (Click):") self.info_lon_label.grid(row=1, column=0, sticky=tk.W, pady=2, padx=(0,2)) self.info_lon_value = ttk.Label(self.map_info_panel_frame, text="N/A", width=22) self.info_lon_value.grid(row=1, column=1, sticky=tk.W, pady=2) self.info_lat_dms_label = ttk.Label(self.map_info_panel_frame, text="Lat DMS:") self.info_lat_dms_label.grid(row=2, column=0, sticky=tk.W, pady=2, padx=(0,2)) self.info_lat_dms_value = ttk.Label(self.map_info_panel_frame, text="N/A", width=22) self.info_lat_dms_value.grid(row=2, column=1, sticky=tk.W, pady=2) self.info_lon_dms_label = ttk.Label(self.map_info_panel_frame, text="Lon DMS:") self.info_lon_dms_label.grid(row=3, column=0, sticky=tk.W, pady=2, padx=(0,2)) self.info_lon_dms_value = ttk.Label(self.map_info_panel_frame, text="N/A", width=22) self.info_lon_dms_value.grid(row=3, column=1, sticky=tk.W, pady=2) self.info_zoom_label = ttk.Label(self.map_info_panel_frame, text="Map Zoom:") self.info_zoom_label.grid(row=4, column=0, sticky=tk.W, pady=2, padx=(0,2)) self.info_zoom_value = ttk.Label(self.map_info_panel_frame, text="N/A", width=22) self.info_zoom_value.grid(row=4, column=1, sticky=tk.W, pady=2) self.info_map_size_label = ttk.Label(self.map_info_panel_frame, text="Map Area:") self.info_map_size_label.grid(row=5, column=0, sticky=tk.W, pady=2, padx=(0,2)) self.info_map_size_value = ttk.Label(self.map_info_panel_frame, text="N/A", width=22, wraplength=160) self.info_map_size_value.grid(row=5, column=1, sticky=tk.W, pady=2) self.map_info_panel_frame.columnconfigure(1, weight=1) # Permette all'etichetta valore di espandersi se necessario # --- 2. Pannello Centrale (per Views Notebook - Mappa) --- self.views_notebook_outer_frame = ttk.Frame(self.main_paned_window) # Un frame per contenere il views_notebook self.main_paned_window.add(self.views_notebook_outer_frame, weight=3) # Più peso verticale alla mappa self.views_notebook = ttk.Notebook(self.views_notebook_outer_frame) self.views_notebook.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) self.map_view_frame = ttk.Frame(self.views_notebook, padding=0) self.views_notebook.add(self.map_view_frame, text="Map View") self.flight_canvas = tk.Canvas( self.map_view_frame, bg="gray60", # Sfondo leggermente più scuro width=100, height=100, # Iniziali piccole, si espanderanno con il pack highlightthickness=0 ) self.flight_canvas.pack(fill=tk.BOTH, expand=True) self.map_manager_instance: Optional[MapCanvasManager] = None if MAP_CANVAS_MANAGER_AVAILABLE and MapCanvasManager is not None: default_map_bbox = { "lat_min": fm_config.DEFAULT_BBOX_LAT_MIN, "lon_min": fm_config.DEFAULT_BBOX_LON_MIN, "lat_max": fm_config.DEFAULT_BBOX_LAT_MAX, "lon_max": fm_config.DEFAULT_BBOX_LON_MAX } self.root.after(250, self._initialize_map_manager, default_map_bbox) # Aumentato delay else: module_logger.error("MapCanvasManager class not available. Map display will be a placeholder.") # Chiama _update_map_placeholder dopo che il canvas è stato packato, # altrimenti winfo_width/height daranno 1. self.root.after(50, lambda: self._update_map_placeholder("Map functionality disabled (Import Error).")) self.table_view_frame = ttk.Frame(self.views_notebook, padding=5) self.views_notebook.add(self.table_view_frame, text="Table View") ttk.Label(self.table_view_frame, text="Table View - Coming Soon", font=("Arial", 12)).pack(expand=True) self.views_notebook.bind("<>", self._on_view_tab_change) # --- 3. Pannello Inferiore (Log e Status Bar) --- self.paned_bottom_panel = ttk.Frame(self.main_paned_window) self.main_paned_window.add(self.paned_bottom_panel, weight=1) self.status_bar_frame = ttk.Frame(self.paned_bottom_panel, padding=(5, 3)) self.status_bar_frame.pack(side=tk.TOP, fill=tk.X, pady=(0,5)) self.semaphore_canvas = tk.Canvas( self.status_bar_frame, width=SEMAPHORE_TOTAL_SIZE, height=SEMAPHORE_TOTAL_SIZE, bg=self.root.cget('bg'), highlightthickness=0 ) self.semaphore_canvas.pack(side=tk.LEFT, padx=(0, 5)) x0 = SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH; y0 = SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH x1 = x0 + SEMAPHORE_SIZE; y1 = y0 + SEMAPHORE_SIZE self._semaphore_oval_id = self.semaphore_canvas.create_oval( x0, y0, x1, y1, fill=SEMAPHORE_COLOR_UNKNOWN, outline="gray30", width=SEMAPHORE_BORDER_WIDTH ) self.status_label = ttk.Label(self.status_bar_frame, text="Status: Initializing...") self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0,2)) self.log_frame = ttk.Frame(self.paned_bottom_panel, padding=(5,0,5,5)) self.log_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=0) log_font_family = "Consolas" if "Consolas" in tkFont.families() else "Courier New" self.log_text_widget = ScrolledText( self.log_frame, state=tk.DISABLED, height=8, wrap=tk.WORD, font=(log_font_family, 9), relief=tk.SUNKEN, borderwidth=1 ) self.log_text_widget.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) from ..utils.logger import setup_logging as setup_app_logging setup_app_logging(gui_log_widget=self.log_text_widget, root_tk_instance=self.root) self.root.protocol("WM_DELETE_WINDOW", self._on_closing) # Chiamate iniziali dopo che tutti i widget sono stati creati e impacchettati self.root.after(10, self._on_mode_change) # Piccolo ritardo per permettere al layout di stabilizzarsi self.update_semaphore_and_status("OK", "System Initialized. Ready.") module_logger.info("MainWindow fully initialized and displayed.") def _initialize_map_manager(self, initial_bbox_for_map: Dict[str, float]): """Initializes the MapCanvasManager after the canvas has valid dimensions.""" if not MAP_CANVAS_MANAGER_AVAILABLE or MapCanvasManager is None: module_logger.error("Attempted to initialize map manager, but MapCanvasManager class is not available.") self._update_map_placeholder("Map Error: MapCanvasManager class missing.") return # Assicurati che il canvas abbia dimensioni prima di inizializzare # Questo viene chiamato da root.after, quindi le dimensioni dovrebbero essere disponibili. canvas_w = self.flight_canvas.winfo_width() canvas_h = self.flight_canvas.winfo_height() if canvas_w > 1 and canvas_h > 1: module_logger.info(f"Canvas is ready ({canvas_w}x{canvas_h}), initializing MapCanvasManager.") try: self.map_manager_instance = MapCanvasManager( app_controller=self.controller, tk_canvas=self.flight_canvas, initial_bbox_dict=initial_bbox_for_map # Nome corretto dell'argomento ) # Dopo l'inizializzazione, MapCanvasManager dovrebbe aver disegnato la mappa iniziale. # Il placeholder di MainWindow non dovrebbe più essere necessario se la mappa è attiva. # MapCanvasManager stesso pulisce il tag "placeholder_text". except ImportError as e_imp: # Specifica per ImportError se MapCanvasManager ha dipendenze mancate al suo interno module_logger.critical(f"Failed to initialize MapCanvasManager due to missing libraries during its init: {e_imp}", exc_info=True) self.show_error_message("Map Error", f"Map libraries missing for manager: {e_imp}. Map functionality disabled.") self._update_map_placeholder(f"Map Error: Libraries missing.\n{e_imp}") except Exception as e_init: # Altre eccezioni durante l'init di MapCanvasManager module_logger.critical(f"Failed to initialize MapCanvasManager: {e_init}", exc_info=True) self.show_error_message("Map Error", f"Could not initialize map: {e_init}") self._update_map_placeholder(f"Map Error: Initialization failed.\n{e_init}") else: module_logger.warning(f"Canvas not ready yet for MapCanvasManager initialization (dims: {canvas_w}x{canvas_h}), retrying...") self.root.after(200, self._initialize_map_manager, initial_bbox_for_map) def update_semaphore_and_status(self, status_level: str, message: str): color_to_set = SEMAPHORE_COLOR_UNKNOWN if status_level == "OK": color_to_set = SEMAPHORE_COLOR_OK elif status_level == "WARNING": color_to_set = SEMAPHORE_COLOR_WARNING elif status_level == "ERROR": color_to_set = SEMAPHORE_COLOR_ERROR elif status_level == "FETCHING": color_to_set = SEMAPHORE_COLOR_FETCHING if hasattr(self, 'semaphore_canvas') and self.semaphore_canvas.winfo_exists(): try: self.semaphore_canvas.itemconfig(self._semaphore_oval_id, fill=color_to_set) except tk.TclError: pass # Ignora se il canvas è distrutto current_status_text = f"Status: {message}" if hasattr(self, 'status_label') and self.status_label.winfo_exists(): try: self.status_label.config(text=current_status_text) except tk.TclError: pass # Non loggare ogni aggiornamento di status per evitare verbosità, a meno che non sia un errore. if status_level in ["ERROR", "CRITICAL"] or "error" in message.lower(): module_logger.error(f"GUI Status Update: Level='{status_level}', Message='{message}'") else: module_logger.debug(f"GUI Status Update: Level='{status_level}', Message='{message}'") def _on_closing(self): module_logger.info("Main window closing event triggered.") if messagebox.askokcancel("Quit", "Do you want to quit Flight Monitor?", parent=self.root): module_logger.info("User confirmed quit.") if self.controller and hasattr(self.controller, 'on_application_exit'): self.controller.on_application_exit() app_destroyed_msg = "Application window will be destroyed." module_logger.info(app_destroyed_msg) if hasattr(self, 'root') and self.root.winfo_exists(): shutdown_gui_logging() if hasattr(self, 'root') and self.root.winfo_exists(): self.root.destroy() print(f"INFO ({__name__}): Application window has been destroyed (post-destroy print).") else: module_logger.info("User cancelled quit.") def _reset_gui_to_stopped_state(self, status_message: Optional[str] = "Monitoring stopped."): if hasattr(self, 'start_button'): self.start_button.config(state=tk.NORMAL) if hasattr(self, 'stop_button'): self.stop_button.config(state=tk.DISABLED) if hasattr(self, 'live_radio'): self.live_radio.config(state=tk.NORMAL) if hasattr(self, 'history_radio'): self.history_radio.config(state=tk.NORMAL) current_mode_is_live = hasattr(self, 'mode_var') and self.mode_var.get() == "Live" self._set_bbox_entries_state(tk.NORMAL if current_mode_is_live else tk.DISABLED) status_level_for_semaphore = "OK" if status_message and ("failed" in status_message.lower() or "error" in status_message.lower()): status_level_for_semaphore = "ERROR" elif status_message and "warning" in status_message.lower(): status_level_for_semaphore = "WARNING" self.update_semaphore_and_status(status_level_for_semaphore, status_message) module_logger.debug(f"GUI controls reset to stopped state. Status: '{status_message}'") def _should_show_main_placeholder(self) -> bool: """Helper to decide if MainWindow should show its placeholder.""" if hasattr(self, 'map_manager_instance') and self.map_manager_instance: # Se il map_manager è inizializzato, si presume che gestisca il contenuto del canvas. # Potrebbe mostrare la sua mappa o il suo placeholder. # MainWindow non dovrebbe sovrascrivere. map_info = self.map_manager_instance.get_current_map_info() if map_info.get("geo_bounds") is not None: # Un segno che la mappa è attiva return False return True def _update_map_placeholder(self, text_to_display: str): if hasattr(self, 'flight_canvas') and self.flight_canvas.winfo_exists(): try: self.flight_canvas.delete("placeholder_text") if self._should_show_main_placeholder(): module_logger.debug(f"MainWindow updating placeholder with: '{text_to_display}'") canvas_w = self.flight_canvas.winfo_width() canvas_h = self.flight_canvas.winfo_height() # --- MODIFICA QUI --- if canvas_w <= 1: canvas_w = getattr(fm_config, 'DEFAULT_CANVAS_WIDTH', 800) # Usa fm_config if canvas_h <= 1: canvas_h = getattr(fm_config, 'DEFAULT_CANVAS_HEIGHT', 600) # Usa fm_config # --- FINE MODIFICA --- self.flight_canvas.create_text( canvas_w / 2, canvas_h / 2, text=text_to_display, tags="placeholder_text", fill="gray50", width=canvas_w - 40 ) else: logger.debug("MainWindow skipping placeholder update as map_manager is active.") except tk.TclError: module_logger.warning("TclError updating map placeholder in MainWindow.") except Exception as e_placeholder: # Cattura altre eccezioni module_logger.error(f"Unexpected error in _update_map_placeholder: {e_placeholder}", exc_info=True) # ... (resto della classe MainWindow) ... def _on_mode_change(self): selected_mode = self.mode_var.get() status_message = f"Mode: {selected_mode}. Ready." # Non aggiornare lo status qui se stiamo per aggiornare il placeholder, # che potrebbe sovrascrivere questo messaggio. # self.update_semaphore_and_status("OK", status_message) if hasattr(self, 'function_notebook') and self.function_notebook.winfo_exists(): try: # ... (logica per abilitare/disabilitare tab come prima) ... live_bbox_tab_index = self.function_notebook.index(self.live_bbox_tab_frame) history_tab_index = self.function_notebook.index(self.history_tab_frame) if selected_mode == "Live": self.function_notebook.tab(live_bbox_tab_index, state='normal') self.function_notebook.tab(history_tab_index, state='disabled') current_func_tab_index = self.function_notebook.index("current") if current_func_tab_index == history_tab_index: self.function_notebook.select(live_bbox_tab_index) elif selected_mode == "History": self.function_notebook.tab(live_bbox_tab_index, state='disabled') self.function_notebook.tab(history_tab_index, state='normal') current_func_tab_index = self.function_notebook.index("current") if current_func_tab_index != history_tab_index: self.function_notebook.select(history_tab_index) except Exception as e_tab_change: # Più generico per TclError o ValueError module_logger.warning(f"Error updating function notebook tabs state: {e_tab_change}") self.clear_all_views_data() # Questo chiamerà map_manager.update_flights_on_map([]) placeholder_text_to_set = "Map Area." if selected_mode == "Live": placeholder_text_to_set = "Map Area - Ready for live data. Define area and press Start." self._set_bbox_entries_state(tk.NORMAL) elif selected_mode == "History": placeholder_text_to_set = "Map Area - Ready for historical data. (Functionality TBD)" self._set_bbox_entries_state(tk.DISABLED) # Aggiorna il placeholder solo se il map_manager non è attivo. # Se map_manager è attivo, avrà già disegnato la mappa o il suo placeholder. self._update_map_placeholder(placeholder_text_to_set) # Aggiorna lo status *dopo* aver potenzialmente mostrato il placeholder self.update_semaphore_and_status("OK", status_message) def _on_function_tab_change(self, event: Any = None): # Aggiunto event=None per chiamate dirette if not (hasattr(self, 'function_notebook') and self.function_notebook.winfo_exists()): return try: selected_tab_index = self.function_notebook.index("current") tab_text = self.function_notebook.tab(selected_tab_index, "text") module_logger.info(f"GUI: Switched function tab to: {tab_text}") placeholder_text = f"Selected tab: {tab_text} - Ready." if "Live: Area Monitor" in tab_text: placeholder_text = "Map Area - Ready for live data. Define area and press Start." elif "Live: Airport" in tab_text: placeholder_text = "Map Area - Live Airport Monitor. (Functionality TBD)" elif "History" in tab_text: placeholder_text = "Map Area - History Analysis. (Functionality TBD)" self._update_map_placeholder(placeholder_text) # Aggiorna placeholder se necessario except tk.TclError: # Può succedere se il notebook è in uno stato strano module_logger.warning("TclError on function tab change, notebook state might be inconsistent.") except ValueError: # Se "current" non è un tab valido module_logger.warning("ValueError on function tab change, 'current' tab not found.") def _on_view_tab_change(self, event: Any = None): # Aggiunto event=None if not (hasattr(self, 'views_notebook') and self.views_notebook.winfo_exists()): return try: selected_tab_index = self.views_notebook.index("current") tab_text = self.views_notebook.tab(selected_tab_index, "text") module_logger.info(f"GUI: Switched view tab to: {tab_text}") except Exception as e_view_tab: module_logger.warning(f"Error on view tab change: {e_view_tab}") def _set_bbox_entries_state(self, state: str): if hasattr(self, 'lat_min_entry'): self.lat_min_entry.config(state=state) if hasattr(self, 'lon_min_entry'): self.lon_min_entry.config(state=state) if hasattr(self, 'lat_max_entry'): self.lat_max_entry.config(state=state) if hasattr(self, 'lon_max_entry'): self.lon_max_entry.config(state=state) module_logger.debug(f"Bounding box entries state set to: {state}") def _start_monitoring(self): selected_mode = self.mode_var.get() module_logger.info(f"GUI: User requested to start {selected_mode} monitoring.") active_func_tab_text = "Unknown Tab" if hasattr(self, 'function_notebook') and self.function_notebook.winfo_exists(): try: active_func_tab_index = self.function_notebook.index("current") active_func_tab_text = self.function_notebook.tab(active_func_tab_index, "text") module_logger.debug(f"GUI: Active function tab is: {active_func_tab_text}") except Exception: pass self.start_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) self.live_radio.config(state=tk.DISABLED) self.history_radio.config(state=tk.DISABLED) if selected_mode == "Live": if "Live: Area Monitor" in active_func_tab_text: bounding_box = self.get_bounding_box() if bounding_box: module_logger.debug(f"Valid bounding box for live monitoring: {bounding_box}") if self.controller: # Il controller chiamerà map_manager.set_target_bbox() self.controller.start_live_monitoring(bounding_box) else: module_logger.critical("Controller not available.") self._reset_gui_to_stopped_state("Critical Error: Controller unavailable.") self.show_error_message("Internal Error", "Application controller is missing.") else: self._reset_gui_to_stopped_state("Failed to start: Invalid bounding box details.") else: module_logger.warning(f"GUI: Start pressed in Live mode, but active tab '{active_func_tab_text}' is not the Live Area Monitor tab.") self.update_semaphore_and_status("WARNING", f"Start not supported on '{active_func_tab_text}' tab in Live mode.") self._reset_gui_to_stopped_state(f"Start not supported on {active_func_tab_text} tab.") elif selected_mode == "History": if "History" in active_func_tab_text: if self.controller: self.controller.start_history_monitoring() module_logger.info("GUI: History monitoring started (placeholder).") else: module_logger.warning(f"GUI: Start pressed in History mode, but active tab '{active_func_tab_text}' is not the History tab.") self.update_semaphore_and_status("WARNING", f"Start not supported on '{active_func_tab_text}' tab in History mode.") self._reset_gui_to_stopped_state(f"Start not supported on {active_func_tab_text} tab.") self._set_bbox_entries_state(tk.DISABLED) def _stop_monitoring(self): module_logger.info("GUI: User requested to stop monitoring.") selected_mode = self.mode_var.get() if self.controller: if selected_mode == "Live": self.controller.stop_live_monitoring() elif selected_mode == "History": self.controller.stop_history_monitoring() # _reset_gui_to_stopped_state viene ora chiamato più robustamente da AppController # alla fine della sequenza di stop, o se il MapCanvasManager aggiorna lo stato. # Tuttavia, per un feedback immediato all'utente, potremmo resettare qui # alcuni controlli e lasciare che lo stato finale sia gestito dal controller/messaggi. # Per ora, lasciamo che il controller gestisca lo stato finale. # Se il MapCanvasManager ha un metodo "show_stopped_state", potremmo chiamarlo. self.update_semaphore_and_status("OK", f"{selected_mode} monitoring stopping...") # Feedback intermedio # Il reset completo dei controlli avviene in _reset_gui_to_stopped_state, # che dovrebbe essere chiamato dal controller o da un messaggio di stato finale. def get_bounding_box(self) -> Optional[Dict[str, float]]: module_logger.debug("Attempting to retrieve and validate bounding box.") try: lat_min = float(self.lat_min_var.get()) lon_min = float(self.lon_min_var.get()) lat_max = float(self.lat_max_var.get()) lon_max = float(self.lon_max_var.get()) except ValueError: msg = "Bounding box coordinates must be valid numbers." module_logger.error(f"Input Error (ValueError): {msg}", exc_info=False) self.show_error_message("Input Error", msg) return None module_logger.debug(f"Raw BBox input: lat_min={lat_min}, lon_min={lon_min}, lat_max={lat_max}, lon_max={lon_max}") if not (-90 <= lat_min <= 90 and -90 <= lat_max <= 90 and -180 <= lon_min <= 180 and -180 <= lon_max <= 180): msg = "Invalid geographic range. Lat: [-90, 90], Lon: [-180, 180]." module_logger.error(f"Validation Error: {msg}") self.show_error_message("Input Error", msg) return None if lat_min >= lat_max: msg = "Latitude Min must be strictly less than Latitude Max." module_logger.error(f"Validation Error: {msg}") self.show_error_message("Input Error", msg) return None if lon_min >= lon_max: # Solitamente non un errore per BBox globali, ma per aree locali sì # Se si supportano BBox che attraversano l'antimeridiano, questa logica cambia. # Per OpenSky, che non supporta BBox sull'antimeridiano, questo è un errore. msg = "Longitude Min must be strictly less than Longitude Max for this service." module_logger.error(f"Validation Error: {msg}") self.show_error_message("Input Error", msg) return None return {"lat_min": lat_min, "lon_min": lon_min, "lat_max": lat_max, "lon_max": lon_max} def display_flights_on_canvas(self, flight_states: List[CanonicalFlightState], _active_bounding_box_context: Dict[str, float]): # Il BBox è per contesto, la mappa usa il suo """ Passa i dati dei voli al MapCanvasManager per la visualizzazione. Il MapCanvasManager è responsabile di disegnare sulla sua vista corrente. """ if not MAP_CANVAS_MANAGER_AVAILABLE or not hasattr(self, 'map_manager_instance') or self.map_manager_instance is None: module_logger.warning("MapCanvasManager not initialized, cannot display flights on map.") # Non mostrare un placeholder qui, perché questo metodo viene chiamato ripetutamente. # Lo stato di "map not ready" dovrebbe essere gestito all'init o se l'init fallisce. return # Passa i voli al map_manager_instance. # Il MapCanvasManager userà questi voli per aggiornare i suoi overlay. # Il bounding_box_context non è direttamente usato per cambiare la vista qui, # ma potrebbe essere usato da MapCanvasManager se volesse disegnare il BBox della richiesta API. try: self.map_manager_instance.update_flights_on_map(flight_states) except Exception as e_map_update: module_logger.error(f"Error updating flights on map via MapCanvasManager: {e_map_update}", exc_info=True) self.show_error_message("Map Display Error", "Could not update flights on map.") def clear_all_views_data(self): """Clears all displayed flight data from all view tabs.""" if hasattr(self, 'map_manager_instance') and self.map_manager_instance: try: self.map_manager_instance.update_flights_on_map([]) # Passa lista vuota per pulire i voli # Il MapCanvasManager dovrebbe mostrare la mappa base o il suo placeholder. except Exception as e_map_clear: module_logger.warning(f"Error clearing flights from map manager: {e_map_clear}") else: # Se il map_manager non c'è, MainWindow gestisce il placeholder self._update_map_placeholder("Map system not available.") module_logger.debug("Cleared data from views (map delegated to map manager).") def show_error_message(self, title: str, message: str): """Displays an error message box and updates the status bar to reflect the error.""" module_logger.error(f"Displaying error message to user: Title='{title}', Message='{message}'") status_bar_msg = f"Error: {message[:70]}" if len(message) > 70: status_bar_msg += "..." self.update_semaphore_and_status("ERROR", status_bar_msg) if hasattr(self, 'root') and self.root.winfo_exists(): messagebox.showerror(title, message, parent=self.root) # --- NUOVO: Placeholder per il menu contestuale della mappa --- def show_map_context_menu(self, latitude: float, longitude: float, screen_x: int, screen_y: int): """ Placeholder: Mostra un menu contestuale per il punto mappa cliccato. """ module_logger.info(f"Placeholder: Show context menu for map click at Lat {latitude:.4f}, Lon {longitude:.4f}") # Esempio di creazione menu: # context_menu = tk.Menu(self.root, tearoff=0) # context_menu.add_command(label=f"Info for {latitude:.3f},{longitude:.3f}", state=tk.DISABLED) # context_menu.add_separator() # context_menu.add_command(label="Center map here", command=lambda: self._center_map_at_coords(latitude, longitude)) # context_menu.add_command(label="Get elevation here (TBD)") # try: # context_menu.tk_popup(screen_x, screen_y) # finally: # context_menu.grab_release() self.update_semaphore_and_status("OK", f"Context click: Lat {latitude:.3f}, Lon {longitude:.3f}") def _center_map_at_coords(self, lat: float, lon: float): """ Placeholder: Istruisce il map manager a ricentrare la mappa.""" module_logger.info(f"Placeholder: Request to center map at {lat:.4f}, {lon:.4f}") if hasattr(self, 'map_manager_instance') and self.map_manager_instance: # MapCanvasManager avrà bisogno di un metodo per ricentrare mantenendo lo zoom attuale # self.map_manager_instance.recenter_and_redraw(lat, lon, self.map_manager_instance._current_zoom) pass def update_map_info_panel(self, lat_deg: Optional[float], lon_deg: Optional[float], lat_dms: str, lon_dms: str, zoom: Optional[int], map_size_str: str): """Aggiorna le etichette nel pannello informazioni mappa.""" if hasattr(self, 'info_lat_value') and self.info_lat_value.winfo_exists(): self.info_lat_value.config(text=f"{lat_deg:.5f}" if lat_deg is not None else "N/A") if hasattr(self, 'info_lon_value') and self.info_lon_value.winfo_exists(): self.info_lon_value.config(text=f"{lon_deg:.5f}" if lon_deg is not None else "N/A") if hasattr(self, 'info_lat_dms_value') and self.info_lat_dms_value.winfo_exists(): self.info_lat_dms_value.config(text=lat_dms) if hasattr(self, 'info_lon_dms_value') and self.info_lon_dms_value.winfo_exists(): self.info_lon_dms_value.config(text=lon_dms) if hasattr(self, 'info_zoom_value') and self.info_zoom_value.winfo_exists(): self.info_zoom_value.config(text=str(zoom) if zoom is not None else "N/A") if hasattr(self, 'info_map_size_value') and self.info_map_size_value.winfo_exists(): self.info_map_size_value.config(text=map_size_str) module_logger.debug(f"Map info panel updated: Lat={lat_deg}, Lon={lon_deg}, Zoom={zoom}, Size='{map_size_str}'")