1872 lines
79 KiB
Python
1872 lines
79 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 standard logging
|
|
import os
|
|
from datetime import datetime, timezone
|
|
import webbrowser
|
|
|
|
from ..data import config as app_config
|
|
|
|
# MODIFIED: Import add_tkinter_handler and shutdown_logging_system from our logger module
|
|
# Also import get_logger for module-level logging
|
|
# Import LOGGING_CONFIG to pass to add_tkinter_handler
|
|
from ..utils.logger import (
|
|
get_logger,
|
|
add_tkinter_handler,
|
|
shutdown_logging_system,
|
|
) # MODIFIED HERE
|
|
from ..data.logging_config import LOGGING_CONFIG # MODIFIED HERE
|
|
|
|
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
|
|
# Use standard print for early critical errors if logger not fully up
|
|
print(
|
|
f"CRITICAL ERROR in MainWindow import: Failed to import MapCanvasManager or map_utils: {e_map_import}. Map functionality will be disabled.",
|
|
flush=True,
|
|
)
|
|
|
|
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.",
|
|
flush=True,
|
|
)
|
|
|
|
module_logger = get_logger(__name__) # Get logger for this module
|
|
|
|
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
|
|
|
|
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)
|
|
|
|
# MODIFIED: Call add_tkinter_handler instead of setup_logging
|
|
# This assumes setup_basic_logging has already been called in __main__.py
|
|
if self.log_text_widget and self.root: # Ensure widgets are created
|
|
add_tkinter_handler( # MODIFIED HERE
|
|
gui_log_widget=self.log_text_widget,
|
|
root_tk_instance_for_gui_handler=self.root, # Pass self.root
|
|
logging_config_dict=LOGGING_CONFIG, # Pass the imported config
|
|
)
|
|
else:
|
|
module_logger.error(
|
|
"log_text_widget or root not available in MainWindow init for logger setup."
|
|
)
|
|
|
|
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 _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() is called here to ensure all handlers are closed
|
|
# and remaining logs are processed before the application fully exits.
|
|
shutdown_logging_system() # MODIFIED: Ensured this is called
|
|
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
|
|
else:
|
|
module_logger.info("User cancelled quit.")
|
|
|
|
# ... (il resto dei metodi di MainWindow rimane invariato) ...
|
|
# ... (_delayed_initialization, _initialize_map_manager, _recreate_map_tools_content, etc.)
|
|
# COPIA QUI TUTTI GLI ALTRI METODI DI MainWindow OMETTENDO SOLO __init__ e _on_closing GIA' RIPORTATI SOPRA
|
|
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}"
|
|
)
|
|
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."
|
|
)
|
|
|
|
def _delayed_initialization(self):
|
|
if not self.root.winfo_exists():
|
|
module_logger.warning(
|
|
"Root window destroyed before delayed initialization."
|
|
)
|
|
return
|
|
|
|
# MODIFIED: Moved set_map_track_length to be called AFTER map_manager_instance is created
|
|
# inside _initialize_map_manager or ensure controller handles it if map_manager not ready.
|
|
# For now, we'll ensure _initialize_map_manager handles the initial track length.
|
|
|
|
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,
|
|
}
|
|
# Pass initial track length to _initialize_map_manager
|
|
initial_track_len = (
|
|
self.track_length_var.get()
|
|
if hasattr(self, "track_length_var")
|
|
else DEFAULT_TRACK_LENGTH
|
|
)
|
|
self.root.after(
|
|
200, self._initialize_map_manager, default_map_bbox, initial_track_len
|
|
)
|
|
else:
|
|
module_logger.error("MapCanvasManager class not available post-init.")
|
|
self.root.after(
|
|
50,
|
|
lambda: self._update_map_placeholder(
|
|
"Map functionality disabled (Import Error)."
|
|
),
|
|
)
|
|
|
|
self.root.after(
|
|
10, self._on_mode_change
|
|
) # Ensure mode change updates UI correctly
|
|
module_logger.info("MainWindow fully initialized.")
|
|
|
|
def _initialize_map_manager(
|
|
self, initial_bbox_for_map: Dict[str, float], initial_track_length: int
|
|
): # Added initial_track_length
|
|
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():
|
|
module_logger.warning(
|
|
"Flight canvas does not exist. Cannot initialize MapCanvasManager."
|
|
)
|
|
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,
|
|
)
|
|
# MODIFIED: Set track length AFTER map_manager_instance is created
|
|
if self.controller and hasattr(self.controller, "set_map_track_length"):
|
|
try:
|
|
self.controller.set_map_track_length(initial_track_length)
|
|
module_logger.info(
|
|
f"Initial map track length set to {initial_track_length} via controller."
|
|
)
|
|
except Exception as e_trk:
|
|
module_logger.error(
|
|
f"Error setting initial track length for map manager: {e_trk}"
|
|
)
|
|
else:
|
|
module_logger.warning(
|
|
"Controller or set_map_track_length not available for initial setup."
|
|
)
|
|
|
|
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(): # Retry if canvas not sized yet
|
|
module_logger.info(
|
|
"Canvas not sized yet, retrying map manager initialization."
|
|
)
|
|
self.root.after(
|
|
300,
|
|
self._initialize_map_manager,
|
|
initial_bbox_for_map,
|
|
initial_track_length,
|
|
)
|
|
|
|
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) # Smaller weight for value labels
|
|
parent_frame.columnconfigure(3, weight=0) # Smaller weight for value labels
|
|
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):
|
|
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]]):
|
|
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: # Fallback to geo if baro is N/A
|
|
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"
|
|
|
|
# Show geo_altitude only if it's significantly different from baro, or if baro is N/A
|
|
if (
|
|
baro_alt_val is not None
|
|
and geo_alt_val is not None
|
|
and abs(baro_alt_val - geo_alt_val) > 1
|
|
) or (baro_alt_val is None and geo_alt_val is not None):
|
|
self.flight_detail_labels["geo_altitude_m"].config(
|
|
text=alt_geo_text
|
|
)
|
|
# else, leave it N/A if baro is present and geo is similar or N/A
|
|
|
|
for key, label_widget in self.flight_detail_labels.items():
|
|
if key in ["baro_altitude_m", "geo_altitude_m"]: # Already handled
|
|
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", # String from DB
|
|
"timestamp_metadata", # Timestamp from DB
|
|
]:
|
|
if (
|
|
isinstance(value, (int, float)) and value > 0
|
|
): # If it's a raw timestamp
|
|
try:
|
|
formatted_value = datetime.fromtimestamp(
|
|
value, tz=timezone.utc
|
|
).strftime("%Y-%m-%d %H:%M:%S Z")
|
|
except: # Should not happen with valid ts
|
|
formatted_value = str(value) + " (raw ts)"
|
|
elif (
|
|
isinstance(value, str) and value.strip()
|
|
): # If it's already a date string
|
|
formatted_value = value
|
|
elif isinstance(value, bool):
|
|
formatted_value = str(value)
|
|
elif (
|
|
key == "built_year" and value
|
|
): # Ensure built_year without decimals
|
|
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: # No flight_data
|
|
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.")
|
|
|
|
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: # Can happen if called during shutdown
|
|
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: # Can happen if called during shutdown
|
|
pass
|
|
|
|
# Log the status update using the module logger (already configured)
|
|
log_level_to_use = logging.INFO # Default
|
|
if status_level == GUI_STATUS_ERROR:
|
|
log_level_to_use = logging.ERROR
|
|
elif status_level == GUI_STATUS_PERMANENT_FAILURE:
|
|
log_level_to_use = logging.CRITICAL
|
|
elif status_level == GUI_STATUS_WARNING:
|
|
log_level_to_use = logging.WARNING
|
|
elif status_level in [GUI_STATUS_FETCHING, GUI_STATUS_OK, GUI_STATUS_UNKNOWN]:
|
|
log_level_to_use = logging.DEBUG
|
|
module_logger.log(
|
|
log_level_to_use,
|
|
f"GUI Status Update: Level='{status_level}', Message='{message}'",
|
|
)
|
|
|
|
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)
|
|
|
|
# Re-enable mode radio buttons
|
|
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() # Update BBox/Track based on new state
|
|
|
|
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 if status_message else "System Ready."
|
|
)
|
|
|
|
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()
|
|
): # Map manager is active, clear placeholder
|
|
try:
|
|
self.flight_canvas.delete("placeholder_text")
|
|
except: # tk.TclError if item doesn't exist or canvas gone
|
|
pass
|
|
return
|
|
|
|
try:
|
|
self.flight_canvas.delete("placeholder_text") # Clear previous one
|
|
canvas_w, canvas_h = (
|
|
self.flight_canvas.winfo_width(),
|
|
self.flight_canvas.winfo_height(),
|
|
)
|
|
if canvas_w <= 1:
|
|
canvas_w = self.canvas_width # Use fallback if not sized
|
|
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, # Wrap text
|
|
)
|
|
except tk.TclError: # Canvas might be gone
|
|
pass
|
|
except Exception as e_placeholder_draw:
|
|
module_logger.error(f"Error drawing placeholder: {e_placeholder_draw}")
|
|
|
|
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."
|
|
module_logger.info(f"GUI: Mode changed to {selected_mode}")
|
|
|
|
try:
|
|
# Get tab indices by their text name for robustness
|
|
tab_indices = {}
|
|
for i in range(self.function_notebook.index("end")):
|
|
tab_text = self.function_notebook.tab(i, "text")
|
|
tab_indices[tab_text] = i
|
|
|
|
live_bbox_idx = tab_indices.get("Live: Area Monitor", -1)
|
|
history_idx = tab_indices.get("History", -1)
|
|
live_airport_idx = 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")
|
|
# Switch to the first live tab if history was selected
|
|
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")
|
|
# Switch to history tab if not already selected
|
|
if (
|
|
self.function_notebook.index("current") != history_idx
|
|
and history_idx != -1
|
|
):
|
|
self.function_notebook.select(history_idx)
|
|
except (
|
|
Exception
|
|
) as e: # tk.TclError can happen if notebook/tabs don't exist as expected
|
|
module_logger.warning(
|
|
f"Error updating function notebook tabs on mode change: {e}",
|
|
exc_info=True,
|
|
)
|
|
|
|
self.clear_all_views_data() # Clear map/table when mode changes
|
|
self._update_controls_state_based_on_mode_and_tab() # Update BBox/Track state
|
|
|
|
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"
|
|
)
|
|
module_logger.info(f"GUI: Switched function tab to: {tab_text}")
|
|
placeholder_text_map = "Map Area." # Default
|
|
if "Live: Area Monitor" in tab_text:
|
|
placeholder_text_map = "Map - Live Area. Define area and press Start."
|
|
elif "Live: Airport" in tab_text:
|
|
placeholder_text_map = "Map - Live Airport. (Coming Soon)"
|
|
elif "History" in tab_text:
|
|
placeholder_text_map = "Map - History Analysis. (Coming Soon)"
|
|
|
|
self._update_map_placeholder(placeholder_text_map)
|
|
self._update_controls_state_based_on_mode_and_tab()
|
|
except tk.TclError: # Can happen if notebook is in a weird state
|
|
module_logger.warning(
|
|
"TclError on function tab change, notebook might be closing.",
|
|
exc_info=False,
|
|
)
|
|
except Exception as e_tab_change:
|
|
module_logger.error(
|
|
f"Error processing function tab change: {e_tab_change}", 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 # Monitoring if stop button is enabled
|
|
)
|
|
|
|
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: # Tab might not be selectable or notebook closing
|
|
pass
|
|
|
|
# Enable BBox entries only if in Live Area mode AND not currently monitoring
|
|
enable_bbox_entries = (
|
|
is_live_mode
|
|
and "Live: Area Monitor" in active_func_tab_text
|
|
and not is_monitoring_active
|
|
)
|
|
self._set_bbox_entries_state(tk.NORMAL if enable_bbox_entries else tk.DISABLED)
|
|
|
|
# Enable Track Length spinbox if in Live Area mode (can be changed even if monitoring, effect on next data)
|
|
# OR make it disabled if monitoring, depending on desired behavior.
|
|
# Current behavior: disable if monitoring.
|
|
enable_track_spinbox = (
|
|
is_live_mode
|
|
and "Live: Area Monitor" in active_func_tab_text
|
|
# and not is_monitoring_active # Uncomment to disable if monitoring active
|
|
)
|
|
if (
|
|
hasattr(self, "track_length_spinbox")
|
|
and self.track_length_spinbox.winfo_exists()
|
|
):
|
|
try:
|
|
# If monitoring is active, disable. Otherwise, enable if conditions met.
|
|
new_state = (
|
|
tk.DISABLED
|
|
if is_monitoring_active
|
|
else ("readonly" if enable_track_spinbox else tk.DISABLED)
|
|
)
|
|
self.track_length_spinbox.config(state=new_state)
|
|
except tk.TclError:
|
|
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 tk.TclError: # Can happen if notebook is in a weird state
|
|
module_logger.warning(
|
|
"TclError on view tab change, notebook might be closing.",
|
|
exc_info=False,
|
|
)
|
|
except Exception as e_view_tab:
|
|
module_logger.error(
|
|
f"Error getting view tab text: {e_view_tab}", exc_info=True
|
|
)
|
|
|
|
def _set_bbox_entries_state(self, state: str):
|
|
for entry_name_attr in [
|
|
"lat_min_entry",
|
|
"lon_min_entry",
|
|
"lat_max_entry",
|
|
"lon_max_entry",
|
|
]:
|
|
entry_widget = getattr(self, entry_name_attr, None)
|
|
if (
|
|
entry_widget
|
|
and hasattr(entry_widget, "winfo_exists")
|
|
and entry_widget.winfo_exists()
|
|
):
|
|
try:
|
|
entry_widget.config(state=state)
|
|
except tk.TclError: # Can happen if widget is destroyed
|
|
pass
|
|
|
|
def _start_monitoring(self):
|
|
if not hasattr(self, "mode_var"):
|
|
self.show_error_message("Internal Error", "Application mode not available.")
|
|
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 tk.TclError:
|
|
module_logger.warning(
|
|
"Could not get active function tab text on start.", exc_info=False
|
|
)
|
|
|
|
# Disable start button, enable stop button, disable mode radios
|
|
for widget_attr_name, new_state in [
|
|
("start_button", tk.DISABLED),
|
|
("stop_button", tk.NORMAL),
|
|
("live_radio", tk.DISABLED),
|
|
("history_radio", tk.DISABLED),
|
|
]:
|
|
widget = getattr(self, widget_attr_name, None)
|
|
if widget and hasattr(widget, "winfo_exists") and widget.winfo_exists():
|
|
widget.config(state=new_state)
|
|
|
|
self._update_controls_state_based_on_mode_and_tab() # This will disable BBox/Track if needed
|
|
|
|
if not self.controller:
|
|
self._reset_gui_to_stopped_state("Critical Error: Controller unavailable.")
|
|
self.show_error_message("Internal Error", "Application 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:
|
|
module_logger.info(
|
|
f"GUI: Starting Live Area monitoring with BBox: {bbox}"
|
|
)
|
|
self.controller.start_live_monitoring(bbox)
|
|
self.update_semaphore_and_status(
|
|
GUI_STATUS_FETCHING, "Live monitoring starting..."
|
|
)
|
|
else:
|
|
self._reset_gui_to_stopped_state("Start failed: Invalid Bounding Box.")
|
|
self.show_error_message(
|
|
"Input Error", "Bounding Box values are invalid or incomplete."
|
|
)
|
|
elif selected_mode == "History" and "History" in active_func_tab_text:
|
|
module_logger.info("GUI: Starting History monitoring.")
|
|
self.controller.start_history_monitoring() # Placeholder
|
|
self.update_semaphore_and_status(
|
|
GUI_STATUS_OK, "History mode (placeholder)."
|
|
) # Placeholder status
|
|
else:
|
|
err_msg = f"Start monitoring not supported on tab '{active_func_tab_text}' for mode '{selected_mode}'."
|
|
module_logger.warning(err_msg)
|
|
self._reset_gui_to_stopped_state(f"Start failed: {err_msg}")
|
|
|
|
def _stop_monitoring(self):
|
|
selected_mode = self.mode_var.get() if hasattr(self, "mode_var") else "Unknown"
|
|
module_logger.info(f"GUI: Stop monitoring requested for mode: {selected_mode}")
|
|
|
|
if not self.controller:
|
|
self._reset_gui_to_stopped_state("Error: Controller missing on stop.")
|
|
return
|
|
|
|
if selected_mode == "Live":
|
|
self.controller.stop_live_monitoring()
|
|
elif selected_mode == "History":
|
|
self.controller.stop_history_monitoring() # Placeholder
|
|
|
|
# GUI will be fully reset by _reset_gui_to_stopped_state called by controller
|
|
# but we can set an intermediate status here.
|
|
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]]:
|
|
required_var_names = [
|
|
"lat_min_var",
|
|
"lon_min_var",
|
|
"lat_max_var",
|
|
"lon_max_var",
|
|
]
|
|
if not all(hasattr(self, var_name) for var_name in required_var_names):
|
|
module_logger.error("BBox string variables not found in GUI.")
|
|
return None
|
|
try:
|
|
# Get all values first
|
|
str_values = [
|
|
getattr(self, var_name).get() for var_name in required_var_names
|
|
]
|
|
|
|
# Check if any are empty before attempting conversion
|
|
if not all(
|
|
s.strip() for s in str_values
|
|
): # Checks for empty or whitespace-only strings
|
|
module_logger.debug("One or more BBox GUI fields are empty.")
|
|
return None
|
|
|
|
lat_min_val, lon_min_val, lat_max_val, lon_max_val = map(float, str_values)
|
|
except ValueError:
|
|
module_logger.warning("Invalid numeric format in BBox GUI fields.")
|
|
return None # ValueError if conversion fails
|
|
|
|
bbox_candidate = {
|
|
"lat_min": lat_min_val,
|
|
"lon_min": lon_min_val,
|
|
"lat_max": lat_max_val,
|
|
"lon_max": lon_max_val,
|
|
}
|
|
if _is_valid_bbox_dict(bbox_candidate):
|
|
return bbox_candidate
|
|
else:
|
|
module_logger.warning(f"BBox from GUI failed validation: {bbox_candidate}")
|
|
return None
|
|
|
|
def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]):
|
|
if not hasattr(self, "lat_min_var"): # Check if vars are initialized
|
|
return
|
|
|
|
if bbox_dict and _is_valid_bbox_dict(bbox_dict):
|
|
decimals = getattr(
|
|
app_config, "COORDINATE_DECIMAL_PLACES", 5
|
|
) # Use a config for decimals
|
|
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: # Can happen if widget is destroyed
|
|
pass
|
|
except Exception as e_set_bbox:
|
|
module_logger.error(f"Error setting BBox GUI fields: {e_set_bbox}")
|
|
else: # Clear fields if bbox_dict is invalid or None
|
|
try:
|
|
self.lat_min_var.set("")
|
|
self.lon_min_var.set("")
|
|
self.lat_max_var.set("")
|
|
self.lon_max_var.set("")
|
|
except tk.TclError:
|
|
pass
|
|
|
|
def display_flights_on_canvas(
|
|
self,
|
|
flight_states: List[CanonicalFlightState],
|
|
_active_bbox_context: Optional[
|
|
Dict[str, float]
|
|
], # bbox_context might be useful later
|
|
):
|
|
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)
|
|
# Update placeholder only if map manager is supposed to be active but no flights are shown
|
|
# (and it's not already showing an error/other message)
|
|
if (
|
|
not flight_states and not self._should_show_main_placeholder()
|
|
): # Map manager IS active
|
|
# Let MapCanvasManager handle its own placeholder for "no flights"
|
|
pass
|
|
elif (
|
|
not flight_states and self._should_show_main_placeholder()
|
|
): # Map manager NOT active
|
|
self._update_map_placeholder("No flights in the selected area.")
|
|
|
|
except Exception as e:
|
|
module_logger.error(
|
|
f"Error in display_flights_on_canvas: {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("GUI: Clearing all views data.")
|
|
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()
|
|
): # No map manager, ensure placeholder is appropriate
|
|
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. (Coming Soon)"
|
|
self._update_map_placeholder(text)
|
|
|
|
# Clear selected flight details panel
|
|
if hasattr(self, "update_selected_flight_details"):
|
|
self.update_selected_flight_details(None)
|
|
|
|
# Future: Clear table view if it's implemented and holds data
|
|
# if hasattr(self, "flight_table_view") and self.flight_table_view:
|
|
# self.flight_table_view.clear_table()
|
|
|
|
def show_error_message(self, title: str, message: str):
|
|
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: # Can happen if root is being destroyed
|
|
pass
|
|
else: # Fallback if GUI not fully up or during shutdown
|
|
print(f"ERROR (No GUI messagebox): {title} - {message}", flush=True)
|
|
|
|
def show_info_message(self, title: str, message: str): # Added for convenience
|
|
if hasattr(self, "root") and self.root.winfo_exists():
|
|
try:
|
|
messagebox.showinfo(title, message, parent=self.root)
|
|
except tk.TclError:
|
|
pass
|
|
else:
|
|
print(f"INFO (No GUI messagebox): {title} - {message}", flush=True)
|
|
|
|
def show_map_context_menu(
|
|
self, latitude: float, longitude: float, screen_x: int, screen_y: int
|
|
):
|
|
if (
|
|
hasattr(self, "map_manager_instance")
|
|
and self.map_manager_instance
|
|
and hasattr(self.map_manager_instance, "show_map_context_menu_from_gui")
|
|
and MAP_CANVAS_MANAGER_AVAILABLE
|
|
):
|
|
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(
|
|
"Map manager or context menu method not available for GUI."
|
|
)
|
|
|
|
def update_clicked_map_info(
|
|
self,
|
|
lat_deg: Optional[float],
|
|
lon_deg: Optional[float],
|
|
lat_dms: str, # Already formatted string
|
|
lon_dms: str, # Already formatted string
|
|
):
|
|
if not hasattr(
|
|
self, "info_lat_value"
|
|
): # Check if info panel widgets are initialized
|
|
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: # Widgets might be destroyed
|
|
pass
|
|
except Exception as e_update_click:
|
|
module_logger.error(
|
|
f"Error updating clicked map info panel: {e_update_click}",
|
|
exc_info=False,
|
|
)
|
|
|
|
def update_general_map_info_display(
|
|
self,
|
|
zoom: Optional[int],
|
|
map_size_str: str, # Already formatted (e.g., "100km x 80km")
|
|
map_geo_bounds: Optional[Tuple[float, float, float, float]], # W, S, E, N
|
|
target_bbox_input: Optional[Dict[str, float]], # Standard dict
|
|
flight_count: Optional[int],
|
|
):
|
|
if not hasattr(
|
|
self, "info_zoom_value"
|
|
): # Check if info panel widgets are initialized
|
|
return
|
|
|
|
decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5)
|
|
try:
|
|
# Map geographic bounds
|
|
map_w, map_s, map_e, map_n = ("N/A",) * 4
|
|
if map_geo_bounds:
|
|
map_w_val, map_s_val, map_e_val, map_n_val = map_geo_bounds
|
|
map_w, map_s, map_e, map_n = (
|
|
f"{map_w_val:.{decimals}f}",
|
|
f"{map_s_val:.{decimals}f}",
|
|
f"{map_e_val:.{decimals}f}",
|
|
f"{map_n_val:.{decimals}f}",
|
|
)
|
|
|
|
for attr_name, value_str in [
|
|
("info_map_bounds_w", map_w),
|
|
("info_map_bounds_s", map_s),
|
|
("info_map_bounds_e", map_e),
|
|
("info_map_bounds_n", map_n),
|
|
]:
|
|
label_widget = getattr(self, attr_name, None)
|
|
if label_widget and label_widget.winfo_exists():
|
|
label_widget.config(text=value_str)
|
|
|
|
# Target BBox input (monitoring area)
|
|
target_w, target_s, target_e, target_n = ("N/A",) * 4
|
|
color_for_target_bbox = BBOX_COLOR_NA # Default color
|
|
if target_bbox_input and _is_valid_bbox_dict(target_bbox_input):
|
|
target_w, target_s, target_e, target_n = (
|
|
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}",
|
|
)
|
|
# Determine color based on how target BBox relates to current map view
|
|
if map_geo_bounds:
|
|
relation_status = self._is_bbox_inside_bbox(
|
|
target_bbox_input, map_geo_bounds
|
|
)
|
|
if relation_status == "Inside":
|
|
color_for_target_bbox = BBOX_COLOR_INSIDE
|
|
elif relation_status == "Partial":
|
|
color_for_target_bbox = BBOX_COLOR_PARTIAL
|
|
else:
|
|
color_for_target_bbox = BBOX_COLOR_OUTSIDE
|
|
else: # No map view bounds, can't determine relation
|
|
color_for_target_bbox = BBOX_COLOR_NA
|
|
|
|
for attr_name, value_str in [
|
|
("info_target_bbox_w", target_w),
|
|
("info_target_bbox_s", target_s),
|
|
("info_target_bbox_e", target_e),
|
|
("info_target_bbox_n", target_n),
|
|
]:
|
|
label_widget = getattr(self, attr_name, None)
|
|
if label_widget and label_widget.winfo_exists():
|
|
label_widget.config(
|
|
text=value_str, foreground=color_for_target_bbox
|
|
)
|
|
|
|
# Other general info
|
|
if hasattr(self, "info_zoom_value") and self.info_zoom_value.winfo_exists():
|
|
self.info_zoom_value.config(
|
|
text=str(zoom) if zoom is not None else "N/A"
|
|
)
|
|
if (
|
|
hasattr(self, "info_map_size_value")
|
|
and self.info_map_size_value.winfo_exists()
|
|
):
|
|
self.info_map_size_value.config(text=map_size_str or "N/A")
|
|
if (
|
|
hasattr(self, "info_flight_count_value")
|
|
and self.info_flight_count_value.winfo_exists()
|
|
):
|
|
self.info_flight_count_value.config(
|
|
text=str(flight_count) if flight_count is not None else "N/A"
|
|
)
|
|
|
|
except tk.TclError: # Widgets might be destroyed
|
|
pass
|
|
except Exception as e_update_gen_info:
|
|
module_logger.error(
|
|
f"Error updating general map info panel: {e_update_gen_info}",
|
|
exc_info=False,
|
|
)
|
|
|
|
def _is_bbox_inside_bbox(
|
|
self,
|
|
inner_bbox_dict: Dict[str, float], # Standard dict {lat_min, lon_min, ...}
|
|
outer_bbox_tuple: Tuple[float, float, float, float], # W, S, E, N
|
|
) -> str: # Returns "Inside", "Outside", "Partial", "N/A"
|
|
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"
|
|
|
|
# Convert outer tuple to a comparable dict for clarity
|
|
outer_dict = {
|
|
"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 # Epsilon for float comparisons
|
|
|
|
# Check for full containment
|
|
if (
|
|
inner_bbox_dict["lon_min"] >= outer_dict["lon_min"] - eps
|
|
and inner_bbox_dict["lat_min"] >= outer_dict["lat_min"] - eps
|
|
and inner_bbox_dict["lon_max"] <= outer_dict["lon_max"] + eps
|
|
and inner_bbox_dict["lat_max"] <= outer_dict["lat_max"] + eps
|
|
):
|
|
return "Inside"
|
|
|
|
# Check for no overlap (completely outside)
|
|
# Inner is to the right of outer OR inner is to the left of outer OR ...
|
|
if (
|
|
inner_bbox_dict["lon_min"]
|
|
>= outer_dict["lon_max"] - eps # Inner right of outer
|
|
or inner_bbox_dict["lon_max"]
|
|
<= outer_dict["lon_min"] + eps # Inner left of outer
|
|
or inner_bbox_dict["lat_min"]
|
|
>= outer_dict["lat_max"] - eps # Inner above outer
|
|
or inner_bbox_dict["lat_max"] <= outer_dict["lat_min"] + eps
|
|
): # Inner below outer
|
|
return "Outside"
|
|
|
|
return (
|
|
"Partial" # If not fully inside and not fully outside, it must be partial
|
|
)
|
|
|
|
def _map_zoom_in(self):
|
|
if self.controller and hasattr(self.controller, "map_zoom_in"):
|
|
self.controller.map_zoom_in()
|
|
|
|
def _map_zoom_out(self):
|
|
if self.controller and hasattr(self.controller, "map_zoom_out"):
|
|
self.controller.map_zoom_out()
|
|
|
|
def _map_pan(self, direction: str):
|
|
if self.controller and hasattr(self.controller, "map_pan_direction"):
|
|
self.controller.map_pan_direction(direction)
|
|
|
|
def _map_center_and_fit(self):
|
|
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 or not lon_str or not patch_str: # Check for empty strings
|
|
self.show_error_message(
|
|
"Input Error", "Latitude, Longitude, and Patch size are required."
|
|
)
|
|
return
|
|
|
|
lat = float(lat_str)
|
|
lon = float(lon_str)
|
|
patch = float(patch_str)
|
|
|
|
if not (-90 <= lat <= 90 and -180 <= lon <= 180 and patch > 0):
|
|
self.show_error_message(
|
|
"Input Error",
|
|
"Invalid latitude (-90 to 90), longitude (-180 to 180), or patch size (>0).",
|
|
)
|
|
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",
|
|
"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 (
|
|
hasattr(self, "track_length_var")
|
|
and self.controller
|
|
and hasattr(self.controller, "set_map_track_length")
|
|
):
|
|
try:
|
|
new_length = self.track_length_var.get()
|
|
if isinstance(new_length, int) and new_length >= 2: # Basic validation
|
|
self.controller.set_map_track_length(new_length)
|
|
module_logger.info(f"GUI: Track length changed to {new_length}")
|
|
else:
|
|
module_logger.warning(
|
|
f"Invalid track length from spinbox: {new_length}"
|
|
)
|
|
except tk.TclError: # Spinbox might not be fully ready or being destroyed
|
|
module_logger.warning(
|
|
"TclError on track length change, spinbox might not be ready."
|
|
)
|
|
except Exception as e_track_len:
|
|
module_logger.error(
|
|
f"Error processing track length change: {e_track_len}",
|
|
exc_info=True,
|
|
)
|