SXXXXXXX_FlightMonitor/flightmonitor/gui/main_window.py

1227 lines
55 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
# Relative imports
from ..data import (
config as fm_config,
)
from ..utils.logger import get_logger, setup_logging, shutdown_gui_logging
from ..data.common_models import CanonicalFlightState
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 ---
SEMAPHORE_SIZE = 12
SEMAPHORE_PAD = 3
SEMAPHORE_BORDER_WIDTH = 1
SEMAPHORE_TOTAL_SIZE = SEMAPHORE_SIZE + 2 * (SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH)
SEMAPHORE_COLOR_OK = "green3"
SEMAPHORE_COLOR_WARNING = "gold"
SEMAPHORE_COLOR_ERROR = "red2"
SEMAPHORE_COLOR_UNKNOWN = "gray70"
SEMAPHORE_COLOR_FETCHING = "sky blue"
# Constants for GUI Status Levels (match AppController)
GUI_STATUS_OK = "OK"
GUI_STATUS_WARNING = "WARNING"
GUI_STATUS_ERROR = "ERROR"
GUI_STATUS_FETCHING = "FETCHING"
GUI_STATUS_UNKNOWN = "UNKNOWN"
FALLBACK_CANVAS_WIDTH = 800
FALLBACK_CANVAS_HEIGHT = 600
# Colors for bounding box coordinate display
BBOX_COLOR_INSIDE = "green4"
BBOX_COLOR_OUTSIDE = "red2"
BBOX_COLOR_PARTIAL = "darkorange" # For partial containment
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
):
self.root = root
self.controller = controller
self.root.title("Flight Monitor")
self.canvas_width = getattr(fm_config, 'DEFAULT_CANVAS_WIDTH', FALLBACK_CANVAS_WIDTH)
self.canvas_height = getattr(fm_config, 'DEFAULT_CANVAS_HEIGHT', FALLBACK_CANVAS_HEIGHT)
controls_estimated_height = 50
bbox_estimated_height = 70
log_area_min_height = 100
map_min_height = 300
status_bar_actual_height = 30
info_panel_min_width = 300 # Increased for new info fields
min_function_notebook_content_height = (
controls_estimated_height + bbox_estimated_height + 30
)
min_paned_top_horizontal_panel_height = min_function_notebook_content_height
min_views_notebook_panel_height = (
map_min_height + 30
)
min_paned_bottom_panel_height = (
log_area_min_height + status_bar_actual_height + 10
)
min_total_height = (
min_paned_top_horizontal_panel_height
+ min_views_notebook_panel_height
+ min_paned_bottom_panel_height
+ 30
)
min_func_nb_width = 400
min_total_width = (
min_func_nb_width + info_panel_min_width + 30
)
if min_total_width < 700:
min_total_width = 700
self.root.minsize(min_total_width, min_total_height)
module_logger.debug(
f"Minimum window size set to: {min_total_width}x{min_total_height}"
)
initial_func_nb_width = 450
initial_info_panel_width = 350 # Increased for new info fields
initial_width = (
initial_func_nb_width + initial_info_panel_width + 20
)
if initial_width < min_total_width:
initial_width = min_total_width
initial_height = 800
if initial_height < min_total_height:
initial_height = min_total_height
self.root.geometry(f"{initial_width}x{initial_height}")
module_logger.debug(
f"Initial window size set to: {initial_width}x{initial_height}"
)
# --- Main Layout: PanedWindow Verticale ---
self.main_paned_window = ttk.PanedWindow(self.root, orient=tk.VERTICAL)
self.main_paned_window.pack(
fill=tk.BOTH, expand=True, padx=5, pady=5
)
# --- 1. Pannello Superiore Orizzontale (per Controlli e Info Mappa) ---
self.top_horizontal_paned_window = ttk.PanedWindow(
self.main_paned_window, orient=tk.HORIZONTAL
)
self.main_paned_window.add(self.top_horizontal_paned_window, weight=1)
# --- 1a. Pannello Sinistro del Top Horizontal Paned Window (Function Notebook) ---
self.left_control_panel_frame = ttk.Frame(
self.top_horizontal_paned_window, padding=(0, 0, 5, 0)
)
self.top_horizontal_paned_window.add(self.left_control_panel_frame, weight=3)
self.function_notebook = ttk.Notebook(self.left_control_panel_frame)
self.function_notebook.pack(
fill=tk.BOTH, expand=True
)
# --- Schede del Function Notebook ---
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 = ttk.Frame(self.live_bbox_tab_frame)
self.controls_frame.pack(side=tk.TOP, fill=tk.X, pady=(0, 5))
self.control_frame = ttk.LabelFrame(
self.controls_frame, 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, 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(fm_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(fm_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(fm_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(fm_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
)
# --- 1b. Pannello Destro del Top Horizontal Paned Window (Info Mappa) ---
self.map_info_panel_frame = ttk.LabelFrame(
self.top_horizontal_paned_window, text="Map Information", padding=10
)
self.top_horizontal_paned_window.add(self.map_info_panel_frame, weight=1)
# Configure grid for map info panel
self.map_info_panel_frame.columnconfigure(1, weight=1) # Values column expands
self.map_info_panel_frame.columnconfigure(2, weight=1) # BBox values column expands
self.map_info_panel_frame.columnconfigure(3, weight=1) # BBox values column expands
# Info: Clicked Coordinates
info_row = 0
ttk.Label(self.map_info_panel_frame, text="Click Lat:").grid(row=info_row, column=0, sticky=tk.W, pady=2, 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=2)
ttk.Label(self.map_info_panel_frame, text="Click Lon:").grid(row=info_row, column=2, sticky=tk.W, pady=2, 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=2)
info_row += 1
ttk.Label(self.map_info_panel_frame, text="Lat DMS:").grid(row=info_row, column=0, sticky=tk.W, pady=2, 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=2)
ttk.Label(self.map_info_panel_frame, text="Lon DMS:").grid(row=info_row, column=2, sticky=tk.W, pady=2, 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=2)
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=5)
# Info: Current Map View Bounds
info_row += 1
ttk.Label(self.map_info_panel_frame, text="Map Bounds:").grid(row=info_row, column=0, sticky=tk.W, pady=2, padx=(0, 2))
ttk.Label(self.map_info_panel_frame, text="W:").grid(row=info_row, column=1, sticky=tk.E, pady=2, 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, sticky=tk.W, pady=2, padx=(0,5))
info_row += 1
ttk.Label(self.map_info_panel_frame, text="").grid(row=info_row, column=0, sticky=tk.W, pady=2, padx=(0, 2)) # Placeholder label
ttk.Label(self.map_info_panel_frame, text="S:").grid(row=info_row, column=1, sticky=tk.E, pady=2, 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, sticky=tk.W, pady=2, padx=(0,5))
info_row += 1
ttk.Label(self.map_info_panel_frame, text="").grid(row=info_row, column=0, sticky=tk.W, pady=2, padx=(0, 2)) # Placeholder label
ttk.Label(self.map_info_panel_frame, text="E:").grid(row=info_row, column=1, sticky=tk.E, pady=2, 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, sticky=tk.W, pady=2, padx=(0,5))
info_row += 1
ttk.Label(self.map_info_panel_frame, text="").grid(row=info_row, column=0, sticky=tk.W, pady=2, padx=(0, 2)) # Placeholder label
ttk.Label(self.map_info_panel_frame, text="N:").grid(row=info_row, column=1, sticky=tk.E, pady=2, 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, sticky=tk.W, pady=2, 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=5)
# Info: Target BBox
info_row += 1
ttk.Label(self.map_info_panel_frame, text="Target BBox:").grid(row=info_row, column=0, sticky=tk.W, pady=2, padx=(0, 2))
ttk.Label(self.map_info_panel_frame, text="W:").grid(row=info_row, column=1, sticky=tk.E, pady=2, 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, sticky=tk.W, pady=2, padx=(0,5))
info_row += 1
ttk.Label(self.map_info_panel_frame, text="").grid(row=info_row, column=0, sticky=tk.W, pady=2, padx=(0, 2)) # Placeholder label
ttk.Label(self.map_info_panel_frame, text="S:").grid(row=info_row, column=1, sticky=tk.E, pady=2, 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, sticky=tk.W, pady=2, padx=(0,5))
info_row += 1
ttk.Label(self.map_info_panel_frame, text="").grid(row=info_row, column=0, sticky=tk.W, pady=2, padx=(0, 2)) # Placeholder label
ttk.Label(self.map_info_panel_frame, text="E:").grid(row=info_row, column=1, sticky=tk.E, pady=2, 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, sticky=tk.W, pady=2, padx=(0,5))
info_row += 1
ttk.Label(self.map_info_panel_frame, text="").grid(row=info_row, column=0, sticky=tk.W, pady=2, padx=(0, 2)) # Placeholder label
ttk.Label(self.map_info_panel_frame, text="N:").grid(row=info_row, column=1, sticky=tk.E, pady=2, 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, sticky=tk.W, pady=2, 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=5)
# Info: Map Zoom and Size
info_row += 1
ttk.Label(self.map_info_panel_frame, text="Map Zoom:").grid(row=info_row, column=0, sticky=tk.W, pady=2, padx=(0, 2))
self.info_zoom_value = ttk.Label(self.map_info_panel_frame, text="N/A", width=10) # Keep some width hint
self.info_zoom_value.grid(row=info_row, column=1, sticky=tk.W, pady=2)
ttk.Label(self.map_info_panel_frame, text="Map Size (km):").grid(row=info_row, column=2, sticky=tk.W, pady=2, padx=(5, 2))
self.info_map_size_value = ttk.Label(self.map_info_panel_frame, text="N/A", width=20, wraplength=150) # Keep some width hint
self.info_map_size_value.grid(row=info_row, column=3, sticky=tk.W, pady=2)
# Info: Flight Count
info_row += 1
ttk.Label(self.map_info_panel_frame, text="Flights Shown:").grid(row=info_row, column=0, sticky=tk.W, pady=2, 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=2)
# --- 2. Pannello Centrale (per Views Notebook - Mappa) ---
self.views_notebook_outer_frame = ttk.Frame(
self.main_paned_window
)
self.main_paned_window.add(self.views_notebook_outer_frame, weight=5)
self.views_notebook = ttk.Notebook(self.views_notebook_outer_frame)
self.views_notebook.pack(fill=tk.BOTH, expand=True, padx=0, pady=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
if MAP_CANVAS_MANAGER_AVAILABLE and MapCanvasManager is not None:
default_map_bbox = {
"lat_min": fm_config.DEFAULT_BBOX_LAT_MIN,
"lon_min": fm_config.DEFAULT_BBOX_LON_MIN,
"lat_max": fm_config.DEFAULT_BBOX_LAT_MAX,
"lon_max": fm_config.DEFAULT_BBOX_LON_MAX,
}
self.root.after(50, self._initialize_map_manager, default_map_bbox)
else:
module_logger.error(
"MapCanvasManager class not available. Map display will be a placeholder."
)
self.root.after(
50,
lambda: self._update_map_placeholder(
"Map functionality disabled (Import Error)."
),
)
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)
# --- 3. Pannello Inferiore (Log e Status Bar) ---
self.paned_bottom_panel = ttk.Frame(self.main_paned_window)
self.main_paned_window.add(self.paned_bottom_panel, weight=2)
self.status_bar_frame = ttk.Frame(self.paned_bottom_panel, 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 = SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH
y0 = SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH
x1 = x0 + SEMAPHORE_SIZE
y1 = y0 + SEMAPHORE_SIZE
self._semaphore_oval_id = self.semaphore_canvas.create_oval(
x0,
y0,
x1,
y1,
fill=SEMAPHORE_COLOR_UNKNOWN,
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.paned_bottom_panel, 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)
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(
10, self._on_mode_change
)
self.update_semaphore_and_status("OK", "System Initialized. Ready.")
module_logger.info("MainWindow fully initialized and displayed.")
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
canvas_w = self.flight_canvas.winfo_width()
canvas_h = 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 due to missing libraries during its init: {e_imp}",
exc_info=True,
)
self.show_error_message(
"Map Error",
f"Map libraries missing for manager: {e_imp}. Map functionality 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 yet for MapCanvasManager initialization (dims: {canvas_w}x{canvas_h}), retrying..."
)
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_UNKNOWN
if status_level == GUI_STATUS_OK:
color_to_set = SEMAPHORE_COLOR_OK
elif status_level == GUI_STATUS_WARNING:
color_to_set = SEMAPHORE_COLOR_WARNING
elif status_level == GUI_STATUS_ERROR:
color_to_set = SEMAPHORE_COLOR_ERROR
elif status_level == GUI_STATUS_FETCHING:
color_to_set = SEMAPHORE_COLOR_FETCHING
if hasattr(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.winfo_exists():
try:
self.status_label.config(text=current_status_text)
except tk.TclError:
pass
if status_level in [GUI_STATUS_ERROR] or (
"error" in message.lower() and status_level != GUI_STATUS_WARNING
):
module_logger.error(
f"GUI Status Update: Level='{status_level}', Message='{message}'"
)
elif status_level in [GUI_STATUS_WARNING]:
module_logger.warning(
f"GUI Status Update: Level='{status_level}', Message='{message}'"
)
else:
module_logger.debug(
f"GUI Status Update: Level='{status_level}', Message='{message}'"
)
def _on_closing(self):
module_logger.info("Main window closing event triggered.")
if (
hasattr(self, "root")
and self.root.winfo_exists()
and messagebox.askokcancel(
"Quit", "Do you want to quit Flight Monitor?", parent=self.root
)
):
module_logger.info("User confirmed quit.")
if self.controller and hasattr(self.controller, "on_application_exit"):
self.controller.on_application_exit()
app_destroyed_msg = "Application window will be destroyed."
module_logger.info(app_destroyed_msg)
if (
hasattr(self, "root")
and self.root.winfo_exists()
and hasattr(self, "log_text_widget")
and self.log_text_widget.winfo_exists()
):
shutdown_gui_logging()
if hasattr(self, "root") and self.root.winfo_exists():
self.root.destroy()
print(
f"INFO ({__name__}): Application window has been destroyed (post-destroy print)."
)
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_for_semaphore = GUI_STATUS_OK
if status_message and (
"failed" in status_message.lower() or "error" in status_message.lower()
):
status_level_for_semaphore = GUI_STATUS_ERROR
elif status_message and "warning" in status_message.lower():
status_level_for_semaphore = GUI_STATUS_WARNING
if hasattr(self, "root") and self.root.winfo_exists():
self.update_semaphore_and_status(status_level_for_semaphore, status_message)
else:
module_logger.debug(
"_reset_gui_to_stopped_state: Root window does not exist, skipping status update."
)
module_logger.debug(
f"GUI controls reset to stopped state. Status: '{status_message}'"
)
def _should_show_main_placeholder(self) -> bool:
if (
hasattr(self, "map_manager_instance")
and self.map_manager_instance is not None
):
return False
return True
def _update_map_placeholder(self, text_to_display: str):
if not self._should_show_main_placeholder() or not (
hasattr(self, "flight_canvas") and self.flight_canvas.winfo_exists()
):
return
try:
if hasattr(self, "flight_canvas") and self.flight_canvas.winfo_exists():
self.flight_canvas.delete(
"placeholder_text"
)
module_logger.debug(
f"MainWindow updating placeholder with: '{text_to_display}'"
)
canvas_w = self.flight_canvas.winfo_width()
canvas_h = 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",
width=canvas_w
- 40,
)
module_logger.debug(
f"Drew placeholder text on canvas {canvas_w}x{canvas_h}."
)
else:
module_logger.warning(
f"Cannot draw placeholder text: Canvas dimensions invalid ({canvas_w}x{canvas_h})."
)
except tk.TclError:
module_logger.warning("TclError updating map placeholder in MainWindow.")
except Exception as e_placeholder:
module_logger.error(
f"Unexpected error in _update_map_placeholder: {e_placeholder}",
exc_info=True,
)
def _on_mode_change(self):
selected_mode = self.mode_var.get()
status_message = f"Mode: {selected_mode}. Ready."
if hasattr(self, "function_notebook") and self.function_notebook.winfo_exists():
try:
live_bbox_tab_index = -1
history_tab_index = -1
if hasattr(self, "live_bbox_tab_frame"):
live_bbox_tab_index = self.function_notebook.index(
self.live_bbox_tab_frame
)
if hasattr(self, "history_tab_frame"):
history_tab_index = self.function_notebook.index(
self.history_tab_frame
)
if live_bbox_tab_index != -1 and history_tab_index != -1:
if selected_mode == "Live":
self.function_notebook.tab(live_bbox_tab_index, state="normal")
self.function_notebook.tab(history_tab_index, state="disabled")
current_func_tab_index = self.function_notebook.index("current")
if current_func_tab_index == history_tab_index:
self.function_notebook.select(live_bbox_tab_index)
elif selected_mode == "History":
self.function_notebook.tab(
live_bbox_tab_index, state="disabled"
)
self.function_notebook.tab(history_tab_index, state="normal")
current_func_tab_index = self.function_notebook.index("current")
if current_func_tab_index != history_tab_index:
self.function_notebook.select(history_tab_index)
else:
module_logger.warning(
"Could not find live_bbox_tab_frame or history_tab_frame indexes in function notebook."
)
except Exception as e_tab_change:
module_logger.warning(
f"Error updating function notebook tabs state: {e_tab_change}"
)
self.clear_all_views_data()
placeholder_text_to_set = "Map Area."
if selected_mode == "Live":
placeholder_text_to_set = (
"Map Area - Ready for live data. Define area and press Start."
)
self._set_bbox_entries_state(tk.NORMAL)
elif selected_mode == "History":
placeholder_text_to_set = (
"Map Area - Ready for historical data. (Functionality TBD)"
)
self._set_bbox_entries_state(tk.DISABLED)
self._update_map_placeholder(placeholder_text_to_set)
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: Any = None
):
if not (
hasattr(self, "function_notebook") and self.function_notebook.winfo_exists()
):
return
try:
selected_tab_index = self.function_notebook.index("current")
tab_text = self.function_notebook.tab(selected_tab_index, "text")
module_logger.info(f"GUI: Switched function tab to: {tab_text}")
placeholder_text = f"Selected tab: {tab_text} - Ready."
if "Live: Area Monitor" in tab_text:
placeholder_text = (
"Map Area - Ready for live data. Define area and press Start."
)
self._set_bbox_entries_state(tk.NORMAL) # Ensure bbox entries are enabled when on Live Area tab
elif "Live: Airport" in tab_text:
placeholder_text = (
"Map Area - Live Airport Monitor. (Functionality TBD)"
)
self._set_bbox_entries_state(tk.DISABLED) # Disable bbox entries on other Live tabs
elif "History" in tab_text:
placeholder_text = "Map Area - History Analysis. (Functionality TBD)"
self._set_bbox_entries_state(tk.DISABLED) # Disable bbox entries on History tab
self._update_map_placeholder(
placeholder_text
)
except tk.TclError:
module_logger.warning(
"TclError on function tab change, notebook state might be inconsistent."
)
except ValueError:
module_logger.warning(
"ValueError on function tab change, 'current' tab not found."
)
def _on_view_tab_change(self, event: Any = None):
if not (hasattr(self, "views_notebook") and self.views_notebook.winfo_exists()):
return
try:
selected_tab_index = self.views_notebook.index("current")
tab_text = self.views_notebook.tab(selected_tab_index, "text")
module_logger.info(f"GUI: Switched view tab to: {tab_text}")
except Exception as e_view_tab:
module_logger.warning(f"Error on view tab change: {e_view_tab}")
def _set_bbox_entries_state(self, state: str):
if hasattr(self, "lat_min_entry") and self.lat_min_entry.winfo_exists():
self.lat_min_entry.config(state=state)
if hasattr(self, "lon_min_entry") and self.lon_min_entry.winfo_exists():
self.lon_min_entry.config(state=state)
if hasattr(self, "lat_max_entry") and self.lat_max_entry.winfo_exists():
self.lat_max_entry.config(state=state)
if hasattr(self, "lon_max_entry") and self.lon_max_entry.winfo_exists():
self.lon_max_entry.config(state=state)
module_logger.debug(f"Bounding box entries state set to: {state}")
def _start_monitoring(self):
selected_mode = self.mode_var.get()
module_logger.info(f"GUI: User requested to 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_index = self.function_notebook.index("current")
active_func_tab_text = self.function_notebook.tab(
active_func_tab_index, "text"
)
module_logger.debug(
f"GUI: Active function tab is: {active_func_tab_text}"
)
except Exception:
pass
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)
if selected_mode == "Live":
if "Live: Area Monitor" in active_func_tab_text:
bounding_box = self.get_bounding_box()
if bounding_box:
module_logger.debug(
f"Valid bounding box for live monitoring: {bounding_box}"
)
if self.controller:
self.controller.start_live_monitoring(bounding_box)
else:
module_logger.critical("Controller not available.")
self._reset_gui_to_stopped_state(
"Critical Error: Controller unavailable."
)
self.show_error_message(
"Internal Error", "Application controller is missing."
)
else:
self._reset_gui_to_stopped_state(
"Monitoring not started: Invalid bounding box details."
)
else:
module_logger.warning(
f"GUI: Start pressed in Live mode, but active tab '{active_func_tab_text}' is not the Live Area Monitor tab."
)
self.update_semaphore_and_status(
GUI_STATUS_WARNING,
f"Start not supported on '{active_func_tab_text}' tab in Live mode.",
)
self._reset_gui_to_stopped_state(
f"Start not supported on {active_func_tab_text} tab."
)
elif selected_mode == "History":
if "History" in active_func_tab_text:
if self.controller:
self.controller.start_history_monitoring()
module_logger.info("GUI: History monitoring started (placeholder).")
else:
module_logger.warning(
f"GUI: Start pressed in History mode, but active tab '{active_func_tab_text}' is not the History tab."
)
self.update_semaphore_and_status(
GUI_STATUS_WARNING,
f"Start not supported on '{active_func_tab_text}' tab in History mode.",
)
self._reset_gui_to_stopped_state(
f"Start not supported on {active_func_tab_text} tab."
)
self._set_bbox_entries_state(tk.DISABLED)
def _stop_monitoring(self):
module_logger.info("GUI: User requested to stop monitoring.")
selected_mode = self.mode_var.get()
if self.controller:
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_OK, f"{selected_mode} monitoring stopping..."
)
def get_bounding_box(self) -> Optional[Dict[str, float]]:
module_logger.debug("Attempting to retrieve and validate bounding box.")
if not (
hasattr(self, "lat_min_var")
and hasattr(self, "lon_min_var")
and hasattr(self, "lat_max_var")
and hasattr(self, "lon_max_var")
):
module_logger.error("BBox entry variables are not initialized.")
self.show_error_message(
"Internal Error", "Bounding box input fields not ready."
)
return None
try:
lat_min = float(self.lat_min_var.get())
lon_min = float(self.lon_min_var.get())
lat_max = float(self.lat_max_var.get())
lon_max = float(self.lon_max_var.get())
except ValueError:
msg = "Bounding box coordinates must be valid numbers."
module_logger.error(f"Input Error (ValueError): {msg}", exc_info=False)
self.show_error_message("Input Error", msg)
return None
bbox_dict = {
"lat_min": lat_min,
"lon_min": lon_min,
"lat_max": lat_max,
"lon_max": lon_max,
}
# Use the imported helper function for validation
if not _is_valid_bbox_dict(bbox_dict):
# The helper logs warnings for specific failures.
# Show a general error message to the user.
msg = "Invalid geographic range or order for Bounding Box. Check values and ensure Min < Max for both Lat and Lon."
module_logger.error(f"Validation Error: {msg}")
self.show_error_message("Input Error", msg)
return None
module_logger.debug(
f"Validated BBox input: {bbox_dict}"
)
return bbox_dict
def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]):
"""Updates the bounding box input fields in the GUI."""
if not hasattr(self, "lat_min_var"):
module_logger.warning("BBox GUI variables not ready for update.")
return
if bbox_dict and _is_valid_bbox_dict(bbox_dict):
self.lat_min_var.set(f"{bbox_dict['lat_min']:.{fm_config.COORDINATE_DECIMAL_PLACES}f}")
self.lon_min_var.set(f"{bbox_dict['lon_min']:.{fm_config.COORDINATE_DECIMAL_PLACES}f}")
self.lat_max_var.set(f"{bbox_dict['lat_max']:.{fm_config.COORDINATE_DECIMAL_PLACES}f}")
self.lon_max_var.set(f"{bbox_dict['lon_max']:.{fm_config.COORDINATE_DECIMAL_PLACES}f}")
module_logger.debug(f"Updated BBox GUI fields to: {bbox_dict}")
else:
module_logger.warning(f"Invalid or empty bbox_dict provided for GUI update: {bbox_dict}")
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")
def display_flights_on_canvas(
self,
flight_states: List[CanonicalFlightState],
_active_bounding_box_context: Dict[str, float], # This is the API request BBox
):
if not (
hasattr(self, "map_manager_instance")
and self.map_manager_instance is not None
):
module_logger.warning(
"MapCanvasManager not initialized, cannot display flights on map."
)
return
try:
self.map_manager_instance.update_flights_on_map(flight_states)
except Exception as e_map_update:
module_logger.error(
f"Error updating flights on map via MapCanvasManager: {e_map_update}",
exc_info=True,
)
self.show_error_message(
"Map Display Error", "Could not update flights on map."
)
def clear_all_views_data(self):
"""Clears all displayed flight data from all view tabs."""
if (
hasattr(self, "map_manager_instance")
and self.map_manager_instance is not None
):
try:
self.map_manager_instance.update_flights_on_map(
[]
)
except Exception as e_map_clear:
module_logger.warning(
f"Error clearing flights from map manager: {e_map_clear}"
)
else:
if hasattr(self, "root") and self.root.winfo_exists():
self._update_map_placeholder("Map system not available.")
module_logger.debug("Cleared data from views (map delegated to map manager).")
def show_error_message(self, title: str, message: str):
module_logger.error(
f"Displaying error message to user: Title='{title}', Message='{message}'"
)
status_bar_msg = f"Error: {message[:70]}"
if len(message) > 70:
status_bar_msg += "..."
if hasattr(self, "root") and self.root.winfo_exists():
if self.root.winfo_exists():
self.update_semaphore_and_status(GUI_STATUS_ERROR, status_bar_msg)
else:
module_logger.warning(
"Root window does not exist, skipping status update."
)
else:
module_logger.warning(
"Root window does not exist, skipping status update and messagebox."
)
print(f"ERROR: {title} - {message}")
if hasattr(self, "root") and self.root.winfo_exists():
messagebox.showerror(title, message, parent=self.root)
def show_map_context_menu(
self, latitude: float, longitude: float, screen_x: int, screen_y: int
):
module_logger.info(
f"Placeholder: Show context menu for map click at Lat {latitude:.4f}, Lon {longitude:.4f}"
)
if self.controller and hasattr(self.controller, "on_map_context_menu_request"):
self.controller.on_map_context_menu_request(latitude, longitude, screen_x, screen_y)
else:
module_logger.warning("Controller or context menu request handler not available.")
# Fallback to basic status update if controller method doesn't exist
if hasattr(self, "root") and self.root.winfo_exists():
self.update_semaphore_and_status(
GUI_STATUS_OK, f"Context click: Lat {latitude:.3f}, Lon {longitude:.3f}"
)
def _center_map_at_coords(self, lat: float, lon: float):
module_logger.info(
f"Placeholder: Request to center map at {lat:.4f}, {lon:.4f}"
)
if (
hasattr(self, "map_manager_instance")
and self.map_manager_instance is not None
and hasattr(self.map_manager_instance, "recenter_map_at_coords")
):
try:
self.map_manager_instance.recenter_map_at_coords(lat, lon)
except Exception as e_recenter:
module_logger.error(f"Error calling map_manager_instance.recenter_map_at_coords: {e_recenter}", exc_info=False)
self.show_error_message("Map Error", "Failed to recenter map.")
else:
module_logger.warning("MapCanvasManager or recenter_map_at_coords method not available, cannot center map.")
def _is_bbox_inside_bbox(self, inner_bbox: Dict[str, float], outer_bbox: Tuple[float, float, float, float]) -> str:
"""
Checks if the inner_bbox is fully inside, partially inside, or outside the outer_bbox.
Returns "Inside", "Partial", or "Outside".
"""
if not _is_valid_bbox_dict(inner_bbox) or outer_bbox is None or len(outer_bbox) != 4:
return "N/A" # Cannot determine containment
# Convert outer_bbox tuple to dict for easier comparison
outer_bbox_dict = {"lon_min": outer_bbox[0], "lat_min": outer_bbox[1], "lon_max": outer_bbox[2], "lat_max": outer_bbox[3]}
# Check if the inner bbox is fully contained within the outer bbox (ignoring antimeridian for simplicity here)
is_fully_inside = (
inner_bbox["lon_min"] >= outer_bbox_dict["lon_min"] and
inner_bbox["lat_min"] >= outer_bbox_dict["lat_min"] and
inner_bbox["lon_max"] <= outer_bbox_dict["lon_max"] and
inner_bbox["lat_max"] <= outer_bbox_dict["lat_max"]
)
if is_fully_inside:
return "Inside"
# Check for any overlap (partial containment)
# Two boxes overlap if they overlap on both axes.
# They DON'T overlap if one is entirely to the left/right/above/below the other.
# Check for no overlap:
no_overlap = (
inner_bbox["lon_max"] < outer_bbox_dict["lon_min"] or
inner_bbox["lon_min"] > outer_bbox_dict["lon_max"] or
inner_bbox["lat_max"] < outer_bbox_dict["lat_min"] or
inner_bbox["lat_min"] > outer_bbox_dict["lat_max"]
)
if no_overlap:
return "Outside"
else:
return "Partial"
def update_map_info_panel(
self,
lat_deg: Optional[float],
lon_deg: Optional[float],
lat_dms: str,
lon_dms: str,
zoom: Optional[int],
map_size_str: str,
map_geo_bounds: Optional[Tuple[float, float, float, float]], # New: Map's actual geo bounds
target_bbox_input: Optional[Dict[str, float]], # New: User's input bbox
flight_count: Optional[int] # New: Number of flights shown
):
"""Aggiorna le etichette nel pannello informazioni mappa."""
if not hasattr(self, "info_lat_value"):
module_logger.warning("Map info panel widgets not ready for update.")
return
# Update clicked coordinates info
if hasattr(self, "info_lat_value") and self.info_lat_value.winfo_exists():
self.info_lat_value.config(text=f"{lat_deg:.{fm_config.COORDINATE_DECIMAL_PLACES}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:.{fm_config.COORDINATE_DECIMAL_PLACES}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)
if hasattr(self, "info_lon_dms_value") and self.info_lon_dms_value.winfo_exists():
self.info_lon_dms_value.config(text=lon_dms)
# Update current map view bounds info
if map_geo_bounds:
map_w, map_s, map_e, map_n = map_geo_bounds
# MODIFIED: Use configured decimal places for coordinate display.
# WHY: Consistent formatting.
# HOW: Added format specifier.
map_w_str = f"{map_w:.{fm_config.COORDINATE_DECIMAL_PLACES}f}"
map_s_str = f"{map_s:.{fm_config.COORDINATE_DECIMAL_PLACES}f}"
map_e_str = f"{map_e:.{fm_config.COORDINATE_DECIMAL_PLACES}f}"
map_n_str = f"{map_n:.{fm_config.COORDINATE_DECIMAL_PLACES}f}"
else:
map_w_str, map_s_str, map_e_str, map_n_str = "N/A", "N/A", "N/A", "N/A"
if hasattr(self, "info_map_bounds_w") and self.info_map_bounds_w.winfo_exists(): self.info_map_bounds_w.config(text=map_w_str)
if hasattr(self, "info_map_bounds_s") and self.info_map_bounds_s.winfo_exists(): self.info_map_bounds_s.config(text=map_s_str)
if hasattr(self, "info_map_bounds_e") and self.info_map_bounds_e.winfo_exists(): self.info_map_bounds_e.config(text=map_e_str)
if hasattr(self, "info_map_bounds_n") and self.info_map_bounds_n.winfo_exists(): self.info_map_bounds_n.config(text=map_n_str)
# Update target BBox info and color based on containment
target_bbox_w_str, target_bbox_s_str, target_bbox_e_str, target_bbox_n_str = "N/A", "N/A", "N/A", "N/A"
bbox_color = BBOX_COLOR_NA # Default color
containment_status = "N/A"
if target_bbox_input and _is_valid_bbox_dict(target_bbox_input):
# MODIFIED: Use configured decimal places for coordinate display.
target_bbox_w_str = f"{target_bbox_input['lon_min']:.{fm_config.COORDINATE_DECIMAL_PLACES}f}"
target_bbox_s_str = f"{target_bbox_input['lat_min']:.{fm_config.COORDINATE_DECIMAL_PLACES}f}"
target_bbox_e_str = f"{target_bbox_input['lon_max']:.{fm_config.COORDINATE_DECIMAL_PLACES}f}"
target_bbox_n_str = f"{target_bbox_input['lat_max']:.{fm_config.COORDINATE_DECIMAL_PLACES}f}"
if map_geo_bounds:
containment_status = self._is_bbox_inside_bbox(target_bbox_input, map_geo_bounds)
if containment_status == "Inside": bbox_color = BBOX_COLOR_INSIDE
elif containment_status == "Partial": bbox_color = BBOX_COLOR_PARTIAL
else: bbox_color = BBOX_COLOR_OUTSIDE # "Outside" or other cases
# Update labels for Target BBox
bbox_labels_to_update = [
self.info_target_bbox_w, self.info_target_bbox_s,
self.info_target_bbox_e, self.info_target_bbox_n
]
bbox_values = [target_bbox_w_str, target_bbox_s_str, target_bbox_e_str, target_bbox_n_str]
for label, value in zip(bbox_labels_to_update, bbox_values):
if hasattr(label, "winfo_exists") and label.winfo_exists():
label.config(text=value, foreground=bbox_color) # Set text and color
# Update Map Zoom, Size, and Flight Count
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():
# MODIFIED: Use configured decimal places for map size.
# WHY: Consistent formatting.
# HOW: Added format specifier.
map_size_formatted = f"{map_size_str:.{fm_config.MAP_SIZE_KM_DECIMAL_PLACES}f}" if isinstance(map_size_str, (int, float)) else map_size_str # Format only if number
self.info_map_size_value.config(text=map_size_formatted) # map_size_str should already be formatted string from controller
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")
# Log the updated information for debugging
module_logger.debug(
f"Map info panel updated: ClickLat={lat_deg}, ClickLon={lon_deg}, Zoom={zoom}, MapSize='{map_size_str}', "
f"MapBounds=({map_w_str},{map_s_str},{map_e_str},{map_n_str}), "
f"TargetBBox=({target_bbox_w_str},{target_bbox_s_str},{target_bbox_e_str},{target_bbox_n_str}) [{containment_status}], "
f"Flights={flight_count}."
)