1983 lines
83 KiB
Python
1983 lines
83 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 # Aggiunto per os.path.basename
|
|
from datetime import datetime, timezone
|
|
|
|
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."
|
|
)
|
|
|
|
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)
|
|
|
|
|
|
# --- NUOVA CLASSE PER LA FINESTRA DI DIALOGO DI IMPORTAZIONE ---
|
|
class ImportProgressDialog(tk.Toplevel):
|
|
def __init__(self, parent, title="Importing Database..."):
|
|
super().__init__(parent)
|
|
self.title(title)
|
|
self.parent = parent
|
|
self.transient(parent)
|
|
self.grab_set()
|
|
self.protocol("WM_DELETE_WINDOW", self._on_close_attempt)
|
|
|
|
self.geometry("450x220") # Leggermente più alta per il bottone close
|
|
self.resizable(False, False)
|
|
|
|
main_frame = ttk.Frame(self, padding="10 10 10 10")
|
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
ttk.Label(
|
|
main_frame,
|
|
text="Please wait while the aircraft database is being imported.",
|
|
).pack(pady=(0, 10))
|
|
|
|
self.rows_processed_var = tk.StringVar(value="Rows Processed: 0")
|
|
ttk.Label(main_frame, textvariable=self.rows_processed_var).pack(anchor=tk.W)
|
|
|
|
self.rows_imported_var = tk.StringVar(value="Rows Imported/Updated: 0")
|
|
ttk.Label(main_frame, textvariable=self.rows_imported_var).pack(
|
|
anchor=tk.W, pady=(0, 10)
|
|
)
|
|
|
|
self.progress_var = tk.DoubleVar()
|
|
self.progressbar = ttk.Progressbar(
|
|
main_frame,
|
|
orient="horizontal",
|
|
length=400,
|
|
mode="determinate",
|
|
variable=self.progress_var,
|
|
)
|
|
self.progressbar.pack(pady=(0, 5), fill=tk.X, expand=True)
|
|
self.progress_label_var = tk.StringVar(value="0/0 (0%)")
|
|
ttk.Label(main_frame, textvariable=self.progress_label_var).pack(pady=(0, 10))
|
|
|
|
self.status_message_var = tk.StringVar(value="Initializing import...")
|
|
ttk.Label(
|
|
main_frame, textvariable=self.status_message_var, wraplength=400
|
|
).pack(pady=(5, 0))
|
|
|
|
self.close_button = ttk.Button(
|
|
main_frame, text="Close", command=self.destroy, state=tk.DISABLED
|
|
)
|
|
self.close_button.pack(pady=(10, 0))
|
|
|
|
self.import_running = False
|
|
self.center_window()
|
|
|
|
def center_window(self):
|
|
self.update_idletasks()
|
|
width = self.winfo_width()
|
|
height = self.winfo_height()
|
|
x = (self.winfo_screenwidth() // 2) - (width // 2)
|
|
y = (self.winfo_screenheight() // 2) - (height // 2)
|
|
if (
|
|
width > 0 and height > 0
|
|
): # Evita errore se la finestra non ha ancora dimensioni
|
|
self.geometry(f"{width}x{height}+{x}+{y}")
|
|
|
|
def update_progress(
|
|
self,
|
|
processed: int,
|
|
imported: int,
|
|
total_rows: Optional[int] = None,
|
|
message: Optional[str] = None,
|
|
):
|
|
if not self.winfo_exists():
|
|
return
|
|
self.rows_processed_var.set(f"Rows Processed: {processed}")
|
|
self.rows_imported_var.set(f"Rows Imported/Updated: {imported}")
|
|
if message:
|
|
self.status_message_var.set(message)
|
|
|
|
if total_rows and total_rows > 0:
|
|
self.progressbar["mode"] = "determinate"
|
|
percentage = (processed / total_rows) * 100 if total_rows > 0 else 0
|
|
self.progressbar["maximum"] = total_rows
|
|
self.progress_var.set(processed)
|
|
self.progress_label_var.set(f"{processed}/{total_rows} ({percentage:.1f}%)")
|
|
elif total_rows == 0: # File vuoto o solo header
|
|
self.progressbar["mode"] = "determinate"
|
|
self.progressbar["maximum"] = 1 # Evita divisione per zero
|
|
self.progress_var.set(1) # Mostra come completato
|
|
self.progress_label_var.set(f"0/0 (100%) - File empty or header only.")
|
|
else: # total_rows is None (conteggio iniziale fallito o non ancora fatto)
|
|
self.progressbar["mode"] = "indeterminate"
|
|
self.progressbar.start(10) # Avvia animazione indeterminata
|
|
self.progress_label_var.set(f"Processed: {processed} (Total rows unknown)")
|
|
|
|
def import_started(self):
|
|
if not self.winfo_exists():
|
|
return
|
|
self.import_running = True
|
|
self.status_message_var.set("Import in progress...")
|
|
self.close_button.config(state=tk.DISABLED)
|
|
self.progressbar["mode"] = (
|
|
"indeterminate" # Inizia in indeterminato se total_rows non è subito noto
|
|
)
|
|
self.progressbar.start(10)
|
|
|
|
def import_finished(self, success: bool, final_message: str):
|
|
if not self.winfo_exists():
|
|
return
|
|
self.import_running = False
|
|
self.status_message_var.set(final_message)
|
|
self.progressbar.stop() # Ferma l'animazione indeterminata se era attiva
|
|
self.progressbar["mode"] = "determinate"
|
|
if success and self.progressbar["maximum"] > 0: # Solo se c'era un massimo > 0
|
|
self.progress_var.set(self.progressbar["maximum"])
|
|
elif success and self.progressbar["maximum"] == 0: # File vuoto gestito
|
|
self.progress_var.set(0)
|
|
# else: la barra rimane allo stato corrente se c'è stato un errore
|
|
|
|
self.close_button.config(state=tk.NORMAL)
|
|
|
|
def _on_close_attempt(self):
|
|
if self.import_running:
|
|
messagebox.showwarning(
|
|
"Import in Progress",
|
|
"Import is currently running. Please wait until it finishes or implement a cancel function.",
|
|
parent=self,
|
|
)
|
|
else:
|
|
self.destroy()
|
|
|
|
|
|
# --- FINE NUOVA CLASSE ---
|
|
|
|
|
|
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 # Riferimento alla dialog
|
|
)
|
|
|
|
if app_config.LAYOUT_START_MAXIMIZED:
|
|
try:
|
|
self.root.state("zoomed")
|
|
module_logger.info(
|
|
"Attempted to start window maximized using 'zoomed' state."
|
|
)
|
|
except tk.TclError:
|
|
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}."
|
|
)
|
|
|
|
min_win_w = getattr(app_config, "LAYOUT_WINDOW_MIN_WIDTH", 900)
|
|
min_win_h = getattr(app_config, "LAYOUT_WINDOW_MIN_HEIGHT", 650)
|
|
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}.")
|
|
|
|
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 = (
|
|
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))
|
|
|
|
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."
|
|
)
|
|
|
|
# ... (Tutti gli altri metodi di MainWindow, inclusi quelli aggiunti e quelli che
|
|
# non sono stati modificati, vanno qui. Ho incluso nuovamente tutti i metodi
|
|
# per completezza, verificando le correzioni precedenti.)
|
|
|
|
def _delayed_initialization(self):
|
|
if not self.root.winfo_exists():
|
|
module_logger.warning(
|
|
"Root window destroyed before delayed initialization."
|
|
)
|
|
return
|
|
|
|
if MAP_CANVAS_MANAGER_AVAILABLE and MapCanvasManager is not None:
|
|
default_map_bbox = {
|
|
"lat_min": app_config.DEFAULT_BBOX_LAT_MIN,
|
|
"lon_min": app_config.DEFAULT_BBOX_LON_MIN,
|
|
"lat_max": app_config.DEFAULT_BBOX_LAT_MAX,
|
|
"lon_max": app_config.DEFAULT_BBOX_LON_MAX,
|
|
}
|
|
self.root.after(200, self._initialize_map_manager, default_map_bbox)
|
|
else:
|
|
module_logger.error(
|
|
"MapCanvasManager class not available post-init. Map display will be a placeholder."
|
|
)
|
|
self.root.after(
|
|
50,
|
|
lambda: self._update_map_placeholder(
|
|
"Map functionality disabled (Import Error)."
|
|
),
|
|
)
|
|
|
|
if (
|
|
hasattr(self, "track_length_var")
|
|
and self.controller
|
|
and hasattr(self.controller, "set_map_track_length")
|
|
):
|
|
try:
|
|
initial_track_len = self.track_length_var.get()
|
|
if initial_track_len > 0:
|
|
module_logger.debug(
|
|
f"Delayed init: Setting initial track length to {initial_track_len}"
|
|
)
|
|
self.controller.set_map_track_length(initial_track_len)
|
|
except Exception as e:
|
|
module_logger.error(
|
|
f"Error setting initial track length during delayed init: {e}"
|
|
)
|
|
|
|
self.root.after(10, self._on_mode_change)
|
|
module_logger.info(
|
|
"MainWindow fully initialized and displayed after delayed setup."
|
|
)
|
|
|
|
def _initialize_map_manager(self, initial_bbox_for_map: Dict[str, float]):
|
|
if not MAP_CANVAS_MANAGER_AVAILABLE or MapCanvasManager is None:
|
|
module_logger.error(
|
|
"Attempted to initialize map manager, but MapCanvasManager class is not available."
|
|
)
|
|
self._update_map_placeholder("Map Error: MapCanvasManager class missing.")
|
|
if self.controller and hasattr(self.controller, "update_general_map_info"):
|
|
self.controller.update_general_map_info()
|
|
return
|
|
if not self.flight_canvas.winfo_exists():
|
|
module_logger.warning(
|
|
"Flight canvas destroyed before map manager initialization."
|
|
)
|
|
return
|
|
|
|
canvas_w, canvas_h = (
|
|
self.flight_canvas.winfo_width(),
|
|
self.flight_canvas.winfo_height(),
|
|
)
|
|
if canvas_w <= 1:
|
|
canvas_w = self.canvas_width
|
|
if canvas_h <= 1:
|
|
canvas_h = self.canvas_height
|
|
|
|
if canvas_w > 1 and canvas_h > 1:
|
|
module_logger.info(
|
|
f"Canvas is ready ({canvas_w}x{canvas_h}), initializing MapCanvasManager."
|
|
)
|
|
try:
|
|
self.map_manager_instance = MapCanvasManager(
|
|
app_controller=self.controller,
|
|
tk_canvas=self.flight_canvas,
|
|
initial_bbox_dict=initial_bbox_for_map,
|
|
)
|
|
if (
|
|
hasattr(self, "track_length_var")
|
|
and self.controller
|
|
and hasattr(self.controller, "set_map_track_length")
|
|
):
|
|
try:
|
|
current_track_len_val = self.track_length_var.get()
|
|
self.controller.set_map_track_length(current_track_len_val)
|
|
except Exception as e_trk:
|
|
module_logger.error(
|
|
f"Error setting initial track length for map manager: {e_trk}"
|
|
)
|
|
|
|
if self.controller and hasattr(
|
|
self.controller, "update_general_map_info"
|
|
):
|
|
self.controller.update_general_map_info()
|
|
except Exception as e_init:
|
|
module_logger.critical(
|
|
f"Failed to initialize MapCanvasManager: {e_init}", exc_info=True
|
|
)
|
|
self.show_error_message(
|
|
"Map Error", f"Could not initialize map: {e_init}"
|
|
)
|
|
self._update_map_placeholder(
|
|
f"Map Error: Initialization failed.\n{e_init}"
|
|
)
|
|
if self.controller and hasattr(
|
|
self.controller, "update_general_map_info"
|
|
):
|
|
self.controller.update_general_map_info()
|
|
else:
|
|
module_logger.warning(
|
|
f"Canvas not ready for MapCanvasManager init (dims: {canvas_w}x{canvas_h}), retrying..."
|
|
)
|
|
if self.root.winfo_exists():
|
|
self.root.after(300, self._initialize_map_manager, initial_bbox_for_map)
|
|
|
|
def _recreate_map_tools_content(self, parent_frame: ttk.Frame):
|
|
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):
|
|
parent_frame.columnconfigure(
|
|
1, weight=0
|
|
) # Le label dei valori non devono espandersi troppo
|
|
parent_frame.columnconfigure(3, weight=0) # Idem
|
|
|
|
info_row = 0
|
|
# Riga 1: Click Coords
|
|
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
|
|
) # width per dimensione fissa
|
|
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
|
|
|
|
# Riga 2: Click DMS
|
|
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
|
|
|
|
# Riga 3: Map Bounds (N & W)
|
|
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
|
|
|
|
# Riga 4: Map Bounds (S & E)
|
|
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
|
|
|
|
# Riga 5: Target BBox (N & W)
|
|
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
|
|
|
|
# Riga 6: Target BBox (S & E)
|
|
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
|
|
|
|
# Riga 7: Zoom, Size, Flights
|
|
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
|
|
) # Adattato wraplength
|
|
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):
|
|
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:",
|
|
), # Già presente in CanonicalFlightState
|
|
("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)
|
|
) # pady a 0
|
|
value_label1 = ttk.Label(
|
|
parent_frame, text="N/A", wraplength=130
|
|
) # wraplength ridotto
|
|
value_label1.grid(
|
|
row=i, column=1, sticky=tk.W, pady=0, padx=(0, 5)
|
|
) # pady a 0, padx ridotto
|
|
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)
|
|
) # pady a 0, padx aggiunto
|
|
value_label2 = ttk.Label(
|
|
parent_frame, text="N/A", wraplength=130
|
|
) # wraplength ridotto
|
|
value_label2.grid(
|
|
row=i, column=3, sticky=tk.W, pady=0, padx=(0, 0)
|
|
) # pady a 0
|
|
self.flight_detail_labels[key2] = value_label2
|
|
|
|
# ... (TUTTI gli altri metodi di MainWindow come _import_aircraft_db_csv, show_info_message,
|
|
# update_selected_flight_details, _on_closing, _reset_gui_to_stopped_state, etc.
|
|
# devono essere presenti qui come nella versione completa che ti ho fornito precedentemente)
|
|
# Per brevità, non li ripeto di nuovo, ma ASSICURATI che siano tutti inclusi.
|
|
# Ho riportato qui sotto quelli cruciali per il funzionamento e le modifiche recenti.
|
|
|
|
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 self.progress_dialog and self.progress_dialog.winfo_exists():
|
|
self.progress_dialog.destroy() # Chiudi dialog precedente se esiste
|
|
self.progress_dialog = ImportProgressDialog(
|
|
self.root, title=f"Importing: {os.path.basename(filepath)}"
|
|
)
|
|
self.controller.import_aircraft_database_from_file_with_progress(
|
|
filepath, self.progress_dialog
|
|
)
|
|
else:
|
|
module_logger.info("GUI: CSV import cancelled by user.")
|
|
|
|
def show_info_message(self, title: str, message: str):
|
|
module_logger.info(f"Displaying info: Title='{title}', Message='{message}'")
|
|
if hasattr(self, "root") and self.root.winfo_exists():
|
|
try:
|
|
messagebox.showinfo(title, message, parent=self.root)
|
|
except tk.TclError:
|
|
module_logger.warning(
|
|
f"TclError showing info messagebox '{title}'. Root window might be gone."
|
|
)
|
|
|
|
def update_selected_flight_details(self, flight_data: Optional[Dict[str, Any]]):
|
|
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 in MainWindow. Cannot update details."
|
|
)
|
|
return
|
|
|
|
all_possible_keys_in_panel = list(self.flight_detail_labels.keys())
|
|
|
|
for key in all_possible_keys_in_panel:
|
|
label_widget = self.flight_detail_labels.get(key)
|
|
if label_widget and label_widget.winfo_exists():
|
|
value_to_display = "N/A"
|
|
if flight_data and key in flight_data:
|
|
value = flight_data.get(key)
|
|
if (
|
|
value is not None and str(value).strip() != ""
|
|
): # Considera stringa vuota come N/A
|
|
if key in ["baro_altitude_m", "geo_altitude_m"] and isinstance(
|
|
value, (float, int)
|
|
):
|
|
value_to_display = f"{value:.0f} m"
|
|
elif key == "velocity_mps" and isinstance(value, (float, int)):
|
|
value_to_display = (
|
|
f"{value:.1f} m/s ({value * 1.94384:.1f} kts)"
|
|
)
|
|
elif key == "vertical_rate_mps" and isinstance(
|
|
value, (float, int)
|
|
):
|
|
value_to_display = (
|
|
f"{value * 196.85:.0f} ft/min ({value:.1f} m/s)"
|
|
)
|
|
elif key == "true_track_deg" and isinstance(
|
|
value, (float, int)
|
|
):
|
|
value_to_display = f"{value:.1f}°"
|
|
elif key in [
|
|
"timestamp",
|
|
"last_contact_timestamp",
|
|
"firstflightdate",
|
|
"timestamp_metadata",
|
|
]:
|
|
if isinstance(value, (int, float)) and value > 0:
|
|
try:
|
|
dt_obj = datetime.fromtimestamp(
|
|
value, tz=timezone.utc
|
|
)
|
|
value_to_display = dt_obj.strftime(
|
|
"%Y-%m-%d %H:%M:%S Z"
|
|
)
|
|
except (
|
|
OSError,
|
|
OverflowError,
|
|
ValueError,
|
|
): # Per timestamp non validi
|
|
value_to_display = str(value) + " (raw ts)"
|
|
elif (
|
|
isinstance(value, str) and value.strip()
|
|
): # Se è già una stringa di data/ora
|
|
value_to_display = value
|
|
elif isinstance(value, bool):
|
|
value_to_display = str(value)
|
|
elif (
|
|
key == "built_year" and value
|
|
): # Controlla se value non è None o 0
|
|
value_to_display = (
|
|
str(int(value))
|
|
if isinstance(value, (float, int)) and value > 0
|
|
else str(value)
|
|
)
|
|
elif isinstance(value, str) and value.strip():
|
|
value_to_display = value
|
|
elif isinstance(value, (int, float)):
|
|
value_to_display = str(value)
|
|
label_widget.config(text=value_to_display)
|
|
module_logger.debug("Flight details panel updated.")
|
|
|
|
# Includi qui TUTTI gli altri metodi di MainWindow che non sono stati toccati da questa modifica,
|
|
# copiandoli dalla versione precedente che ti ho fornito.
|
|
# Esempio: update_semaphore_and_status, _on_closing, _reset_gui_to_stopped_state, etc.
|
|
# È fondamentale che il file sia completo.
|
|
|
|
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 = False
|
|
if hasattr(self, "root") and self.root.winfo_exists():
|
|
user_confirmed_quit = messagebox.askokcancel(
|
|
"Quit", "Do you want to quit Flight Monitor?", parent=self.root
|
|
)
|
|
else:
|
|
user_confirmed_quit = True
|
|
module_logger.warning(
|
|
"Root window non-existent during _on_closing, proceeding with cleanup."
|
|
)
|
|
|
|
if user_confirmed_quit:
|
|
module_logger.info(
|
|
"User confirmed quit or quit forced. Proceeding with cleanup."
|
|
)
|
|
if self.controller and hasattr(self.controller, "on_application_exit"):
|
|
try:
|
|
self.controller.on_application_exit()
|
|
except Exception as e:
|
|
module_logger.error(
|
|
f"Error during controller.on_application_exit: {e}",
|
|
exc_info=True,
|
|
)
|
|
module_logger.info("Shutting down logging system.")
|
|
try:
|
|
shutdown_logging_system()
|
|
except Exception as e:
|
|
module_logger.error(
|
|
f"Error during shutdown_logging_system: {e}", exc_info=True
|
|
)
|
|
if hasattr(self, "root") and self.root.winfo_exists():
|
|
try:
|
|
self.root.destroy()
|
|
except tk.TclError as e:
|
|
module_logger.error(
|
|
f"TclError destroying root: {e}.", exc_info=False
|
|
)
|
|
except Exception as e:
|
|
module_logger.error(
|
|
f"Unexpected error destroying root: {e}", exc_info=True
|
|
)
|
|
else:
|
|
module_logger.info("User cancelled quit.")
|
|
|
|
def _reset_gui_to_stopped_state(
|
|
self, status_message: Optional[str] = "Monitoring stopped."
|
|
):
|
|
if hasattr(self, "start_button") and self.start_button.winfo_exists():
|
|
self.start_button.config(state=tk.NORMAL)
|
|
if hasattr(self, "stop_button") and self.stop_button.winfo_exists():
|
|
self.stop_button.config(state=tk.DISABLED)
|
|
if hasattr(self, "live_radio") and self.live_radio.winfo_exists():
|
|
self.live_radio.config(state=tk.NORMAL)
|
|
if hasattr(self, "history_radio") and self.history_radio.winfo_exists():
|
|
self.history_radio.config(state=tk.NORMAL)
|
|
self._update_controls_state_based_on_mode_and_tab()
|
|
status_level = GUI_STATUS_OK
|
|
if status_message and (
|
|
"failed" in status_message.lower() or "error" in status_message.lower()
|
|
):
|
|
status_level = GUI_STATUS_ERROR
|
|
elif status_message and "warning" in status_message.lower():
|
|
status_level = GUI_STATUS_WARNING
|
|
if hasattr(self, "root") and self.root.winfo_exists():
|
|
self.update_semaphore_and_status(status_level, status_message)
|
|
else:
|
|
module_logger.debug(
|
|
"Root window gone, skipping status update in _reset_gui_to_stopped_state."
|
|
)
|
|
module_logger.info(
|
|
f"GUI controls reset to stopped state. Status: '{status_message}'"
|
|
)
|
|
|
|
def _should_show_main_placeholder(self) -> bool:
|
|
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()
|
|
):
|
|
module_logger.debug("Flight canvas not available for placeholder update.")
|
|
return
|
|
if not self._should_show_main_placeholder():
|
|
try:
|
|
self.flight_canvas.delete("placeholder_text")
|
|
except tk.TclError:
|
|
pass
|
|
except Exception as e:
|
|
module_logger.warning(
|
|
f"Error deleting placeholder: {e}", exc_info=False
|
|
)
|
|
return
|
|
try:
|
|
self.flight_canvas.delete("placeholder_text")
|
|
canvas_w, canvas_h = (
|
|
self.flight_canvas.winfo_width(),
|
|
self.flight_canvas.winfo_height(),
|
|
)
|
|
if canvas_w <= 1:
|
|
canvas_w = self.canvas_width
|
|
if canvas_h <= 1:
|
|
canvas_h = self.canvas_height
|
|
if canvas_w > 1 and canvas_h > 1:
|
|
self.flight_canvas.create_text(
|
|
canvas_w / 2,
|
|
canvas_h / 2,
|
|
text=text_to_display,
|
|
tags="placeholder_text",
|
|
fill="gray50",
|
|
font=("Arial", 12, "italic"),
|
|
justify=tk.CENTER,
|
|
width=canvas_w - 40,
|
|
)
|
|
else:
|
|
module_logger.warning(
|
|
f"Cannot draw placeholder: Canvas dims invalid ({canvas_w}x{canvas_h})."
|
|
)
|
|
except tk.TclError:
|
|
module_logger.warning(
|
|
"TclError updating map placeholder (canvas might be gone)."
|
|
)
|
|
except Exception as e:
|
|
module_logger.error(
|
|
f"Unexpected error in _update_map_placeholder: {e}", exc_info=True
|
|
)
|
|
|
|
def _on_mode_change(self):
|
|
if not (
|
|
hasattr(self, "mode_var")
|
|
and hasattr(self, "function_notebook")
|
|
and self.function_notebook.winfo_exists()
|
|
):
|
|
module_logger.warning("_on_mode_change: Essential widgets not ready.")
|
|
return
|
|
selected_mode = self.mode_var.get()
|
|
status_message = f"Mode: {selected_mode}. Ready."
|
|
module_logger.info(f"Mode changed to: {selected_mode}")
|
|
try:
|
|
tab_indices = {}
|
|
for i in range(self.function_notebook.index("end")):
|
|
current_tab_widget_name = self.function_notebook.tabs()[i]
|
|
actual_widget = self.function_notebook.nametowidget(
|
|
current_tab_widget_name
|
|
)
|
|
if (
|
|
hasattr(self, "live_bbox_tab_frame")
|
|
and actual_widget == self.live_bbox_tab_frame
|
|
):
|
|
tab_indices["LiveArea"] = i
|
|
elif (
|
|
hasattr(self, "history_tab_frame")
|
|
and actual_widget == self.history_tab_frame
|
|
):
|
|
tab_indices["History"] = i
|
|
elif (
|
|
hasattr(self, "live_airport_tab_frame")
|
|
and actual_widget == self.live_airport_tab_frame
|
|
):
|
|
tab_indices["LiveAirport"] = i
|
|
|
|
live_bbox_idx, history_idx, live_airport_idx = (
|
|
tab_indices.get("LiveArea", -1),
|
|
tab_indices.get("History", -1),
|
|
tab_indices.get("LiveAirport", -1),
|
|
)
|
|
|
|
if selected_mode == "Live":
|
|
if live_bbox_idx != -1:
|
|
self.function_notebook.tab(live_bbox_idx, state="normal")
|
|
if live_airport_idx != -1:
|
|
self.function_notebook.tab(live_airport_idx, state="normal")
|
|
if history_idx != -1:
|
|
self.function_notebook.tab(history_idx, state="disabled")
|
|
current_idx = self.function_notebook.index("current")
|
|
if current_idx == history_idx and live_bbox_idx != -1:
|
|
self.function_notebook.select(live_bbox_idx)
|
|
elif selected_mode == "History":
|
|
if live_bbox_idx != -1:
|
|
self.function_notebook.tab(live_bbox_idx, state="disabled")
|
|
if live_airport_idx != -1:
|
|
self.function_notebook.tab(live_airport_idx, state="disabled")
|
|
if history_idx != -1:
|
|
self.function_notebook.tab(history_idx, state="normal")
|
|
current_idx = self.function_notebook.index("current")
|
|
if current_idx != history_idx and history_idx != -1:
|
|
self.function_notebook.select(history_idx)
|
|
except tk.TclError as e:
|
|
module_logger.warning(f"TclError finding tab IDs: {e}", exc_info=False)
|
|
except Exception as e:
|
|
module_logger.warning(
|
|
f"Error updating func tabs state in mode change: {e}", exc_info=True
|
|
)
|
|
self.clear_all_views_data()
|
|
self._update_controls_state_based_on_mode_and_tab()
|
|
if hasattr(self, "root") and self.root.winfo_exists():
|
|
self.update_semaphore_and_status(GUI_STATUS_OK, status_message)
|
|
|
|
def _on_function_tab_change(self, event: Optional[tk.Event] = None):
|
|
if not (
|
|
hasattr(self, "function_notebook") and self.function_notebook.winfo_exists()
|
|
):
|
|
module_logger.debug("_on_function_tab_change: Function notebook not ready.")
|
|
return
|
|
try:
|
|
tab_text = self.function_notebook.tab(
|
|
self.function_notebook.index("current"), "text"
|
|
)
|
|
module_logger.info(f"GUI: Switched function tab to: {tab_text}")
|
|
placeholder_text_map = "Map Area."
|
|
if "Live: Area Monitor" in tab_text:
|
|
placeholder_text_map = "Map - Live Area. Define area and press Start."
|
|
elif "Live: Airport" in tab_text:
|
|
placeholder_text_map = "Map - Live Airport. (Functionality TBD)"
|
|
elif "History" in tab_text:
|
|
placeholder_text_map = "Map - History Analysis. (Functionality TBD)"
|
|
self._update_map_placeholder(placeholder_text_map)
|
|
self._update_controls_state_based_on_mode_and_tab()
|
|
except (tk.TclError, ValueError) as e:
|
|
module_logger.warning(f"Error on function tab change ({type(e).__name__}).")
|
|
except Exception as e:
|
|
module_logger.error(
|
|
f"Unexpected error in _on_function_tab_change: {e}", exc_info=True
|
|
)
|
|
|
|
def _update_controls_state_based_on_mode_and_tab(self):
|
|
is_live_mode = hasattr(self, "mode_var") and self.mode_var.get() == "Live"
|
|
is_monitoring_active = (
|
|
hasattr(self, "stop_button")
|
|
and self.stop_button.winfo_exists()
|
|
and self.stop_button.cget("state") == tk.NORMAL
|
|
)
|
|
active_func_tab_text = ""
|
|
if hasattr(self, "function_notebook") and self.function_notebook.winfo_exists():
|
|
try:
|
|
active_func_tab_text = self.function_notebook.tab(
|
|
self.function_notebook.index("current"), "text"
|
|
)
|
|
except (tk.TclError, ValueError):
|
|
pass
|
|
enable_bbox_entries = (
|
|
is_live_mode
|
|
and "Live: Area Monitor" in active_func_tab_text
|
|
and not is_monitoring_active
|
|
)
|
|
enable_track_length = (
|
|
is_live_mode and "Live: Area Monitor" in active_func_tab_text
|
|
)
|
|
self._set_bbox_entries_state(tk.NORMAL if enable_bbox_entries else tk.DISABLED)
|
|
if (
|
|
hasattr(self, "track_length_spinbox")
|
|
and self.track_length_spinbox.winfo_exists()
|
|
):
|
|
try:
|
|
final_track_spin_state = (
|
|
tk.DISABLED
|
|
if is_monitoring_active
|
|
else ("readonly" if enable_track_length else tk.DISABLED)
|
|
)
|
|
self.track_length_spinbox.config(state=final_track_spin_state)
|
|
except tk.TclError:
|
|
pass
|
|
module_logger.debug(
|
|
f"Controls state updated. BBox: {'Enabled' if enable_bbox_entries else 'Disabled'}. TrackLength: {'Enabled' if enable_track_length and not is_monitoring_active else 'Disabled'}"
|
|
)
|
|
|
|
def _on_view_tab_change(self, event: Optional[tk.Event] = None):
|
|
if not (hasattr(self, "views_notebook") and self.views_notebook.winfo_exists()):
|
|
module_logger.debug("_on_view_tab_change: Views notebook not ready.")
|
|
return
|
|
try:
|
|
tab_text = self.views_notebook.tab(
|
|
self.views_notebook.index("current"), "text"
|
|
)
|
|
module_logger.info(f"GUI: Switched view tab to: {tab_text}")
|
|
except (tk.TclError, ValueError):
|
|
module_logger.warning(f"Error on view tab change ({type(e).__name__}).")
|
|
except Exception as e:
|
|
module_logger.warning(f"Error on view tab change: {e}", exc_info=True)
|
|
|
|
def _set_bbox_entries_state(self, state: str):
|
|
entries_names = [
|
|
"lat_min_entry",
|
|
"lon_min_entry",
|
|
"lat_max_entry",
|
|
"lon_max_entry",
|
|
]
|
|
entries = [getattr(self, name, None) for name in entries_names]
|
|
if all(e and hasattr(e, "winfo_exists") and e.winfo_exists() for e in entries):
|
|
try:
|
|
for entry in entries:
|
|
entry.config(state=state) # type: ignore
|
|
except tk.TclError:
|
|
module_logger.warning(
|
|
"TclError setting BBox entries state (widgets gone)."
|
|
)
|
|
|
|
def _start_monitoring(self):
|
|
if not hasattr(self, "mode_var"):
|
|
module_logger.error("Start: mode_var N/A.")
|
|
self.show_error_message("Internal Error", "App mode N/A.")
|
|
return
|
|
selected_mode = self.mode_var.get()
|
|
module_logger.info(f"GUI: Start {selected_mode} monitoring.")
|
|
active_func_tab_text = "Unknown Tab"
|
|
if hasattr(self, "function_notebook") and self.function_notebook.winfo_exists():
|
|
try:
|
|
active_func_tab_text = self.function_notebook.tab(
|
|
self.function_notebook.index("current"), "text"
|
|
)
|
|
except Exception:
|
|
module_logger.warning("Could not get active function tab for start.")
|
|
if hasattr(self, "start_button") and self.start_button.winfo_exists():
|
|
self.start_button.config(state=tk.DISABLED)
|
|
if hasattr(self, "stop_button") and self.stop_button.winfo_exists():
|
|
self.stop_button.config(state=tk.NORMAL)
|
|
if hasattr(self, "live_radio") and self.live_radio.winfo_exists():
|
|
self.live_radio.config(state=tk.DISABLED)
|
|
if hasattr(self, "history_radio") and self.history_radio.winfo_exists():
|
|
self.history_radio.config(state=tk.DISABLED)
|
|
self._update_controls_state_based_on_mode_and_tab()
|
|
if not self.controller:
|
|
module_logger.critical("Controller N/A.")
|
|
self._reset_gui_to_stopped_state("Critical Error: Controller unavailable.")
|
|
self.show_error_message("Internal Error", "App controller missing.")
|
|
return
|
|
if selected_mode == "Live":
|
|
if "Live: Area Monitor" in active_func_tab_text:
|
|
bbox = self.get_bounding_box_from_gui()
|
|
if bbox:
|
|
self.controller.start_live_monitoring(bbox)
|
|
else:
|
|
self._reset_gui_to_stopped_state("Start failed: Invalid BBox.")
|
|
else:
|
|
module_logger.warning(
|
|
f"Start in Live mode, but tab '{active_func_tab_text}' is not 'Live: Area Monitor'. GUI state issue."
|
|
)
|
|
self.update_semaphore_and_status(
|
|
GUI_STATUS_WARNING,
|
|
f"Start not supported on '{active_func_tab_text}'.",
|
|
)
|
|
self._reset_gui_to_stopped_state(
|
|
f"Start not supported on {active_func_tab_text}."
|
|
)
|
|
elif selected_mode == "History":
|
|
if "History" in active_func_tab_text:
|
|
self.controller.start_history_monitoring()
|
|
else:
|
|
module_logger.warning(
|
|
f"Start in History mode, but tab '{active_func_tab_text}' is not 'History'. GUI state issue."
|
|
)
|
|
self.update_semaphore_and_status(
|
|
GUI_STATUS_WARNING,
|
|
f"Start not supported on '{active_func_tab_text}'.",
|
|
)
|
|
self._reset_gui_to_stopped_state(
|
|
f"Start not supported on {active_func_tab_text}."
|
|
)
|
|
|
|
def _stop_monitoring(self):
|
|
module_logger.info("GUI: User requested to stop monitoring.")
|
|
selected_mode = self.mode_var.get() if hasattr(self, "mode_var") else "Unknown"
|
|
if not self.controller:
|
|
module_logger.error("Controller N/A to stop.")
|
|
self._reset_gui_to_stopped_state("Error: Controller missing.")
|
|
return
|
|
if selected_mode == "Live":
|
|
self.controller.stop_live_monitoring()
|
|
elif selected_mode == "History":
|
|
self.controller.stop_history_monitoring()
|
|
if hasattr(self, "root") and self.root.winfo_exists():
|
|
self.update_semaphore_and_status(
|
|
GUI_STATUS_FETCHING, f"{selected_mode} monitoring stopping..."
|
|
)
|
|
|
|
def get_bounding_box_from_gui(self) -> Optional[Dict[str, float]]:
|
|
module_logger.debug("Getting BBox from GUI.")
|
|
req_vars_names = ["lat_min_var", "lon_min_var", "lat_max_var", "lon_max_var"]
|
|
if not all(hasattr(self, v_name) for v_name in req_vars_names):
|
|
module_logger.error("BBox StringVars N/A.")
|
|
self.show_error_message(
|
|
"Internal Error", "BBox input fields are not available."
|
|
)
|
|
return None
|
|
try:
|
|
vals_str = [getattr(self, v_name).get() for v_name in req_vars_names]
|
|
if not all(s.strip() for s in vals_str):
|
|
module_logger.error("One or more BBox fields are empty.")
|
|
self.show_error_message(
|
|
"Input Error", "All Bounding Box fields are required."
|
|
)
|
|
return None
|
|
lat_min, lon_min, lat_max, lon_max = map(float, vals_str)
|
|
except ValueError:
|
|
module_logger.error("Invalid number format in BBox fields.")
|
|
self.show_error_message(
|
|
"Input Error", "Bounding Box coordinates must be valid numbers."
|
|
)
|
|
return None
|
|
except Exception as e:
|
|
module_logger.error(
|
|
f"Unexpected error reading BBox fields: {e}", exc_info=True
|
|
)
|
|
self.show_error_message(
|
|
"Internal Error",
|
|
"An unexpected error occurred while reading BBox fields.",
|
|
)
|
|
return None
|
|
bbox_dict = {
|
|
"lat_min": lat_min,
|
|
"lon_min": lon_min,
|
|
"lat_max": lat_max,
|
|
"lon_max": lon_max,
|
|
}
|
|
if not _is_valid_bbox_dict(bbox_dict):
|
|
self.show_error_message(
|
|
"Input Error",
|
|
"Invalid Bounding Box range or order. Ensure Lat Min < Lat Max, Lon Min < Lon Max, and valid geo limits.",
|
|
)
|
|
return None
|
|
return bbox_dict
|
|
|
|
def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]):
|
|
if not hasattr(self, "lat_min_var"):
|
|
module_logger.warning("BBox GUI StringVars not available for update.")
|
|
return
|
|
if bbox_dict and _is_valid_bbox_dict(bbox_dict):
|
|
decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5)
|
|
try:
|
|
self.lat_min_var.set(f"{bbox_dict['lat_min']:.{decimals}f}")
|
|
self.lon_min_var.set(f"{bbox_dict['lon_min']:.{decimals}f}")
|
|
self.lat_max_var.set(f"{bbox_dict['lat_max']:.{decimals}f}")
|
|
self.lon_max_var.set(f"{bbox_dict['lon_max']:.{decimals}f}")
|
|
except tk.TclError:
|
|
module_logger.warning(
|
|
"TclError updating BBox GUI fields (widgets might be gone)."
|
|
)
|
|
except Exception as e:
|
|
module_logger.error(
|
|
f"Error updating BBox GUI fields: {e}", exc_info=True
|
|
)
|
|
else:
|
|
module_logger.warning(
|
|
f"Invalid or empty bbox_dict provided for GUI update: {bbox_dict}. Setting fields to N/A."
|
|
)
|
|
try:
|
|
self.lat_min_var.set("N/A")
|
|
self.lon_min_var.set("N/A")
|
|
self.lat_max_var.set("N/A")
|
|
self.lon_max_var.set("N/A")
|
|
except tk.TclError:
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
def display_flights_on_canvas(
|
|
self,
|
|
flight_states: List[CanonicalFlightState],
|
|
_active_bbox_context: Optional[Dict[str, float]],
|
|
):
|
|
if not (
|
|
hasattr(self, "map_manager_instance")
|
|
and self.map_manager_instance
|
|
and MAP_CANVAS_MANAGER_AVAILABLE
|
|
):
|
|
module_logger.warning("MapCanvasManager N/A, cannot display flights.")
|
|
if hasattr(self, "root") and self.root.winfo_exists():
|
|
self._update_map_placeholder("Map N/A to display flights.")
|
|
return
|
|
try:
|
|
self.map_manager_instance.update_flights_on_map(flight_states)
|
|
if not flight_states and self._should_show_main_placeholder():
|
|
self._update_map_placeholder("No flights in the selected area.")
|
|
except Exception as e:
|
|
module_logger.error(
|
|
f"Error updating flights on map via map_manager: {e}", exc_info=True
|
|
)
|
|
self.show_error_message(
|
|
"Map Display Error", "Could not update flights on map."
|
|
)
|
|
|
|
def clear_all_views_data(self):
|
|
module_logger.info("Clearing data from all views.")
|
|
if (
|
|
hasattr(self, "map_manager_instance")
|
|
and self.map_manager_instance
|
|
and MAP_CANVAS_MANAGER_AVAILABLE
|
|
):
|
|
try:
|
|
self.map_manager_instance.clear_map_display()
|
|
except Exception as e:
|
|
module_logger.warning(f"Error clearing map via map manager: {e}")
|
|
elif self._should_show_main_placeholder():
|
|
mode = self.mode_var.get() if hasattr(self, "mode_var") else "Unknown"
|
|
text = f"Map - {mode}. Data cleared."
|
|
if mode == "Live":
|
|
text = "Map - Live. Define area and Start."
|
|
elif mode == "History":
|
|
text = "Map - History. (TBD)"
|
|
self._update_map_placeholder(text)
|
|
if hasattr(self, "update_selected_flight_details"):
|
|
self.update_selected_flight_details(None)
|
|
|
|
def show_error_message(self, title: str, message: str):
|
|
module_logger.error(f"Displaying error: Title='{title}', Message='{message}'")
|
|
status_msg = f"Error: {message[:70]}{'...' if len(message)>70 else ''}"
|
|
if hasattr(self, "root") and self.root.winfo_exists():
|
|
self.update_semaphore_and_status(GUI_STATUS_ERROR, status_msg)
|
|
try:
|
|
messagebox.showerror(title, message, parent=self.root)
|
|
except tk.TclError:
|
|
module_logger.warning(
|
|
f"TclError showing error messagebox '{title}'. Root window might be gone."
|
|
)
|
|
else:
|
|
module_logger.warning(
|
|
"Root window not available, skipping status update and messagebox for error."
|
|
)
|
|
print(f"ERROR (No GUI): {title} - {message}", flush=True)
|
|
|
|
def show_map_context_menu(
|
|
self, latitude: float, longitude: float, screen_x: int, screen_y: int
|
|
):
|
|
module_logger.info(
|
|
f"MainWindow: Request context menu for Lat {latitude:.4f}, Lon {longitude:.4f}"
|
|
)
|
|
if (
|
|
hasattr(self, "map_manager_instance")
|
|
and self.map_manager_instance
|
|
and hasattr(self.map_manager_instance, "show_map_context_menu_from_gui")
|
|
):
|
|
try:
|
|
self.map_manager_instance.show_map_context_menu_from_gui(
|
|
latitude, longitude, screen_x, screen_y
|
|
)
|
|
except Exception as e:
|
|
module_logger.error(
|
|
f"Error delegating context menu to MapCanvasManager: {e}",
|
|
exc_info=True,
|
|
)
|
|
else:
|
|
module_logger.warning(
|
|
"Controller or context menu handler N/A for MainWindow context menu."
|
|
)
|
|
|
|
def update_clicked_map_info(
|
|
self,
|
|
lat_deg: Optional[float],
|
|
lon_deg: Optional[float],
|
|
lat_dms: str,
|
|
lon_dms: str,
|
|
):
|
|
if not hasattr(self, "info_lat_value"):
|
|
module_logger.warning(
|
|
"Map info panel (click details) widgets not available for update."
|
|
)
|
|
return
|
|
decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5)
|
|
try:
|
|
if hasattr(self, "info_lat_value") and self.info_lat_value.winfo_exists():
|
|
self.info_lat_value.config(
|
|
text=f"{lat_deg:.{decimals}f}" if lat_deg is not None else "N/A"
|
|
)
|
|
if hasattr(self, "info_lon_value") and self.info_lon_value.winfo_exists():
|
|
self.info_lon_value.config(
|
|
text=f"{lon_deg:.{decimals}f}" if lon_deg is not None else "N/A"
|
|
)
|
|
if (
|
|
hasattr(self, "info_lat_dms_value")
|
|
and self.info_lat_dms_value.winfo_exists()
|
|
):
|
|
self.info_lat_dms_value.config(text=lat_dms or "N/A")
|
|
if (
|
|
hasattr(self, "info_lon_dms_value")
|
|
and self.info_lon_dms_value.winfo_exists()
|
|
):
|
|
self.info_lon_dms_value.config(text=lon_dms or "N/A")
|
|
except tk.TclError:
|
|
module_logger.warning(
|
|
"TclError updating clicked map info (widgets might be gone)."
|
|
)
|
|
except Exception as e:
|
|
module_logger.error(f"Error updating clicked map info: {e}", exc_info=True)
|
|
|
|
def update_general_map_info_display(
|
|
self,
|
|
zoom: Optional[int],
|
|
map_size_str: str,
|
|
map_geo_bounds: Optional[Tuple[float, float, float, float]],
|
|
target_bbox_input: Optional[Dict[str, float]],
|
|
flight_count: Optional[int],
|
|
):
|
|
if not hasattr(self, "info_zoom_value"):
|
|
module_logger.warning(
|
|
"Map info panel (general info) widgets not available for update."
|
|
)
|
|
return
|
|
|
|
decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5)
|
|
try:
|
|
mw, ms, me, mn = "N/A", "N/A", "N/A", "N/A"
|
|
if map_geo_bounds:
|
|
mw, ms, me, mn = (f"{c:.{decimals}f}" for c in map_geo_bounds)
|
|
if (
|
|
hasattr(self, "info_map_bounds_w")
|
|
and self.info_map_bounds_w.winfo_exists()
|
|
):
|
|
self.info_map_bounds_w.config(text=mw)
|
|
if (
|
|
hasattr(self, "info_map_bounds_s")
|
|
and self.info_map_bounds_s.winfo_exists()
|
|
):
|
|
self.info_map_bounds_s.config(text=ms)
|
|
if (
|
|
hasattr(self, "info_map_bounds_e")
|
|
and self.info_map_bounds_e.winfo_exists()
|
|
):
|
|
self.info_map_bounds_e.config(text=me)
|
|
if (
|
|
hasattr(self, "info_map_bounds_n")
|
|
and self.info_map_bounds_n.winfo_exists()
|
|
):
|
|
self.info_map_bounds_n.config(text=mn)
|
|
|
|
tw, ts, te, tn = "N/A", "N/A", "N/A", "N/A"
|
|
color_target_bbox_text = BBOX_COLOR_NA
|
|
status_target_bbox = "N/A"
|
|
if target_bbox_input and _is_valid_bbox_dict(target_bbox_input):
|
|
tw = f"{target_bbox_input['lon_min']:.{decimals}f}"
|
|
ts = f"{target_bbox_input['lat_min']:.{decimals}f}"
|
|
te = f"{target_bbox_input['lon_max']:.{decimals}f}"
|
|
tn = f"{target_bbox_input['lat_max']:.{decimals}f}"
|
|
if map_geo_bounds:
|
|
status_target_bbox = self._is_bbox_inside_bbox(
|
|
target_bbox_input, map_geo_bounds
|
|
)
|
|
if status_target_bbox == "Inside":
|
|
color_target_bbox_text = BBOX_COLOR_INSIDE
|
|
elif status_target_bbox == "Partial":
|
|
color_target_bbox_text = BBOX_COLOR_PARTIAL
|
|
else:
|
|
color_target_bbox_text = BBOX_COLOR_OUTSIDE
|
|
|
|
bbox_labels_target_names = [
|
|
"info_target_bbox_w",
|
|
"info_target_bbox_s",
|
|
"info_target_bbox_e",
|
|
"info_target_bbox_n",
|
|
]
|
|
bbox_labels_target_values = [tw, ts, te, tn]
|
|
for name, val_str in zip(
|
|
bbox_labels_target_names, bbox_labels_target_values
|
|
):
|
|
if hasattr(self, name):
|
|
label_widget = getattr(self, name)
|
|
if label_widget and label_widget.winfo_exists():
|
|
label_widget.config(
|
|
text=val_str, foreground=color_target_bbox_text
|
|
)
|
|
|
|
if hasattr(self, "info_zoom_value") and self.info_zoom_value.winfo_exists():
|
|
self.info_zoom_value.config(
|
|
text=str(zoom) if zoom is not None else "N/A"
|
|
)
|
|
if (
|
|
hasattr(self, "info_map_size_value")
|
|
and self.info_map_size_value.winfo_exists()
|
|
):
|
|
self.info_map_size_value.config(text=map_size_str or "N/A")
|
|
if (
|
|
hasattr(self, "info_flight_count_value")
|
|
and self.info_flight_count_value.winfo_exists()
|
|
):
|
|
self.info_flight_count_value.config(
|
|
text=str(flight_count) if flight_count is not None else "N/A"
|
|
)
|
|
|
|
module_logger.debug(
|
|
f"General map info panel updated: Zoom={zoom}, Size='{map_size_str}', Flights={flight_count}, TargetBBoxStatus='{status_target_bbox}'"
|
|
)
|
|
except tk.TclError:
|
|
module_logger.warning(
|
|
"TclError updating general map info (widgets might be gone)."
|
|
)
|
|
except Exception as e:
|
|
module_logger.error(f"Error updating general map info: {e}", exc_info=True)
|
|
|
|
def _is_bbox_inside_bbox(
|
|
self,
|
|
inner_bbox_dict: Dict[str, float],
|
|
outer_bbox_tuple: Tuple[float, float, float, float],
|
|
) -> str:
|
|
if not _is_valid_bbox_dict(inner_bbox_dict):
|
|
return "N/A"
|
|
if not (
|
|
outer_bbox_tuple
|
|
and len(outer_bbox_tuple) == 4
|
|
and all(isinstance(c, (int, float)) for c in outer_bbox_tuple)
|
|
):
|
|
return "N/A"
|
|
outer_dict_temp = {
|
|
"lon_min": outer_bbox_tuple[0],
|
|
"lat_min": outer_bbox_tuple[1],
|
|
"lon_max": outer_bbox_tuple[2],
|
|
"lat_max": outer_bbox_tuple[3],
|
|
}
|
|
eps = 1e-6
|
|
fully_inside = (
|
|
inner_bbox_dict["lon_min"] >= outer_dict_temp["lon_min"] - eps
|
|
and inner_bbox_dict["lat_min"] >= outer_dict_temp["lat_min"] - eps
|
|
and inner_bbox_dict["lon_max"] <= outer_dict_temp["lon_max"] + eps
|
|
and inner_bbox_dict["lat_max"] <= outer_dict_temp["lat_max"] + eps
|
|
)
|
|
if fully_inside:
|
|
return "Inside"
|
|
no_overlap = (
|
|
inner_bbox_dict["lon_max"] <= outer_dict_temp["lon_min"] + eps
|
|
or inner_bbox_dict["lon_min"] >= outer_dict_temp["lon_max"] - eps
|
|
or inner_bbox_dict["lat_max"] <= outer_dict_temp["lat_min"] + eps
|
|
or inner_bbox_dict["lat_min"] >= outer_dict_temp["lat_max"] - eps
|
|
)
|
|
if no_overlap:
|
|
return "Outside"
|
|
return "Partial"
|
|
|
|
def _map_zoom_in(self):
|
|
module_logger.debug("GUI: Map Zoom In button pressed.")
|
|
if self.controller and hasattr(self.controller, "map_zoom_in"):
|
|
self.controller.map_zoom_in()
|
|
else:
|
|
module_logger.warning("Controller or map_zoom_in N/A.")
|
|
self.show_error_message(
|
|
"Action Failed", "Map zoom control is not available."
|
|
)
|
|
|
|
def _map_zoom_out(self):
|
|
module_logger.debug("GUI: Map Zoom Out button pressed.")
|
|
if self.controller and hasattr(self.controller, "map_zoom_out"):
|
|
self.controller.map_zoom_out()
|
|
else:
|
|
module_logger.warning("Controller or map_zoom_out N/A.")
|
|
self.show_error_message(
|
|
"Action Failed", "Map zoom control is not available."
|
|
)
|
|
|
|
def _map_pan(self, direction: str):
|
|
module_logger.debug(f"GUI: Map Pan '{direction}' button pressed.")
|
|
if self.controller and hasattr(self.controller, "map_pan_direction"):
|
|
self.controller.map_pan_direction(direction)
|
|
else:
|
|
module_logger.warning("Controller or map_pan_direction N/A.")
|
|
self.show_error_message(
|
|
"Action Failed", "Map pan control is not available."
|
|
)
|
|
|
|
def _map_center_and_fit(self):
|
|
module_logger.debug("GUI: Map Center & Fit Patch button pressed.")
|
|
try:
|
|
lat_str, lon_str, patch_str = (
|
|
self.center_lat_var.get(),
|
|
self.center_lon_var.get(),
|
|
self.center_patch_size_var.get(),
|
|
)
|
|
if not lat_str.strip() or not lon_str.strip() or not patch_str.strip():
|
|
self.show_error_message(
|
|
"Input Error",
|
|
"Latitude, Longitude, and Patch Size are required for centering.",
|
|
)
|
|
return
|
|
lat, lon, patch_size_km = float(lat_str), float(lon_str), float(patch_str)
|
|
if not (-90.0 <= lat <= 90.0):
|
|
self.show_error_message(
|
|
"Input Error", "Latitude must be between -90 and 90."
|
|
)
|
|
return
|
|
if not (-180.0 <= lon <= 180.0):
|
|
self.show_error_message(
|
|
"Input Error", "Longitude must be between -180 and 180."
|
|
)
|
|
return
|
|
if patch_size_km <= 0:
|
|
self.show_error_message(
|
|
"Input Error", "Patch size must be a positive number (km)."
|
|
)
|
|
return
|
|
if self.controller and hasattr(
|
|
self.controller, "map_center_on_coords_and_fit_patch"
|
|
):
|
|
self.controller.map_center_on_coords_and_fit_patch(
|
|
lat, lon, patch_size_km
|
|
)
|
|
else:
|
|
module_logger.warning(
|
|
"Controller or map_center_on_coords_and_fit_patch N/A."
|
|
)
|
|
self.show_error_message(
|
|
"Action Failed", "Map centering control is not available."
|
|
)
|
|
except ValueError:
|
|
self.show_error_message(
|
|
"Input Error",
|
|
"Latitude, Longitude, and Patch Size must be valid numbers.",
|
|
)
|
|
except Exception as e:
|
|
module_logger.error(f"Error in _map_center_and_fit: {e}", exc_info=True)
|
|
self.show_error_message("Error", f"An unexpected error occurred: {e}")
|
|
|
|
def _on_track_length_change(self):
|
|
if not hasattr(self, "track_length_var") or not hasattr(self, "controller"):
|
|
module_logger.debug(
|
|
"Track length var or controller not ready for change notification."
|
|
)
|
|
return
|
|
try:
|
|
new_length = self.track_length_var.get()
|
|
if not (2 <= new_length <= 100):
|
|
module_logger.warning(
|
|
f"Track length value {new_length} out of spinbox range. Ignoring change."
|
|
)
|
|
return
|
|
module_logger.info(f"GUI: Track length changed by user to: {new_length}")
|
|
if self.controller and hasattr(self.controller, "set_map_track_length"):
|
|
self.controller.set_map_track_length(new_length)
|
|
else:
|
|
module_logger.warning(
|
|
"Controller or set_map_track_length method not available."
|
|
)
|
|
except tk.TclError:
|
|
module_logger.warning(
|
|
"TclError getting track length. Value might be invalid (e.g. non-integer)."
|
|
)
|
|
except Exception as e:
|
|
module_logger.error(f"Error in _on_track_length_change: {e}", exc_info=True)
|