1609 lines
67 KiB
Python
1609 lines
67 KiB
Python
# FlightMonitor/gui/main_window.py
|
|
"""
|
|
Main window of the Flight Monitor application.
|
|
Handles the layout with multiple notebooks for functions and views,
|
|
user interactions, status display including a semaphore, and logging.
|
|
"""
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
from tkinter import messagebox
|
|
from tkinter import filedialog
|
|
from tkinter import Menu
|
|
from tkinter.scrolledtext import ScrolledText
|
|
from tkinter import font as tkFont
|
|
from typing import List, Dict, Optional, Tuple, Any
|
|
import logging
|
|
import os
|
|
from datetime import datetime, timezone
|
|
import webbrowser
|
|
|
|
from ..data import config as app_config
|
|
from ..utils.logger import get_logger, setup_logging, shutdown_logging_system
|
|
from ..data.common_models import CanonicalFlightState
|
|
from ..utils.gui_utils import (
|
|
GUI_STATUS_OK,
|
|
GUI_STATUS_WARNING,
|
|
GUI_STATUS_ERROR,
|
|
GUI_STATUS_FETCHING,
|
|
GUI_STATUS_UNKNOWN,
|
|
GUI_STATUS_PERMANENT_FAILURE,
|
|
SEMAPHORE_COLOR_STATUS_MAP,
|
|
)
|
|
|
|
try:
|
|
from ..map.map_canvas_manager import MapCanvasManager
|
|
from ..map.map_utils import _is_valid_bbox_dict
|
|
|
|
MAP_CANVAS_MANAGER_AVAILABLE = True
|
|
except ImportError as e_map_import:
|
|
MapCanvasManager = None # type: ignore
|
|
_is_valid_bbox_dict = lambda x: False # type: ignore
|
|
MAP_CANVAS_MANAGER_AVAILABLE = False
|
|
print(
|
|
f"CRITICAL ERROR in MainWindow import: Failed to import MapCanvasManager or map_utils: {e_map_import}. Map functionality will be disabled."
|
|
)
|
|
|
|
try:
|
|
from .dialogs.import_progress_dialog import ImportProgressDialog
|
|
|
|
IMPORT_DIALOG_AVAILABLE = True
|
|
except ImportError as e_dialog_import:
|
|
ImportProgressDialog = None # type: ignore
|
|
IMPORT_DIALOG_AVAILABLE = False
|
|
print(
|
|
f"ERROR in MainWindow import: Failed to import ImportProgressDialog: {e_dialog_import}. Import progress UI will be basic."
|
|
)
|
|
|
|
# Import per FullFlightDetailsWindow (verrà usato da AppController per creare la finestra)
|
|
# Non è strettamente necessario importarlo qui se MainWindow non lo istanzia direttamente,
|
|
# ma può essere utile per il type hinting se passiamo riferimenti.
|
|
# Per ora, lo lasciamo commentato qui, dato che AppController lo importerà localmente al bisogno.
|
|
# try:
|
|
# from .dialogs.full_flight_details_window import FullFlightDetailsWindow
|
|
# FULL_DETAILS_WINDOW_AVAILABLE = True
|
|
# except ImportError as e_details_dialog:
|
|
# FullFlightDetailsWindow = None # type: ignore
|
|
# FULL_DETAILS_WINDOW_AVAILABLE = False
|
|
# print(f"WARNING in MainWindow import: Failed to import FullFlightDetailsWindow: {e_details_dialog}. Full details window might not work.")
|
|
|
|
|
|
module_logger = get_logger(__name__)
|
|
|
|
SEMAPHORE_SIZE = 12
|
|
SEMAPHORE_PAD = 3
|
|
SEMAPHORE_BORDER_WIDTH = 1
|
|
SEMAPHORE_TOTAL_SIZE = SEMAPHORE_SIZE + 2 * (SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH)
|
|
|
|
FALLBACK_CANVAS_WIDTH = getattr(app_config, "DEFAULT_CANVAS_WIDTH", 800)
|
|
FALLBACK_CANVAS_HEIGHT = getattr(app_config, "DEFAULT_CANVAS_HEIGHT", 600)
|
|
|
|
BBOX_COLOR_INSIDE = "green4"
|
|
BBOX_COLOR_OUTSIDE = "red2"
|
|
BBOX_COLOR_PARTIAL = "darkorange"
|
|
BBOX_COLOR_NA = "gray50"
|
|
|
|
DEFAULT_TRACK_LENGTH = getattr(app_config, "DEFAULT_TRACK_HISTORY_POINTS", 20)
|
|
|
|
|
|
class MainWindow:
|
|
def __init__(self, root: tk.Tk, controller: Any):
|
|
self.root = root
|
|
self.controller = controller
|
|
self.root.title("Flight Monitor")
|
|
self.progress_dialog: Optional[ImportProgressDialog] = None
|
|
self.full_flight_details_window: Optional[tk.Toplevel] = (
|
|
None # Sarà di tipo FullFlightDetailsWindow
|
|
)
|
|
|
|
if app_config.LAYOUT_START_MAXIMIZED:
|
|
try:
|
|
self.root.state("zoomed")
|
|
except tk.TclError:
|
|
try:
|
|
self.root.geometry(
|
|
f"{self.root.winfo_screenwidth()}x{self.root.winfo_screenheight()}+0+0"
|
|
)
|
|
except tk.TclError:
|
|
pass
|
|
|
|
min_win_w = getattr(app_config, "LAYOUT_WINDOW_MIN_WIDTH", 900)
|
|
min_win_h = getattr(app_config, "LAYOUT_WINDOW_MIN_HEIGHT", 650)
|
|
self.root.minsize(min_win_w, min_win_h)
|
|
|
|
self.canvas_width = getattr(
|
|
app_config, "DEFAULT_CANVAS_WIDTH", FALLBACK_CANVAS_WIDTH
|
|
)
|
|
self.canvas_height = getattr(
|
|
app_config, "DEFAULT_CANVAS_HEIGHT", FALLBACK_CANVAS_HEIGHT
|
|
)
|
|
|
|
self.menubar = Menu(self.root)
|
|
self.file_menu = Menu(self.menubar, tearoff=0)
|
|
self.file_menu.add_command(
|
|
label="Import Aircraft Database (CSV)...",
|
|
command=self._import_aircraft_db_csv,
|
|
)
|
|
self.file_menu.add_separator()
|
|
self.file_menu.add_command(label="Exit", command=self._on_closing)
|
|
self.menubar.add_cascade(label="File", menu=self.file_menu)
|
|
self.root.config(menu=self.menubar)
|
|
|
|
self.main_horizontal_paned_window = ttk.PanedWindow(
|
|
self.root, orient=tk.HORIZONTAL
|
|
)
|
|
self.main_horizontal_paned_window.pack(
|
|
fill=tk.BOTH, expand=True, padx=5, pady=5
|
|
)
|
|
|
|
self.left_column_frame = ttk.Frame(self.main_horizontal_paned_window)
|
|
self.main_horizontal_paned_window.add(
|
|
self.left_column_frame,
|
|
weight=app_config.LAYOUT_MAIN_HORIZONTAL_WEIGHTS.get("left_column", 25),
|
|
)
|
|
|
|
self.left_vertical_paned_window = ttk.PanedWindow(
|
|
self.left_column_frame, orient=tk.VERTICAL
|
|
)
|
|
self.left_vertical_paned_window.pack(fill=tk.BOTH, expand=True)
|
|
|
|
self.function_notebook_frame = ttk.Frame(self.left_vertical_paned_window)
|
|
self.left_vertical_paned_window.add(
|
|
self.function_notebook_frame,
|
|
weight=app_config.LAYOUT_LEFT_VERTICAL_WEIGHTS.get("function_notebook", 65),
|
|
)
|
|
self.function_notebook = ttk.Notebook(self.function_notebook_frame)
|
|
self.function_notebook.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
|
|
|
|
self.live_bbox_tab_frame = ttk.Frame(self.function_notebook, padding=5)
|
|
self.function_notebook.add(self.live_bbox_tab_frame, text="Live: Area Monitor")
|
|
|
|
self.live_controls_options_frame = ttk.Frame(self.live_bbox_tab_frame)
|
|
self.live_controls_options_frame.pack(side=tk.TOP, fill=tk.X, pady=(0, 5))
|
|
self.controls_frame_live_area = ttk.Frame(self.live_controls_options_frame)
|
|
self.controls_frame_live_area.pack(side=tk.TOP, fill=tk.X)
|
|
|
|
self.control_frame = ttk.LabelFrame(
|
|
self.controls_frame_live_area, text="Controls", padding=(10, 5)
|
|
)
|
|
self.control_frame.pack(side=tk.TOP, fill=tk.X)
|
|
self.mode_var = tk.StringVar(value="Live")
|
|
self.live_radio = ttk.Radiobutton(
|
|
self.control_frame,
|
|
text="Live",
|
|
variable=self.mode_var,
|
|
value="Live",
|
|
command=self._on_mode_change,
|
|
)
|
|
self.live_radio.pack(side=tk.LEFT, padx=(0, 5))
|
|
self.history_radio = ttk.Radiobutton(
|
|
self.control_frame,
|
|
text="History",
|
|
variable=self.mode_var,
|
|
value="History",
|
|
command=self._on_mode_change,
|
|
)
|
|
self.history_radio.pack(side=tk.LEFT, padx=5)
|
|
self.start_button = ttk.Button(
|
|
self.control_frame, text="Start Monitoring", command=self._start_monitoring
|
|
)
|
|
self.start_button.pack(side=tk.LEFT, padx=5)
|
|
self.stop_button = ttk.Button(
|
|
self.control_frame,
|
|
text="Stop Monitoring",
|
|
command=self._stop_monitoring,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.stop_button.pack(side=tk.LEFT, padx=5)
|
|
|
|
self.bbox_frame = ttk.LabelFrame(
|
|
self.controls_frame_live_area,
|
|
text="Geographic Area (Bounding Box)",
|
|
padding=(10, 5),
|
|
)
|
|
self.bbox_frame.pack(side=tk.TOP, fill=tk.X, pady=5)
|
|
self.bbox_frame.columnconfigure(1, weight=1)
|
|
self.bbox_frame.columnconfigure(3, weight=1)
|
|
ttk.Label(self.bbox_frame, text="Lat Min:").grid(
|
|
row=0, column=0, padx=(0, 2), pady=2, sticky=tk.W
|
|
)
|
|
self.lat_min_var = tk.StringVar(value=str(app_config.DEFAULT_BBOX_LAT_MIN))
|
|
self.lat_min_entry = ttk.Entry(self.bbox_frame, textvariable=self.lat_min_var)
|
|
self.lat_min_entry.grid(row=0, column=1, padx=(0, 5), pady=2, sticky=tk.EW)
|
|
ttk.Label(self.bbox_frame, text="Lon Min:").grid(
|
|
row=0, column=2, padx=(5, 2), pady=2, sticky=tk.W
|
|
)
|
|
self.lon_min_var = tk.StringVar(value=str(app_config.DEFAULT_BBOX_LON_MIN))
|
|
self.lon_min_entry = ttk.Entry(self.bbox_frame, textvariable=self.lon_min_var)
|
|
self.lon_min_entry.grid(row=0, column=3, padx=(0, 0), pady=2, sticky=tk.EW)
|
|
ttk.Label(self.bbox_frame, text="Lat Max:").grid(
|
|
row=1, column=0, padx=(0, 2), pady=2, sticky=tk.W
|
|
)
|
|
self.lat_max_var = tk.StringVar(value=str(app_config.DEFAULT_BBOX_LAT_MAX))
|
|
self.lat_max_entry = ttk.Entry(self.bbox_frame, textvariable=self.lat_max_var)
|
|
self.lat_max_entry.grid(row=1, column=1, padx=(0, 5), pady=2, sticky=tk.EW)
|
|
ttk.Label(self.bbox_frame, text="Lon Max:").grid(
|
|
row=1, column=2, padx=(5, 2), pady=2, sticky=tk.W
|
|
)
|
|
self.lon_max_var = tk.StringVar(value=str(app_config.DEFAULT_BBOX_LON_MAX))
|
|
self.lon_max_entry = ttk.Entry(self.bbox_frame, textvariable=self.lon_max_var)
|
|
self.lon_max_entry.grid(row=1, column=3, padx=(0, 0), pady=2, sticky=tk.EW)
|
|
|
|
self.track_options_frame = ttk.LabelFrame(
|
|
self.live_controls_options_frame, text="Track Options", padding=(10, 5)
|
|
)
|
|
self.track_options_frame.pack(side=tk.TOP, fill=tk.X, pady=(10, 0))
|
|
ttk.Label(self.track_options_frame, text="Track Length (points):").pack(
|
|
side=tk.LEFT, padx=(0, 5)
|
|
)
|
|
self.track_length_var = tk.IntVar(value=DEFAULT_TRACK_LENGTH)
|
|
self.track_length_spinbox = ttk.Spinbox(
|
|
self.track_options_frame,
|
|
from_=2,
|
|
to=100,
|
|
textvariable=self.track_length_var,
|
|
width=5,
|
|
command=self._on_track_length_change,
|
|
state="readonly",
|
|
)
|
|
self.track_length_spinbox.pack(side=tk.LEFT, padx=5)
|
|
|
|
self.live_airport_tab_frame = ttk.Frame(self.function_notebook, padding=5)
|
|
self.function_notebook.add(self.live_airport_tab_frame, text="Live: Airport")
|
|
ttk.Label(
|
|
self.live_airport_tab_frame,
|
|
text="Live from Airport - Coming Soon",
|
|
font=("Arial", 10),
|
|
).pack(expand=True)
|
|
self.history_tab_frame = ttk.Frame(self.function_notebook, padding=5)
|
|
self.function_notebook.add(self.history_tab_frame, text="History")
|
|
ttk.Label(
|
|
self.history_tab_frame,
|
|
text="History Analysis - Coming Soon",
|
|
font=("Arial", 10),
|
|
).pack(expand=True)
|
|
self.function_notebook.bind(
|
|
"<<NotebookTabChanged>>", self._on_function_tab_change
|
|
)
|
|
|
|
self.log_status_area_frame = ttk.Frame(self.left_vertical_paned_window)
|
|
self.left_vertical_paned_window.add(
|
|
self.log_status_area_frame,
|
|
weight=app_config.LAYOUT_LEFT_VERTICAL_WEIGHTS.get("log_status_area", 35),
|
|
)
|
|
|
|
self.status_bar_frame = ttk.Frame(self.log_status_area_frame, padding=(5, 3))
|
|
self.status_bar_frame.pack(side=tk.TOP, fill=tk.X, pady=(0, 5))
|
|
self.semaphore_canvas = tk.Canvas(
|
|
self.status_bar_frame,
|
|
width=SEMAPHORE_TOTAL_SIZE,
|
|
height=SEMAPHORE_TOTAL_SIZE,
|
|
bg=self.root.cget("bg"),
|
|
highlightthickness=0,
|
|
)
|
|
self.semaphore_canvas.pack(side=tk.LEFT, padx=(0, 5))
|
|
x0, y0, x1, y1 = (
|
|
SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH,
|
|
SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH,
|
|
SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH + SEMAPHORE_SIZE,
|
|
SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH + SEMAPHORE_SIZE,
|
|
)
|
|
self._semaphore_oval_id = self.semaphore_canvas.create_oval(
|
|
x0,
|
|
y0,
|
|
x1,
|
|
y1,
|
|
fill=SEMAPHORE_COLOR_STATUS_MAP.get(GUI_STATUS_UNKNOWN, "gray70"),
|
|
outline="gray30",
|
|
width=SEMAPHORE_BORDER_WIDTH,
|
|
)
|
|
self.status_label = ttk.Label(
|
|
self.status_bar_frame, text="Status: Initializing..."
|
|
)
|
|
self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 2))
|
|
|
|
self.log_frame = ttk.Frame(self.log_status_area_frame, padding=(5, 0, 5, 5))
|
|
self.log_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=0)
|
|
log_font_family = (
|
|
"Consolas" if "Consolas" in tkFont.families() else "Courier New"
|
|
)
|
|
self.log_text_widget = ScrolledText(
|
|
self.log_frame,
|
|
state=tk.DISABLED,
|
|
height=10,
|
|
wrap=tk.WORD,
|
|
font=(log_font_family, 9),
|
|
relief=tk.SUNKEN,
|
|
borderwidth=1,
|
|
)
|
|
self.log_text_widget.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
|
|
|
|
self.right_column_frame = ttk.Frame(self.main_horizontal_paned_window)
|
|
self.main_horizontal_paned_window.add(
|
|
self.right_column_frame,
|
|
weight=app_config.LAYOUT_MAIN_HORIZONTAL_WEIGHTS.get("right_column", 75),
|
|
)
|
|
|
|
self.right_vertical_paned_window = ttk.PanedWindow(
|
|
self.right_column_frame, orient=tk.VERTICAL
|
|
)
|
|
self.right_vertical_paned_window.pack(fill=tk.BOTH, expand=True)
|
|
|
|
self.views_notebook_outer_frame = ttk.Frame(self.right_vertical_paned_window)
|
|
self.right_vertical_paned_window.add(
|
|
self.views_notebook_outer_frame,
|
|
weight=app_config.LAYOUT_RIGHT_VERTICAL_WEIGHTS.get("views_notebook", 80),
|
|
)
|
|
self.views_notebook = ttk.Notebook(self.views_notebook_outer_frame)
|
|
self.views_notebook.pack(fill=tk.BOTH, expand=True, padx=0, pady=(2, 0))
|
|
|
|
self.map_view_frame = ttk.Frame(self.views_notebook, padding=0)
|
|
self.views_notebook.add(self.map_view_frame, text="Map View")
|
|
self.flight_canvas = tk.Canvas(
|
|
self.map_view_frame,
|
|
bg="gray60",
|
|
width=self.canvas_width,
|
|
height=self.canvas_height,
|
|
highlightthickness=0,
|
|
)
|
|
self.flight_canvas.pack(fill=tk.BOTH, expand=True)
|
|
self.map_manager_instance: Optional[MapCanvasManager] = None
|
|
|
|
self.table_view_frame = ttk.Frame(self.views_notebook, padding=5)
|
|
self.views_notebook.add(self.table_view_frame, text="Table View")
|
|
ttk.Label(
|
|
self.table_view_frame, text="Table View - Coming Soon", font=("Arial", 12)
|
|
).pack(expand=True)
|
|
self.views_notebook.bind("<<NotebookTabChanged>>", self._on_view_tab_change)
|
|
|
|
self.map_tools_info_area_frame = ttk.Frame(
|
|
self.right_vertical_paned_window, padding=(0, 5, 0, 0)
|
|
)
|
|
self.right_vertical_paned_window.add(
|
|
self.map_tools_info_area_frame,
|
|
weight=app_config.LAYOUT_RIGHT_VERTICAL_WEIGHTS.get("map_tools_info", 20),
|
|
)
|
|
|
|
self.bottom_panel_container = ttk.Frame(self.map_tools_info_area_frame)
|
|
self.bottom_panel_container.pack(fill=tk.BOTH, expand=True)
|
|
|
|
bottom_panel_weights = getattr(
|
|
app_config,
|
|
"LAYOUT_BOTTOM_PANELS_HORIZONTAL_WEIGHTS",
|
|
{"map_tools": 1, "map_info": 2, "flight_details": 2},
|
|
)
|
|
self.bottom_panel_container.columnconfigure(
|
|
0, weight=bottom_panel_weights.get("map_tools", 1)
|
|
)
|
|
self.bottom_panel_container.columnconfigure(
|
|
1, weight=bottom_panel_weights.get("map_info", 2)
|
|
)
|
|
self.bottom_panel_container.columnconfigure(
|
|
2, weight=bottom_panel_weights.get("flight_details", 2)
|
|
)
|
|
|
|
self.map_tool_frame = ttk.LabelFrame(
|
|
self.bottom_panel_container, text="Map Tools", padding=5
|
|
)
|
|
self.map_tool_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 2))
|
|
self._recreate_map_tools_content(self.map_tool_frame)
|
|
|
|
self.map_info_panel_frame = ttk.LabelFrame(
|
|
self.bottom_panel_container, text="Map Information", padding=10
|
|
)
|
|
self.map_info_panel_frame.grid(row=0, column=1, sticky="nsew", padx=2)
|
|
self._recreate_map_info_content(self.map_info_panel_frame)
|
|
|
|
self.selected_flight_details_frame = ttk.LabelFrame(
|
|
self.bottom_panel_container, text="Selected Flight Details", padding=10
|
|
)
|
|
self.selected_flight_details_frame.grid(
|
|
row=0, column=2, sticky="nsew", padx=(2, 0)
|
|
)
|
|
self._create_selected_flight_details_content(self.selected_flight_details_frame)
|
|
|
|
from ..data.logging_config import LOGGING_CONFIG
|
|
|
|
setup_logging(
|
|
gui_log_widget=self.log_text_widget,
|
|
root_tk_instance=self.root,
|
|
logging_config_dict=LOGGING_CONFIG,
|
|
)
|
|
|
|
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
|
|
self.root.after(100, self._delayed_initialization)
|
|
module_logger.info(
|
|
"MainWindow basic structure initialized. Delayed init pending."
|
|
)
|
|
|
|
def _import_aircraft_db_csv(self):
|
|
if not self.controller:
|
|
self.show_error_message(
|
|
"Controller Error", "Application controller not available."
|
|
)
|
|
module_logger.error("Controller not available for aircraft DB import.")
|
|
return
|
|
|
|
if not hasattr(
|
|
self.controller, "import_aircraft_database_from_file_with_progress"
|
|
):
|
|
self.show_error_message(
|
|
"Function Error",
|
|
"Progress import function not available in controller.",
|
|
)
|
|
module_logger.error(
|
|
"Method 'import_aircraft_database_from_file_with_progress' missing in controller."
|
|
)
|
|
return
|
|
|
|
filepath = filedialog.askopenfilename(
|
|
master=self.root,
|
|
title="Select Aircraft Database CSV File",
|
|
filetypes=(("CSV files", "*.csv"), ("All files", "*.*")),
|
|
)
|
|
if filepath:
|
|
module_logger.info(f"GUI: Selected CSV file for import: {filepath}")
|
|
|
|
if IMPORT_DIALOG_AVAILABLE and ImportProgressDialog is not None:
|
|
if self.progress_dialog and self.progress_dialog.winfo_exists():
|
|
try:
|
|
self.progress_dialog.destroy()
|
|
except tk.TclError:
|
|
module_logger.warning(
|
|
"Could not destroy previous progress dialog cleanly."
|
|
)
|
|
self.progress_dialog = ImportProgressDialog(
|
|
self.root, file_name=filepath
|
|
)
|
|
self.controller.import_aircraft_database_from_file_with_progress(
|
|
filepath, self.progress_dialog
|
|
)
|
|
else:
|
|
module_logger.warning(
|
|
"ImportProgressDialog class not available. Using basic status update for import."
|
|
)
|
|
if hasattr(
|
|
self.controller, "import_aircraft_database_from_file"
|
|
): # Fallback
|
|
self.show_info_message(
|
|
"Import Started",
|
|
f"Starting import of {os.path.basename(filepath)}...\nThis might take a while. Check logs for completion.",
|
|
)
|
|
self.controller.import_aircraft_database_from_file(filepath) # type: ignore
|
|
else:
|
|
self.show_error_message(
|
|
"Import Error",
|
|
"Import progress UI is not available and no fallback import method found.",
|
|
)
|
|
else:
|
|
module_logger.info("GUI: CSV import cancelled by user.")
|
|
|
|
def _show_full_flight_details_action(self):
|
|
icao_to_show = None
|
|
if (
|
|
hasattr(self, "flight_detail_labels")
|
|
and "icao24" in self.flight_detail_labels
|
|
):
|
|
label_widget = self.flight_detail_labels["icao24"]
|
|
if label_widget.winfo_exists():
|
|
current_icao_text = label_widget.cget("text")
|
|
if current_icao_text != "N/A" and current_icao_text.strip():
|
|
icao_to_show = current_icao_text
|
|
|
|
if icao_to_show:
|
|
if self.controller and hasattr(
|
|
self.controller, "request_and_show_full_flight_details"
|
|
):
|
|
module_logger.info(
|
|
f"Requesting full details window for ICAO: {icao_to_show}"
|
|
)
|
|
# Il controller ora gestisce la creazione e la visualizzazione della finestra
|
|
self.controller.request_and_show_full_flight_details(icao_to_show)
|
|
else:
|
|
module_logger.error(
|
|
"Controller or method 'request_and_show_full_flight_details' not available."
|
|
)
|
|
self.show_error_message(
|
|
"Error", "Cannot open full details window (controller issue)."
|
|
)
|
|
else:
|
|
self.show_info_message(
|
|
"No Flight Selected",
|
|
"Please select a flight on the map first to see full details.",
|
|
)
|
|
module_logger.warning(
|
|
"Full details button clicked, but no flight ICAO found in details panel."
|
|
)
|
|
|
|
# ... (Tutti gli altri metodi di MainWindow, come _delayed_initialization, _recreate_map_tools_content, etc.
|
|
# devono essere copiati qui. Per brevità, li ometto ma devono essere presenti.)
|
|
def _delayed_initialization(self):
|
|
if not self.root.winfo_exists():
|
|
module_logger.warning(
|
|
"Root window destroyed before delayed initialization."
|
|
)
|
|
return
|
|
if MAP_CANVAS_MANAGER_AVAILABLE and MapCanvasManager is not None:
|
|
default_map_bbox = {
|
|
"lat_min": app_config.DEFAULT_BBOX_LAT_MIN,
|
|
"lon_min": app_config.DEFAULT_BBOX_LON_MIN,
|
|
"lat_max": app_config.DEFAULT_BBOX_LAT_MAX,
|
|
"lon_max": app_config.DEFAULT_BBOX_LON_MAX,
|
|
}
|
|
self.root.after(200, self._initialize_map_manager, default_map_bbox)
|
|
else:
|
|
module_logger.error("MapCanvasManager class not available post-init.")
|
|
self.root.after(
|
|
50,
|
|
lambda: self._update_map_placeholder(
|
|
"Map functionality disabled (Import Error)."
|
|
),
|
|
)
|
|
if (
|
|
hasattr(self, "track_length_var")
|
|
and self.controller
|
|
and hasattr(self.controller, "set_map_track_length")
|
|
):
|
|
try:
|
|
initial_track_len = self.track_length_var.get()
|
|
if initial_track_len > 0:
|
|
self.controller.set_map_track_length(initial_track_len)
|
|
except Exception as e:
|
|
module_logger.error(f"Error setting initial track length: {e}")
|
|
self.root.after(10, self._on_mode_change)
|
|
module_logger.info("MainWindow fully initialized.")
|
|
|
|
def _initialize_map_manager(self, initial_bbox_for_map: Dict[str, float]):
|
|
if not MAP_CANVAS_MANAGER_AVAILABLE or MapCanvasManager is None:
|
|
self._update_map_placeholder("Map Error: Manager class missing.")
|
|
if self.controller and hasattr(self.controller, "update_general_map_info"):
|
|
self.controller.update_general_map_info()
|
|
return
|
|
if not self.flight_canvas.winfo_exists():
|
|
return
|
|
canvas_w, canvas_h = (
|
|
self.flight_canvas.winfo_width(),
|
|
self.flight_canvas.winfo_height(),
|
|
)
|
|
if canvas_w <= 1:
|
|
canvas_w = self.canvas_width
|
|
if canvas_h <= 1:
|
|
canvas_h = self.canvas_height
|
|
if canvas_w > 1 and canvas_h > 1:
|
|
try:
|
|
self.map_manager_instance = MapCanvasManager(
|
|
app_controller=self.controller,
|
|
tk_canvas=self.flight_canvas,
|
|
initial_bbox_dict=initial_bbox_for_map,
|
|
)
|
|
if (
|
|
hasattr(self, "track_length_var")
|
|
and self.controller
|
|
and hasattr(self.controller, "set_map_track_length")
|
|
):
|
|
try:
|
|
self.controller.set_map_track_length(
|
|
self.track_length_var.get()
|
|
)
|
|
except Exception as e_trk:
|
|
module_logger.error(
|
|
f"Error setting initial track length for map manager: {e_trk}"
|
|
)
|
|
if self.controller and hasattr(
|
|
self.controller, "update_general_map_info"
|
|
):
|
|
self.controller.update_general_map_info()
|
|
except Exception as e_init:
|
|
module_logger.critical(
|
|
f"Failed to initialize MapCanvasManager: {e_init}", exc_info=True
|
|
)
|
|
self.show_error_message(
|
|
"Map Error", f"Could not initialize map: {e_init}"
|
|
)
|
|
self._update_map_placeholder(f"Map Error: Init failed.\n{e_init}")
|
|
if self.controller and hasattr(
|
|
self.controller, "update_general_map_info"
|
|
):
|
|
self.controller.update_general_map_info()
|
|
else:
|
|
if self.root.winfo_exists():
|
|
self.root.after(300, self._initialize_map_manager, initial_bbox_for_map)
|
|
|
|
def _recreate_map_tools_content(self, parent_frame: ttk.Frame):
|
|
# ... (come prima)
|
|
controls_map_container = ttk.Frame(parent_frame)
|
|
controls_map_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
zoom_frame = ttk.Frame(controls_map_container)
|
|
zoom_frame.pack(side=tk.TOP, pady=(0, 10))
|
|
ttk.Button(zoom_frame, text="Zoom In (+)", command=self._map_zoom_in).pack(
|
|
side=tk.LEFT, padx=2
|
|
)
|
|
ttk.Button(zoom_frame, text="Zoom Out (-)", command=self._map_zoom_out).pack(
|
|
side=tk.LEFT, padx=2
|
|
)
|
|
pan_frame = ttk.Frame(controls_map_container)
|
|
pan_frame.pack(side=tk.TOP, pady=(0, 10))
|
|
ttk.Button(pan_frame, text="Up", command=lambda: self._map_pan("up")).grid(
|
|
row=0, column=1, padx=2, pady=2
|
|
)
|
|
ttk.Button(pan_frame, text="Left", command=lambda: self._map_pan("left")).grid(
|
|
row=1, column=0, padx=2, pady=2
|
|
)
|
|
ttk.Button(
|
|
pan_frame, text="Right", command=lambda: self._map_pan("right")
|
|
).grid(row=1, column=2, padx=2, pady=2)
|
|
ttk.Button(pan_frame, text="Down", command=lambda: self._map_pan("down")).grid(
|
|
row=2, column=1, padx=2, pady=2
|
|
)
|
|
pan_frame.columnconfigure(0, weight=1)
|
|
pan_frame.columnconfigure(2, weight=1)
|
|
center_frame = ttk.LabelFrame(
|
|
controls_map_container, text="Center Map", padding=5
|
|
)
|
|
center_frame.pack(side=tk.TOP, fill=tk.X, pady=(5, 0))
|
|
center_frame.columnconfigure(1, weight=1)
|
|
center_frame.columnconfigure(3, weight=1)
|
|
ttk.Label(center_frame, text="Lat:").grid(
|
|
row=0, column=0, padx=(0, 2), pady=2, sticky=tk.W
|
|
)
|
|
self.center_lat_var = tk.StringVar()
|
|
self.center_lat_entry = ttk.Entry(
|
|
center_frame, textvariable=self.center_lat_var, width=10
|
|
)
|
|
self.center_lat_entry.grid(row=0, column=1, padx=(0, 5), pady=2, sticky=tk.EW)
|
|
ttk.Label(center_frame, text="Lon:").grid(
|
|
row=0, column=2, padx=(5, 2), pady=2, sticky=tk.W
|
|
)
|
|
self.center_lon_var = tk.StringVar()
|
|
self.center_lon_entry = ttk.Entry(
|
|
center_frame, textvariable=self.center_lon_var, width=10
|
|
)
|
|
self.center_lon_entry.grid(row=0, column=3, padx=(0, 0), pady=2, sticky=tk.EW)
|
|
ttk.Label(center_frame, text="Patch (km):").grid(
|
|
row=1, column=0, padx=(0, 2), pady=2, sticky=tk.W
|
|
)
|
|
self.center_patch_size_var = tk.StringVar(value="100")
|
|
self.center_patch_size_entry = ttk.Entry(
|
|
center_frame, textvariable=self.center_patch_size_var, width=7
|
|
)
|
|
self.center_patch_size_entry.grid(
|
|
row=1, column=1, padx=(0, 5), pady=2, sticky=tk.W
|
|
)
|
|
self.center_map_button = ttk.Button(
|
|
center_frame, text="Center & Fit Patch", command=self._map_center_and_fit
|
|
)
|
|
self.center_map_button.grid(
|
|
row=1, column=2, columnspan=2, padx=5, pady=5, sticky=tk.E
|
|
)
|
|
|
|
def _recreate_map_info_content(self, parent_frame: ttk.Frame):
|
|
# ... (come prima)
|
|
parent_frame.columnconfigure(1, weight=0)
|
|
parent_frame.columnconfigure(3, weight=0)
|
|
info_row = 0
|
|
ttk.Label(parent_frame, text="Click Lat:").grid(
|
|
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
|
|
)
|
|
self.info_lat_value = ttk.Label(parent_frame, text="N/A", width=12)
|
|
self.info_lat_value.grid(
|
|
row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
ttk.Label(parent_frame, text="Lon:").grid(
|
|
row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2)
|
|
)
|
|
self.info_lon_value = ttk.Label(parent_frame, text="N/A", width=12)
|
|
self.info_lon_value.grid(
|
|
row=info_row, column=3, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
info_row += 1
|
|
ttk.Label(parent_frame, text="Lat DMS:").grid(
|
|
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
|
|
)
|
|
self.info_lat_dms_value = ttk.Label(
|
|
parent_frame, text="N/A", width=18, wraplength=140
|
|
)
|
|
self.info_lat_dms_value.grid(
|
|
row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
ttk.Label(parent_frame, text="Lon DMS:").grid(
|
|
row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2)
|
|
)
|
|
self.info_lon_dms_value = ttk.Label(
|
|
parent_frame, text="N/A", width=18, wraplength=140
|
|
)
|
|
self.info_lon_dms_value.grid(
|
|
row=info_row, column=3, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
info_row += 1
|
|
ttk.Separator(parent_frame, orient=tk.HORIZONTAL).grid(
|
|
row=info_row, column=0, columnspan=4, sticky=tk.EW, pady=2
|
|
)
|
|
info_row += 1
|
|
ttk.Label(parent_frame, text="Map N:").grid(
|
|
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
|
|
)
|
|
self.info_map_bounds_n = ttk.Label(parent_frame, text="N/A", width=12)
|
|
self.info_map_bounds_n.grid(
|
|
row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
ttk.Label(parent_frame, text="W:").grid(
|
|
row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2)
|
|
)
|
|
self.info_map_bounds_w = ttk.Label(parent_frame, text="N/A", width=12)
|
|
self.info_map_bounds_w.grid(
|
|
row=info_row, column=3, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
info_row += 1
|
|
ttk.Label(parent_frame, text="Map S:").grid(
|
|
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
|
|
)
|
|
self.info_map_bounds_s = ttk.Label(parent_frame, text="N/A", width=12)
|
|
self.info_map_bounds_s.grid(
|
|
row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
ttk.Label(parent_frame, text="E:").grid(
|
|
row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2)
|
|
)
|
|
self.info_map_bounds_e = ttk.Label(parent_frame, text="N/A", width=12)
|
|
self.info_map_bounds_e.grid(
|
|
row=info_row, column=3, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
info_row += 1
|
|
ttk.Separator(parent_frame, orient=tk.HORIZONTAL).grid(
|
|
row=info_row, column=0, columnspan=4, sticky=tk.EW, pady=2
|
|
)
|
|
info_row += 1
|
|
ttk.Label(parent_frame, text="Target N:").grid(
|
|
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
|
|
)
|
|
self.info_target_bbox_n = ttk.Label(parent_frame, text="N/A", width=12)
|
|
self.info_target_bbox_n.grid(
|
|
row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
ttk.Label(parent_frame, text="W:").grid(
|
|
row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2)
|
|
)
|
|
self.info_target_bbox_w = ttk.Label(parent_frame, text="N/A", width=12)
|
|
self.info_target_bbox_w.grid(
|
|
row=info_row, column=3, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
info_row += 1
|
|
ttk.Label(parent_frame, text="Target S:").grid(
|
|
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
|
|
)
|
|
self.info_target_bbox_s = ttk.Label(parent_frame, text="N/A", width=12)
|
|
self.info_target_bbox_s.grid(
|
|
row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
ttk.Label(parent_frame, text="E:").grid(
|
|
row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2)
|
|
)
|
|
self.info_target_bbox_e = ttk.Label(parent_frame, text="N/A", width=12)
|
|
self.info_target_bbox_e.grid(
|
|
row=info_row, column=3, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
info_row += 1
|
|
ttk.Separator(parent_frame, orient=tk.HORIZONTAL).grid(
|
|
row=info_row, column=0, columnspan=4, sticky=tk.EW, pady=2
|
|
)
|
|
info_row += 1
|
|
ttk.Label(parent_frame, text="Zoom:").grid(
|
|
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
|
|
)
|
|
self.info_zoom_value = ttk.Label(parent_frame, text="N/A", width=4)
|
|
self.info_zoom_value.grid(
|
|
row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
ttk.Label(parent_frame, text="Flights:").grid(
|
|
row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2)
|
|
)
|
|
self.info_flight_count_value = ttk.Label(parent_frame, text="N/A", width=5)
|
|
self.info_flight_count_value.grid(
|
|
row=info_row, column=3, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
info_row += 1
|
|
ttk.Label(parent_frame, text="Map Size:").grid(
|
|
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
|
|
)
|
|
self.info_map_size_value = ttk.Label(parent_frame, text="N/A", wraplength=180)
|
|
self.info_map_size_value.grid(
|
|
row=info_row, column=1, columnspan=3, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
|
|
def _create_selected_flight_details_content(self, parent_frame: ttk.LabelFrame):
|
|
# ... (come prima)
|
|
parent_frame.columnconfigure(1, weight=1)
|
|
parent_frame.columnconfigure(3, weight=1)
|
|
self.flight_detail_labels: Dict[str, ttk.Label] = {}
|
|
fields_col1 = [
|
|
("icao24", "ICAO24:"),
|
|
("baro_altitude_m", "Altitude (baro):"),
|
|
("velocity_mps", "Speed (GS):"),
|
|
("true_track_deg", "Track (°):"),
|
|
("squawk", "Squawk:"),
|
|
("position_source", "Pos. Source:"),
|
|
("registration", "Registration:"),
|
|
("model", "Model:"),
|
|
("operator", "Operator:"),
|
|
("categorydescription", "Category:"),
|
|
("serialnumber", "Serial No.:"),
|
|
("firstflightdate", "First Flight:"),
|
|
]
|
|
fields_col2 = [
|
|
("callsign", "Callsign:"),
|
|
("geo_altitude_m", "Altitude (Geo):"),
|
|
("vertical_rate_mps", "Vert. Rate:"),
|
|
("on_ground", "On Ground:"),
|
|
("spi", "SPI:"),
|
|
("origin_country", "Origin Country:"),
|
|
("manufacturername", "Manufacturer:"),
|
|
("typecode", "Type Code:"),
|
|
("operatorcallsign", "Op. Callsign:"),
|
|
("built_year", "Built Year:"),
|
|
("country", "Country (Reg):"),
|
|
("timestamp_metadata", "DB Timestamp:"),
|
|
]
|
|
max_rows = max(len(fields_col1), len(fields_col2))
|
|
for i in range(max_rows):
|
|
if i < len(fields_col1):
|
|
key1, label_text1 = fields_col1[i]
|
|
ttk.Label(
|
|
parent_frame,
|
|
text=label_text1,
|
|
font="-weight bold" if key1 == "icao24" else None,
|
|
).grid(row=i, column=0, sticky=tk.W, pady=0, padx=(0, 2))
|
|
value_label1 = ttk.Label(parent_frame, text="N/A", wraplength=130)
|
|
value_label1.grid(row=i, column=1, sticky=tk.W, pady=0, padx=(0, 10))
|
|
self.flight_detail_labels[key1] = value_label1
|
|
if i < len(fields_col2):
|
|
key2, label_text2 = fields_col2[i]
|
|
ttk.Label(parent_frame, text=label_text2).grid(
|
|
row=i, column=2, sticky=tk.W, pady=0, padx=(5, 2)
|
|
)
|
|
value_label2 = ttk.Label(parent_frame, text="N/A", wraplength=130)
|
|
value_label2.grid(row=i, column=3, sticky=tk.W, pady=0, padx=(0, 0))
|
|
self.flight_detail_labels[key2] = value_label2
|
|
|
|
self.full_details_button = ttk.Button(
|
|
parent_frame,
|
|
text="Aircraft Full Details...",
|
|
command=self._show_full_flight_details_action,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.full_details_button.grid(
|
|
row=max_rows, column=0, columnspan=4, pady=(10, 0), sticky="ew"
|
|
)
|
|
|
|
def update_selected_flight_details(self, flight_data: Optional[Dict[str, Any]]):
|
|
# ... (come prima)
|
|
module_logger.debug(
|
|
f"GUI: Updating flight details panel for: {flight_data.get('icao24', 'None') if flight_data else 'None'}"
|
|
)
|
|
if not hasattr(self, "flight_detail_labels"):
|
|
module_logger.error(
|
|
"flight_detail_labels not initialized. Cannot update details."
|
|
)
|
|
return
|
|
all_panel_keys = list(self.flight_detail_labels.keys())
|
|
for key in all_panel_keys:
|
|
label_widget = self.flight_detail_labels.get(key)
|
|
if label_widget and label_widget.winfo_exists():
|
|
label_widget.config(text="N/A")
|
|
if flight_data:
|
|
baro_alt_val = flight_data.get("baro_altitude_m")
|
|
geo_alt_val = flight_data.get("geo_altitude_m")
|
|
if (
|
|
"baro_altitude_m" in self.flight_detail_labels
|
|
and self.flight_detail_labels["baro_altitude_m"].winfo_exists()
|
|
):
|
|
alt_text = "N/A"
|
|
if baro_alt_val is not None:
|
|
alt_text = f"{baro_alt_val:.0f} m"
|
|
elif geo_alt_val is not None:
|
|
alt_text = f"{geo_alt_val:.0f} m (geo)"
|
|
self.flight_detail_labels["baro_altitude_m"].config(text=alt_text)
|
|
if (
|
|
"geo_altitude_m" in self.flight_detail_labels
|
|
and self.flight_detail_labels["geo_altitude_m"].winfo_exists()
|
|
):
|
|
alt_geo_text = "N/A"
|
|
if geo_alt_val is not None:
|
|
alt_geo_text = f"{geo_alt_val:.0f} m"
|
|
if (
|
|
baro_alt_val is not None
|
|
and geo_alt_val is not None
|
|
and abs(baro_alt_val - geo_alt_val) > 1
|
|
):
|
|
self.flight_detail_labels["geo_altitude_m"].config(
|
|
text=alt_geo_text
|
|
)
|
|
elif baro_alt_val is None and geo_alt_val is not None:
|
|
self.flight_detail_labels["geo_altitude_m"].config(
|
|
text=alt_geo_text
|
|
)
|
|
for key, label_widget in self.flight_detail_labels.items():
|
|
if key in ["baro_altitude_m", "geo_altitude_m"]:
|
|
continue
|
|
if label_widget and label_widget.winfo_exists():
|
|
value = flight_data.get(key)
|
|
formatted_value = "N/A"
|
|
if value is not None and str(value).strip() != "":
|
|
if key == "velocity_mps" and isinstance(value, (float, int)):
|
|
formatted_value = (
|
|
f"{value:.1f} m/s ({value * 1.94384:.1f} kts)"
|
|
)
|
|
elif key == "vertical_rate_mps" and isinstance(
|
|
value, (float, int)
|
|
):
|
|
formatted_value = (
|
|
f"{value * 196.85:.0f} ft/min ({value:.1f} m/s)"
|
|
)
|
|
elif key == "true_track_deg" and isinstance(
|
|
value, (float, int)
|
|
):
|
|
formatted_value = f"{value:.1f}°"
|
|
elif key in [
|
|
"timestamp",
|
|
"last_contact_timestamp",
|
|
"firstflightdate",
|
|
"timestamp_metadata",
|
|
]:
|
|
if isinstance(value, (int, float)) and value > 0:
|
|
try:
|
|
formatted_value = datetime.fromtimestamp(
|
|
value, tz=timezone.utc
|
|
).strftime("%Y-%m-%d %H:%M:%S Z")
|
|
except:
|
|
formatted_value = str(value) + " (raw ts)"
|
|
elif isinstance(value, str) and value.strip():
|
|
formatted_value = value
|
|
elif isinstance(value, bool):
|
|
formatted_value = str(value)
|
|
elif key == "built_year" and value:
|
|
formatted_value = (
|
|
str(int(value))
|
|
if isinstance(value, (float, int)) and value > 0
|
|
else str(value)
|
|
)
|
|
else:
|
|
formatted_value = str(value)
|
|
label_widget.config(text=formatted_value)
|
|
if (
|
|
hasattr(self, "full_details_button")
|
|
and self.full_details_button.winfo_exists()
|
|
):
|
|
self.full_details_button.config(
|
|
state=tk.NORMAL if flight_data.get("icao24") else tk.DISABLED
|
|
)
|
|
else:
|
|
if (
|
|
hasattr(self, "full_details_button")
|
|
and self.full_details_button.winfo_exists()
|
|
):
|
|
self.full_details_button.config(state=tk.DISABLED)
|
|
module_logger.debug("Selected flight details panel updated.")
|
|
|
|
# ... Ripeto qui gli altri metodi per completezza, assicurati che siano tutti presenti nel tuo file ...
|
|
def update_semaphore_and_status(self, status_level: str, message: str):
|
|
color_to_set = SEMAPHORE_COLOR_STATUS_MAP.get(
|
|
status_level, SEMAPHORE_COLOR_STATUS_MAP.get(GUI_STATUS_UNKNOWN, "gray60")
|
|
)
|
|
if (
|
|
hasattr(self, "semaphore_canvas")
|
|
and self.semaphore_canvas
|
|
and self.semaphore_canvas.winfo_exists()
|
|
):
|
|
try:
|
|
self.semaphore_canvas.itemconfig(
|
|
self._semaphore_oval_id, fill=color_to_set
|
|
)
|
|
except tk.TclError:
|
|
pass
|
|
current_status_text = f"Status: {message}"
|
|
if (
|
|
hasattr(self, "status_label")
|
|
and self.status_label
|
|
and self.status_label.winfo_exists()
|
|
):
|
|
try:
|
|
self.status_label.config(text=current_status_text)
|
|
except tk.TclError:
|
|
pass
|
|
log_level_to_use = logging.INFO
|
|
if status_level == GUI_STATUS_ERROR:
|
|
log_level_to_use = logging.ERROR
|
|
elif status_level == GUI_STATUS_PERMANENT_FAILURE:
|
|
log_level_to_use = logging.CRITICAL
|
|
elif status_level == GUI_STATUS_WARNING:
|
|
log_level_to_use = logging.WARNING
|
|
elif status_level in [GUI_STATUS_FETCHING, GUI_STATUS_OK, GUI_STATUS_UNKNOWN]:
|
|
log_level_to_use = logging.DEBUG
|
|
module_logger.log(
|
|
log_level_to_use,
|
|
f"GUI Status Update: Level='{status_level}', Message='{message}'",
|
|
)
|
|
|
|
def _on_closing(self):
|
|
module_logger.info("Main window closing event triggered.")
|
|
user_confirmed_quit = (
|
|
messagebox.askokcancel(
|
|
"Quit", "Do you want to quit Flight Monitor?", parent=self.root
|
|
)
|
|
if self.root.winfo_exists()
|
|
else True
|
|
)
|
|
if user_confirmed_quit:
|
|
if self.controller and hasattr(self.controller, "on_application_exit"):
|
|
try:
|
|
self.controller.on_application_exit()
|
|
except Exception as e:
|
|
module_logger.error(
|
|
f"Error during controller.on_application_exit: {e}",
|
|
exc_info=True,
|
|
)
|
|
try:
|
|
shutdown_logging_system()
|
|
except Exception as e:
|
|
module_logger.error(
|
|
f"Error during shutdown_logging_system: {e}", exc_info=True
|
|
)
|
|
if hasattr(self, "root") and self.root.winfo_exists():
|
|
try:
|
|
self.root.destroy()
|
|
except Exception:
|
|
pass # Ignore errors on final destroy
|
|
else:
|
|
module_logger.info("User cancelled quit.")
|
|
|
|
def _reset_gui_to_stopped_state(
|
|
self, status_message: Optional[str] = "Monitoring stopped."
|
|
):
|
|
if hasattr(self, "start_button") and self.start_button.winfo_exists():
|
|
self.start_button.config(state=tk.NORMAL)
|
|
if hasattr(self, "stop_button") and self.stop_button.winfo_exists():
|
|
self.stop_button.config(state=tk.DISABLED)
|
|
if hasattr(self, "live_radio") and self.live_radio.winfo_exists():
|
|
self.live_radio.config(state=tk.NORMAL)
|
|
if hasattr(self, "history_radio") and self.history_radio.winfo_exists():
|
|
self.history_radio.config(state=tk.NORMAL)
|
|
self._update_controls_state_based_on_mode_and_tab()
|
|
status_level = GUI_STATUS_OK
|
|
if status_message and (
|
|
"failed" in status_message.lower() or "error" in status_message.lower()
|
|
):
|
|
status_level = GUI_STATUS_ERROR
|
|
elif status_message and "warning" in status_message.lower():
|
|
status_level = GUI_STATUS_WARNING
|
|
if hasattr(self, "root") and self.root.winfo_exists():
|
|
self.update_semaphore_and_status(status_level, status_message)
|
|
|
|
def _should_show_main_placeholder(self) -> bool:
|
|
return not (
|
|
hasattr(self, "map_manager_instance")
|
|
and self.map_manager_instance is not None
|
|
and MAP_CANVAS_MANAGER_AVAILABLE
|
|
)
|
|
|
|
def _update_map_placeholder(self, text_to_display: str):
|
|
if not (
|
|
hasattr(self, "flight_canvas")
|
|
and self.flight_canvas
|
|
and self.flight_canvas.winfo_exists()
|
|
):
|
|
return
|
|
if not self._should_show_main_placeholder():
|
|
try:
|
|
self.flight_canvas.delete("placeholder_text")
|
|
except:
|
|
pass
|
|
return
|
|
try:
|
|
self.flight_canvas.delete("placeholder_text")
|
|
canvas_w, canvas_h = (
|
|
self.flight_canvas.winfo_width(),
|
|
self.flight_canvas.winfo_height(),
|
|
)
|
|
if canvas_w <= 1:
|
|
canvas_w = self.canvas_width
|
|
if canvas_h <= 1:
|
|
canvas_h = self.canvas_height
|
|
if canvas_w > 1 and canvas_h > 1:
|
|
self.flight_canvas.create_text(
|
|
canvas_w / 2,
|
|
canvas_h / 2,
|
|
text=text_to_display,
|
|
tags="placeholder_text",
|
|
fill="gray50",
|
|
font=("Arial", 12, "italic"),
|
|
justify=tk.CENTER,
|
|
width=canvas_w - 40,
|
|
)
|
|
except:
|
|
pass # Ignore TclErrors if canvas is gone
|
|
|
|
def _on_mode_change(self):
|
|
if not (
|
|
hasattr(self, "mode_var")
|
|
and hasattr(self, "function_notebook")
|
|
and self.function_notebook.winfo_exists()
|
|
):
|
|
return
|
|
selected_mode = self.mode_var.get()
|
|
status_message = f"Mode: {selected_mode}. Ready."
|
|
try:
|
|
tab_indices = {
|
|
name: i
|
|
for i, name_tuple in enumerate(
|
|
map(
|
|
lambda t: (self.function_notebook.tab(t, "text"), t),
|
|
self.function_notebook.tabs(),
|
|
)
|
|
)
|
|
for name in name_tuple
|
|
}
|
|
live_bbox_idx, history_idx, live_airport_idx = (
|
|
tab_indices.get("Live: Area Monitor", -1),
|
|
tab_indices.get("History", -1),
|
|
tab_indices.get("Live: Airport", -1),
|
|
)
|
|
if selected_mode == "Live":
|
|
if live_bbox_idx != -1:
|
|
self.function_notebook.tab(live_bbox_idx, state="normal")
|
|
if live_airport_idx != -1:
|
|
self.function_notebook.tab(live_airport_idx, state="normal")
|
|
if history_idx != -1:
|
|
self.function_notebook.tab(history_idx, state="disabled")
|
|
if (
|
|
self.function_notebook.index("current") == history_idx
|
|
and live_bbox_idx != -1
|
|
):
|
|
self.function_notebook.select(live_bbox_idx)
|
|
elif selected_mode == "History":
|
|
if live_bbox_idx != -1:
|
|
self.function_notebook.tab(live_bbox_idx, state="disabled")
|
|
if live_airport_idx != -1:
|
|
self.function_notebook.tab(live_airport_idx, state="disabled")
|
|
if history_idx != -1:
|
|
self.function_notebook.tab(history_idx, state="normal")
|
|
if (
|
|
self.function_notebook.index("current") != history_idx
|
|
and history_idx != -1
|
|
):
|
|
self.function_notebook.select(history_idx)
|
|
except Exception as e:
|
|
module_logger.warning(f"Error updating func tabs: {e}", exc_info=True)
|
|
self.clear_all_views_data()
|
|
self._update_controls_state_based_on_mode_and_tab()
|
|
if hasattr(self, "root") and self.root.winfo_exists():
|
|
self.update_semaphore_and_status(GUI_STATUS_OK, status_message)
|
|
|
|
def _on_function_tab_change(self, event: Optional[tk.Event] = None):
|
|
if not (
|
|
hasattr(self, "function_notebook") and self.function_notebook.winfo_exists()
|
|
):
|
|
return
|
|
try:
|
|
tab_text = self.function_notebook.tab(
|
|
self.function_notebook.index("current"), "text"
|
|
)
|
|
placeholder_text_map = "Map Area."
|
|
if "Live: Area Monitor" in tab_text:
|
|
placeholder_text_map = "Map - Live Area. Define area and press Start."
|
|
elif "Live: Airport" in tab_text:
|
|
placeholder_text_map = "Map - Live Airport. (TBD)"
|
|
elif "History" in tab_text:
|
|
placeholder_text_map = "Map - History Analysis. (TBD)"
|
|
self._update_map_placeholder(placeholder_text_map)
|
|
self._update_controls_state_based_on_mode_and_tab()
|
|
except:
|
|
pass
|
|
|
|
def _update_controls_state_based_on_mode_and_tab(self):
|
|
is_live_mode = hasattr(self, "mode_var") and self.mode_var.get() == "Live"
|
|
is_monitoring_active = (
|
|
hasattr(self, "stop_button")
|
|
and self.stop_button.winfo_exists()
|
|
and self.stop_button.cget("state") == tk.NORMAL
|
|
)
|
|
active_func_tab_text = ""
|
|
if hasattr(self, "function_notebook") and self.function_notebook.winfo_exists():
|
|
try:
|
|
active_func_tab_text = self.function_notebook.tab(
|
|
self.function_notebook.index("current"), "text"
|
|
)
|
|
except:
|
|
pass
|
|
enable_bbox = (
|
|
is_live_mode
|
|
and "Live: Area Monitor" in active_func_tab_text
|
|
and not is_monitoring_active
|
|
)
|
|
enable_track = is_live_mode and "Live: Area Monitor" in active_func_tab_text
|
|
self._set_bbox_entries_state(tk.NORMAL if enable_bbox else tk.DISABLED)
|
|
if (
|
|
hasattr(self, "track_length_spinbox")
|
|
and self.track_length_spinbox.winfo_exists()
|
|
):
|
|
try:
|
|
self.track_length_spinbox.config(
|
|
state=(
|
|
tk.DISABLED
|
|
if is_monitoring_active
|
|
else ("readonly" if enable_track else tk.DISABLED)
|
|
)
|
|
)
|
|
except:
|
|
pass
|
|
|
|
def _on_view_tab_change(self, event: Optional[tk.Event] = None):
|
|
if not (hasattr(self, "views_notebook") and self.views_notebook.winfo_exists()):
|
|
return
|
|
try:
|
|
module_logger.info(
|
|
f"GUI: Switched view tab to: {self.views_notebook.tab(self.views_notebook.index('current'), 'text')}"
|
|
)
|
|
except:
|
|
pass
|
|
|
|
def _set_bbox_entries_state(self, state: str):
|
|
for name in [
|
|
"lat_min_entry",
|
|
"lon_min_entry",
|
|
"lat_max_entry",
|
|
"lon_max_entry",
|
|
]:
|
|
entry = getattr(self, name, None)
|
|
if entry and hasattr(entry, "winfo_exists") and entry.winfo_exists():
|
|
try:
|
|
entry.config(state=state)
|
|
except:
|
|
pass
|
|
|
|
def _start_monitoring(self):
|
|
if not hasattr(self, "mode_var"):
|
|
self.show_error_message("Internal Error", "App mode N/A.")
|
|
return
|
|
selected_mode = self.mode_var.get()
|
|
active_func_tab_text = ""
|
|
if hasattr(self, "function_notebook") and self.function_notebook.winfo_exists():
|
|
try:
|
|
active_func_tab_text = self.function_notebook.tab(
|
|
self.function_notebook.index("current"), "text"
|
|
)
|
|
except:
|
|
pass
|
|
for btn_name, new_state in [
|
|
("start_button", tk.DISABLED),
|
|
("stop_button", tk.NORMAL),
|
|
("live_radio", tk.DISABLED),
|
|
("history_radio", tk.DISABLED),
|
|
]:
|
|
btn = getattr(self, btn_name, None)
|
|
if btn and hasattr(btn, "winfo_exists") and btn.winfo_exists():
|
|
btn.config(state=new_state)
|
|
self._update_controls_state_based_on_mode_and_tab()
|
|
if not self.controller:
|
|
self._reset_gui_to_stopped_state("Critical Error: Controller unavailable.")
|
|
self.show_error_message("Internal Error", "App controller missing.")
|
|
return
|
|
if selected_mode == "Live" and "Live: Area Monitor" in active_func_tab_text:
|
|
bbox = self.get_bounding_box_from_gui()
|
|
if bbox:
|
|
self.controller.start_live_monitoring(bbox)
|
|
else:
|
|
self._reset_gui_to_stopped_state("Start failed: Invalid BBox.")
|
|
elif selected_mode == "History" and "History" in active_func_tab_text:
|
|
self.controller.start_history_monitoring()
|
|
else:
|
|
self._reset_gui_to_stopped_state(
|
|
f"Start not supported on '{active_func_tab_text}'."
|
|
)
|
|
|
|
def _stop_monitoring(self):
|
|
selected_mode = self.mode_var.get() if hasattr(self, "mode_var") else "Unknown"
|
|
if not self.controller:
|
|
self._reset_gui_to_stopped_state("Error: Controller missing.")
|
|
return
|
|
if selected_mode == "Live":
|
|
self.controller.stop_live_monitoring()
|
|
elif selected_mode == "History":
|
|
self.controller.stop_history_monitoring()
|
|
if hasattr(self, "root") and self.root.winfo_exists():
|
|
self.update_semaphore_and_status(
|
|
GUI_STATUS_FETCHING, f"{selected_mode} monitoring stopping..."
|
|
)
|
|
|
|
def get_bounding_box_from_gui(self) -> Optional[Dict[str, float]]:
|
|
# ... (come prima)
|
|
req_vars_names = ["lat_min_var", "lon_min_var", "lat_max_var", "lon_max_var"]
|
|
if not all(hasattr(self, v_name) for v_name in req_vars_names):
|
|
return None
|
|
try:
|
|
vals_str = [getattr(self, v_name).get() for v_name in req_vars_names]
|
|
if not all(s.strip() for s in vals_str):
|
|
return None
|
|
lat_min, lon_min, lat_max, lon_max = map(float, vals_str)
|
|
except:
|
|
return None
|
|
bbox_dict = {
|
|
"lat_min": lat_min,
|
|
"lon_min": lon_min,
|
|
"lat_max": lat_max,
|
|
"lon_max": lon_max,
|
|
}
|
|
return bbox_dict if _is_valid_bbox_dict(bbox_dict) else None
|
|
|
|
def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]):
|
|
# ... (come prima)
|
|
if not hasattr(self, "lat_min_var"):
|
|
return
|
|
if bbox_dict and _is_valid_bbox_dict(bbox_dict):
|
|
decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5)
|
|
try:
|
|
self.lat_min_var.set(f"{bbox_dict['lat_min']:.{decimals}f}")
|
|
self.lon_min_var.set(f"{bbox_dict['lon_min']:.{decimals}f}")
|
|
self.lat_max_var.set(f"{bbox_dict['lat_max']:.{decimals}f}")
|
|
self.lon_max_var.set(f"{bbox_dict['lon_max']:.{decimals}f}")
|
|
except:
|
|
pass
|
|
else:
|
|
try:
|
|
self.lat_min_var.set("N/A")
|
|
self.lon_min_var.set("N/A")
|
|
self.lat_max_var.set("N/A")
|
|
self.lon_max_var.set("N/A")
|
|
except:
|
|
pass
|
|
|
|
def display_flights_on_canvas(
|
|
self,
|
|
flight_states: List[CanonicalFlightState],
|
|
_active_bbox_context: Optional[Dict[str, float]],
|
|
):
|
|
# ... (come prima)
|
|
if not (
|
|
hasattr(self, "map_manager_instance")
|
|
and self.map_manager_instance
|
|
and MAP_CANVAS_MANAGER_AVAILABLE
|
|
):
|
|
if hasattr(self, "root") and self.root.winfo_exists():
|
|
self._update_map_placeholder("Map N/A to display flights.")
|
|
return
|
|
try:
|
|
self.map_manager_instance.update_flights_on_map(flight_states)
|
|
if not flight_states and self._should_show_main_placeholder():
|
|
self._update_map_placeholder("No flights in the selected area.")
|
|
except Exception as e:
|
|
self.show_error_message(
|
|
"Map Display Error", "Could not update flights on map."
|
|
)
|
|
|
|
def clear_all_views_data(self):
|
|
# ... (come prima)
|
|
if (
|
|
hasattr(self, "map_manager_instance")
|
|
and self.map_manager_instance
|
|
and MAP_CANVAS_MANAGER_AVAILABLE
|
|
):
|
|
try:
|
|
self.map_manager_instance.clear_map_display()
|
|
except Exception as e:
|
|
module_logger.warning(f"Error clearing map via map manager: {e}")
|
|
elif self._should_show_main_placeholder():
|
|
mode = self.mode_var.get() if hasattr(self, "mode_var") else "Unknown"
|
|
text = f"Map - {mode}. Data cleared."
|
|
if mode == "Live":
|
|
text = "Map - Live. Define area and Start."
|
|
elif mode == "History":
|
|
text = "Map - History. (TBD)"
|
|
self._update_map_placeholder(text)
|
|
if hasattr(self, "update_selected_flight_details"):
|
|
self.update_selected_flight_details(None)
|
|
|
|
def show_error_message(self, title: str, message: str):
|
|
# ... (come prima)
|
|
status_msg = f"Error: {message[:70]}{'...' if len(message)>70 else ''}"
|
|
if hasattr(self, "root") and self.root.winfo_exists():
|
|
self.update_semaphore_and_status(GUI_STATUS_ERROR, status_msg)
|
|
try:
|
|
messagebox.showerror(title, message, parent=self.root)
|
|
except tk.TclError:
|
|
pass
|
|
else:
|
|
print(f"ERROR (No GUI): {title} - {message}", flush=True)
|
|
|
|
def show_map_context_menu(
|
|
self, latitude: float, longitude: float, screen_x: int, screen_y: int
|
|
):
|
|
# ... (come prima)
|
|
if (
|
|
hasattr(self, "map_manager_instance")
|
|
and self.map_manager_instance
|
|
and hasattr(self.map_manager_instance, "show_map_context_menu_from_gui")
|
|
):
|
|
try:
|
|
self.map_manager_instance.show_map_context_menu_from_gui(
|
|
latitude, longitude, screen_x, screen_y
|
|
)
|
|
except Exception as e:
|
|
module_logger.error(
|
|
f"Error delegating context menu: {e}", exc_info=True
|
|
)
|
|
|
|
def update_clicked_map_info(
|
|
self,
|
|
lat_deg: Optional[float],
|
|
lon_deg: Optional[float],
|
|
lat_dms: str,
|
|
lon_dms: str,
|
|
):
|
|
# ... (come prima)
|
|
if not hasattr(self, "info_lat_value"):
|
|
return
|
|
decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5)
|
|
try:
|
|
if hasattr(self, "info_lat_value") and self.info_lat_value.winfo_exists():
|
|
self.info_lat_value.config(
|
|
text=f"{lat_deg:.{decimals}f}" if lat_deg is not None else "N/A"
|
|
)
|
|
if hasattr(self, "info_lon_value") and self.info_lon_value.winfo_exists():
|
|
self.info_lon_value.config(
|
|
text=f"{lon_deg:.{decimals}f}" if lon_deg is not None else "N/A"
|
|
)
|
|
if (
|
|
hasattr(self, "info_lat_dms_value")
|
|
and self.info_lat_dms_value.winfo_exists()
|
|
):
|
|
self.info_lat_dms_value.config(text=lat_dms or "N/A")
|
|
if (
|
|
hasattr(self, "info_lon_dms_value")
|
|
and self.info_lon_dms_value.winfo_exists()
|
|
):
|
|
self.info_lon_dms_value.config(text=lon_dms or "N/A")
|
|
except:
|
|
pass
|
|
|
|
def update_general_map_info_display(
|
|
self,
|
|
zoom: Optional[int],
|
|
map_size_str: str,
|
|
map_geo_bounds: Optional[Tuple[float, float, float, float]],
|
|
target_bbox_input: Optional[Dict[str, float]],
|
|
flight_count: Optional[int],
|
|
):
|
|
# ... (come prima)
|
|
if not hasattr(self, "info_zoom_value"):
|
|
return
|
|
decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5)
|
|
try:
|
|
mw, ms, me, mn = ("N/A",) * 4
|
|
if map_geo_bounds:
|
|
mw, ms, me, mn = (f"{c:.{decimals}f}" for c in map_geo_bounds)
|
|
for name, val in [
|
|
("info_map_bounds_w", mw),
|
|
("info_map_bounds_s", ms),
|
|
("info_map_bounds_e", me),
|
|
("info_map_bounds_n", mn),
|
|
]:
|
|
lbl = getattr(self, name, None)
|
|
if lbl and lbl.winfo_exists():
|
|
lbl.config(text=val)
|
|
tw, ts, te, tn = ("N/A",) * 4
|
|
color_target = BBOX_COLOR_NA
|
|
if target_bbox_input and _is_valid_bbox_dict(target_bbox_input):
|
|
tw, ts, te, tn = (
|
|
f"{target_bbox_input['lon_min']:.{decimals}f}",
|
|
f"{target_bbox_input['lat_min']:.{decimals}f}",
|
|
f"{target_bbox_input['lon_max']:.{decimals}f}",
|
|
f"{target_bbox_input['lat_max']:.{decimals}f}",
|
|
)
|
|
status_bbox = (
|
|
self._is_bbox_inside_bbox(target_bbox_input, map_geo_bounds)
|
|
if map_geo_bounds
|
|
else "Outside"
|
|
)
|
|
if status_bbox == "Inside":
|
|
color_target = BBOX_COLOR_INSIDE
|
|
elif status_bbox == "Partial":
|
|
color_target = BBOX_COLOR_PARTIAL
|
|
else:
|
|
color_target = BBOX_COLOR_OUTSIDE
|
|
for name, val in [
|
|
("info_target_bbox_w", tw),
|
|
("info_target_bbox_s", ts),
|
|
("info_target_bbox_e", te),
|
|
("info_target_bbox_n", tn),
|
|
]:
|
|
lbl = getattr(self, name, None)
|
|
if lbl and lbl.winfo_exists():
|
|
lbl.config(text=val, foreground=color_target)
|
|
for name, val_direct in [
|
|
("info_zoom_value", str(zoom) if zoom is not None else "N/A"),
|
|
("info_map_size_value", map_size_str or "N/A"),
|
|
(
|
|
"info_flight_count_value",
|
|
str(flight_count) if flight_count is not None else "N/A",
|
|
),
|
|
]:
|
|
lbl = getattr(self, name, None)
|
|
if lbl and lbl.winfo_exists():
|
|
lbl.config(text=val_direct)
|
|
except:
|
|
pass # Evita crash se i widget non ci sono
|
|
|
|
def _is_bbox_inside_bbox(
|
|
self,
|
|
inner_bbox_dict: Dict[str, float],
|
|
outer_bbox_tuple: Tuple[float, float, float, float],
|
|
) -> str:
|
|
# ... (come prima)
|
|
if not _is_valid_bbox_dict(inner_bbox_dict) or not (
|
|
outer_bbox_tuple
|
|
and len(outer_bbox_tuple) == 4
|
|
and all(isinstance(c, (int, float)) for c in outer_bbox_tuple)
|
|
):
|
|
return "N/A"
|
|
outer = {
|
|
"lon_min": outer_bbox_tuple[0],
|
|
"lat_min": outer_bbox_tuple[1],
|
|
"lon_max": outer_bbox_tuple[2],
|
|
"lat_max": outer_bbox_tuple[3],
|
|
}
|
|
eps = 1e-6
|
|
if (
|
|
inner_bbox_dict["lon_min"] >= outer["lon_min"] - eps
|
|
and inner_bbox_dict["lat_min"] >= outer["lat_min"] - eps
|
|
and inner_bbox_dict["lon_max"] <= outer["lon_max"] + eps
|
|
and inner_bbox_dict["lat_max"] <= outer["lat_max"] + eps
|
|
):
|
|
return "Inside"
|
|
if (
|
|
inner_bbox_dict["lon_max"] <= outer["lon_min"] + eps
|
|
or inner_bbox_dict["lon_min"] >= outer["lon_max"] - eps
|
|
or inner_bbox_dict["lat_max"] <= outer["lat_min"] + eps
|
|
or inner_bbox_dict["lat_min"] >= outer["lat_max"] - eps
|
|
):
|
|
return "Outside"
|
|
return "Partial"
|
|
|
|
def _map_zoom_in(self): # ... (come prima)
|
|
if self.controller and hasattr(self.controller, "map_zoom_in"):
|
|
self.controller.map_zoom_in()
|
|
|
|
def _map_zoom_out(self): # ... (come prima)
|
|
if self.controller and hasattr(self.controller, "map_zoom_out"):
|
|
self.controller.map_zoom_out()
|
|
|
|
def _map_pan(self, direction: str): # ... (come prima)
|
|
if self.controller and hasattr(self.controller, "map_pan_direction"):
|
|
self.controller.map_pan_direction(direction)
|
|
|
|
def _map_center_and_fit(self): # ... (come prima)
|
|
try:
|
|
lat, lon, patch = (
|
|
float(self.center_lat_var.get()),
|
|
float(self.center_lon_var.get()),
|
|
float(self.center_patch_size_var.get()),
|
|
)
|
|
if not (-90 <= lat <= 90 and -180 <= lon <= 180 and patch > 0):
|
|
self.show_error_message("Input Error", "Invalid lat/lon/patch.")
|
|
return
|
|
if self.controller and hasattr(
|
|
self.controller, "map_center_on_coords_and_fit_patch"
|
|
):
|
|
self.controller.map_center_on_coords_and_fit_patch(lat, lon, patch)
|
|
except ValueError:
|
|
self.show_error_message("Input Error", "Lat/Lon/Patch must be numbers.")
|
|
except Exception as e:
|
|
self.show_error_message("Error", f"Unexpected error: {e}")
|
|
|
|
def _on_track_length_change(self): # ... (come prima)
|
|
if (
|
|
hasattr(self, "track_length_var")
|
|
and self.controller
|
|
and hasattr(self.controller, "set_map_track_length")
|
|
):
|
|
try:
|
|
self.controller.set_map_track_length(self.track_length_var.get())
|
|
except:
|
|
pass
|