add profiling area bbox

This commit is contained in:
VALLONGOL 2025-06-10 13:43:58 +02:00
parent 953c02d879
commit 6fa4928b0c
6 changed files with 378 additions and 293 deletions

View File

@ -0,0 +1 @@
{}

View File

@ -1,6 +1,7 @@
# FlightMonitor/controller/app_controller.py # FlightMonitor/controller/app_controller.py
from queue import Queue, Empty as QueueEmpty from queue import Queue, Empty as QueueEmpty
from typing import List, Optional, Dict, Any, TYPE_CHECKING from typing import List, Optional, Dict, Any, TYPE_CHECKING
from tkinter import simpledialog
from ..data.opensky_live_adapter import OpenSkyLiveAdapter, AdapterMessage from ..data.opensky_live_adapter import OpenSkyLiveAdapter, AdapterMessage
from ..data import config as app_config from ..data import config as app_config
@ -12,6 +13,7 @@ from .aircraft_db_importer import AircraftDBImporter
from .live_data_processor import LiveDataProcessor from .live_data_processor import LiveDataProcessor
from .map_command_handler import MapCommandHandler from .map_command_handler import MapCommandHandler
from .cleanup_manager import CleanupManager from .cleanup_manager import CleanupManager
from ..data.area_profile_manager import AreaProfileManager
if TYPE_CHECKING: if TYPE_CHECKING:
from ..gui.main_window import MainWindow from ..gui.main_window import MainWindow
@ -41,6 +43,7 @@ class AppController:
self.live_data_processor: Optional[LiveDataProcessor] = None self.live_data_processor: Optional[LiveDataProcessor] = None
self.map_command_handler: Optional[MapCommandHandler] = None self.map_command_handler: Optional[MapCommandHandler] = None
self.cleanup_manager: Optional[CleanupManager] = None self.cleanup_manager: Optional[CleanupManager] = None
self.profile_manager: Optional[AreaProfileManager] = None
self.active_detail_window_icao: Optional[str] = None self.active_detail_window_icao: Optional[str] = None
self.active_detail_window_ref: Optional["FullFlightDetailsWindow"] = None # type: ignore self.active_detail_window_ref: Optional["FullFlightDetailsWindow"] = None # type: ignore
@ -59,6 +62,13 @@ class AppController:
module_logger.critical(f"CRITICAL: Failed to initialize AircraftDatabaseManager: {e}", exc_info=True) module_logger.critical(f"CRITICAL: Failed to initialize AircraftDatabaseManager: {e}", exc_info=True)
self.aircraft_db_manager = None self.aircraft_db_manager = None
try:
self.profile_manager = AreaProfileManager() # NUOVA RIGA
module_logger.info("AreaProfileManager initialized successfully.")
except Exception as e:
module_logger.error(f"Failed to initialize AreaProfileManager: {e}", exc_info=True)
module_logger.info("AppController initialized.") module_logger.info("AppController initialized.")
def set_main_window(self, main_window_instance: "MainWindow"): def set_main_window(self, main_window_instance: "MainWindow"):
@ -450,3 +460,76 @@ class AppController:
self.main_window.map_manager_instance.set_max_track_points(length) self.main_window.map_manager_instance.set_max_track_points(length)
else: else:
module_logger.warning("Controller: Cannot set track length, map manager not available.") module_logger.warning("Controller: Cannot set track length, map manager not available.")
def get_profile_names(self) -> List[str]:
"""Returns the list of available profile names."""
if self.profile_manager:
return self.profile_manager.get_profile_names()
return []
def load_area_profile(self, profile_name: str):
"""Loads a profile and updates the GUI and map."""
if not self.profile_manager:
module_logger.error("Profile manager not available.")
return
profile_data = self.profile_manager.get_profile_data(profile_name)
if profile_data:
module_logger.info(f"Loading profile: '{profile_name}'")
# Aggiorna i campi della GUI
if self.main_window:
self.main_window.update_bbox_gui_fields(profile_data)
# Aggiorna la mappa
if self.main_window and self.main_window.map_manager_instance:
self.main_window.map_manager_instance.set_target_bbox(profile_data)
else:
module_logger.warning(f"Profile '{profile_name}' not found.")
def save_current_area_as_profile(self):
"""Saves the current BBox values as a new profile."""
if not self.profile_manager or not self.main_window:
module_logger.error("Profile manager or main window not available.")
return
current_bbox = self.main_window.get_bounding_box_from_gui()
if not current_bbox:
self.main_window.show_error_message("Save Error", "The current Bounding Box values are invalid.")
return
# Chiedi all'utente un nome per il nuovo profilo
profile_name = simpledialog.askstring(
"Save Profile", "Enter a name for the new area profile:",
parent=self.main_window.root
)
if not profile_name or not profile_name.strip():
module_logger.info("Profile save cancelled by user.")
return
profile_name = profile_name.strip()
if self.profile_manager.save_profile(profile_name, current_bbox):
module_logger.info(f"Successfully saved profile '{profile_name}'.")
# Aggiorna la ComboBox nella GUI
if hasattr(self.main_window, "area_management_panel"):
self.main_window.area_management_panel.update_profile_list()
self.main_window.area_management_panel.set_selected_profile(profile_name)
self.main_window.show_info_message("Success", f"Profile '{profile_name}' saved successfully.")
else:
self.main_window.show_error_message("Save Error", f"Could not save profile '{profile_name}'. The name might be invalid or reserved.")
def delete_area_profile(self, profile_name: str):
"""Deletes a user-defined profile."""
if not self.profile_manager or not self.main_window:
module_logger.error("Profile manager or main window not available.")
return
if self.profile_manager.delete_profile(profile_name):
module_logger.info(f"Successfully deleted profile '{profile_name}'.")
# Aggiorna la ComboBox nella GUI
if hasattr(self.main_window, "area_management_panel"):
self.main_window.area_management_panel.update_profile_list()
self.main_window.show_info_message("Success", f"Profile '{profile_name}' deleted.")
else:
self.main_window.show_error_message("Delete Error", f"Could not delete profile '{profile_name}'. It might be the default profile or not exist.")

