SXXXXXXX_FlightMonitor/flightmonitor/gui/main_window.py
2025-05-30 14:13:58 +02:00

1958 lines
84 KiB
Python

# 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
from datetime import datetime, timezone
import webbrowser
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."
)
# --- NUOVO IMPORT PER LA DIALOG ---
try:
from .dialogs.import_progress_dialog import ImportProgressDialog
IMPORT_DIALOG_AVAILABLE = True
except ImportError as e_dialog_import:
ImportProgressDialog = None # type: ignore
IMPORT_DIALOG_AVAILABLE = False
print(
f"ERROR in MainWindow import: Failed to import ImportProgressDialog: {e_dialog_import}. Import progress UI will be basic."
)
# --- FINE NUOVO IMPORT ---
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)
# La classe ImportProgressDialog è stata spostata in gui/dialogs/import_progress_dialog.py
# La classe FullFlightDetailsWindow sarà creata in gui/dialogs/full_flight_details_window.py
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
self.full_flight_details_window: Optional[tk.Toplevel] = (
None # Sarà di tipo FullFlightDetailsWindow
)
# ... (resto dell'__init__ come nella versione precedente completa,
# inclusa la creazione del menubar, dei paned window, dei notebook,
# dei pannelli Map Tools, Map Information, e Selected Flight Details.
# La logica di creazione di questi widget rimane la stessa.)
# Assicurati che la chiamata a _import_aircraft_db_csv nel menu
# sia corretta.
if app_config.LAYOUT_START_MAXIMIZED:
try:
self.root.state("zoomed")
except tk.TclError: # Fallback
try:
self.root.geometry(
f"{self.root.winfo_screenwidth()}x{self.root.winfo_screenheight()}+0+0"
)
except tk.TclError:
pass # Ignora se anche questo fallisce
min_win_w = getattr(app_config, "LAYOUT_WINDOW_MIN_WIDTH", 900)
min_win_h = getattr(app_config, "LAYOUT_WINDOW_MIN_HEIGHT", 650)
self.root.minsize(min_win_w, min_win_h)
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(
"<<NotebookTabChanged>>", 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, x1, y1 = (
SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH,
SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH,
SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH + SEMAPHORE_SIZE,
SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH + 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("<<NotebookTabChanged>>", 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."
)
def _import_aircraft_db_csv(self):
"""Opens a file dialog to select a CSV and calls the controller to import it."""
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
# Usa il nuovo metodo del controller che gestisce la dialog di progresso
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 IMPORT_DIALOG_AVAILABLE and ImportProgressDialog is not None:
# Chiudi una dialog di progresso precedente se esiste e è ancora viva
if self.progress_dialog and self.progress_dialog.winfo_exists():
try:
self.progress_dialog.destroy()
except tk.TclError:
module_logger.warning(
"Could not destroy previous progress dialog cleanly."
)
self.progress_dialog = ImportProgressDialog(
self.root, file_name=filepath
)
self.controller.import_aircraft_database_from_file_with_progress(
filepath, self.progress_dialog
)
else: # Fallback se ImportProgressDialog non è disponibile
module_logger.warning(
"ImportProgressDialog not available. Using basic status update for import."
)
if hasattr(
self.controller, "import_aircraft_database_from_file"
): # Fallback a vecchio metodo se esiste
self.controller.import_aircraft_database_from_file(filepath)
else: # Altrimenti, solo un messaggio di errore
self.show_error_message(
"Import Error", "Import progress UI is not available."
)
else:
module_logger.info("GUI: CSV import cancelled by user.")
# ... (TUTTI gli altri metodi di MainWindow, inclusi quelli per il layout e la gestione eventi,
# _delayed_initialization, _initialize_map_manager, _recreate_map_tools_content,
# _recreate_map_info_content, _create_selected_flight_details_content,
# show_info_message, update_selected_flight_details, _on_closing, etc.
# devono essere presenti qui come nelle versioni precedenti.
# Ho riportato quelli modificati o cruciali per questa iterazione.)
# Assicurati che tutti i metodi da _delayed_initialization a _on_track_length_change
# siano presenti qui, come ti ho fornito nella versione precedente completa di MainWindow.
# Per non rendere questa risposta eccessivamente lunga, li ometto qui, ma devono esserci.
# Ho incluso nuovamente qui sotto quelli modificati o aggiunti di recente.
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) # Imposta stato iniziale controlli
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):
# ... (come nella versione precedente completa)
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):
# ... (come nella versione precedente completa, con layout compatto)
parent_frame.columnconfigure(1, weight=0)
parent_frame.columnconfigure(3, weight=0)
info_row = 0
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)
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
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
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
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
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
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
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)
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):
# ... (come nella versione precedente completa, con layout compatto)
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:"),
("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))
value_label1 = ttk.Label(parent_frame, text="N/A", wraplength=130)
value_label1.grid(row=i, column=1, sticky=tk.W, pady=0, padx=(0, 10))
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)
)
value_label2 = ttk.Label(parent_frame, text="N/A", wraplength=130)
value_label2.grid(row=i, column=3, sticky=tk.W, pady=0, padx=(0, 0))
self.flight_detail_labels[key2] = value_label2
# Bottone per Full Details (da implementare la sua azione)
self.full_details_button = ttk.Button(
parent_frame,
text="Aircraft Full Details...",
command=self._show_full_flight_details_action,
state=tk.DISABLED,
)
self.full_details_button.grid(
row=max_rows, column=0, columnspan=4, pady=(10, 0), sticky="ew"
)
def _show_full_flight_details_action(self):
"""Handles the action for the 'Aircraft Full Details...' button."""
icao_to_show = None
if (
hasattr(self, "flight_detail_labels")
and "icao24" in self.flight_detail_labels
):
label_widget = self.flight_detail_labels["icao24"]
if label_widget.winfo_exists():
current_icao_text = label_widget.cget("text")
if current_icao_text != "N/A" and current_icao_text.strip():
icao_to_show = current_icao_text
if icao_to_show:
# --- VERIFICA QUESTA RIGA ---
if self.controller and hasattr(
self.controller, "request_and_show_full_flight_details"
):
module_logger.info(
f"Requesting full details window for ICAO: {icao_to_show}"
)
self.controller.request_and_show_full_flight_details(
icao_to_show
) # Chiamata corretta
# --- FINE VERIFICA ---
else:
module_logger.error(
"Controller or required method 'request_and_show_full_flight_details' not available."
)
self.show_error_message(
"Error", "Cannot open full details window (controller issue)."
)
else:
self.show_info_message(
"No Flight Selected",
"Please select a flight on the map first to see full details.",
)
module_logger.warning(
"Full details button clicked, but no flight ICAO found in details panel."
)
def update_selected_flight_details(self, flight_data: Optional[Dict[str, Any]]):
# ... (come nella versione precedente completa, con formattazione e gestione altitudine)
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. Cannot update details."
)
return
all_panel_keys = list(self.flight_detail_labels.keys())
for key in all_panel_keys:
label_widget = self.flight_detail_labels.get(key)
if label_widget and label_widget.winfo_exists():
label_widget.config(text="N/A") # Reset first
if flight_data:
# Special handling for altitude to show one primary value clearly
baro_alt_val = flight_data.get("baro_altitude_m")
geo_alt_val = flight_data.get("geo_altitude_m")
if (
"baro_altitude_m" in self.flight_detail_labels
and self.flight_detail_labels["baro_altitude_m"].winfo_exists()
):
alt_text = "N/A"
if baro_alt_val is not None:
alt_text = f"{baro_alt_val:.0f} m"
elif geo_alt_val is not None:
alt_text = (
f"{geo_alt_val:.0f} m (geo)" # Fallback a geo se baro non c'è
)
self.flight_detail_labels["baro_altitude_m"].config(text=alt_text)
if (
"geo_altitude_m" in self.flight_detail_labels
and self.flight_detail_labels["geo_altitude_m"].winfo_exists()
):
alt_geo_text = "N/A"
if geo_alt_val is not None:
alt_geo_text = f"{geo_alt_val:.0f} m"
# Mostra geo solo se è diverso da baro o se baro non c'è (o per confronto)
# Se baro_altitude_m è il campo primario per "Altitude:", questo può essere solo "(geo)"
# Se baro_alt_val è None e geo_alt_val c'è, baro_altitude_m mostrerà già geo.
# Quindi, questa riga aggiorna specificamente il campo "Altitude (Geo):"
if (
baro_alt_val is not None
and geo_alt_val is not None
and abs(baro_alt_val - geo_alt_val) > 1
): # Mostra se significativamente diverso
self.flight_detail_labels["geo_altitude_m"].config(
text=alt_geo_text
)
elif (
baro_alt_val is None and geo_alt_val is not None
): # Se baro non c'era, geo è già nel campo baro, qui specifichiamo geo
self.flight_detail_labels["geo_altitude_m"].config(
text=alt_geo_text
)
# else: lascialo N/A se uguale a baro o se baro è già geo
for key, label_widget in self.flight_detail_labels.items():
if key in ["baro_altitude_m", "geo_altitude_m"]:
continue # Già gestiti
if label_widget and label_widget.winfo_exists():
value = flight_data.get(key)
formatted_value = "N/A"
if value is not None and str(value).strip() != "":
if key == "velocity_mps" and isinstance(value, (float, int)):
formatted_value = (
f"{value:.1f} m/s ({value * 1.94384:.1f} kts)"
)
elif key == "vertical_rate_mps" and isinstance(
value, (float, int)
):
formatted_value = (
f"{value * 196.85:.0f} ft/min ({value:.1f} m/s)"
)
elif key == "true_track_deg" and isinstance(
value, (float, int)
):
formatted_value = f"{value:.1f}°"
elif key in [
"timestamp",
"last_contact_timestamp",
"firstflightdate",
"timestamp_metadata",
]:
if isinstance(value, (int, float)) and value > 0:
try:
formatted_value = datetime.fromtimestamp(
value, tz=timezone.utc
).strftime("%Y-%m-%d %H:%M:%S Z")
except:
formatted_value = str(value) + " (raw ts)"
elif isinstance(value, str) and value.strip():
formatted_value = value
elif isinstance(value, bool):
formatted_value = str(value)
elif key == "built_year" and value:
formatted_value = (
str(int(value))
if isinstance(value, (float, int)) and value > 0
else str(value)
)
else:
formatted_value = str(value)
label_widget.config(text=formatted_value)
if (
hasattr(self, "full_details_button")
and self.full_details_button.winfo_exists()
):
self.full_details_button.config(
state=tk.NORMAL if flight_data.get("icao24") else tk.DISABLED
)
else: # flight_data is None
if (
hasattr(self, "full_details_button")
and self.full_details_button.winfo_exists()
):
self.full_details_button.config(state=tk.DISABLED)
module_logger.debug("Selected flight details panel updated.")
# Tutti gli altri metodi di MainWindow (_on_closing, _reset_gui_to_stopped_state, etc.)
# dovrebbero essere inclusi qui come nelle versioni precedenti.
# Per brevità, non li ripeto TUTTI, ma è cruciale che il tuo file sia completo.
# Ho già incluso quelli modificati o aggiunti più di recente.
# Verifica la completezza rispetto alla tua ultima versione funzionante.
# In particolare, i metodi per la gestione dei tab, start/stop monitoring,
# gestione bbox, zoom/pan mappa, aggiornamento info generali mappa.
def update_semaphore_and_status(self, status_level: str, message: str):
# ... (come prima)
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):
# ... (come prima)
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."
):
# ... (come prima)
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:
# ... (come prima)
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):
# ... (come prima)
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):
# ... (come prima)
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):
# ... (come prima)
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):
# ... (come prima)
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):
# ... (come prima)
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):
# ... (come prima)
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):
# ... (come prima)
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):
# ... (come prima)
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]]:
# ... (come prima)
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]):
# ... (come prima)
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]],
):
# ... (come prima)
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):
# ... (come prima)
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):
# ... (come prima)
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
):
# ... (come prima)
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,
):
# ... (come prima)
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],
):
# ... (come prima, con layout compatto)
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:
# ... (come prima)
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):
# ... (come prima)
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):
# ... (come prima)
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):
# ... (come prima)
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):
# ... (come prima)
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):
# ... (come prima)
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)