320 lines
12 KiB
Python
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) |