SXXXXXXX_FlightMonitor/flightmonitor/gui/main_window.py
2025-05-27 07:33:08 +02:00

1510 lines
66 KiB
Python

# FlightMonitor/gui/main_window.py
"""
Main window of the Flight Monitor application.
Handles the layout with multiple notebooks for functions and views,
user interactions, status display including a semaphore, and logging.
"""
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
from tkinter.scrolledtext import ScrolledText
from tkinter import font as tkFont
from typing import List, Dict, Optional, Tuple, Any
import logging # Import per i livelli di logging (es. logging.ERROR)
# Relative imports
from ..data import config as app_config # Standardized alias
from ..utils.logger import get_logger, setup_logging, shutdown_gui_logging
from ..data.common_models import CanonicalFlightState
# Importa le costanti di stato GUI dal modulo utils centralizzato
from ..utils.gui_utils import (
GUI_STATUS_OK,
GUI_STATUS_WARNING,
GUI_STATUS_ERROR,
GUI_STATUS_FETCHING,
GUI_STATUS_UNKNOWN,
GUI_STATUS_PERMANENT_FAILURE, # Assicurati sia definito in gui_utils se usato
SEMAPHORE_COLOR_STATUS_MAP, # Mappa colori per il semaforo
)
try:
from ..map.map_canvas_manager import MapCanvasManager
from ..map.map_utils import _is_valid_bbox_dict # Import helper for bbox validation
MAP_CANVAS_MANAGER_AVAILABLE = True
except ImportError as e_map_import:
MapCanvasManager = None # type: ignore
_is_valid_bbox_dict = lambda x: False # Provide a fallback lambda
MAP_CANVAS_MANAGER_AVAILABLE = False
print(
f"CRITICAL ERROR in MainWindow: Failed to import MapCanvasManager or map_utils: {e_map_import}. Map functionality will be disabled."
)
module_logger = get_logger(__name__)
# --- Constants for Semaphore (dimensioni fisiche) ---
SEMAPHORE_SIZE = 12
SEMAPHORE_PAD = 3
SEMAPHORE_BORDER_WIDTH = 1
SEMAPHORE_TOTAL_SIZE = SEMAPHORE_SIZE + 2 * (SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH)
FALLBACK_CANVAS_WIDTH = 800
FALLBACK_CANVAS_HEIGHT = 600
# Colors for bounding box coordinate display in map info panel
BBOX_COLOR_INSIDE = "green4"
BBOX_COLOR_OUTSIDE = "red2"
BBOX_COLOR_PARTIAL = "darkorange"
BBOX_COLOR_NA = "gray50"
class MainWindow:
"""
Main window of the Flight Monitor application.
Handles layout with function and view notebooks, interactions, status, and logging.
"""
def __init__(self, root: tk.Tk, controller: Any): # app_controller.AppController
self.root = root
self.controller = controller
self.root.title("Flight Monitor")
if app_config.LAYOUT_START_MAXIMIZED:
try:
self.root.state("zoomed")
module_logger.info(
"Attempted to start window maximized using 'zoomed' state from config."
)
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 using screen dimensions from config: {screen_width}x{screen_height}."
)
except tk.TclError as e_screen_info:
module_logger.warning(
f"Could not get screen dimensions or set geometry: {e_screen_info}. Using default Tkinter size."
)
min_win_w = getattr(app_config, "LAYOUT_WINDOW_MIN_WIDTH", 700)
min_win_h = getattr(app_config, "LAYOUT_WINDOW_MIN_HEIGHT", 500)
try:
self.root.minsize(min_win_w, min_win_h)
module_logger.info(
f"Minimum window size set to {min_win_w}x{min_win_h} from config."
)
except tk.TclError as e_minsize:
module_logger.warning(
f"Could not set minsize from config: {e_minsize}. Using Tkinter defaults."
)
self.canvas_width = getattr(
app_config, "DEFAULT_CANVAS_WIDTH", FALLBACK_CANVAS_WIDTH
)
self.canvas_height = getattr(
app_config, "DEFAULT_CANVAS_HEIGHT", FALLBACK_CANVAS_HEIGHT
)
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", 35),
)
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", 70),
)
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.controls_frame_live_area = ttk.Frame(self.live_bbox_tab_frame)
self.controls_frame_live_area.pack(side=tk.TOP, fill=tk.X, pady=(0, 5))
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.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", 30),
)
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=8,
wrap=tk.WORD,
font=(log_font_family, 9),
relief=tk.SUNKEN,
borderwidth=1,
)
self.log_text_widget.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
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", 65),
)
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=100,
height=100,
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.map_tools_info_horizontal_paned = ttk.PanedWindow(
self.map_tools_info_area_frame, orient=tk.HORIZONTAL
)
self.map_tools_info_horizontal_paned.pack(fill=tk.BOTH, expand=True)
self.map_tool_frame = ttk.LabelFrame(
self.map_tools_info_horizontal_paned, text="Map Tools", padding=5
)
self.map_tools_info_horizontal_paned.add(
self.map_tool_frame,
weight=app_config.LAYOUT_TOOLS_INFO_HORIZONTAL_WEIGHTS.get("map_tools", 40),
)
controls_map_container = ttk.Frame(self.map_tool_frame)
controls_map_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
zoom_frame = ttk.Frame(controls_map_container)
zoom_frame.pack(side=tk.TOP, pady=(0, 10))
ttk.Button(zoom_frame, text="Zoom In (+)", command=self._map_zoom_in).pack(
side=tk.LEFT, padx=2
)
ttk.Button(zoom_frame, text="Zoom Out (-)", command=self._map_zoom_out).pack(
side=tk.LEFT, padx=2
)
pan_frame = ttk.Frame(controls_map_container)
pan_frame.pack(side=tk.TOP, pady=(0, 10))
ttk.Button(pan_frame, text="Up", command=lambda: self._map_pan("up")).grid(
row=0, column=1, padx=2, pady=2
)
ttk.Button(pan_frame, text="Left", command=lambda: self._map_pan("left")).grid(
row=1, column=0, padx=2, pady=2
)
ttk.Button(
pan_frame, text="Right", command=lambda: self._map_pan("right")
).grid(row=1, column=2, padx=2, pady=2)
ttk.Button(pan_frame, text="Down", command=lambda: self._map_pan("down")).grid(
row=2, column=1, padx=2, pady=2
)
pan_frame.columnconfigure(1, weight=1) # Center doesn't shrink
center_frame = ttk.LabelFrame(
controls_map_container, text="Center Map", padding=5
)
center_frame.pack(side=tk.TOP, fill=tk.X, pady=(5, 0))
center_frame.columnconfigure(1, weight=1)
ttk.Label(center_frame, text="Lat:").grid(
row=0, column=0, padx=(0, 2), pady=2, sticky=tk.W
)
self.center_lat_var = tk.StringVar()
self.center_lat_entry = ttk.Entry(
center_frame, textvariable=self.center_lat_var, width=10
)
self.center_lat_entry.grid(row=0, column=1, padx=(0, 5), pady=2, sticky=tk.EW)
ttk.Label(center_frame, text="Lon:").grid(
row=0, column=2, padx=(5, 2), pady=2, sticky=tk.W
)
self.center_lon_var = tk.StringVar()
self.center_lon_entry = ttk.Entry(
center_frame, textvariable=self.center_lon_var, width=10
)
self.center_lon_entry.grid(row=0, column=3, padx=(0, 0), pady=2, sticky=tk.EW)
ttk.Label(center_frame, text="Patch (km):").grid(
row=1, column=0, padx=(0, 2), pady=2, sticky=tk.W
)
self.center_patch_size_var = tk.StringVar(value="100")
self.center_patch_size_entry = ttk.Entry(
center_frame, textvariable=self.center_patch_size_var, width=7
)
self.center_patch_size_entry.grid(
row=1, column=1, padx=(0, 5), pady=2, sticky=tk.W
)
self.center_map_button = ttk.Button(
center_frame, text="Center & Fit Patch", command=self._map_center_and_fit
)
self.center_map_button.grid(
row=1, column=2, columnspan=2, padx=5, pady=5, sticky=tk.E
)
self.map_info_panel_frame = ttk.LabelFrame(
self.map_tools_info_horizontal_paned, text="Map Information", padding=10
)
self.map_tools_info_horizontal_paned.add(
self.map_info_panel_frame,
weight=app_config.LAYOUT_TOOLS_INFO_HORIZONTAL_WEIGHTS.get("map_info", 60),
)
self.map_info_panel_frame.columnconfigure(1, weight=1)
self.map_info_panel_frame.columnconfigure(3, weight=1)
info_row = 0
ttk.Label(self.map_info_panel_frame, text="Click Lat:").grid(
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
)
self.info_lat_value = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_lat_value.grid(row=info_row, column=1, sticky=tk.W, pady=1)
ttk.Label(self.map_info_panel_frame, text="Click Lon:").grid(
row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2)
)
self.info_lon_value = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_lon_value.grid(row=info_row, column=3, sticky=tk.W, pady=1)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="Lat DMS:").grid(
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
)
self.info_lat_dms_value = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_lat_dms_value.grid(row=info_row, column=1, sticky=tk.W, pady=1)
ttk.Label(self.map_info_panel_frame, text="Lon DMS:").grid(
row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2)
)
self.info_lon_dms_value = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_lon_dms_value.grid(row=info_row, column=3, sticky=tk.W, pady=1)
info_row += 1
ttk.Separator(self.map_info_panel_frame, orient=tk.HORIZONTAL).grid(
row=info_row, column=0, columnspan=4, sticky=tk.EW, pady=3
)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="Map Bounds:").grid(
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
)
ttk.Label(self.map_info_panel_frame, text="W:").grid(
row=info_row, column=1, sticky=tk.E, pady=1, padx=(0, 2)
)
self.info_map_bounds_w = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_map_bounds_w.grid(
row=info_row, column=2, columnspan=2, sticky=tk.W, pady=1, padx=(0, 5)
)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="").grid(row=info_row, column=0)
ttk.Label(self.map_info_panel_frame, text="S:").grid(
row=info_row, column=1, sticky=tk.E, pady=1, padx=(0, 2)
)
self.info_map_bounds_s = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_map_bounds_s.grid(
row=info_row, column=2, columnspan=2, sticky=tk.W, pady=1, padx=(0, 5)
)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="").grid(row=info_row, column=0)
ttk.Label(self.map_info_panel_frame, text="E:").grid(
row=info_row, column=1, sticky=tk.E, pady=1, padx=(0, 2)
)
self.info_map_bounds_e = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_map_bounds_e.grid(
row=info_row, column=2, columnspan=2, sticky=tk.W, pady=1, padx=(0, 5)
)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="").grid(row=info_row, column=0)
ttk.Label(self.map_info_panel_frame, text="N:").grid(
row=info_row, column=1, sticky=tk.E, pady=1, padx=(0, 2)
)
self.info_map_bounds_n = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_map_bounds_n.grid(
row=info_row, column=2, columnspan=2, sticky=tk.W, pady=1, padx=(0, 5)
)
info_row += 1
ttk.Separator(self.map_info_panel_frame, orient=tk.HORIZONTAL).grid(
row=info_row, column=0, columnspan=4, sticky=tk.EW, pady=3
)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="Target BBox:").grid(
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
)
ttk.Label(self.map_info_panel_frame, text="W:").grid(
row=info_row, column=1, sticky=tk.E, pady=1, padx=(0, 2)
)
self.info_target_bbox_w = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_target_bbox_w.grid(
row=info_row, column=2, columnspan=2, sticky=tk.W, pady=1, padx=(0, 5)
)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="").grid(row=info_row, column=0)
ttk.Label(self.map_info_panel_frame, text="S:").grid(
row=info_row, column=1, sticky=tk.E, pady=1, padx=(0, 2)
)
self.info_target_bbox_s = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_target_bbox_s.grid(
row=info_row, column=2, columnspan=2, sticky=tk.W, pady=1, padx=(0, 5)
)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="").grid(row=info_row, column=0)
ttk.Label(self.map_info_panel_frame, text="E:").grid(
row=info_row, column=1, sticky=tk.E, pady=1, padx=(0, 2)
)
self.info_target_bbox_e = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_target_bbox_e.grid(
row=info_row, column=2, columnspan=2, sticky=tk.W, pady=1, padx=(0, 5)
)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="").grid(row=info_row, column=0)
ttk.Label(self.map_info_panel_frame, text="N:").grid(
row=info_row, column=1, sticky=tk.E, pady=1, padx=(0, 2)
)
self.info_target_bbox_n = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_target_bbox_n.grid(
row=info_row, column=2, columnspan=2, sticky=tk.W, pady=1, padx=(0, 5)
)
info_row += 1
ttk.Separator(self.map_info_panel_frame, orient=tk.HORIZONTAL).grid(
row=info_row, column=0, columnspan=4, sticky=tk.EW, pady=3
)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="Map Zoom:").grid(
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
)
self.info_zoom_value = ttk.Label(self.map_info_panel_frame, text="N/A", width=8)
self.info_zoom_value.grid(row=info_row, column=1, sticky=tk.W, pady=1)
ttk.Label(self.map_info_panel_frame, text="Map Size:").grid(
row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2)
)
self.info_map_size_value = ttk.Label(
self.map_info_panel_frame, text="N/A", width=18, wraplength=140
)
self.info_map_size_value.grid(row=info_row, column=3, sticky=tk.W, pady=1)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="Flights:").grid(
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
)
self.info_flight_count_value = ttk.Label(self.map_info_panel_frame, text="N/A")
self.info_flight_count_value.grid(row=info_row, column=1, sticky=tk.W, pady=1)
from ..data.logging_config import LOGGING_CONFIG
setup_logging(
gui_log_widget=self.log_text_widget,
root_tk_instance=self.root,
logging_config_dict=LOGGING_CONFIG,
)
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
self.root.after(100, self._delayed_initialization)
module_logger.info(
"MainWindow basic structure initialized. Delayed init pending."
)
def _delayed_initialization(self):
if not self.root.winfo_exists():
module_logger.warning(
"Root window destroyed before delayed initialization."
)
return
if MAP_CANVAS_MANAGER_AVAILABLE and MapCanvasManager is not None:
default_map_bbox = {
"lat_min": app_config.DEFAULT_BBOX_LAT_MIN,
"lon_min": app_config.DEFAULT_BBOX_LON_MIN,
"lat_max": app_config.DEFAULT_BBOX_LAT_MAX,
"lon_max": app_config.DEFAULT_BBOX_LON_MAX,
}
self.root.after(50, self._initialize_map_manager, default_map_bbox)
else:
module_logger.error(
"MapCanvasManager class not available post-init. Map display will be a placeholder."
)
self.root.after(
50,
lambda: self._update_map_placeholder(
"Map functionality disabled (Import Error)."
),
)
self.root.after(10, self._on_mode_change)
self.update_semaphore_and_status(GUI_STATUS_OK, "System Initialized. Ready.")
module_logger.info(
"MainWindow fully initialized and displayed after delayed setup."
)
def _initialize_map_manager(self, initial_bbox_for_map: Dict[str, float]):
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 self.controller and hasattr(
self.controller, "update_general_map_info"
):
self.controller.update_general_map_info()
except ImportError as e_imp:
module_logger.critical(
f"Failed to initialize MapCanvasManager (ImportError): {e_imp}",
exc_info=True,
)
self.show_error_message(
"Map Error",
f"Map libraries missing for manager: {e_imp}. Map disabled.",
)
self._update_map_placeholder(f"Map Error: Libraries missing.\n{e_imp}")
if self.controller and hasattr(
self.controller, "update_general_map_info"
):
self.controller.update_general_map_info()
except Exception as e_init:
module_logger.critical(
f"Failed to initialize MapCanvasManager: {e_init}", exc_info=True
)
self.show_error_message(
"Map Error", f"Could not initialize map: {e_init}"
)
self._update_map_placeholder(
f"Map Error: Initialization failed.\n{e_init}"
)
if self.controller and hasattr(
self.controller, "update_general_map_info"
):
self.controller.update_general_map_info()
else:
module_logger.warning(
f"Canvas not ready for MapCanvasManager init (dims: {canvas_w}x{canvas_h}), retrying..."
)
if self.root.winfo_exists():
self.root.after(200, self._initialize_map_manager, initial_bbox_for_map)
def update_semaphore_and_status(self, status_level: str, message: str):
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 GUI logging.")
try:
shutdown_gui_logging()
except Exception as e:
module_logger.error(
f"Error during shutdown_gui_logging: {e}", exc_info=True
)
if hasattr(self, "root") and self.root.winfo_exists():
try:
self.root.destroy()
module_logger.info("Application window has been destroyed.")
except tk.TclError as e:
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)
current_mode_is_live = (
hasattr(self, "mode_var") and self.mode_var.get() == "Live"
)
self._set_bbox_entries_state(tk.NORMAL if current_mode_is_live else tk.DISABLED)
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")
module_logger.debug(f"MainWindow updating placeholder: '{text_to_display}'")
canvas_w, canvas_h = (
self.flight_canvas.winfo_width(),
self.flight_canvas.winfo_height(),
)
if canvas_w <= 1:
canvas_w = self.canvas_width
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")
): # Iterate safely up to "end"
current_tab_widget_name_in_notebook = self.function_notebook.tabs()[i]
actual_widget_controlled_by_tab = self.function_notebook.nametowidget(
current_tab_widget_name_in_notebook
)
if (
hasattr(self, "live_bbox_tab_frame")
and actual_widget_controlled_by_tab == self.live_bbox_tab_frame
):
tab_indices["LiveArea"] = i
elif (
hasattr(self, "history_tab_frame")
and actual_widget_controlled_by_tab == self.history_tab_frame
):
tab_indices["History"] = i
elif (
hasattr(self, "live_airport_tab_frame")
and actual_widget_controlled_by_tab == self.live_airport_tab_frame
):
tab_indices["LiveAirport"] = i
live_bbox_idx, history_idx, live_airport_idx = (
tab_indices.get("LiveArea", -1),
tab_indices.get("History", -1),
tab_indices.get("LiveAirport", -1),
)
if selected_mode == "Live":
if live_bbox_idx != -1:
self.function_notebook.tab(live_bbox_idx, state="normal")
if live_airport_idx != -1:
self.function_notebook.tab(live_airport_idx, state="normal")
if history_idx != -1:
self.function_notebook.tab(history_idx, state="disabled")
current_idx = self.function_notebook.index("current")
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._on_function_tab_change()
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, enable_bbox_entries = "Map Area.", False
if "Live: Area Monitor" in tab_text:
placeholder_text_map, enable_bbox_entries = (
"Map - Live Area. Define area and press Start.",
True,
)
elif "Live: Airport" in tab_text:
placeholder_text_map = "Map - Live Airport. (TBD)"
elif "History" in tab_text:
placeholder_text_map = "Map - History Analysis. (TBD)"
self._update_map_placeholder(placeholder_text_map)
self._set_bbox_entries_state(
tk.NORMAL if enable_bbox_entries else tk.DISABLED
)
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 _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) as e:
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 = [
getattr(self, n, None)
for n in [
"lat_min_entry",
"lon_min_entry",
"lat_max_entry",
"lon_max_entry",
]
]
if all(e and e.winfo_exists() for e in entries):
try:
for entry in entries:
entry.config(state=state)
module_logger.debug(f"BBox entries state set to: {state}")
except tk.TclError:
module_logger.warning(
"TclError setting BBox entries state (widgets gone)."
)
else:
module_logger.debug("One or more BBox entries not available to set state.")
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._set_bbox_entries_state(tk.DISABLED)
if not self.controller:
module_logger.critical("Controller N/A.")
self._reset_gui_to_stopped_state("Critical Error: Controller unavailable.")
self.show_error_message("Internal Error", "App controller missing.")
return
if selected_mode == "Live":
if "Live: Area Monitor" in active_func_tab_text:
bbox = self.get_bounding_box_from_gui()
if bbox:
module_logger.debug(f"Valid BBox for live: {bbox}")
self.controller.start_live_monitoring(bbox)
else:
self._reset_gui_to_stopped_state("Start failed: Invalid BBox.")
else:
module_logger.warning(
f"Start in Live mode, tab '{active_func_tab_text}' not 'Live: Area Monitor'."
)
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()
module_logger.info("GUI: History monitoring started (placeholder).")
else:
module_logger.warning(
f"Start in History mode, tab '{active_func_tab_text}' not 'History'."
)
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 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 = ["lat_min_var", "lon_min_var", "lat_max_var", "lon_max_var"]
if not all(hasattr(self, v) for v in req_vars):
module_logger.error("BBox StringVars N/A.")
self.show_error_message("Internal Error", "BBox fields N/A.")
return None
try:
vals_str = [getattr(self, v).get() for v in req_vars]
if not all(s.strip() for s in vals_str):
module_logger.error("Empty BBox field.")
self.show_error_message("Input Error", "All BBox fields required.")
return None
lat_min, lon_min, lat_max, lon_max = map(float, vals_str)
except ValueError:
module_logger.error("Invalid BBox number format.")
self.show_error_message("Input Error", "BBox coords must be numbers.")
return None
except Exception as e:
module_logger.error(f"Error getting BBox: {e}", exc_info=True)
self.show_error_message("Internal Error", "Error reading BBox.")
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):
module_logger.error(f"Invalid BBox range/order: {bbox_dict}.")
self.show_error_message(
"Input Error", "Invalid BBox range. Min < Max for Lat/Lon."
)
return None
module_logger.debug(f"Validated BBox from GUI: {bbox_dict}")
return bbox_dict
def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]):
if not hasattr(self, "lat_min_var"):
module_logger.warning("BBox GUI StringVars N/A 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 (widgets gone).")
except Exception as e:
module_logger.error(f"Error updating BBox GUI: {e}", exc_info=True)
else:
module_logger.warning(
f"Invalid/empty bbox_dict for GUI update: {bbox_dict}"
)
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: {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.update_flights_on_map([])
except Exception as e:
module_logger.warning(f"Error clearing flights from map manager: {e}")
if self._should_show_main_placeholder():
mode = self.mode_var.get() if hasattr(self, "mode_var") else "Unknown"
text = f"Map - {mode}. Data cleared."
if mode == "Live":
text = "Map - Live. Define area and Start."
elif mode == "History":
text = "Map - History. (TBD)"
self._update_map_placeholder(text)
def show_error_message(self, title: str, message: str):
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 gone."
)
else:
module_logger.warning("Root N/A, skipping status/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,
)
elif self.controller:
root_widget = (
self.root
if hasattr(self, "root") and self.root.winfo_exists()
else None
)
if not root_widget:
module_logger.warning("Cannot show map context menu: root N/A.")
return
try:
menu = tk.Menu(root_widget, tearoff=0)
decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5)
menu.add_command(
label=f"Point: {latitude:.{decimals}f}, {longitude:.{decimals}f}",
state=tk.DISABLED,
)
menu.add_separator()
if hasattr(self.controller, "recenter_map_at_coords"):
menu.add_command(
label="Center map here",
command=lambda: self.controller.recenter_map_at_coords(
latitude, longitude
),
)
else:
menu.add_command(label="Center map here (N/A)", state=tk.DISABLED)
if hasattr(self.controller, "set_bbox_around_coords"):
area_km = getattr(
self.controller, "DEFAULT_CLICK_AREA_SIZE_KM", 50.0
)
menu.add_command(
label=f"Set {area_km:.0f}km Mon. Area",
command=lambda: self.controller.set_bbox_around_coords(
latitude, longitude, area_km
),
)
else:
menu.add_command(label="Set Mon. Area (N/A)", state=tk.DISABLED)
menu.add_command(label="Get elevation (TBD)", state=tk.DISABLED)
menu.tk_popup(screen_x, screen_y)
except tk.TclError as e:
module_logger.warning(f"TclError showing map context menu: {e}.")
except Exception as e:
module_logger.error(
f"Error showing map context menu: {e}", exc_info=True
)
else:
module_logger.warning(
"Controller or context menu handler N/A for MainWindow context menu."
)
def update_clicked_map_info(
self,
lat_deg: Optional[float],
lon_deg: Optional[float],
lat_dms: str,
lon_dms: str,
):
if not hasattr(self, "info_lat_value"):
module_logger.warning("Map info panel (click) widgets N/A.")
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 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) widgets N/A.")
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 = BBOX_COLOR_NA
status = "N/A"
if target_bbox_input and _is_valid_bbox_dict(target_bbox_input):
tw, ts, te, tn = (
f"{target_bbox_input[k]:.{decimals}f}"
for k in ["lon_min", "lat_min", "lon_max", "lat_max"]
)
if map_geo_bounds:
status = self._is_bbox_inside_bbox(
target_bbox_input, map_geo_bounds
)
if status == "Inside":
color = BBOX_COLOR_INSIDE
elif status == "Partial":
color = BBOX_COLOR_PARTIAL
else:
color = BBOX_COLOR_OUTSIDE
else:
color = BBOX_COLOR_NA
bbox_labels = [
getattr(self, n, None)
for n in [
"info_target_bbox_w",
"info_target_bbox_s",
"info_target_bbox_e",
"info_target_bbox_n",
]
]
for label, val_str in zip(bbox_labels, [tw, ts, te, tn]):
if label and label.winfo_exists():
label.config(text=val_str, foreground=color)
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}'"
)
except tk.TclError:
module_logger.warning("TclError updating general map info (widgets 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 = {
"lon_min": outer_bbox_tuple[0],
"lat_min": outer_bbox_tuple[1],
"lon_max": outer_bbox_tuple[2],
"lat_max": outer_bbox_tuple[3],
}
# Non è garantito che outer_bbox_tuple (bounds della mappa) abbia min < max se la vista è strana (es. antimeridiano)
# ma per il confronto con inner_bbox_dict (che è validato), _is_valid_bbox_dict su 'outer' potrebbe fallire.
# La logica di confronto dovrebbe gestire correttamente anche se outer_lon_min > outer_lon_max (antimeridiano)
# Tuttavia, per questo semplice controllo di contenimento, assumiamo che outer non attraversi l'antimeridiano in modo complesso.
# if not _is_valid_bbox_dict(outer): return "N/A" # Questo potrebbe essere troppo restrittivo per outer_bbox_tuple
eps = 1e-6
inside = (
inner_bbox_dict["lon_min"] >= outer["lon_min"] - eps
and inner_bbox_dict["lat_min"] >= outer["lat_min"] - eps
and inner_bbox_dict["lon_max"] <= outer["lon_max"] + eps
and inner_bbox_dict["lat_max"] <= outer["lat_max"] + eps
)
if inside:
return "Inside"
no_overlap = (
inner_bbox_dict["lon_max"] <= outer["lon_min"] + eps
or inner_bbox_dict["lon_min"] >= outer["lon_max"] - eps
or inner_bbox_dict["lat_max"] <= outer["lat_min"] + eps
or inner_bbox_dict["lat_min"] >= outer["lat_max"] - eps
)
if no_overlap:
return "Outside"
return "Partial"
# --- Nuovi Metodi Handler per i Controlli Mappa ---
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 method not available.")
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 method not available.")
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 method not available."
)
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 = self.center_lat_var.get()
lon_str = self.center_lon_var.get()
patch_str = self.center_patch_size_var.get()
if (
not lat_str.strip() or not lon_str.strip() or not patch_str.strip()
): # Controlla stringhe non vuote
self.show_error_message(
"Input Error",
"Latitude, Longitude, and Patch Size are required for centering.",
)
return
lat = float(lat_str)
lon = float(lon_str)
patch_size_km = float(patch_str)
if not (-90.0 <= lat <= 90.0):
self.show_error_message(
"Input Error", "Latitude must be between -90 and 90."
)
return
if not (-180.0 <= lon <= 180.0):
self.show_error_message(
"Input Error", "Longitude must be between -180 and 180."
)
return
if patch_size_km <= 0:
self.show_error_message(
"Input Error", "Patch size must be a positive number (km)."
)
return
if self.controller and hasattr(
self.controller, "map_center_on_coords_and_fit_patch"
):
self.controller.map_center_on_coords_and_fit_patch(
lat, lon, patch_size_km
)
else:
module_logger.warning(
"Controller or map_center_on_coords_and_fit_patch method not available."
)
self.show_error_message(
"Action Failed", "Map centering control is not available."
)
except ValueError:
self.show_error_message(
"Input Error",
"Latitude, Longitude, and Patch Size must be valid numbers.",
)
except Exception as e:
module_logger.error(f"Error in _map_center_and_fit: {e}", exc_info=True)
self.show_error_message("Error", f"An unexpected error occurred: {e}")