# 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 import filedialog from tkinter import Menu from tkinter.scrolledtext import ScrolledText from tkinter import font as tkFont from typing import List, Dict, Optional, Tuple, Any import logging import os # Aggiunto per os.path.basename from datetime import datetime, timezone from ..data import config as app_config from ..utils.logger import get_logger, setup_logging, shutdown_logging_system from ..data.common_models import CanonicalFlightState from ..utils.gui_utils import ( GUI_STATUS_OK, GUI_STATUS_WARNING, GUI_STATUS_ERROR, GUI_STATUS_FETCHING, GUI_STATUS_UNKNOWN, GUI_STATUS_PERMANENT_FAILURE, SEMAPHORE_COLOR_STATUS_MAP, ) try: from ..map.map_canvas_manager import MapCanvasManager from ..map.map_utils import _is_valid_bbox_dict MAP_CANVAS_MANAGER_AVAILABLE = True except ImportError as e_map_import: MapCanvasManager = None # type: ignore _is_valid_bbox_dict = lambda x: False # type: ignore MAP_CANVAS_MANAGER_AVAILABLE = False print( f"CRITICAL ERROR in MainWindow import: Failed to import MapCanvasManager or map_utils: {e_map_import}. Map functionality will be disabled." ) module_logger = get_logger(__name__) SEMAPHORE_SIZE = 12 SEMAPHORE_PAD = 3 SEMAPHORE_BORDER_WIDTH = 1 SEMAPHORE_TOTAL_SIZE = SEMAPHORE_SIZE + 2 * (SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH) FALLBACK_CANVAS_WIDTH = getattr(app_config, "DEFAULT_CANVAS_WIDTH", 800) FALLBACK_CANVAS_HEIGHT = getattr(app_config, "DEFAULT_CANVAS_HEIGHT", 600) BBOX_COLOR_INSIDE = "green4" BBOX_COLOR_OUTSIDE = "red2" BBOX_COLOR_PARTIAL = "darkorange" BBOX_COLOR_NA = "gray50" DEFAULT_TRACK_LENGTH = getattr(app_config, "DEFAULT_TRACK_HISTORY_POINTS", 20) # --- NUOVA CLASSE PER LA FINESTRA DI DIALOGO DI IMPORTAZIONE --- class ImportProgressDialog(tk.Toplevel): def __init__(self, parent, title="Importing Database..."): super().__init__(parent) self.title(title) self.parent = parent self.transient(parent) self.grab_set() self.protocol("WM_DELETE_WINDOW", self._on_close_attempt) self.geometry("450x220") # Leggermente più alta per il bottone close self.resizable(False, False) main_frame = ttk.Frame(self, padding="10 10 10 10") main_frame.pack(fill=tk.BOTH, expand=True) ttk.Label( main_frame, text="Please wait while the aircraft database is being imported.", ).pack(pady=(0, 10)) self.rows_processed_var = tk.StringVar(value="Rows Processed: 0") ttk.Label(main_frame, textvariable=self.rows_processed_var).pack(anchor=tk.W) self.rows_imported_var = tk.StringVar(value="Rows Imported/Updated: 0") ttk.Label(main_frame, textvariable=self.rows_imported_var).pack( anchor=tk.W, pady=(0, 10) ) self.progress_var = tk.DoubleVar() self.progressbar = ttk.Progressbar( main_frame, orient="horizontal", length=400, mode="determinate", variable=self.progress_var, ) self.progressbar.pack(pady=(0, 5), fill=tk.X, expand=True) self.progress_label_var = tk.StringVar(value="0/0 (0%)") ttk.Label(main_frame, textvariable=self.progress_label_var).pack(pady=(0, 10)) self.status_message_var = tk.StringVar(value="Initializing import...") ttk.Label( main_frame, textvariable=self.status_message_var, wraplength=400 ).pack(pady=(5, 0)) self.close_button = ttk.Button( main_frame, text="Close", command=self.destroy, state=tk.DISABLED ) self.close_button.pack(pady=(10, 0)) self.import_running = False self.center_window() def center_window(self): self.update_idletasks() width = self.winfo_width() height = self.winfo_height() x = (self.winfo_screenwidth() // 2) - (width // 2) y = (self.winfo_screenheight() // 2) - (height // 2) if ( width > 0 and height > 0 ): # Evita errore se la finestra non ha ancora dimensioni self.geometry(f"{width}x{height}+{x}+{y}") def update_progress( self, processed: int, imported: int, total_rows: Optional[int] = None, message: Optional[str] = None, ): if not self.winfo_exists(): return self.rows_processed_var.set(f"Rows Processed: {processed}") self.rows_imported_var.set(f"Rows Imported/Updated: {imported}") if message: self.status_message_var.set(message) if total_rows and total_rows > 0: self.progressbar["mode"] = "determinate" percentage = (processed / total_rows) * 100 if total_rows > 0 else 0 self.progressbar["maximum"] = total_rows self.progress_var.set(processed) self.progress_label_var.set(f"{processed}/{total_rows} ({percentage:.1f}%)") elif total_rows == 0: # File vuoto o solo header self.progressbar["mode"] = "determinate" self.progressbar["maximum"] = 1 # Evita divisione per zero self.progress_var.set(1) # Mostra come completato self.progress_label_var.set(f"0/0 (100%) - File empty or header only.") else: # total_rows is None (conteggio iniziale fallito o non ancora fatto) self.progressbar["mode"] = "indeterminate" self.progressbar.start(10) # Avvia animazione indeterminata self.progress_label_var.set(f"Processed: {processed} (Total rows unknown)") def import_started(self): if not self.winfo_exists(): return self.import_running = True self.status_message_var.set("Import in progress...") self.close_button.config(state=tk.DISABLED) self.progressbar["mode"] = ( "indeterminate" # Inizia in indeterminato se total_rows non è subito noto ) self.progressbar.start(10) def import_finished(self, success: bool, final_message: str): if not self.winfo_exists(): return self.import_running = False self.status_message_var.set(final_message) self.progressbar.stop() # Ferma l'animazione indeterminata se era attiva self.progressbar["mode"] = "determinate" if success and self.progressbar["maximum"] > 0: # Solo se c'era un massimo > 0 self.progress_var.set(self.progressbar["maximum"]) elif success and self.progressbar["maximum"] == 0: # File vuoto gestito self.progress_var.set(0) # else: la barra rimane allo stato corrente se c'è stato un errore self.close_button.config(state=tk.NORMAL) def _on_close_attempt(self): if self.import_running: messagebox.showwarning( "Import in Progress", "Import is currently running. Please wait until it finishes or implement a cancel function.", parent=self, ) else: self.destroy() # --- FINE NUOVA CLASSE --- class MainWindow: def __init__(self, root: tk.Tk, controller: Any): self.root = root self.controller = controller self.root.title("Flight Monitor") self.progress_dialog: Optional[ImportProgressDialog] = ( None # Riferimento alla dialog ) if app_config.LAYOUT_START_MAXIMIZED: try: self.root.state("zoomed") module_logger.info( "Attempted to start window maximized using 'zoomed' state." ) except tk.TclError: try: screen_width = self.root.winfo_screenwidth() screen_height = self.root.winfo_screenheight() self.root.geometry(f"{screen_width}x{screen_height}+0+0") module_logger.info( f"Started window maximized using screen dimensions: {screen_width}x{screen_height}." ) except tk.TclError as e_geom: module_logger.warning( f"Could not get screen dimensions or set geometry for maximization: {e_geom}." ) min_win_w = getattr(app_config, "LAYOUT_WINDOW_MIN_WIDTH", 900) min_win_h = getattr(app_config, "LAYOUT_WINDOW_MIN_HEIGHT", 650) try: self.root.minsize(min_win_w, min_win_h) module_logger.info(f"Minimum window size set to {min_win_w}x{min_win_h}.") except tk.TclError as e_minsize: module_logger.warning(f"Could not set minsize from config: {e_minsize}.") self.canvas_width = getattr( app_config, "DEFAULT_CANVAS_WIDTH", FALLBACK_CANVAS_WIDTH ) self.canvas_height = getattr( app_config, "DEFAULT_CANVAS_HEIGHT", FALLBACK_CANVAS_HEIGHT ) self.menubar = Menu(self.root) self.file_menu = Menu(self.menubar, tearoff=0) self.file_menu.add_command( label="Import Aircraft Database (CSV)...", command=self._import_aircraft_db_csv, ) self.file_menu.add_separator() self.file_menu.add_command(label="Exit", command=self._on_closing) self.menubar.add_cascade(label="File", menu=self.file_menu) self.root.config(menu=self.menubar) self.main_horizontal_paned_window = ttk.PanedWindow( self.root, orient=tk.HORIZONTAL ) self.main_horizontal_paned_window.pack( fill=tk.BOTH, expand=True, padx=5, pady=5 ) self.left_column_frame = ttk.Frame(self.main_horizontal_paned_window) self.main_horizontal_paned_window.add( self.left_column_frame, weight=app_config.LAYOUT_MAIN_HORIZONTAL_WEIGHTS.get("left_column", 25), ) self.left_vertical_paned_window = ttk.PanedWindow( self.left_column_frame, orient=tk.VERTICAL ) self.left_vertical_paned_window.pack(fill=tk.BOTH, expand=True) self.function_notebook_frame = ttk.Frame(self.left_vertical_paned_window) self.left_vertical_paned_window.add( self.function_notebook_frame, weight=app_config.LAYOUT_LEFT_VERTICAL_WEIGHTS.get("function_notebook", 65), ) self.function_notebook = ttk.Notebook(self.function_notebook_frame) self.function_notebook.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) 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.live_controls_options_frame = ttk.Frame(self.live_bbox_tab_frame) self.live_controls_options_frame.pack(side=tk.TOP, fill=tk.X, pady=(0, 5)) self.controls_frame_live_area = ttk.Frame(self.live_controls_options_frame) self.controls_frame_live_area.pack(side=tk.TOP, fill=tk.X) self.control_frame = ttk.LabelFrame( self.controls_frame_live_area, 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_live_area, 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(app_config.DEFAULT_BBOX_LAT_MIN)) self.lat_min_entry = ttk.Entry(self.bbox_frame, textvariable=self.lat_min_var) 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(app_config.DEFAULT_BBOX_LON_MIN)) self.lon_min_entry = ttk.Entry(self.bbox_frame, textvariable=self.lon_min_var) 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(app_config.DEFAULT_BBOX_LAT_MAX)) self.lat_max_entry = ttk.Entry(self.bbox_frame, textvariable=self.lat_max_var) 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(app_config.DEFAULT_BBOX_LON_MAX)) self.lon_max_entry = ttk.Entry(self.bbox_frame, textvariable=self.lon_max_var) self.lon_max_entry.grid(row=1, column=3, padx=(0, 0), pady=2, sticky=tk.EW) self.track_options_frame = ttk.LabelFrame( self.live_controls_options_frame, text="Track Options", padding=(10, 5) ) self.track_options_frame.pack(side=tk.TOP, fill=tk.X, pady=(10, 0)) ttk.Label(self.track_options_frame, text="Track Length (points):").pack( side=tk.LEFT, padx=(0, 5) ) self.track_length_var = tk.IntVar(value=DEFAULT_TRACK_LENGTH) self.track_length_spinbox = ttk.Spinbox( self.track_options_frame, from_=2, to=100, textvariable=self.track_length_var, width=5, command=self._on_track_length_change, state="readonly", ) self.track_length_spinbox.pack(side=tk.LEFT, padx=5) 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 ) self.log_status_area_frame = ttk.Frame(self.left_vertical_paned_window) self.left_vertical_paned_window.add( self.log_status_area_frame, weight=app_config.LAYOUT_LEFT_VERTICAL_WEIGHTS.get("log_status_area", 35), ) self.status_bar_frame = ttk.Frame(self.log_status_area_frame, 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, y0 = ( SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH, SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH, ) x1, y1 = x0 + SEMAPHORE_SIZE, y0 + SEMAPHORE_SIZE self._semaphore_oval_id = self.semaphore_canvas.create_oval( x0, y0, x1, y1, fill=SEMAPHORE_COLOR_STATUS_MAP.get(GUI_STATUS_UNKNOWN, "gray70"), 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.log_status_area_frame, 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=10, 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) self.right_column_frame = ttk.Frame(self.main_horizontal_paned_window) self.main_horizontal_paned_window.add( self.right_column_frame, weight=app_config.LAYOUT_MAIN_HORIZONTAL_WEIGHTS.get("right_column", 75), ) self.right_vertical_paned_window = ttk.PanedWindow( self.right_column_frame, orient=tk.VERTICAL ) self.right_vertical_paned_window.pack(fill=tk.BOTH, expand=True) self.views_notebook_outer_frame = ttk.Frame(self.right_vertical_paned_window) self.right_vertical_paned_window.add( self.views_notebook_outer_frame, weight=app_config.LAYOUT_RIGHT_VERTICAL_WEIGHTS.get("views_notebook", 80), ) self.views_notebook = ttk.Notebook(self.views_notebook_outer_frame) self.views_notebook.pack(fill=tk.BOTH, expand=True, padx=0, pady=(2, 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", width=self.canvas_width, height=self.canvas_height, highlightthickness=0, ) self.flight_canvas.pack(fill=tk.BOTH, expand=True) self.map_manager_instance: Optional[MapCanvasManager] = None 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) self.map_tools_info_area_frame = ttk.Frame( self.right_vertical_paned_window, padding=(0, 5, 0, 0) ) self.right_vertical_paned_window.add( self.map_tools_info_area_frame, weight=app_config.LAYOUT_RIGHT_VERTICAL_WEIGHTS.get("map_tools_info", 20), ) self.bottom_panel_container = ttk.Frame(self.map_tools_info_area_frame) self.bottom_panel_container.pack(fill=tk.BOTH, expand=True) bottom_panel_weights = getattr( app_config, "LAYOUT_BOTTOM_PANELS_HORIZONTAL_WEIGHTS", {"map_tools": 1, "map_info": 2, "flight_details": 2}, ) self.bottom_panel_container.columnconfigure( 0, weight=bottom_panel_weights.get("map_tools", 1) ) self.bottom_panel_container.columnconfigure( 1, weight=bottom_panel_weights.get("map_info", 2) ) self.bottom_panel_container.columnconfigure( 2, weight=bottom_panel_weights.get("flight_details", 2) ) self.map_tool_frame = ttk.LabelFrame( self.bottom_panel_container, text="Map Tools", padding=5 ) self.map_tool_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 2)) self._recreate_map_tools_content(self.map_tool_frame) self.map_info_panel_frame = ttk.LabelFrame( self.bottom_panel_container, text="Map Information", padding=10 ) self.map_info_panel_frame.grid(row=0, column=1, sticky="nsew", padx=2) self._recreate_map_info_content(self.map_info_panel_frame) self.selected_flight_details_frame = ttk.LabelFrame( self.bottom_panel_container, text="Selected Flight Details", padding=10 ) self.selected_flight_details_frame.grid( row=0, column=2, sticky="nsew", padx=(2, 0) ) self._create_selected_flight_details_content(self.selected_flight_details_frame) from ..data.logging_config import LOGGING_CONFIG setup_logging( gui_log_widget=self.log_text_widget, root_tk_instance=self.root, logging_config_dict=LOGGING_CONFIG, ) self.root.protocol("WM_DELETE_WINDOW", self._on_closing) self.root.after(100, self._delayed_initialization) module_logger.info( "MainWindow basic structure initialized. Delayed init pending." ) # ... (Tutti gli altri metodi di MainWindow, inclusi quelli aggiunti e quelli che # non sono stati modificati, vanno qui. Ho incluso nuovamente tutti i metodi # per completezza, verificando le correzioni precedenti.) def _delayed_initialization(self): if not self.root.winfo_exists(): module_logger.warning( "Root window destroyed before delayed initialization." ) return if MAP_CANVAS_MANAGER_AVAILABLE and MapCanvasManager is not None: default_map_bbox = { "lat_min": app_config.DEFAULT_BBOX_LAT_MIN, "lon_min": app_config.DEFAULT_BBOX_LON_MIN, "lat_max": app_config.DEFAULT_BBOX_LAT_MAX, "lon_max": app_config.DEFAULT_BBOX_LON_MAX, } self.root.after(200, self._initialize_map_manager, default_map_bbox) else: module_logger.error( "MapCanvasManager class not available post-init. Map display will be a placeholder." ) self.root.after( 50, lambda: self._update_map_placeholder( "Map functionality disabled (Import Error)." ), ) if ( hasattr(self, "track_length_var") and self.controller and hasattr(self.controller, "set_map_track_length") ): try: initial_track_len = self.track_length_var.get() if initial_track_len > 0: module_logger.debug( f"Delayed init: Setting initial track length to {initial_track_len}" ) self.controller.set_map_track_length(initial_track_len) except Exception as e: module_logger.error( f"Error setting initial track length during delayed init: {e}" ) self.root.after(10, self._on_mode_change) module_logger.info( "MainWindow fully initialized and displayed after delayed setup." ) def _initialize_map_manager(self, initial_bbox_for_map: Dict[str, float]): 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.") if self.controller and hasattr(self.controller, "update_general_map_info"): self.controller.update_general_map_info() return if not self.flight_canvas.winfo_exists(): module_logger.warning( "Flight canvas destroyed before map manager initialization." ) return canvas_w, canvas_h = ( self.flight_canvas.winfo_width(), self.flight_canvas.winfo_height(), ) if canvas_w <= 1: canvas_w = self.canvas_width if canvas_h <= 1: canvas_h = self.canvas_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, ) if ( hasattr(self, "track_length_var") and self.controller and hasattr(self.controller, "set_map_track_length") ): try: current_track_len_val = self.track_length_var.get() self.controller.set_map_track_length(current_track_len_val) except Exception as e_trk: module_logger.error( f"Error setting initial track length for map manager: {e_trk}" ) if self.controller and hasattr( self.controller, "update_general_map_info" ): self.controller.update_general_map_info() except Exception as e_init: 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}" ) if self.controller and hasattr( self.controller, "update_general_map_info" ): self.controller.update_general_map_info() else: module_logger.warning( f"Canvas not ready for MapCanvasManager init (dims: {canvas_w}x{canvas_h}), retrying..." ) if self.root.winfo_exists(): self.root.after(300, self._initialize_map_manager, initial_bbox_for_map) def _recreate_map_tools_content(self, parent_frame: ttk.Frame): controls_map_container = ttk.Frame(parent_frame) controls_map_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) zoom_frame = ttk.Frame(controls_map_container) zoom_frame.pack(side=tk.TOP, pady=(0, 10)) ttk.Button(zoom_frame, text="Zoom In (+)", command=self._map_zoom_in).pack( side=tk.LEFT, padx=2 ) ttk.Button(zoom_frame, text="Zoom Out (-)", command=self._map_zoom_out).pack( side=tk.LEFT, padx=2 ) pan_frame = ttk.Frame(controls_map_container) pan_frame.pack(side=tk.TOP, pady=(0, 10)) ttk.Button(pan_frame, text="Up", command=lambda: self._map_pan("up")).grid( row=0, column=1, padx=2, pady=2 ) ttk.Button(pan_frame, text="Left", command=lambda: self._map_pan("left")).grid( row=1, column=0, padx=2, pady=2 ) ttk.Button( pan_frame, text="Right", command=lambda: self._map_pan("right") ).grid(row=1, column=2, padx=2, pady=2) ttk.Button(pan_frame, text="Down", command=lambda: self._map_pan("down")).grid( row=2, column=1, padx=2, pady=2 ) pan_frame.columnconfigure(0, weight=1) pan_frame.columnconfigure(2, weight=1) center_frame = ttk.LabelFrame( controls_map_container, text="Center Map", padding=5 ) center_frame.pack(side=tk.TOP, fill=tk.X, pady=(5, 0)) center_frame.columnconfigure(1, weight=1) center_frame.columnconfigure(3, weight=1) ttk.Label(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( 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(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( 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(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="100") self.center_patch_size_entry = ttk.Entry( 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( center_frame, text="Center & Fit Patch", command=self._map_center_and_fit ) self.center_map_button.grid( row=1, column=2, columnspan=2, padx=5, pady=5, sticky=tk.E ) def _recreate_map_info_content(self, parent_frame: ttk.Frame): parent_frame.columnconfigure( 1, weight=0 ) # Le label dei valori non devono espandersi troppo parent_frame.columnconfigure(3, weight=0) # Idem info_row = 0 # Riga 1: Click Coords ttk.Label(parent_frame, text="Click Lat:").grid( row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2) ) self.info_lat_value = ttk.Label( parent_frame, text="N/A", width=12 ) # width per dimensione fissa self.info_lat_value.grid( row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5) ) ttk.Label(parent_frame, text="Lon:").grid( row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2) ) self.info_lon_value = ttk.Label(parent_frame, text="N/A", width=12) self.info_lon_value.grid( row=info_row, column=3, sticky=tk.W, pady=1, padx=(0, 5) ) info_row += 1 # Riga 2: Click DMS ttk.Label(parent_frame, text="Lat DMS:").grid( row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2) ) self.info_lat_dms_value = ttk.Label( parent_frame, text="N/A", width=18, wraplength=140 ) self.info_lat_dms_value.grid( row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5) ) ttk.Label(parent_frame, text="Lon DMS:").grid( row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2) ) self.info_lon_dms_value = ttk.Label( parent_frame, text="N/A", width=18, wraplength=140 ) self.info_lon_dms_value.grid( row=info_row, column=3, sticky=tk.W, pady=1, padx=(0, 5) ) info_row += 1 ttk.Separator(parent_frame, orient=tk.HORIZONTAL).grid( row=info_row, column=0, columnspan=4, sticky=tk.EW, pady=2 ) info_row += 1 # Riga 3: Map Bounds (N & W) ttk.Label(parent_frame, text="Map N:").grid( row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2) ) self.info_map_bounds_n = ttk.Label(parent_frame, text="N/A", width=12) self.info_map_bounds_n.grid( row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5) ) ttk.Label(parent_frame, text="W:").grid( row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2) ) self.info_map_bounds_w = ttk.Label(parent_frame, text="N/A", width=12) self.info_map_bounds_w.grid( row=info_row, column=3, sticky=tk.W, pady=1, padx=(0, 5) ) info_row += 1 # Riga 4: Map Bounds (S & E) ttk.Label(parent_frame, text="Map S:").grid( row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2) ) self.info_map_bounds_s = ttk.Label(parent_frame, text="N/A", width=12) self.info_map_bounds_s.grid( row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5) ) ttk.Label(parent_frame, text="E:").grid( row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2) ) self.info_map_bounds_e = ttk.Label(parent_frame, text="N/A", width=12) self.info_map_bounds_e.grid( row=info_row, column=3, sticky=tk.W, pady=1, padx=(0, 5) ) info_row += 1 ttk.Separator(parent_frame, orient=tk.HORIZONTAL).grid( row=info_row, column=0, columnspan=4, sticky=tk.EW, pady=2 ) info_row += 1 # Riga 5: Target BBox (N & W) ttk.Label(parent_frame, text="Target N:").grid( row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2) ) self.info_target_bbox_n = ttk.Label(parent_frame, text="N/A", width=12) self.info_target_bbox_n.grid( row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5) ) ttk.Label(parent_frame, text="W:").grid( row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2) ) self.info_target_bbox_w = ttk.Label(parent_frame, text="N/A", width=12) self.info_target_bbox_w.grid( row=info_row, column=3, sticky=tk.W, pady=1, padx=(0, 5) ) info_row += 1 # Riga 6: Target BBox (S & E) ttk.Label(parent_frame, text="Target S:").grid( row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2) ) self.info_target_bbox_s = ttk.Label(parent_frame, text="N/A", width=12) self.info_target_bbox_s.grid( row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5) ) ttk.Label(parent_frame, text="E:").grid( row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2) ) self.info_target_bbox_e = ttk.Label(parent_frame, text="N/A", width=12) self.info_target_bbox_e.grid( row=info_row, column=3, sticky=tk.W, pady=1, padx=(0, 5) ) info_row += 1 ttk.Separator(parent_frame, orient=tk.HORIZONTAL).grid( row=info_row, column=0, columnspan=4, sticky=tk.EW, pady=2 ) info_row += 1 # Riga 7: Zoom, Size, Flights ttk.Label(parent_frame, text="Zoom:").grid( row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2) ) self.info_zoom_value = ttk.Label(parent_frame, text="N/A", width=4) self.info_zoom_value.grid( row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5) ) ttk.Label(parent_frame, text="Flights:").grid( row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2) ) self.info_flight_count_value = ttk.Label(parent_frame, text="N/A", width=5) self.info_flight_count_value.grid( row=info_row, column=3, sticky=tk.W, pady=1, padx=(0, 5) ) info_row += 1 ttk.Label(parent_frame, text="Map Size:").grid( row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2) ) self.info_map_size_value = ttk.Label( parent_frame, text="N/A", wraplength=180 ) # Adattato wraplength self.info_map_size_value.grid( row=info_row, column=1, columnspan=3, sticky=tk.W, pady=1, padx=(0, 5) ) def _create_selected_flight_details_content(self, parent_frame: ttk.LabelFrame): parent_frame.columnconfigure(1, weight=1) parent_frame.columnconfigure(3, weight=1) self.flight_detail_labels: Dict[str, ttk.Label] = {} fields_col1 = [ ("icao24", "ICAO24:"), ("baro_altitude_m", "Altitude (baro):"), ("velocity_mps", "Speed (GS):"), ("true_track_deg", "Track (°):"), ("squawk", "Squawk:"), ("position_source", "Pos. Source:"), ("registration", "Registration:"), ("model", "Model:"), ("operator", "Operator:"), ("categorydescription", "Category:"), ("serialnumber", "Serial No.:"), ("firstflightdate", "First Flight:"), ] fields_col2 = [ ("callsign", "Callsign:"), ("geo_altitude_m", "Altitude (Geo):"), ("vertical_rate_mps", "Vert. Rate:"), ("on_ground", "On Ground:"), ("spi", "SPI:"), ( "origin_country", "Origin Country:", ), # Già presente in CanonicalFlightState ("manufacturername", "Manufacturer:"), ("typecode", "Type Code:"), ("operatorcallsign", "Op. Callsign:"), ("built_year", "Built Year:"), ("country", "Country (Reg):"), ("timestamp_metadata", "DB Timestamp:"), ] max_rows = max(len(fields_col1), len(fields_col2)) for i in range(max_rows): if i < len(fields_col1): key1, label_text1 = fields_col1[i] ttk.Label( parent_frame, text=label_text1, font="-weight bold" if key1 == "icao24" else None, ).grid( row=i, column=0, sticky=tk.W, pady=0, padx=(0, 2) ) # pady a 0 value_label1 = ttk.Label( parent_frame, text="N/A", wraplength=130 ) # wraplength ridotto value_label1.grid( row=i, column=1, sticky=tk.W, pady=0, padx=(0, 5) ) # pady a 0, padx ridotto self.flight_detail_labels[key1] = value_label1 if i < len(fields_col2): key2, label_text2 = fields_col2[i] ttk.Label(parent_frame, text=label_text2).grid( row=i, column=2, sticky=tk.W, pady=0, padx=(5, 2) ) # pady a 0, padx aggiunto value_label2 = ttk.Label( parent_frame, text="N/A", wraplength=130 ) # wraplength ridotto value_label2.grid( row=i, column=3, sticky=tk.W, pady=0, padx=(0, 0) ) # pady a 0 self.flight_detail_labels[key2] = value_label2 # ... (TUTTI gli altri metodi di MainWindow come _import_aircraft_db_csv, show_info_message, # update_selected_flight_details, _on_closing, _reset_gui_to_stopped_state, etc. # devono essere presenti qui come nella versione completa che ti ho fornito precedentemente) # Per brevità, non li ripeto di nuovo, ma ASSICURATI che siano tutti inclusi. # Ho riportato qui sotto quelli cruciali per il funzionamento e le modifiche recenti. def _import_aircraft_db_csv(self): if not self.controller: self.show_error_message( "Controller Error", "Application controller not available." ) module_logger.error("Controller not available for aircraft DB import.") return if not hasattr( self.controller, "import_aircraft_database_from_file_with_progress" ): self.show_error_message( "Function Error", "Progress import function not available in controller.", ) module_logger.error( "Method 'import_aircraft_database_from_file_with_progress' missing in controller." ) return filepath = filedialog.askopenfilename( master=self.root, title="Select Aircraft Database CSV File", filetypes=(("CSV files", "*.csv"), ("All files", "*.*")), ) if filepath: module_logger.info(f"GUI: Selected CSV file for import: {filepath}") if self.progress_dialog and self.progress_dialog.winfo_exists(): self.progress_dialog.destroy() # Chiudi dialog precedente se esiste self.progress_dialog = ImportProgressDialog( self.root, title=f"Importing: {os.path.basename(filepath)}" ) self.controller.import_aircraft_database_from_file_with_progress( filepath, self.progress_dialog ) else: module_logger.info("GUI: CSV import cancelled by user.") def show_info_message(self, title: str, message: str): module_logger.info(f"Displaying info: Title='{title}', Message='{message}'") if hasattr(self, "root") and self.root.winfo_exists(): try: messagebox.showinfo(title, message, parent=self.root) except tk.TclError: module_logger.warning( f"TclError showing info messagebox '{title}'. Root window might be gone." ) def update_selected_flight_details(self, flight_data: Optional[Dict[str, Any]]): module_logger.debug( f"GUI: Updating flight details panel for: {flight_data.get('icao24', 'None') if flight_data else 'None'}" ) if not hasattr(self, "flight_detail_labels"): module_logger.error( "flight_detail_labels not initialized in MainWindow. Cannot update details." ) return all_possible_keys_in_panel = list(self.flight_detail_labels.keys()) for key in all_possible_keys_in_panel: label_widget = self.flight_detail_labels.get(key) if label_widget and label_widget.winfo_exists(): value_to_display = "N/A" if flight_data and key in flight_data: value = flight_data.get(key) if ( value is not None and str(value).strip() != "" ): # Considera stringa vuota come N/A if key in ["baro_altitude_m", "geo_altitude_m"] and isinstance( value, (float, int) ): value_to_display = f"{value:.0f} m" elif key == "velocity_mps" and isinstance(value, (float, int)): value_to_display = ( f"{value:.1f} m/s ({value * 1.94384:.1f} kts)" ) elif key == "vertical_rate_mps" and isinstance( value, (float, int) ): value_to_display = ( f"{value * 196.85:.0f} ft/min ({value:.1f} m/s)" ) elif key == "true_track_deg" and isinstance( value, (float, int) ): value_to_display = f"{value:.1f}°" elif key in [ "timestamp", "last_contact_timestamp", "firstflightdate", "timestamp_metadata", ]: if isinstance(value, (int, float)) and value > 0: try: dt_obj = datetime.fromtimestamp( value, tz=timezone.utc ) value_to_display = dt_obj.strftime( "%Y-%m-%d %H:%M:%S Z" ) except ( OSError, OverflowError, ValueError, ): # Per timestamp non validi value_to_display = str(value) + " (raw ts)" elif ( isinstance(value, str) and value.strip() ): # Se è già una stringa di data/ora value_to_display = value elif isinstance(value, bool): value_to_display = str(value) elif ( key == "built_year" and value ): # Controlla se value non è None o 0 value_to_display = ( str(int(value)) if isinstance(value, (float, int)) and value > 0 else str(value) ) elif isinstance(value, str) and value.strip(): value_to_display = value elif isinstance(value, (int, float)): value_to_display = str(value) label_widget.config(text=value_to_display) module_logger.debug("Flight details panel updated.") # Includi qui TUTTI gli altri metodi di MainWindow che non sono stati toccati da questa modifica, # copiandoli dalla versione precedente che ti ho fornito. # Esempio: update_semaphore_and_status, _on_closing, _reset_gui_to_stopped_state, etc. # È fondamentale che il file sia completo. def update_semaphore_and_status(self, status_level: str, message: str): color_to_set = SEMAPHORE_COLOR_STATUS_MAP.get( status_level, SEMAPHORE_COLOR_STATUS_MAP.get(GUI_STATUS_UNKNOWN, "gray60") ) if ( hasattr(self, "semaphore_canvas") and 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 current_status_text = f"Status: {message}" if ( hasattr(self, "status_label") and self.status_label and self.status_label.winfo_exists() ): try: self.status_label.config(text=current_status_text) except tk.TclError: pass log_level_to_use = logging.INFO if status_level == GUI_STATUS_ERROR: log_level_to_use = logging.ERROR elif status_level == GUI_STATUS_PERMANENT_FAILURE: log_level_to_use = logging.CRITICAL elif status_level == GUI_STATUS_WARNING: log_level_to_use = logging.WARNING elif status_level in [GUI_STATUS_FETCHING, GUI_STATUS_OK, GUI_STATUS_UNKNOWN]: log_level_to_use = logging.DEBUG module_logger.log( log_level_to_use, f"GUI Status Update: Level='{status_level}', Message='{message}'", ) def _on_closing(self): module_logger.info("Main window closing event triggered.") user_confirmed_quit = False if hasattr(self, "root") and self.root.winfo_exists(): user_confirmed_quit = messagebox.askokcancel( "Quit", "Do you want to quit Flight Monitor?", parent=self.root ) else: user_confirmed_quit = True module_logger.warning( "Root window non-existent during _on_closing, proceeding with cleanup." ) if user_confirmed_quit: module_logger.info( "User confirmed quit or quit forced. Proceeding with cleanup." ) if self.controller and hasattr(self.controller, "on_application_exit"): try: self.controller.on_application_exit() except Exception as e: module_logger.error( f"Error during controller.on_application_exit: {e}", exc_info=True, ) module_logger.info("Shutting down logging system.") try: shutdown_logging_system() except Exception as e: module_logger.error( f"Error during shutdown_logging_system: {e}", exc_info=True ) if hasattr(self, "root") and self.root.winfo_exists(): try: self.root.destroy() except tk.TclError as e: module_logger.error( f"TclError destroying root: {e}.", exc_info=False ) except Exception as e: module_logger.error( f"Unexpected error destroying root: {e}", exc_info=True ) 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") and self.start_button.winfo_exists(): self.start_button.config(state=tk.NORMAL) if hasattr(self, "stop_button") and self.stop_button.winfo_exists(): self.stop_button.config(state=tk.DISABLED) if hasattr(self, "live_radio") and self.live_radio.winfo_exists(): self.live_radio.config(state=tk.NORMAL) if hasattr(self, "history_radio") and self.history_radio.winfo_exists(): self.history_radio.config(state=tk.NORMAL) self._update_controls_state_based_on_mode_and_tab() status_level = GUI_STATUS_OK if status_message and ( "failed" in status_message.lower() or "error" in status_message.lower() ): status_level = GUI_STATUS_ERROR elif status_message and "warning" in status_message.lower(): status_level = GUI_STATUS_WARNING if hasattr(self, "root") and self.root.winfo_exists(): self.update_semaphore_and_status(status_level, status_message) else: module_logger.debug( "Root window gone, skipping status update in _reset_gui_to_stopped_state." ) module_logger.info( f"GUI controls reset to stopped state. Status: '{status_message}'" ) def _should_show_main_placeholder(self) -> bool: return not ( hasattr(self, "map_manager_instance") and self.map_manager_instance is not None and MAP_CANVAS_MANAGER_AVAILABLE ) def _update_map_placeholder(self, text_to_display: str): if not ( hasattr(self, "flight_canvas") and self.flight_canvas and self.flight_canvas.winfo_exists() ): module_logger.debug("Flight canvas not available for placeholder update.") return if not self._should_show_main_placeholder(): try: self.flight_canvas.delete("placeholder_text") except tk.TclError: pass except Exception as e: module_logger.warning( f"Error deleting placeholder: {e}", exc_info=False ) return try: self.flight_canvas.delete("placeholder_text") canvas_w, canvas_h = ( self.flight_canvas.winfo_width(), self.flight_canvas.winfo_height(), ) if canvas_w <= 1: canvas_w = self.canvas_width if canvas_h <= 1: canvas_h = self.canvas_height if canvas_w > 1 and canvas_h > 1: self.flight_canvas.create_text( canvas_w / 2, canvas_h / 2, text=text_to_display, tags="placeholder_text", fill="gray50", font=("Arial", 12, "italic"), justify=tk.CENTER, width=canvas_w - 40, ) else: module_logger.warning( f"Cannot draw placeholder: Canvas dims invalid ({canvas_w}x{canvas_h})." ) except tk.TclError: module_logger.warning( "TclError updating map placeholder (canvas might be gone)." ) except Exception as e: module_logger.error( f"Unexpected error in _update_map_placeholder: {e}", exc_info=True ) def _on_mode_change(self): if not ( hasattr(self, "mode_var") and hasattr(self, "function_notebook") and self.function_notebook.winfo_exists() ): module_logger.warning("_on_mode_change: Essential widgets not ready.") return selected_mode = self.mode_var.get() status_message = f"Mode: {selected_mode}. Ready." module_logger.info(f"Mode changed to: {selected_mode}") try: tab_indices = {} for i in range(self.function_notebook.index("end")): current_tab_widget_name = self.function_notebook.tabs()[i] actual_widget = self.function_notebook.nametowidget( current_tab_widget_name ) if ( hasattr(self, "live_bbox_tab_frame") and actual_widget == self.live_bbox_tab_frame ): tab_indices["LiveArea"] = i elif ( hasattr(self, "history_tab_frame") and actual_widget == self.history_tab_frame ): tab_indices["History"] = i elif ( hasattr(self, "live_airport_tab_frame") and actual_widget == self.live_airport_tab_frame ): tab_indices["LiveAirport"] = i live_bbox_idx, history_idx, live_airport_idx = ( tab_indices.get("LiveArea", -1), tab_indices.get("History", -1), tab_indices.get("LiveAirport", -1), ) if selected_mode == "Live": if live_bbox_idx != -1: self.function_notebook.tab(live_bbox_idx, state="normal") if live_airport_idx != -1: self.function_notebook.tab(live_airport_idx, state="normal") if history_idx != -1: self.function_notebook.tab(history_idx, state="disabled") current_idx = self.function_notebook.index("current") if current_idx == history_idx and live_bbox_idx != -1: self.function_notebook.select(live_bbox_idx) elif selected_mode == "History": if live_bbox_idx != -1: self.function_notebook.tab(live_bbox_idx, state="disabled") if live_airport_idx != -1: self.function_notebook.tab(live_airport_idx, state="disabled") if history_idx != -1: self.function_notebook.tab(history_idx, state="normal") current_idx = self.function_notebook.index("current") if current_idx != history_idx and history_idx != -1: self.function_notebook.select(history_idx) except tk.TclError as e: module_logger.warning(f"TclError finding tab IDs: {e}", exc_info=False) except Exception as e: module_logger.warning( f"Error updating func tabs state in mode change: {e}", exc_info=True ) self.clear_all_views_data() self._update_controls_state_based_on_mode_and_tab() if hasattr(self, "root") and self.root.winfo_exists(): self.update_semaphore_and_status(GUI_STATUS_OK, status_message) def _on_function_tab_change(self, event: Optional[tk.Event] = None): if not ( hasattr(self, "function_notebook") and self.function_notebook.winfo_exists() ): module_logger.debug("_on_function_tab_change: Function notebook not ready.") return try: tab_text = self.function_notebook.tab( self.function_notebook.index("current"), "text" ) module_logger.info(f"GUI: Switched function tab to: {tab_text}") placeholder_text_map = "Map Area." if "Live: Area Monitor" in tab_text: placeholder_text_map = "Map - Live Area. Define area and press Start." elif "Live: Airport" in tab_text: placeholder_text_map = "Map - Live Airport. (Functionality TBD)" elif "History" in tab_text: placeholder_text_map = "Map - History Analysis. (Functionality TBD)" self._update_map_placeholder(placeholder_text_map) self._update_controls_state_based_on_mode_and_tab() except (tk.TclError, ValueError) as e: module_logger.warning(f"Error on function tab change ({type(e).__name__}).") except Exception as e: module_logger.error( f"Unexpected error in _on_function_tab_change: {e}", exc_info=True ) def _update_controls_state_based_on_mode_and_tab(self): is_live_mode = hasattr(self, "mode_var") and self.mode_var.get() == "Live" is_monitoring_active = ( hasattr(self, "stop_button") and self.stop_button.winfo_exists() and self.stop_button.cget("state") == tk.NORMAL ) active_func_tab_text = "" if hasattr(self, "function_notebook") and self.function_notebook.winfo_exists(): try: active_func_tab_text = self.function_notebook.tab( self.function_notebook.index("current"), "text" ) except (tk.TclError, ValueError): pass enable_bbox_entries = ( is_live_mode and "Live: Area Monitor" in active_func_tab_text and not is_monitoring_active ) enable_track_length = ( is_live_mode and "Live: Area Monitor" in active_func_tab_text ) self._set_bbox_entries_state(tk.NORMAL if enable_bbox_entries else tk.DISABLED) if ( hasattr(self, "track_length_spinbox") and self.track_length_spinbox.winfo_exists() ): try: final_track_spin_state = ( tk.DISABLED if is_monitoring_active else ("readonly" if enable_track_length else tk.DISABLED) ) self.track_length_spinbox.config(state=final_track_spin_state) except tk.TclError: pass module_logger.debug( f"Controls state updated. BBox: {'Enabled' if enable_bbox_entries else 'Disabled'}. TrackLength: {'Enabled' if enable_track_length and not is_monitoring_active else 'Disabled'}" ) def _on_view_tab_change(self, event: Optional[tk.Event] = None): if not (hasattr(self, "views_notebook") and self.views_notebook.winfo_exists()): module_logger.debug("_on_view_tab_change: Views notebook not ready.") return try: tab_text = self.views_notebook.tab( self.views_notebook.index("current"), "text" ) module_logger.info(f"GUI: Switched view tab to: {tab_text}") except (tk.TclError, ValueError): module_logger.warning(f"Error on view tab change ({type(e).__name__}).") except Exception as e: module_logger.warning(f"Error on view tab change: {e}", exc_info=True) def _set_bbox_entries_state(self, state: str): entries_names = [ "lat_min_entry", "lon_min_entry", "lat_max_entry", "lon_max_entry", ] entries = [getattr(self, name, None) for name in entries_names] if all(e and hasattr(e, "winfo_exists") and e.winfo_exists() for e in entries): try: for entry in entries: entry.config(state=state) # type: ignore except tk.TclError: module_logger.warning( "TclError setting BBox entries state (widgets gone)." ) def _start_monitoring(self): if not hasattr(self, "mode_var"): module_logger.error("Start: mode_var N/A.") self.show_error_message("Internal Error", "App mode N/A.") return selected_mode = self.mode_var.get() module_logger.info(f"GUI: 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_text = self.function_notebook.tab( self.function_notebook.index("current"), "text" ) except Exception: module_logger.warning("Could not get active function tab for start.") if hasattr(self, "start_button") and self.start_button.winfo_exists(): self.start_button.config(state=tk.DISABLED) if hasattr(self, "stop_button") and self.stop_button.winfo_exists(): self.stop_button.config(state=tk.NORMAL) if hasattr(self, "live_radio") and self.live_radio.winfo_exists(): self.live_radio.config(state=tk.DISABLED) if hasattr(self, "history_radio") and self.history_radio.winfo_exists(): self.history_radio.config(state=tk.DISABLED) self._update_controls_state_based_on_mode_and_tab() if not self.controller: module_logger.critical("Controller N/A.") self._reset_gui_to_stopped_state("Critical Error: Controller unavailable.") self.show_error_message("Internal Error", "App controller missing.") return if selected_mode == "Live": if "Live: Area Monitor" in active_func_tab_text: bbox = self.get_bounding_box_from_gui() if bbox: self.controller.start_live_monitoring(bbox) else: self._reset_gui_to_stopped_state("Start failed: Invalid BBox.") else: module_logger.warning( f"Start in Live mode, but tab '{active_func_tab_text}' is not 'Live: Area Monitor'. GUI state issue." ) self.update_semaphore_and_status( GUI_STATUS_WARNING, f"Start not supported on '{active_func_tab_text}'.", ) self._reset_gui_to_stopped_state( f"Start not supported on {active_func_tab_text}." ) elif selected_mode == "History": if "History" in active_func_tab_text: self.controller.start_history_monitoring() else: module_logger.warning( f"Start in History mode, but tab '{active_func_tab_text}' is not 'History'. GUI state issue." ) self.update_semaphore_and_status( GUI_STATUS_WARNING, f"Start not supported on '{active_func_tab_text}'.", ) self._reset_gui_to_stopped_state( f"Start not supported on {active_func_tab_text}." ) def _stop_monitoring(self): module_logger.info("GUI: User requested to stop monitoring.") selected_mode = self.mode_var.get() if hasattr(self, "mode_var") else "Unknown" if not self.controller: module_logger.error("Controller N/A to stop.") self._reset_gui_to_stopped_state("Error: Controller missing.") return if selected_mode == "Live": self.controller.stop_live_monitoring() elif selected_mode == "History": self.controller.stop_history_monitoring() if hasattr(self, "root") and self.root.winfo_exists(): self.update_semaphore_and_status( GUI_STATUS_FETCHING, f"{selected_mode} monitoring stopping..." ) def get_bounding_box_from_gui(self) -> Optional[Dict[str, float]]: module_logger.debug("Getting BBox from GUI.") req_vars_names = ["lat_min_var", "lon_min_var", "lat_max_var", "lon_max_var"] if not all(hasattr(self, v_name) for v_name in req_vars_names): module_logger.error("BBox StringVars N/A.") self.show_error_message( "Internal Error", "BBox input fields are not available." ) return None try: vals_str = [getattr(self, v_name).get() for v_name in req_vars_names] if not all(s.strip() for s in vals_str): module_logger.error("One or more BBox fields are empty.") self.show_error_message( "Input Error", "All Bounding Box fields are required." ) return None lat_min, lon_min, lat_max, lon_max = map(float, vals_str) except ValueError: module_logger.error("Invalid number format in BBox fields.") self.show_error_message( "Input Error", "Bounding Box coordinates must be valid numbers." ) return None except Exception as e: module_logger.error( f"Unexpected error reading BBox fields: {e}", exc_info=True ) self.show_error_message( "Internal Error", "An unexpected error occurred while reading BBox fields.", ) return None bbox_dict = { "lat_min": lat_min, "lon_min": lon_min, "lat_max": lat_max, "lon_max": lon_max, } if not _is_valid_bbox_dict(bbox_dict): self.show_error_message( "Input Error", "Invalid Bounding Box range or order. Ensure Lat Min < Lat Max, Lon Min < Lon Max, and valid geo limits.", ) return None return bbox_dict def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]): if not hasattr(self, "lat_min_var"): module_logger.warning("BBox GUI StringVars not available for update.") return if bbox_dict and _is_valid_bbox_dict(bbox_dict): decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5) try: self.lat_min_var.set(f"{bbox_dict['lat_min']:.{decimals}f}") self.lon_min_var.set(f"{bbox_dict['lon_min']:.{decimals}f}") self.lat_max_var.set(f"{bbox_dict['lat_max']:.{decimals}f}") self.lon_max_var.set(f"{bbox_dict['lon_max']:.{decimals}f}") except tk.TclError: module_logger.warning( "TclError updating BBox GUI fields (widgets might be gone)." ) except Exception as e: module_logger.error( f"Error updating BBox GUI fields: {e}", exc_info=True ) else: module_logger.warning( f"Invalid or empty bbox_dict provided for GUI update: {bbox_dict}. Setting fields to N/A." ) try: self.lat_min_var.set("N/A") self.lon_min_var.set("N/A") self.lat_max_var.set("N/A") self.lon_max_var.set("N/A") except tk.TclError: pass except Exception: pass def display_flights_on_canvas( self, flight_states: List[CanonicalFlightState], _active_bbox_context: Optional[Dict[str, float]], ): if not ( hasattr(self, "map_manager_instance") and self.map_manager_instance and MAP_CANVAS_MANAGER_AVAILABLE ): module_logger.warning("MapCanvasManager N/A, cannot display flights.") if hasattr(self, "root") and self.root.winfo_exists(): self._update_map_placeholder("Map N/A to display flights.") return try: self.map_manager_instance.update_flights_on_map(flight_states) if not flight_states and self._should_show_main_placeholder(): self._update_map_placeholder("No flights in the selected area.") except Exception as e: module_logger.error( f"Error updating flights on map via map_manager: {e}", exc_info=True ) self.show_error_message( "Map Display Error", "Could not update flights on map." ) def clear_all_views_data(self): module_logger.info("Clearing data from all views.") if ( hasattr(self, "map_manager_instance") and self.map_manager_instance and MAP_CANVAS_MANAGER_AVAILABLE ): try: self.map_manager_instance.clear_map_display() except Exception as e: module_logger.warning(f"Error clearing map via map manager: {e}") elif self._should_show_main_placeholder(): mode = self.mode_var.get() if hasattr(self, "mode_var") else "Unknown" text = f"Map - {mode}. Data cleared." if mode == "Live": text = "Map - Live. Define area and Start." elif mode == "History": text = "Map - History. (TBD)" self._update_map_placeholder(text) if hasattr(self, "update_selected_flight_details"): self.update_selected_flight_details(None) def show_error_message(self, title: str, message: str): module_logger.error(f"Displaying error: Title='{title}', Message='{message}'") status_msg = f"Error: {message[:70]}{'...' if len(message)>70 else ''}" if hasattr(self, "root") and self.root.winfo_exists(): self.update_semaphore_and_status(GUI_STATUS_ERROR, status_msg) try: messagebox.showerror(title, message, parent=self.root) except tk.TclError: module_logger.warning( f"TclError showing error messagebox '{title}'. Root window might be gone." ) else: module_logger.warning( "Root window not available, skipping status update and messagebox for error." ) print(f"ERROR (No GUI): {title} - {message}", flush=True) def show_map_context_menu( self, latitude: float, longitude: float, screen_x: int, screen_y: int ): module_logger.info( f"MainWindow: Request context menu for Lat {latitude:.4f}, Lon {longitude:.4f}" ) if ( hasattr(self, "map_manager_instance") and self.map_manager_instance and hasattr(self.map_manager_instance, "show_map_context_menu_from_gui") ): try: self.map_manager_instance.show_map_context_menu_from_gui( latitude, longitude, screen_x, screen_y ) except Exception as e: module_logger.error( f"Error delegating context menu to MapCanvasManager: {e}", exc_info=True, ) else: module_logger.warning( "Controller or context menu handler N/A for MainWindow context menu." ) def update_clicked_map_info( self, lat_deg: Optional[float], lon_deg: Optional[float], lat_dms: str, lon_dms: str, ): if not hasattr(self, "info_lat_value"): module_logger.warning( "Map info panel (click details) widgets not available for update." ) return decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5) try: if hasattr(self, "info_lat_value") and self.info_lat_value.winfo_exists(): self.info_lat_value.config( text=f"{lat_deg:.{decimals}f}" 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:.{decimals}f}" 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 or "N/A") if ( hasattr(self, "info_lon_dms_value") and self.info_lon_dms_value.winfo_exists() ): self.info_lon_dms_value.config(text=lon_dms or "N/A") except tk.TclError: module_logger.warning( "TclError updating clicked map info (widgets might be gone)." ) except Exception as e: module_logger.error(f"Error updating clicked map info: {e}", exc_info=True) def update_general_map_info_display( self, zoom: Optional[int], map_size_str: str, map_geo_bounds: Optional[Tuple[float, float, float, float]], target_bbox_input: Optional[Dict[str, float]], flight_count: Optional[int], ): if not hasattr(self, "info_zoom_value"): module_logger.warning( "Map info panel (general info) widgets not available for update." ) return decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5) try: mw, ms, me, mn = "N/A", "N/A", "N/A", "N/A" if map_geo_bounds: mw, ms, me, mn = (f"{c:.{decimals}f}" for c in map_geo_bounds) if ( hasattr(self, "info_map_bounds_w") and self.info_map_bounds_w.winfo_exists() ): self.info_map_bounds_w.config(text=mw) if ( hasattr(self, "info_map_bounds_s") and self.info_map_bounds_s.winfo_exists() ): self.info_map_bounds_s.config(text=ms) if ( hasattr(self, "info_map_bounds_e") and self.info_map_bounds_e.winfo_exists() ): self.info_map_bounds_e.config(text=me) if ( hasattr(self, "info_map_bounds_n") and self.info_map_bounds_n.winfo_exists() ): self.info_map_bounds_n.config(text=mn) tw, ts, te, tn = "N/A", "N/A", "N/A", "N/A" color_target_bbox_text = BBOX_COLOR_NA status_target_bbox = "N/A" if target_bbox_input and _is_valid_bbox_dict(target_bbox_input): tw = f"{target_bbox_input['lon_min']:.{decimals}f}" ts = f"{target_bbox_input['lat_min']:.{decimals}f}" te = f"{target_bbox_input['lon_max']:.{decimals}f}" tn = f"{target_bbox_input['lat_max']:.{decimals}f}" if map_geo_bounds: status_target_bbox = self._is_bbox_inside_bbox( target_bbox_input, map_geo_bounds ) if status_target_bbox == "Inside": color_target_bbox_text = BBOX_COLOR_INSIDE elif status_target_bbox == "Partial": color_target_bbox_text = BBOX_COLOR_PARTIAL else: color_target_bbox_text = BBOX_COLOR_OUTSIDE bbox_labels_target_names = [ "info_target_bbox_w", "info_target_bbox_s", "info_target_bbox_e", "info_target_bbox_n", ] bbox_labels_target_values = [tw, ts, te, tn] for name, val_str in zip( bbox_labels_target_names, bbox_labels_target_values ): if hasattr(self, name): label_widget = getattr(self, name) if label_widget and label_widget.winfo_exists(): label_widget.config( text=val_str, foreground=color_target_bbox_text ) 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 or "N/A") if ( hasattr(self, "info_flight_count_value") and self.info_flight_count_value.winfo_exists() ): self.info_flight_count_value.config( text=str(flight_count) if flight_count is not None else "N/A" ) module_logger.debug( f"General map info panel updated: Zoom={zoom}, Size='{map_size_str}', Flights={flight_count}, TargetBBoxStatus='{status_target_bbox}'" ) except tk.TclError: module_logger.warning( "TclError updating general map info (widgets might be gone)." ) except Exception as e: module_logger.error(f"Error updating general map info: {e}", exc_info=True) def _is_bbox_inside_bbox( self, inner_bbox_dict: Dict[str, float], outer_bbox_tuple: Tuple[float, float, float, float], ) -> str: if not _is_valid_bbox_dict(inner_bbox_dict): return "N/A" if not ( outer_bbox_tuple and len(outer_bbox_tuple) == 4 and all(isinstance(c, (int, float)) for c in outer_bbox_tuple) ): return "N/A" outer_dict_temp = { "lon_min": outer_bbox_tuple[0], "lat_min": outer_bbox_tuple[1], "lon_max": outer_bbox_tuple[2], "lat_max": outer_bbox_tuple[3], } eps = 1e-6 fully_inside = ( inner_bbox_dict["lon_min"] >= outer_dict_temp["lon_min"] - eps and inner_bbox_dict["lat_min"] >= outer_dict_temp["lat_min"] - eps and inner_bbox_dict["lon_max"] <= outer_dict_temp["lon_max"] + eps and inner_bbox_dict["lat_max"] <= outer_dict_temp["lat_max"] + eps ) if fully_inside: return "Inside" no_overlap = ( inner_bbox_dict["lon_max"] <= outer_dict_temp["lon_min"] + eps or inner_bbox_dict["lon_min"] >= outer_dict_temp["lon_max"] - eps or inner_bbox_dict["lat_max"] <= outer_dict_temp["lat_min"] + eps or inner_bbox_dict["lat_min"] >= outer_dict_temp["lat_max"] - eps ) if no_overlap: return "Outside" return "Partial" def _map_zoom_in(self): module_logger.debug("GUI: Map Zoom In button pressed.") if self.controller and hasattr(self.controller, "map_zoom_in"): self.controller.map_zoom_in() else: module_logger.warning("Controller or map_zoom_in N/A.") self.show_error_message( "Action Failed", "Map zoom control is not available." ) def _map_zoom_out(self): module_logger.debug("GUI: Map Zoom Out button pressed.") if self.controller and hasattr(self.controller, "map_zoom_out"): self.controller.map_zoom_out() else: module_logger.warning("Controller or map_zoom_out N/A.") self.show_error_message( "Action Failed", "Map zoom control is not available." ) def _map_pan(self, direction: str): module_logger.debug(f"GUI: Map Pan '{direction}' button pressed.") if self.controller and hasattr(self.controller, "map_pan_direction"): self.controller.map_pan_direction(direction) else: module_logger.warning("Controller or map_pan_direction N/A.") self.show_error_message( "Action Failed", "Map pan control is not available." ) def _map_center_and_fit(self): module_logger.debug("GUI: Map Center & Fit Patch button pressed.") try: lat_str, lon_str, patch_str = ( self.center_lat_var.get(), self.center_lon_var.get(), self.center_patch_size_var.get(), ) if not lat_str.strip() or not lon_str.strip() or not patch_str.strip(): self.show_error_message( "Input Error", "Latitude, Longitude, and Patch Size are required for centering.", ) return lat, lon, patch_size_km = float(lat_str), float(lon_str), float(patch_str) if not (-90.0 <= lat <= 90.0): self.show_error_message( "Input Error", "Latitude must be between -90 and 90." ) return if not (-180.0 <= lon <= 180.0): self.show_error_message( "Input Error", "Longitude must be between -180 and 180." ) return if patch_size_km <= 0: self.show_error_message( "Input Error", "Patch size must be a positive number (km)." ) return 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_size_km ) else: module_logger.warning( "Controller or map_center_on_coords_and_fit_patch N/A." ) self.show_error_message( "Action Failed", "Map centering control is not available." ) except ValueError: self.show_error_message( "Input Error", "Latitude, Longitude, and Patch Size must be valid numbers.", ) except Exception as e: module_logger.error(f"Error in _map_center_and_fit: {e}", exc_info=True) self.show_error_message("Error", f"An unexpected error occurred: {e}") def _on_track_length_change(self): if not hasattr(self, "track_length_var") or not hasattr(self, "controller"): module_logger.debug( "Track length var or controller not ready for change notification." ) return try: new_length = self.track_length_var.get() if not (2 <= new_length <= 100): module_logger.warning( f"Track length value {new_length} out of spinbox range. Ignoring change." ) return module_logger.info(f"GUI: Track length changed by user to: {new_length}") if self.controller and hasattr(self.controller, "set_map_track_length"): self.controller.set_map_track_length(new_length) else: module_logger.warning( "Controller or set_map_track_length method not available." ) except tk.TclError: module_logger.warning( "TclError getting track length. Value might be invalid (e.g. non-integer)." ) except Exception as e: module_logger.error(f"Error in _on_track_length_change: {e}", exc_info=True)