fix graphics

This commit is contained in:
VALLONGOL 2025-05-15 16:39:47 +02:00
parent 27e8459438
commit 23232b6039

View File

@ -1,21 +1,26 @@
# 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
from typing import List, Dict, Optional, Tuple, Any
# Relative imports
from ..data import config
from ..utils.logger import get_logger # Solo get_logger è usato direttamente qui
from ..utils.logger import get_logger
from ..data.common_models import CanonicalFlightState
module_logger = get_logger(__name__) # flightmonitor.gui.main_window
# --- Constants for Semaphore ---
SEMAPHORE_SIZE = 12
SEMAPHORE_PAD = 3 # Aumentato leggermente per più spazio visivo
SEMAPHORE_PAD = 3
SEMAPHORE_BORDER_WIDTH = 1
SEMAPHORE_TOTAL_SIZE = SEMAPHORE_SIZE + 2 * (SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH)
@ -23,129 +28,182 @@ SEMAPHORE_COLOR_OK = "green3"
SEMAPHORE_COLOR_WARNING = "gold"
SEMAPHORE_COLOR_ERROR = "red2"
SEMAPHORE_COLOR_UNKNOWN = "gray70"
SEMAPHORE_COLOR_FETCHING = "sky blue" # Nuovo colore per quando sta attivamente facendo qualcosa
SEMAPHORE_COLOR_FETCHING = "sky blue"
class MainWindow:
"""
Main window of the Flight Monitor application.
Handles the layout, user interactions, and status display including a semaphore.
Handles layout with function and view notebooks, interactions, status, and logging.
"""
def __init__(self, root: tk.Tk, controller):
"""
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")
# Stime delle altezze dei componenti
controls_bbox_estimated_height = 120
log_area_min_height = 80 # Altezza minima desiderata per il log quando si ridimensiona
# Estimate heights for layout
controls_estimated_height = 50 # Height of just the control buttons/radios
bbox_estimated_height = 70 # Height of just the bbox input fields
log_area_min_height = 80
map_min_height = 200
status_bar_actual_height = 30
window_padding_buffer = 20
min_total_height = (controls_bbox_estimated_height +
map_min_height +
log_area_min_height +
status_bar_actual_height +
window_padding_buffer)
# Calculate minimum acceptable window size
min_function_notebook_height = controls_estimated_height + bbox_estimated_height + 20 # Buffer for padding/tabs
min_views_notebook_height = map_min_height + 20 # Buffer for tabs
min_paned_top_panel_height = min_function_notebook_height + min_views_notebook_height
min_paned_bottom_panel_height = log_area_min_height + status_bar_actual_height + 10 # Buffer for spacing
min_total_height = min_paned_top_panel_height + min_paned_bottom_panel_height + 10 # Buffer for panedwindow separator
min_total_width = config.DEFAULT_CANVAS_WIDTH // 2 + 100
if min_total_width < 500: min_total_width = 500
self.root.minsize(min_total_width, min_total_height) # Questo è corretto per la finestra principale
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 = config.DEFAULT_CANVAS_WIDTH + 250
initial_height = (config.DEFAULT_CANVAS_HEIGHT +
controls_bbox_estimated_height +
100 + # Altezza iniziale log
status_bar_actual_height +
window_padding_buffer + 20)
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 ---
# Calculate initial window size
initial_width = config.DEFAULT_CANVAS_WIDTH + 250
initial_function_notebook_height = controls_estimated_height + bbox_estimated_height + 20
initial_views_notebook_height = config.DEFAULT_CANVAS_HEIGHT + 20
initial_log_height = 100 # pixels
initial_total_height = (initial_function_notebook_height + initial_views_notebook_height +
initial_log_height + status_bar_actual_height + 10 + 10)
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 (Controls+Views area | Log+Status area) ---
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))
# --- Top Frame (Controls + Map) ---
self.top_frame = ttk.Frame(self.main_paned_window)
# MODIFIED: Rimosso minsize da .add()
self.main_paned_window.add(self.top_frame, weight=3)
# --- Top Panel of Main PanedWindow (to hold Function Notebook and Views Notebook) ---
self.paned_top_panel = ttk.Frame(self.main_paned_window) # This frame holds the two notebooks
self.main_paned_window.add(self.paned_top_panel, weight=3) # Gets more space
self.controls_and_map_frame = ttk.Frame(self.top_frame)
self.controls_and_map_frame.pack(fill=tk.BOTH, expand=True)
# --- Function Notebook (Top 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)) # Fill X, but don't expand vertically
# ... (Control Frame, BBox Frame, Output Frame (Canvas) come prima) ...
self.control_frame = ttk.LabelFrame(self.controls_and_map_frame, text="Controls", padding=(10, 5))
self.control_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=(5,5))
self.mode_var = tk.StringVar(value="Live")
# --- Tab: Live from BBox ---
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")
# --- Frame for Controls (inside live_bbox_tab_frame) ---
# Move controls and bbox input into this specific tab frame
self.controls_frame = ttk.Frame(self.live_bbox_tab_frame)
self.controls_frame.pack(side=tk.TOP, fill=tk.X, pady=(0,5))
# Control Frame (inside controls_frame)
self.control_frame = ttk.LabelFrame(self.controls_frame, text="Controls", padding=(10, 5))
self.control_frame.pack(side=tk.TOP, fill=tk.X)
# Radio buttons (Mode selection applies globally, keep reference)
self.mode_var = tk.StringVar(value="Live") # Default mode
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)
# Start/Stop buttons (apply globally, keep reference)
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_and_map_frame, text="Geographic Area (Bounding Box)", padding=(10,5))
self.bbox_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)
# Bounding Box Input Frame (inside controls_frame)
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) # pack below control_frame
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(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(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(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(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.output_frame = ttk.LabelFrame(self.controls_and_map_frame, text="Flight Map", padding=5)
self.output_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5)
# --- Placeholder Tabs for other Functions ---
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)
# Bind event to handle function tab changes (less frequent than view changes)
self.function_notebook.bind("<<NotebookTabChanged>>", self._on_function_tab_change)
# --- Views Notebook (Middle Notebook, fills remaining space in paned_top_panel) ---
self.views_notebook = ttk.Notebook(self.paned_top_panel)
self.views_notebook.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) # Fills remaining space in paned_top_panel
# --- Tab: Map View ---
self.map_view_frame = ttk.Frame(self.views_notebook, padding=5)
self.views_notebook.add(self.map_view_frame, text="Map View")
# Canvas for Flight Map (inside map_view_frame)
self.flight_canvas = tk.Canvas(
self.output_frame, bg="gray90",
self.map_view_frame, bg="gray90",
width=config.DEFAULT_CANVAS_WIDTH, height=config.DEFAULT_CANVAS_HEIGHT,
highlightthickness=0
)
self.flight_canvas.pack(fill=tk.BOTH, expand=True)
# --- Log Area ---
self.log_frame = ttk.LabelFrame(self.main_paned_window, text="Application Log", padding=5)
# MODIFIED: Rimosso minsize da .add()
self.main_paned_window.add(self.log_frame, weight=1)
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=2, pady=2)
from ..utils.logger import setup_logging as setup_app_logging
setup_app_logging(gui_log_widget=self.log_text_widget)
# --- Status Bar ---
self.status_bar_frame = ttk.Frame(self.root, padding=(5, 3))
self.status_bar_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=(5,3))
# --- Tab: Table View (Placeholder) ---
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)
# Bind event to handle view tab changes
self.views_notebook.bind("<<NotebookTabChanged>>", self._on_view_tab_change)
# --- Bottom Panel of Main PanedWindow (to hold Status Bar and Log Area) ---
self.paned_bottom_panel = ttk.Frame(self.main_paned_window)
self.main_paned_window.add(self.paned_bottom_panel, weight=1) # Gets less space
# --- Status Bar (with Semaphore - inside paned_bottom_panel) ---
self.status_bar_frame = ttk.Frame(self.paned_bottom_panel, padding=(5, 3))
# Pack at the top of the bottom panel
self.status_bar_frame.pack(side=tk.TOP, fill=tk.X, pady=(0,5)) # pady bottom to separate from log
self.semaphore_canvas = tk.Canvas(
self.status_bar_frame, width=SEMAPHORE_TOTAL_SIZE, height=SEMAPHORE_TOTAL_SIZE,
bg="SystemButtonFace", highlightthickness=0
self.status_bar_frame,
width=SEMAPHORE_TOTAL_SIZE,
height=SEMAPHORE_TOTAL_SIZE,
bg="SystemButtonFace",
highlightthickness=0
)
self.semaphore_canvas.pack(side=tk.LEFT, padx=(0, 5))
@ -158,51 +216,55 @@ class MainWindow:
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.root.protocol("WM_DELETE_WINDOW", self._on_closing)
self._on_mode_change()
self.update_semaphore_and_status("OK", "System Initialized. Ready.")
module_logger.info("MainWindow fully initialized and displayed.")
# print("MW: __init__ - Fine, status iniziale impostato.") # Rimuovi o commenta i print di debug estremo
# --- Log Area (ScrolledText Widget - inside paned_bottom_panel) ---
# This frame now directly contains the log widget
# self.log_frame = ttk.LabelFrame(self.paned_bottom_panel, text="Application Log", padding=5) # No longer a LabelFrame
self.log_frame = ttk.Frame(self.paned_bottom_panel, padding=(5,0,5,5)) # Just a frame, pady bottom
# Pack log_frame below status_bar_frame
self.log_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=0) # Fills remaining space in bottom panel
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) # No internal padding, handled by log_frame padding
from ..utils.logger import setup_logging as setup_app_logging
setup_app_logging(gui_log_widget=self.log_text_widget)
# Finalize initialization
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
# Initial calls after all widgets are packed
self._on_mode_change() # This will call _update_map_placeholder based on default mode (Live)
self.update_semaphore_and_status("OK", "System Initialized. Ready.") # Initial status display
module_logger.info("MainWindow fully initialized and displayed.")
def update_semaphore_and_status(self,
status_level: str,
message: str):
"""
Updates the semaphore color and the status bar message.
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
Args:
status_level (str): The level of the status. Expected values:
"OK", "WARNING", "ERROR", "UNKNOWN", "FETCHING".
message (str): The message to display in the status bar.
"""
color_to_set = SEMAPHORE_COLOR_UNKNOWN # Default
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": # New visual state for active 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:
module_logger.warning("TclError updating semaphore color, canvas widget might be gone.")
try: self.semaphore_canvas.itemconfig(self._semaphore_oval_id, fill=color_to_set)
except tk.TclError: module_logger.warning("TclError updating semaphore color.")
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:
module_logger.warning("TclError updating status label, widget might be gone.")
try: self.status_label.config(text=current_status_text)
except tk.TclError: module_logger.warning("TclError updating status label text.")
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):
@ -215,8 +277,8 @@ class MainWindow:
else:
module_logger.info("User cancelled quit.")
def _reset_gui_to_stopped_state(self, status_message: Optional[str] = "Monitoring stopped."):
# Reset controls state (buttons, radios, bbox entries)
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)
@ -225,10 +287,11 @@ class MainWindow:
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" # Default for a clean stop by user
# Determine semaphore level based on the message content
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" # If stop was due to an error state
elif status_message and "warning" in status_message.lower(): # For less severe issues
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)
@ -236,31 +299,136 @@ class MainWindow:
def _on_mode_change(self):
"""Handles UI changes when the global monitoring mode (Live/History) changes."""
selected_mode = self.mode_var.get()
status_message = f"Mode: {selected_mode}. Ready."
self.update_semaphore_and_status("OK", status_message)
placeholder_text_to_set = "Map Area - Select mode and configure."
# Control visibility/state of functionality tabs based on global mode
# For now, Live BBox tab is only for Live mode. History tab is only for History mode.
if hasattr(self, 'function_notebook') and self.function_notebook.winfo_exists():
try:
live_bbox_tab_index = self.function_notebook.index(self.live_bbox_tab_frame)
history_tab_index = self.function_notebook.index(self.history_tab_frame)
# live_airport_tab_index = self.function_notebook.index(self.live_airport_tab_frame) # Future
if selected_mode == "Live":
# Enable Live tabs, disable History tabs
self.function_notebook.tab(live_bbox_tab_index, state='normal')
# self.function_notebook.tab(live_airport_tab_index, state='normal') # Future
self.function_notebook.tab(history_tab_index, state='disabled')
# Switch to a Live-appropriate tab if on a disabled one
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) # Select the Live BBox tab
elif selected_mode == "History":
# Enable History tabs, disable Live tabs
self.function_notebook.tab(live_bbox_tab_index, state='disabled')
# self.function_notebook.tab(live_airport_tab_index, state='disabled') # Future
self.function_notebook.tab(history_tab_index, state='normal')
# Switch to History tab if on a disabled one
current_func_tab_index = self.function_notebook.index("current")
if current_func_tab_index != history_tab_index:
self.function_notebook.select(history_tab_index) # Select the History tab
# Update views notebook visibility/state if needed (e.g., disable Map view in History)
# But views notebook is flexible, let's keep it enabled for now.
except tk.TclError as e:
module_logger.warning(f"TclError updating function notebook tabs state: {e}")
except ValueError as e: # Tab index not found
module_logger.warning(f"ValueError finding function notebook tab index: {e}")
# Clear existing data from views and update appropriate placeholder
self.clear_all_views_data() # Clear data from ALL views
# Update placeholder text based on the *current function tab* (driven by mode change)
# We might need to get the text from the currently selected tab frame's controls/concept
# For now, let's just update the map placeholder based on mode as before.
placeholder_text_to_set = "Data Area." # Default
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)
# The bbox_frame is enabled/disabled by _set_bbox_entries_state below
# based on the *global* mode.
self._set_bbox_entries_state(tk.NORMAL) # Enable bbox entry for Live mode
elif selected_mode == "History":
placeholder_text_to_set = "Map Area - Ready for historical data. (Functionality TBD)"
self._set_bbox_entries_state(tk.DISABLED)
self._set_bbox_entries_state(tk.DISABLED) # Disable bbox entry for History mode
# Update map placeholder regardless of the current view tab
self._update_map_placeholder(placeholder_text_to_set)
def _on_function_tab_change(self, event: Any):
"""Handles when the user switches between function tabs (e.g., Live BBox, History)."""
if not (hasattr(self, 'function_notebook') and self.function_notebook.winfo_exists()):
return
self.clear_canvas_data() # Clear old flight data
self._update_map_placeholder(placeholder_text_to_set) # Set new placeholder
selected_tab_index = self.function_notebook.index("current")
# tab_widget = self.function_notebook.winfo_children()[selected_tab_index] # Get the frame widget
tab_text = self.function_notebook.tab(selected_tab_index, "text")
module_logger.info(f"GUI: Switched function tab to: {tab_text}")
# Logic to handle UI changes or state based on which function tab is active
# For example, enable/disable certain global controls like Start/Stop based on tab capability.
# The global mode (Live/History) in _on_mode_change already sets basic control states.
# We might need to synchronize the mode_var with the selected tab if tabs imply mode.
# For now, let _on_mode_change manage control states based on mode_var.
# Update the UI related to the specific tab content if needed.
# The bbox_frame is in the Live BBox tab. Its state is managed by _on_mode_change.
# If the user manually switches function tabs *without* changing mode,
# the bbox entries should reflect the current *global* mode.
# Update placeholder text when function tab changes (as it implies a different task)
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)"
# In history mode, we might show historical data placeholder here.
# The actual placeholder text depends on whether data is loaded.
self._update_map_placeholder(placeholder_text)
# You might also want to clear views data when switching function tabs,
# especially if the new tab implies loading new/different data.
# self.clear_all_views_data() # Consider clearing data on function tab switch
def _on_view_tab_change(self, event: Any):
"""Handles when the user switches between view tabs (Map/Table)."""
if not (hasattr(self, 'views_notebook') and self.views_notebook.winfo_exists()):
return
selected_tab_index = self.views_notebook.index("current")
# tab_widget = self.views_notebook.winfo_children()[selected_tab_index]
tab_text = self.views_notebook.tab(selected_tab_index, "text")
module_logger.info(f"GUI: Switched view tab to: {tab_text}")
# Logic specific to view tab change (e.g., ensuring table is populated with current data)
# For now, simply logging the change is sufficient.
def _update_map_placeholder(self, text_to_display: str):
# Ensure the map canvas exists
if hasattr(self, 'flight_canvas') and self.flight_canvas.winfo_exists():
try:
self.flight_canvas.delete("placeholder_text") # Remove any old one first
# Remove any old placeholder first
self.flight_canvas.delete("placeholder_text")
# Create new placeholder text
canvas_w = self.flight_canvas.winfo_width() if self.flight_canvas.winfo_width() > 1 else config.DEFAULT_CANVAS_WIDTH
canvas_h = self.flight_canvas.winfo_height() if self.flight_canvas.winfo_height() > 1 else config.DEFAULT_CANVAS_HEIGHT
self.flight_canvas.create_text(
canvas_w / 2, canvas_h / 2,
text=text_to_display, tags="placeholder_text", fill="gray50" # Darker gray
text=text_to_display, tags="placeholder_text", fill="gray50"
)
except tk.TclError:
module_logger.warning("TclError updating map placeholder, canvas might be gone.")
@ -268,7 +436,6 @@ class MainWindow:
def _set_bbox_entries_state(self, state: str):
if hasattr(self, 'lat_min_entry'): self.lat_min_entry.config(state=state)
# ... (checks for other entries)
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)
@ -278,52 +445,76 @@ class MainWindow:
def _start_monitoring(self):
selected_mode = self.mode_var.get()
module_logger.info(f"GUI: User requested to start {selected_mode} monitoring.")
self.update_semaphore_and_status("WARNING", f"Attempting to start {selected_mode} monitoring...")
# Determine which function tab is currently active
active_func_tab_index = -1
active_func_tab_text = "Unknown Tab"
if hasattr(self, 'function_notebook') and self.function_notebook.winfo_exists():
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}")
# Set controls state immediately
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
# Keep radios disabled while monitoring is starting/active
self.live_radio.config(state=tk.DISABLED)
self.history_radio.config(state=tk.DISABLED)
self._set_bbox_entries_state(tk.DISABLED)
# Bbox entries state is managed by _set_bbox_entries_state called below or in _on_mode_change
# Logic to start monitoring based on the GLOBAL mode and the ACTIVE FUNCTION TAB
# For now, only "Live from BBox" tab is implemented for Live mode
if selected_mode == "Live":
bounding_box = self.get_bounding_box()
if bounding_box:
if self.controller:
self.controller.start_live_monitoring(bounding_box)
else: # Should not happen if app structure is correct
module_logger.critical("Controller not available when attempting to start live monitoring.")
self._reset_gui_to_stopped_state("Critical Error: Controller unavailable.")
self.show_error_message("Internal Error", "Application controller is missing.")
else: # Invalid bounding box, get_bounding_box() already showed error and set semaphore
self._reset_gui_to_stopped_state("Failed to start: Invalid bounding box.")
# Only start Live monitoring if the "Live: Area Monitor" tab is active
if "Live: Area Monitor" in active_func_tab_text: # Check text for simplicity now
bounding_box = self.get_bounding_box() # This shows GUI error if input invalid and sets semaphore
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) # Controller updates status
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: # Invalid bounding box, error shown by get_bounding_box()
self._reset_gui_to_stopped_state("Failed to start: Invalid bounding box details.")
else:
# User pressed Start in Live mode, but not on the BBox tab
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 self.controller: self.controller.start_history_monitoring()
self.clear_canvas_data()
self._update_map_placeholder("Displaying historical data (placeholder)...")
# ... (placeholder drawing for history)
if hasattr(self, 'flight_canvas') and self.flight_canvas.winfo_exists():
try:
self.flight_canvas.create_oval(90,90,110,110,fill="blue",outline="black",tags="flight_dot")
self.flight_canvas.create_text(100,125,text="HIST_FLT",tags="flight_label")
except tk.TclError: pass
module_logger.info("GUI: Switched to History mode (placeholder display).")
# Future: Start History monitoring based on active History tab parameters
if "History" in active_func_tab_text: # Check text for simplicity now
if self.controller: self.controller.start_history_monitoring() # Placeholder call
# ... (update GUI for history monitoring started state) ...
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.")
# Ensure bbox entries are disabled after pressing start, regardless of tab,
# as controls are locked while monitoring is starting/active.
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()
# Signal controller to stop background processes first
if self.controller:
if selected_mode == "Live":
self.controller.stop_live_monitoring() # Controller will manage final status update
elif selected_mode == "History":
self.controller.stop_history_monitoring()
# Reset GUI controls. The final status message (OK, STOPPED) will come from the controller
# via a status update message, or _reset_gui_to_stopped_state provides a generic one.
# Reset GUI controls to stopped state
self._reset_gui_to_stopped_state(f"{selected_mode} monitoring stopped by user.")
module_logger.debug("GUI controls reset after user initiated stop.")
def get_bounding_box(self) -> Optional[Dict[str, float]]:
@ -336,8 +527,8 @@ class MainWindow:
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) # No need for full stack trace here
self.show_error_message("Input Error", msg) # This sets semaphore to ERROR
module_logger.error(f"Input Error (ValueError): {msg}", exc_info=False)
self.show_error_message("Input Error", msg) # Sets semaphore to ERROR
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}")
@ -348,38 +539,41 @@ class MainWindow:
self.show_error_message("Input Error", msg)
return None
if lat_min >= lat_max:
msg = "Latitude Min must be less than Latitude 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:
msg = "Longitude Min must be less than Longitude Max."
msg = "Longitude Min must be strictly less than Longitude Max."
module_logger.error(f"Validation Error: {msg}")
self.show_error_message("Input Error", msg)
return None
valid_bbox = {"lat_min": lat_min, "lon_min": lon_min, "lat_max": lat_max, "lon_max": lon_max}
module_logger.info(f"Valid bounding box obtained: {valid_bbox}")
return valid_bbox
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],
bounding_box: Dict[str, float]):
"""Displays flight data from a list of CanonicalFlightState objects on the canvas."""
"""
Displays flight data from a list of CanonicalFlightState objects on the map canvas.
This method specifically updates the Map View tab's canvas.
"""
if not (hasattr(self, 'flight_canvas') and self.flight_canvas.winfo_exists()):
module_logger.warning("Flight canvas widget does not exist, cannot display flights.")
return
self.clear_canvas_data() # Clear previous flight dots and labels
self.clear_canvas_data() # Clear previous flight dots and labels from map canvas
if not flight_states:
self._update_map_placeholder("No flights currently in the selected area.")
module_logger.info("display_flights_on_canvas: No flight states to display.")
module_logger.info("display_flights_on_canvas: No flight states to display for this update.")
return
else: # Flights are present, ensure placeholder is removed
else: # Flights are present, ensure placeholder is removed from map canvas
if hasattr(self, 'flight_canvas') and self.flight_canvas.winfo_exists():
self.flight_canvas.delete("placeholder_text")
# Check if placeholder exists on THIS canvas before deleting
if self.flight_canvas.find_withtag("placeholder_text"):
self.flight_canvas.delete("placeholder_text")
canvas_width = self.flight_canvas.winfo_width()
canvas_height = self.flight_canvas.winfo_height()
@ -394,60 +588,71 @@ class MainWindow:
displayed_count = 0
for state in flight_states:
# Ensure it's a CanonicalFlightState with coordinates
if not isinstance(state, CanonicalFlightState) or state.latitude is None or state.longitude is None:
if not isinstance(state, CanonicalFlightState):
module_logger.warning(f"Skipping non-CanonicalFlightState item during map display: {type(state)}")
else:
module_logger.debug(f"Skipping flight ICAO {state.icao24} due to missing coordinates for map display.")
continue
# Check if within the display bounding box
if not (lon_min_bbox <= state.longitude <= lon_max_bbox and lat_min_bbox <= state.latitude <= lat_max_bbox):
continue
continue # Skip if outside current bbox
# Calculate pixel coordinates
x_pixel = ((state.longitude - lon_min_bbox) / delta_lon_bbox) * canvas_width
y_pixel = canvas_height - (((state.latitude - lat_min_bbox) / delta_lat_bbox) * canvas_height)
radius = 3
display_label = state.callsign if state.callsign else state.icao24
try:
# Draw the aircraft dot
self.flight_canvas.create_oval(
x_pixel - radius, y_pixel - radius, x_pixel + radius, y_pixel + radius,
fill="red", outline="black", tags="flight_dot"
fill="red", outline="black", tags=("flight_dot", f"icao_{state.icao24}") # Added ICAO tag for potential updates
)
# Draw the label
self.flight_canvas.create_text(
x_pixel, y_pixel - (radius + 7), text=display_label, # Increased y-offset for label
font=("Arial", 7), tags="flight_label", anchor=tk.S
x_pixel, y_pixel - (radius + 7), text=display_label,
font=("Arial", 7), tags=("flight_label", f"icao_label_{state.icao24}"), anchor=tk.S # Added ICAO label tag
)
displayed_count += 1
except tk.TclError:
module_logger.warning("TclError drawing flight on canvas, widget might be gone.")
break
break # Stop trying if canvas is invalid
if displayed_count > 0:
module_logger.info(f"Displayed {displayed_count} flight states on map.")
# Status message like "X flights displayed" will be set by AppController via update_semaphore_and_status
module_logger.info(f"Displayed {displayed_count} flight states on map canvas.")
def clear_all_views_data(self):
"""Clears all displayed flight data from all view tabs."""
self.clear_canvas_data() # Clear data from the map canvas
# Future: Call a method to clear data from other views (e.g., table)
# if hasattr(self, 'flight_table_widget') and self.flight_table_widget.winfo_exists():
# self.flight_table_widget.clear_data() # Example for a table widget
module_logger.debug("Cleared data from all views.")
def clear_canvas_data(self):
"""Clears only flight-specific items (dots and labels) from the canvas."""
"""Clears only flight-specific items (dots and labels) from the map canvas."""
if hasattr(self, 'flight_canvas') and self.flight_canvas.winfo_exists():
try:
self.flight_canvas.delete("flight_dot")
self.flight_canvas.delete("flight_label")
module_logger.debug("Canvas flight items (dots and labels) cleared.")
# Placeholder text is managed separately by _update_map_placeholder or display_flights_on_canvas
module_logger.debug("Map canvas flight items (dots and labels) cleared.")
except tk.TclError:
module_logger.warning("TclError clearing canvas flight items, widget might be gone.")
module_logger.warning("TclError clearing map canvas flight items, widget might be gone.")
# This method becomes the primary way to update status text from within MainWindow if needed,
# but AppController should be the main driver for status updates via update_semaphore_and_status.
def _set_status_text(self, message: str):
"""Updates only the text part of the status bar. Internal use or simple messages."""
if hasattr(self, 'status_label') and self.status_label.winfo_exists():
try:
self.status_label.config(text=f"Status: {message}")
except tk.TclError:
module_logger.warning("TclError updating status label text directly, widget might be gone.")
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) # Set semaphore and status
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)