SXXXXXXX_FlightMonitor/flightmonitor/gui/panels/playback_panel.py

320 lines
12 KiB
Python

# 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("<<ComboboxSelected>>", 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("<<ComboboxSelected>>", 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)