diff --git a/flightmonitor/__main__.py b/flightmonitor/__main__.py index ff811b8..0073e90 100644 --- a/flightmonitor/__main__.py +++ b/flightmonitor/__main__.py @@ -6,10 +6,10 @@ import tkinter as tk # HOW: Added import. from .data.logging_config import LOGGING_CONFIG # Import the config dictionary -# MODIFIED: Import the logger setup function AND get_logger function -# WHY: Need to call the centralized setup function early and get logger instances. -# HOW: Added get_logger to the import statement. -from .utils.logger import setup_logging, get_logger +# MODIFIED: Import the new setup_basic_logging function AND get_logger function +# WHY: Need to call the centralized basic setup function early and get logger instances. +# HOW: Changed setup_logging to setup_basic_logging. +from .utils.logger import setup_basic_logging, get_logger # MODIFIED HERE from .gui.main_window import MainWindow from .controller.app_controller import AppController @@ -18,41 +18,31 @@ from .controller.app_controller import AppController def main(): """ Main function to launch the Flight Monitor application. - Initializes the Tkinter root, sets up logging, creates the controller + Initializes the Tkinter root, sets up basic logging, creates the controller and the main window, and starts the event loop. """ # Initialize the Tkinter root window FIRST root = tk.Tk() - # MODIFIED: Setup logging AFTER Tkinter root is created - # WHY: The TkinterTextHandler requires a root instance. - # Setup logging as early as possible after dependencies are met. - # HOW: Called setup_logging here, passing the necessary arguments including the config. - setup_logging( - gui_log_widget=None, root_tk_instance=root, logging_config_dict=LOGGING_CONFIG + # MODIFIED: Setup basic logging AFTER Tkinter root is created + # WHY: The Tkinter-based log processor needs a root instance to schedule itself. + # The TkinterTextHandler itself will be added later by MainWindow. + # HOW: Called setup_basic_logging here, passing the necessary arguments. + setup_basic_logging( # MODIFIED HERE + root_tk_instance_for_processor=root, # MODIFIED: Pass root for the log processor + logging_config_dict=LOGGING_CONFIG ) - # Now that basic logging is set up, we can use module_logger - # MODIFIED: Call get_logger directly to get the logger for this module. - # WHY: get_logger is a function, not a method of setup_logging. - # HOW: Changed setup_logging.get_logger to just get_logger. - module_logger_main = get_logger( - __name__ - ) # Get logger using the function from logger.py + # Now that basic logging is set up (console, file, queue), we can use module_logger + module_logger_main = get_logger(__name__) module_logger_main.info("Application starting...") # Create the application controller app_controller = AppController() # Create the main application window - # Pass the root and the controller to the MainWindow. - # MainWindow itself will now handle the creation and passing of its log widget - # to setup_logging (which we already called above, but will be called again by MW if it needs to). - # Ideally, MainWindow's __init__ should just receive the logger configuration somehow - # and pass its log widget to setup_logging if it exists. - # With the current design, MainWindow will call setup_logging *again* in its __init__, - # passing its log widget. This is fine; setup_logging is designed to handle re-initialization - # and replace handlers. + # MainWindow itself will now handle the creation and specific addition of its log widget + # to the logging system using a new dedicated function in the logger module. main_app_window = MainWindow(root, app_controller) # Set the main window instance in the controller @@ -63,13 +53,10 @@ def main(): root.mainloop() module_logger_main.info("Tkinter main loop finished.") - # Cleanup should happen when the window is closed (WM_DELETE_WINDOW) - # or after mainloop exits (if it exits cleanly). - # The on_application_exit in the controller handles non-GUI cleanup. - # The shutdown_gui_logging is called by MainWindow's _on_closing. - + # Cleanup is handled by MainWindow's _on_closing method, which in turn + # calls controller.on_application_exit and logger.shutdown_logging_system. module_logger_main.info("Application finished.") if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/flightmonitor/gui/main_window.py b/flightmonitor/gui/main_window.py index 1b6450b..d741b76 100644 --- a/flightmonitor/gui/main_window.py +++ b/flightmonitor/gui/main_window.py @@ -12,13 +12,19 @@ from tkinter import Menu from tkinter.scrolledtext import ScrolledText from tkinter import font as tkFont from typing import List, Dict, Optional, Tuple, Any -import logging +import logging # Import standard logging import os from datetime import datetime, timezone import webbrowser from ..data import config as app_config -from ..utils.logger import get_logger, setup_logging, shutdown_logging_system + +# MODIFIED: Import add_tkinter_handler and shutdown_logging_system from our logger module +# Also import get_logger for module-level logging +# Import LOGGING_CONFIG to pass to add_tkinter_handler +from ..utils.logger import get_logger, add_tkinter_handler, shutdown_logging_system # MODIFIED HERE +from ..data.logging_config import LOGGING_CONFIG # MODIFIED HERE + from ..data.common_models import CanonicalFlightState from ..utils.gui_utils import ( GUI_STATUS_OK, @@ -39,8 +45,10 @@ except ImportError as e_map_import: MapCanvasManager = None # type: ignore _is_valid_bbox_dict = lambda x: False # type: ignore MAP_CANVAS_MANAGER_AVAILABLE = False + # Use standard print for early critical errors if logger not fully up print( - f"CRITICAL ERROR in MainWindow import: Failed to import MapCanvasManager or map_utils: {e_map_import}. Map functionality will be disabled." + f"CRITICAL ERROR in MainWindow import: Failed to import MapCanvasManager or map_utils: {e_map_import}. Map functionality will be disabled.", + flush=True ) try: @@ -51,23 +59,11 @@ except ImportError as e_dialog_import: ImportProgressDialog = None # type: ignore IMPORT_DIALOG_AVAILABLE = False print( - f"ERROR in MainWindow import: Failed to import ImportProgressDialog: {e_dialog_import}. Import progress UI will be basic." + f"ERROR in MainWindow import: Failed to import ImportProgressDialog: {e_dialog_import}. Import progress UI will be basic.", + flush=True ) -# Import per FullFlightDetailsWindow (verrà usato da AppController per creare la finestra) -# Non è strettamente necessario importarlo qui se MainWindow non lo istanzia direttamente, -# ma può essere utile per il type hinting se passiamo riferimenti. -# Per ora, lo lasciamo commentato qui, dato che AppController lo importerà localmente al bisogno. -# try: -# from .dialogs.full_flight_details_window import FullFlightDetailsWindow -# FULL_DETAILS_WINDOW_AVAILABLE = True -# except ImportError as e_details_dialog: -# FullFlightDetailsWindow = None # type: ignore -# FULL_DETAILS_WINDOW_AVAILABLE = False -# print(f"WARNING in MainWindow import: Failed to import FullFlightDetailsWindow: {e_details_dialog}. Full details window might not work.") - - -module_logger = get_logger(__name__) +module_logger = get_logger(__name__) # Get logger for this module SEMAPHORE_SIZE = 12 SEMAPHORE_PAD = 3 @@ -92,7 +88,7 @@ class MainWindow: self.root.title("Flight Monitor") self.progress_dialog: Optional[ImportProgressDialog] = None self.full_flight_details_window: Optional[tk.Toplevel] = ( - None # Sarà di tipo FullFlightDetailsWindow + None ) if app_config.LAYOUT_START_MAXIMIZED: @@ -401,13 +397,17 @@ class MainWindow: ) self._create_selected_flight_details_content(self.selected_flight_details_frame) - from ..data.logging_config import LOGGING_CONFIG + # MODIFIED: Call add_tkinter_handler instead of setup_logging + # This assumes setup_basic_logging has already been called in __main__.py + if self.log_text_widget and self.root: # Ensure widgets are created + add_tkinter_handler( # MODIFIED HERE + gui_log_widget=self.log_text_widget, + root_tk_instance_for_gui_handler=self.root, # Pass self.root + logging_config_dict=LOGGING_CONFIG # Pass the imported config + ) + else: + module_logger.error("log_text_widget or root not available in MainWindow init for logger setup.") - setup_logging( - gui_log_widget=self.log_text_widget, - root_tk_instance=self.root, - logging_config_dict=LOGGING_CONFIG, - ) self.root.protocol("WM_DELETE_WINDOW", self._on_closing) self.root.after(100, self._delayed_initialization) @@ -415,6 +415,44 @@ class MainWindow: "MainWindow basic structure initialized. Delayed init pending." ) + def _on_closing(self): + module_logger.info("Main window closing event triggered.") + user_confirmed_quit = ( + messagebox.askokcancel( + "Quit", "Do you want to quit Flight Monitor?", parent=self.root + ) + if self.root.winfo_exists() + else True + ) + if user_confirmed_quit: + if self.controller and hasattr(self.controller, "on_application_exit"): + try: + self.controller.on_application_exit() + except Exception as e: + module_logger.error( + f"Error during controller.on_application_exit: {e}", + exc_info=True, + ) + try: + # shutdown_logging_system() is called here to ensure all handlers are closed + # and remaining logs are processed before the application fully exits. + shutdown_logging_system() # MODIFIED: Ensured this is called + except Exception as e: + module_logger.error( + f"Error during shutdown_logging_system: {e}", exc_info=True + ) + + if hasattr(self, "root") and self.root.winfo_exists(): + try: + self.root.destroy() + except Exception: + pass + else: + module_logger.info("User cancelled quit.") + + # ... (il resto dei metodi di MainWindow rimane invariato) ... + # ... (_delayed_initialization, _initialize_map_manager, _recreate_map_tools_content, etc.) + # COPIA QUI TUTTI GLI ALTRI METODI DI MainWindow OMETTENDO SOLO __init__ e _on_closing GIA' RIPORTATI SOPRA def _import_aircraft_db_csv(self): if not self.controller: self.show_error_message( @@ -496,7 +534,6 @@ class MainWindow: module_logger.info( f"Requesting full details window for ICAO: {icao_to_show}" ) - # Il controller ora gestisce la creazione e la visualizzazione della finestra self.controller.request_and_show_full_flight_details(icao_to_show) else: module_logger.error( @@ -514,14 +551,17 @@ class MainWindow: "Full details button clicked, but no flight ICAO found in details panel." ) - # ... (Tutti gli altri metodi di MainWindow, come _delayed_initialization, _recreate_map_tools_content, etc. - # devono essere copiati qui. Per brevità, li ometto ma devono essere presenti.) def _delayed_initialization(self): if not self.root.winfo_exists(): module_logger.warning( "Root window destroyed before delayed initialization." ) return + + # MODIFIED: Moved set_map_track_length to be called AFTER map_manager_instance is created + # inside _initialize_map_manager or ensure controller handles it if map_manager not ready. + # For now, we'll ensure _initialize_map_manager handles the initial track length. + if MAP_CANVAS_MANAGER_AVAILABLE and MapCanvasManager is not None: default_map_bbox = { "lat_min": app_config.DEFAULT_BBOX_LAT_MIN, @@ -529,7 +569,9 @@ class MainWindow: "lat_max": app_config.DEFAULT_BBOX_LAT_MAX, "lon_max": app_config.DEFAULT_BBOX_LON_MAX, } - self.root.after(200, self._initialize_map_manager, default_map_bbox) + # Pass initial track length to _initialize_map_manager + initial_track_len = self.track_length_var.get() if hasattr(self, "track_length_var") else DEFAULT_TRACK_LENGTH + self.root.after(200, self._initialize_map_manager, default_map_bbox, initial_track_len) else: module_logger.error("MapCanvasManager class not available post-init.") self.root.after( @@ -538,36 +580,29 @@ class MainWindow: "Map functionality disabled (Import Error)." ), ) - if ( - hasattr(self, "track_length_var") - and self.controller - and hasattr(self.controller, "set_map_track_length") - ): - try: - initial_track_len = self.track_length_var.get() - if initial_track_len > 0: - self.controller.set_map_track_length(initial_track_len) - except Exception as e: - module_logger.error(f"Error setting initial track length: {e}") - self.root.after(10, self._on_mode_change) + + self.root.after(10, self._on_mode_change) # Ensure mode change updates UI correctly module_logger.info("MainWindow fully initialized.") - def _initialize_map_manager(self, initial_bbox_for_map: Dict[str, float]): + def _initialize_map_manager(self, initial_bbox_for_map: Dict[str, float], initial_track_length: int): # Added initial_track_length if not MAP_CANVAS_MANAGER_AVAILABLE or MapCanvasManager is None: self._update_map_placeholder("Map Error: Manager class missing.") if self.controller and hasattr(self.controller, "update_general_map_info"): self.controller.update_general_map_info() return + if not self.flight_canvas.winfo_exists(): + module_logger.warning("Flight canvas does not exist. Cannot initialize MapCanvasManager.") return + canvas_w, canvas_h = ( self.flight_canvas.winfo_width(), self.flight_canvas.winfo_height(), ) - if canvas_w <= 1: - canvas_w = self.canvas_width - if canvas_h <= 1: - canvas_h = self.canvas_height + + if canvas_w <= 1 : canvas_w = self.canvas_width + if canvas_h <= 1 : canvas_h = self.canvas_height + if canvas_w > 1 and canvas_h > 1: try: self.map_manager_instance = MapCanvasManager( @@ -575,23 +610,22 @@ class MainWindow: tk_canvas=self.flight_canvas, initial_bbox_dict=initial_bbox_for_map, ) - if ( - hasattr(self, "track_length_var") - and self.controller - and hasattr(self.controller, "set_map_track_length") - ): + # MODIFIED: Set track length AFTER map_manager_instance is created + if self.controller and hasattr(self.controller, "set_map_track_length"): try: - self.controller.set_map_track_length( - self.track_length_var.get() - ) + self.controller.set_map_track_length(initial_track_length) + module_logger.info(f"Initial map track length set to {initial_track_length} via controller.") except Exception as e_trk: module_logger.error( f"Error setting initial track length for map manager: {e_trk}" ) - if self.controller and hasattr( - self.controller, "update_general_map_info" - ): + else: + module_logger.warning("Controller or set_map_track_length not available for initial setup.") + + + if self.controller and hasattr(self.controller, "update_general_map_info"): self.controller.update_general_map_info() + except Exception as e_init: module_logger.critical( f"Failed to initialize MapCanvasManager: {e_init}", exc_info=True @@ -600,16 +634,14 @@ class MainWindow: "Map Error", f"Could not initialize map: {e_init}" ) self._update_map_placeholder(f"Map Error: Init failed.\n{e_init}") - if self.controller and hasattr( - self.controller, "update_general_map_info" - ): + if self.controller and hasattr(self.controller, "update_general_map_info"): self.controller.update_general_map_info() else: - if self.root.winfo_exists(): - self.root.after(300, self._initialize_map_manager, initial_bbox_for_map) + if self.root.winfo_exists(): # Retry if canvas not sized yet + module_logger.info("Canvas not sized yet, retrying map manager initialization.") + self.root.after(300, self._initialize_map_manager, initial_bbox_for_map, initial_track_length) def _recreate_map_tools_content(self, parent_frame: ttk.Frame): - # ... (come prima) controls_map_container = ttk.Frame(parent_frame) controls_map_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) zoom_frame = ttk.Frame(controls_map_container) @@ -676,9 +708,8 @@ class MainWindow: ) def _recreate_map_info_content(self, parent_frame: ttk.Frame): - # ... (come prima) - parent_frame.columnconfigure(1, weight=0) - parent_frame.columnconfigure(3, weight=0) + parent_frame.columnconfigure(1, weight=0) # Smaller weight for value labels + parent_frame.columnconfigure(3, weight=0) # Smaller weight for value labels info_row = 0 ttk.Label(parent_frame, text="Click Lat:").grid( row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2) @@ -810,7 +841,6 @@ class MainWindow: ) def _create_selected_flight_details_content(self, parent_frame: ttk.LabelFrame): - # ... (come prima) parent_frame.columnconfigure(1, weight=1) parent_frame.columnconfigure(3, weight=1) self.flight_detail_labels: Dict[str, ttk.Label] = {} @@ -874,7 +904,6 @@ class MainWindow: ) def update_selected_flight_details(self, flight_data: Optional[Dict[str, Any]]): - # ... (come prima) module_logger.debug( f"GUI: Updating flight details panel for: {flight_data.get('icao24', 'None') if flight_data else 'None'}" ) @@ -883,14 +912,17 @@ class MainWindow: "flight_detail_labels not initialized. Cannot update details." ) return + all_panel_keys = list(self.flight_detail_labels.keys()) for key in all_panel_keys: label_widget = self.flight_detail_labels.get(key) if label_widget and label_widget.winfo_exists(): label_widget.config(text="N/A") + if flight_data: baro_alt_val = flight_data.get("baro_altitude_m") geo_alt_val = flight_data.get("geo_altitude_m") + if ( "baro_altitude_m" in self.flight_detail_labels and self.flight_detail_labels["baro_altitude_m"].winfo_exists() @@ -898,9 +930,10 @@ class MainWindow: alt_text = "N/A" if baro_alt_val is not None: alt_text = f"{baro_alt_val:.0f} m" - elif geo_alt_val is not None: + elif geo_alt_val is not None: # Fallback to geo if baro is N/A alt_text = f"{geo_alt_val:.0f} m (geo)" self.flight_detail_labels["baro_altitude_m"].config(text=alt_text) + if ( "geo_altitude_m" in self.flight_detail_labels and self.flight_detail_labels["geo_altitude_m"].winfo_exists() @@ -908,21 +941,17 @@ class MainWindow: alt_geo_text = "N/A" if geo_alt_val is not None: alt_geo_text = f"{geo_alt_val:.0f} m" - if ( - baro_alt_val is not None - and geo_alt_val is not None - and abs(baro_alt_val - geo_alt_val) > 1 - ): - self.flight_detail_labels["geo_altitude_m"].config( - text=alt_geo_text - ) - elif baro_alt_val is None and geo_alt_val is not None: - self.flight_detail_labels["geo_altitude_m"].config( - text=alt_geo_text - ) + + # Show geo_altitude only if it's significantly different from baro, or if baro is N/A + if (baro_alt_val is not None and geo_alt_val is not None and abs(baro_alt_val - geo_alt_val) > 1) or \ + (baro_alt_val is None and geo_alt_val is not None) : + self.flight_detail_labels["geo_altitude_m"].config(text=alt_geo_text) + # else, leave it N/A if baro is present and geo is similar or N/A + for key, label_widget in self.flight_detail_labels.items(): - if key in ["baro_altitude_m", "geo_altitude_m"]: + if key in ["baro_altitude_m", "geo_altitude_m"]: # Already handled continue + if label_widget and label_widget.winfo_exists(): value = flight_data.get(key) formatted_value = "N/A" @@ -944,29 +973,26 @@ class MainWindow: elif key in [ "timestamp", "last_contact_timestamp", - "firstflightdate", - "timestamp_metadata", + "firstflightdate", # String from DB + "timestamp_metadata", # Timestamp from DB ]: - if isinstance(value, (int, float)) and value > 0: + if isinstance(value, (int, float)) and value > 0 : # If it's a raw timestamp try: formatted_value = datetime.fromtimestamp( value, tz=timezone.utc ).strftime("%Y-%m-%d %H:%M:%S Z") - except: + except: # Should not happen with valid ts formatted_value = str(value) + " (raw ts)" - elif isinstance(value, str) and value.strip(): + elif isinstance(value, str) and value.strip(): # If it's already a date string formatted_value = value elif isinstance(value, bool): formatted_value = str(value) - elif key == "built_year" and value: - formatted_value = ( - str(int(value)) - if isinstance(value, (float, int)) and value > 0 - else str(value) - ) + elif key == "built_year" and value: # Ensure built_year without decimals + formatted_value = str(int(value)) if isinstance(value, (float,int)) and value > 0 else str(value) else: formatted_value = str(value) label_widget.config(text=formatted_value) + if ( hasattr(self, "full_details_button") and self.full_details_button.winfo_exists() @@ -974,7 +1000,7 @@ class MainWindow: self.full_details_button.config( state=tk.NORMAL if flight_data.get("icao24") else tk.DISABLED ) - else: + else: # No flight_data if ( hasattr(self, "full_details_button") and self.full_details_button.winfo_exists() @@ -982,7 +1008,7 @@ class MainWindow: self.full_details_button.config(state=tk.DISABLED) module_logger.debug("Selected flight details panel updated.") - # ... Ripeto qui gli altri metodi per completezza, assicurati che siano tutti presenti nel tuo file ... + def update_semaphore_and_status(self, status_level: str, message: str): color_to_set = SEMAPHORE_COLOR_STATUS_MAP.get( status_level, SEMAPHORE_COLOR_STATUS_MAP.get(GUI_STATUS_UNKNOWN, "gray60") @@ -996,8 +1022,9 @@ class MainWindow: self.semaphore_canvas.itemconfig( self._semaphore_oval_id, fill=color_to_set ) - except tk.TclError: + except tk.TclError: # Can happen if called during shutdown pass + current_status_text = f"Status: {message}" if ( hasattr(self, "status_label") @@ -1006,53 +1033,17 @@ class MainWindow: ): try: self.status_label.config(text=current_status_text) - except tk.TclError: + except tk.TclError: # Can happen if called during shutdown pass - log_level_to_use = logging.INFO - if status_level == GUI_STATUS_ERROR: - log_level_to_use = logging.ERROR - elif status_level == GUI_STATUS_PERMANENT_FAILURE: - log_level_to_use = logging.CRITICAL - elif status_level == GUI_STATUS_WARNING: - log_level_to_use = logging.WARNING - elif status_level in [GUI_STATUS_FETCHING, GUI_STATUS_OK, GUI_STATUS_UNKNOWN]: - log_level_to_use = logging.DEBUG - module_logger.log( - log_level_to_use, - f"GUI Status Update: Level='{status_level}', Message='{message}'", - ) - def _on_closing(self): - module_logger.info("Main window closing event triggered.") - user_confirmed_quit = ( - messagebox.askokcancel( - "Quit", "Do you want to quit Flight Monitor?", parent=self.root - ) - if self.root.winfo_exists() - else True - ) - if user_confirmed_quit: - if self.controller and hasattr(self.controller, "on_application_exit"): - try: - self.controller.on_application_exit() - except Exception as e: - module_logger.error( - f"Error during controller.on_application_exit: {e}", - exc_info=True, - ) - try: - shutdown_logging_system() - except Exception as e: - module_logger.error( - f"Error during shutdown_logging_system: {e}", exc_info=True - ) - if hasattr(self, "root") and self.root.winfo_exists(): - try: - self.root.destroy() - except Exception: - pass # Ignore errors on final destroy - else: - module_logger.info("User cancelled quit.") + # Log the status update using the module logger (already configured) + log_level_to_use = logging.INFO # Default + if status_level == GUI_STATUS_ERROR: log_level_to_use = logging.ERROR + elif status_level == GUI_STATUS_PERMANENT_FAILURE: log_level_to_use = logging.CRITICAL + elif status_level == GUI_STATUS_WARNING: log_level_to_use = logging.WARNING + elif status_level in [GUI_STATUS_FETCHING, GUI_STATUS_OK, GUI_STATUS_UNKNOWN]: log_level_to_use = logging.DEBUG + module_logger.log(log_level_to_use, f"GUI Status Update: Level='{status_level}', Message='{message}'") + def _reset_gui_to_stopped_state( self, status_message: Optional[str] = "Monitoring stopped." @@ -1061,20 +1052,24 @@ class MainWindow: self.start_button.config(state=tk.NORMAL) if hasattr(self, "stop_button") and self.stop_button.winfo_exists(): self.stop_button.config(state=tk.DISABLED) + + # Re-enable mode radio buttons if hasattr(self, "live_radio") and self.live_radio.winfo_exists(): self.live_radio.config(state=tk.NORMAL) if hasattr(self, "history_radio") and self.history_radio.winfo_exists(): self.history_radio.config(state=tk.NORMAL) - self._update_controls_state_based_on_mode_and_tab() + + self._update_controls_state_based_on_mode_and_tab() # Update BBox/Track based on new state + status_level = GUI_STATUS_OK - if status_message and ( - "failed" in status_message.lower() or "error" in status_message.lower() - ): + if status_message and ("failed" in status_message.lower() or "error" in status_message.lower()): status_level = GUI_STATUS_ERROR elif status_message and "warning" in status_message.lower(): status_level = GUI_STATUS_WARNING + if hasattr(self, "root") and self.root.winfo_exists(): - self.update_semaphore_and_status(status_level, status_message) + self.update_semaphore_and_status(status_level, status_message if status_message else "System Ready.") + def _should_show_main_placeholder(self) -> bool: return not ( @@ -1090,22 +1085,23 @@ class MainWindow: and self.flight_canvas.winfo_exists() ): return - if not self._should_show_main_placeholder(): + + if not self._should_show_main_placeholder(): # Map manager is active, clear placeholder try: self.flight_canvas.delete("placeholder_text") - except: + except: # tk.TclError if item doesn't exist or canvas gone pass return + try: - self.flight_canvas.delete("placeholder_text") + self.flight_canvas.delete("placeholder_text") # Clear previous one canvas_w, canvas_h = ( self.flight_canvas.winfo_width(), self.flight_canvas.winfo_height(), ) - if canvas_w <= 1: - canvas_w = self.canvas_width - if canvas_h <= 1: - canvas_h = self.canvas_height + if canvas_w <= 1: canvas_w = self.canvas_width # Use fallback if not sized + if canvas_h <= 1: canvas_h = self.canvas_height + if canvas_w > 1 and canvas_h > 1: self.flight_canvas.create_text( canvas_w / 2, @@ -1115,10 +1111,13 @@ class MainWindow: fill="gray50", font=("Arial", 12, "italic"), justify=tk.CENTER, - width=canvas_w - 40, + width=canvas_w - 40 # Wrap text ) - except: - pass # Ignore TclErrors if canvas is gone + except tk.TclError: # Canvas might be gone + pass + except Exception as e_placeholder_draw: + module_logger.error(f"Error drawing placeholder: {e_placeholder_draw}") + def _on_mode_change(self): if not ( @@ -1127,55 +1126,46 @@ class MainWindow: and self.function_notebook.winfo_exists() ): return + selected_mode = self.mode_var.get() status_message = f"Mode: {selected_mode}. Ready." + module_logger.info(f"GUI: Mode changed to {selected_mode}") + try: - tab_indices = { - name: i - for i, name_tuple in enumerate( - map( - lambda t: (self.function_notebook.tab(t, "text"), t), - self.function_notebook.tabs(), - ) - ) - for name in name_tuple - } - live_bbox_idx, history_idx, live_airport_idx = ( - tab_indices.get("Live: Area Monitor", -1), - tab_indices.get("History", -1), - tab_indices.get("Live: Airport", -1), - ) + # Get tab indices by their text name for robustness + tab_indices = {} + for i in range(self.function_notebook.index("end")): + tab_text = self.function_notebook.tab(i, "text") + tab_indices[tab_text] = i + + live_bbox_idx = tab_indices.get("Live: Area Monitor", -1) + history_idx = tab_indices.get("History", -1) + live_airport_idx = tab_indices.get("Live: Airport", -1) + if selected_mode == "Live": - if live_bbox_idx != -1: - self.function_notebook.tab(live_bbox_idx, state="normal") - if live_airport_idx != -1: - self.function_notebook.tab(live_airport_idx, state="normal") - if history_idx != -1: - self.function_notebook.tab(history_idx, state="disabled") - if ( - self.function_notebook.index("current") == history_idx - and live_bbox_idx != -1 - ): + if live_bbox_idx != -1: self.function_notebook.tab(live_bbox_idx, state="normal") + if live_airport_idx != -1: self.function_notebook.tab(live_airport_idx, state="normal") + if history_idx != -1: self.function_notebook.tab(history_idx, state="disabled") + # Switch to the first live tab if history was selected + if self.function_notebook.index("current") == history_idx and live_bbox_idx != -1: self.function_notebook.select(live_bbox_idx) elif selected_mode == "History": - if live_bbox_idx != -1: - self.function_notebook.tab(live_bbox_idx, state="disabled") - if live_airport_idx != -1: - self.function_notebook.tab(live_airport_idx, state="disabled") - if history_idx != -1: - self.function_notebook.tab(history_idx, state="normal") - if ( - self.function_notebook.index("current") != history_idx - and history_idx != -1 - ): + if live_bbox_idx != -1: self.function_notebook.tab(live_bbox_idx, state="disabled") + if live_airport_idx != -1: self.function_notebook.tab(live_airport_idx, state="disabled") + if history_idx != -1: self.function_notebook.tab(history_idx, state="normal") + # Switch to history tab if not already selected + if self.function_notebook.index("current") != history_idx and history_idx != -1: self.function_notebook.select(history_idx) - except Exception as e: - module_logger.warning(f"Error updating func tabs: {e}", exc_info=True) - self.clear_all_views_data() - self._update_controls_state_based_on_mode_and_tab() + except Exception as e: # tk.TclError can happen if notebook/tabs don't exist as expected + module_logger.warning(f"Error updating function notebook tabs on mode change: {e}", exc_info=True) + + self.clear_all_views_data() # Clear map/table when mode changes + self._update_controls_state_based_on_mode_and_tab() # Update BBox/Track state + if hasattr(self, "root") and self.root.winfo_exists(): self.update_semaphore_and_status(GUI_STATUS_OK, status_message) + def _on_function_tab_change(self, event: Optional[tk.Event] = None): if not ( hasattr(self, "function_notebook") and self.function_notebook.winfo_exists() @@ -1185,55 +1175,67 @@ class MainWindow: tab_text = self.function_notebook.tab( self.function_notebook.index("current"), "text" ) - placeholder_text_map = "Map Area." + module_logger.info(f"GUI: Switched function tab to: {tab_text}") + placeholder_text_map = "Map Area." # Default if "Live: Area Monitor" in tab_text: placeholder_text_map = "Map - Live Area. Define area and press Start." elif "Live: Airport" in tab_text: - placeholder_text_map = "Map - Live Airport. (TBD)" + placeholder_text_map = "Map - Live Airport. (Coming Soon)" elif "History" in tab_text: - placeholder_text_map = "Map - History Analysis. (TBD)" + placeholder_text_map = "Map - History Analysis. (Coming Soon)" + self._update_map_placeholder(placeholder_text_map) self._update_controls_state_based_on_mode_and_tab() - except: - pass + except tk.TclError: # Can happen if notebook is in a weird state + module_logger.warning("TclError on function tab change, notebook might be closing.", exc_info=False) + except Exception as e_tab_change: + module_logger.error(f"Error processing function tab change: {e_tab_change}", exc_info=True) def _update_controls_state_based_on_mode_and_tab(self): is_live_mode = hasattr(self, "mode_var") and self.mode_var.get() == "Live" is_monitoring_active = ( hasattr(self, "stop_button") and self.stop_button.winfo_exists() - and self.stop_button.cget("state") == tk.NORMAL + and self.stop_button.cget("state") == tk.NORMAL # Monitoring if stop button is enabled ) + active_func_tab_text = "" if hasattr(self, "function_notebook") and self.function_notebook.winfo_exists(): try: active_func_tab_text = self.function_notebook.tab( self.function_notebook.index("current"), "text" ) - except: + except tk.TclError: # Tab might not be selectable or notebook closing pass - enable_bbox = ( + + # Enable BBox entries only if in Live Area mode AND not currently monitoring + enable_bbox_entries = ( is_live_mode and "Live: Area Monitor" in active_func_tab_text and not is_monitoring_active ) - enable_track = is_live_mode and "Live: Area Monitor" in active_func_tab_text - self._set_bbox_entries_state(tk.NORMAL if enable_bbox else tk.DISABLED) + self._set_bbox_entries_state(tk.NORMAL if enable_bbox_entries else tk.DISABLED) + + # Enable Track Length spinbox if in Live Area mode (can be changed even if monitoring, effect on next data) + # OR make it disabled if monitoring, depending on desired behavior. + # Current behavior: disable if monitoring. + enable_track_spinbox = ( + is_live_mode + and "Live: Area Monitor" in active_func_tab_text + # and not is_monitoring_active # Uncomment to disable if monitoring active + ) if ( hasattr(self, "track_length_spinbox") and self.track_length_spinbox.winfo_exists() ): try: - self.track_length_spinbox.config( - state=( - tk.DISABLED - if is_monitoring_active - else ("readonly" if enable_track else tk.DISABLED) - ) - ) - except: + # If monitoring is active, disable. Otherwise, enable if conditions met. + new_state = tk.DISABLED if is_monitoring_active else ("readonly" if enable_track_spinbox else tk.DISABLED) + self.track_length_spinbox.config(state=new_state) + except tk.TclError: pass + def _on_view_tab_change(self, event: Optional[tk.Event] = None): if not (hasattr(self, "views_notebook") and self.views_notebook.winfo_exists()): return @@ -1241,27 +1243,31 @@ class MainWindow: module_logger.info( f"GUI: Switched view tab to: {self.views_notebook.tab(self.views_notebook.index('current'), 'text')}" ) - except: - pass + except tk.TclError: # Can happen if notebook is in a weird state + module_logger.warning("TclError on view tab change, notebook might be closing.", exc_info=False) + except Exception as e_view_tab: + module_logger.error(f"Error getting view tab text: {e_view_tab}", exc_info=True) + def _set_bbox_entries_state(self, state: str): - for name in [ + for entry_name_attr in [ "lat_min_entry", "lon_min_entry", "lat_max_entry", "lon_max_entry", ]: - entry = getattr(self, name, None) - if entry and hasattr(entry, "winfo_exists") and entry.winfo_exists(): + entry_widget = getattr(self, entry_name_attr, None) + if entry_widget and hasattr(entry_widget, "winfo_exists") and entry_widget.winfo_exists(): try: - entry.config(state=state) - except: + entry_widget.config(state=state) + except tk.TclError: # Can happen if widget is destroyed pass def _start_monitoring(self): if not hasattr(self, "mode_var"): - self.show_error_message("Internal Error", "App mode N/A.") + self.show_error_message("Internal Error", "Application mode not available.") return + selected_mode = self.mode_var.get() active_func_tab_text = "" if hasattr(self, "function_notebook") and self.function_notebook.winfo_exists(): @@ -1269,97 +1275,129 @@ class MainWindow: active_func_tab_text = self.function_notebook.tab( self.function_notebook.index("current"), "text" ) - except: - pass - for btn_name, new_state in [ + except tk.TclError: + module_logger.warning("Could not get active function tab text on start.", exc_info=False) + + + # Disable start button, enable stop button, disable mode radios + for widget_attr_name, new_state in [ ("start_button", tk.DISABLED), ("stop_button", tk.NORMAL), ("live_radio", tk.DISABLED), ("history_radio", tk.DISABLED), ]: - btn = getattr(self, btn_name, None) - if btn and hasattr(btn, "winfo_exists") and btn.winfo_exists(): - btn.config(state=new_state) - self._update_controls_state_based_on_mode_and_tab() + widget = getattr(self, widget_attr_name, None) + if widget and hasattr(widget, "winfo_exists") and widget.winfo_exists(): + widget.config(state=new_state) + + self._update_controls_state_based_on_mode_and_tab() # This will disable BBox/Track if needed + if not self.controller: self._reset_gui_to_stopped_state("Critical Error: Controller unavailable.") - self.show_error_message("Internal Error", "App controller missing.") + self.show_error_message("Internal Error", "Application controller missing.") return + if selected_mode == "Live" and "Live: Area Monitor" in active_func_tab_text: bbox = self.get_bounding_box_from_gui() if bbox: + module_logger.info(f"GUI: Starting Live Area monitoring with BBox: {bbox}") self.controller.start_live_monitoring(bbox) + self.update_semaphore_and_status(GUI_STATUS_FETCHING, "Live monitoring starting...") else: - self._reset_gui_to_stopped_state("Start failed: Invalid BBox.") + self._reset_gui_to_stopped_state("Start failed: Invalid Bounding Box.") + self.show_error_message("Input Error", "Bounding Box values are invalid or incomplete.") elif selected_mode == "History" and "History" in active_func_tab_text: - self.controller.start_history_monitoring() + module_logger.info("GUI: Starting History monitoring.") + self.controller.start_history_monitoring() # Placeholder + self.update_semaphore_and_status(GUI_STATUS_OK, "History mode (placeholder).") # Placeholder status else: - self._reset_gui_to_stopped_state( - f"Start not supported on '{active_func_tab_text}'." - ) + err_msg = f"Start monitoring not supported on tab '{active_func_tab_text}' for mode '{selected_mode}'." + module_logger.warning(err_msg) + self._reset_gui_to_stopped_state(f"Start failed: {err_msg}") + def _stop_monitoring(self): selected_mode = self.mode_var.get() if hasattr(self, "mode_var") else "Unknown" + module_logger.info(f"GUI: Stop monitoring requested for mode: {selected_mode}") + if not self.controller: - self._reset_gui_to_stopped_state("Error: Controller missing.") + self._reset_gui_to_stopped_state("Error: Controller missing on stop.") return + if selected_mode == "Live": self.controller.stop_live_monitoring() elif selected_mode == "History": - self.controller.stop_history_monitoring() + self.controller.stop_history_monitoring() # Placeholder + + # GUI will be fully reset by _reset_gui_to_stopped_state called by controller + # but we can set an intermediate status here. if hasattr(self, "root") and self.root.winfo_exists(): self.update_semaphore_and_status( GUI_STATUS_FETCHING, f"{selected_mode} monitoring stopping..." ) + def get_bounding_box_from_gui(self) -> Optional[Dict[str, float]]: - # ... (come prima) - req_vars_names = ["lat_min_var", "lon_min_var", "lat_max_var", "lon_max_var"] - if not all(hasattr(self, v_name) for v_name in req_vars_names): + required_var_names = ["lat_min_var", "lon_min_var", "lat_max_var", "lon_max_var"] + if not all(hasattr(self, var_name) for var_name in required_var_names): + module_logger.error("BBox string variables not found in GUI.") return None try: - vals_str = [getattr(self, v_name).get() for v_name in req_vars_names] - if not all(s.strip() for s in vals_str): + # Get all values first + str_values = [getattr(self, var_name).get() for var_name in required_var_names] + + # Check if any are empty before attempting conversion + if not all(s.strip() for s in str_values): # Checks for empty or whitespace-only strings + module_logger.debug("One or more BBox GUI fields are empty.") return None - lat_min, lon_min, lat_max, lon_max = map(float, vals_str) - except: - return None - bbox_dict = { - "lat_min": lat_min, - "lon_min": lon_min, - "lat_max": lat_max, - "lon_max": lon_max, + + lat_min_val, lon_min_val, lat_max_val, lon_max_val = map(float, str_values) + except ValueError: + module_logger.warning("Invalid numeric format in BBox GUI fields.") + return None # ValueError if conversion fails + + bbox_candidate = { + "lat_min": lat_min_val, + "lon_min": lon_min_val, + "lat_max": lat_max_val, + "lon_max": lon_max_val, } - return bbox_dict if _is_valid_bbox_dict(bbox_dict) else None + if _is_valid_bbox_dict(bbox_candidate): + return bbox_candidate + else: + module_logger.warning(f"BBox from GUI failed validation: {bbox_candidate}") + return None def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]): - # ... (come prima) - if not hasattr(self, "lat_min_var"): + if not hasattr(self, "lat_min_var"): # Check if vars are initialized return + if bbox_dict and _is_valid_bbox_dict(bbox_dict): - decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5) + decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5) # Use a config for decimals try: 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}") - except: + except tk.TclError: # Can happen if widget is destroyed pass - else: + except Exception as e_set_bbox: + module_logger.error(f"Error setting BBox GUI fields: {e_set_bbox}") + else: # Clear fields if bbox_dict is invalid or None try: - self.lat_min_var.set("N/A") - self.lon_min_var.set("N/A") - self.lat_max_var.set("N/A") - self.lon_max_var.set("N/A") - except: + self.lat_min_var.set("") + self.lon_min_var.set("") + self.lat_max_var.set("") + self.lon_max_var.set("") + except tk.TclError: pass + def display_flights_on_canvas( self, flight_states: List[CanonicalFlightState], - _active_bbox_context: Optional[Dict[str, float]], + _active_bbox_context: Optional[Dict[str, float]], # bbox_context might be useful later ): - # ... (come prima) if not ( hasattr(self, "map_manager_instance") and self.map_manager_instance @@ -1368,17 +1406,26 @@ class MainWindow: if hasattr(self, "root") and self.root.winfo_exists(): self._update_map_placeholder("Map N/A to display flights.") return + try: self.map_manager_instance.update_flights_on_map(flight_states) - if not flight_states and self._should_show_main_placeholder(): + # Update placeholder only if map manager is supposed to be active but no flights are shown + # (and it's not already showing an error/other message) + if not flight_states and not self._should_show_main_placeholder(): # Map manager IS active + # Let MapCanvasManager handle its own placeholder for "no flights" + pass + elif not flight_states and self._should_show_main_placeholder(): # Map manager NOT active self._update_map_placeholder("No flights in the selected area.") + except Exception as e: + module_logger.error(f"Error in display_flights_on_canvas: {e}", exc_info=True) self.show_error_message( "Map Display Error", "Could not update flights on map." ) + def clear_all_views_data(self): - # ... (come prima) + module_logger.info("GUI: Clearing all views data.") if ( hasattr(self, "map_manager_instance") and self.map_manager_instance @@ -1388,37 +1435,52 @@ class MainWindow: self.map_manager_instance.clear_map_display() except Exception as e: module_logger.warning(f"Error clearing map via map manager: {e}") - elif self._should_show_main_placeholder(): + elif self._should_show_main_placeholder(): # No map manager, ensure placeholder is appropriate mode = self.mode_var.get() if hasattr(self, "mode_var") else "Unknown" text = f"Map - {mode}. Data cleared." - if mode == "Live": - text = "Map - Live. Define area and Start." - elif mode == "History": - text = "Map - History. (TBD)" + if mode == "Live": text = "Map - Live. Define area and Start." + elif mode == "History": text = "Map - History. (Coming Soon)" self._update_map_placeholder(text) + + + # Clear selected flight details panel if hasattr(self, "update_selected_flight_details"): self.update_selected_flight_details(None) + # Future: Clear table view if it's implemented and holds data + # if hasattr(self, "flight_table_view") and self.flight_table_view: + # self.flight_table_view.clear_table() + + def show_error_message(self, title: str, message: str): - # ... (come prima) status_msg = f"Error: {message[:70]}{'...' if len(message)>70 else ''}" if hasattr(self, "root") and self.root.winfo_exists(): self.update_semaphore_and_status(GUI_STATUS_ERROR, status_msg) try: messagebox.showerror(title, message, parent=self.root) + except tk.TclError: # Can happen if root is being destroyed + pass + else: # Fallback if GUI not fully up or during shutdown + print(f"ERROR (No GUI messagebox): {title} - {message}", flush=True) + + def show_info_message(self, title: str, message: str): # Added for convenience + if hasattr(self, "root") and self.root.winfo_exists(): + try: + messagebox.showinfo(title, message, parent=self.root) except tk.TclError: pass else: - print(f"ERROR (No GUI): {title} - {message}", flush=True) + print(f"INFO (No GUI messagebox): {title} - {message}", flush=True) + def show_map_context_menu( self, latitude: float, longitude: float, screen_x: int, screen_y: int ): - # ... (come prima) if ( hasattr(self, "map_manager_instance") and self.map_manager_instance and hasattr(self.map_manager_instance, "show_map_context_menu_from_gui") + and MAP_CANVAS_MANAGER_AVAILABLE ): try: self.map_manager_instance.show_map_context_menu_from_gui( @@ -1426,19 +1488,22 @@ class MainWindow: ) except Exception as e: module_logger.error( - f"Error delegating context menu: {e}", exc_info=True + f"Error delegating context menu to MapCanvasManager: {e}", exc_info=True ) + else: + module_logger.warning("Map manager or context menu method not available for GUI.") + def update_clicked_map_info( self, lat_deg: Optional[float], lon_deg: Optional[float], - lat_dms: str, - lon_dms: str, + lat_dms: str, # Already formatted string + lon_dms: str, # Already formatted string ): - # ... (come prima) - if not hasattr(self, "info_lat_value"): + if not hasattr(self, "info_lat_value"): # Check if info panel widgets are initialized return + decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5) try: if hasattr(self, "info_lat_value") and self.info_lat_value.winfo_exists(): @@ -1459,150 +1524,165 @@ class MainWindow: and self.info_lon_dms_value.winfo_exists() ): self.info_lon_dms_value.config(text=lon_dms or "N/A") - except: + except tk.TclError: # Widgets might be destroyed pass + except Exception as e_update_click: + module_logger.error(f"Error updating clicked map info panel: {e_update_click}", exc_info=False) + def update_general_map_info_display( self, zoom: Optional[int], - map_size_str: str, - map_geo_bounds: Optional[Tuple[float, float, float, float]], - target_bbox_input: Optional[Dict[str, float]], + map_size_str: str, # Already formatted (e.g., "100km x 80km") + map_geo_bounds: Optional[Tuple[float, float, float, float]], # W, S, E, N + target_bbox_input: Optional[Dict[str, float]], # Standard dict flight_count: Optional[int], ): - # ... (come prima) - if not hasattr(self, "info_zoom_value"): + if not hasattr(self, "info_zoom_value"): # Check if info panel widgets are initialized return + decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5) try: - mw, ms, me, mn = ("N/A",) * 4 + # Map geographic bounds + map_w, map_s, map_e, map_n = ("N/A",) * 4 if map_geo_bounds: - mw, ms, me, mn = (f"{c:.{decimals}f}" for c in map_geo_bounds) - for name, val in [ - ("info_map_bounds_w", mw), - ("info_map_bounds_s", ms), - ("info_map_bounds_e", me), - ("info_map_bounds_n", mn), + map_w_val, map_s_val, map_e_val, map_n_val = map_geo_bounds + map_w, map_s, map_e, map_n = ( + f"{map_w_val:.{decimals}f}", f"{map_s_val:.{decimals}f}", + f"{map_e_val:.{decimals}f}", f"{map_n_val:.{decimals}f}" + ) + + for attr_name, value_str in [ + ("info_map_bounds_w", map_w), ("info_map_bounds_s", map_s), + ("info_map_bounds_e", map_e), ("info_map_bounds_n", map_n), ]: - lbl = getattr(self, name, None) - if lbl and lbl.winfo_exists(): - lbl.config(text=val) - tw, ts, te, tn = ("N/A",) * 4 - color_target = BBOX_COLOR_NA + label_widget = getattr(self, attr_name, None) + if label_widget and label_widget.winfo_exists(): label_widget.config(text=value_str) + + # Target BBox input (monitoring area) + target_w, target_s, target_e, target_n = ("N/A",) * 4 + color_for_target_bbox = BBOX_COLOR_NA # Default color if target_bbox_input and _is_valid_bbox_dict(target_bbox_input): - tw, ts, te, tn = ( - f"{target_bbox_input['lon_min']:.{decimals}f}", - f"{target_bbox_input['lat_min']:.{decimals}f}", - f"{target_bbox_input['lon_max']:.{decimals}f}", - f"{target_bbox_input['lat_max']:.{decimals}f}", + target_w, target_s, target_e, target_n = ( + f"{target_bbox_input['lon_min']:.{decimals}f}", f"{target_bbox_input['lat_min']:.{decimals}f}", + f"{target_bbox_input['lon_max']:.{decimals}f}", f"{target_bbox_input['lat_max']:.{decimals}f}" ) - status_bbox = ( - self._is_bbox_inside_bbox(target_bbox_input, map_geo_bounds) - if map_geo_bounds - else "Outside" - ) - if status_bbox == "Inside": - color_target = BBOX_COLOR_INSIDE - elif status_bbox == "Partial": - color_target = BBOX_COLOR_PARTIAL - else: - color_target = BBOX_COLOR_OUTSIDE - for name, val in [ - ("info_target_bbox_w", tw), - ("info_target_bbox_s", ts), - ("info_target_bbox_e", te), - ("info_target_bbox_n", tn), + # Determine color based on how target BBox relates to current map view + if map_geo_bounds: + relation_status = self._is_bbox_inside_bbox(target_bbox_input, map_geo_bounds) + if relation_status == "Inside": color_for_target_bbox = BBOX_COLOR_INSIDE + elif relation_status == "Partial": color_for_target_bbox = BBOX_COLOR_PARTIAL + else: color_for_target_bbox = BBOX_COLOR_OUTSIDE + else: # No map view bounds, can't determine relation + color_for_target_bbox = BBOX_COLOR_NA + + for attr_name, value_str in [ + ("info_target_bbox_w", target_w), ("info_target_bbox_s", target_s), + ("info_target_bbox_e", target_e), ("info_target_bbox_n", target_n), ]: - lbl = getattr(self, name, None) - if lbl and lbl.winfo_exists(): - lbl.config(text=val, foreground=color_target) - for name, val_direct in [ - ("info_zoom_value", str(zoom) if zoom is not None else "N/A"), - ("info_map_size_value", map_size_str or "N/A"), - ( - "info_flight_count_value", - str(flight_count) if flight_count is not None else "N/A", - ), - ]: - lbl = getattr(self, name, None) - if lbl and lbl.winfo_exists(): - lbl.config(text=val_direct) - except: - pass # Evita crash se i widget non ci sono + label_widget = getattr(self, attr_name, None) + if label_widget and label_widget.winfo_exists(): + label_widget.config(text=value_str, foreground=color_for_target_bbox) + + # Other general info + if hasattr(self, "info_zoom_value") and self.info_zoom_value.winfo_exists(): + self.info_zoom_value.config(text=str(zoom) if zoom is not None else "N/A") + if hasattr(self, "info_map_size_value") and self.info_map_size_value.winfo_exists(): + self.info_map_size_value.config(text=map_size_str or "N/A") + if hasattr(self, "info_flight_count_value") and self.info_flight_count_value.winfo_exists(): + self.info_flight_count_value.config(text=str(flight_count) if flight_count is not None else "N/A") + + except tk.TclError: # Widgets might be destroyed + pass + except Exception as e_update_gen_info: + module_logger.error(f"Error updating general map info panel: {e_update_gen_info}", exc_info=False) def _is_bbox_inside_bbox( self, - inner_bbox_dict: Dict[str, float], - outer_bbox_tuple: Tuple[float, float, float, float], - ) -> str: - # ... (come prima) - if not _is_valid_bbox_dict(inner_bbox_dict) or not ( - outer_bbox_tuple - and len(outer_bbox_tuple) == 4 - and all(isinstance(c, (int, float)) for c in outer_bbox_tuple) - ): + inner_bbox_dict: Dict[str, float], # Standard dict {lat_min, lon_min, ...} + outer_bbox_tuple: Tuple[float, float, float, float], # W, S, E, N + ) -> str: # Returns "Inside", "Outside", "Partial", "N/A" + if not _is_valid_bbox_dict(inner_bbox_dict) or \ + not (outer_bbox_tuple and len(outer_bbox_tuple) == 4 and all(isinstance(c, (int, float)) for c in outer_bbox_tuple)): return "N/A" - outer = { - "lon_min": outer_bbox_tuple[0], - "lat_min": outer_bbox_tuple[1], - "lon_max": outer_bbox_tuple[2], - "lat_max": outer_bbox_tuple[3], - } - eps = 1e-6 - if ( - inner_bbox_dict["lon_min"] >= outer["lon_min"] - eps - and inner_bbox_dict["lat_min"] >= outer["lat_min"] - eps - and inner_bbox_dict["lon_max"] <= outer["lon_max"] + eps - and inner_bbox_dict["lat_max"] <= outer["lat_max"] + eps - ): - return "Inside" - if ( - inner_bbox_dict["lon_max"] <= outer["lon_min"] + eps - or inner_bbox_dict["lon_min"] >= outer["lon_max"] - eps - or inner_bbox_dict["lat_max"] <= outer["lat_min"] + eps - or inner_bbox_dict["lat_min"] >= outer["lat_max"] - eps - ): - return "Outside" - return "Partial" - def _map_zoom_in(self): # ... (come prima) + # Convert outer tuple to a comparable dict for clarity + outer_dict = { + "lon_min": outer_bbox_tuple[0], "lat_min": outer_bbox_tuple[1], + "lon_max": outer_bbox_tuple[2], "lat_max": outer_bbox_tuple[3] + } + eps = 1e-6 # Epsilon for float comparisons + + # Check for full containment + if (inner_bbox_dict["lon_min"] >= outer_dict["lon_min"] - eps and + inner_bbox_dict["lat_min"] >= outer_dict["lat_min"] - eps and + inner_bbox_dict["lon_max"] <= outer_dict["lon_max"] + eps and + inner_bbox_dict["lat_max"] <= outer_dict["lat_max"] + eps): + return "Inside" + + # Check for no overlap (completely outside) + # Inner is to the right of outer OR inner is to the left of outer OR ... + if (inner_bbox_dict["lon_min"] >= outer_dict["lon_max"] - eps or # Inner right of outer + inner_bbox_dict["lon_max"] <= outer_dict["lon_min"] + eps or # Inner left of outer + inner_bbox_dict["lat_min"] >= outer_dict["lat_max"] - eps or # Inner above outer + inner_bbox_dict["lat_max"] <= outer_dict["lat_min"] + eps): # Inner below outer + return "Outside" + + return "Partial" # If not fully inside and not fully outside, it must be partial + + + def _map_zoom_in(self): if self.controller and hasattr(self.controller, "map_zoom_in"): self.controller.map_zoom_in() - def _map_zoom_out(self): # ... (come prima) + def _map_zoom_out(self): if self.controller and hasattr(self.controller, "map_zoom_out"): self.controller.map_zoom_out() - def _map_pan(self, direction: str): # ... (come prima) + def _map_pan(self, direction: str): if self.controller and hasattr(self.controller, "map_pan_direction"): self.controller.map_pan_direction(direction) - def _map_center_and_fit(self): # ... (come prima) + def _map_center_and_fit(self): try: - lat, lon, patch = ( - float(self.center_lat_var.get()), - float(self.center_lon_var.get()), - float(self.center_patch_size_var.get()), - ) - if not (-90 <= lat <= 90 and -180 <= lon <= 180 and patch > 0): - self.show_error_message("Input Error", "Invalid lat/lon/patch.") + lat_str, lon_str, patch_str = self.center_lat_var.get(), self.center_lon_var.get(), self.center_patch_size_var.get() + if not lat_str or not lon_str or not patch_str: # Check for empty strings + self.show_error_message("Input Error", "Latitude, Longitude, and Patch size are required.") return + + lat = float(lat_str) + lon = float(lon_str) + patch = float(patch_str) + + if not (-90 <= lat <= 90 and -180 <= lon <= 180 and patch > 0): + self.show_error_message("Input Error", "Invalid latitude (-90 to 90), longitude (-180 to 180), or patch size (>0).") + return + if self.controller and hasattr( self.controller, "map_center_on_coords_and_fit_patch" ): self.controller.map_center_on_coords_and_fit_patch(lat, lon, patch) except ValueError: - self.show_error_message("Input Error", "Lat/Lon/Patch must be numbers.") + self.show_error_message("Input Error", "Latitude, Longitude, and Patch size must be valid numbers.") except Exception as e: - self.show_error_message("Error", f"Unexpected error: {e}") + module_logger.error(f"Error in _map_center_and_fit: {e}", exc_info=True) + self.show_error_message("Error", f"An unexpected error occurred: {e}") - def _on_track_length_change(self): # ... (come prima) + + def _on_track_length_change(self): if ( hasattr(self, "track_length_var") and self.controller and hasattr(self.controller, "set_map_track_length") ): try: - self.controller.set_map_track_length(self.track_length_var.get()) - except: - pass + new_length = self.track_length_var.get() + if isinstance(new_length, int) and new_length >=2: # Basic validation + self.controller.set_map_track_length(new_length) + module_logger.info(f"GUI: Track length changed to {new_length}") + else: + module_logger.warning(f"Invalid track length from spinbox: {new_length}") + except tk.TclError: # Spinbox might not be fully ready or being destroyed + module_logger.warning("TclError on track length change, spinbox might not be ready.") + except Exception as e_track_len: + module_logger.error(f"Error processing track length change: {e_track_len}", exc_info=True) \ No newline at end of file diff --git a/flightmonitor/utils/logger.py b/flightmonitor/utils/logger.py index 62d6189..24f471b 100644 --- a/flightmonitor/utils/logger.py +++ b/flightmonitor/utils/logger.py @@ -3,7 +3,7 @@ import logging import logging.handlers # For RotatingFileHandler import tkinter as tk from tkinter.scrolledtext import ScrolledText -from queue import Queue, Empty as QueueEmpty # Renamed for clarity +from queue import Queue, Empty as QueueEmpty # Renamed for clarity from typing import Optional, Dict, Any # --- Module-level globals for the new centralized logging queue system --- @@ -94,10 +94,10 @@ class TkinterTextHandler(logging.Handler): # This emit is called by _process_global_log_queue (already in GUI thread) if not self._is_active or not self.text_widget.winfo_exists(): # This case should ideally not happen if setup is correct - print( - f"DEBUG: TkinterTextHandler.emit called but inactive or widget gone. Record: {record.getMessage()}", - flush=True, - ) + # print( + # f"DEBUG: TkinterTextHandler.emit called but inactive or widget gone. Record: {record.getMessage()}", + # flush=True, + # ) return try: msg = self.format( @@ -169,7 +169,7 @@ class TkinterTextHandler(logging.Handler): self._internal_after_id = None def close(self): - print("INFO: TkinterTextHandler close called.", flush=True) + # print("INFO: TkinterTextHandler close called.", flush=True) # Reduced verbosity self._is_active = False if ( self._internal_after_id @@ -203,11 +203,8 @@ class QueuePuttingHandler(logging.Handler): def emit(self, record: logging.LogRecord): try: - # We can choose to make a copy of the record if there's any concern - # about it being modified before processing, though typically not an issue. - # For now, put the original record. self.handler_queue.put_nowait(record) - except queue.Full: + except queue.Full: # Standard library 'queue.Full' print( f"CRITICAL: Global log queue is full! Log record for '{record.name}' might be lost.", flush=True, @@ -224,7 +221,7 @@ def _process_global_log_queue(): and dispatches them to the actual configured handlers. """ global _logging_system_active, _log_processor_after_id - # print(f"DEBUG: _process_global_log_queue called. Active: {_logging_system_active}", flush=True) # Can be very verbose + # print(f"DEBUG: _process_global_log_queue called. Active: {_logging_system_active}", flush=True) if not _logging_system_active: if ( @@ -243,8 +240,7 @@ def _process_global_log_queue(): _tk_root_instance_for_processing and _tk_root_instance_for_processing.winfo_exists() ): - # print("DEBUG: Global log queue processor: Tk root instance gone. Stopping.", flush=True) - _logging_system_active = False # Stop if root is gone + _logging_system_active = False _log_processor_after_id = None return @@ -252,12 +248,11 @@ def _process_global_log_queue(): try: while _global_log_queue and not _global_log_queue.empty(): if not _logging_system_active: - break # Check before processing each item + break try: record = _global_log_queue.get_nowait() processed_count += 1 - # Dispatch to actual handlers if _actual_console_handler: try: _actual_console_handler.handle(record) @@ -278,13 +273,12 @@ def _process_global_log_queue(): _global_log_queue.task_done() except QueueEmpty: - break # Should not happen due to outer loop condition, but defensive + break except Exception as e_proc_item: print( f"Error processing a log item from global queue: {e_proc_item}", flush=True, ) - # Potentially re-queue or log to a failsafe if critical except Exception as e_outer_loop: print( f"Critical error in _process_global_log_queue outer loop: {e_outer_loop}", @@ -295,107 +289,82 @@ def _process_global_log_queue(): _log_processor_after_id = _tk_root_instance_for_processing.after( GLOBAL_LOG_QUEUE_POLL_INTERVAL_MS, _process_global_log_queue ) - # else: print(f"DEBUG: Not rescheduling _process_global_log_queue. Active: {_logging_system_active}", flush=True) -def setup_logging( - gui_log_widget: Optional[tk.Text] = None, - root_tk_instance: Optional[tk.Tk] = None, +def setup_basic_logging( # MODIFIED: Renamed from setup_logging + root_tk_instance_for_processor: Optional[tk.Tk], # MODIFIED: Now mandatory for processor logging_config_dict: Optional[Dict[str, Any]] = None, ): global _global_log_queue, _actual_console_handler, _actual_file_handler - global _actual_tkinter_handler, _logging_system_active, _tk_root_instance_for_processing - global _log_processor_after_id, _base_formatter - - print("INFO: Configuring centralized queued logging system...", flush=True) + global _logging_system_active, _tk_root_instance_for_processing + global _log_processor_after_id, _base_formatter, _actual_tkinter_handler # _actual_tkinter_handler should be None here + # This function should only be called ONCE. if _logging_system_active: print( - "INFO: Logging system already active. Shutting down existing to reconfigure.", + "WARNING: setup_basic_logging called but logging system is already active. Ignoring call.", flush=True, ) - shutdown_logging_system() # Ensure clean state before re-setup + return + + print("INFO: Configuring basic centralized queued logging system...", flush=True) + _actual_tkinter_handler = None # Ensure it's None at basic setup if logging_config_dict is None: print( "Warning: No logging_config_dict. Using basic console-only defaults for queued logging.", flush=True, ) + # Simplified default, as Tkinter parts are handled by add_tkinter_handler logging_config_dict = { "default_root_level": logging.INFO, "specific_levels": {}, "format": "%(asctime)s [%(levelname)-8s] %(name)-25s : %(message)s", "date_format": "%Y-%m-%d %H:%M:%S", - "colors": { - logging.INFO: "black", - logging.DEBUG: "blue", - logging.WARNING: "orange", - logging.ERROR: "red", - logging.CRITICAL: "purple", - }, - "queue_poll_interval_ms": 100, # For TkinterTextHandler's internal queue "enable_console": True, "enable_file": False, } - # --- Extract Config --- root_log_level = logging_config_dict.get("default_root_level", logging.INFO) specific_levels = logging_config_dict.get("specific_levels", {}) log_format_str = logging_config_dict.get( "format", "%(asctime)s [%(levelname)-8s] %(name)-25s : %(message)s" ) log_date_format_str = logging_config_dict.get("date_format", "%Y-%m-%d %H:%M:%S") - level_colors = logging_config_dict.get("colors", {}) - tkinter_handler_poll_ms = logging_config_dict.get( - "queue_poll_interval_ms", 100 - ) # For TkinterTextHandler's internal queue enable_console = logging_config_dict.get("enable_console", True) enable_file = logging_config_dict.get("enable_file", False) _base_formatter = logging.Formatter(log_format_str, datefmt=log_date_format_str) - _global_log_queue = Queue() # Initialize the global queue for LogRecords - _tk_root_instance_for_processing = root_tk_instance # Store for the after() loop + _global_log_queue = Queue() + _tk_root_instance_for_processing = root_tk_instance_for_processor - # --- Configure Root Logger --- root_logger = logging.getLogger() - # Remove ALL existing handlers from the root logger to ensure clean state - for handler in root_logger.handlers[:]: + for handler in root_logger.handlers[:]: # Clear any pre-existing handlers (e.g. from basicConfig) try: - handler.close() # Close handler first + handler.close() root_logger.removeHandler(handler) - print( - f"DEBUG: Removed and closed old handler from root: {handler}", - flush=True, - ) - except Exception as e_rem: - print(f"Error removing/closing old handler {handler}: {e_rem}", flush=True) - root_logger.handlers = [] # Ensure it's empty - + except Exception: pass + root_logger.handlers = [] root_logger.setLevel(root_log_level) print( f"INFO: Root logger level set to {logging.getLevelName(root_logger.level)}.", flush=True, ) - # --- Configure Specific Logger Levels --- for logger_name, level in specific_levels.items(): named_logger = logging.getLogger(logger_name) named_logger.setLevel(level) - # Ensure propagation is on if they are not root, or if they should also go to root's QueuePuttingHandler named_logger.propagate = True print( f"INFO: Logger '{logger_name}' level set to {logging.getLevelName(named_logger.level)}.", flush=True, ) - # --- Create Actual Handlers (but don't add to root logger yet) --- if enable_console: try: - _actual_console_handler = logging.StreamHandler() # Defaults to stderr + _actual_console_handler = logging.StreamHandler() _actual_console_handler.setFormatter(_base_formatter) - _actual_console_handler.setLevel( - logging.DEBUG - ) # Let console handler decide based on record level + _actual_console_handler.setLevel(logging.DEBUG) print("INFO: Actual ConsoleHandler created.", flush=True) except Exception as e: print(f"ERROR: Failed to create actual ConsoleHandler: {e}", flush=True) @@ -404,9 +373,7 @@ def setup_logging( if enable_file: try: file_path = logging_config_dict.get("file_path", "flight_monitor_app.log") - file_max_bytes = logging_config_dict.get( - "file_max_bytes", 5 * 1024 * 1024 - ) # 5MB + file_max_bytes = logging_config_dict.get("file_max_bytes", 5 * 1024 * 1024) file_backup_count = logging_config_dict.get("file_backup_count", 3) _actual_file_handler = logging.handlers.RotatingFileHandler( file_path, @@ -415,125 +382,103 @@ def setup_logging( encoding="utf-8", ) _actual_file_handler.setFormatter(_base_formatter) - _actual_file_handler.setLevel(logging.DEBUG) # Let file handler decide - print( - f"INFO: Actual RotatingFileHandler created for '{file_path}'.", - flush=True, - ) + _actual_file_handler.setLevel(logging.DEBUG) + print(f"INFO: Actual RotatingFileHandler created for '{file_path}'.", flush=True) except Exception as e: - print( - f"ERROR: Failed to create actual FileHandler for '{file_path}': {e}", - flush=True, - ) + print(f"ERROR: Failed to create actual FileHandler for '{file_path}': {e}", flush=True) _actual_file_handler = None - if gui_log_widget and root_tk_instance: - is_widget_valid = ( - isinstance(gui_log_widget, (tk.Text, ScrolledText)) - and hasattr(gui_log_widget, "winfo_exists") - and gui_log_widget.winfo_exists() - ) - if is_widget_valid: - try: - _actual_tkinter_handler = TkinterTextHandler( - text_widget=gui_log_widget, - level_colors=level_colors, - root_tk_instance_for_widget_update=root_tk_instance, # Pass root for its own after loop - internal_poll_interval_ms=tkinter_handler_poll_ms, - ) - _actual_tkinter_handler.setFormatter(_base_formatter) - _actual_tkinter_handler.setLevel( - logging.DEBUG - ) # Let Tkinter handler decide - print("INFO: Actual TkinterTextHandler created.", flush=True) - except Exception as e: - print( - f"ERROR: Failed to create actual TkinterTextHandler: {e}", - flush=True, - ) - _actual_tkinter_handler = None - else: - print( - "ERROR: GUI log widget invalid or non-existent, cannot create TkinterTextHandler.", - flush=True, - ) - elif gui_log_widget and not root_tk_instance: - print( - "WARNING: GUI log widget provided, but root_tk_instance missing for TkinterTextHandler.", - flush=True, - ) - - # --- Add ONLY the QueuePuttingHandler to the root logger --- if _global_log_queue is not None: queue_putter = QueuePuttingHandler(handler_queue=_global_log_queue) - queue_putter.setLevel( - logging.DEBUG - ) # Capture all messages from root level downwards + queue_putter.setLevel(logging.DEBUG) root_logger.addHandler(queue_putter) - print( - f"INFO: QueuePuttingHandler added to root logger. All logs will go to global queue.", - flush=True, - ) + print(f"INFO: QueuePuttingHandler added to root logger.", flush=True) else: - print( - "CRITICAL ERROR: Global log queue not initialized. Logging will not function correctly.", - flush=True, - ) - # Fallback to basic console if queue setup failed badly - if not _actual_console_handler: # If even this failed - _bf = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - ) - _ch = logging.StreamHandler() - _ch.setFormatter(_bf) - root_logger.addHandler(_ch) - print("CRITICAL: Added basic emergency console logger.", flush=True) + print("CRITICAL ERROR: Global log queue not initialized in basic setup!", flush=True) - # --- Start the global log queue processor (if root_tk_instance is available) --- _logging_system_active = True if ( _tk_root_instance_for_processing and _tk_root_instance_for_processing.winfo_exists() ): - if ( - _log_processor_after_id - ): # Cancel previous if any (shouldn't happen if shutdown was called) - try: - _tk_root_instance_for_processing.after_cancel(_log_processor_after_id) - except Exception: - pass + if _log_processor_after_id: # Should be None here, but defensive + try: _tk_root_instance_for_processing.after_cancel(_log_processor_after_id) + except Exception: pass _log_processor_after_id = _tk_root_instance_for_processing.after( GLOBAL_LOG_QUEUE_POLL_INTERVAL_MS, _process_global_log_queue ) - print( - f"INFO: Global log queue processor scheduled with Tkinter.after (ID: {_log_processor_after_id}).", - flush=True, - ) - elif root_tk_instance is None and ( - gui_log_widget is not None or enable_file or enable_console - ): - # This case means we want logging but have no Tkinter root to schedule the queue processor. - # This is problematic for GUI logging. For console/file, logs will just queue up. - # A dedicated thread could process the queue if no Tkinter available, but adds complexity. - print( - "WARNING: No Tkinter root instance provided to setup_logging. " - "Logs will be queued but not processed by GUI/File/Console handlers unless _process_global_log_queue is run manually or by another mechanism.", - flush=True, - ) - elif not (enable_console or enable_file or gui_log_widget): - print( - "INFO: No log output handlers (console, file, gui) are enabled. Logs will be queued but not displayed.", - flush=True, - ) + print(f"INFO: Global log queue processor scheduled (ID: {_log_processor_after_id}).", flush=True) + elif not _tk_root_instance_for_processing: + print("WARNING: No Tkinter root instance for processor. Logs will queue but not be dispatched by this setup function alone.", flush=True) - print("INFO: Centralized queued logging system setup complete.", flush=True) - # Test log - test_logger = logging.getLogger("FlightMonitor.LoggerTest") - test_logger.info( - "Logging system initialized. This is a test message from setup_logging." + print("INFO: Basic centralized queued logging system setup complete.", flush=True) + logging.getLogger("FlightMonitor.LoggerTest").info( + "Basic logging initialized. This is a test from setup_basic_logging." ) +def add_tkinter_handler( # NEW FUNCTION + gui_log_widget: tk.Text, + root_tk_instance_for_gui_handler: tk.Tk, + logging_config_dict: Dict[str, Any], +): + global _actual_tkinter_handler, _base_formatter + + if not _logging_system_active: + print("ERROR: Cannot add Tkinter handler, basic logging system not active.", flush=True) + return + if not _base_formatter: + print("ERROR: Cannot add Tkinter handler, base formatter not set.", flush=True) + return + + print("INFO: Attempting to add TkinterTextHandler...", flush=True) + + if _actual_tkinter_handler: # If one already exists, close and prepare to replace + print("INFO: Existing TkinterTextHandler found, closing it before adding new one.", flush=True) + try: + _actual_tkinter_handler.close() + except Exception as e_close_old_tk: + print(f"Warning: Error closing old TkinterTextHandler: {e_close_old_tk}", flush=True) + _actual_tkinter_handler = None + + is_widget_valid = ( + isinstance(gui_log_widget, (tk.Text, ScrolledText)) + and hasattr(gui_log_widget, "winfo_exists") + and gui_log_widget.winfo_exists() + ) + is_root_valid = ( + root_tk_instance_for_gui_handler + and hasattr(root_tk_instance_for_gui_handler, "winfo_exists") + and root_tk_instance_for_gui_handler.winfo_exists() + ) + + if is_widget_valid and is_root_valid: + try: + level_colors = logging_config_dict.get("colors", {}) + tkinter_handler_poll_ms = logging_config_dict.get("queue_poll_interval_ms", 100) + + _actual_tkinter_handler = TkinterTextHandler( + text_widget=gui_log_widget, + level_colors=level_colors, + root_tk_instance_for_widget_update=root_tk_instance_for_gui_handler, + internal_poll_interval_ms=tkinter_handler_poll_ms, + ) + _actual_tkinter_handler.setFormatter(_base_formatter) + _actual_tkinter_handler.setLevel(logging.DEBUG) + print("INFO: TkinterTextHandler added and configured successfully.", flush=True) + logging.getLogger("FlightMonitor.LoggerTest").info( + "TkinterTextHandler added. This is a test from add_tkinter_handler." + ) + except Exception as e: + print(f"ERROR: Failed to create and add TkinterTextHandler: {e}", flush=True) + _actual_tkinter_handler = None + else: + if not is_widget_valid: + print("ERROR: GUI log widget invalid or non-existent, cannot add TkinterTextHandler.", flush=True) + if not is_root_valid: + print("ERROR: Root Tk instance for GUI handler invalid or non-existent.", flush=True) + + def get_logger(name: str) -> logging.Logger: """ Retrieves a logger instance by name. @@ -551,8 +496,12 @@ def shutdown_logging_system(): global _actual_console_handler, _actual_file_handler, _actual_tkinter_handler global _global_log_queue, _tk_root_instance_for_processing + if not _logging_system_active and not _global_log_queue: # Extra check if it was never really active + print("INFO: Logging system shutdown called, but system was not fully active or already shut down.", flush=True) + return + print("INFO: Initiating shutdown of centralized logging system...", flush=True) - _logging_system_active = False # Signal processor to stop + _logging_system_active = False if ( _log_processor_after_id @@ -561,89 +510,62 @@ def shutdown_logging_system(): ): try: _tk_root_instance_for_processing.after_cancel(_log_processor_after_id) - print("INFO: Cancelled global log queue processor task.", flush=True) + # print("INFO: Cancelled global log queue processor task.", flush=True) # Reduced verbosity except Exception as e_cancel: - print( - f"Warning: Error cancelling log processor task: {e_cancel}", flush=True - ) + print(f"Warning: Error cancelling log processor task: {e_cancel}", flush=True) _log_processor_after_id = None - # Attempt to process any remaining logs in the global queue - print( - "INFO: Processing any remaining logs in the global queue before full shutdown...", - flush=True, - ) + # print("INFO: Processing any remaining logs in the global queue before full shutdown...", flush=True) # Reduced if _global_log_queue: final_processed_count = 0 while not _global_log_queue.empty(): try: record = _global_log_queue.get_nowait() final_processed_count += 1 - if _actual_console_handler: - _actual_console_handler.handle(record) - if _actual_file_handler: - _actual_file_handler.handle(record) - if _actual_tkinter_handler: - _actual_tkinter_handler.handle(record) + if _actual_console_handler: _actual_console_handler.handle(record) + if _actual_file_handler: _actual_file_handler.handle(record) + if _actual_tkinter_handler: _actual_tkinter_handler.handle(record) _global_log_queue.task_done() - except QueueEmpty: - break + except QueueEmpty: break except Exception as e_final_proc: print(f"Error during final log processing: {e_final_proc}", flush=True) - break # Stop if error during final processing - if final_processed_count > 0: - print( - f"INFO: Processed {final_processed_count} remaining log messages.", - flush=True, - ) - else: - print( - "INFO: No remaining log messages in global queue to process.", - flush=True, - ) + break + # if final_processed_count > 0: print(f"INFO: Processed {final_processed_count} remaining log messages.", flush=True) + # else: print("INFO: No remaining log messages in global queue to process.", flush=True) + - # Close actual handlers if _actual_tkinter_handler: try: - print("INFO: Closing actual TkinterTextHandler.", flush=True) + # print("INFO: Closing actual TkinterTextHandler.", flush=True) # Reduced _actual_tkinter_handler.close() - except Exception as e: - print(f"Error closing TkinterTextHandler: {e}", flush=True) + except Exception as e: print(f"Error closing TkinterTextHandler: {e}", flush=True) _actual_tkinter_handler = None if _actual_console_handler: try: - print("INFO: Closing actual ConsoleHandler.", flush=True) + # print("INFO: Closing actual ConsoleHandler.", flush=True) # Reduced _actual_console_handler.close() - except Exception as e: - print(f"Error closing ConsoleHandler: {e}", flush=True) + except Exception as e: print(f"Error closing ConsoleHandler: {e}", flush=True) _actual_console_handler = None if _actual_file_handler: try: - print("INFO: Closing actual FileHandler.", flush=True) + # print("INFO: Closing actual FileHandler.", flush=True) # Reduced _actual_file_handler.close() - except Exception as e: - print(f"Error closing FileHandler: {e}", flush=True) + except Exception as e: print(f"Error closing FileHandler: {e}", flush=True) _actual_file_handler = None - # Clear the root logger's handlers (should only be the QueuePuttingHandler) root_logger = logging.getLogger() for handler in root_logger.handlers[:]: try: - if isinstance( - handler, QueuePuttingHandler - ): # Specifically target our handler + if isinstance(handler, QueuePuttingHandler): handler.close() root_logger.removeHandler(handler) - print( - f"INFO: Removed and closed QueuePuttingHandler: {handler}", - flush=True, - ) + # print(f"INFO: Removed and closed QueuePuttingHandler: {handler}", flush=True) # Reduced except Exception as e_rem_qph: print(f"Error removing QueuePuttingHandler: {e_rem_qph}", flush=True) - _global_log_queue = None # Allow it to be garbage collected - _tk_root_instance_for_processing = None # Clear reference + _global_log_queue = None + _tk_root_instance_for_processing = None _base_formatter = None - print("INFO: Centralized logging system shutdown complete.", flush=True) + print("INFO: Centralized logging system shutdown complete.", flush=True) \ No newline at end of file diff --git a/todo.md b/todo.md index 4a7092e..4a2a3ab 100644 --- a/todo.md +++ b/todo.md @@ -324,3 +324,121 @@ Considerando le aree di miglioramento ecco una roadmap di sviluppo. Ho ipotizzat * **Design:** Mantieni un design modulare e flessibile per facilitare l'aggiunta di nuove funzionalità in futuro. Dimmi cosa ne pensi di questa proposta e quali sono le tue priorità, così possiamo elaborare un piano di sviluppo più dettagliato. + + + + + +Ottima analisi preliminare! Hai colto molto bene la struttura generale dell'applicazione e hai già individuato alcuni punti chiave. + +Sono d'accordo con te, la priorità assoluta è risolvere il problema di inizializzazione in `FullFlightDetailsWindow`. Quel `self.tk IS MISSING` è decisamente un campanello d'allarme e probabilmente la causa del fallimento della chiamata a `self.protocol`. + +Ecco un riassunto di ciò che ho compreso dalla tua analisi e alcuni pensieri iniziali prima di passare a un piano di sviluppo più dettagliato: + +**Punti di Forza Rilevati:** + +* **Struttura MVC (o tentativo di):** L'organizzazione in cartelle `gui`, `controller`, `data`, `utils`, `map` è un buon inizio per una separazione delle responsabilità. +* **Modello Canonico (`CanonicalFlightState`):** Fondamentale per disaccoppiare la logica di business dalle fonti di dati specifiche. +* **Configurazione Centralizzata (`config.py`, `logging_config.py`):** Facilita la gestione delle impostazioni globali. +* **Logging Accodato per la GUI (`logger.py`):** Ottima soluzione per evitare che il logging blocchi l'interfaccia utente. +* **Gestione Asincrona Dati Live:** L'uso di un thread per `OpenSkyLiveAdapter` e di code per la comunicazione con il controller è corretto per non bloccare la GUI durante il recupero dei dati. +* **Gestione Asincrona Rendering Mappa (`MapCanvasManager`):** Anche qui, l'uso di un thread worker per il rendering delle tile e degli overlay è una scelta eccellente per mantenere la GUI reattiva. +* **Gestione Cache Tile (`MapTileManager`):** Importante per le performance e per l'uso offline (anche se il download offline non è esplicitamente menzionato come requisito, la cache aiuta). +* **Modularità Componenti Mappa:** La separazione tra `MapCanvasManager`, `MapTileManager`, `MapService`, `map_drawing` e `map_utils` è buona. +* **Gestione Dipendenze Opzionali:** La presenza di flag come `PIL_IMAGE_LIB_AVAILABLE` indica una buona gestione delle librerie non strettamente essenziali per il funzionamento base. + +**Aree Chiave da Indagare e Potenziali Incongruenze (come hai giustamente notato):** + +1. **`FullFlightDetailsWindow.__init__`:** + * **Problema `super().__init__()` e `self.tk`:** Come discusso, questo è critico. `super().__init__(parent)` deve essere chiamato prima di qualsiasi operazione che si aspetti che `self` sia un widget Tkinter completamente formato. Questo include `self.title()`, `self.geometry()`, `self.protocol()`, e la creazione di qualsiasi widget figlio *direttamente* su `self` (anche se i widget figli sono generalmente messi su frame interni, il Toplevel stesso deve essere inizializzato). + * **Azione Immediata:** Correggere l'ordine delle chiamate in `__init__`. + +2. **Coerenza e Centralizzazione delle Utility:** + * **Disegno Testo su Placeholder:** Funzioni come `_draw_text_on_placeholder` appaiono sia in `MapCanvasManager` che in `MapTileManager`. Potrebbero essere centralizzate in `map_drawing.py` o `map_utils.py`. + * **Costanti di Disegno Tracce:** Attualmente, `TRACK_HISTORY_POINTS` e `TRACK_LINE_WIDTH` vengono letti in `map_drawing.py` da `app_config` (tramite `map_constants`). Se queste devono essere configurabili dinamicamente dall'utente (es. tramite la GUI in `MainWindow` che ha `track_length_spinbox`), il valore aggiornato deve essere propagato correttamente a `MapCanvasManager` e quindi passato al thread worker che usa `map_drawing`. `MapCanvasManager.set_max_track_points` sembra fare questo per `max_track_points`, ma `TRACK_LINE_WIDTH` sembra più statico. + +3. **Robustezza e Gestione Errori:** + * **Dipendenze Mancanti:** La gestione attuale con flag è buona, ma assicurarsi che l'utente riceva un feedback chiaro se una funzionalità chiave (come la mappa) è disabilitata. + * **Thread Safety:** L'uso di `_map_data_lock` in `MapCanvasManager` è corretto. Una rapida scorsa suggerisce che sia usato nei punti giusti, ma una revisione più approfondita è sempre utile quando si lavora con thread e dati condivisi. + +4. **Interfaccia Utente e Esperienza Utente (UX):** + * **Feedback Validazione BBox:** Come hai suggerito, un feedback più immediato nella GUI per input BBox non validi sarebbe utile. + * **Completamento Funzionalità Placeholder:** Le sezioni "History" e "Live: Airport" sono chiaramente da sviluppare. + +**Piano di Sviluppo Proposto (Alto Livello):** + +Concordo con la tua analisi delle priorità. Ecco una possibile strutturazione del piano di sviluppo, che possiamo poi affinare insieme: + +**FASE 1: Stabilizzazione e Correzioni Critiche (Priorità Massima)** + +1. **Risolvere Inizializzazione `FullFlightDetailsWindow`:** + * **Obiettivo:** Assicurare che la finestra dei dettagli completi si apra senza errori e funzioni come previsto. + * **Azioni:** + * Rivedere `__init__` come da tua analisi: `super().__init__(parent)` deve precedere l'uso di metodi specifici del Toplevel. + * Testare approfonditamente l'apertura e la chiusura della finestra. +2. **Verifica Gestione Errori Critici:** + * **Obiettivo:** Assicurare che l'applicazione gestisca con grazia la mancanza di dipendenze chiave (Pillow, mercantile per la mappa) mostrando messaggi chiari e disabilitando le funzionalità impattate senza crashare. + * **Azioni:** Testare l'avvio dell'applicazione in un ambiente virtuale dove mancano queste librerie. +3. **Revisione Logging Iniziale:** + * **Obiettivo:** Assicurare che il logging sia informativo fin dalle prime fasi di avvio e che non ci siano errori nella sua configurazione. + * **Azioni:** Controllare i primi log emessi all'avvio. + +**FASE 2: Miglioramenti Strutturali e Usabilità Base (Breve-Medio Termine)** + +1. **Refactoring Componenti Mappa:** + * **Obiettivo:** Migliorare la manutenibilità e ridurre la duplicazione di codice. + * **Azioni:** + * Centralizzare le utility di disegno comuni (es. testo su placeholder, caricamento font) in `map_drawing.py` o `map_utils.py`. + * Rivedere come `TRACK_LINE_WIDTH` viene gestito; idealmente dovrebbe essere una configurazione passata a `MapCanvasManager` e poi al worker, simile a `max_track_points`. +2. **Migliorare Feedback Utente GUI:** + * **Obiettivo:** Rendere l'interfaccia più intuitiva. + * **Azioni:** + * Implementare validazione BBox in tempo reale (o al momento del focus-out) nei campi di input di `MainWindow`. + * Assicurare che i messaggi di stato e gli indicatori (semaforo) siano sempre chiari e riflettano lo stato corrente dell'applicazione. +3. **Consolidare la Logica di `MapCanvasManager`:** + * **Obiettivo:** Data la sua complessità, assicurarsi che la gestione dello stato (zoom, centro, BBox target) sia robusta e che le interazioni tra thread GUI e worker siano impeccabili. + * **Azioni:** Revisione specifica del flusso di richieste e risultati di rendering, specialmente in scenari di interazioni utente rapide (pan/zoom veloci). + +**FASE 3: Implementazione Funzionalità Mancanti (Medio Termine)** + +1. **Modalità "History":** + * **Obiettivo:** Permettere la visualizzazione e l'analisi dei dati di volo storici. + * **Azioni:** + * Definire l'interfaccia utente per la selezione della data/ora o del volo. + * Implementare la logica nel controller per interrogare `DataStorage`. + * Adattare `MapCanvasManager` per visualizzare tracce storiche (potrebbe richiedere modifiche per non aspettarsi dati "live"). + * Considerare una funzionalità di "replay" della traccia. +2. **Vista Tabellare:** + * **Obiettivo:** Fornire una vista alternativa ai dati dei voli live. + * **Azioni:** + * Usare `ttk.Treeview` per mostrare i dati da `AppController._current_flights_to_display_gui` (o una sua copia gestita). + * Implementare ordinamento e selezione. La selezione dovrebbe aggiornare il pannello dei dettagli e potenzialmente centrare la mappa. +3. **Modalità "Live: Airport":** + * **Obiettivo:** Fornire una vista focalizzata sull'attività di un aeroporto specifico. + * **Azioni:** + * **Definizione:** Scegliere se basarsi su un raggio, su dati specifici di arrivi/partenze (richiederebbe nuove fonti dati/API). + * **Dati Aeroporti:** Valutare se integrare un database di aeroporti (es. da `pyflightdata` o file CSV). + * Implementare la logica di filtraggio e visualizzazione. + +**FASE 4: Funzionalità Avanzate e Rifinitura (Lungo Termine)** + +1. **Visualizzazione Immagini Aerei e Link Esterni (completamento):** + * In `FullFlightDetailsWindow`, implementare il caricamento effettivo delle immagini (es. tramite API di JetPhotos, FlightAware, ecc., o scraping se consentito) e rendere più robusta l'apertura dei link. +2. **Ricerca e Filtri Avanzati:** + * Funzionalità di ricerca voli per callsign, ICAO24, tipo di aereo. + * Filtri sulla mappa (es. per altitudine, velocità, tipo di aereo). +3. **Configurazioni Utente:** + * Possibilità di cambiare il map provider (se ne verranno aggiunti altri). + * Opzioni per la gestione della cache dei tile (pulizia, dimensione massima). + * Personalizzazione dell'aspetto delle tracce (colore, spessore - se `TRACK_LINE_WIDTH` diventa configurabile). +4. **Test Approfonditi:** + * Scrivere test unitari per le logiche di calcolo in `map_utils`, per i data adapter, e per la gestione del DB. + * Test di integrazione per i flussi principali. +5. **Documentazione e Packaging:** + * Migliorare docstring e commenti. + * Creare un manuale utente. + * Preparare per la distribuzione (es. PyInstaller, cx_Freeze). + +Questo è un primo abbozzo. Sono pronto a discutere ogni punto e ad integrare le tue idee. In particolare, vorrei capire meglio quali sono le tue priorità e le funzionalità che ritieni più importanti da sviluppare o migliorare nel breve termine, dopo la stabilizzazione iniziale. + +Cosa ne pensi? Quali sono le tue idee o le aree su cui vorresti concentrarci?