fix playback command
This commit is contained in:
parent
306fdab18e
commit
3f3cf56750
@ -8,6 +8,7 @@ import subprocess
|
|||||||
from queue import Queue, Empty as QueueEmpty
|
from queue import Queue, Empty as QueueEmpty
|
||||||
from typing import List, Optional, Dict, Any, TYPE_CHECKING
|
from typing import List, Optional, Dict, Any, TYPE_CHECKING
|
||||||
from tkinter import messagebox, simpledialog
|
from tkinter import messagebox, simpledialog
|
||||||
|
from datetime import timezone,datetime
|
||||||
|
|
||||||
# Import dei processori e degli adapter
|
# Import dei processori e degli adapter
|
||||||
from flightmonitor.controller.historical_data_processor import HistoricalDataProcessor
|
from flightmonitor.controller.historical_data_processor import HistoricalDataProcessor
|
||||||
@ -614,9 +615,23 @@ class AppController:
|
|||||||
self.stop_live_monitoring()
|
self.stop_live_monitoring()
|
||||||
self.stop_historical_download()
|
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")
|
date_str = session_info.get("date_str")
|
||||||
start_ts = session_info.get("start_timestamp")
|
start_ts = session_info.get("start_timestamp")
|
||||||
end_ts = session_info.get("end_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 = {
|
bbox = {
|
||||||
"lat_min": session_info.get("lat_min"),
|
"lat_min": session_info.get("lat_min"),
|
||||||
"lon_min": session_info.get("lon_min"),
|
"lon_min": session_info.get("lon_min"),
|
||||||
@ -1165,3 +1180,69 @@ class AppController:
|
|||||||
return True
|
return True
|
||||||
module_logger.warning("Map tile manager not available to clear cache.")
|
module_logger.warning("Map tile manager not available to clear cache.")
|
||||||
return False
|
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')}.")
|
||||||
@ -105,6 +105,15 @@ class PlaybackAdapter(BaseLiveDataAdapter):
|
|||||||
f"{self.name}: Thread starting playback for session on {self.session_date_str} "
|
f"{self.name}: Thread starting playback for session on {self.session_date_str} "
|
||||||
f"from {self.start_ts:.0f} to {self.end_ts:.0f}."
|
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.")
|
self._send_status_to_queue(STATUS_STARTING, "Playback session started.")
|
||||||
|
|
||||||
if not self.data_storage:
|
if not self.data_storage:
|
||||||
@ -176,3 +185,45 @@ class PlaybackAdapter(BaseLiveDataAdapter):
|
|||||||
else:
|
else:
|
||||||
module_logger.info(f"{self.name}: Playback stopped by user.")
|
module_logger.info(f"{self.name}: Playback stopped by user.")
|
||||||
self._send_status_to_queue(STATUS_STOPPED, "Playback stopped.")
|
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)
|
||||||
@ -24,6 +24,7 @@ class PlaybackPanel:
|
|||||||
|
|
||||||
# --- Internal State ---
|
# --- Internal State ---
|
||||||
self._session_data_map: Dict[str, Dict[str, Any]] = {}
|
self._session_data_map: Dict[str, Dict[str, Any]] = {}
|
||||||
|
self._current_session_info: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
# --- Tkinter Variables ---
|
# --- Tkinter Variables ---
|
||||||
self.selected_date_var = tk.StringVar()
|
self.selected_date_var = tk.StringVar()
|
||||||
@ -37,6 +38,7 @@ class PlaybackPanel:
|
|||||||
self.play_button: Optional[ttk.Button] = None
|
self.play_button: Optional[ttk.Button] = None
|
||||||
self.pause_button: Optional[ttk.Button] = None
|
self.pause_button: Optional[ttk.Button] = None
|
||||||
self.stop_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.timeline_slider: Optional[ttk.Scale] = None
|
||||||
|
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
@ -47,7 +49,6 @@ class PlaybackPanel:
|
|||||||
container = ttk.Frame(self.parent_frame, padding="10 10 10 10")
|
container = ttk.Frame(self.parent_frame, padding="10 10 10 10")
|
||||||
container.pack(fill=tk.BOTH, expand=True)
|
container.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
# --- Session Selection Frame ---
|
|
||||||
session_frame = ttk.LabelFrame(
|
session_frame = ttk.LabelFrame(
|
||||||
container, text="Playback Session Selection", padding=10
|
container, text="Playback Session Selection", padding=10
|
||||||
)
|
)
|
||||||
@ -80,6 +81,7 @@ class PlaybackPanel:
|
|||||||
selectmode="browse",
|
selectmode="browse",
|
||||||
)
|
)
|
||||||
self.sessions_treeview.grid(row=0, column=0, sticky="nsew")
|
self.sessions_treeview.grid(row=0, column=0, sticky="nsew")
|
||||||
|
self.sessions_treeview.bind("<<TreeviewSelect>>", self._on_session_selected_in_tree)
|
||||||
|
|
||||||
self.sessions_treeview.heading("start_time", text="Start Time (UTC)")
|
self.sessions_treeview.heading("start_time", text="Start Time (UTC)")
|
||||||
self.sessions_treeview.column("start_time", width=130, anchor=tk.W)
|
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")
|
scrollbar.grid(row=0, column=1, sticky="ns")
|
||||||
self.sessions_treeview.configure(yscrollcommand=scrollbar.set)
|
self.sessions_treeview.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
# --- Playback Controls Frame ---
|
|
||||||
controls_frame = ttk.LabelFrame(container, text="Playback Controls", padding=10)
|
controls_frame = ttk.LabelFrame(container, text="Playback Controls", padding=10)
|
||||||
controls_frame.pack(fill=tk.BOTH, expand=True, pady=(10, 0))
|
controls_frame.pack(fill=tk.BOTH, expand=True, pady=(10, 0))
|
||||||
controls_frame.columnconfigure(1, weight=1)
|
controls_frame.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
# Timeline and Clock
|
|
||||||
clock_font = tkFont.Font(family="Courier New", size=14, weight="bold")
|
clock_font = tkFont.Font(family="Courier New", size=14, weight="bold")
|
||||||
virtual_clock_label = ttk.Label(
|
virtual_clock_label = ttk.Label(
|
||||||
controls_frame,
|
controls_frame,
|
||||||
@ -122,8 +122,10 @@ class PlaybackPanel:
|
|||||||
self.timeline_slider.grid(
|
self.timeline_slider.grid(
|
||||||
row=1, column=0, columnspan=4, sticky="ew", pady=(0, 10)
|
row=1, column=0, columnspan=4, sticky="ew", pady=(0, 10)
|
||||||
)
|
)
|
||||||
|
self.timeline_slider.bind("<ButtonPress-1>", self._on_timeline_press)
|
||||||
|
self.timeline_slider.bind("<B1-Motion>", self._on_timeline_drag)
|
||||||
|
self.timeline_slider.bind("<ButtonRelease-1>", self._on_timeline_release)
|
||||||
|
|
||||||
# Buttons and Speed Control
|
|
||||||
button_frame = ttk.Frame(controls_frame)
|
button_frame = ttk.Frame(controls_frame)
|
||||||
button_frame.grid(row=2, column=0, columnspan=4)
|
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.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))
|
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_options = ["0.5", "1.0", "2.0", "4.0", "8.0"]
|
||||||
speed_combobox = ttk.Combobox(
|
speed_combobox = ttk.Combobox(
|
||||||
@ -149,6 +156,7 @@ class PlaybackPanel:
|
|||||||
textvariable=self.playback_speed_var,
|
textvariable=self.playback_speed_var,
|
||||||
values=speed_options,
|
values=speed_options,
|
||||||
width=5,
|
width=5,
|
||||||
|
state="readonly"
|
||||||
)
|
)
|
||||||
speed_combobox.pack(side=tk.LEFT)
|
speed_combobox.pack(side=tk.LEFT)
|
||||||
speed_combobox.bind("<<ComboboxSelected>>", self._on_speed_change)
|
speed_combobox.bind("<<ComboboxSelected>>", self._on_speed_change)
|
||||||
@ -157,8 +165,7 @@ class PlaybackPanel:
|
|||||||
self.update_available_dates()
|
self.update_available_dates()
|
||||||
|
|
||||||
def update_available_dates(self):
|
def update_available_dates(self):
|
||||||
if not self.controller:
|
if not self.controller: return
|
||||||
return
|
|
||||||
dates = self.controller.get_available_recording_dates()
|
dates = self.controller.get_available_recording_dates()
|
||||||
if self.date_combobox:
|
if self.date_combobox:
|
||||||
self.date_combobox["values"] = dates
|
self.date_combobox["values"] = dates
|
||||||
@ -183,8 +190,7 @@ class PlaybackPanel:
|
|||||||
|
|
||||||
def update_sessions_for_date(self, date_str: str):
|
def update_sessions_for_date(self, date_str: str):
|
||||||
self._clear_sessions_treeview()
|
self._clear_sessions_treeview()
|
||||||
if not self.controller or not self.sessions_treeview:
|
if not self.controller or not self.sessions_treeview: return
|
||||||
return
|
|
||||||
|
|
||||||
sessions = self.controller.get_sessions_for_date(date_str)
|
sessions = self.controller.get_sessions_for_date(date_str)
|
||||||
if not sessions:
|
if not sessions:
|
||||||
@ -193,39 +199,25 @@ class PlaybackPanel:
|
|||||||
|
|
||||||
for session in sessions:
|
for session in sessions:
|
||||||
scan_id = session.get("scan_id")
|
scan_id = session.get("scan_id")
|
||||||
if scan_id is None:
|
if scan_id is None: continue
|
||||||
continue
|
|
||||||
|
|
||||||
self._session_data_map[str(scan_id)] = session
|
self._session_data_map[str(scan_id)] = session
|
||||||
try:
|
try:
|
||||||
start_ts = session.get("start_timestamp", 0)
|
start_ts, end_ts = session.get("start_timestamp", 0), session.get("end_timestamp", 0)
|
||||||
end_ts = 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}]"
|
||||||
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}]"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sessions_treeview.insert(
|
self.sessions_treeview.insert(
|
||||||
"",
|
"", "end", iid=str(scan_id),
|
||||||
"end",
|
|
||||||
values=(
|
values=(
|
||||||
start_dt.strftime("%H:%M:%S"),
|
datetime.fromtimestamp(start_ts, timezone.utc).strftime("%H:%M:%S"),
|
||||||
duration_str,
|
str(duration_delta).split(".")[0],
|
||||||
session.get("type", "Unknown"),
|
session.get("type", "Unknown"),
|
||||||
area_desc,
|
area_desc,
|
||||||
),
|
),
|
||||||
iid=str(scan_id),
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
module_logger.error(f"Failed to process session for display: {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()
|
children = self.sessions_treeview.get_children()
|
||||||
if children:
|
if children:
|
||||||
self.sessions_treeview.selection_set(children[0])
|
self.sessions_treeview.selection_set(children[0])
|
||||||
@ -235,86 +227,171 @@ class PlaybackPanel:
|
|||||||
self.play_button.config(state=tk.DISABLED)
|
self.play_button.config(state=tk.DISABLED)
|
||||||
|
|
||||||
def _on_play(self):
|
def _on_play(self):
|
||||||
if not self.controller or not self.sessions_treeview:
|
if not self.controller or not self.sessions_treeview: return
|
||||||
return
|
|
||||||
|
|
||||||
selected_items = self.sessions_treeview.selection()
|
selected_items = self.sessions_treeview.selection()
|
||||||
if not selected_items:
|
if not selected_items:
|
||||||
messagebox.showwarning("No Selection", "Please select a session to play.", parent=self.parent_frame)
|
messagebox.showwarning("No Selection", "Please select a session to play.", parent=self.parent_frame)
|
||||||
return
|
return
|
||||||
|
|
||||||
session_id = selected_items[0]
|
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)
|
messagebox.showerror("Error", "Could not find data for the selected session.", parent=self.parent_frame)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.controller.start_playback(session_info)
|
self.controller.start_playback(self._current_session_info)
|
||||||
|
|
||||||
def _on_pause(self):
|
def _on_pause(self):
|
||||||
if self.controller:
|
if self.controller: self.controller.pause_playback()
|
||||||
self.controller.pause_playback()
|
|
||||||
|
|
||||||
def _on_stop(self):
|
def _on_stop(self):
|
||||||
if self.controller:
|
if self.controller: self.controller.stop_playback()
|
||||||
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):
|
def _on_speed_change(self, event=None):
|
||||||
if self.controller:
|
if self.controller:
|
||||||
try:
|
try:
|
||||||
speed = float(self.playback_speed_var.get())
|
self.controller.set_playback_speed(float(self.playback_speed_var.get()))
|
||||||
self.controller.set_playback_speed(speed)
|
except (ValueError, TypeError): pass
|
||||||
except (ValueError, TypeError):
|
|
||||||
module_logger.warning("Invalid playback speed value.")
|
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):
|
def set_controls_state(self, is_playing: bool, is_paused: bool = False):
|
||||||
if not self.parent_frame.winfo_exists():
|
if not self.parent_frame.winfo_exists():
|
||||||
return
|
return
|
||||||
|
|
||||||
# Controls disabled during playback
|
# Stato per i controlli di selezione della sessione
|
||||||
scan_and_select_state = tk.DISABLED if is_playing else tk.NORMAL
|
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:
|
if self.date_combobox:
|
||||||
self.date_combobox.config(state="readonly" if not is_playing else tk.DISABLED)
|
self.date_combobox.config(state=session_selection_readonly_state)
|
||||||
# Also disable scan button and treeview
|
if self.sessions_treeview:
|
||||||
# ... (assuming direct widget references or iterating children)
|
# 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_playing:
|
||||||
if is_paused:
|
if is_paused:
|
||||||
|
if self.play_button:
|
||||||
self.play_button.config(state=tk.NORMAL, text="▶ Resume")
|
self.play_button.config(state=tk.NORMAL, text="▶ Resume")
|
||||||
self.play_button.configure(command=self._on_resume)
|
self.play_button.configure(command=self._on_resume)
|
||||||
|
if self.pause_button:
|
||||||
self.pause_button.config(state=tk.DISABLED)
|
self.pause_button.config(state=tk.DISABLED)
|
||||||
self.stop_button.config(state=tk.NORMAL)
|
|
||||||
else: # Playing
|
else: # Playing
|
||||||
|
if self.play_button:
|
||||||
self.play_button.config(state=tk.DISABLED, text="▶ Play")
|
self.play_button.config(state=tk.DISABLED, text="▶ Play")
|
||||||
|
if self.pause_button:
|
||||||
self.pause_button.config(state=tk.NORMAL)
|
self.pause_button.config(state=tk.NORMAL)
|
||||||
self.stop_button.config(state=tk.NORMAL)
|
|
||||||
else: # Stopped
|
else: # Stopped
|
||||||
|
if self.play_button:
|
||||||
self.play_button.config(state=tk.NORMAL, text="▶ Play")
|
self.play_button.config(state=tk.NORMAL, text="▶ Play")
|
||||||
self.play_button.configure(command=self._on_play)
|
self.play_button.configure(command=self._on_play)
|
||||||
|
if self.pause_button:
|
||||||
self.pause_button.config(state=tk.DISABLED)
|
self.pause_button.config(state=tk.DISABLED)
|
||||||
self.stop_button.config(state=tk.DISABLED)
|
|
||||||
|
|
||||||
def _on_resume(self):
|
def _on_resume(self):
|
||||||
if self.controller:
|
if self.controller: self.controller.resume_playback()
|
||||||
self.controller.resume_playback()
|
|
||||||
|
|
||||||
def update_virtual_clock(self, timestamp: float):
|
def update_virtual_clock(self, timestamp: float):
|
||||||
if not self.parent_frame.winfo_exists():
|
if not self.parent_frame.winfo_exists(): return
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
dt_object = datetime.fromtimestamp(timestamp, timezone.utc)
|
self.virtual_clock_var.set(datetime.fromtimestamp(timestamp, timezone.utc).strftime("%H:%M:%S"))
|
||||||
self.virtual_clock_var.set(dt_object.strftime("%H:%M:%S"))
|
except (ValueError, TypeError): pass
|
||||||
except (ValueError, TypeError):
|
|
||||||
self.virtual_clock_var.set("Invalid Time")
|
|
||||||
|
|
||||||
def update_timeline(self, current_ts: float, start_ts: float, end_ts: float):
|
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:
|
if not self.parent_frame.winfo_exists() or not self.timeline_slider: return
|
||||||
return
|
|
||||||
|
|
||||||
if (end_ts - start_ts) > 0:
|
if (end_ts - start_ts) > 0:
|
||||||
progress_percent = ((current_ts - start_ts) / (end_ts - start_ts)) * 100
|
self.timeline_var.set(((current_ts - start_ts) / (end_ts - start_ts)) * 100)
|
||||||
self.timeline_var.set(progress_percent)
|
|
||||||
else:
|
else:
|
||||||
self.timeline_var.set(100)
|
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)
|
||||||
@ -123,6 +123,7 @@ class MapCanvasManager:
|
|||||||
# --- Data for Drawing ---
|
# --- Data for Drawing ---
|
||||||
self._current_flights_to_display_gui: List[CanonicalFlightState] = []
|
self._current_flights_to_display_gui: List[CanonicalFlightState] = []
|
||||||
self._active_aircraft_states: Dict[str, CanonicalFlightState] = {}
|
self._active_aircraft_states: Dict[str, CanonicalFlightState] = {}
|
||||||
|
self._last_playback_timestamp: float = 0.0
|
||||||
self.flight_tracks_gui: Dict[str, deque] = {}
|
self.flight_tracks_gui: Dict[str, deque] = {}
|
||||||
self.max_track_points: int = app_config.DEFAULT_TRACK_HISTORY_POINTS
|
self.max_track_points: int = app_config.DEFAULT_TRACK_HISTORY_POINTS
|
||||||
self._map_data_lock: threading.Lock = threading.Lock()
|
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.flight_tracks_gui[state.icao24].append((state.latitude, state.longitude, state.timestamp))
|
||||||
self._request_overlay_render()
|
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:
|
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}
|
# Se il nuovo timestamp è inferiore all'ultimo, è un "seek" all'indietro.
|
||||||
for icao in timed_out_icaos:
|
# Dobbiamo resettare completamente lo stato per ricostruire la cronologia.
|
||||||
del self._active_aircraft_states[icao]
|
is_seek_event = virtual_timestamp < self._last_playback_timestamp
|
||||||
if icao in self.flight_tracks_gui: del self.flight_tracks_gui[icao]
|
|
||||||
|
|
||||||
for state in new_flight_states:
|
if is_seek_event:
|
||||||
state.last_contact_timestamp = virtual_timestamp # Update last seen time for playback
|
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
|
self._active_aircraft_states[state.icao24] = state
|
||||||
|
|
||||||
|
# Aggiungi il punto alla traccia
|
||||||
if state.icao24 not in self.flight_tracks_gui:
|
if state.icao24 not in self.flight_tracks_gui:
|
||||||
self.flight_tracks_gui[state.icao24] = deque(maxlen=self.max_track_points + 5)
|
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))
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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._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()
|
self._request_overlay_render()
|
||||||
|
|
||||||
def get_current_map_info(self) -> Dict[str, Any]:
|
def get_current_map_info(self) -> Dict[str, Any]:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user