View File

@ -0,0 +1,114 @@
# FlightMonitor/data/area_profile_manager.py
"""
Manages loading, saving, and handling of geographic area profiles.
"""
import json
import os
from typing import Dict, Optional, List
from ..utils.logger import get_logger
from . import config as app_config
module_logger = get_logger(__name__)
PROFILE_FILENAME = "area_profiles.json"
DEFAULT_PROFILE_NAME = "Default Zone"
class AreaProfileManager:
"""
Handles reading from and writing to the area profiles JSON file.
"""
def __init__(self, profiles_directory: str = "config"):
"""
Initializes the AreaProfileManager.
Args:
profiles_directory: The directory where the profiles file will be stored.
"""
self.profiles_path = os.path.join(os.path.abspath(profiles_directory), PROFILE_FILENAME)
self._profiles: Dict[str, Dict[str, float]] = {}
self._load_profiles()
def _create_default_profile(self) -> Dict[str, float]:
"""Creates the default profile from the application config."""
return {
"lat_min": app_config.DEFAULT_BBOX_LAT_MIN,
"lon_min": app_config.DEFAULT_BBOX_LON_MIN,
"lat_max": app_config.DEFAULT_BBOX_LAT_MAX,
"lon_max": app_config.DEFAULT_BBOX_LON_MAX,
}
def _load_profiles(self):
"""Loads profiles from the JSON file and ensures the default profile exists."""
# Start with the mandatory default profile
self._profiles = {DEFAULT_PROFILE_NAME: self._create_default_profile()}
if os.path.exists(self.profiles_path):
try:
with open(self.profiles_path, "r") as f:
user_profiles = json.load(f)
if isinstance(user_profiles, dict):
# Merge user profiles, default profile cannot be overwritten
for name, data in user_profiles.items():
if name != DEFAULT_PROFILE_NAME:
self._profiles[name] = data
module_logger.info(f"Loaded {len(user_profiles)} user profiles from {self.profiles_path}")
except (json.JSONDecodeError, IOError) as e:
module_logger.error(f"Error loading profiles from {self.profiles_path}: {e}")
else:
module_logger.info("Area profiles file not found. Starting with default profile.")
# We can optionally save the default profile to create the file on first run
self._save_profiles()
def _save_profiles(self):
"""Saves the current user-defined profiles to the JSON file."""
# Ensure the directory exists
try:
os.makedirs(os.path.dirname(self.profiles_path), exist_ok=True)
except OSError as e:
module_logger.error(f"Could not create directory for profiles file: {e}")
return
# Prepare user-defined profiles for saving (exclude the default)
profiles_to_save = {
name: data for name, data in self._profiles.items() if name != DEFAULT_PROFILE_NAME
}
try:
with open(self.profiles_path, "w") as f:
json.dump(profiles_to_save, f, indent=4)
module_logger.info(f"Successfully saved {len(profiles_to_save)} profiles to {self.profiles_path}")
except IOError as e:
module_logger.error(f"Error saving profiles to {self.profiles_path}: {e}")
def get_profile_names(self) -> List[str]:
"""Returns a sorted list of all available profile names."""
return sorted(self._profiles.keys())
def get_profile_data(self, name: str) -> Optional[Dict[str, float]]:
"""Returns the BBox data for a given profile name."""
return self._profiles.get(name)
def save_profile(self, name: str, data: Dict[str, float]) -> bool:
"""
Saves a new or updated profile. Returns False if name is invalid or default.
"""
if not name or name == DEFAULT_PROFILE_NAME:
module_logger.warning(f"Attempt to save invalid or default profile name: '{name}'")
return False
self._profiles[name] = data
self._save_profiles()
return True
def delete_profile(self, name: str) -> bool:
"""Deletes a profile. Returns False if the profile is default or not found."""
if name == DEFAULT_PROFILE_NAME:
module_logger.warning("Cannot delete the default profile.")
return False
if name in self._profiles:
del self._profiles[name]
self._save_profiles()
return True
module_logger.warning(f"Profile '{name}' not found for deletion.")
return False

View File

