# 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 (come prima) controls_estimated_height = 50 bbox_estimated_height = 70 log_area_min_height = 80 map_min_height = 200 # Altezza minima per il canvas della mappa status_bar_actual_height = 30 min_function_notebook_height = controls_estimated_height + bbox_estimated_height + 20 # Usa map_min_height per il calcolo delle viste min_views_notebook_height = map_min_height + 20 min_paned_top_panel_height = max(min_function_notebook_height, min_views_notebook_height) # Il pannello top deve contenere il più alto dei due min_paned_bottom_panel_height = log_area_min_height + status_bar_actual_height + 10 min_total_height = min_paned_top_panel_height + min_paned_bottom_panel_height + 20 # Buffer per separatore e padding # Usa DEFAULT_CANVAS_WIDTH da fm_config per la larghezza minima min_total_width = getattr(fm_config, 'DEFAULT_CANVAS_WIDTH', 800) // 2 + 150 # Adattato if min_total_width < 600: min_total_width = 600 # Un po' più largo 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}") initial_width = getattr(fm_config, 'DEFAULT_CANVAS_WIDTH', 800) + 250 initial_function_notebook_height = controls_estimated_height + bbox_estimated_height + 40 # Più spazio per padding/tabs initial_views_notebook_height = getattr(fm_config, 'DEFAULT_CANVAS_HEIGHT', 600) + 40 # Più spazio initial_log_height = 120 initial_total_height = max(initial_function_notebook_height, initial_views_notebook_height) + \ initial_log_height + status_bar_actual_height + 30 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 --- self.main_paned_window = ttk.PanedWindow(self.root, orient=tk.VERTICAL) self.main_paned_window.pack(fill=tk.BOTH, expand=True, padx=10, pady=(5,0)) self.paned_top_panel = ttk.Frame(self.main_paned_window) self.main_paned_window.add(self.paned_top_panel, weight=3) # --- Function Notebook --- self.function_notebook = ttk.Notebook(self.paned_top_panel) self.function_notebook.pack(side=tk.TOP, fill=tk.X, expand=False, padx=0, pady=(0,5)) 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") 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) # --- Views Notebook --- self.views_notebook = ttk.Notebook(self.paned_top_panel) self.views_notebook.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) self.map_view_frame = ttk.Frame(self.views_notebook, padding=5) self.views_notebook.add(self.map_view_frame, text="Map View") self.flight_canvas = tk.Canvas( self.map_view_frame, bg="gray75", # Sfondo leggermente più scuro per distinguerlo dal placeholder width=getattr(fm_config, 'DEFAULT_CANVAS_WIDTH', 800), height=getattr(fm_config, 'DEFAULT_CANVAS_HEIGHT', 600), 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(150, self._initialize_map_manager, default_map_bbox) # Aumentato leggermente il delay else: module_logger.error("MapCanvasManager could not be imported. Map display will be a placeholder.") 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) # --- Bottom Panel of Main PanedWindow --- 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 # Usa colore di sfondo root ) 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) self._on_mode_change() # Imposta stato iniziale tab e placeholder 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