579 lines
23 KiB
Python
579 lines
23 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, Tuple
|
|
|
|
import math
|
|
|
|
from target_simulator.core.models import (
|
|
Waypoint,
|
|
ManeuverType,
|
|
TurnDirection,
|
|
KNOTS_TO_FPS,
|
|
FPS_TO_KNOTS,
|
|
G_IN_FPS2,
|
|
NM_TO_FT,
|
|
)
|
|
|
|
|
|
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,
|
|
previous_waypoint_end_pos_ft: Tuple[float, float, float] = (0.0, 0.0, 0.0),
|
|
):
|
|
super().__init__(master)
|
|
self.transient(master)
|
|
self.grab_set()
|
|
self.title("Edit Waypoint" if waypoint_to_edit else "Add Waypoint")
|
|
if is_first_waypoint:
|
|
self.title("Set Initial Position")
|
|
self.resizable(False, False)
|
|
|
|
self.is_first_waypoint = is_first_waypoint
|
|
self.previous_waypoint_end_pos_ft = previous_waypoint_end_pos_ft
|
|
self.result_waypoint: Optional[Waypoint] = None
|
|
self._is_updating_turn_def = False # Flag to prevent update loops
|
|
|
|
self._setup_variables()
|
|
|
|
# Add traces to variables for automatic updates
|
|
self.ftp_duration_var.trace_add("write", self._update_resultant_speed)
|
|
self.ftp_range_var.trace_add("write", self._update_resultant_speed)
|
|
self.ftp_az_var.trace_add("write", self._update_resultant_speed)
|
|
self.ftp_alt_var.trace_add("write", self._update_resultant_speed)
|
|
|
|
self._create_widgets()
|
|
|
|
if waypoint_to_edit:
|
|
self._load_waypoint_data(waypoint_to_edit)
|
|
|
|
self._on_maneuver_type_change()
|
|
self._center_window()
|
|
|
|
# Trigger initial calculation for resultant speed
|
|
self.after(50, self._update_resultant_speed)
|
|
|
|
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.ftp_duration_var = tk.DoubleVar(value=10.0)
|
|
self.ftp_range_var = tk.DoubleVar(value=20.0)
|
|
self.ftp_az_var = tk.DoubleVar(value=0.0)
|
|
self.ftp_alt_var = tk.DoubleVar(value=10000.0)
|
|
self.ftp_vel_var = tk.DoubleVar(value=300.0)
|
|
self.ftp_hdg_var = tk.DoubleVar(value=180.0)
|
|
self.resultant_speed_kn_var = tk.StringVar(value="Calculating...")
|
|
|
|
self.ffd_duration_var = tk.DoubleVar(value=10.0)
|
|
self.ffd_vel_var = tk.DoubleVar(value=300.0)
|
|
self.ffd_hdg_var = tk.DoubleVar(value=90.0)
|
|
self.ffd_alt_var = tk.DoubleVar(value=10000.0)
|
|
|
|
self.dm_duration_var = tk.DoubleVar(value=10.0)
|
|
self.dm_speed_kn_var = tk.DoubleVar(value=300.0)
|
|
self.dm_lon_accel_g_var = tk.DoubleVar(value=0.0)
|
|
self.dm_ver_accel_g_var = tk.DoubleVar(value=0.0)
|
|
self.dm_turn_def_var = tk.StringVar(value="g_force")
|
|
self.dm_lat_accel_g_var = tk.DoubleVar(value=1.0)
|
|
self.dm_turn_rate_var = tk.DoubleVar(value=3.0)
|
|
self.dm_turn_dir_var = tk.StringVar(value=TurnDirection.RIGHT.value)
|
|
|
|
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)
|
|
if wp.maneuver_type == ManeuverType.FLY_TO_POINT:
|
|
self.ftp_duration_var.set(wp.duration_s or 10.0)
|
|
self.ftp_range_var.set(wp.target_range_nm or 0.0)
|
|
self.ftp_az_var.set(wp.target_azimuth_deg or 0.0)
|
|
self.ftp_alt_var.set(wp.target_altitude_ft or 10000.0)
|
|
self.ftp_vel_var.set((wp.target_velocity_fps or 0.0) * FPS_TO_KNOTS)
|
|
self.ftp_hdg_var.set(wp.target_heading_deg or 0.0)
|
|
elif wp.maneuver_type == ManeuverType.FLY_FOR_DURATION:
|
|
self.ffd_duration_var.set(wp.duration_s or 10.0)
|
|
self.ffd_vel_var.set((wp.target_velocity_fps or 0.0) * FPS_TO_KNOTS)
|
|
self.ffd_hdg_var.set(wp.target_heading_deg or 0.0)
|
|
self.ffd_alt_var.set(wp.target_altitude_ft or 10000.0)
|
|
elif wp.maneuver_type == ManeuverType.DYNAMIC_MANEUVER:
|
|
self.dm_duration_var.set(wp.duration_s or 10.0)
|
|
self.dm_speed_kn_var.set((wp.maneuver_speed_fps or 300.0) * FPS_TO_KNOTS)
|
|
self.dm_lon_accel_g_var.set(wp.longitudinal_acceleration_g or 0.0)
|
|
self.dm_lat_accel_g_var.set(wp.lateral_acceleration_g or 0.0)
|
|
self.dm_ver_accel_g_var.set(wp.vertical_acceleration_g or 0.0)
|
|
if wp.turn_direction:
|
|
self.dm_turn_dir_var.set(wp.turn_direction.value)
|
|
# Sync the turn rate spinbox from the loaded G value
|
|
self._update_turn_rate_from_g()
|
|
|
|
def _center_window(self):
|
|
"""Center this Toplevel over the parent window.
|
|
|
|
Computes coordinates so the dialog appears centered relative to the
|
|
master widget and applies the geometry offset.
|
|
"""
|
|
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):
|
|
"""Create dialog widgets for all maneuver types and action buttons.
|
|
|
|
Builds the combobox used to select the maneuver type and the specific
|
|
sub-frames for each maneuver's parameter inputs. Also attaches OK/Cancel
|
|
buttons and configures initial combo state for first-waypoint dialogs.
|
|
"""
|
|
main_frame = ttk.Frame(self, padding=10)
|
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
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)
|
|
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)
|
|
button_frame = ttk.Frame(main_frame)
|
|
button_frame.pack(fill=tk.X, pady=(15, 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
|
|
)
|
|
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)
|
|
|
|
def _create_fly_to_point_frame(self, parent) -> ttk.Frame:
|
|
"""Create and return the subframe used to configure a 'Fly to Point'.
|
|
|
|
This contains inputs for duration, range, azimuth, altitude and the
|
|
read-only resultant speed display. When editing the initial position
|
|
additional fields for initial velocity/heading are added.
|
|
"""
|
|
frame = ttk.LabelFrame(parent, text="Position and State", padding=10)
|
|
frame.columnconfigure(1, weight=1)
|
|
|
|
# Duration
|
|
ttk.Label(frame, text="Duration to Point (s):").grid(
|
|
row=0, column=0, sticky=tk.W, pady=2
|
|
)
|
|
ttk.Spinbox(
|
|
frame, from_=0.1, to=3600 * 24, textvariable=self.ftp_duration_var, width=10
|
|
).grid(row=0, column=1, sticky=tk.EW, pady=2)
|
|
|
|
# Range
|
|
ttk.Label(frame, text="Target Range (NM):").grid(
|
|
row=1, column=0, sticky=tk.W, pady=2
|
|
)
|
|
ttk.Spinbox(
|
|
frame, from_=0, to=1000, textvariable=self.ftp_range_var, width=10
|
|
).grid(row=1, column=1, sticky=tk.EW, pady=2)
|
|
|
|
# Azimuth
|
|
ttk.Label(frame, text="Target Azimuth (°):").grid(
|
|
row=2, column=0, sticky=tk.W, pady=2
|
|
)
|
|
ttk.Spinbox(
|
|
frame, from_=-180, to=180, textvariable=self.ftp_az_var, width=10
|
|
).grid(row=2, column=1, sticky=tk.EW, pady=2)
|
|
|
|
# Altitude
|
|
ttk.Label(frame, text="Target Altitude (ft):").grid(
|
|
row=3, column=0, sticky=tk.W, pady=2
|
|
)
|
|
ttk.Spinbox(
|
|
frame, from_=-1000, to=80000, textvariable=self.ftp_alt_var, width=10
|
|
).grid(row=3, column=1, sticky=tk.EW, pady=2)
|
|
|
|
# Resultant Speed (read-only)
|
|
ttk.Label(frame, text="Resultant Speed (kn):").grid(
|
|
row=4, column=0, sticky=tk.W, pady=5
|
|
)
|
|
ttk.Entry(
|
|
frame, textvariable=self.resultant_speed_kn_var, state="readonly"
|
|
).grid(row=4, column=1, sticky=tk.EW, pady=5)
|
|
|
|
if self.is_first_waypoint:
|
|
# Separator
|
|
ttk.Separator(frame, orient=tk.HORIZONTAL).grid(
|
|
row=5, column=0, columnspan=2, sticky="ew", pady=8
|
|
)
|
|
|
|
ttk.Label(frame, text="Initial Velocity (kn):").grid(
|
|
row=6, column=0, sticky=tk.W, pady=2
|
|
)
|
|
ttk.Spinbox(
|
|
frame, from_=0, to=2000, textvariable=self.ftp_vel_var, width=10
|
|
).grid(row=6, column=1, sticky=tk.EW, pady=2)
|
|
|
|
ttk.Label(frame, text="Initial Heading (°):").grid(
|
|
row=7, column=0, sticky=tk.W, pady=2
|
|
)
|
|
ttk.Spinbox(
|
|
frame,
|
|
from_=0,
|
|
to=359.9,
|
|
format="%.1f",
|
|
increment=0.1,
|
|
textvariable=self.ftp_hdg_var,
|
|
width=10,
|
|
).grid(row=7, column=1, sticky=tk.EW, pady=2)
|
|
return frame
|
|
|
|
def _update_resultant_speed(self, *args):
|
|
"""Calculates and displays the speed required to reach the destination in time."""
|
|
if self.is_first_waypoint:
|
|
self.resultant_speed_kn_var.set("N/A (Initial Position)")
|
|
return
|
|
|
|
try:
|
|
duration_s = self.ftp_duration_var.get()
|
|
if duration_s <= 0:
|
|
self.resultant_speed_kn_var.set("Infinite (duration <= 0)")
|
|
return
|
|
|
|
range_nm = self.ftp_range_var.get()
|
|
az_deg = self.ftp_az_var.get()
|
|
alt_ft = self.ftp_alt_var.get()
|
|
|
|
# Convert destination polar coordinates (relative to origin) to Cartesian
|
|
az_rad = math.radians(az_deg)
|
|
dest_x_ft = range_nm * NM_TO_FT * math.sin(az_rad)
|
|
dest_y_ft = range_nm * NM_TO_FT * math.cos(az_rad)
|
|
dest_z_ft = alt_ft
|
|
|
|
# Get start position from the end of the previous waypoint
|
|
start_x, start_y, start_z = self.previous_waypoint_end_pos_ft
|
|
|
|
# Calculate 3D distance to travel
|
|
dx = dest_x_ft - start_x
|
|
dy = dest_y_ft - start_y
|
|
dz = dest_z_ft - start_z
|
|
distance_ft = math.sqrt(dx**2 + dy**2 + dz**2)
|
|
|
|
# Calculate speed
|
|
speed_fps = distance_ft / duration_s
|
|
speed_knots = speed_fps * FPS_TO_KNOTS
|
|
|
|
self.resultant_speed_kn_var.set(f"{speed_knots:.2f}")
|
|
|
|
except (ValueError, tk.TclError):
|
|
# This can happen if the entry is temporarily empty during typing
|
|
self.resultant_speed_kn_var.set("Calculating...")
|
|
|
|
def _create_fly_for_duration_frame(self, parent) -> ttk.Frame:
|
|
"""Create the subframe for 'Fly for Duration' maneuver parameters.
|
|
|
|
Provides duration, constant velocity, heading and altitude inputs.
|
|
"""
|
|
frame = ttk.LabelFrame(parent, text="Constant State Flight", 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 * 24, textvariable=self.ffd_duration_var, width=10
|
|
).grid(row=0, column=1, sticky=tk.EW, pady=2)
|
|
ttk.Label(frame, text="Constant Velocity (kn):").grid(
|
|
row=1, column=0, sticky=tk.W, pady=2
|
|
)
|
|
ttk.Spinbox(
|
|
frame, from_=0, to=2000, textvariable=self.ffd_vel_var, width=10
|
|
).grid(row=1, column=1, sticky=tk.EW, pady=2)
|
|
ttk.Label(frame, text="Constant Heading (°):").grid(
|
|
row=2, column=0, sticky=tk.W, pady=2
|
|
)
|
|
ttk.Spinbox(
|
|
frame,
|
|
from_=0,
|
|
to=359.9,
|
|
format="%.1f",
|
|
increment=0.1,
|
|
textvariable=self.ffd_hdg_var,
|
|
width=10,
|
|
).grid(row=2, column=1, sticky=tk.EW, pady=2)
|
|
ttk.Label(frame, text="Constant Altitude (ft):").grid(
|
|
row=3, column=0, sticky=tk.W, pady=2
|
|
)
|
|
ttk.Spinbox(
|
|
frame, from_=-1000, to=80000, textvariable=self.ffd_alt_var, width=10
|
|
).grid(row=3, column=1, sticky=tk.EW, pady=2)
|
|
return frame
|
|
|
|
def _create_dynamic_maneuver_frame(self, parent) -> ttk.Frame:
|
|
"""Create the subframe for dynamic maneuver controls.
|
|
|
|
This frame exposes duration, maneuver speed and turn definition
|
|
controls (either G-force based or explicit turn rate), as well as
|
|
longitudinal/vertical acceleration inputs.
|
|
"""
|
|
frame = ttk.LabelFrame(parent, text="Dynamic Maneuver Controls", 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 * 24, textvariable=self.dm_duration_var, width=10
|
|
).grid(row=0, column=1, sticky=tk.EW, pady=2)
|
|
ttk.Label(frame, text="Maneuver Speed (kn):").grid(
|
|
row=1, column=0, sticky=tk.W, pady=2
|
|
)
|
|
speed_spinbox = ttk.Spinbox(
|
|
frame,
|
|
from_=0,
|
|
to=2000,
|
|
textvariable=self.dm_speed_kn_var,
|
|
width=10,
|
|
command=self._update_g_from_turn_rate,
|
|
)
|
|
speed_spinbox.grid(row=1, column=1, sticky=tk.EW, pady=2)
|
|
turn_frame = ttk.LabelFrame(frame, text="Turn Definition", padding=5)
|
|
turn_frame.grid(row=2, column=0, columnspan=2, sticky=tk.EW, pady=5)
|
|
g_force_rb = ttk.Radiobutton(
|
|
turn_frame,
|
|
text="By G-Force",
|
|
variable=self.dm_turn_def_var,
|
|
value="g_force",
|
|
command=self._on_turn_definition_change,
|
|
)
|
|
g_force_rb.grid(row=0, column=0, sticky=tk.W)
|
|
self.dm_lat_g_spinbox = ttk.Spinbox(
|
|
turn_frame,
|
|
from_=0,
|
|
to=9,
|
|
format="%.2f",
|
|
increment=0.1,
|
|
textvariable=self.dm_lat_accel_g_var,
|
|
width=8,
|
|
command=self._update_turn_rate_from_g,
|
|
)
|
|
self.dm_lat_g_spinbox.grid(row=0, column=1, padx=5)
|
|
turn_rate_rb = ttk.Radiobutton(
|
|
turn_frame,
|
|
text="By Turn Rate (°/s)",
|
|
variable=self.dm_turn_def_var,
|
|
value="turn_rate",
|
|
command=self._on_turn_definition_change,
|
|
)
|
|
turn_rate_rb.grid(row=1, column=0, sticky=tk.W)
|
|
self.dm_turn_rate_spinbox = ttk.Spinbox(
|
|
turn_frame,
|
|
from_=0,
|
|
to=30,
|
|
format="%.2f",
|
|
increment=0.1,
|
|
textvariable=self.dm_turn_rate_var,
|
|
width=8,
|
|
command=self._update_g_from_turn_rate,
|
|
)
|
|
self.dm_turn_rate_spinbox.grid(row=1, column=1, padx=5)
|
|
ttk.Label(turn_frame, text="Direction:").grid(
|
|
row=0, column=2, sticky=tk.W, padx=(10, 2)
|
|
)
|
|
ttk.Combobox(
|
|
turn_frame,
|
|
textvariable=self.dm_turn_dir_var,
|
|
values=[d.value for d in TurnDirection],
|
|
state="readonly",
|
|
width=7,
|
|
).grid(row=0, column=3, sticky=tk.W)
|
|
self._on_turn_definition_change()
|
|
ttk.Label(frame, text="Longitudinal Accel (g):").grid(
|
|
row=3, column=0, sticky=tk.W, pady=2
|
|
)
|
|
ttk.Spinbox(
|
|
frame,
|
|
from_=-5,
|
|
to=10,
|
|
format="%.2f",
|
|
increment=0.1,
|
|
textvariable=self.dm_lon_accel_g_var,
|
|
width=10,
|
|
).grid(row=3, column=1, sticky=tk.EW, pady=2)
|
|
ttk.Label(frame, text="Vertical Accel (g):").grid(
|
|
row=4, column=0, sticky=tk.W, pady=2
|
|
)
|
|
ttk.Spinbox(
|
|
frame,
|
|
from_=-5,
|
|
to=5,
|
|
format="%.2f",
|
|
increment=0.1,
|
|
textvariable=self.dm_ver_accel_g_var,
|
|
width=10,
|
|
).grid(row=4, column=1, sticky=tk.EW, pady=2)
|
|
return frame
|
|
|
|
def _update_g_from_turn_rate(self):
|
|
"""Update lateral G value when turn rate or speed changes.
|
|
|
|
Converts the configured turn rate (deg/s) and maneuver speed into an
|
|
equivalent lateral acceleration (g) and writes it back to the related
|
|
variable used by the UI.
|
|
"""
|
|
if self._is_updating_turn_def:
|
|
return
|
|
self._is_updating_turn_def = True
|
|
try:
|
|
rate_deg_s = self.dm_turn_rate_var.get()
|
|
speed_kn = self.dm_speed_kn_var.get()
|
|
if speed_kn > 0:
|
|
accel_fps2 = math.radians(rate_deg_s) * (speed_kn * KNOTS_TO_FPS)
|
|
g_load = accel_fps2 / G_IN_FPS2
|
|
self.dm_lat_accel_g_var.set(round(g_load, 2))
|
|
except (ValueError, tk.TclError):
|
|
pass
|
|
finally:
|
|
self._is_updating_turn_def = False
|
|
|
|
def _update_turn_rate_from_g(self):
|
|
"""Compute turn rate (deg/s) from configured lateral G and speed.
|
|
|
|
The inverse of _update_g_from_turn_rate, used to keep the two UI
|
|
controls synchronized depending on the selected turn definition mode.
|
|
"""
|
|
if self._is_updating_turn_def:
|
|
return
|
|
self._is_updating_turn_def = True
|
|
try:
|
|
g_load = self.dm_lat_accel_g_var.get()
|
|
speed_kn = self.dm_speed_kn_var.get()
|
|
speed_fps = speed_kn * KNOTS_TO_FPS
|
|
if speed_fps > 0:
|
|
accel_fps2 = g_load * G_IN_FPS2
|
|
rate_rad_s = accel_fps2 / speed_fps
|
|
self.dm_turn_rate_var.set(round(math.degrees(rate_rad_s), 2))
|
|
except (ValueError, tk.TclError):
|
|
pass
|
|
finally:
|
|
self._is_updating_turn_def = False
|
|
|
|
def _on_turn_definition_change(self):
|
|
"""Toggle UI state between G-force and turn-rate based turn definition.
|
|
|
|
Enables/disables the appropriate widgets and triggers a conversion
|
|
to maintain consistent values between the two representations.
|
|
"""
|
|
if self.dm_turn_def_var.get() == "g_force":
|
|
self.dm_lat_g_spinbox.config(state=tk.NORMAL)
|
|
self.dm_turn_rate_spinbox.config(state=tk.DISABLED)
|
|
self._update_turn_rate_from_g()
|
|
else:
|
|
self.dm_lat_g_spinbox.config(state=tk.DISABLED)
|
|
self.dm_turn_rate_spinbox.config(state=tk.NORMAL)
|
|
self._update_g_from_turn_rate()
|
|
|
|
def _on_maneuver_type_change(self, event=None):
|
|
"""Switch visible parameter subframe according to selected maneuver.
|
|
|
|
Hides all subframes and shows only the one matching the current
|
|
ManeuverType selected by the user.
|
|
"""
|
|
try:
|
|
m_type = ManeuverType(self.maneuver_type_var.get())
|
|
except ValueError:
|
|
return
|
|
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):
|
|
"""Validate inputs and create a Waypoint instance from the dialog.
|
|
|
|
Reads values from the currently visible subframe and builds a
|
|
Waypoint dataclass instance which is stored in `self.result_waypoint`.
|
|
The dialog is closed on success; validation errors are shown to the
|
|
user in a modal messagebox.
|
|
"""
|
|
try:
|
|
m_type = ManeuverType(self.maneuver_type_var.get())
|
|
wp = Waypoint(maneuver_type=m_type)
|
|
if m_type == ManeuverType.FLY_TO_POINT:
|
|
(
|
|
wp.duration_s,
|
|
wp.target_range_nm,
|
|
wp.target_azimuth_deg,
|
|
wp.target_altitude_ft,
|
|
) = (
|
|
self.ftp_duration_var.get(),
|
|
self.ftp_range_var.get(),
|
|
self.ftp_az_var.get(),
|
|
self.ftp_alt_var.get(),
|
|
)
|
|
if self.is_first_waypoint:
|
|
wp.target_velocity_fps, wp.target_heading_deg = (
|
|
self.ftp_vel_var.get() * KNOTS_TO_FPS,
|
|
self.ftp_hdg_var.get(),
|
|
)
|
|
elif m_type == ManeuverType.FLY_FOR_DURATION:
|
|
(
|
|
wp.duration_s,
|
|
wp.target_velocity_fps,
|
|
wp.target_heading_deg,
|
|
wp.target_altitude_ft,
|
|
) = (
|
|
self.ffd_duration_var.get(),
|
|
self.ffd_vel_var.get() * KNOTS_TO_FPS,
|
|
self.ffd_hdg_var.get(),
|
|
self.ffd_alt_var.get(),
|
|
)
|
|
elif m_type == ManeuverType.DYNAMIC_MANEUVER:
|
|
(
|
|
wp.duration_s,
|
|
wp.maneuver_speed_fps,
|
|
wp.longitudinal_acceleration_g,
|
|
wp.vertical_acceleration_g,
|
|
wp.turn_direction,
|
|
) = (
|
|
self.dm_duration_var.get(),
|
|
self.dm_speed_kn_var.get() * KNOTS_TO_FPS,
|
|
self.dm_lon_accel_g_var.get(),
|
|
self.dm_ver_accel_g_var.get(),
|
|
TurnDirection(self.dm_turn_dir_var.get()),
|
|
)
|
|
if self.dm_turn_def_var.get() == "g_force":
|
|
wp.lateral_acceleration_g = self.dm_lat_accel_g_var.get()
|
|
else:
|
|
self._update_g_from_turn_rate()
|
|
wp.lateral_acceleration_g = self.dm_lat_accel_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):
|
|
"""Cancel editing and close the dialog without returning a waypoint."""
|
|
self.result_waypoint = None
|
|
self.destroy()
|