SXXXXXXX_FlightMonitor/flightmonitor/gui/main_window.py

1843 lines
80 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.scrolledtext import ScrolledText
from tkinter import font as tkFont
from typing import List, Dict, Optional, Tuple, Any
import logging # Import per i livelli di logging (es. logging.ERROR)
# Relative imports
from ..data import config as app_config # Standardized alias
from ..utils.logger import get_logger, setup_logging, shutdown_logging_system
from ..data.common_models import CanonicalFlightState
# Importa le costanti di stato GUI dal modulo utils centralizzato
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
MAP_CANVAS_MANAGER_AVAILABLE = False
# Usiamo un logger temporaneo se module_logger non è ancora inizializzato
# o semplicemente print per errori critici di avvio.
print(
f"CRITICAL ERROR in MainWindow import: Failed to import MapCanvasManager or map_utils: {e_map_import}. Map functionality will be disabled."
)
# Questo logger viene inizializzato dopo la configurazione del logging
module_logger = get_logger(__name__)
# --- Constants for Semaphore (dimensioni fisiche) ---
SEMAPHORE_SIZE = 12
SEMAPHORE_PAD = 3
SEMAPHORE_BORDER_WIDTH = 1
SEMAPHORE_TOTAL_SIZE = SEMAPHORE_SIZE + 2 * (SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH)
FALLBACK_CANVAS_WIDTH = 800
FALLBACK_CANVAS_HEIGHT = 600
BBOX_COLOR_INSIDE = "green4"
BBOX_COLOR_OUTSIDE = "red2"
BBOX_COLOR_PARTIAL = "darkorange"
BBOX_COLOR_NA = "gray50"
DEFAULT_TRACK_LENGTH = 20 # Valore di default per la lunghezza della traccia
class MainWindow:
"""
Main window of the Flight Monitor application.
Handles layout with function and view notebooks, interactions, status, and logging.
"""
def __init__(self, root: tk.Tk, controller: Any): # app_controller.AppController
self.root = root
self.controller = controller
self.root.title("Flight Monitor")
if app_config.LAYOUT_START_MAXIMIZED:
try:
self.root.state("zoomed") # Windows/ alcuni Linux
module_logger.info(
"Attempted to start window maximized using 'zoomed' state."
)
except tk.TclError: # Fallback per altri sistemi (es. macOS, alcuni Linux)
try:
screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight()
self.root.geometry(f"{screen_width}x{screen_height}+0+0")
module_logger.info(
f"Started window maximized using screen dimensions: {screen_width}x{screen_height}."
)
except tk.TclError as e_geom:
module_logger.warning(
f"Could not get screen dimensions or set geometry for maximization: {e_geom}. Using default Tkinter size."
)
min_win_w = getattr(app_config, "LAYOUT_WINDOW_MIN_WIDTH", 700)
min_win_h = getattr(app_config, "LAYOUT_WINDOW_MIN_HEIGHT", 500)
try:
self.root.minsize(min_win_w, min_win_h)
module_logger.info(f"Minimum window size set to {min_win_w}x{min_win_h}.")
except tk.TclError as e_minsize:
module_logger.warning(
f"Could not set minsize from config: {e_minsize}. Using Tkinter defaults."
)
self.canvas_width = getattr(
app_config, "DEFAULT_CANVAS_WIDTH", FALLBACK_CANVAS_WIDTH
)
self.canvas_height = getattr(
app_config, "DEFAULT_CANVAS_HEIGHT", FALLBACK_CANVAS_HEIGHT
)
# --- Layout Principale Orizzontale ---
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
)
# --- Colonna Sinistra ---
self.left_column_frame = ttk.Frame(self.main_horizontal_paned_window)
# MODIFIED: Removed 'minsize' option as it's invalid for PanedWindow.add()
# WHY: Causes TclError: unknown option "-minsize".
# HOW: Deleted the minsize parameter.
self.main_horizontal_paned_window.add(
self.left_column_frame,
weight=app_config.LAYOUT_MAIN_HORIZONTAL_WEIGHTS.get("left_column", 20),
)
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)
# Area Function Notebook (sopra a sinistra)
self.function_notebook_frame = ttk.Frame(self.left_vertical_paned_window)
# MODIFIED: Removed 'minsize' option.
self.left_vertical_paned_window.add(
self.function_notebook_frame,
weight=app_config.LAYOUT_LEFT_VERTICAL_WEIGHTS.get("function_notebook", 70),
)
self.function_notebook = ttk.Notebook(self.function_notebook_frame)
self.function_notebook.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
# Tab "Live: Area Monitor"
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")
# Contenitore per controlli BBox e opzioni traccia nel tab Live Area
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))
# Frame per i controlli Start/Stop e BBox (dentro live_controls_options_frame)
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)
# Altri Tab del Function Notebook
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
)
# Area Log e Status (sotto a sinistra)
self.log_status_area_frame = ttk.Frame(self.left_vertical_paned_window)
# MODIFIED: Removed 'minsize' option.
self.left_vertical_paned_window.add(
self.log_status_area_frame,
weight=app_config.LAYOUT_LEFT_VERTICAL_WEIGHTS.get("log_status_area", 30),
)
# Status Bar
self.status_bar_frame = ttk.Frame(self.log_status_area_frame, padding=(5, 3))
self.status_bar_frame.pack(side=tk.TOP, fill=tk.X, pady=(0, 5))
self.semaphore_canvas = tk.Canvas(
self.status_bar_frame,
width=SEMAPHORE_TOTAL_SIZE,
height=SEMAPHORE_TOTAL_SIZE,
bg=self.root.cget("bg"),
highlightthickness=0,
)
self.semaphore_canvas.pack(side=tk.LEFT, padx=(0, 5))
x0, y0 = (
SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH,
SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH,
)
x1, y1 = x0 + SEMAPHORE_SIZE, y0 + SEMAPHORE_SIZE
self._semaphore_oval_id = self.semaphore_canvas.create_oval(
x0,
y0,
x1,
y1,
fill=SEMAPHORE_COLOR_STATUS_MAP.get(GUI_STATUS_UNKNOWN, "gray70"),
outline="gray30",
width=SEMAPHORE_BORDER_WIDTH,
)
self.status_label = ttk.Label(
self.status_bar_frame, text="Status: Initializing..."
)
self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 2))
# Log Widget
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=8,
wrap=tk.WORD,
font=(log_font_family, 9),
relief=tk.SUNKEN,
borderwidth=1,
)
self.log_text_widget.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
# --- Colonna Destra ---
self.right_column_frame = ttk.Frame(self.main_horizontal_paned_window)
# MODIFIED: Removed 'minsize' option.
self.main_horizontal_paned_window.add(
self.right_column_frame,
weight=app_config.LAYOUT_MAIN_HORIZONTAL_WEIGHTS.get("right_column", 80),
)
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)
# Area Views Notebook (sopra a destra)
self.views_notebook_outer_frame = ttk.Frame(self.right_vertical_paned_window)
# MODIFIED: Removed 'minsize' option.
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))
# Tab Map View
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=100,
height=100,
highlightthickness=0,
)
self.flight_canvas.pack(fill=tk.BOTH, expand=True)
self.map_manager_instance: Optional[MapCanvasManager] = None
# Tab Table View
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)
# Area Tools e Info Mappa (sotto a destra)
self.map_tools_info_area_frame = ttk.Frame(
self.right_vertical_paned_window, padding=(0, 5, 0, 0)
)
# MODIFIED: Removed 'minsize' option.
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.map_tools_info_horizontal_paned = ttk.PanedWindow(
self.map_tools_info_area_frame, orient=tk.HORIZONTAL
)
self.map_tools_info_horizontal_paned.pack(fill=tk.BOTH, expand=True)
# Frame Map Tools
self.map_tool_frame = ttk.LabelFrame(
self.map_tools_info_horizontal_paned, text="Map Tools", padding=5
)
# MODIFIED: Removed 'minsize' option.
self.map_tools_info_horizontal_paned.add(
self.map_tool_frame,
weight=app_config.LAYOUT_TOOLS_INFO_HORIZONTAL_WEIGHTS.get("map_tools", 20),
)
controls_map_container = ttk.Frame(self.map_tool_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(1, 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)
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
)
# Frame Map Info Panel
self.map_info_panel_frame = ttk.LabelFrame(
self.map_tools_info_horizontal_paned, text="Map Information", padding=10
)
# MODIFIED: Removed 'minsize' option.
self.map_tools_info_horizontal_paned.add(
self.map_info_panel_frame,
weight=app_config.LAYOUT_TOOLS_INFO_HORIZONTAL_WEIGHTS.get("map_info", 80),
)
self.map_info_panel_frame.columnconfigure(1, weight=1)
self.map_info_panel_frame.columnconfigure(3, weight=1)
info_row = 0
ttk.Label(self.map_info_panel_frame, text="Click Lat:").grid(
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
)
self.info_lat_value = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_lat_value.grid(row=info_row, column=1, sticky=tk.W, pady=1)
ttk.Label(self.map_info_panel_frame, text="Click Lon:").grid(
row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2)
)
self.info_lon_value = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_lon_value.grid(row=info_row, column=3, sticky=tk.W, pady=1)
info_row += 1
ttk.Label(self.map_info_panel_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(self.map_info_panel_frame, text="N/A")
self.info_lat_dms_value.grid(row=info_row, column=1, sticky=tk.W, pady=1)
ttk.Label(self.map_info_panel_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(self.map_info_panel_frame, text="N/A")
self.info_lon_dms_value.grid(row=info_row, column=3, sticky=tk.W, pady=1)
info_row += 1
ttk.Separator(self.map_info_panel_frame, orient=tk.HORIZONTAL).grid(
row=info_row, column=0, columnspan=4, sticky=tk.EW, pady=3
)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="Map Bounds:").grid(
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
)
ttk.Label(self.map_info_panel_frame, text="W:").grid(
row=info_row, column=1, sticky=tk.E, pady=1, padx=(0, 2)
)
self.info_map_bounds_w = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_map_bounds_w.grid(
row=info_row, column=2, columnspan=2, sticky=tk.W, pady=1, padx=(0, 5)
)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="").grid(row=info_row, column=0)
ttk.Label(self.map_info_panel_frame, text="S:").grid(
row=info_row, column=1, sticky=tk.E, pady=1, padx=(0, 2)
)
self.info_map_bounds_s = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_map_bounds_s.grid(
row=info_row, column=2, columnspan=2, sticky=tk.W, pady=1, padx=(0, 5)
)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="").grid(row=info_row, column=0)
ttk.Label(self.map_info_panel_frame, text="E:").grid(
row=info_row, column=1, sticky=tk.E, pady=1, padx=(0, 2)
)
self.info_map_bounds_e = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_map_bounds_e.grid(
row=info_row, column=2, columnspan=2, sticky=tk.W, pady=1, padx=(0, 5)
)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="").grid(row=info_row, column=0)
ttk.Label(self.map_info_panel_frame, text="N:").grid(
row=info_row, column=1, sticky=tk.E, pady=1, padx=(0, 2)
)
self.info_map_bounds_n = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_map_bounds_n.grid(
row=info_row, column=2, columnspan=2, sticky=tk.W, pady=1, padx=(0, 5)
)
info_row += 1
ttk.Separator(self.map_info_panel_frame, orient=tk.HORIZONTAL).grid(
row=info_row, column=0, columnspan=4, sticky=tk.EW, pady=3
)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="Target BBox:").grid(
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
)
ttk.Label(self.map_info_panel_frame, text="W:").grid(
row=info_row, column=1, sticky=tk.E, pady=1, padx=(0, 2)
)
self.info_target_bbox_w = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_target_bbox_w.grid(
row=info_row, column=2, columnspan=2, sticky=tk.W, pady=1, padx=(0, 5)
)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="").grid(row=info_row, column=0)
ttk.Label(self.map_info_panel_frame, text="S:").grid(
row=info_row, column=1, sticky=tk.E, pady=1, padx=(0, 2)
)
self.info_target_bbox_s = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_target_bbox_s.grid(
row=info_row, column=2, columnspan=2, sticky=tk.W, pady=1, padx=(0, 5)
)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="").grid(row=info_row, column=0)
ttk.Label(self.map_info_panel_frame, text="E:").grid(
row=info_row, column=1, sticky=tk.E, pady=1, padx=(0, 2)
)
self.info_target_bbox_e = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_target_bbox_e.grid(
row=info_row, column=2, columnspan=2, sticky=tk.W, pady=1, padx=(0, 5)
)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="").grid(row=info_row, column=0)
ttk.Label(self.map_info_panel_frame, text="N:").grid(
row=info_row, column=1, sticky=tk.E, pady=1, padx=(0, 2)
)
self.info_target_bbox_n = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_target_bbox_n.grid(
row=info_row, column=2, columnspan=2, sticky=tk.W, pady=1, padx=(0, 5)
)
info_row += 1
ttk.Separator(self.map_info_panel_frame, orient=tk.HORIZONTAL).grid(
row=info_row, column=0, columnspan=4, sticky=tk.EW, pady=3
)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="Map Zoom:").grid(
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
)
self.info_zoom_value = ttk.Label(self.map_info_panel_frame, text="N/A", width=8)
self.info_zoom_value.grid(row=info_row, column=1, sticky=tk.W, pady=1)
ttk.Label(self.map_info_panel_frame, text="Map Size:").grid(
row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2)
)
self.info_map_size_value = ttk.Label(
self.map_info_panel_frame, text="N/A", width=18, wraplength=140
)
self.info_map_size_value.grid(row=info_row, column=3, sticky=tk.W, pady=1)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="Flights:").grid(
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
)
self.info_flight_count_value = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_flight_count_value.grid(row=info_row, column=1, sticky=tk.W, pady=1)
# --- Setup Finale ---
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 _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(50, 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)."
),
)
# MODIFICATO: Inizializza la lunghezza della traccia nel controller all'avvio
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)
else:
module_logger.warning(
f"Delayed init: Invalid initial track length from var: {initial_track_len}. Using default in MapManager."
)
except tk.TclError:
module_logger.error(
"Delayed init: TclError getting initial track length. Using default in MapManager."
)
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 lo stato iniziale dei tab e controlli
self.update_semaphore_and_status(GUI_STATUS_OK, "System Initialized. Ready.")
module_logger.info(
"MainWindow fully initialized and displayed after delayed setup."
)
def _initialize_map_manager(self, initial_bbox_for_map: Dict[str, float]):
# ... (come prima)
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 # Usa fallback se non ancora disegnato
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,
)
# Dopo aver inizializzato il map_manager, invia la lunghezza traccia iniziale se non già fatto
# Questo è ridondante se _delayed_initialization lo fa già, ma sicuro.
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:
pass # ignora errori qui, dovrebbe essere già stato impostato
if self.controller and hasattr(
self.controller, "update_general_map_info"
):
self.controller.update_general_map_info()
except ImportError as e_imp:
module_logger.critical(
f"Failed to initialize MapCanvasManager (ImportError): {e_imp}",
exc_info=True,
)
self.show_error_message(
"Map Error",
f"Map libraries missing for manager: {e_imp}. Map disabled.",
)
self._update_map_placeholder(f"Map Error: Libraries missing.\n{e_imp}")
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(200, self._initialize_map_manager, initial_bbox_for_map)
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 # Widget potrebbe essere stato distrutto
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 # Default
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 # Per non affollare log INFO
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: # Se root non esiste, forziamo la chiusura
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.") # Messaggio aggiornato
try:
# MODIFIED: Changed shutdown_gui_logging() to shutdown_logging_system()
# WHY: The function name was changed in the logger.py module.
# HOW: Updated the function call.
shutdown_logging_system() # Assicurati che questa funzione sia robusta
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()
module_logger.info("Application window has been destroyed.")
except tk.TclError as e: # Può accadere se distrutta altrove
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 per bottoni start/stop e radio) ...
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)
# Stato bbox entries e track length spinbox dipende dalla modalità e tab attivi
self._update_controls_state_based_on_mode_and_tab() # Richiama helper per coerenza
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: # Finestra potrebbe essere in chiusura
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()
): # Se la mappa è attiva, non mostrare placeholder
try:
self.flight_canvas.delete("placeholder_text")
except tk.TclError:
pass # Ignora se non esiste
except Exception as e:
module_logger.warning(
f"Error deleting placeholder: {e}", exc_info=False
)
return
try:
self.flight_canvas.delete(
"placeholder_text"
) # Cancella precedente, se esiste
module_logger.debug(f"MainWindow updating placeholder: '{text_to_display}'")
canvas_w, canvas_h = (
self.flight_canvas.winfo_width(),
self.flight_canvas.winfo_height(),
)
if canvas_w <= 1:
canvas_w = self.canvas_width # Fallback se non ancora disegnato
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: # Canvas potrebbe essere stato distrutto
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, ma ora chiama _update_controls_state_based_on_mode_and_tab) ...
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:
# Logica per abilitare/disabilitare i tab del function_notebook
# ... (come prima) ...
tab_indices = {}
for i in range(self.function_notebook.index("end")):
current_tab_widget_name_in_notebook = self.function_notebook.tabs()[i]
actual_widget_controlled_by_tab = self.function_notebook.nametowidget(
current_tab_widget_name_in_notebook
)
if (
hasattr(self, "live_bbox_tab_frame")
and actual_widget_controlled_by_tab == self.live_bbox_tab_frame
):
tab_indices["LiveArea"] = i
elif (
hasattr(self, "history_tab_frame")
and actual_widget_controlled_by_tab == self.history_tab_frame
):
tab_indices["History"] = i
elif (
hasattr(self, "live_airport_tab_frame")
and actual_widget_controlled_by_tab == 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")
# Se il tab history era selezionato, passa al tab live area (se esiste)
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")
# Se un tab live era selezionato, passa al tab history (se esiste)
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() # Pulisce la mappa e altre viste
self._update_controls_state_based_on_mode_and_tab() # Aggiorna stato controlli BBox e traccia
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, ma ora chiama _update_controls_state_based_on_mode_and_tab) ...
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." # Default
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() # Aggiorna stato controlli
except (tk.TclError, ValueError) as e: # Es. se il notebook non ha tab
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
)
# NUOVO METODO HELPER per centralizzare la logica di abilitazione/disabilitazione controlli
def _update_controls_state_based_on_mode_and_tab(self):
"""
Updates the state of BBox entries and Track Length spinbox based on
the current mode and selected function tab.
"""
is_live_mode = hasattr(self, "mode_var") and self.mode_var.get() == "Live"
is_monitoring_active = (
hasattr(self, "stop_button") 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 # Tab potrebbe non essere ancora selezionabile o notebook vuoto
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
) # Può essere cambiato anche durante il monitoraggio live
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:
# Lo spinbox è disabilitato solo se il monitoraggio è attivo E NON siamo nel tab Live Area.
# O, più semplicemente, disabilitato se il monitoraggio è attivo, riabilitato se fermo e in tab corretto.
# Se il monitoraggio è attivo, la lunghezza traccia NON dovrebbe essere cambiabile
# per evitare inconsistenze immediate con le code/deques nel map_manager.
# Quindi: abilitato se non stiamo monitorando E siamo nel tab giusto.
# O, se vogliamo permettere di cambiarlo live: lo stato dipende solo da is_monitoring_active.
# Per ora, lo disabilitiamo durante il monitoraggio per semplicità.
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 # Widget potrebbe non esistere più
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): # Può accadere se il notebook è vuoto
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 = [
getattr(self, n, None)
for n in [
"lat_min_entry",
"lon_min_entry",
"lat_max_entry",
"lon_max_entry",
]
]
if all(e and e.winfo_exists() for e in entries):
try:
for entry in entries:
entry.config(state=state)
# module_logger.debug(f"BBox entries state set to: {state}") # Loggato da _update_controls_state...
except tk.TclError:
module_logger.warning(
"TclError setting BBox entries state (widgets gone)."
)
# else: module_logger.debug("One or more BBox entries not available to set state.") # Troppo verboso
def _start_monitoring(self):
# ... (come prima, ma ora chiama _update_controls_state_based_on_mode_and_tab)
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() # Aggiorna stato di BBox e Track Length
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:
module_logger.debug(f"Valid BBox for live: {bbox}")
self.controller.start_live_monitoring(bbox)
else: # get_bounding_box_from_gui mostra già l'errore
self._reset_gui_to_stopped_state("Start failed: Invalid BBox.")
else: # Non dovrebbe succedere se i tab sono disabilitati correttamente
module_logger.warning(
f"Start in Live mode, but tab '{active_func_tab_text}' is not 'Live: Area Monitor'. This indicates a 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() # Placeholder
module_logger.info("GUI: History monitoring started (placeholder).")
else: # Non dovrebbe succedere
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, _reset_gui_to_stopped_state si occuperà di riabilitare i controlli)
module_logger.info("GUI: User requested to stop monitoring.")
selected_mode = self.mode_var.get() # Assumiamo mode_var esista
if not self.controller:
module_logger.error("Controller N/A to stop.")
self._reset_gui_to_stopped_state(
"Error: Controller missing."
) # Tenta comunque il reset
return
if selected_mode == "Live":
self.controller.stop_live_monitoring()
elif selected_mode == "History":
self.controller.stop_history_monitoring() # Placeholder
# _reset_gui_to_stopped_state verrà chiamato dal controller (o dal suo flusso)
# quando il monitoring è effettivamente fermo.
# Qui aggiorniamo solo lo stato temporaneo.
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 = ["lat_min_var", "lon_min_var", "lat_max_var", "lon_max_var"]
if not all(hasattr(self, v) for v in req_vars):
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).get() for v in req_vars]
if not all(
s.strip() for s in vals_str
): # Controlla che nessuna stringa sia vuota o solo spazi
module_logger.error("One or more BBox fields are empty.")
self.show_error_message(
"Input Error", "All Bounding Box fields are required."
)
return None
# Conversione a float
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: # Catch-all per altri errori imprevisti
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
): # _is_valid_bbox_dict dovrebbe loggare i dettagli
self.show_error_message(
"Input Error",
"Invalid Bounding Box range or order. Please ensure Lat Min < Lat Max, and Lon Min < Lon Max, and values are within valid geographic limits.",
)
return None
module_logger.debug(f"Validated BBox from GUI: {bbox_dict}")
return bbox_dict
def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]):
# ... (come prima)
if not hasattr(
self, "lat_min_var"
): # Controlla uno degli attributi per vedere se i widget sono pronti
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
) # Usa costante da config
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: # Widget potrebbero essere stati distrutti
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: # Prova a resettare i campi a N/A
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 # Ignora se i widget sono spariti
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()
): # Solo se la root esiste ancora
self._update_map_placeholder("Map N/A to display flights.")
return
try:
self.map_manager_instance.update_flights_on_map(
flight_states
) # Questo ora gestirà le tracce internamente
# Se la lista è vuota e la mappa dovrebbe essere un placeholder, aggiorna il placeholder.
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: {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.update_flights_on_map(
[]
) # Passa lista vuota per pulire voli e tracce
except Exception as e:
module_logger.warning(f"Error clearing flights from map manager: {e}")
# Aggiorna placeholder se la mappa non è attiva/pronta
if 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)
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 ''}" # Messaggio breve per status bar
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: # Finestra potrebbe essere in chiusura
module_logger.warning(
f"TclError showing error messagebox '{title}'. Root window might be gone."
)
else: # Se la root non esiste, logga solo su console
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,
)
elif self.controller: # Fallback se MapCanvasManager non ha il suo menu
root_widget = (
self.root
if hasattr(self, "root") and self.root.winfo_exists()
else None
)
if not root_widget:
module_logger.warning(
"Cannot show map context menu: root window not available."
)
return
try:
menu = tk.Menu(root_widget, tearoff=0)
decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5)
menu.add_command(
label=f"Point: {latitude:.{decimals}f}, {longitude:.{decimals}f}",
state=tk.DISABLED,
)
menu.add_separator()
if hasattr(self.controller, "recenter_map_at_coords"):
menu.add_command(
label="Center map here",
command=lambda: self.controller.recenter_map_at_coords(
latitude, longitude
),
)
else:
menu.add_command(label="Center map here (N/A)", state=tk.DISABLED)
if hasattr(self.controller, "set_bbox_around_coords"):
area_km_cfg_name = "DEFAULT_CLICK_AREA_SIZE_KM" # Nome della costante nel controller
area_km = getattr(
self.controller, area_km_cfg_name, 50.0
) # Usa getattr per fallback
menu.add_command(
label=f"Set {area_km:.0f}km Mon. Area",
command=lambda: self.controller.set_bbox_around_coords(
latitude, longitude, area_km
),
)
else:
menu.add_command(label="Set Mon. Area (N/A)", state=tk.DISABLED)
menu.add_command(
label="Get elevation (TBD)", state=tk.DISABLED
) # Esempio per future aggiunte
menu.tk_popup(screen_x, screen_y)
except (
tk.TclError
) as e: # Può accadere se la finestra viene chiusa mentre il menu è attivo
module_logger.warning(f"TclError showing map context menu: {e}.")
except Exception as e:
module_logger.error(
f"Error showing map context menu: {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"): # Controlla uno degli attributi
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: # Widget potrebbero non esistere più
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]], # W,S,E,N
target_bbox_input: Optional[Dict[str, float]], # lat_min, lon_min, ...
flight_count: Optional[int],
):
# ... (come prima)
if not hasattr(self, "info_zoom_value"): # Controlla uno degli attributi
module_logger.warning(
"Map info panel (general info) widgets not available for update."
)
return
decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5)
try:
# Map Bounds
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)
# Target BBox (usato per il monitoring, disegnato in blu)
tw, ts, te, tn = "N/A", "N/A", "N/A", "N/A"
color_target_bbox_text = BBOX_COLOR_NA # Grigio se N/A
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: # Solo se abbiamo i limiti della mappa corrente
status_target_bbox = self._is_bbox_inside_bbox(
target_bbox_input, map_geo_bounds
) # W,S,E,N
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 # Anche se N/A per map_geo_bounds, lo coloriamo outside
bbox_labels_target = [
getattr(self, n, None)
for n in [
"info_target_bbox_w",
"info_target_bbox_s",
"info_target_bbox_e",
"info_target_bbox_n",
]
]
for label, val_str in zip(bbox_labels_target, [tw, ts, te, tn]):
if label and label.winfo_exists():
label.config(text=val_str, foreground=color_target_bbox_text)
# Altre Info
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" # Inner deve essere valido
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 deve essere una tupla valida di numeri
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],
}
# Per outer_bbox_tuple (limiti mappa), lon_min può essere > lon_max se attraversa l'antimeridiano.
# La logica di _is_valid_bbox_dict fallirebbe. Dobbiamo gestirlo.
# Questa funzione è solo per colorare il testo, non per logica di fetch precisa.
# Per ora, assumiamo che i limiti della mappa siano "normalizzati" o la logica di confronto li gestisca.
# La logica di OpenSky non supporta BBox che attraversano l'antimeridiano (lon_min > lon_max).
# Quindi, per il "target_bbox_input" _is_valid_bbox_dict è corretto.
# Per "map_geo_bounds", può accadere, ma il confronto semplice qui sotto potrebbe dare risultati strani.
# Per un semplice confronto visivo, se la mappa attraversa l'antimeridiano, questo confronto è difficile.
# Semplifichiamo assumendo che map_geo_bounds sia per lo più "normale" o che il target BBox sia piccolo.
eps = 1e-6 # Tolleranza per confronti float
# Verifica se inner è completamente contenuto in outer
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"
# Verifica se non c'è sovrapposizione (inner è completamente fuori outer)
no_overlap = (
inner_bbox_dict["lon_max"]
<= outer_dict_temp["lon_min"] + eps # inner a sinistra di outer
or inner_bbox_dict["lon_min"]
>= outer_dict_temp["lon_max"] - eps # inner a destra di outer
or inner_bbox_dict["lat_max"]
<= outer_dict_temp["lat_min"] + eps # inner sotto outer
or inner_bbox_dict["lat_min"]
>= outer_dict_temp["lat_max"] - eps # inner sopra outer
)
if no_overlap:
return "Outside"
return "Partial" # Altrimenti c'è sovrapposizione parziale
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 method not available.")
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 method not available.")
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 method not available."
)
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 = self.center_lat_var.get()
lon_str = self.center_lon_var.get()
patch_str = 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 = float(lat_str)
lon = float(lon_str)
patch_size_km = 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 method not available."
)
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}")
# NUOVO METODO CALLBACK per lo Spinbox della lunghezza traccia
def _on_track_length_change(self):
"""
Called when the track length spinbox value changes.
Notifies the controller.
"""
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()
# Lo Spinbox dovrebbe già validare il range, ma un controllo extra non fa male
if not (2 <= new_length <= 100): # Usa i limiti dello spinbox
module_logger.warning(
f"Track length value {new_length} out of spinbox range. Ignoring change."
)
# Potremmo resettare il valore a uno valido o mostrare un errore
# self.track_length_var.set(max(2, min(new_length, 100))) # Corregge
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)