# FlightMonitor/controller/app_controller.py from queue import Queue, Empty as QueueEmpty import threading import tkinter as tk # Usato solo per tk.TclError e TYPE_CHECKING import time 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 # Alias standardizzato from ..utils.logger import get_logger from ..data.storage import DataStorage from ..data.common_models import CanonicalFlightState from typing import List, Optional, Dict, Any, TYPE_CHECKING # Importa le costanti di stato GUI centralizzate from ..utils.gui_utils import ( GUI_STATUS_OK, GUI_STATUS_WARNING, GUI_STATUS_ERROR, GUI_STATUS_FETCHING, GUI_STATUS_UNKNOWN, # GUI_STATUS_PERMANENT_FAILURE # Già importato da opensky_live_adapter come STATUS_PERMANENT_FAILURE, # ma AppController lo usa per la GUI, quindi è bene averlo qui # o assicurarsi che i valori siano identici. # Per ora lo commento qui per evitare ridefinizioni se i valori sono gli stessi. # Se i valori fossero diversi, dovremmo usare alias distinti. ) # Avoid circular import for type hinting if not strictly necessary at runtime if TYPE_CHECKING: from ..gui.main_window import MainWindow from ..map.map_canvas_manager import MapCanvasManager # Module-level logger module_logger = get_logger(__name__) GUI_QUEUE_CHECK_INTERVAL_MS = 150 # Rimane specifico del controller ADAPTER_JOIN_TIMEOUT_SECONDS = 5.0 # Rimane specifico del controller # Le costanti GUI_STATUS_* sono ora importate da gui_utils # Default area size (in km) for setting a monitoring box from a map click DEFAULT_CLICK_AREA_SIZE_KM = ( 50.0 # Potrebbe andare in app_config o map_constants se usato altrove ) class AppController: def __init__(self): """ Initializes the AppController. The main_window instance is set separately via set_main_window. """ 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 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 in AppController: {e}", exc_info=True, ) self.data_storage = None # Assicura che sia None in caso di fallimento module_logger.info("AppController initialized.") def set_main_window(self, main_window_instance: "MainWindow"): """Sets the main window instance and shows initial status or errors.""" self.main_window = main_window_instance module_logger.debug( f"Main window instance ({type(main_window_instance)}) set in AppController." ) 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") ): if not self.data_storage: err_msg = ( "Data storage init failed. Data will not be saved. Check logs." ) self.main_window.update_semaphore_and_status(GUI_STATUS_ERROR, err_msg) else: # data_storage è inizializzato self.main_window.update_semaphore_and_status( GUI_STATUS_OK, "System Initialized. Ready." ) else: module_logger.error( "Main window not set or lacks update_semaphore_and_status during set_main_window." ) def _process_flight_data_queue(self): """ Processes messages from the OpenSkyLiveAdapter's output queue. Runs on the main Tkinter thread via root.after(). """ if not self.flight_data_queue: module_logger.warning( "_process_flight_data_queue: flight_data_queue is None." ) return if not ( self.main_window and hasattr(self.main_window, "root") and self.main_window.root.winfo_exists() ): module_logger.info( "_process_flight_data_queue: Main window or root does not exist. Stopping queue processing." ) self._gui_after_id = None # Assicura che non venga riprogrammato return try: while not self.flight_data_queue.empty(): message: Optional[AdapterMessage] = None try: message = self.flight_data_queue.get(block=False, timeout=0.01) except QueueEmpty: break # No more messages for now except Exception as e_get: module_logger.error( f"Error getting message from queue: {e_get}. Continuing processing...", exc_info=False, ) continue # Prova a processare il prossimo messaggio if ( message is None ): # Dovrebbe essere gestito da QueueEmpty, ma per sicurezza 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 ): # Controlla che il payload non sia None module_logger.debug( f"Received flight data with {len(flight_states_payload)} states. Processing..." ) if self.data_storage: saved_count = 0 for state in flight_states_payload: if not isinstance(state, CanonicalFlightState): module_logger.warning( f"Received non-CanonicalFlightState object: {type(state)}. Skipping." ) 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 as e_storage: module_logger.error( f"Error saving flight state {state.icao24} to storage: {e_storage}", exc_info=False, ) if saved_count > 0: module_logger.info( f"Saved {saved_count} position updates to DB." ) # Logica per la visualizzazione sulla mappa 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", ) # Metodo del MapManager and hasattr( self.main_window, "display_flights_on_canvas" ) # Metodo di MainWindow and self.is_live_monitoring_active and self._active_bounding_box # Assicurati che ci sia un BBox attivo per il contesto ): module_logger.debug( f"Controller: Calling display_flights_on_canvas with {len(flight_states_payload)} states." ) try: self.main_window.display_flights_on_canvas( flight_states_payload, self._active_bounding_box ) except Exception as e_display: module_logger.error( f"Error calling display_flights_on_canvas: {e_display}", exc_info=False, ) # else: (logging di debug già presente per condizioni non soddisfatte) 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 ) as e_tcl_status: # GUI potrebbe essere in chiusura module_logger.warning( f"TclError updating status bar (OK): {e_tcl_status}. GUI likely closing." ) self._gui_after_id = None return # Interrompi il processing della coda except Exception as e_status: module_logger.error( f"Error updating status bar (OK): {e_status}", exc_info=False, ) else: # flight_states_payload è None module_logger.warning( "Received flight_data message with None payload." ) 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 as e_tcl_status: module_logger.warning( f"TclError updating status bar (WARN): {e_tcl_status}. GUI likely closing." ) self._gui_after_id = None return except Exception as e_status: module_logger.error( f"Error updating status bar (WARN): {e_status}", exc_info=False, ) 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}" ) # Rinomino per chiarezza module_logger.info( f"Processing Adapter Status: Code='{status_code}', Message='{gui_message_from_adapter}'" ) gui_status_level_to_set = GUI_STATUS_UNKNOWN # Default action_required_by_controller = ( None # Azioni che il controller deve intraprendere ) if status_code == STATUS_STARTING: gui_status_level_to_set = GUI_STATUS_FETCHING elif status_code == STATUS_FETCHING: gui_status_level_to_set = GUI_STATUS_FETCHING elif status_code == STATUS_RECOVERED: gui_status_level_to_set = GUI_STATUS_OK elif status_code == STATUS_RATE_LIMITED: gui_status_level_to_set = GUI_STATUS_WARNING details = message.get("details", {}) delay = details.get("delay", "N/A") gui_message_from_adapter = ( f"API Rate Limit. Retry in {float(delay):.0f}s." if isinstance(delay, (int, float)) else f"API Rate Limit. Retry: {delay}." ) elif status_code == STATUS_API_ERROR_TEMPORARY: gui_status_level_to_set = GUI_STATUS_WARNING details = message.get("details", {}) err_code = details.get("status_code", "N/A") delay = details.get("delay", "N/A") gui_message_from_adapter = ( f"Temp API Error ({err_code}). Retry in {float(delay):.0f}s." if isinstance(delay, (int, float)) else f"Temp API Error ({err_code}). Retry: {delay}." ) elif status_code == STATUS_PERMANENT_FAILURE: gui_status_level_to_set = GUI_STATUS_ERROR action_required_by_controller = "STOP_MONITORING" elif status_code == STATUS_STOPPED: gui_status_level_to_set = GUI_STATUS_OK # gui_message_from_adapter già impostato 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 as e_tcl_status: module_logger.warning( f"TclError updating status bar ({gui_status_level_to_set}): {e_tcl_status}. GUI likely closing." ) self._gui_after_id = None return except Exception as e_status: module_logger.error( f"Error updating status bar ({gui_status_level_to_set}): {e_status}", exc_info=False, ) if action_required_by_controller == "STOP_MONITORING": module_logger.critical( "Permanent failure from adapter. Triggering controller stop." ) self.stop_live_monitoring( from_error=True ) # Passa from_error=True break # Esci dal while loop della coda else: # Unknown message type module_logger.warning( f"Unknown message type from adapter: '{message_type}'. Msg: {message}" ) if hasattr(self.main_window, "update_semaphore_and_status"): try: self.main_window.update_semaphore_and_status( GUI_STATUS_WARNING, f"Unknown adapter message: {message_type}", ) except tk.TclError as e_tcl_status: module_logger.warning( f"TclError updating status bar (UNKNOWN MSG): {e_tcl_status}. GUI likely closing." ) self._gui_after_id = None return except Exception as e_status: module_logger.error( f"Error updating status bar (UNKNOWN MSG): {e_status}", exc_info=False, ) except Exception as e_message_processing: module_logger.error( f"Error processing adapter message (Type: {message.get('type')}): {e_message_processing}", exc_info=True, ) finally: try: self.flight_data_queue.task_done() except ( Exception ) as e_task_done: # ValueError se chiamato troppe volte, RuntimeError se su coda chiusa module_logger.error( f"Error calling task_done on queue: {e_task_done}", exc_info=False, ) except ( tk.TclError ) as e_tcl_outer: # Errore Tkinter durante il loop principale della coda (es. finestra distrutta) module_logger.warning( f"TclError during adapter queue processing: {e_tcl_outer}. Aborting queue processing.", exc_info=False, ) self._gui_after_id = None return except Exception as e_outer: # Altre eccezioni critiche nel loop module_logger.error( f"Unexpected critical error processing adapter message queue: {e_outer}", exc_info=True, ) if hasattr(self.main_window, "update_semaphore_and_status"): try: self.main_window.update_semaphore_and_status( GUI_STATUS_ERROR, "Critical error processing data. See logs." ) except tk.TclError: pass except Exception: pass # Ignora errori se la GUI sta morendo finally: # Ripianifica il prossimo controllo della coda se ancora attivo e la GUI esiste 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 tk.TclError: # Finestra potrebbe essere sparita module_logger.warning( "TclError scheduling next queue check, window might be gone." ) self._gui_after_id = None except Exception as e_after: module_logger.error( f"Error scheduling next queue check: {e_after}", exc_info=True ) self._gui_after_id = None else: # Se non attivo o GUI non esiste, assicurati che _gui_after_id sia None if ( self._gui_after_id and self.main_window and hasattr(self.main_window, "root") and self.main_window.root.winfo_exists() ): try: self.main_window.root.after_cancel( self._gui_after_id ) # Tenta di cancellare se esiste ancora except tk.TclError: pass except Exception: pass self._gui_after_id = None module_logger.debug( "_process_flight_data_queue: Not rescheduling (monitoring inactive or GUI gone)." ) def start_live_monitoring(self, bounding_box: Dict[str, float]): if not self.main_window: module_logger.error( "Controller: Main window not set. Cannot start live monitoring." ) return if ( not bounding_box ): # Dovrebbe essere validato da MainWindow.get_bounding_box_from_gui err_msg = "Controller: Bounding box is required but was not provided." module_logger.error(err_msg) if hasattr(self.main_window, "_reset_gui_to_stopped_state"): self.main_window._reset_gui_to_stopped_state( "Start failed: Bounding box missing." ) return if not self.data_storage: err_msg = "DataStorage not initialized. Live monitoring cannot start." module_logger.error(f"Controller: {err_msg}") if hasattr(self.main_window, "update_semaphore_and_status"): self.main_window.update_semaphore_and_status( GUI_STATUS_ERROR, err_msg + " Check logs." ) if hasattr(self.main_window, "_reset_gui_to_stopped_state"): self.main_window._reset_gui_to_stopped_state( err_msg ) # Passa messaggio di errore return if self.is_live_monitoring_active: module_logger.warning( "Controller: Live monitoring already active. Start request ignored." ) # Potrebbe essere utile resettare i bottoni GUI se questo stato è raggiunto inaspettatamente if hasattr( self.main_window, "_reset_gui_to_stopped_state" ): # Ma con messaggio appropriato 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 # Salva il BBox attivo # Imposta il BBox target nel MapCanvasManager (disegnerà il box blu) 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, "set_target_bbox") ): try: module_logger.debug( f"Controller instructing map manager to set view for BBox: {bounding_box}" ) self.main_window.map_manager_instance.set_target_bbox(bounding_box) except Exception as e_map_set_bbox: module_logger.error( f"Error instructing map manager to set BBox {bounding_box}: {e_map_set_bbox}", exc_info=True, ) if hasattr(self.main_window, "update_semaphore_and_status"): self.main_window.update_semaphore_and_status( GUI_STATUS_WARNING, "Map update error on start. See logs." ) else: # Se map_manager non è pronto, pulisci la vista if hasattr(self.main_window, "clear_all_views_data"): try: self.main_window.clear_all_views_data() except Exception as e_clear: module_logger.error( f"Error calling clear_all_views_data on start: {e_clear}", exc_info=False, ) # Gestione coda if self.flight_data_queue is None: self.flight_data_queue = Queue(maxsize=100) # Maxsize configurabile? module_logger.debug("Created new flight data queue.") else: # Svuota la coda da messaggi precedenti while not self.flight_data_queue.empty(): try: old_message = self.flight_data_queue.get_nowait() self.flight_data_queue.task_done() module_logger.debug( f"Discarded old message from queue: {old_message.get('type', 'Unknown Type')}" ) except QueueEmpty: break except Exception as e_q_clear: module_logger.warning( f"Error clearing old message from queue: {e_q_clear}" ) break # Gestione thread adattatore precedente adapter_thread_to_stop = self.live_adapter_thread if adapter_thread_to_stop and adapter_thread_to_stop.is_alive(): module_logger.warning( "Controller: Old LiveAdapter thread alive. Attempting stop and join." ) try: adapter_thread_to_stop.stop() if ( self.main_window and hasattr(self.main_window, "root") and self.main_window.root.winfo_exists() ): try: self.main_window.root.update_idletasks() # Processa eventi GUI per permettere al thread di vedere lo stop except Exception: pass # Ignora se la GUI sta chiudendo adapter_thread_to_stop.join(timeout=ADAPTER_JOIN_TIMEOUT_SECONDS) if adapter_thread_to_stop.is_alive(): module_logger.error( f"Controller: Old LiveAdapter thread DID NOT join in time!" ) else: module_logger.info( "Controller: Old LiveAdapter thread joined successfully." ) except Exception as e_stop_join: module_logger.error( f"Error during old adapter stop/join: {e_stop_join}", exc_info=True ) finally: self.live_adapter_thread = None else: module_logger.debug( "Controller: No active LiveAdapter thread to stop or already stopped." ) # Crea e avvia nuovo thread adattatore self.live_adapter_thread = OpenSkyLiveAdapter( output_queue=self.flight_data_queue, bounding_box=self._active_bounding_box, ) self.is_live_monitoring_active = True # Imposta prima di start() self.live_adapter_thread.start() module_logger.info( f"Controller: New live adapter thread '{self.live_adapter_thread.name}' started." ) # Cancella e riprogramma il polling della coda GUI if ( self._gui_after_id and self.main_window and hasattr(self.main_window, "root") and self.main_window.root.winfo_exists() ): try: self.main_window.root.after_cancel(self._gui_after_id) module_logger.debug( "Controller: Cancelled previous GUI queue check callback." ) except Exception: module_logger.debug( "Controller: Error cancelling previous GUI queue check (might not exist)." ) finally: self._gui_after_id = None if ( self.main_window and hasattr(self.main_window, "root") and self.main_window.root.winfo_exists() ): self._gui_after_id = self.main_window.root.after( 100, self._process_flight_data_queue ) # Breve ritardo iniziale module_logger.info("Controller: GUI queue polling scheduled.") else: # Fallimento critico se la GUI non esiste per il polling module_logger.error( "Controller: Cannot schedule GUI queue polling: MainWindow or root does not exist. Aborting live monitoring." ) self.is_live_monitoring_active = False if self.live_adapter_thread and self.live_adapter_thread.is_alive(): try: self.live_adapter_thread.stop() except Exception as e_stop_fail: module_logger.error(f"Error trying to stop adapter: {e_stop_fail}") # Resetta GUI a stato fermo 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): # Controlla se c'è qualcosa da fermare if not self.is_live_monitoring_active and not ( from_error and self.live_adapter_thread and self.live_adapter_thread.is_alive() ): module_logger.debug( f"Controller: Stop requested but live monitoring/adapter not active (from_error={from_error}). Ignoring." ) # Assicurati che la GUI sia nello stato fermo se chiamata inaspettatamente 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 or not started." ) return module_logger.info( f"Controller: Stopping live monitoring (from_error={from_error})." ) self.is_live_monitoring_active = ( False # Segnala immediatamente che non siamo più attivi ) # Cancella il polling della coda GUI if ( self._gui_after_id and self.main_window and hasattr(self.main_window, "root") and self.main_window.root.winfo_exists() ): try: self.main_window.root.after_cancel(self._gui_after_id) module_logger.debug("Controller: Cancelled GUI queue check callback.") except Exception: module_logger.debug( "Controller: Error cancelling GUI queue check (might not exist or root gone)." ) finally: self._gui_after_id = None # Ferma il thread adattatore adapter_thread_to_stop = self.live_adapter_thread if adapter_thread_to_stop and adapter_thread_to_stop.is_alive(): module_logger.debug( f"Controller: Signaling LiveAdapter thread ({adapter_thread_to_stop.name}) to stop." ) try: adapter_thread_to_stop.stop() if ( self.main_window and hasattr(self.main_window, "root") and self.main_window.root.winfo_exists() ): try: self.main_window.root.update_idletasks() except Exception: pass module_logger.debug( f"Controller: Waiting for LiveAdapter thread ({adapter_thread_to_stop.name}) to join..." ) adapter_thread_to_stop.join(timeout=ADAPTER_JOIN_TIMEOUT_SECONDS) if adapter_thread_to_stop.is_alive(): module_logger.error( f"Controller: LiveAdapter thread ({adapter_thread_to_stop.name}) did NOT join in time!" ) else: module_logger.info( f"Controller: LiveAdapter thread ({adapter_thread_to_stop.name}) joined successfully." ) except Exception as e_stop_join: module_logger.error( f"Error during adapter stop/join sequence: {e_stop_join}", exc_info=True, ) finally: self.live_adapter_thread = ( None # Assicura che sia None dopo il tentativo di stop ) else: module_logger.debug( "Controller: No active LiveAdapter thread to stop or already stopped." ) self.live_adapter_thread = None # Assicura che sia None # Processa messaggi finali dalla coda (es. STATUS_STOPPED) if ( self.flight_data_queue and self.main_window and hasattr(self.main_window, "root") and self.main_window.root.winfo_exists() ): module_logger.debug( "Controller: Processing final messages from adapter queue post-join..." ) try: # Chiamata singola a _process_flight_data_queue senza riprogrammazione per svuotare la coda # Deve essere eseguita nel thread GUI, quindi non direttamente qui se questo è chiamato da altro thread. # Assumendo che stop_live_monitoring sia chiamato dal thread GUI (es. bottone stop). # Se no, dovremmo usare root.after(0, self._process_flight_data_queue) e gestire la non-riprogrammazione. # Per ora, lo chiamo direttamente, ma poi _process_flight_data_queue NON deve riprogrammare se is_live_monitoring_active è False. self._process_flight_data_queue() # Questo si occuperà dei messaggi rimanenti module_logger.debug( "Controller: Requested final processing of adapter queue." ) except Exception as e_final_loop: module_logger.error( f"Controller: Unexpected error in final queue processing request: {e_final_loop}", exc_info=True, ) else: module_logger.debug( "Controller: No flight data queue or GUI to process after stop." ) # Pulisci dati dalla mappa e altre viste if hasattr(self.main_window, "clear_all_views_data"): try: self.main_window.clear_all_views_data() except Exception as e_clear_views: module_logger.error( f"Error calling clear_all_views_data after stop: {e_clear_views}", exc_info=False, ) # Resetta i controlli GUI allo stato fermo if hasattr(self.main_window, "_reset_gui_to_stopped_state"): stop_status_msg = "Monitoring stopped." if from_error: stop_status_msg = "Monitoring stopped due to an error." try: self.main_window._reset_gui_to_stopped_state(stop_status_msg) except tk.TclError: module_logger.warning( "TclError resetting GUI state after stop. GUI likely gone." ) except Exception as e_reset_gui: module_logger.error( f"Error resetting GUI state after stop: {e_reset_gui}", exc_info=True, ) self._active_bounding_box = None # Resetta il BBox attivo module_logger.info( "Controller: Live monitoring shutdown sequence fully completed." ) def on_application_exit(self): """Performs cleanup when the application is exiting.""" module_logger.info( "Controller: Application exit requested. Cleaning up resources." ) 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: module_logger.debug( "Controller: Live monitoring/adapter active during app exit, stopping it." ) self.stop_live_monitoring(from_error=False) # Non è un errore dell'adapter else: module_logger.debug( "Controller: Live monitoring/adapter not active or already stopped during app exit." ) if self.data_storage: module_logger.debug( "Controller: Closing DataStorage connection during app exit." ) 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 module_logger.info("Controller: Cleanup on application exit finished.") # --- History Mode (Placeholders) --- def start_history_monitoring(self): 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." module_logger.error(f"Controller: {err_msg}") 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 module_logger.info("Controller: History monitoring started (placeholder).") 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): if not self.main_window: return module_logger.info("Controller: History monitoring stopped (placeholder).") if hasattr(self.main_window, "update_semaphore_and_status"): self.main_window.update_semaphore_and_status( GUI_STATUS_OK, "History monitoring stopped." ) # --- MAP INTERACTION METHODS (chiamati da MapCanvasManager o MainWindow) --- def on_map_left_click( self, latitude: float, longitude: float, screen_x: int, screen_y: int ): """Handles a left-click event on the map canvas.""" 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, ) # Importa qui per evitare dipendenza a livello di modulo se non necessario 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 not available for left click." ) except Exception as e_dms: module_logger.warning( f"Error calculating 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 ) else: module_logger.warning( "Main window not available to update clicked map info." ) def on_map_right_click( self, latitude: float, longitude: float, screen_x: int, screen_y: int ): """Handles a right-click event on the map canvas.""" module_logger.debug( f"Controller: Map right-clicked at Geo ({latitude:.5f}, {longitude:.5f})" ) if self.main_window: # Aggiorna info click prima di mostrare il menu 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 not available for right click." ) except Exception as e_dms: module_logger.warning( f"Error calculating 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 panel (right click): {e_tcl}. GUI closing." ) except Exception as e_update: module_logger.error( f"Error updating map clicked info panel (right click): {e_update}", exc_info=False, ) # Richiedi a MainWindow di mostrare il menu contestuale 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 not available for right click handling.") def on_map_context_menu_request( self, latitude: float, longitude: float, screen_x: int, screen_y: int ): """Receives context menu request from MapCanvasManager and delegates to MainWindow.""" 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 not available to show context menu.") def recenter_map_at_coords(self, lat: float, lon: float): """Requests MapCanvasManager to recenter the map view.""" 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 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 calling map_manager.recenter_map_at_coords: {e_recenter}", exc_info=False, ) if hasattr(self.main_window, "show_error_message"): self.main_window.show_error_message( "Map Error", "Failed to recenter map." ) 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, ): """Requests MapCanvasManager to set a BBox around coordinates.""" 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 if hasattr(map_manager, "set_bbox_around_coords"): try: map_manager.set_bbox_around_coords( center_lat, center_lon, area_size_km ) # MapManager calcolerà e chiamerà set_target_bbox + update_bbox_gui_fields except Exception as e_set_bbox: module_logger.error( f"Error calling map_manager.set_bbox_around_coords: {e_set_bbox}", exc_info=False, ) if hasattr(self.main_window, "show_error_message"): self.main_window.show_error_message( "Map Error", "Failed to set monitoring area." ) 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]): """Updates the BBox input fields in the GUI (called by MapCanvasManager).""" 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 not available to update BBox GUI fields." ) def update_general_map_info(self): """Called by MapCanvasManager to update general map info in GUI.""" 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." ) # Se map_manager non è pronto, potremmo voler aggiornare la GUI con N/A 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 tk.TclError: pass except Exception: pass return map_manager: "MapCanvasManager" = self.main_window.map_manager_instance 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: {map_info}") zoom, map_geo_bounds, target_bbox_input, flight_count = ( map_info.get("zoom"), map_info.get("map_geo_bounds"), map_info.get("target_bbox_input"), map_info.get("flight_count"), ) map_size_km_w, map_size_km_h = map_info.get( "map_size_km_w" ), 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, 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, ) else: module_logger.warning( "MainWindow missing 'update_general_map_info_display' method." ) except Exception as e_get_info: module_logger.error( f"Error getting map info from map manager: {e_get_info}", exc_info=False, ) if hasattr( self.main_window, "update_general_map_info_display" ): # Fallback con N/A 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 tk.TclError: pass except Exception: pass else: module_logger.warning( "MapCanvasManager missing 'get_current_map_info' method." ) if self.main_window and hasattr( self.main_window, "update_general_map_info_display" ): # Fallback con N/A 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 tk.TclError: pass except Exception: pass # --- NUOVI METODI PER CONTROLLI MAPPA DA GUI --- def map_zoom_in(self): """Handles 'Zoom In' request from GUI.""" 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 if hasattr(map_manager, "zoom_in_at_center"): try: map_manager.zoom_in_at_center() except Exception as e: module_logger.error( f"Error calling 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 not available for zoom in.") def map_zoom_out(self): """Handles 'Zoom Out' request from GUI.""" 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 if hasattr(map_manager, "zoom_out_at_center"): try: map_manager.zoom_out_at_center() except Exception as e: module_logger.error( f"Error calling 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 not available for zoom out.") def map_pan_direction(self, direction: str): """Handles 'Pan' request from GUI.""" 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 if hasattr( map_manager, "pan_map_fixed_step" ): # Assumiamo un metodo pan_map_fixed_step(direction) try: map_manager.pan_map_fixed_step(direction) except Exception as e: module_logger.error( f"Error calling 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 not available for pan {direction}.") def map_center_on_coords_and_fit_patch( self, lat: float, lon: float, patch_size_km: float ): """Handles 'Center & Fit Patch' request from GUI.""" 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 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 calling 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 not available for center and fit patch.")