diff --git a/flightmonitor/controller/app_controller.py b/flightmonitor/controller/app_controller.py index 466e813..86b0167 100644 --- a/flightmonitor/controller/app_controller.py +++ b/flightmonitor/controller/app_controller.py @@ -8,6 +8,7 @@ import subprocess from queue import Queue, Empty as QueueEmpty from typing import List, Optional, Dict, Any, TYPE_CHECKING from tkinter import messagebox, simpledialog +from datetime import timezone,datetime # Import dei processori e degli adapter from flightmonitor.controller.historical_data_processor import HistoricalDataProcessor @@ -613,10 +614,24 @@ class AppController: self.stop_live_monitoring() self.stop_historical_download() + + if self.playback_data_queue: + while not self.playback_data_queue.empty(): + try: + self.playback_data_queue.get_nowait() + except QueueEmpty: + break + module_logger.info("Playback data queue cleared before starting new session.") date_str = session_info.get("date_str") start_ts = session_info.get("start_timestamp") end_ts = session_info.get("end_timestamp") + + module_logger.info( + f"Controller: Validating playback session info. " + f"Start TS: {start_ts} ({datetime.fromtimestamp(start_ts, timezone.utc) if start_ts else 'N/A'}), " + f"End TS: {end_ts} ({datetime.fromtimestamp(end_ts, timezone.utc) if end_ts else 'N/A'})" + ) bbox = { "lat_min": session_info.get("lat_min"), "lon_min": session_info.get("lon_min"), @@ -1164,4 +1179,70 @@ class AppController: self.main_window.map_manager_instance.tile_manager.clear_entire_service_cache() return True module_logger.warning("Map tile manager not available to clear cache.") - return False \ No newline at end of file + return False + + def seek_playback_to_time(self, timestamp: float): + """Seeks the playback to a specific time and forces a single frame update.""" + if self.playback_adapter_thread and self.is_playback_active: + self.pause_playback() # Ensure playback is paused + self.playback_adapter_thread.seek_to_time(timestamp) + + # After seeking, force the adapter to push one frame of data for that timestamp + # to the queue, so the GUI updates to the new position. + self.playback_adapter_thread.force_update_for_timestamp(timestamp) + module_logger.info(f"Playback seeked to {timestamp} and a frame update was forced.") + + def reset_playback(self): + """Resets the CURRENTLY ACTIVE playback to its starting point.""" + if self.playback_adapter_thread and self.is_playback_active: + self.playback_adapter_thread.reset() + self.pause_playback() # Mette in pausa + # Forza un aggiornamento al primo frame per resettare la mappa + start_ts = self.playback_adapter_thread.start_ts + self.playback_adapter_thread.force_update_for_timestamp(start_ts) + module_logger.info("Active playback session has been reset.") + + def reset_playback_to_session(self, session_info: Dict[str, Any]): + """ + Resets the GUI to the start of a given session, fetching and displaying + the initial frame of data without starting the playback loop. + """ + if self.is_playback_active: + self.stop_playback() # Se c'era un altro playback, lo ferma + + start_ts = session_info.get("start_timestamp") + date_str = session_info.get("date_str") + if start_ts is None or not date_str or not self.data_storage: + return + + # Aggiorna la GUI (orologio e timeline) + if self.main_window: + playback_panel = self.main_window.function_notebook_panel.playback_panel + if playback_panel: + playback_panel.update_virtual_clock(start_ts) + end_ts = session_info.get("end_timestamp", start_ts) + playback_panel.update_timeline(start_ts, start_ts, end_ts) + + # Preleva il primo "frame" di dati direttamente dallo storage + # per mostrare lo stato iniziale sulla mappa. + try: + initial_frame_states = self.data_storage.get_positions_in_range( + date_str, start_ts, start_ts + 1.0 # Dati del primo secondo + ) + + if self.main_window and self.main_window.map_manager_instance: + # Passa i dati al map manager per il disegno + self.main_window.map_manager_instance.update_playback_frame( + initial_frame_states, start_ts + ) + else: + # Se la mappa non è pronta, pulisce comunque la vista + if self.main_window: + self.main_window.clear_all_views_data() + + except Exception as e: + module_logger.error(f"Failed to fetch initial frame for session reset: {e}", exc_info=True) + if self.main_window: + self.main_window.clear_all_views_data() + + module_logger.info(f"Playback view reset to the start of session ID {session_info.get('scan_id')}.") \ No newline at end of file diff --git a/flightmonitor/data/playback_adapter.py b/flightmonitor/data/playback_adapter.py index a0d3dbe..17e1600 100644 --- a/flightmonitor/data/playback_adapter.py +++ b/flightmonitor/data/playback_adapter.py @@ -105,6 +105,15 @@ class PlaybackAdapter(BaseLiveDataAdapter): f"{self.name}: Thread starting playback for session on {self.session_date_str} " f"from {self.start_ts:.0f} to {self.end_ts:.0f}." ) + + if self.start_ts >= self.end_ts: + err_msg = f"Invalid time range: start_ts ({self.start_ts}) is not less than end_ts ({self.end_ts}). Stopping." + module_logger.error(f"{self.name}: {err_msg}") + self._send_status_to_queue(STATUS_PERMANENT_FAILURE, err_msg) + # Invia anche un messaggio di stop normale per chiudere pulitamente la UI + self._send_status_to_queue(STATUS_STOPPED, "Playback failed due to invalid time range.") + return + self._send_status_to_queue(STATUS_STARTING, "Playback session started.") if not self.data_storage: @@ -175,4 +184,46 @@ class PlaybackAdapter(BaseLiveDataAdapter): self._send_status_to_queue(STATUS_STOPPED, "Playback finished.") else: module_logger.info(f"{self.name}: Playback stopped by user.") - self._send_status_to_queue(STATUS_STOPPED, "Playback stopped.") \ No newline at end of file + self._send_status_to_queue(STATUS_STOPPED, "Playback stopped.") + + def seek_to_time(self, timestamp: float): + """Jumps the playback to a specific timestamp within the session.""" + with self._control_lock: + # Clamp the timestamp to be within the valid session range + self._virtual_time = max(self.start_ts, min(timestamp, self.end_ts)) + module_logger.info(f"{self.name}: Seeking playback to timestamp {self._virtual_time:.0f}.") + + def reset(self): + """Resets the playback to the beginning of the session.""" + with self._control_lock: + self._virtual_time = self.start_ts + module_logger.info(f"{self.name}: Playback reset to the beginning.") + + def force_update_for_timestamp(self, timestamp: float): + """ + Queries all data from the session start up to the specified timestamp + and puts it into the output queue as a single "frame". + """ + if not self.data_storage: + module_logger.error(f"{self.name}: DataStorage not available. Cannot force update.") + return + + module_logger.info(f"{self.name}: Forcing data update for all history up to timestamp {timestamp:.0f}.") + try: + # Query for ALL data from the beginning of the session up to the seek time. + all_states_until_seek = self.data_storage.get_positions_in_range( + self.session_date_str, self.start_ts, timestamp + ) + + # Il "payload" ora contiene la storia completa fino a quel punto. + # Il MapCanvasManager userà questi dati per costruire le tracce e mostrare l'ultimo stato. + payload = {"canonical": all_states_until_seek, "raw_json": "{}"} + message: AdapterMessage = { + "type": MSG_TYPE_FLIGHT_DATA, + "timestamp": timestamp, # L'orologio si sincronizza con l'istante scelto + "payload": payload, + } + self.output_queue.put(message) + + except Exception as e: + module_logger.error(f"{self.name}: Error during forced data update: {e}", exc_info=True) \ No newline at end of file diff --git a/flightmonitor/gui/panels/playback_panel.py b/flightmonitor/gui/panels/playback_panel.py index fa26067..2016971 100644 --- a/flightmonitor/gui/panels/playback_panel.py +++ b/flightmonitor/gui/panels/playback_panel.py @@ -24,6 +24,7 @@ class PlaybackPanel: # --- Internal State --- self._session_data_map: Dict[str, Dict[str, Any]] = {} + self._current_session_info: Optional[Dict[str, Any]] = None # --- Tkinter Variables --- self.selected_date_var = tk.StringVar() @@ -37,6 +38,7 @@ class PlaybackPanel: self.play_button: Optional[ttk.Button] = None self.pause_button: Optional[ttk.Button] = None self.stop_button: Optional[ttk.Button] = None + self.reset_button: Optional[ttk.Button] = None self.timeline_slider: Optional[ttk.Scale] = None self._build_ui() @@ -47,7 +49,6 @@ class PlaybackPanel: container = ttk.Frame(self.parent_frame, padding="10 10 10 10") container.pack(fill=tk.BOTH, expand=True) - # --- Session Selection Frame --- session_frame = ttk.LabelFrame( container, text="Playback Session Selection", padding=10 ) @@ -80,6 +81,7 @@ class PlaybackPanel: selectmode="browse", ) self.sessions_treeview.grid(row=0, column=0, sticky="nsew") + self.sessions_treeview.bind("<>", self._on_session_selected_in_tree) self.sessions_treeview.heading("start_time", text="Start Time (UTC)") self.sessions_treeview.column("start_time", width=130, anchor=tk.W) @@ -96,12 +98,10 @@ class PlaybackPanel: scrollbar.grid(row=0, column=1, sticky="ns") self.sessions_treeview.configure(yscrollcommand=scrollbar.set) - # --- Playback Controls Frame --- controls_frame = ttk.LabelFrame(container, text="Playback Controls", padding=10) controls_frame.pack(fill=tk.BOTH, expand=True, pady=(10, 0)) controls_frame.columnconfigure(1, weight=1) - # Timeline and Clock clock_font = tkFont.Font(family="Courier New", size=14, weight="bold") virtual_clock_label = ttk.Label( controls_frame, @@ -122,8 +122,10 @@ class PlaybackPanel: self.timeline_slider.grid( row=1, column=0, columnspan=4, sticky="ew", pady=(0, 10) ) + self.timeline_slider.bind("", self._on_timeline_press) + self.timeline_slider.bind("", self._on_timeline_drag) + self.timeline_slider.bind("", self._on_timeline_release) - # Buttons and Speed Control button_frame = ttk.Frame(controls_frame) button_frame.grid(row=2, column=0, columnspan=4) @@ -142,6 +144,11 @@ class PlaybackPanel: ) self.stop_button.pack(side=tk.LEFT, padx=5) + self.reset_button = ttk.Button( + button_frame, text="⟲ Reset", command=self._on_reset, state=tk.DISABLED + ) + self.reset_button.pack(side=tk.LEFT, padx=(10, 5)) + ttk.Label(button_frame, text="Speed:").pack(side=tk.LEFT, padx=(15, 2)) speed_options = ["0.5", "1.0", "2.0", "4.0", "8.0"] speed_combobox = ttk.Combobox( @@ -149,6 +156,7 @@ class PlaybackPanel: textvariable=self.playback_speed_var, values=speed_options, width=5, + state="readonly" ) speed_combobox.pack(side=tk.LEFT) speed_combobox.bind("<>", self._on_speed_change) @@ -157,8 +165,7 @@ class PlaybackPanel: self.update_available_dates() def update_available_dates(self): - if not self.controller: - return + if not self.controller: return dates = self.controller.get_available_recording_dates() if self.date_combobox: self.date_combobox["values"] = dates @@ -183,8 +190,7 @@ class PlaybackPanel: def update_sessions_for_date(self, date_str: str): self._clear_sessions_treeview() - if not self.controller or not self.sessions_treeview: - return + if not self.controller or not self.sessions_treeview: return sessions = self.controller.get_sessions_for_date(date_str) if not sessions: @@ -193,39 +199,25 @@ class PlaybackPanel: for session in sessions: scan_id = session.get("scan_id") - if scan_id is None: - continue + if scan_id is None: continue self._session_data_map[str(scan_id)] = session try: - start_ts = session.get("start_timestamp", 0) - end_ts = session.get("end_timestamp", 0) - - start_dt = datetime.fromtimestamp(start_ts, timezone.utc) - end_dt = datetime.fromtimestamp(end_ts, timezone.utc) - - duration_delta = end_dt - start_dt - duration_str = str(duration_delta).split(".")[0] - - area_desc = ( - f"BBox [{session.get('lat_min', 0):.2f}, {session.get('lon_min', 0):.2f}]" - ) - + start_ts, end_ts = session.get("start_timestamp", 0), session.get("end_timestamp", 0) + duration_delta = timedelta(seconds=end_ts - start_ts) + area_desc = f"BBox [{session.get('lat_min', 0):.2f}, {session.get('lon_min', 0):.2f}]" self.sessions_treeview.insert( - "", - "end", + "", "end", iid=str(scan_id), values=( - start_dt.strftime("%H:%M:%S"), - duration_str, + datetime.fromtimestamp(start_ts, timezone.utc).strftime("%H:%M:%S"), + str(duration_delta).split(".")[0], session.get("type", "Unknown"), area_desc, ), - iid=str(scan_id), ) except Exception as e: module_logger.error(f"Failed to process session for display: {e}") - # Select the first item by default and enable play button children = self.sessions_treeview.get_children() if children: self.sessions_treeview.selection_set(children[0]) @@ -235,86 +227,171 @@ class PlaybackPanel: self.play_button.config(state=tk.DISABLED) def _on_play(self): - if not self.controller or not self.sessions_treeview: - return - + if not self.controller or not self.sessions_treeview: return selected_items = self.sessions_treeview.selection() if not selected_items: messagebox.showwarning("No Selection", "Please select a session to play.", parent=self.parent_frame) return session_id = selected_items[0] - session_info = self._session_data_map.get(session_id) + self._current_session_info = self._session_data_map.get(session_id) # <-- Riga importante - if not session_info: + if not self._current_session_info: messagebox.showerror("Error", "Could not find data for the selected session.", parent=self.parent_frame) return - self.controller.start_playback(session_info) + self.controller.start_playback(self._current_session_info) def _on_pause(self): - if self.controller: - self.controller.pause_playback() + if self.controller: self.controller.pause_playback() def _on_stop(self): - if self.controller: - self.controller.stop_playback() + if self.controller: self.controller.stop_playback() + self._current_session_info = None # <-- Riga importante + + def _on_reset(self): + # Se il playback è attivo, lo resetta. + # Se non è attivo, l'utente dovrebbe selezionare una riga per resettare la vista. + if self.controller and self.controller.is_playback_active: + self.controller.reset_playback() + elif self.controller and self._current_session_info: + # Se non è attivo ma una sessione è selezionata, resetta comunque la vista. + self.controller.reset_playback_to_session(self._current_session_info) def _on_speed_change(self, event=None): if self.controller: try: - speed = float(self.playback_speed_var.get()) - self.controller.set_playback_speed(speed) - except (ValueError, TypeError): - module_logger.warning("Invalid playback speed value.") + self.controller.set_playback_speed(float(self.playback_speed_var.get())) + except (ValueError, TypeError): pass + + def _on_timeline_press(self, event=None): + if self.controller and self.controller.is_playback_active: + self.controller.pause_playback() + + def _on_timeline_release(self, event=None): + if not self.controller or not self.controller.is_playback_active or not self._current_session_info: + return + + start_ts = self._current_session_info.get("start_timestamp", 0) + end_ts = self._current_session_info.get("end_timestamp", 0) + duration = end_ts - start_ts + + if duration > 0: + seek_percentage = self.timeline_var.get() / 100.0 + seek_timestamp = start_ts + (duration * seek_percentage) + self.controller.seek_playback_to_time(seek_timestamp) def set_controls_state(self, is_playing: bool, is_paused: bool = False): if not self.parent_frame.winfo_exists(): return - - # Controls disabled during playback - scan_and_select_state = tk.DISABLED if is_playing else tk.NORMAL - - if self.date_combobox: - self.date_combobox.config(state="readonly" if not is_playing else tk.DISABLED) - # Also disable scan button and treeview - # ... (assuming direct widget references or iterating children) + # Stato per i controlli di selezione della sessione + session_selection_state = tk.DISABLED if is_playing else tk.NORMAL + session_selection_readonly_state = tk.DISABLED if is_playing else "readonly" + + if self.date_combobox: + self.date_combobox.config(state=session_selection_readonly_state) + if self.sessions_treeview: + # Per i Treeview, non c'è uno stato 'readonly', ma possiamo intercettare gli eventi + # per prevenire la selezione. Per ora, lo lasciamo attivo per semplicità visiva. + # In alternativa, potremmo disabilitarlo ma diventerebbe grigio. + pass + + # Trova il bottone "Scan for Recordings" e impostane lo stato + # Questo assume che il bottone sia un figlio diretto del frame del notebook o di un suo figlio + for child in self.parent_frame.winfo_children(): + if isinstance(child, ttk.Frame): # Cerca dentro i frame + for sub_child in child.winfo_children(): + if isinstance(sub_child, ttk.Button) and "scan" in sub_child.cget("text").lower(): + sub_child.config(state=session_selection_state) + + + # Stato per i controlli di playback (timeline, pulsanti) + playback_controls_state = tk.NORMAL if is_playing else tk.DISABLED + + if self.timeline_slider: + self.timeline_slider.config(state=playback_controls_state) + + if self.stop_button: + self.stop_button.config(state=playback_controls_state) + + is_session_selected = bool(self._current_session_info) + reset_state = tk.NORMAL if is_session_selected else tk.DISABLED + if self.reset_button: + self.reset_button.config(state=reset_state) + + # Stato specifico per Play/Pause if is_playing: if is_paused: - self.play_button.config(state=tk.NORMAL, text="▶ Resume") - self.play_button.configure(command=self._on_resume) + if self.play_button: + self.play_button.config(state=tk.NORMAL, text="▶ Resume") + self.play_button.configure(command=self._on_resume) + if self.pause_button: + self.pause_button.config(state=tk.DISABLED) + else: # Playing + if self.play_button: + self.play_button.config(state=tk.DISABLED, text="▶ Play") + if self.pause_button: + self.pause_button.config(state=tk.NORMAL) + else: # Stopped + if self.play_button: + self.play_button.config(state=tk.NORMAL, text="▶ Play") + self.play_button.configure(command=self._on_play) + if self.pause_button: self.pause_button.config(state=tk.DISABLED) - self.stop_button.config(state=tk.NORMAL) - else: # Playing - self.play_button.config(state=tk.DISABLED, text="▶ Play") - self.pause_button.config(state=tk.NORMAL) - self.stop_button.config(state=tk.NORMAL) - else: # Stopped - self.play_button.config(state=tk.NORMAL, text="▶ Play") - self.play_button.configure(command=self._on_play) - self.pause_button.config(state=tk.DISABLED) - self.stop_button.config(state=tk.DISABLED) def _on_resume(self): - if self.controller: - self.controller.resume_playback() + if self.controller: self.controller.resume_playback() def update_virtual_clock(self, timestamp: float): - if not self.parent_frame.winfo_exists(): - return + if not self.parent_frame.winfo_exists(): return try: - dt_object = datetime.fromtimestamp(timestamp, timezone.utc) - self.virtual_clock_var.set(dt_object.strftime("%H:%M:%S")) - except (ValueError, TypeError): - self.virtual_clock_var.set("Invalid Time") + self.virtual_clock_var.set(datetime.fromtimestamp(timestamp, timezone.utc).strftime("%H:%M:%S")) + except (ValueError, TypeError): pass def update_timeline(self, current_ts: float, start_ts: float, end_ts: float): - if not self.parent_frame.winfo_exists() or not self.timeline_slider: - return - + if not self.parent_frame.winfo_exists() or not self.timeline_slider: return if (end_ts - start_ts) > 0: - progress_percent = ((current_ts - start_ts) / (end_ts - start_ts)) * 100 - self.timeline_var.set(progress_percent) + self.timeline_var.set(((current_ts - start_ts) / (end_ts - start_ts)) * 100) else: - self.timeline_var.set(100) \ No newline at end of file + self.timeline_var.set(100) + + def _on_timeline_drag(self, event=None): + """Updates the virtual clock in real-time as the user drags the timeline.""" + if not self._current_session_info: + return + + start_ts = self._current_session_info.get("start_timestamp", 0) + end_ts = self._current_session_info.get("end_timestamp", 0) + duration = end_ts - start_ts + + if duration > 0: + # Get the current value of the slider as it's being dragged + seek_percentage = self.timeline_var.get() / 100.0 + current_drag_timestamp = start_ts + (duration * seek_percentage) + # Update only the clock display, do not send any commands yet + self.update_virtual_clock(current_drag_timestamp) + + def _on_session_selected_in_tree(self, event=None): + """Called when the user selects a different session in the treeview.""" + if not self.sessions_treeview: return + + selected_items = self.sessions_treeview.selection() + if not selected_items: + self._current_session_info = None + if self.reset_button: self.reset_button.config(state=tk.DISABLED) + if self.play_button: self.play_button.config(state=tk.DISABLED) + return + + session_id = selected_items[0] + self._current_session_info = self._session_data_map.get(session_id) + + if self._current_session_info: + # Abilita i pulsanti Play e Reset + if self.play_button: self.play_button.config(state=tk.NORMAL) + if self.reset_button: self.reset_button.config(state=tk.NORMAL) + # Resetta la visualizzazione della mappa e l'orologio all'inizio della sessione selezionata + self.controller.reset_playback_to_session(self._current_session_info) + else: + if self.reset_button: self.reset_button.config(state=tk.DISABLED) + if self.play_button: self.play_button.config(state=tk.DISABLED) \ No newline at end of file diff --git a/flightmonitor/map/map_canvas_manager.py b/flightmonitor/map/map_canvas_manager.py index 22f4c45..9251a68 100644 --- a/flightmonitor/map/map_canvas_manager.py +++ b/flightmonitor/map/map_canvas_manager.py @@ -123,6 +123,7 @@ class MapCanvasManager: # --- Data for Drawing --- self._current_flights_to_display_gui: List[CanonicalFlightState] = [] self._active_aircraft_states: Dict[str, CanonicalFlightState] = {} + self._last_playback_timestamp: float = 0.0 self.flight_tracks_gui: Dict[str, deque] = {} self.max_track_points: int = app_config.DEFAULT_TRACK_HISTORY_POINTS self._map_data_lock: threading.Lock = threading.Lock() @@ -507,22 +508,56 @@ class MapCanvasManager: self.flight_tracks_gui[state.icao24].append((state.latitude, state.longitude, state.timestamp)) self._request_overlay_render() - def update_playback_frame(self, new_flight_states: List[CanonicalFlightState], virtual_timestamp: float): + def update_playback_frame(self, frame_flight_states: List[CanonicalFlightState], virtual_timestamp: float): + """ + Updates the map for a playback frame. Can handle a single tick or a full history refresh on seek. + """ with self._map_data_lock: - timed_out_icaos = {icao for icao, state in self._active_aircraft_states.items() if virtual_timestamp - state.last_contact_timestamp > AIRCRAFT_VISIBILITY_TIMEOUT_SECONDS} - for icao in timed_out_icaos: - del self._active_aircraft_states[icao] - if icao in self.flight_tracks_gui: del self.flight_tracks_gui[icao] - - for state in new_flight_states: - state.last_contact_timestamp = virtual_timestamp # Update last seen time for playback + # Se il nuovo timestamp è inferiore all'ultimo, è un "seek" all'indietro. + # Dobbiamo resettare completamente lo stato per ricostruire la cronologia. + is_seek_event = virtual_timestamp < self._last_playback_timestamp + + if is_seek_event: + logger.debug("Playback seek detected. Resetting active states and tracks.") + self._active_aircraft_states.clear() + self.flight_tracks_gui.clear() + + # Processa tutti gli stati ricevuti per questo "frame" + # Durante un tick normale, sarà una piccola lista. Durante un seek, sarà l'intera storia. + for state in frame_flight_states: + # Aggiorna lo stato più recente dell'aereo self._active_aircraft_states[state.icao24] = state + + # Aggiungi il punto alla traccia if state.icao24 not in self.flight_tracks_gui: self.flight_tracks_gui[state.icao24] = deque(maxlen=self.max_track_points + 5) + if state.latitude is not None and state.longitude is not None and state.timestamp is not None: - self.flight_tracks_gui[state.icao24].append((state.latitude, state.longitude, state.timestamp)) + self.flight_tracks_gui[state.icao24].append( + (state.latitude, state.longitude, state.timestamp) + ) + + # Rimuovi aerei che non sono più attivi (importante per il tick normale) + # Durante un seek, questo non farà nulla perché abbiamo appena pulito _active_aircraft_states + active_icaos_in_frame = {s.icao24 for s in frame_flight_states} + current_active_icaos = set(self._active_aircraft_states.keys()) + + # Se non è un seek, possiamo fare il pruning dei velivoli scaduti + if not is_seek_event: + timed_out_icaos = { + icao for icao, state in self._active_aircraft_states.items() + if virtual_timestamp - state.last_contact_timestamp > AIRCRAFT_VISIBILITY_TIMEOUT_SECONDS + } + if timed_out_icaos: + for icao in timed_out_icaos: + if icao in self._active_aircraft_states: del self._active_aircraft_states[icao] + if icao in self.flight_tracks_gui: del self.flight_tracks_gui[icao] + # L'elenco degli aerei da disegnare è lo stato finale degli aerei attivi self._current_flights_to_display_gui = list(self._active_aircraft_states.values()) + self._last_playback_timestamp = virtual_timestamp + + # Richiedi un aggiornamento dell'overlay con le tracce e posizioni appena calcolate self._request_overlay_render() def get_current_map_info(self) -> Dict[str, Any]: