1843 lines
80 KiB
Python
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)
|