# FlightMonitor/gui/panels/playback_panel.py """ Panel for controlling and viewing flight data playback. """ import tkinter as tk from tkinter import ttk, font as tkFont, messagebox from typing import Dict, Any, Optional, List from datetime import datetime, timedelta, timezone from flightmonitor.utils.logger import get_logger module_logger = get_logger(__name__) class PlaybackPanel: """ Manages the UI for the Playback function tab, allowing users to select and control the playback of recorded flight data sessions. """ def __init__(self, parent_frame: ttk.Frame, controller: Any): self.parent_frame = parent_frame self.controller = controller # --- Internal State --- self._session_data_map: Dict[str, Dict[str, Any]] = {} # --- Tkinter Variables --- self.selected_date_var = tk.StringVar() self.virtual_clock_var = tk.StringVar(value="--:--:--") self.playback_speed_var = tk.StringVar(value="1.0") self.timeline_var = tk.DoubleVar() # --- Widget References --- self.date_combobox: Optional[ttk.Combobox] = None self.sessions_treeview: Optional[ttk.Treeview] = None self.play_button: Optional[ttk.Button] = None self.pause_button: Optional[ttk.Button] = None self.stop_button: Optional[ttk.Button] = None self.timeline_slider: Optional[ttk.Scale] = None self._build_ui() module_logger.debug("PlaybackPanel initialized.") def _build_ui(self): """Builds all the widgets for the playback panel.""" 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 ) session_frame.pack(fill=tk.X, expand=False, pady=(0, 10)) session_frame.columnconfigure(1, weight=1) scan_button = ttk.Button( session_frame, text="Scan for Recordings", command=self._on_scan_for_recordings, ) scan_button.grid(row=0, column=0, padx=(0, 5), sticky="w") self.date_combobox = ttk.Combobox( session_frame, textvariable=self.selected_date_var, state="readonly" ) self.date_combobox.grid(row=0, column=1, columnspan=2, sticky="ew") self.date_combobox.bind("<>", self._on_date_selected) tree_frame = ttk.Frame(session_frame) tree_frame.grid(row=1, column=0, columnspan=3, sticky="nsew", pady=(10, 0)) tree_frame.columnconfigure(0, weight=1) tree_frame.rowconfigure(0, weight=1) self.sessions_treeview = ttk.Treeview( tree_frame, columns=("start_time", "duration", "type", "area_description"), show="headings", height=5, selectmode="browse", ) self.sessions_treeview.grid(row=0, column=0, sticky="nsew") self.sessions_treeview.heading("start_time", text="Start Time (UTC)") self.sessions_treeview.column("start_time", width=130, anchor=tk.W) self.sessions_treeview.heading("duration", text="Duration") self.sessions_treeview.column("duration", width=80, anchor=tk.CENTER) self.sessions_treeview.heading("type", text="Type") self.sessions_treeview.column("type", width=120, anchor=tk.W) self.sessions_treeview.heading("area_description", text="Area Description") self.sessions_treeview.column("area_description", width=150, stretch=True) scrollbar = ttk.Scrollbar( tree_frame, orient="vertical", command=self.sessions_treeview.yview ) 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, textvariable=self.virtual_clock_var, font=clock_font, anchor="center", ) virtual_clock_label.grid(row=0, column=0, columnspan=4, pady=(0, 5)) self.timeline_slider = ttk.Scale( controls_frame, from_=0, to=100, orient="horizontal", variable=self.timeline_var, state=tk.DISABLED, ) self.timeline_slider.grid( row=1, column=0, columnspan=4, sticky="ew", pady=(0, 10) ) # Buttons and Speed Control button_frame = ttk.Frame(controls_frame) button_frame.grid(row=2, column=0, columnspan=4) self.play_button = ttk.Button( button_frame, text="▶ Play", command=self._on_play, state=tk.DISABLED ) self.play_button.pack(side=tk.LEFT, padx=5) self.pause_button = ttk.Button( button_frame, text="❚❚ Pause", command=self._on_pause, state=tk.DISABLED ) self.pause_button.pack(side=tk.LEFT, padx=5) self.stop_button = ttk.Button( button_frame, text="■ Stop", command=self._on_stop, state=tk.DISABLED ) self.stop_button.pack(side=tk.LEFT, padx=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( button_frame, textvariable=self.playback_speed_var, values=speed_options, width=5, ) speed_combobox.pack(side=tk.LEFT) speed_combobox.bind("<>", self._on_speed_change) def _on_scan_for_recordings(self): self.update_available_dates() def update_available_dates(self): if not self.controller: return dates = self.controller.get_available_recording_dates() if self.date_combobox: self.date_combobox["values"] = dates if dates: self.selected_date_var.set(dates[0]) self._on_date_selected() else: self.selected_date_var.set("") self._clear_sessions_treeview() self.play_button.config(state=tk.DISABLED) def _on_date_selected(self, event=None): date_str = self.selected_date_var.get() if date_str: self.update_sessions_for_date(date_str) def _clear_sessions_treeview(self): self._session_data_map.clear() if self.sessions_treeview: for item in self.sessions_treeview.get_children(): self.sessions_treeview.delete(item) def update_sessions_for_date(self, date_str: str): self._clear_sessions_treeview() if not self.controller or not self.sessions_treeview: return sessions = self.controller.get_sessions_for_date(date_str) if not sessions: self.play_button.config(state=tk.DISABLED) return for session in sessions: scan_id = session.get("scan_id") 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}]" ) self.sessions_treeview.insert( "", "end", values=( start_dt.strftime("%H:%M:%S"), duration_str, 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]) self.sessions_treeview.focus(children[0]) self.play_button.config(state=tk.NORMAL) else: self.play_button.config(state=tk.DISABLED) def _on_play(self): 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) if not session_info: messagebox.showerror("Error", "Could not find data for the selected session.", parent=self.parent_frame) return self.controller.start_playback(session_info) def _on_pause(self): if self.controller: self.controller.pause_playback() def _on_stop(self): if self.controller: self.controller.stop_playback() 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.") 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) if is_playing: if is_paused: self.play_button.config(state=tk.NORMAL, text="▶ Resume") self.play_button.configure(command=self._on_resume) 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() def update_virtual_clock(self, timestamp: float): 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") 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 (end_ts - start_ts) > 0: progress_percent = ((current_ts - start_ts) / (end_ts - start_ts)) * 100 self.timeline_var.set(progress_percent) else: self.timeline_var.set(100)