add profiling area bbox
This commit is contained in:
parent
953c02d879
commit
6fa4928b0c
1
config/area_profiles.json
Normal file
1
config/area_profiles.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@ -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"):
|
||||||
@ -449,4 +459,77 @@ class AppController:
|
|||||||
if self.main_window and self.main_window.map_manager_instance:
|
if self.main_window and self.main_window.map_manager_instance:
|
||||||
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.")
|
||||||
114
flightmonitor/data/area_profile_manager.py
Normal file
114
flightmonitor/data/area_profile_manager.py
Normal 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
|
||||||
@ -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:
|
||||||
|
|||||||
@ -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
|
|
||||||
@ -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))
|
||||||
self.function_notebook = ttk.Notebook(self.parent_frame)
|
|
||||||
self.function_notebook.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
|
|
||||||
|
|
||||||
|
# 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.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):
|
||||||
module_logger.info("FunctionNotebookPanel: Stop Live Monitoring clicked.")
|
if self.controller: self.controller.delete_area_profile(profile_to_delete)
|
||||||
self.controller.stop_live_monitoring()
|
|
||||||
|
|
||||||
def _on_tab_change(self, event: Optional[tk.Event] = None):
|
def _on_tab_change(self, event=None):
|
||||||
"""Handles changes in the selected function tab."""
|
|
||||||
if not self.function_notebook: return
|
if not self.function_notebook: return
|
||||||
|
tab_text = self.function_notebook.tab(self.function_notebook.index("current"), "text")
|
||||||
|
if self.controller and hasattr(self.controller, "on_function_tab_changed"):
|
||||||
|
self.controller.on_function_tab_changed(tab_text)
|
||||||
|
|
||||||
|
# --- Public Methods ---
|
||||||
|
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:
|
try:
|
||||||
tab_text = self.function_notebook.tab(self.function_notebook.index("current"), "text")
|
bbox_candidate = {
|
||||||
module_logger.info(f"FunctionNotebookPanel: Switched to tab: {tab_text}")
|
"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("")
|
||||||
|
|
||||||
# Delegate placeholder updates to MainWindow via controller
|
|
||||||
if self.controller and hasattr(self.controller, "on_function_tab_changed"):
|
|
||||||
self.controller.on_function_tab_changed(tab_text)
|
|
||||||
|
|
||||||
except (tk.TclError, IndexError) as e:
|
|
||||||
module_logger.warning(f"Error getting selected tab info: {e}")
|
|
||||||
|
|
||||||
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)
|
||||||
Loading…
Reference in New Issue
Block a user