S1005403_RisCC/target_simulator/gui/waypoint_editor_window.py

205 lines
11 KiB
Python

# target_simulator/gui/waypoint_editor_window.py
"""
A modal dialog window for creating or editing a single Waypoint.
"""
import tkinter as tk
from tkinter import ttk, messagebox
from typing import Optional
from target_simulator.core.models import Waypoint, ManeuverType, KNOTS_TO_FPS, FPS_TO_KNOTS
class WaypointEditorWindow(tk.Toplevel):
"""A dialog for entering and editing data for a single waypoint."""
def __init__(self, master, is_first_waypoint: bool = False, waypoint_to_edit: Optional[Waypoint] = None, waypoint_index: Optional[int] = None):
super().__init__(master)
self.transient(master)
self.grab_set()
self.title("Waypoint Editor" if not is_first_waypoint else "Set Initial Position")
self.resizable(False, False)
self.is_first_waypoint = is_first_waypoint
self.result_waypoint: Optional[Waypoint] = None
self.waypoint_index = waypoint_index
self._setup_variables()
# Se stiamo editando un waypoint, imposta subito il tipo per mostrare il frame corretto
if waypoint_to_edit:
self.maneuver_type_var.set(waypoint_to_edit.maneuver_type.value)
self._create_widgets()
if waypoint_to_edit:
self._load_waypoint_data(waypoint_to_edit)
# Mostra subito il frame corretto in base al tipo
self._on_maneuver_type_change()
self._center_window()
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
self.wait_window(self)
def _setup_variables(self):
"""Initializes all tk Variables."""
self.maneuver_type_var = tk.StringVar()
self.duration_var = tk.DoubleVar(value=10.0)
self.t_range_var = tk.DoubleVar(value=20.0)
self.t_az_var = tk.DoubleVar(value=0.0)
self.t_alt_var = tk.DoubleVar(value=10000.0)
self.t_vel_var = tk.DoubleVar(value=300.0)
self.t_hdg_var = tk.DoubleVar(value=90.0)
# Dynamic maneuver variables
self.dyn_vel_var = tk.DoubleVar(value=300.0)
self.lateral_g_var = tk.DoubleVar(value=0.0)
self.longitudinal_g_var = tk.DoubleVar(value=0.0)
self.vertical_g_var = tk.DoubleVar(value=0.0)
def _load_waypoint_data(self, wp: Waypoint):
"""Populates the fields with data from an existing waypoint."""
self.maneuver_type_var.set(wp.maneuver_type.value)
self.duration_var.set(wp.duration_s or 10.0)
self.t_range_var.set(wp.target_range_nm or 0.0)
self.t_az_var.set(wp.target_azimuth_deg or 0.0)
self.t_alt_var.set(wp.target_altitude_ft or 10000.0)
if wp.target_velocity_fps is not None:
self.t_vel_var.set(wp.target_velocity_fps * FPS_TO_KNOTS)
else:
self.t_vel_var.set(0.0)
if wp.target_heading_deg is not None:
self.t_hdg_var.set(wp.target_heading_deg)
else:
self.t_hdg_var.set(0.0)
# Carica i parametri dinamici se presenti
self.dyn_vel_var.set((getattr(wp, 'dynamic_velocity_fps', 0.0) or 0.0) * FPS_TO_KNOTS)
self.lateral_g_var.set(getattr(wp, 'lateral_g', 0.0) or 0.0)
self.longitudinal_g_var.set(getattr(wp, 'longitudinal_g', 0.0) or 0.0)
self.vertical_g_var.set(getattr(wp, 'vertical_g', 0.0) or 0.0)
def _center_window(self):
self.update_idletasks()
parent = self.master
x = parent.winfo_x() + (parent.winfo_width() // 2) - (self.winfo_width() // 2)
y = parent.winfo_y() + (parent.winfo_height() // 2) - (self.winfo_height() // 2)
self.geometry(f"+{x}+{y}")
def _create_widgets(self):
main_frame = ttk.Frame(self, padding=10)
main_frame.pack(fill=tk.BOTH, expand=True)
# --- Waypoint ID (se disponibile) ---
if self.waypoint_index is not None:
id_frame = ttk.Frame(main_frame)
id_frame.pack(fill=tk.X, pady=(0, 5))
ttk.Label(id_frame, text=f"Waypoint ID: {self.waypoint_index}", font=("Arial", 11, "bold")).pack(side=tk.LEFT)
# --- Maneuver Type (always visible, but maybe disabled) ---
type_frame = ttk.Frame(main_frame)
type_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(type_frame, text="Maneuver Type:").pack(side=tk.LEFT)
self.maneuver_combo = ttk.Combobox(type_frame, textvariable=self.maneuver_type_var,
values=[m.value for m in ManeuverType], state="readonly")
self.maneuver_combo.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=5)
self.maneuver_combo.bind("<<ComboboxSelected>>", self._on_maneuver_type_change)
# --- Dynamic Frames ---
self.fly_to_point_frame = self._create_fly_to_point_frame(main_frame)
self.fly_for_duration_frame = self._create_fly_for_duration_frame(main_frame)
self.dynamic_maneuver_frame = self._create_dynamic_maneuver_frame(main_frame)
# --- Buttons ---
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=tk.X, pady=(10, 0), side=tk.BOTTOM)
ttk.Button(button_frame, text="OK", command=self._on_ok).pack(side=tk.RIGHT, padx=5)
ttk.Button(button_frame, text="Cancel", command=self._on_cancel).pack(side=tk.RIGHT)
# --- Initial State Logic ---
if self.is_first_waypoint:
self.maneuver_type_var.set(ManeuverType.FLY_TO_POINT.value)
self.maneuver_combo.config(state=tk.DISABLED)
else:
self.maneuver_type_var.set(ManeuverType.FLY_FOR_DURATION.value)
self._on_maneuver_type_change()
def _create_dynamic_maneuver_frame(self, parent) -> ttk.Frame:
frame = ttk.Frame(parent, padding=10)
frame.columnconfigure(1, weight=1)
ttk.Label(frame, text="Duration (s):").grid(row=0, column=0, sticky=tk.W, pady=2)
ttk.Spinbox(frame, from_=0.1, to=3600, textvariable=self.duration_var, width=10).grid(row=0, column=1, sticky=tk.EW, pady=2)
ttk.Label(frame, text="Velocity (kn):").grid(row=1, column=0, sticky=tk.W, pady=2)
ttk.Spinbox(frame, from_=0, to=2000, textvariable=self.dyn_vel_var, width=10).grid(row=1, column=1, sticky=tk.EW, pady=2)
ttk.Label(frame, text="Lateral G:").grid(row=2, column=0, sticky=tk.W, pady=2)
ttk.Spinbox(frame, from_=-12, to=12, textvariable=self.lateral_g_var, width=10, increment=0.1).grid(row=2, column=1, sticky=tk.EW, pady=2)
ttk.Label(frame, text="Longitudinal G:").grid(row=3, column=0, sticky=tk.W, pady=2)
ttk.Spinbox(frame, from_=-5, to=5, textvariable=self.longitudinal_g_var, width=10, increment=0.1).grid(row=3, column=1, sticky=tk.EW, pady=2)
ttk.Label(frame, text="Vertical G:").grid(row=4, column=0, sticky=tk.W, pady=2)
ttk.Spinbox(frame, from_=-5, to=5, textvariable=self.vertical_g_var, width=10, increment=0.1).grid(row=4, column=1, sticky=tk.EW, pady=2)
return frame
def _create_fly_to_point_frame(self, parent) -> ttk.Frame:
frame = ttk.Frame(parent, padding=10)
frame.columnconfigure(1, weight=1)
ttk.Label(frame, text="Range (NM):").grid(row=0, column=0, sticky=tk.W, pady=2)
ttk.Spinbox(frame, from_=0, to=1000, textvariable=self.t_range_var, width=10).grid(row=0, column=1, sticky=tk.EW, pady=2)
ttk.Label(frame, text="Azimuth (°):").grid(row=1, column=0, sticky=tk.W, pady=2)
ttk.Spinbox(frame, from_=-180, to=180, textvariable=self.t_az_var, width=10).grid(row=1, column=1, sticky=tk.EW, pady=2)
ttk.Label(frame, text="Altitude (ft):").grid(row=2, column=0, sticky=tk.W, pady=2)
ttk.Spinbox(frame, from_=-1000, to=80000, textvariable=self.t_alt_var, width=10).grid(row=2, column=1, sticky=tk.EW, pady=2)
ttk.Label(frame, text="Time to Point (s):").grid(row=3, column=0, sticky=tk.W, pady=2)
ttk.Spinbox(frame, from_=0.1, to=3600, textvariable=self.duration_var, width=10).grid(row=3, column=1, sticky=tk.EW, pady=2)
ttk.Label(frame, text="Final Velocity (kn):").grid(row=4, column=0, sticky=tk.W, pady=2)
ttk.Spinbox(frame, from_=0, to=2000, textvariable=self.t_vel_var, width=10).grid(row=4, column=1, sticky=tk.EW, pady=2)
ttk.Label(frame, text="Final Heading (°):").grid(row=5, column=0, sticky=tk.W, pady=2)
ttk.Spinbox(frame, from_=0, to=359.9, textvariable=self.t_hdg_var, width=10).grid(row=5, column=1, sticky=tk.EW, pady=2)
return frame
def _create_fly_for_duration_frame(self, parent) -> ttk.Frame:
frame = ttk.Frame(parent, padding=10)
frame.columnconfigure(1, weight=1)
ttk.Label(frame, text="Duration (s):").grid(row=0, column=0, sticky=tk.W, pady=2)
ttk.Spinbox(frame, from_=0.1, to=3600, textvariable=self.duration_var, width=10).grid(row=0, column=1, sticky=tk.EW, pady=2)
ttk.Label(frame, text="Target Velocity (kn):").grid(row=1, column=0, sticky=tk.W, pady=2)
ttk.Spinbox(frame, from_=0, to=2000, textvariable=self.t_vel_var, width=10).grid(row=1, column=1, sticky=tk.EW, pady=2)
ttk.Label(frame, text="Target Heading (°):").grid(row=2, column=0, sticky=tk.W, pady=2)
ttk.Spinbox(frame, from_=0, to=359.9, textvariable=self.t_hdg_var, width=10).grid(row=2, column=1, sticky=tk.EW, pady=2)
return frame
def _on_maneuver_type_change(self, event=None):
m_type = ManeuverType(self.maneuver_type_var.get())
self.fly_to_point_frame.pack_forget()
self.fly_for_duration_frame.pack_forget()
self.dynamic_maneuver_frame.pack_forget()
if m_type == ManeuverType.FLY_TO_POINT:
self.fly_to_point_frame.pack(fill=tk.BOTH, expand=True)
elif m_type == ManeuverType.FLY_FOR_DURATION:
self.fly_for_duration_frame.pack(fill=tk.BOTH, expand=True)
elif m_type == ManeuverType.DYNAMIC_MANEUVER:
self.dynamic_maneuver_frame.pack(fill=tk.BOTH, expand=True)
def _on_ok(self):
try:
m_type = ManeuverType(self.maneuver_type_var.get())
wp = Waypoint(maneuver_type=m_type)
if m_type == ManeuverType.FLY_TO_POINT:
wp.target_range_nm = self.t_range_var.get()
wp.target_azimuth_deg = self.t_az_var.get()
wp.target_altitude_ft = self.t_alt_var.get()
wp.duration_s = self.duration_var.get()
wp.target_velocity_fps = self.t_vel_var.get() * KNOTS_TO_FPS
wp.target_heading_deg = self.t_hdg_var.get()
elif m_type == ManeuverType.FLY_FOR_DURATION:
wp.duration_s = self.duration_var.get()
wp.target_velocity_fps = self.t_vel_var.get() * KNOTS_TO_FPS
wp.target_heading_deg = self.t_hdg_var.get()
if self.is_first_waypoint:
wp.target_altitude_ft = self.t_alt_var.get()
elif m_type == ManeuverType.DYNAMIC_MANEUVER:
wp.duration_s = self.duration_var.get()
wp.dynamic_velocity_fps = self.dyn_vel_var.get() * KNOTS_TO_FPS
wp.lateral_g = self.lateral_g_var.get()
wp.longitudinal_g = self.longitudinal_g_var.get()
wp.vertical_g = self.vertical_g_var.get()
self.result_waypoint = wp
self.destroy()
except (ValueError, tk.TclError) as e:
messagebox.showerror("Invalid Input", f"Please enter valid numbers.\nError: {e}", parent=self)
def _on_cancel(self):
self.result_waypoint = None
self.destroy()