638 lines
35 KiB
Python
638 lines
35 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 # Rinominiamo per evitare conflitto con il 'config' del modulo tk
|
|
from ..utils.logger import get_logger, shutdown_gui_logging
|
|
from ..data.common_models import CanonicalFlightState
|
|
try:
|
|
from ..map.map_canvas_manager import MapCanvasManager
|
|
MAP_CANVAS_MANAGER_AVAILABLE = True
|
|
except ImportError as e_map_import:
|
|
MapCanvasManager = None # type: ignore
|
|
MAP_CANVAS_MANAGER_AVAILABLE = False
|
|
# Il logger potrebbe non essere ancora pronto, quindi usiamo print per questo errore critico
|
|
print(f"CRITICAL ERROR in MainWindow: Failed to import MapCanvasManager: {e_map_import}. Map functionality will be disabled.")
|
|
|
|
|
|
module_logger = get_logger(__name__) # flightmonitor.gui.main_window
|
|
|
|
# --- 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"
|
|
|
|
|
|
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): # controller type can be AppController
|
|
"""
|
|
Initializes the main window.
|
|
|
|
Args:
|
|
root (tk.Tk): The root Tkinter object.
|
|
controller: The application controller instance.
|
|
"""
|
|
self.root = root
|
|
self.controller = controller
|
|
self.root.title("Flight Monitor")
|
|
|
|
# Stima e calcolo dimensioni finestra (come prima)
|
|
controls_estimated_height = 50
|
|
bbox_estimated_height = 70
|
|
log_area_min_height = 80
|
|
map_min_height = 200 # Altezza minima per il canvas della mappa
|
|
status_bar_actual_height = 30
|
|
|
|
min_function_notebook_height = controls_estimated_height + bbox_estimated_height + 20
|
|
# Usa map_min_height per il calcolo delle viste
|
|
min_views_notebook_height = map_min_height + 20
|
|
|
|
min_paned_top_panel_height = max(min_function_notebook_height, min_views_notebook_height) # Il pannello top deve contenere il più alto dei due
|
|
min_paned_bottom_panel_height = log_area_min_height + status_bar_actual_height + 10
|
|
|
|
min_total_height = min_paned_top_panel_height + min_paned_bottom_panel_height + 20 # Buffer per separatore e padding
|
|
# Usa DEFAULT_CANVAS_WIDTH da fm_config per la larghezza minima
|
|
min_total_width = getattr(fm_config, 'DEFAULT_CANVAS_WIDTH', 800) // 2 + 150 # Adattato
|
|
if min_total_width < 600: min_total_width = 600 # Un po' più largo
|
|
|
|
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_width = getattr(fm_config, 'DEFAULT_CANVAS_WIDTH', 800) + 250
|
|
initial_function_notebook_height = controls_estimated_height + bbox_estimated_height + 40 # Più spazio per padding/tabs
|
|
initial_views_notebook_height = getattr(fm_config, 'DEFAULT_CANVAS_HEIGHT', 600) + 40 # Più spazio
|
|
initial_log_height = 120
|
|
|
|
initial_total_height = max(initial_function_notebook_height, initial_views_notebook_height) + \
|
|
initial_log_height + status_bar_actual_height + 30
|
|
if initial_total_height < min_total_height: initial_total_height = min_total_height
|
|
|
|
self.root.geometry(f"{initial_width}x{initial_total_height}")
|
|
module_logger.debug(f"Initial window size set to: {initial_width}x{initial_total_height}")
|
|
|
|
# --- Main Layout: PanedWindow ---
|
|
self.main_paned_window = ttk.PanedWindow(self.root, orient=tk.VERTICAL)
|
|
self.main_paned_window.pack(fill=tk.BOTH, expand=True, padx=10, pady=(5,0))
|
|
|
|
self.paned_top_panel = ttk.Frame(self.main_paned_window)
|
|
self.main_paned_window.add(self.paned_top_panel, weight=3)
|
|
|
|
# --- Function Notebook ---
|
|
self.function_notebook = ttk.Notebook(self.paned_top_panel)
|
|
self.function_notebook.pack(side=tk.TOP, fill=tk.X, expand=False, padx=0, pady=(0,5))
|
|
|
|
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, width=10)
|
|
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, width=10)
|
|
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, width=10)
|
|
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, width=10)
|
|
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)
|
|
|
|
# --- Views Notebook ---
|
|
self.views_notebook = ttk.Notebook(self.paned_top_panel)
|
|
self.views_notebook.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
|
|
|
|
self.map_view_frame = ttk.Frame(self.views_notebook, padding=5)
|
|
self.views_notebook.add(self.map_view_frame, text="Map View")
|
|
|
|
self.flight_canvas = tk.Canvas(
|
|
self.map_view_frame, bg="gray75", # Sfondo leggermente più scuro per distinguerlo dal placeholder
|
|
width=getattr(fm_config, 'DEFAULT_CANVAS_WIDTH', 800),
|
|
height=getattr(fm_config, 'DEFAULT_CANVAS_HEIGHT', 600),
|
|
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(150, self._initialize_map_manager, default_map_bbox) # Aumentato leggermente il delay
|
|
else:
|
|
module_logger.error("MapCanvasManager could not be imported. Map display will be a placeholder.")
|
|
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)
|
|
|
|
# --- Bottom Panel of Main PanedWindow ---
|
|
self.paned_bottom_panel = ttk.Frame(self.main_paned_window)
|
|
self.main_paned_window.add(self.paned_bottom_panel, weight=1)
|
|
|
|
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 # Usa colore di sfondo root
|
|
)
|
|
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=8, wrap=tk.WORD,
|
|
font=(log_font_family, 9), relief=tk.SUNKEN, borderwidth=1
|
|
)
|
|
self.log_text_widget.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
|
|
|
|
from ..utils.logger import setup_logging as setup_app_logging
|
|
setup_app_logging(gui_log_widget=self.log_text_widget, root_tk_instance=self.root)
|
|
|
|
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
|
|
|
|
self._on_mode_change() # Imposta stato iniziale tab e placeholder
|
|
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]):
|
|
"""Initializes the MapCanvasManager after the canvas has valid dimensions."""
|
|
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.")
|
|
return
|
|
|
|
# Assicurati che il canvas abbia dimensioni prima di inizializzare
|
|
# Questo viene chiamato da root.after, quindi le dimensioni dovrebbero essere disponibili.
|
|
canvas_w = self.flight_canvas.winfo_width()
|
|
canvas_h = self.flight_canvas.winfo_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 # Nome corretto dell'argomento
|
|
)
|
|
# Dopo l'inizializzazione, MapCanvasManager dovrebbe aver disegnato la mappa iniziale.
|
|
# Il placeholder di MainWindow non dovrebbe più essere necessario se la mappa è attiva.
|
|
# MapCanvasManager stesso pulisce il tag "placeholder_text".
|
|
except ImportError as e_imp: # Specifica per ImportError se MapCanvasManager ha dipendenze mancate al suo interno
|
|
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}")
|
|
except Exception as e_init: # Altre eccezioni durante l'init di MapCanvasManager
|
|
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}")
|
|
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 == "OK": color_to_set = SEMAPHORE_COLOR_OK
|
|
elif status_level == "WARNING": color_to_set = SEMAPHORE_COLOR_WARNING
|
|
elif status_level == "ERROR": color_to_set = SEMAPHORE_COLOR_ERROR
|
|
elif status_level == "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 # Ignora se il canvas è distrutto
|
|
|
|
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
|
|
|
|
# Non loggare ogni aggiornamento di status per evitare verbosità, a meno che non sia un errore.
|
|
if status_level in ["ERROR", "CRITICAL"] or "error" in message.lower():
|
|
module_logger.error(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 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():
|
|
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'): self.start_button.config(state=tk.NORMAL)
|
|
if hasattr(self, 'stop_button'): self.stop_button.config(state=tk.DISABLED)
|
|
if hasattr(self, 'live_radio'): self.live_radio.config(state=tk.NORMAL)
|
|
if hasattr(self, 'history_radio'): 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 = "OK"
|
|
if status_message and ("failed" in status_message.lower() or "error" in status_message.lower()):
|
|
status_level_for_semaphore = "ERROR"
|
|
elif status_message and "warning" in status_message.lower():
|
|
status_level_for_semaphore = "WARNING"
|
|
|
|
self.update_semaphore_and_status(status_level_for_semaphore, status_message)
|
|
module_logger.debug(f"GUI controls reset to stopped state. Status: '{status_message}'")
|
|
|
|
|
|
def _should_show_main_placeholder(self) -> bool:
|
|
"""Helper to decide if MainWindow should show its placeholder."""
|
|
if hasattr(self, 'map_manager_instance') and self.map_manager_instance:
|
|
# Se il map_manager è inizializzato, si presume che gestisca il contenuto del canvas.
|
|
# Potrebbe mostrare la sua mappa o il suo placeholder.
|
|
# MainWindow non dovrebbe sovrascrivere.
|
|
map_info = self.map_manager_instance.get_current_map_info()
|
|
if map_info.get("geo_bounds") is not None: # Un segno che la mappa è attiva
|
|
return False
|
|
return True
|
|
|
|
def _update_map_placeholder(self, text_to_display: str):
|
|
if hasattr(self, 'flight_canvas') and self.flight_canvas.winfo_exists():
|
|
try:
|
|
self.flight_canvas.delete("placeholder_text")
|
|
|
|
if self._should_show_main_placeholder():
|
|
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()
|
|
|
|
# --- MODIFICA QUI ---
|
|
if canvas_w <= 1:
|
|
canvas_w = getattr(fm_config, 'DEFAULT_CANVAS_WIDTH', 800) # Usa fm_config
|
|
if canvas_h <= 1:
|
|
canvas_h = getattr(fm_config, 'DEFAULT_CANVAS_HEIGHT', 600) # Usa fm_config
|
|
# --- FINE MODIFICA ---
|
|
|
|
self.flight_canvas.create_text(
|
|
canvas_w / 2, canvas_h / 2,
|
|
text=text_to_display, tags="placeholder_text", fill="gray50",
|
|
width=canvas_w - 40
|
|
)
|
|
else:
|
|
logger.debug("MainWindow skipping placeholder update as map_manager is active.")
|
|
except tk.TclError:
|
|
module_logger.warning("TclError updating map placeholder in MainWindow.")
|
|
except Exception as e_placeholder: # Cattura altre eccezioni
|
|
module_logger.error(f"Unexpected error in _update_map_placeholder: {e_placeholder}", exc_info=True)
|
|
|
|
# ... (resto della classe MainWindow) ...
|
|
|
|
|
|
def _on_mode_change(self):
|
|
selected_mode = self.mode_var.get()
|
|
status_message = f"Mode: {selected_mode}. Ready."
|
|
# Non aggiornare lo status qui se stiamo per aggiornare il placeholder,
|
|
# che potrebbe sovrascrivere questo messaggio.
|
|
# self.update_semaphore_and_status("OK", status_message)
|
|
|
|
if hasattr(self, 'function_notebook') and self.function_notebook.winfo_exists():
|
|
try:
|
|
# ... (logica per abilitare/disabilitare tab come prima) ...
|
|
live_bbox_tab_index = self.function_notebook.index(self.live_bbox_tab_frame)
|
|
history_tab_index = self.function_notebook.index(self.history_tab_frame)
|
|
|
|
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)
|
|
except Exception as e_tab_change: # Più generico per TclError o ValueError
|
|
module_logger.warning(f"Error updating function notebook tabs state: {e_tab_change}")
|
|
|
|
self.clear_all_views_data() # Questo chiamerà map_manager.update_flights_on_map([])
|
|
|
|
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)
|
|
|
|
# Aggiorna il placeholder solo se il map_manager non è attivo.
|
|
# Se map_manager è attivo, avrà già disegnato la mappa o il suo placeholder.
|
|
self._update_map_placeholder(placeholder_text_to_set)
|
|
# Aggiorna lo status *dopo* aver potenzialmente mostrato il placeholder
|
|
self.update_semaphore_and_status("OK", status_message)
|
|
|
|
|
|
def _on_function_tab_change(self, event: Any = None): # Aggiunto event=None per chiamate dirette
|
|
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."
|
|
elif "Live: Airport" in tab_text:
|
|
placeholder_text = "Map Area - Live Airport Monitor. (Functionality TBD)"
|
|
elif "History" in tab_text:
|
|
placeholder_text = "Map Area - History Analysis. (Functionality TBD)"
|
|
|
|
self._update_map_placeholder(placeholder_text) # Aggiorna placeholder se necessario
|
|
except tk.TclError: # Può succedere se il notebook è in uno stato strano
|
|
module_logger.warning("TclError on function tab change, notebook state might be inconsistent.")
|
|
except ValueError: # Se "current" non è un tab valido
|
|
module_logger.warning("ValueError on function tab change, 'current' tab not found.")
|
|
|
|
|
|
def _on_view_tab_change(self, event: Any = None): # Aggiunto event=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'): self.lat_min_entry.config(state=state)
|
|
if hasattr(self, 'lon_min_entry'): self.lon_min_entry.config(state=state)
|
|
if hasattr(self, 'lat_max_entry'): self.lat_max_entry.config(state=state)
|
|
if hasattr(self, 'lon_max_entry'): 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
|
|
|
|
|
|
self.start_button.config(state=tk.DISABLED)
|
|
self.stop_button.config(state=tk.NORMAL)
|
|
self.live_radio.config(state=tk.DISABLED)
|
|
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:
|
|
# Il controller chiamerà map_manager.set_target_bbox()
|
|
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("Failed to start: 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("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("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()
|
|
|
|
# _reset_gui_to_stopped_state viene ora chiamato più robustamente da AppController
|
|
# alla fine della sequenza di stop, o se il MapCanvasManager aggiorna lo stato.
|
|
# Tuttavia, per un feedback immediato all'utente, potremmo resettare qui
|
|
# alcuni controlli e lasciare che lo stato finale sia gestito dal controller/messaggi.
|
|
# Per ora, lasciamo che il controller gestisca lo stato finale.
|
|
# Se il MapCanvasManager ha un metodo "show_stopped_state", potremmo chiamarlo.
|
|
self.update_semaphore_and_status("OK", f"{selected_mode} monitoring stopping...") # Feedback intermedio
|
|
# Il reset completo dei controlli avviene in _reset_gui_to_stopped_state,
|
|
# che dovrebbe essere chiamato dal controller o da un messaggio di stato finale.
|
|
|
|
|
|
def get_bounding_box(self) -> Optional[Dict[str, float]]:
|
|
module_logger.debug("Attempting to retrieve and validate bounding box.")
|
|
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
|
|
|
|
module_logger.debug(f"Raw BBox input: lat_min={lat_min}, lon_min={lon_min}, lat_max={lat_max}, lon_max={lon_max}")
|
|
if not (-90 <= lat_min <= 90 and -90 <= lat_max <= 90 and
|
|
-180 <= lon_min <= 180 and -180 <= lon_max <= 180):
|
|
msg = "Invalid geographic range. Lat: [-90, 90], Lon: [-180, 180]."
|
|
module_logger.error(f"Validation Error: {msg}")
|
|
self.show_error_message("Input Error", msg)
|
|
return None
|
|
if lat_min >= lat_max:
|
|
msg = "Latitude Min must be strictly less than Latitude Max."
|
|
module_logger.error(f"Validation Error: {msg}")
|
|
self.show_error_message("Input Error", msg)
|
|
return None
|
|
if lon_min >= lon_max: # Solitamente non un errore per BBox globali, ma per aree locali sì
|
|
# Se si supportano BBox che attraversano l'antimeridiano, questa logica cambia.
|
|
# Per OpenSky, che non supporta BBox sull'antimeridiano, questo è un errore.
|
|
msg = "Longitude Min must be strictly less than Longitude Max for this service."
|
|
module_logger.error(f"Validation Error: {msg}")
|
|
self.show_error_message("Input Error", msg)
|
|
return None
|
|
|
|
return {"lat_min": lat_min, "lon_min": lon_min, "lat_max": lat_max, "lon_max": lon_max}
|
|
|
|
|
|
def display_flights_on_canvas(self,
|
|
flight_states: List[CanonicalFlightState],
|
|
_active_bounding_box_context: Dict[str, float]): # Il BBox è per contesto, la mappa usa il suo
|
|
"""
|
|
Passa i dati dei voli al MapCanvasManager per la visualizzazione.
|
|
Il MapCanvasManager è responsabile di disegnare sulla sua vista corrente.
|
|
"""
|
|
if not MAP_CANVAS_MANAGER_AVAILABLE or not hasattr(self, 'map_manager_instance') or self.map_manager_instance is None:
|
|
module_logger.warning("MapCanvasManager not initialized, cannot display flights on map.")
|
|
# Non mostrare un placeholder qui, perché questo metodo viene chiamato ripetutamente.
|
|
# Lo stato di "map not ready" dovrebbe essere gestito all'init o se l'init fallisce.
|
|
return
|
|
|
|
# Passa i voli al map_manager_instance.
|
|
# Il MapCanvasManager userà questi voli per aggiornare i suoi overlay.
|
|
# Il bounding_box_context non è direttamente usato per cambiare la vista qui,
|
|
# ma potrebbe essere usato da MapCanvasManager se volesse disegnare il BBox della richiesta API.
|
|
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:
|
|
try:
|
|
self.map_manager_instance.update_flights_on_map([]) # Passa lista vuota per pulire i voli
|
|
# Il MapCanvasManager dovrebbe mostrare la mappa base o il suo placeholder.
|
|
except Exception as e_map_clear:
|
|
module_logger.warning(f"Error clearing flights from map manager: {e_map_clear}")
|
|
else: # Se il map_manager non c'è, MainWindow gestisce il placeholder
|
|
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):
|
|
"""Displays an error message box and updates the status bar to reflect the error."""
|
|
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 += "..."
|
|
self.update_semaphore_and_status("ERROR", status_bar_msg)
|
|
|
|
if hasattr(self, 'root') and self.root.winfo_exists():
|
|
messagebox.showerror(title, message, parent=self.root)
|
|
|
|
# --- NUOVO: Placeholder per il menu contestuale della mappa ---
|
|
def show_map_context_menu(self, latitude: float, longitude: float, screen_x: int, screen_y: int):
|
|
""" Placeholder: Mostra un menu contestuale per il punto mappa cliccato. """
|
|
module_logger.info(f"Placeholder: Show context menu for map click at Lat {latitude:.4f}, Lon {longitude:.4f}")
|
|
# Esempio di creazione menu:
|
|
# context_menu = tk.Menu(self.root, tearoff=0)
|
|
# context_menu.add_command(label=f"Info for {latitude:.3f},{longitude:.3f}", state=tk.DISABLED)
|
|
# context_menu.add_separator()
|
|
# context_menu.add_command(label="Center map here", command=lambda: self._center_map_at_coords(latitude, longitude))
|
|
# context_menu.add_command(label="Get elevation here (TBD)")
|
|
# try:
|
|
# context_menu.tk_popup(screen_x, screen_y)
|
|
# finally:
|
|
# context_menu.grab_release()
|
|
self.update_semaphore_and_status("OK", f"Context click: Lat {latitude:.3f}, Lon {longitude:.3f}")
|
|
|
|
def _center_map_at_coords(self, lat: float, lon: float):
|
|
""" Placeholder: Istruisce il map manager a ricentrare la mappa."""
|
|
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:
|
|
# MapCanvasManager avrà bisogno di un metodo per ricentrare mantenendo lo zoom attuale
|
|
# self.map_manager_instance.recenter_and_redraw(lat, lon, self.map_manager_instance._current_zoom)
|
|
pass |