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

1609 lines
67 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."
)
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."
)
# Import per FullFlightDetailsWindow (verrà usato da AppController per creare la finestra)
# Non è strettamente necessario importarlo qui se MainWindow non lo istanzia direttamente,
# ma può essere utile per il type hinting se passiamo riferimenti.
# Per ora, lo lasciamo commentato qui, dato che AppController lo importerà localmente al bisogno.
# try:
# from .dialogs.full_flight_details_window import FullFlightDetailsWindow
# FULL_DETAILS_WINDOW_AVAILABLE = True
# except ImportError as e_details_dialog:
# FullFlightDetailsWindow = None # type: ignore
# FULL_DETAILS_WINDOW_AVAILABLE = False
# print(f"WARNING in MainWindow import: Failed to import FullFlightDetailsWindow: {e_details_dialog}. Full details window might not work.")
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)
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
)
if app_config.LAYOUT_START_MAXIMIZED:
try:
self.root.state("zoomed")
except tk.TclError:
try:
self.root.geometry(
f"{self.root.winfo_screenwidth()}x{self.root.winfo_screenheight()}+0+0"
)
except tk.TclError:
pass
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):
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 IMPORT_DIALOG_AVAILABLE and ImportProgressDialog is not None:
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:
module_logger.warning(
"ImportProgressDialog class not available. Using basic status update for import."
)
if hasattr(
self.controller, "import_aircraft_database_from_file"
): # Fallback
self.show_info_message(
"Import Started",
f"Starting import of {os.path.basename(filepath)}...\nThis might take a while. Check logs for completion.",
)
self.controller.import_aircraft_database_from_file(filepath) # type: ignore
else:
self.show_error_message(
"Import Error",
"Import progress UI is not available and no fallback import method found.",
)
else:
module_logger.info("GUI: CSV import cancelled by user.")
def _show_full_flight_details_action(self):
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:
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}"
)
# Il controller ora gestisce la creazione e la visualizzazione della finestra
self.controller.request_and_show_full_flight_details(icao_to_show)
else:
module_logger.error(
"Controller or 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."
)
# ... (Tutti gli altri metodi di MainWindow, come _delayed_initialization, _recreate_map_tools_content, etc.
# devono essere copiati qui. Per brevità, li ometto ma devono essere presenti.)
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.")
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:
self.controller.set_map_track_length(initial_track_len)
except Exception as e:
module_logger.error(f"Error setting initial track length: {e}")
self.root.after(10, self._on_mode_change)
module_logger.info("MainWindow fully initialized.")
def _initialize_map_manager(self, initial_bbox_for_map: Dict[str, float]):
if not MAP_CANVAS_MANAGER_AVAILABLE or MapCanvasManager is None:
self._update_map_placeholder("Map Error: Manager 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():
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:
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:
self.controller.set_map_track_length(
self.track_length_var.get()
)
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: Init failed.\n{e_init}")
if self.controller and hasattr(
self.controller, "update_general_map_info"
):
self.controller.update_general_map_info()
else:
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 prima)
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 prima)
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 prima)
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
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 update_selected_flight_details(self, flight_data: Optional[Dict[str, Any]]):
# ... (come prima)
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")
if flight_data:
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)"
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"
if (
baro_alt_val is not None
and geo_alt_val is not None
and abs(baro_alt_val - geo_alt_val) > 1
):
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:
self.flight_detail_labels["geo_altitude_m"].config(
text=alt_geo_text
)
for key, label_widget in self.flight_detail_labels.items():
if key in ["baro_altitude_m", "geo_altitude_m"]:
continue
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:
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.")
# ... Ripeto qui gli altri metodi per completezza, assicurati che siano tutti presenti nel tuo file ...
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 = (
messagebox.askokcancel(
"Quit", "Do you want to quit Flight Monitor?", parent=self.root
)
if self.root.winfo_exists()
else True
)
if user_confirmed_quit:
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,
)
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 Exception:
pass # Ignore errors on final destroy
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)
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()
):
return
if not self._should_show_main_placeholder():
try:
self.flight_canvas.delete("placeholder_text")
except:
pass
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,
)
except:
pass # Ignore TclErrors if canvas is gone
def _on_mode_change(self):
if not (
hasattr(self, "mode_var")
and hasattr(self, "function_notebook")
and self.function_notebook.winfo_exists()
):
return
selected_mode = self.mode_var.get()
status_message = f"Mode: {selected_mode}. Ready."
try:
tab_indices = {
name: i
for i, name_tuple in enumerate(
map(
lambda t: (self.function_notebook.tab(t, "text"), t),
self.function_notebook.tabs(),
)
)
for name in name_tuple
}
live_bbox_idx, history_idx, live_airport_idx = (
tab_indices.get("Live: Area Monitor", -1),
tab_indices.get("History", -1),
tab_indices.get("Live: Airport", -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")
if (
self.function_notebook.index("current") == 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")
if (
self.function_notebook.index("current") != history_idx
and history_idx != -1
):
self.function_notebook.select(history_idx)
except Exception as e:
module_logger.warning(f"Error updating func tabs: {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()
):
return
try:
tab_text = self.function_notebook.tab(
self.function_notebook.index("current"), "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. (TBD)"
elif "History" in tab_text:
placeholder_text_map = "Map - History Analysis. (TBD)"
self._update_map_placeholder(placeholder_text_map)
self._update_controls_state_based_on_mode_and_tab()
except:
pass
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:
pass
enable_bbox = (
is_live_mode
and "Live: Area Monitor" in active_func_tab_text
and not is_monitoring_active
)
enable_track = is_live_mode and "Live: Area Monitor" in active_func_tab_text
self._set_bbox_entries_state(tk.NORMAL if enable_bbox else tk.DISABLED)
if (
hasattr(self, "track_length_spinbox")
and self.track_length_spinbox.winfo_exists()
):
try:
self.track_length_spinbox.config(
state=(
tk.DISABLED
if is_monitoring_active
else ("readonly" if enable_track else tk.DISABLED)
)
)
except:
pass
def _on_view_tab_change(self, event: Optional[tk.Event] = None):
if not (hasattr(self, "views_notebook") and self.views_notebook.winfo_exists()):
return
try:
module_logger.info(
f"GUI: Switched view tab to: {self.views_notebook.tab(self.views_notebook.index('current'), 'text')}"
)
except:
pass
def _set_bbox_entries_state(self, state: str):
for name in [
"lat_min_entry",
"lon_min_entry",
"lat_max_entry",
"lon_max_entry",
]:
entry = getattr(self, name, None)
if entry and hasattr(entry, "winfo_exists") and entry.winfo_exists():
try:
entry.config(state=state)
except:
pass
def _start_monitoring(self):
if not hasattr(self, "mode_var"):
self.show_error_message("Internal Error", "App mode N/A.")
return
selected_mode = self.mode_var.get()
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:
pass
for btn_name, new_state in [
("start_button", tk.DISABLED),
("stop_button", tk.NORMAL),
("live_radio", tk.DISABLED),
("history_radio", tk.DISABLED),
]:
btn = getattr(self, btn_name, None)
if btn and hasattr(btn, "winfo_exists") and btn.winfo_exists():
btn.config(state=new_state)
self._update_controls_state_based_on_mode_and_tab()
if not self.controller:
self._reset_gui_to_stopped_state("Critical Error: Controller unavailable.")
self.show_error_message("Internal Error", "App controller missing.")
return
if selected_mode == "Live" and "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.")
elif selected_mode == "History" and "History" in active_func_tab_text:
self.controller.start_history_monitoring()
else:
self._reset_gui_to_stopped_state(
f"Start not supported on '{active_func_tab_text}'."
)
def _stop_monitoring(self):
selected_mode = self.mode_var.get() if hasattr(self, "mode_var") else "Unknown"
if not self.controller:
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)
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):
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):
return None
lat_min, lon_min, lat_max, lon_max = map(float, vals_str)
except:
return None
bbox_dict = {
"lat_min": lat_min,
"lon_min": lon_min,
"lat_max": lat_max,
"lon_max": lon_max,
}
return bbox_dict if _is_valid_bbox_dict(bbox_dict) else None
def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]):
# ... (come prima)
if not hasattr(self, "lat_min_var"):
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:
pass
else:
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:
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
):
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:
self.show_error_message(
"Map Display Error", "Could not update flights on map."
)
def clear_all_views_data(self):
# ... (come prima)
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)
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:
pass
else:
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)
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: {e}", exc_info=True
)
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"):
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:
pass
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)
if not hasattr(self, "info_zoom_value"):
return
decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5)
try:
mw, ms, me, mn = ("N/A",) * 4
if map_geo_bounds:
mw, ms, me, mn = (f"{c:.{decimals}f}" for c in map_geo_bounds)
for name, val in [
("info_map_bounds_w", mw),
("info_map_bounds_s", ms),
("info_map_bounds_e", me),
("info_map_bounds_n", mn),
]:
lbl = getattr(self, name, None)
if lbl and lbl.winfo_exists():
lbl.config(text=val)
tw, ts, te, tn = ("N/A",) * 4
color_target = BBOX_COLOR_NA
if target_bbox_input and _is_valid_bbox_dict(target_bbox_input):
tw, ts, te, tn = (
f"{target_bbox_input['lon_min']:.{decimals}f}",
f"{target_bbox_input['lat_min']:.{decimals}f}",
f"{target_bbox_input['lon_max']:.{decimals}f}",
f"{target_bbox_input['lat_max']:.{decimals}f}",
)
status_bbox = (
self._is_bbox_inside_bbox(target_bbox_input, map_geo_bounds)
if map_geo_bounds
else "Outside"
)
if status_bbox == "Inside":
color_target = BBOX_COLOR_INSIDE
elif status_bbox == "Partial":
color_target = BBOX_COLOR_PARTIAL
else:
color_target = BBOX_COLOR_OUTSIDE
for name, val in [
("info_target_bbox_w", tw),
("info_target_bbox_s", ts),
("info_target_bbox_e", te),
("info_target_bbox_n", tn),
]:
lbl = getattr(self, name, None)
if lbl and lbl.winfo_exists():
lbl.config(text=val, foreground=color_target)
for name, val_direct in [
("info_zoom_value", str(zoom) if zoom is not None else "N/A"),
("info_map_size_value", map_size_str or "N/A"),
(
"info_flight_count_value",
str(flight_count) if flight_count is not None else "N/A",
),
]:
lbl = getattr(self, name, None)
if lbl and lbl.winfo_exists():
lbl.config(text=val_direct)
except:
pass # Evita crash se i widget non ci sono
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) or 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 = {
"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
if (
inner_bbox_dict["lon_min"] >= outer["lon_min"] - eps
and inner_bbox_dict["lat_min"] >= outer["lat_min"] - eps
and inner_bbox_dict["lon_max"] <= outer["lon_max"] + eps
and inner_bbox_dict["lat_max"] <= outer["lat_max"] + eps
):
return "Inside"
if (
inner_bbox_dict["lon_max"] <= outer["lon_min"] + eps
or inner_bbox_dict["lon_min"] >= outer["lon_max"] - eps
or inner_bbox_dict["lat_max"] <= outer["lat_min"] + eps
or inner_bbox_dict["lat_min"] >= outer["lat_max"] - eps
):
return "Outside"
return "Partial"
def _map_zoom_in(self): # ... (come prima)
if self.controller and hasattr(self.controller, "map_zoom_in"):
self.controller.map_zoom_in()
def _map_zoom_out(self): # ... (come prima)
if self.controller and hasattr(self.controller, "map_zoom_out"):
self.controller.map_zoom_out()
def _map_pan(self, direction: str): # ... (come prima)
if self.controller and hasattr(self.controller, "map_pan_direction"):
self.controller.map_pan_direction(direction)
def _map_center_and_fit(self): # ... (come prima)
try:
lat, lon, patch = (
float(self.center_lat_var.get()),
float(self.center_lon_var.get()),
float(self.center_patch_size_var.get()),
)
if not (-90 <= lat <= 90 and -180 <= lon <= 180 and patch > 0):
self.show_error_message("Input Error", "Invalid lat/lon/patch.")
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)
except ValueError:
self.show_error_message("Input Error", "Lat/Lon/Patch must be numbers.")
except Exception as e:
self.show_error_message("Error", f"Unexpected error: {e}")
def _on_track_length_change(self): # ... (come prima)
if (
hasattr(self, "track_length_var")
and self.controller
and hasattr(self.controller, "set_map_track_length")
):
try:
self.controller.set_map_track_length(self.track_length_var.get())
except:
pass