S1005403_RisCC/target_simulator/gui/add_target_window.py
2025-11-13 10:57:10 +01:00

166 lines
5.9 KiB
Python

"""Toplevel window for adding or editing a target.
This module defines the `AddTargetWindow` class, which provides a dialog
for users to input the initial parameters of a new target.
"""
import tkinter as tk
from tkinter import ttk, messagebox
from target_simulator.core.models import Target, MIN_TARGET_ID, MAX_TARGET_ID, Waypoint
class AddTargetWindow(tk.Toplevel):
"""A modal dialog window for adding a new target.
This window prompts the user for the target's initial kinematic parameters,
such as position, velocity, and altitude.
"""
def __init__(self, master, existing_ids: list[int]):
"""
Initialize the AddTargetWindow.
Args:
master (tk.Widget): The parent widget.
existing_ids (list[int]): A list of target IDs that are already in use.
"""
super().__init__(master)
self.master_view = master
self.new_target: Target | None = None
self.existing_ids = existing_ids
self.title("Add New Target")
self.transient(master)
self.grab_set()
self.resizable(False, False)
self._create_widgets()
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
# wait_window will be called by the caller after pre-populating data
def _create_widgets(self):
"""Create and layout all the widgets for the dialog."""
main_frame = ttk.Frame(self, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
# --- Target ID ---
id_frame = ttk.LabelFrame(main_frame, text="Target Identifier")
id_frame.pack(fill=tk.X, expand=True, pady=5)
ttk.Label(id_frame, text="Target ID:").pack(side=tk.LEFT, padx=5, pady=5)
# Propose a valid ID
proposed_id = next(
(
i
for i in range(MIN_TARGET_ID, MAX_TARGET_ID + 1)
if i not in self.existing_ids
),
None,
)
self.id_var = tk.IntVar(
value=proposed_id if proposed_id is not None else MIN_TARGET_ID
)
self.id_spinbox = ttk.Spinbox(
id_frame,
from_=MIN_TARGET_ID,
to=MAX_TARGET_ID,
textvariable=self.id_var,
width=10,
)
self.id_spinbox.pack(side=tk.LEFT, padx=5, pady=5)
# --- Kinematics Frame ---
kinematics_frame = ttk.LabelFrame(main_frame, text="Kinematics (Spherical)")
kinematics_frame.pack(fill=tk.X, expand=True, pady=5)
self.range_var = tk.DoubleVar(value=20.0)
self.az_var = tk.DoubleVar(value=45.0)
self.vel_knots_var = tk.DoubleVar(value=500.0)
self.hdg_var = tk.DoubleVar(value=270.0)
self.alt_var = tk.DoubleVar(value=10000.0)
# Using a grid for alignment
ttk.Label(kinematics_frame, text="Range (NM):").grid(
row=0, column=0, sticky=tk.W, padx=5, pady=2
)
ttk.Spinbox(
kinematics_frame, from_=0, to=1000, textvariable=self.range_var
).grid(row=0, column=1, sticky=tk.EW, padx=5, pady=2)
ttk.Label(kinematics_frame, text="Azimuth (°):").grid(
row=1, column=0, sticky=tk.W, padx=5, pady=2
)
ttk.Spinbox(
kinematics_frame, from_=-180, to=180, textvariable=self.az_var
).grid(row=1, column=1, sticky=tk.EW, padx=5, pady=2)
ttk.Label(kinematics_frame, text="Velocity (knots):").grid(
row=2, column=0, sticky=tk.W, padx=5, pady=2
)
ttk.Spinbox(
kinematics_frame, from_=0, to=2000, textvariable=self.vel_knots_var
).grid(row=2, column=1, sticky=tk.EW, padx=5, pady=2)
ttk.Label(kinematics_frame, text="Heading (°):").grid(
row=3, column=0, sticky=tk.W, padx=5, pady=2
)
ttk.Spinbox(kinematics_frame, from_=0, to=360, textvariable=self.hdg_var).grid(
row=3, column=1, sticky=tk.EW, padx=5, pady=2
)
ttk.Label(kinematics_frame, text="Altitude (ft):").grid(
row=4, column=0, sticky=tk.W, padx=5, pady=2
)
ttk.Spinbox(
kinematics_frame, from_=-1000, to=80000, textvariable=self.alt_var
).grid(row=4, column=1, sticky=tk.EW, padx=5, pady=2)
# --- Buttons ---
button_frame = ttk.Frame(main_frame)
button_frame.pack(pady=10)
ttk.Button(button_frame, text="OK", command=self._on_ok).pack(
side=tk.LEFT, padx=5
)
ttk.Button(button_frame, text="Cancel", command=self._on_cancel).pack(
side=tk.LEFT, padx=5
)
def _on_cancel(self):
"""Handle the Cancel button click or window close event."""
self.new_target = None
self.destroy()
def _on_ok(self):
"""
Validate user input, create a new Target object, and close the window.
If validation fails, an error message is displayed.
"""
target_id = self.id_var.get()
if target_id in self.existing_ids:
messagebox.showerror(
"Invalid ID", f"Target ID {target_id} is already in use.", parent=self
)
return
try:
# Conversion from knots to fps
knots_to_fps = 1.68781
velocity_fps = self.vel_knots_var.get() * knots_to_fps
default_trajectory = [
Waypoint(
duration_s=3600,
target_velocity_fps=velocity_fps,
target_heading_deg=self.hdg_var.get(),
)
]
self.new_target = Target(
target_id=target_id,
initial_range_nm=self.range_var.get(),
initial_azimuth_deg=self.az_var.get(),
initial_altitude_ft=self.alt_var.get(),
trajectory=default_trajectory,
)
self.destroy()
except ValueError as e:
messagebox.showerror("Validation Error", str(e), parent=self)