# FlightMonitor/controller/app_controller.py from queue import Queue, Empty as QueueEmpty import threading import tkinter as tk import time import os import csv import copy from datetime import datetime, timezone # Aggiunto per passare la data a DataStorage from ..data.opensky_live_adapter import ( OpenSkyLiveAdapter, AdapterMessage, MSG_TYPE_FLIGHT_DATA, MSG_TYPE_ADAPTER_STATUS, STATUS_STARTING, STATUS_FETCHING, STATUS_RECOVERED, STATUS_RATE_LIMITED, STATUS_API_ERROR_TEMPORARY, STATUS_PERMANENT_FAILURE, STATUS_STOPPED, ) from ..data import config as app_config from ..utils.logger import get_logger from ..data.storage import DataStorage from ..data.common_models import CanonicalFlightState from ..data.aircraft_database_manager import AircraftDatabaseManager from typing import List, Optional, Dict, Any, TYPE_CHECKING, Callable from ..utils.gui_utils import ( GUI_STATUS_OK, GUI_STATUS_WARNING, GUI_STATUS_ERROR, GUI_STATUS_FETCHING, GUI_STATUS_UNKNOWN, ) if TYPE_CHECKING: from ..gui.main_window import MainWindow from ..gui.dialogs.import_progress_dialog import ImportProgressDialog from ..gui.dialogs.full_flight_details_window import ( FullFlightDetailsWindow, ) # Importato per type hinting from ..map.map_canvas_manager import MapCanvasManager module_logger = get_logger(__name__) GUI_QUEUE_CHECK_INTERVAL_MS = 150 ADAPTER_JOIN_TIMEOUT_SECONDS = 5.0 DEFAULT_CLICK_AREA_SIZE_KM = 50.0 class AppController: # ... (__init__, set_main_window, _process_flight_data_queue, # start_live_monitoring, stop_live_monitoring, on_application_exit, # start_history_monitoring, stop_history_monitoring come nella versione precedente completa. # Assicurati che aircraft_db_manager sia inizializzato e chiuso correttamente.) def __init__(self): self.main_window: Optional["MainWindow"] = None self.live_adapter_thread: Optional[OpenSkyLiveAdapter] = None self.is_live_monitoring_active: bool = False self.flight_data_queue: Optional[Queue[AdapterMessage]] = None self._gui_after_id: Optional[str] = None self._active_bounding_box: Optional[Dict[str, float]] = None self.data_storage: Optional[DataStorage] = None self.aircraft_db_manager: Optional[AircraftDatabaseManager] = None try: self.data_storage = DataStorage() module_logger.info("DataStorage initialized successfully by AppController.") except Exception as e: module_logger.critical( f"CRITICAL: Failed to initialize DataStorage: {e}", exc_info=True ) self.data_storage = None try: self.aircraft_db_manager = AircraftDatabaseManager() module_logger.info( "AircraftDatabaseManager initialized successfully by AppController." ) except Exception as e: module_logger.critical( f"CRITICAL: Failed to initialize AircraftDatabaseManager: {e}", exc_info=True, ) self.aircraft_db_manager = None module_logger.info("AppController initialized.") def set_main_window(self, main_window_instance: "MainWindow"): self.main_window = main_window_instance module_logger.debug( f"Main window instance ({type(main_window_instance)}) set in AppController." ) initial_status_msg = "System Initialized. Ready." initial_status_level = GUI_STATUS_OK if not self.data_storage: err_msg_ds = "Data storage init failed. History will not be saved." module_logger.error(err_msg_ds) initial_status_msg = err_msg_ds initial_status_level = GUI_STATUS_ERROR if not self.aircraft_db_manager: err_msg_adb = "Aircraft DB init failed. Static details may be unavailable." module_logger.error(err_msg_adb) if initial_status_level == GUI_STATUS_OK: initial_status_msg = err_msg_adb initial_status_level = GUI_STATUS_WARNING else: initial_status_msg += f" {err_msg_adb}" initial_status_level = GUI_STATUS_ERROR if ( self.main_window and hasattr(self.main_window, "root") and self.main_window.root.winfo_exists() and hasattr(self.main_window, "update_semaphore_and_status") ): self.main_window.update_semaphore_and_status( initial_status_level, initial_status_msg ) else: module_logger.error( "Main window not set or lacks update_semaphore_and_status during set_main_window." ) def _process_flight_data_queue(self): # ... (come nella versione precedente completa) if not self.flight_data_queue: return if not ( self.main_window and hasattr(self.main_window, "root") and self.main_window.root.winfo_exists() ): self._gui_after_id = None return try: while not self.flight_data_queue.empty(): message = None try: message = self.flight_data_queue.get(block=False, timeout=0.01) except QueueEmpty: break except Exception: continue if message is None: continue try: message_type = message.get("type") if message_type == MSG_TYPE_FLIGHT_DATA: flight_states_payload: Optional[List[CanonicalFlightState]] = ( message.get("payload") ) if flight_states_payload is not None: if self.data_storage: saved_count = 0 for state in flight_states_payload: if not isinstance(state, CanonicalFlightState): continue try: flight_id = self.data_storage.add_or_update_flight_daily( icao24=state.icao24, callsign=state.callsign, origin_country=state.origin_country, detection_timestamp=state.timestamp, ) if flight_id: pos_id = ( self.data_storage.add_position_daily( flight_id, state ) ) if pos_id: saved_count += 1 except Exception: pass if saved_count > 0: module_logger.info( f"Saved {saved_count} position updates to DB." ) if ( hasattr(self.main_window, "map_manager_instance") and self.main_window.map_manager_instance is not None and hasattr( self.main_window.map_manager_instance, "update_flights_on_map", ) and self.is_live_monitoring_active and self._active_bounding_box ): self.main_window.map_manager_instance.update_flights_on_map( flight_states_payload ) gui_message = ( f"Live data: {len(flight_states_payload)} aircraft tracked." if flight_states_payload else "Live data: No aircraft in area." ) if hasattr(self.main_window, "update_semaphore_and_status"): try: self.main_window.update_semaphore_and_status( GUI_STATUS_OK, gui_message ) except tk.TclError: self._gui_after_id = None return else: if hasattr(self.main_window, "update_semaphore_and_status"): try: self.main_window.update_semaphore_and_status( GUI_STATUS_WARNING, "Received empty data payload.", ) except tk.TclError: self._gui_after_id = None return elif message_type == MSG_TYPE_ADAPTER_STATUS: status_code = message.get("status_code") gui_message_from_adapter = message.get( "message", f"Adapter status: {status_code}" ) gui_status_level_to_set = GUI_STATUS_UNKNOWN action_required = None details_from_adapter = message.get("details", {}) if status_code == STATUS_PERMANENT_FAILURE: action_required = "STOP_MONITORING" # ... (other status handling) if hasattr(self.main_window, "update_semaphore_and_status"): try: self.main_window.update_semaphore_and_status( gui_status_level_to_set, gui_message_from_adapter ) except tk.TclError: self._gui_after_id = None return if action_required == "STOP_MONITORING": self.stop_live_monitoring(from_error=True) break except Exception as e_msg_proc: module_logger.error( f"Error processing adapter message: {e_msg_proc}", exc_info=True ) finally: try: self.flight_data_queue.task_done() except: pass except Exception: pass finally: if ( self.is_live_monitoring_active and self.main_window and hasattr(self.main_window, "root") and self.main_window.root.winfo_exists() ): try: self._gui_after_id = self.main_window.root.after( GUI_QUEUE_CHECK_INTERVAL_MS, self._process_flight_data_queue ) except: self._gui_after_id = None else: self._gui_after_id = None def start_live_monitoring(self, bounding_box: Dict[str, float]): # ... (come nella versione precedente completa) if not self.main_window: module_logger.error("Controller: Main window not set.") return if not bounding_box: err_msg = "Controller: Bounding box is required." module_logger.error(err_msg) if hasattr(self.main_window, "_reset_gui_to_stopped_state"): self.main_window._reset_gui_to_stopped_state(f"Start failed: {err_msg}") return if not self.data_storage: err_msg_ds = "DataStorage not initialized. History will not be saved." if hasattr(self.main_window, "update_semaphore_and_status"): self.main_window.update_semaphore_and_status( GUI_STATUS_WARNING, err_msg_ds + " Check logs." ) if self.is_live_monitoring_active: if hasattr(self.main_window, "_reset_gui_to_stopped_state"): self.main_window._reset_gui_to_stopped_state( "Monitoring stop in progress or already active." ) return module_logger.info( f"Controller: Starting live monitoring for bbox: {bounding_box}" ) self._active_bounding_box = bounding_box if ( hasattr(self.main_window, "map_manager_instance") and self.main_window.map_manager_instance and hasattr(self.main_window.map_manager_instance, "set_target_bbox") ): try: self.main_window.map_manager_instance.set_target_bbox(bounding_box) except Exception as e_map: module_logger.error(f"Error setting map BBox: {e_map}", exc_info=True) else: if hasattr(self.main_window, "clear_all_views_data"): self.main_window.clear_all_views_data() if self.flight_data_queue is None: self.flight_data_queue = Queue(maxsize=200) else: while not self.flight_data_queue.empty(): try: self.flight_data_queue.get_nowait() self.flight_data_queue.task_done() except: break if self.live_adapter_thread and self.live_adapter_thread.is_alive(): try: self.live_adapter_thread.stop() if self.main_window and self.main_window.root.winfo_exists(): self.main_window.root.update_idletasks() self.live_adapter_thread.join(timeout=ADAPTER_JOIN_TIMEOUT_SECONDS) except Exception as e_join: module_logger.error( f"Error stopping old adapter: {e_join}", exc_info=True ) finally: self.live_adapter_thread = None self.live_adapter_thread = OpenSkyLiveAdapter( output_queue=self.flight_data_queue, bounding_box=self._active_bounding_box, polling_interval=app_config.LIVE_POLLING_INTERVAL_SECONDS, ) self.is_live_monitoring_active = True self.live_adapter_thread.start() if ( self._gui_after_id and self.main_window and self.main_window.root.winfo_exists() ): try: self.main_window.root.after_cancel(self._gui_after_id) except: pass finally: self._gui_after_id = None if self.main_window and self.main_window.root.winfo_exists(): self._gui_after_id = self.main_window.root.after( 100, self._process_flight_data_queue ) else: # Fallback se la GUI non c'è self.is_live_monitoring_active = False if self.live_adapter_thread and self.live_adapter_thread.is_alive(): self.live_adapter_thread.stop() if hasattr(self.main_window, "_reset_gui_to_stopped_state"): self.main_window._reset_gui_to_stopped_state("Start failed: GUI error.") def stop_live_monitoring(self, from_error: bool = False): # ... (come nella versione precedente completa) if not self.is_live_monitoring_active and not ( from_error and self.live_adapter_thread and self.live_adapter_thread.is_alive() ): if ( hasattr(self.main_window, "_reset_gui_to_stopped_state") and not from_error ): self.main_window._reset_gui_to_stopped_state( "Monitoring already stopped." ) return module_logger.info( f"Controller: Stopping live monitoring (from_error={from_error})." ) self.is_live_monitoring_active = False if ( self._gui_after_id and self.main_window and self.main_window.root.winfo_exists() ): try: self.main_window.root.after_cancel(self._gui_after_id) except: pass finally: self._gui_after_id = None if self.live_adapter_thread and self.live_adapter_thread.is_alive(): try: self.live_adapter_thread.stop() if self.main_window and self.main_window.root.winfo_exists(): self.main_window.root.update_idletasks() self.live_adapter_thread.join(timeout=ADAPTER_JOIN_TIMEOUT_SECONDS) except Exception as e_join: module_logger.error(f"Error stopping adapter: {e_join}", exc_info=True) finally: self.live_adapter_thread = None if ( self.flight_data_queue and self.main_window and self.main_window.root.winfo_exists() ): try: self._process_flight_data_queue() except Exception as e_final_q: module_logger.error( f"Error in final queue processing: {e_final_q}", exc_info=True ) if hasattr(self.main_window, "clear_all_views_data"): self.main_window.clear_all_views_data() if hasattr(self.main_window, "_reset_gui_to_stopped_state"): msg = ( "Monitoring stopped due to an error." if from_error else "Monitoring stopped." ) try: self.main_window._reset_gui_to_stopped_state(msg) except Exception as e_reset: module_logger.error(f"Error resetting GUI: {e_reset}", exc_info=True) self._active_bounding_box = None def on_application_exit(self): # ... (come nella versione precedente completa, inclusa la chiusura di aircraft_db_manager) module_logger.info( "Controller: Application exit requested. Cleaning up resources." ) if ( self.main_window and hasattr(self.main_window, "map_manager_instance") and self.main_window.map_manager_instance is not None ): map_manager = self.main_window.map_manager_instance if hasattr(map_manager, "shutdown_worker") and callable( map_manager.shutdown_worker ): try: map_manager.shutdown_worker() module_logger.info( "Controller: MapCanvasManager worker shutdown requested." ) except Exception as e_map_shutdown: module_logger.error( f"Controller: Error during MapCanvasManager worker shutdown: {e_map_shutdown}", exc_info=True, ) is_adapter_considered_running = ( self.live_adapter_thread and self.live_adapter_thread.is_alive() ) or self.is_live_monitoring_active if is_adapter_considered_running: self.stop_live_monitoring(from_error=False) if self.data_storage: try: self.data_storage.close_connection() except Exception as e_db_close: module_logger.error( f"Error closing DataStorage: {e_db_close}", exc_info=True ) finally: self.data_storage = None if self.aircraft_db_manager: try: self.aircraft_db_manager.close_connection() except Exception as e_ac_db_close: module_logger.error( f"Error closing AircraftDatabaseManager: {e_ac_db_close}", exc_info=True, ) finally: self.aircraft_db_manager = None module_logger.info("Controller: Cleanup on application exit finished.") def start_history_monitoring(self): # Placeholder # ... (come prima) if not self.main_window: module_logger.error("Main window not set for history.") return if not self.data_storage: err_msg = "DataStorage not initialized. Cannot use history features." if hasattr(self.main_window, "update_semaphore_and_status"): self.main_window.update_semaphore_and_status(GUI_STATUS_ERROR, err_msg) if hasattr(self.main_window, "_reset_gui_to_stopped_state"): self.main_window._reset_gui_to_stopped_state( f"History start failed: {err_msg}" ) return if hasattr(self.main_window, "update_semaphore_and_status"): self.main_window.update_semaphore_and_status( GUI_STATUS_OK, "History mode active (placeholder)." ) def stop_history_monitoring(self): # Placeholder # ... (come prima) if not self.main_window: return if hasattr(self.main_window, "update_semaphore_and_status"): self.main_window.update_semaphore_and_status( GUI_STATUS_OK, "History monitoring stopped." ) def on_map_left_click( self, latitude: float, longitude: float, screen_x: int, screen_y: int ): # ... (come prima, solo aggiornamento info click) module_logger.debug( f"Controller: Map left-clicked at Geo ({latitude:.5f}, {longitude:.5f})" ) if self.main_window and hasattr(self.main_window, "update_clicked_map_info"): lat_dms_str, lon_dms_str = "N/A", "N/A" try: from ..map.map_utils import deg_to_dms_string lat_dms_str = ( deg_to_dms_string(latitude, "lat") if latitude is not None else "N/A" ) lon_dms_str = ( deg_to_dms_string(longitude, "lon") if longitude is not None else "N/A" ) except ImportError: module_logger.warning("map_utils.deg_to_dms_string N/A for left click.") except Exception as e_dms: module_logger.warning( f"Error DMS for left click: {e_dms}", exc_info=False ) lat_dms_str = lon_dms_str = "N/A (CalcErr)" try: self.main_window.update_clicked_map_info( lat_deg=latitude, lon_deg=longitude, lat_dms=lat_dms_str, lon_dms=lon_dms_str, ) except tk.TclError as e_tcl: module_logger.warning( f"TclError updating map clicked info panel: {e_tcl}. GUI closing." ) except Exception as e_update: module_logger.error( f"Error updating map clicked info panel: {e_update}", exc_info=False ) def request_detailed_flight_info(self, icao24: str): # ... (come prima) module_logger.info(f"Controller: Detailed info request for ICAO24: {icao24}") if not self.main_window: module_logger.error("Controller: MainWindow not set.") return if not icao24: if hasattr(self.main_window, "update_selected_flight_details"): self.main_window.update_selected_flight_details(None) return live_data: Optional[Dict[str, Any]] = None static_data: Optional[Dict[str, Any]] = None combined_details: Dict[str, Any] = {"icao24": icao24.lower()} if ( self.main_window and hasattr(self.main_window, "map_manager_instance") and self.main_window.map_manager_instance and hasattr( self.main_window.map_manager_instance, "_current_flights_to_display_gui" ) and hasattr(self.main_window.map_manager_instance, "_map_data_lock") ): map_mgr = self.main_window.map_manager_instance with map_mgr._map_data_lock: # type: ignore for state in map_mgr._current_flights_to_display_gui: # type: ignore if state.icao24.lower() == icao24.lower(): live_data = state.to_dict() break if live_data: combined_details.update(live_data) if self.aircraft_db_manager: static_data = self.aircraft_db_manager.get_aircraft_details(icao24) if static_data: for k, v in static_data.items(): if k not in combined_details: combined_details[k] = v if hasattr(self.main_window, "update_selected_flight_details"): self.main_window.update_selected_flight_details(combined_details) def import_aircraft_database_from_file_with_progress(self, csv_filepath: str, progress_dialog_ref: "ImportProgressDialog"): # type: ignore # ... (come prima, con avvio thread) module_logger.info( f"Controller: Requesting aircraft DB import with progress from: {csv_filepath}" ) if not self.aircraft_db_manager: module_logger.error( "AircraftDatabaseManager not initialized. Cannot import." ) if progress_dialog_ref and progress_dialog_ref.winfo_exists(): progress_dialog_ref.import_finished( False, "Error: Database manager not active." ) elif self.main_window and hasattr(self.main_window, "show_error_message"): self.main_window.show_error_message( "Database Error", "Aircraft database manager not active." ) return if not self.main_window or not self.main_window.root.winfo_exists(): module_logger.error("MainWindow not available to start import thread.") if progress_dialog_ref and progress_dialog_ref.winfo_exists(): progress_dialog_ref.import_finished( False, "Error: Main application window not available." ) return if progress_dialog_ref and progress_dialog_ref.winfo_exists(): progress_dialog_ref.import_started() import_thread = threading.Thread( target=self._perform_db_import_with_progress_threaded, args=(csv_filepath, progress_dialog_ref), daemon=True, ) import_thread.start() module_logger.info(f"Aircraft DB import thread started for: {csv_filepath}") def _count_csv_rows(self, csv_filepath: str) -> Optional[int]: # ... (come prima, con aumento csv.field_size_limit e module_logger) try: current_limit = csv.field_size_limit() new_limit_target = 10 * 1024 * 1024 if new_limit_target > current_limit: csv.field_size_limit(new_limit_target) module_logger.info( f"Count Rows: Increased csv.field_size_limit from {current_limit} to {new_limit_target}" ) except Exception as e_limit: module_logger.error( f"Count Rows: Error attempting to increase csv.field_size_limit: {e_limit}" ) try: with open(csv_filepath, "r", encoding="utf-8-sig") as f: reader = csv.reader(f) try: header = next(reader) _ = header except StopIteration: module_logger.info( f"Count rows: CSV file '{csv_filepath}' is empty." ) return 0 row_count = sum(1 for _ in reader) return row_count except FileNotFoundError: module_logger.error(f"Count rows: CSV file not found: {csv_filepath}") return None except csv.Error as e_csv: module_logger.error( f"Count rows: CSV format error in '{csv_filepath}': {e_csv}", exc_info=True, ) return None except Exception as e: module_logger.error( f"Count rows: General error reading CSV {csv_filepath}: {e}", exc_info=True, ) return None def _perform_db_import_with_progress_threaded(self, csv_filepath: str, progress_dialog_ref: "ImportProgressDialog"): # type: ignore # ... (come prima, con schedule_gui_update e passaggio callback) if not self.aircraft_db_manager: module_logger.error("AircraftDBManager N/A in import thread.") if ( progress_dialog_ref and progress_dialog_ref.winfo_exists() and self.main_window and self.main_window.root.winfo_exists() ): self.main_window.root.after( 0, lambda: progress_dialog_ref.import_finished( False, "Internal Error: DB Manager missing." ), ) return module_logger.info(f"Import thread: Starting row count for: {csv_filepath}") def schedule_gui_update(callable_func: Callable, *args: Any): # type: ignore if ( self.main_window and hasattr(self.main_window, "root") and self.main_window.root.winfo_exists() ): try: self.main_window.root.after(0, lambda: callable_func(*args)) except Exception as e_after: module_logger.error( f"Error scheduling GUI update from import thread: {e_after}" ) total_data_rows = self._count_csv_rows(csv_filepath) if total_data_rows is None: if progress_dialog_ref and progress_dialog_ref.winfo_exists(): schedule_gui_update( progress_dialog_ref.import_finished, False, f"Error: Could not read/count rows in '{os.path.basename(csv_filepath)}'. Check logs.", ) return if total_data_rows == 0: if progress_dialog_ref and progress_dialog_ref.winfo_exists(): schedule_gui_update( progress_dialog_ref.update_progress, 0, 0, 0, f"File '{os.path.basename(csv_filepath)}' is empty or header-only.", ) schedule_gui_update( progress_dialog_ref.import_finished, True, "Import complete: No data rows to import.", ) return if progress_dialog_ref and progress_dialog_ref.winfo_exists(): schedule_gui_update( progress_dialog_ref.update_progress, 0, 0, total_data_rows, f"Found {total_data_rows} data rows. Starting import from '{os.path.basename(csv_filepath)}'...", ) def import_progress_update_for_dialog_from_controller( processed_csv_rows, imported_db_rows, total_for_cb ): if progress_dialog_ref and progress_dialog_ref.winfo_exists(): schedule_gui_update( progress_dialog_ref.update_progress, processed_csv_rows, imported_db_rows, total_for_cb, f"Importing CSV row {processed_csv_rows}...", ) processed_final, imported_final = self.aircraft_db_manager.import_from_csv( csv_filepath, replace_existing=True, progress_callback=import_progress_update_for_dialog_from_controller, total_rows_for_callback=total_data_rows, ) final_message = f"DB Import complete. Processed: {processed_final}, Imported/Updated: {imported_final}." success = True if imported_final == 0 and processed_final > 0 and total_data_rows > 0: final_message = f"DB Import: {processed_final} CSV data rows processed, 0 imported (check CSV format/logs)." elif imported_final == 0 and processed_final == 0 and total_data_rows > 0: final_message = f"DB Import failed. Could not process rows from '{os.path.basename(csv_filepath)}'." success = False elif total_data_rows == 0: final_message = f"Import complete: File '{os.path.basename(csv_filepath)}' had no data rows." success = True if progress_dialog_ref and progress_dialog_ref.winfo_exists(): schedule_gui_update( progress_dialog_ref.update_progress, processed_final, imported_final, total_data_rows, "Finalizing import...", ) schedule_gui_update( progress_dialog_ref.import_finished, success, final_message ) def request_and_show_full_flight_details(self, icao24: str): module_logger.info( f"Controller: Requesting to show full details for ICAO24: {icao24}" ) if ( not self.main_window or not hasattr(self.main_window, "root") or not self.main_window.root.winfo_exists() ): module_logger.error( "Controller: MainWindow not available to show full flight details." ) return if not icao24: module_logger.warning("Controller: Empty ICAO24 for full details request.") if hasattr(self.main_window, "show_info_message"): self.main_window.show_info_message( "Flight Details", "No ICAO24 provided for full details." ) return static_data: Optional[Dict[str, Any]] = None if self.aircraft_db_manager: static_data = self.aircraft_db_manager.get_aircraft_details(icao24) live_data: Optional[Dict[str, Any]] = None if ( self.main_window and hasattr(self.main_window, "map_manager_instance") and self.main_window.map_manager_instance and hasattr( self.main_window.map_manager_instance, "_current_flights_to_display_gui" ) and hasattr(self.main_window.map_manager_instance, "_map_data_lock") ): map_mgr = self.main_window.map_manager_instance with map_mgr._map_data_lock: # type: ignore for state in map_mgr._current_flights_to_display_gui: # type: ignore if state.icao24.lower() == icao24.lower(): live_data = state.to_dict() break full_track_data_list: List[Dict[str, Any]] = [] # Inizializza come lista vuota if self.data_storage: try: # Per ora, recuperiamo la traccia solo per il giorno UTC corrente # In futuro, potremmo voler passare un intervallo di date o un flight_id specifico current_utc_date = datetime.now(timezone.utc) track_states = self.data_storage.get_flight_track_for_icao_on_date( icao24, current_utc_date ) if track_states: full_track_data_list = [state.to_dict() for state in track_states] module_logger.debug( f"FullDetails: Found {len(full_track_data_list)} historical track points for {icao24} (today)." ) else: module_logger.info( f"FullDetails: No historical track data found for {icao24} (today)." ) except Exception as e_track: module_logger.error( f"FullDetails: Error retrieving historical track for {icao24}: {e_track}", exc_info=True, ) else: module_logger.warning( "FullDetails: DataStorage not available for historical track." ) try: from ..gui.dialogs.full_flight_details_window import ( FullFlightDetailsWindow, ) # Importa qui # Gestisci la chiusura della finestra precedente if ( hasattr(self.main_window, "full_flight_details_window") and self.main_window.full_flight_details_window and self.main_window.full_flight_details_window.winfo_exists() ): # type: ignore try: self.main_window.full_flight_details_window.destroy() # type: ignore except tk.TclError: pass # Potrebbe essere già in fase di distruzione details_win = FullFlightDetailsWindow(self.main_window.root, icao24, self) self.main_window.full_flight_details_window = details_win details_win.update_details( static_data, live_data, full_track_data_list ) # Passa la traccia except ImportError: module_logger.error( "FullFlightDetailsWindow class not found. Cannot display full details." ) if hasattr(self.main_window, "show_error_message"): self.main_window.show_error_message( "UI Error", "Could not open full details window (import error)." ) except Exception as e_show_details: module_logger.error( f"Error showing full flight details window for {icao24}: {e_show_details}", exc_info=True, ) if hasattr(self.main_window, "show_error_message"): self.main_window.show_error_message( "Error", f"Could not display full details: {e_show_details}" ) # Assicurati che tutti gli altri metodi (on_map_right_click, etc.) siano presenti come prima. def on_map_right_click( self, latitude: float, longitude: float, screen_x: int, screen_y: int ): # ... (come prima) module_logger.debug( f"Controller: Map right-clicked at Geo ({latitude:.5f}, {longitude:.5f})" ) if self.main_window: if hasattr(self.main_window, "update_clicked_map_info"): lat_dms_str, lon_dms_str = "N/A", "N/A" try: from ..map.map_utils import deg_to_dms_string lat_dms_str = ( deg_to_dms_string(latitude, "lat") if latitude is not None else "N/A" ) lon_dms_str = ( deg_to_dms_string(longitude, "lon") if longitude is not None else "N/A" ) except ImportError: module_logger.warning( "map_utils.deg_to_dms_string N/A for right click." ) except Exception as e_dms: module_logger.warning( f"Error DMS for right click: {e_dms}", exc_info=False ) lat_dms_str = lon_dms_str = "N/A (CalcErr)" try: self.main_window.update_clicked_map_info( lat_deg=latitude, lon_deg=longitude, lat_dms=lat_dms_str, lon_dms=lon_dms_str, ) except tk.TclError as e_tcl: module_logger.warning( f"TclError updating map clicked info (right): {e_tcl}. GUI closing." ) except Exception as e_update: module_logger.error( f"Error updating map clicked info (right): {e_update}", exc_info=False, ) if hasattr(self.main_window, "show_map_context_menu"): try: self.main_window.show_map_context_menu( latitude, longitude, screen_x, screen_y ) except tk.TclError as e_tcl_menu: module_logger.warning( f"TclError showing map context menu: {e_tcl_menu}. GUI closing." ) except Exception as e_menu: module_logger.error( f"Error showing map context menu: {e_menu}", exc_info=False ) else: module_logger.warning("Main window N/A for right click handling.") def on_map_context_menu_request( self, latitude: float, longitude: float, screen_x: int, screen_y: int ): # ... (come prima) module_logger.debug( f"Controller received context menu request for ({latitude:.5f}, {longitude:.5f}) at screen ({screen_x}, {screen_y})." ) if self.main_window and hasattr(self.main_window, "show_map_context_menu"): try: self.main_window.show_map_context_menu( latitude, longitude, screen_x, screen_y ) except tk.TclError as e_tcl: module_logger.warning( f"TclError delegating show_map_context_menu: {e_tcl}. GUI closing." ) except Exception as e_menu: module_logger.error( f"Error delegating show_map_context_menu: {e_menu}", exc_info=False ) else: module_logger.warning("Main window N/A to show context menu.") def recenter_map_at_coords(self, lat: float, lon: float): # ... (come prima) module_logger.info( f"Controller request: recenter map at ({lat:.5f}, {lon:.5f})." ) if ( self.main_window and hasattr(self.main_window, "map_manager_instance") and self.main_window.map_manager_instance ): map_manager: "MapCanvasManager" = self.main_window.map_manager_instance # type: ignore if hasattr(map_manager, "recenter_map_at_coords"): try: map_manager.recenter_map_at_coords(lat, lon) except Exception as e_recenter: module_logger.error( f"Error map_manager.recenter_map_at_coords: {e_recenter}", exc_info=False, ) else: module_logger.warning( "MapCanvasManager missing 'recenter_map_at_coords' method." ) else: module_logger.warning( "Main window or MapCanvasManager N/A to recenter map." ) def set_bbox_around_coords( self, center_lat: float, center_lon: float, area_size_km: float = DEFAULT_CLICK_AREA_SIZE_KM, ): # ... (come prima) module_logger.info( f"Controller request: set BBox ({area_size_km:.1f}km) around ({center_lat:.5f}, {center_lon:.5f})." ) if ( self.main_window and hasattr(self.main_window, "map_manager_instance") and self.main_window.map_manager_instance ): map_manager: "MapCanvasManager" = self.main_window.map_manager_instance # type: ignore if hasattr(map_manager, "set_bbox_around_coords"): try: map_manager.set_bbox_around_coords( center_lat, center_lon, area_size_km ) except Exception as e_set_bbox: module_logger.error( f"Error map_manager.set_bbox_around_coords: {e_set_bbox}", exc_info=False, ) else: module_logger.warning( "MapCanvasManager missing 'set_bbox_around_coords' method." ) else: module_logger.warning("Main window or MapCanvasManager N/A to set BBox.") def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]): # ... (come prima) module_logger.debug( f"Controller request: update BBox GUI fields with: {bbox_dict}" ) if self.main_window and hasattr(self.main_window, "update_bbox_gui_fields"): try: self.main_window.update_bbox_gui_fields(bbox_dict) except tk.TclError as e_tcl: module_logger.warning( f"TclError updating BBox GUI fields: {e_tcl}. GUI closing." ) except Exception as e_update: module_logger.error( f"Error updating BBox GUI fields: {e_update}", exc_info=False ) else: module_logger.warning("Main window N/A to update BBox GUI fields.") def update_general_map_info(self): # ... (come prima, con import copy all'inizio del file) module_logger.debug("Controller: Request to update general map info.") if not ( self.main_window and hasattr(self.main_window, "map_manager_instance") and self.main_window.map_manager_instance ): module_logger.debug( "Skipping general map info update: MainWindow or map manager not ready." ) if self.main_window and hasattr( self.main_window, "update_general_map_info_display" ): try: self.main_window.update_general_map_info_display( zoom=None, map_size_str="N/A", map_geo_bounds=None, target_bbox_input=None, flight_count=None, ) except Exception: pass return map_manager: "MapCanvasManager" = self.main_window.map_manager_instance # type: ignore if hasattr(map_manager, "get_current_map_info"): map_info = {} try: map_info = map_manager.get_current_map_info() module_logger.debug( f"Fetched map info from manager for general display: {map_info}" ) zoom = map_info.get("zoom") map_geo_bounds = map_info.get("map_geo_bounds") target_bbox_input_from_map_info = map_info.get("target_bbox_input") flight_count = map_info.get("flight_count") map_size_km_w = map_info.get("map_size_km_w") map_size_km_h = map_info.get("map_size_km_h") map_size_str = "N/A" if map_size_km_w is not None and map_size_km_h is not None: try: decimals = getattr(app_config, "MAP_SIZE_KM_DECIMAL_PLACES", 1) map_size_str = f"{map_size_km_w:.{decimals}f}km x {map_size_km_h:.{decimals}f}km" except Exception as e_format: module_logger.warning( f"Error formatting map size: {e_format}. Using N/A.", exc_info=False, ) map_size_str = "N/A (FormatErr)" if hasattr(self.main_window, "update_general_map_info_display"): try: self.main_window.update_general_map_info_display( zoom=zoom, map_size_str=map_size_str, map_geo_bounds=map_geo_bounds, target_bbox_input=target_bbox_input_from_map_info, flight_count=flight_count, ) except tk.TclError as e_tcl: module_logger.warning( f"TclError updating general map info panel: {e_tcl}. GUI closing." ) except Exception as e_update: module_logger.error( f"Error updating general map info panel: {e_update}", exc_info=False, ) except Exception as e_get_info: module_logger.error( f"Error getting map info from map manager in AppController: {e_get_info}", exc_info=True, ) if hasattr(self.main_window, "update_general_map_info_display"): try: self.main_window.update_general_map_info_display( zoom=None, map_size_str="N/A", map_geo_bounds=None, target_bbox_input=None, flight_count=None, ) except Exception: pass else: module_logger.warning( "MapCanvasManager missing 'get_current_map_info' method." ) def map_zoom_in(self): # ... (come prima) module_logger.debug("Controller: Map Zoom In requested.") if ( self.main_window and hasattr(self.main_window, "map_manager_instance") and self.main_window.map_manager_instance ): map_manager: "MapCanvasManager" = self.main_window.map_manager_instance # type: ignore if hasattr(map_manager, "zoom_in_at_center"): try: map_manager.zoom_in_at_center() except Exception as e: module_logger.error( f"Error map_manager.zoom_in_at_center: {e}", exc_info=False ) else: module_logger.warning( "MapCanvasManager missing 'zoom_in_at_center' method." ) else: module_logger.warning("Map manager N/A for zoom in.") def map_zoom_out(self): # ... (come prima) module_logger.debug("Controller: Map Zoom Out requested.") if ( self.main_window and hasattr(self.main_window, "map_manager_instance") and self.main_window.map_manager_instance ): map_manager: "MapCanvasManager" = self.main_window.map_manager_instance # type: ignore if hasattr(map_manager, "zoom_out_at_center"): try: map_manager.zoom_out_at_center() except Exception as e: module_logger.error( f"Error map_manager.zoom_out_at_center: {e}", exc_info=False ) else: module_logger.warning( "MapCanvasManager missing 'zoom_out_at_center' method." ) else: module_logger.warning("Map manager N/A for zoom out.") def map_pan_direction(self, direction: str): # ... (come prima) module_logger.debug(f"Controller: Map Pan '{direction}' requested.") if ( self.main_window and hasattr(self.main_window, "map_manager_instance") and self.main_window.map_manager_instance ): map_manager: "MapCanvasManager" = self.main_window.map_manager_instance # type: ignore if hasattr(map_manager, "pan_map_fixed_step"): try: map_manager.pan_map_fixed_step(direction) except Exception as e: module_logger.error( f"Error map_manager.pan_map_fixed_step('{direction}'): {e}", exc_info=False, ) else: module_logger.warning( "MapCanvasManager missing 'pan_map_fixed_step' method." ) else: module_logger.warning(f"Map manager N/A for pan {direction}.") def map_center_on_coords_and_fit_patch( self, lat: float, lon: float, patch_size_km: float ): # ... (come prima) module_logger.debug( f"Controller: Center map at ({lat:.4f},{lon:.4f}) and fit {patch_size_km}km patch." ) if ( self.main_window and hasattr(self.main_window, "map_manager_instance") and self.main_window.map_manager_instance ): map_manager: "MapCanvasManager" = self.main_window.map_manager_instance # type: ignore if hasattr(map_manager, "center_map_and_fit_patch"): try: map_manager.center_map_and_fit_patch(lat, lon, patch_size_km) except Exception as e: module_logger.error( f"Error map_manager.center_map_and_fit_patch: {e}", exc_info=False, ) else: module_logger.warning( "MapCanvasManager missing 'center_map_and_fit_patch' method." ) else: module_logger.warning("Map manager N/A for center and fit patch.") def set_map_track_length(self, length: int): # ... (come prima) module_logger.info( f"Controller: Request to set map track length to {length} points." ) if ( self.main_window and hasattr(self.main_window, "map_manager_instance") and self.main_window.map_manager_instance is not None and hasattr(self.main_window.map_manager_instance, "set_max_track_points") ): try: self.main_window.map_manager_instance.set_max_track_points(length) except Exception as e: module_logger.error( f"Error calling map_manager.set_max_track_points({length}): {e}", exc_info=True, ) if hasattr(self.main_window, "show_error_message"): self.main_window.show_error_message( "Map Configuration Error", f"Failed to set track length on map: {e}", ) else: module_logger.warning( "MapCanvasManager or 'set_max_track_points' N/A to set track length." )