1227 lines
55 KiB
Python
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}."
|
|
) |