@ -5,10 +5,8 @@ Handles the layout with multiple notebooks for functions and views,
user interactions, status display including a semaphore, and logging. user interactions, status display including a semaphore, and logging.
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox, filedialog, Menu, font as tkFont from tkinter import ttk, messagebox, filedialog, Menu
from typing import List, Dict, Optional, Any, Tuple, TYPE_CHECKING from typing import List, Dict, Optional, Any, Tuple
import os
from ..data import config as app_config from ..data import config as app_config
from ..utils.logger import get_logger, add_tkinter_handler, shutdown_logging_system from ..utils.logger import get_logger, add_tkinter_handler, shutdown_logging_system
@ -20,25 +18,23 @@ from .panels.map_tools_panel import MapToolsPanel
from .panels.map_info_panel import MapInfoPanel from .panels.map_info_panel import MapInfoPanel
from .panels.selected_flight_details_panel import SelectedFlightDetailsPanel from .panels.selected_flight_details_panel import SelectedFlightDetailsPanel
from .panels.views_notebook_panel import ViewsNotebookPanel from .panels.views_notebook_panel import ViewsNotebookPanel
# New and updated imports
from .panels.function_notebook_panel import FunctionNotebookPanel from .panels.function_notebook_panel import FunctionNotebookPanel
from .panels.area_management_panel import AreaManagementPanel # New panel
try: try:
from ..map.map_canvas_manager import MapCanvasManager from ..map.map_canvas_manager import MapCanvasManager
MAP_CANVAS_MANAGER_AVAILABLE = True MAP_CANVAS_MANAGER_AVAILABLE = True
except ImportError: except ImportError as e_map_import:
MapCanvasManager = None MapCanvasManager = None
MAP_CANVAS_MANAGER_AVAILABLE = False MAP_CANVAS_MANAGER_AVAILABLE = False
print("CRITICAL ERROR in MainWindow: Failed to import MapCanvasManager.", flush=True) print(f"CRITICAL ERROR in MainWindow: Failed to import MapCanvasManager: {e_map_import}.", flush=True)
try: try:
from .dialogs.import_progress_dialog import ImportProgressDialog from .dialogs.import_progress_dialog import ImportProgressDialog
IMPORT_DIALOG_AVAILABLE = True IMPORT_DIALOG_AVAILABLE = True
except ImportError: except ImportError as e_dialog_import:
ImportProgressDialog = None ImportProgressDialog = None
IMPORT_DIALOG_AVAILABLE = False IMPORT_DIALOG_AVAILABLE = False
print("ERROR in MainWindow: Failed to import ImportProgressDialog.", flush=True) print(f"ERROR in MainWindow: Failed to import ImportProgressDialog: {e_dialog_import}.", flush=True)
module_logger = get_logger(__name__) module_logger = get_logger(__name__)
@ -59,15 +55,14 @@ class MainWindow:
except tk.TclError: except tk.TclError:
pass pass
self.root.minsize( self.root.minsize(
getattr(app_config, "LAYOUT_WINDOW_MIN_WIDTH", 900), app_config.LAYOUT_WINDOW_MIN_WIDTH,
getattr(app_config, "LAYOUT_WINDOW_MIN_HEIGHT", 650) app_config.LAYOUT_WINDOW_MIN_HEIGHT
) )
self._create_menu() self._create_menu()
self._create_main_layout() self._create_main_layout()
self._create_panels() self._create_panels()
# Setup logging to the GUI widget
if self.log_status_panel.get_log_widget() and self.root: if self.log_status_panel.get_log_widget() and self.root:
add_tkinter_handler( add_tkinter_handler(
gui_log_widget=self.log_status_panel.get_log_widget(), gui_log_widget=self.log_status_panel.get_log_widget(),
@ -94,53 +89,37 @@ class MainWindow:
self.root.config(menu=menubar) self.root.config(menu=menubar)
def _create_main_layout(self): def _create_main_layout(self):
# Main horizontal division
main_hpane = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL) main_hpane = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
main_hpane.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) main_hpane.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# Left column
left_column_frame = ttk.Frame(main_hpane) left_column_frame = ttk.Frame(main_hpane)
main_hpane.add(left_column_frame, weight=app_config.LAYOUT_MAIN_HORIZONTAL_WEIGHTS.get("left_column", 25)) main_hpane.add(left_column_frame, weight=app_config.LAYOUT_MAIN_HORIZONTAL_WEIGHTS["left_column"])
# Right column
right_column_frame = ttk.Frame(main_hpane) right_column_frame = ttk.Frame(main_hpane)
main_hpane.add(right_column_frame, weight=app_config.LAYOUT_MAIN_HORIZONTAL_WEIGHTS.get("right_column", 75)) main_hpane.add(right_column_frame, weight=app_config.LAYOUT_MAIN_HORIZONTAL_WEIGHTS["right_column"])
# --- Configure Left Column --- left_vpane = ttk.PanedWindow(left_column_frame, orient=tk.VERTICAL)
left_column_frame.rowconfigure(0, weight=3) # Area for BBox and Profiles left_vpane.pack(fill=tk.BOTH, expand=True)
left_column_frame.rowconfigure(1, weight=3) # Area for Function Notebook
left_column_frame.rowconfigure(2, weight=4) # Area for Log/Status
left_column_frame.columnconfigure(0, weight=1)
self.area_management_frame = ttk.Frame(left_column_frame) self.function_and_area_frame = ttk.Frame(left_vpane)
self.area_management_frame.grid(row=0, column=0, sticky="nsew", pady=(0, 5)) left_vpane.add(self.function_and_area_frame, weight=app_config.LAYOUT_LEFT_VERTICAL_WEIGHTS["function_notebook"])
self.function_notebook_frame = ttk.Frame(left_column_frame) self.log_status_area_frame_container = ttk.Frame(left_vpane)
self.function_notebook_frame.grid(row=1, column=0, sticky="nsew") left_vpane.add(self.log_status_area_frame_container, weight=app_config.LAYOUT_LEFT_VERTICAL_WEIGHTS["log_status_area"])
self.log_status_area_frame_container = ttk.Frame(left_column_frame, padding=(0,5,0,0))
self.log_status_area_frame_container.grid(row=2, column=0, sticky="nsew")
# --- Configure Right Column ---
right_vpane = ttk.PanedWindow(right_column_frame, orient=tk.VERTICAL) right_vpane = ttk.PanedWindow(right_column_frame, orient=tk.VERTICAL)
right_vpane.pack(fill=tk.BOTH, expand=True) right_vpane.pack(fill=tk.BOTH, expand=True)
self.views_notebook_outer_frame = ttk.Frame(right_vpane) self.views_notebook_outer_frame = ttk.Frame(right_vpane)
right_vpane.add(self.views_notebook_outer_frame, weight=app_config.LAYOUT_RIGHT_VERTICAL_WEIGHTS.get("views_notebook", 80)) right_vpane.add(self.views_notebook_outer_frame, weight=app_config.LAYOUT_RIGHT_VERTICAL_WEIGHTS["views_notebook"])
self.map_tools_info_area_frame = ttk.Frame(right_vpane)
self.map_tools_info_area_frame = ttk.Frame(right_vpane, padding=(0, 5, 0, 0)) right_vpane.add(self.map_tools_info_area_frame, weight=app_config.LAYOUT_RIGHT_VERTICAL_WEIGHTS["map_tools_info"])
right_vpane.add(self.map_tools_info_area_frame, weight=app_config.LAYOUT_RIGHT_VERTICAL_WEIGHTS.get("map_tools_info", 20))
def _create_panels(self): def _create_panels(self):
# Left column panels self.function_notebook_panel = FunctionNotebookPanel(self.function_and_area_frame, self.controller)
self.area_management_panel = AreaManagementPanel(self.area_management_frame, self.controller)
self.function_notebook_panel = FunctionNotebookPanel(self.function_notebook_frame, self.controller)
self.log_status_panel = LogStatusPanel(self.log_status_area_frame_container, self.root) self.log_status_panel = LogStatusPanel(self.log_status_area_frame_container, self.root)
# Right column panels
self.views_notebook_panel = ViewsNotebookPanel(self.views_notebook_outer_frame) self.views_notebook_panel = ViewsNotebookPanel(self.views_notebook_outer_frame)
# Bottom panels container
bottom_panel_container = ttk.Frame(self.map_tools_info_area_frame) bottom_panel_container = ttk.Frame(self.map_tools_info_area_frame)
bottom_panel_container.pack(fill=tk.BOTH, expand=True) bottom_panel_container.pack(fill=tk.BOTH, expand=True)
bottom_weights = app_config.LAYOUT_BOTTOM_PANELS_HORIZONTAL_WEIGHTS bottom_weights = app_config.LAYOUT_BOTTOM_PANELS_HORIZONTAL_WEIGHTS
@ -160,37 +139,29 @@ class MainWindow:
selected_flight_details_frame.grid(row=0, column=2, sticky="nsew", padx=(2, 0)) selected_flight_details_frame.grid(row=0, column=2, sticky="nsew", padx=(2, 0))
self.selected_flight_details_panel = SelectedFlightDetailsPanel(selected_flight_details_frame, self.controller) self.selected_flight_details_panel = SelectedFlightDetailsPanel(selected_flight_details_frame, self.controller)
# --- Public Methods and Callbacks ---
def get_bounding_box_from_gui(self) -> Optional[Dict[str, float]]: def get_bounding_box_from_gui(self) -> Optional[Dict[str, float]]:
"""Retrieves the bounding box from the dedicated AreaManagementPanel.""" return self.function_notebook_panel.get_bounding_box_input()
return self.area_management_panel.get_bounding_box_input()
def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]): def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]):
"""Delegates updating BBox GUI fields to the AreaManagementPanel.""" self.function_notebook_panel.update_bbox_gui_fields(bbox_dict)
self.area_management_panel.update_bbox_gui_fields(bbox_dict)
def set_monitoring_button_states(self, is_monitoring_active: bool): def set_monitoring_button_states(self, is_monitoring_active: bool):
"""Delegates setting the state of monitoring buttons."""
self.function_notebook_panel.set_monitoring_button_states(is_monitoring_active) self.function_notebook_panel.set_monitoring_button_states(is_monitoring_active)
self.area_management_panel.set_controls_state(not is_monitoring_active) self.function_notebook_panel.set_controls_state(not is_monitoring_active)
def update_semaphore_and_status(self, status_level: str, message: str): def update_semaphore_and_status(self, status_level: str, message: str):
self.log_status_panel.update_status_display(status_level, message) self.log_status_panel.update_status_display(status_level, message)
def _reset_gui_to_stopped_state(self, status_message: Optional[str] = "Monitoring stopped."): def _reset_gui_to_stopped_state(self, status_message: Optional[str] = "Monitoring stopped."):
"""Resets the GUI to its initial, non-monitoring state."""
self.set_monitoring_button_states(False) self.set_monitoring_button_states(False)
status_level = GUI_STATUS_ERROR if "error" in status_message.lower() else GUI_STATUS_OK status_level = GUI_STATUS_ERROR if "error" in status_message.lower() else GUI_STATUS_OK
self.update_semaphore_and_status(status_level, status_message) self.update_semaphore_and_status(status_level, status_message)
def _on_closing(self): def _on_closing(self):
if messagebox.askokcancel("Quit", "Do you want to quit Flight Monitor?", parent=self.root): if messagebox.askokcancel("Quit", "Do you want to quit Flight Monitor?", parent=self.root):
if self.controller and hasattr(self.controller, "on_application_exit"): if self.controller: self.controller.on_application_exit()
self.controller.on_application_exit()
shutdown_logging_system() shutdown_logging_system()
if self.root and self.root.winfo_exists(): if self.root: self.root.destroy()
self.root.destroy()
def _import_aircraft_db_csv(self): def _import_aircraft_db_csv(self):
if not (self.controller and hasattr(self.controller, "import_aircraft_database_from_file_with_progress")): if not (self.controller and hasattr(self.controller, "import_aircraft_database_from_file_with_progress")):
@ -203,22 +174,18 @@ class MainWindow:
) )
if filepath: if filepath:
if IMPORT_DIALOG_AVAILABLE and ImportProgressDialog: if IMPORT_DIALOG_AVAILABLE and ImportProgressDialog:
if self.progress_dialog and self.progress_dialog.winfo_exists():
self.progress_dialog.destroy()
self.progress_dialog = ImportProgressDialog(self.root, file_name=filepath) self.progress_dialog = ImportProgressDialog(self.root, file_name=filepath)
self.controller.import_aircraft_database_from_file_with_progress( self.controller.import_aircraft_database_from_file_with_progress(
filepath, self.progress_dialog filepath, self.progress_dialog
) )
else: else:
self.show_info_message("Import Started", "Importing in background. Check logs for completion.") self.show_info_message("Import Started", "Importing in background. Check logs for completion.")
# Fallback to a non-progress version if you have one
# self.controller.import_aircraft_database_from_file(filepath)
# Other methods (show_error_message, update_selected_flight_details, etc.) remain mostly the same
# But now they delegate to the appropriate sub-panel if necessary.
def show_error_message(self, title: str, message: str): def show_error_message(self, title: str, message: str):
if self.root.winfo_exists(): if self.root.winfo_exists():
messagebox.showerror(title, message, parent=self.root) messagebox.showerror(title, message, parent=self.root)
self.update_semaphore_and_status(GUI_STATUS_ERROR, message)
def show_info_message(self, title: str, message: str): def show_info_message(self, title: str, message: str):
if self.root.winfo_exists(): if self.root.winfo_exists():
@ -238,20 +205,18 @@ class MainWindow:
def _delayed_initialization(self): def _delayed_initialization(self):
if not self.root.winfo_exists(): return if not self.root.winfo_exists(): return
if MAP_CANVAS_MANAGER_AVAILABLE and MapCanvasManager: if MAP_CANVAS_MANAGER_AVAILABLE:
default_map_bbox = self.get_bounding_box_from_gui() initial_bbox = self.get_bounding_box_from_gui()
# Initial track length is now handled by map_canvas_manager default self.root.after(200, self._initialize_map_manager, initial_bbox)
self.root.after(200, self._initialize_map_manager, default_map_bbox)
else: else:
self._update_map_placeholder("Map functionality disabled (Import Error).") self._update_map_placeholder("Map functionality disabled (Import Error).")
self.function_notebook_panel._on_tab_change() # Trigger initial state self.function_notebook_panel._on_tab_change()
module_logger.info("MainWindow fully initialized.") module_logger.info("MainWindow fully initialized.")
def _initialize_map_manager(self, initial_bbox_for_map: Optional[Dict[str, float]]): def _initialize_map_manager(self, initial_bbox_for_map: Optional[Dict[str, float]]):
flight_canvas = self.views_notebook_panel.get_map_canvas() flight_canvas = self.views_notebook_panel.get_map_canvas()
if not (flight_canvas and flight_canvas.winfo_exists()): if not (flight_canvas and flight_canvas.winfo_exists()):
module_logger.warning("Flight canvas not available for map manager init.")
return return
canvas_w, canvas_h = flight_canvas.winfo_width(), flight_canvas.winfo_height() canvas_w, canvas_h = flight_canvas.winfo_width(), flight_canvas.winfo_height()
@ -275,8 +240,7 @@ class MainWindow:
flight_canvas = self.views_notebook_panel.get_map_canvas() flight_canvas = self.views_notebook_panel.get_map_canvas()
if not (flight_canvas and flight_canvas.winfo_exists()): return if not (flight_canvas and flight_canvas.winfo_exists()): return
# Check if map manager is active. If so, don't show placeholder. if self.map_manager_instance and not self.map_manager_instance.is_detail_map:
if self.map_manager_instance:
flight_canvas.delete("placeholder_text") flight_canvas.delete("placeholder_text")
return return
@ -290,24 +254,11 @@ class MainWindow:
except tk.TclError: except tk.TclError:
pass pass
# Pass-through methods for map info updates
def update_clicked_map_info(self, lat_deg, lon_deg, lat_dms, lon_dms): def update_clicked_map_info(self, lat_deg, lon_deg, lat_dms, lon_dms):
self.map_info_panel.update_clicked_map_info(lat_deg, lon_deg, lat_dms, lon_dms) self.map_info_panel.update_clicked_map_info(lat_deg, lon_deg, lat_dms, lon_dms)
def update_general_map_info_display( def update_general_map_info_display(self, zoom, map_size_str, map_geo_bounds, target_bbox_input, flight_count):
self, self.map_info_panel.update_general_map_info_display(zoom, map_size_str, map_geo_bounds, target_bbox_input, flight_count)
zoom: Optional[int],
map_size_str: str,
map_geo_bounds: Optional[Tuple[float, float, float, float]],
target_bbox_input: Optional[Dict[str, float]], # Aggiunto il parametro mancante
flight_count: Optional[int],
):
if hasattr(self, "map_info_panel") and self.map_info_panel:
self.map_info_panel.update_general_map_info_display(
zoom, map_size_str, map_geo_bounds, target_bbox_input, flight_count
)
else:
module_logger.warning("MapInfoPanel not initialized for update_general_map_info_display.")
def show_map_context_menu(self, latitude, longitude, screen_x, screen_y): def show_map_context_menu(self, latitude, longitude, screen_x, screen_y):
if self.map_manager_instance: if self.map_manager_instance:

View File

@ -1,123 +0,0 @@
# FlightMonitor/gui/panels/area_management_panel.py
"""
Panel for managing the geographic area of interest (Bounding Box)
and area profiles.
"""
import tkinter as tk
from tkinter import ttk
from typing import Dict, Any, Optional
from ...utils.logger import get_logger
from ...data import config as app_config
from ...map.map_utils import _is_valid_bbox_dict
module_logger = get_logger(__name__)
class AreaManagementPanel:
"""
Manages the GUI elements for Bounding Box input and area profiles.
"""
def __init__(self, parent_frame: ttk.Frame, controller: Any):
"""
Initializes the AreaManagementPanel.
Args:
parent_frame: The parent ttk.Frame where this panel will be placed.
controller: The application controller instance.
"""
self.parent_frame = parent_frame
self.controller = controller
# --- Tkinter Variables ---
# Creiamo le variabili, ma le popoliamo subito dopo.
self.lat_min_var = tk.StringVar()
self.lon_min_var = tk.StringVar()
self.lat_max_var = tk.StringVar()
self.lon_max_var = tk.StringVar()
# Popoliamo le variabili con i valori di default dal config
self.lat_min_var.set(str(app_config.DEFAULT_BBOX_LAT_MIN))
self.lon_min_var.set(str(app_config.DEFAULT_BBOX_LON_MIN))
self.lat_max_var.set(str(app_config.DEFAULT_BBOX_LAT_MAX))
self.lon_max_var.set(str(app_config.DEFAULT_BBOX_LON_MAX))
self._build_ui()
module_logger.debug("AreaManagementPanel initialized.")
def _build_ui(self):
"""Builds the UI elements for area management."""
# Bounding Box Input Frame
self.bbox_frame = ttk.LabelFrame(
self.parent_frame,
text="Geographic Area (Bounding Box)",
padding=(10, 5)
)
self.bbox_frame.pack(side=tk.TOP, fill=tk.X, expand=True, padx=2, pady=(0,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_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_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_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_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)
# Placeholder for Profile Management
self.profiles_frame = ttk.LabelFrame(
self.parent_frame, text="Area Profiles", padding=(10, 5)
)
self.profiles_frame.pack(side=tk.TOP, fill=tk.X, expand=True, padx=2, pady=5)
ttk.Label(self.profiles_frame, text="Profile management coming soon...").pack()
def get_bounding_box_input(self) -> Optional[Dict[str, float]]:
"""
Retrieves and validates the bounding box coordinates from the GUI input fields.
Returns a dictionary {lat_min, lon_min, lat_max, lon_max} or None if invalid.
"""
try:
bbox_candidate = {
"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()),
}
if _is_valid_bbox_dict(bbox_candidate):
return bbox_candidate
else:
module_logger.warning(f"BBox from GUI failed validation: {bbox_candidate}")
return None
except (ValueError, TypeError):
module_logger.warning("Invalid numeric format in BBox GUI fields.")
return None
def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]):
"""Updates the bounding box input fields in the GUI."""
if bbox_dict and _is_valid_bbox_dict(bbox_dict):
decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5)
self.lat_min_var.set(f"{bbox_dict['lat_min']:.{decimals}f}")
self.lon_min_var.set(f"{bbox_dict['lon_min']:.{decimals}f}")
self.lat_max_var.set(f"{bbox_dict['lat_max']:.{decimals}f}")
self.lon_max_var.set(f"{bbox_dict['lon_max']:.{decimals}f}")
else:
self.lat_min_var.set("")
self.lon_min_var.set("")
self.lat_max_var.set("")
self.lon_max_var.set("")
def set_controls_state(self, enabled: bool):
"""Enables or disables all controls in this panel."""
state = tk.NORMAL if enabled else tk.DISABLED
for entry in [self.lat_min_entry, self.lon_min_entry, self.lat_max_entry, self.lon_max_entry]:
if entry.winfo_exists():
entry.config(state=state)
# Future: Add profile buttons here

View File

@ -1,136 +1,195 @@
# FlightMonitor/gui/panels/function_notebook_panel.py # FlightMonitor/gui/panels/function_notebook_panel.py
""" """
Panel managing the function-related notebooks (Live, Historical Download, Playback) Panel managing the area profiles, BBox input, and the function notebooks.
and their associated controls.
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk, messagebox
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from ...utils.logger import get_logger from ...utils.logger import get_logger
from ...data import config as app_config
from ...map.map_utils import _is_valid_bbox_dict
from ...data.area_profile_manager import DEFAULT_PROFILE_NAME
module_logger = get_logger(__name__) module_logger = get_logger(__name__)
class FunctionNotebookPanel: class FunctionNotebookPanel:
""" """
Manages the function-selection notebook (Live, Historical Download, Playback). Manages the combined Area Profiles, BBox, and Function Notebooks panel.
""" """
def __init__(self, parent_frame: ttk.Frame, controller: Any): def __init__(self, parent_frame: ttk.Frame, controller: Any):
"""
Initializes the FunctionNotebookPanel.
Args:
parent_frame: The parent ttk.Frame where this notebook will be placed.
controller: The application controller instance to delegate actions.
"""
self.parent_frame = parent_frame self.parent_frame = parent_frame
self.controller = controller self.controller = controller
# --- UI Elements --- # --- Tkinter Variables ---
self.function_notebook: Optional[ttk.Notebook] = None self.lat_min_var = tk.StringVar()
self.start_live_button: Optional[ttk.Button] = None self.lon_min_var = tk.StringVar()
self.stop_live_button: Optional[ttk.Button] = None self.lat_max_var = tk.StringVar()
self.lon_max_var = tk.StringVar()
# Placeholder for future controls self.selected_profile_var = tk.StringVar()
self.start_download_button: Optional[ttk.Button] = None
self._build_ui() self._build_ui()
module_logger.debug("FunctionNotebookPanel initialized.") self.update_profile_list()
self.set_selected_profile(DEFAULT_PROFILE_NAME)
module_logger.debug("FunctionNotebookPanel (unified) initialized.")
def _build_ui(self): def _build_ui(self):
""" # --- Main Container for Area Management ---
Builds the main notebook structure and all its tabs and controls. area_frame = ttk.LabelFrame(self.parent_frame, text="Area Profiles & BBox", padding=10)
""" area_frame.pack(side=tk.TOP, fill=tk.X, padx=2, pady=(0, 5))
# Configure grid for the area frame
area_frame.columnconfigure(1, weight=1)
# --- Row 0: Profile Selection and Buttons ---
profile_controls_frame = ttk.Frame(area_frame)
profile_controls_frame.grid(row=0, column=0, columnspan=4, sticky="ew", pady=(0, 10))
profile_controls_frame.columnconfigure(0, weight=1) # Let combobox expand
self.profile_combobox = ttk.Combobox(
profile_controls_frame, textvariable=self.selected_profile_var, state="readonly"
)
self.profile_combobox.grid(row=0, column=0, sticky="ew", padx=(0, 5))
self.profile_combobox.bind("<<ComboboxSelected>>", self._on_profile_selected)
new_button = ttk.Button(profile_controls_frame, text="New", command=self._on_new_profile, width=5)
new_button.grid(row=0, column=1, padx=(0, 2))
save_button = ttk.Button(profile_controls_frame, text="Save", command=self._on_save_profile, width=5)
save_button.grid(row=0, column=2, padx=(0, 2))
delete_button = ttk.Button(profile_controls_frame, text="Delete", command=self._on_delete_profile, width=7)
delete_button.grid(row=0, column=3)
# --- Row 1 & 2: Bounding Box Entries ---
ttk.Label(area_frame, text="Lat Min:").grid(row=1, column=0, padx=(0, 2), pady=2, sticky=tk.W)
self.lat_min_entry = ttk.Entry(area_frame, textvariable=self.lat_min_var)
self.lat_min_entry.grid(row=1, column=1, padx=(0, 5), pady=2, sticky=tk.EW)
ttk.Label(area_frame, text="Lon Min:").grid(row=1, column=2, padx=(5, 2), pady=2, sticky=tk.W)
self.lon_min_entry = ttk.Entry(area_frame, textvariable=self.lon_min_var)
self.lon_min_entry.grid(row=1, column=3, padx=(0, 0), pady=2, sticky=tk.EW)
ttk.Label(area_frame, text="Lat Max:").grid(row=2, column=0, padx=(0, 2), pady=2, sticky=tk.W)
self.lat_max_entry = ttk.Entry(area_frame, textvariable=self.lat_max_var)
self.lat_max_entry.grid(row=2, column=1, padx=(0, 5), pady=2, sticky=tk.EW)
ttk.Label(area_frame, text="Lon Max:").grid(row=2, column=2, padx=(5, 2), pady=2, sticky=tk.W)
self.lon_max_entry = ttk.Entry(area_frame, textvariable=self.lon_max_var)
self.lon_max_entry.grid(row=2, column=3, padx=(0, 0), pady=2, sticky=tk.EW)
# --- Function Notebook ---
self.function_notebook = ttk.Notebook(self.parent_frame) self.function_notebook = ttk.Notebook(self.parent_frame)
self.function_notebook.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) self.function_notebook.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=2, pady=2)
# --- Live Tab --- # --- Live Tab ---
live_tab_frame = ttk.Frame(self.function_notebook, padding=10) live_tab_frame = ttk.Frame(self.function_notebook, padding=10)
self.function_notebook.add(live_tab_frame, text="Live Monitor") self.function_notebook.add(live_tab_frame, text="Live Monitor")
self.start_live_button = ttk.Button(live_tab_frame, text="Start Live Monitoring", command=self._on_start_live_monitoring)
self.start_live_button = ttk.Button(
live_tab_frame, text="Start Live Monitoring", command=self._on_start_live_monitoring
)
self.start_live_button.pack(side=tk.LEFT, padx=5, pady=5) self.start_live_button.pack(side=tk.LEFT, padx=5, pady=5)
self.stop_live_button = ttk.Button(live_tab_frame, text="Stop Live Monitoring", command=self._on_stop_live_monitoring, state=tk.DISABLED)
self.stop_live_button = ttk.Button(
live_tab_frame,
text="Stop Live Monitoring",
command=self._on_stop_live_monitoring,
state=tk.DISABLED,
)
self.stop_live_button.pack(side=tk.LEFT, padx=5, pady=5) self.stop_live_button.pack(side=tk.LEFT, padx=5, pady=5)
# --- Historical Download Tab --- # --- Historical Download Tab ---
download_tab_frame = ttk.Frame(self.function_notebook, padding=10) download_tab_frame = ttk.Frame(self.function_notebook, padding=10)
self.function_notebook.add(download_tab_frame, text="Historical Download") self.function_notebook.add(download_tab_frame, text="Historical Download")
ttk.Label(download_tab_frame, text="Historical download controls...").pack()
ttk.Label(
download_tab_frame,
text="Historical download controls will be here.",
font=("Arial", 10, "italic")
).pack(expand=True)
# --- Playback Tab --- # --- Playback Tab ---
playback_tab_frame = ttk.Frame(self.function_notebook, padding=10) playback_tab_frame = ttk.Frame(self.function_notebook, padding=10)
self.function_notebook.add(playback_tab_frame, text="Playback") self.function_notebook.add(playback_tab_frame, text="Playback")
ttk.Label(playback_tab_frame, text="Playback controls...").pack()
ttk.Label(
playback_tab_frame,
text="Playback controls will be here.",
font=("Arial", 10, "italic")
).pack(expand=True)
self.function_notebook.bind("<<NotebookTabChanged>>", self._on_tab_change) self.function_notebook.bind("<<NotebookTabChanged>>", self._on_tab_change)
# --- Event Handlers ---
def _on_start_live_monitoring(self): def _on_start_live_monitoring(self):
"""Callback for the 'Start Live Monitoring' button.""" if self.controller: self.controller.start_live_monitoring()
if not self.controller:
module_logger.error("Controller not available to start live monitoring.")
return
module_logger.info("FunctionNotebookPanel: Start Live Monitoring clicked.")
# The controller will fetch the BBox from AreaManagementPanel via MainWindow
self.controller.start_live_monitoring()
def _on_stop_live_monitoring(self): def _on_stop_live_monitoring(self):
"""Callback for the 'Stop Live Monitoring' button.""" if self.controller: self.controller.stop_live_monitoring()
if not self.controller:
module_logger.error("Controller not available to stop live monitoring.") def _on_profile_selected(self, event=None):
selected_name = self.selected_profile_var.get()
if selected_name and self.controller:
self.controller.load_area_profile(selected_name)
def _on_new_profile(self):
self.selected_profile_var.set("")
self.update_bbox_gui_fields({})
def _on_save_profile(self):
if self.controller: self.controller.save_current_area_as_profile()
def _on_delete_profile(self):
profile_to_delete = self.selected_profile_var.get()
if not profile_to_delete or profile_to_delete == DEFAULT_PROFILE_NAME:
messagebox.showerror("Delete Error", "Cannot delete the default profile or no profile selected.", parent=self.parent_frame)
return return
if messagebox.askyesno("Confirm Delete", f"Delete profile '{profile_to_delete}'?", parent=self.parent_frame):
if self.controller: self.controller.delete_area_profile(profile_to_delete)
module_logger.info("FunctionNotebookPanel: Stop Live Monitoring clicked.") def _on_tab_change(self, event=None):
self.controller.stop_live_monitoring()
def _on_tab_change(self, event: Optional[tk.Event] = None):
"""Handles changes in the selected function tab."""
if not self.function_notebook: return if not self.function_notebook: return
try:
tab_text = self.function_notebook.tab(self.function_notebook.index("current"), "text") tab_text = self.function_notebook.tab(self.function_notebook.index("current"), "text")
module_logger.info(f"FunctionNotebookPanel: Switched to tab: {tab_text}")
# Delegate placeholder updates to MainWindow via controller
if self.controller and hasattr(self.controller, "on_function_tab_changed"): if self.controller and hasattr(self.controller, "on_function_tab_changed"):
self.controller.on_function_tab_changed(tab_text) self.controller.on_function_tab_changed(tab_text)
except (tk.TclError, IndexError) as e: # --- Public Methods ---
module_logger.warning(f"Error getting selected tab info: {e}") def update_profile_list(self):
if self.controller:
profile_names = self.controller.get_profile_names()
self.profile_combobox["values"] = profile_names
self.set_selected_profile(self.selected_profile_var.get() or DEFAULT_PROFILE_NAME)
def set_selected_profile(self, profile_name: str):
if profile_name in self.profile_combobox["values"]:
self.selected_profile_var.set(profile_name)
elif self.profile_combobox["values"]:
self.selected_profile_var.set(self.profile_combobox["values"][0])
def get_bounding_box_input(self):
# ... (metodo rimane invariato)
try:
bbox_candidate = {
"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()),
}
return bbox_candidate if _is_valid_bbox_dict(bbox_candidate) else None
except (ValueError, TypeError):
return None
def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]):
# ... (metodo rimane invariato)
if bbox_dict and _is_valid_bbox_dict(bbox_dict):
decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5)
self.lat_min_var.set(f"{bbox_dict['lat_min']:.{decimals}f}")
self.lon_min_var.set(f"{bbox_dict['lon_min']:.{decimals}f}")
self.lat_max_var.set(f"{bbox_dict['lat_max']:.{decimals}f}")
self.lon_max_var.set(f"{bbox_dict['lon_max']:.{decimals}f}")
else:
self.lat_min_var.set("")
self.lon_min_var.set("")
self.lat_max_var.set("")
self.lon_max_var.set("")
def set_monitoring_button_states(self, is_monitoring_active: bool): def set_monitoring_button_states(self, is_monitoring_active: bool):
""" # ... (metodo rimane invariato)
Sets the state of the Start/Stop buttons for the Live tab.
"""
if self.start_live_button and self.start_live_button.winfo_exists(): if self.start_live_button and self.start_live_button.winfo_exists():
self.start_live_button.config(state=tk.DISABLED if is_monitoring_active else tk.NORMAL) self.start_live_button.config(state=tk.DISABLED if is_monitoring_active else tk.NORMAL)
if self.stop_live_button and self.stop_live_button.winfo_exists(): if self.stop_live_button and self.stop_live_button.winfo_exists():
self.stop_live_button.config(state=tk.NORMAL if is_monitoring_active else tk.DISABLED) self.stop_live_button.config(state=tk.NORMAL if is_monitoring_active else tk.DISABLED)
def get_active_tab_text(self) -> Optional[str]: def set_controls_state(self, enabled: bool):
"""Returns the text of the currently active tab.""" state = tk.NORMAL if enabled else tk.DISABLED
if not self.function_notebook: return None readonly_state = "readonly" if enabled else tk.DISABLED
try:
return self.function_notebook.tab(self.function_notebook.index("current"), "text") self.profile_combobox.config(state=readonly_state)
except (tk.TclError, IndexError): for entry in [self.lat_min_entry, self.lon_min_entry, self.lat_max_entry, self.lon_max_entry]:
return None if entry.winfo_exists(): entry.config(state=state)
# Abilita/Disabilita i bottoni dei profili
for child in self.profile_combobox.master.winfo_children():
if isinstance(child, ttk.Button):
child.config(state=state)