aggiunto le tre componenti delle accelerazioni e sistemata la preview nella simulazione

This commit is contained in:
VALLONGOL 2025-10-15 11:31:51 +02:00
parent 7eb4a7379e
commit d899c04ba8
3 changed files with 156 additions and 118 deletions

View File

@ -201,7 +201,7 @@
{ {
"maneuver_type": "Fly to Point", "maneuver_type": "Fly to Point",
"duration_s": 1.0, "duration_s": 1.0,
"target_range_nm": 80.0, "target_range_nm": 28.0,
"target_azimuth_deg": 0.0, "target_azimuth_deg": 0.0,
"target_altitude_ft": 10000.0, "target_altitude_ft": 10000.0,
"target_velocity_fps": 506.343, "target_velocity_fps": 506.343,
@ -213,7 +213,7 @@
}, },
{ {
"maneuver_type": "Fly for Duration", "maneuver_type": "Fly for Duration",
"duration_s": 10.0, "duration_s": 300.0,
"target_altitude_ft": 10000.0, "target_altitude_ft": 10000.0,
"target_velocity_fps": 506.343, "target_velocity_fps": 506.343,
"target_heading_deg": 180.0, "target_heading_deg": 180.0,
@ -221,6 +221,26 @@
"lateral_acceleration_g": 0.0, "lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0, "vertical_acceleration_g": 0.0,
"turn_direction": "Right" "turn_direction": "Right"
},
{
"maneuver_type": "Dynamic Maneuver",
"duration_s": 9.0,
"maneuver_speed_fps": 1519.0289999999995,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 9.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly for Duration",
"duration_s": 100.0,
"target_altitude_ft": 10000.0,
"target_velocity_fps": 1012.686,
"target_heading_deg": -90.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
} }
], ],
"use_spline": false "use_spline": false

View File

@ -15,7 +15,7 @@ MAX_TARGET_ID = 15
KNOTS_TO_FPS = 1.68781 KNOTS_TO_FPS = 1.68781
FPS_TO_KNOTS = 1 / KNOTS_TO_FPS FPS_TO_KNOTS = 1 / KNOTS_TO_FPS
NM_TO_FT = 6076.12 NM_TO_FT = 6076.12
G_IN_FPS2 = 32.174 # Standard gravity in ft/s^2 G_IN_FPS2 = 32.174
class ManeuverType(Enum): class ManeuverType(Enum):
FLY_TO_POINT = "Fly to Point" FLY_TO_POINT = "Fly to Point"
@ -31,39 +31,32 @@ class Waypoint:
"""Represents a segment of a target's trajectory, defining a maneuver.""" """Represents a segment of a target's trajectory, defining a maneuver."""
maneuver_type: ManeuverType = ManeuverType.FLY_FOR_DURATION maneuver_type: ManeuverType = ManeuverType.FLY_FOR_DURATION
# --- Common Parameters ---
duration_s: Optional[float] = 10.0 duration_s: Optional[float] = 10.0
# --- Parameters for FLY_TO_POINT ---
target_range_nm: Optional[float] = None target_range_nm: Optional[float] = None
target_azimuth_deg: Optional[float] = None target_azimuth_deg: Optional[float] = None
target_altitude_ft: Optional[float] = None target_altitude_ft: Optional[float] = None
# --- Parameters for FLY_FOR_DURATION (constant state) & FLY_TO_POINT (final state) ---
target_velocity_fps: Optional[float] = None target_velocity_fps: Optional[float] = None
target_heading_deg: Optional[float] = None target_heading_deg: Optional[float] = None
# --- Parameters for DYNAMIC_MANEUVER --- maneuver_speed_fps: Optional[float] = None
longitudinal_acceleration_g: Optional[float] = 0.0 longitudinal_acceleration_g: Optional[float] = 0.0
lateral_acceleration_g: Optional[float] = 0.0 lateral_acceleration_g: Optional[float] = 0.0
vertical_acceleration_g: Optional[float] = 0.0 vertical_acceleration_g: Optional[float] = 0.0
turn_direction: Optional[TurnDirection] = TurnDirection.RIGHT turn_direction: Optional[TurnDirection] = TurnDirection.RIGHT
def to_dict(self) -> Dict: def to_dict(self) -> Dict:
"""Serializes the waypoint to a dictionary."""
data = {"maneuver_type": self.maneuver_type.value} data = {"maneuver_type": self.maneuver_type.value}
for f in fields(self): for f in fields(self):
if not f.name.startswith('_') and f.name != "maneuver_type": if not f.name.startswith('_') and f.name != "maneuver_type":
val = getattr(self, f.name) val = getattr(self, f.name)
if isinstance(val, Enum): if isinstance(val, Enum): data[f.name] = val.value
data[f.name] = val.value elif val is not None: data[f.name] = val
elif val is not None:
data[f.name] = val
return data return data
@dataclass @dataclass
class Target: class Target:
"""Represents a dynamic 3D target with a programmable trajectory."""
target_id: int target_id: int
trajectory: List[Waypoint] = field(default_factory=list) trajectory: List[Waypoint] = field(default_factory=list)
active: bool = True active: bool = True
@ -80,7 +73,6 @@ class Target:
_pos_x_ft: float = field(init=False, repr=False, default=0.0) _pos_x_ft: float = field(init=False, repr=False, default=0.0)
_pos_y_ft: float = field(init=False, repr=False, default=0.0) _pos_y_ft: float = field(init=False, repr=False, default=0.0)
_pos_z_ft: float = field(init=False, repr=False, default=0.0) _pos_z_ft: float = field(init=False, repr=False, default=0.0)
_sim_time_s: float = field(init=False, default=0.0) _sim_time_s: float = field(init=False, default=0.0)
_total_duration_s: float = field(init=False, default=0.0) _total_duration_s: float = field(init=False, default=0.0)
_path: List[Tuple[float, float, float, float]] = field(init=False, repr=False, default_factory=list) _path: List[Tuple[float, float, float, float]] = field(init=False, repr=False, default_factory=list)
@ -93,15 +85,14 @@ class Target:
def reset_simulation(self): def reset_simulation(self):
self._sim_time_s = 0.0 self._sim_time_s = 0.0
self._generate_path() self._generate_path()
if self._path: if self._path:
initial_pos = self._path[0] initial_pos = self._path[0]
self._pos_x_ft, self._pos_y_ft, self._pos_z_ft = initial_pos[1], initial_pos[2], initial_pos[3] self._pos_x_ft, self._pos_y_ft, self._pos_z_ft = initial_pos[1], initial_pos[2], initial_pos[3]
if len(self._path) > 1: if len(self._path) > 1:
next_pos = self._path[1] next_pos = self._path[1]
dx, dy, dz = next_pos[1] - self._pos_x_ft, next_pos[2] - self._pos_y_ft, next_pos[3] - self._pos_z_ft dx, dy, dz = next_pos[1]-self._pos_x_ft, next_pos[2]-self._pos_y_ft, next_pos[3]-self._pos_z_ft
dt = next_pos[0] - initial_pos[0] dt = next_pos[0] - initial_pos[0]
dist_3d, dist_2d = math.sqrt(dx**2 + dy**2 + dz**2), math.sqrt(dx**2 + dy**2) dist_3d, dist_2d = math.sqrt(dx**2+dy**2+dz**2), math.sqrt(dx**2+dy**2)
self.current_heading_deg = math.degrees(math.atan2(dx, dy)) % 360 if dist_2d > 0.1 else 0.0 self.current_heading_deg = math.degrees(math.atan2(dx, dy)) % 360 if dist_2d > 0.1 else 0.0
self.current_pitch_deg = math.degrees(math.atan2(dz, dist_2d)) if dist_2d > 0.1 else 0.0 self.current_pitch_deg = math.degrees(math.atan2(dz, dist_2d)) if dist_2d > 0.1 else 0.0
self.current_velocity_fps = dist_3d / dt if dt > 0 else 0.0 self.current_velocity_fps = dist_3d / dt if dt > 0 else 0.0
@ -124,22 +115,17 @@ class Target:
first_wp = waypoints[0] first_wp = waypoints[0]
keyframes = [] keyframes = []
pos_ft = [(first_wp.target_range_nm or 0.0)*NM_TO_FT*math.sin(math.radians(first_wp.target_azimuth_deg or 0.0)),
(first_wp.target_range_nm or 0.0)*NM_TO_FT*math.cos(math.radians(first_wp.target_azimuth_deg or 0.0)),
first_wp.target_altitude_ft or 0.0]
pos_ft = [ speed_fps = first_wp.target_velocity_fps or 0.0
(first_wp.target_range_nm or 0.0) * NM_TO_FT * math.sin(math.radians(first_wp.target_azimuth_deg or 0.0)),
(first_wp.target_range_nm or 0.0) * NM_TO_FT * math.cos(math.radians(first_wp.target_azimuth_deg or 0.0)),
first_wp.target_altitude_ft or 0.0
]
heading_rad = math.radians(first_wp.target_heading_deg or 0.0) heading_rad = math.radians(first_wp.target_heading_deg or 0.0)
pitch_rad = 0.0 pitch_rad = 0.0
speed_fps = first_wp.target_velocity_fps or 0.0
vel_ft_s = [ vel_ft_s = [speed_fps*math.cos(pitch_rad)*math.sin(heading_rad),
speed_fps * math.cos(pitch_rad) * math.sin(heading_rad), speed_fps*math.cos(pitch_rad)*math.cos(heading_rad),
speed_fps * math.cos(pitch_rad) * math.cos(heading_rad), speed_fps*math.sin(pitch_rad)]
speed_fps * math.sin(pitch_rad)
]
keyframes.append((total_duration_s, pos_ft[0], pos_ft[1], pos_ft[2])) keyframes.append((total_duration_s, pos_ft[0], pos_ft[1], pos_ft[2]))
@ -148,13 +134,10 @@ class Target:
if i == 0: continue if i == 0: continue
if wp.maneuver_type == ManeuverType.FLY_TO_POINT: if wp.maneuver_type == ManeuverType.FLY_TO_POINT:
start_pos = list(pos_ft) start_pos, start_time = list(pos_ft), total_duration_s
start_time = total_duration_s target_pos = [(wp.target_range_nm or 0.0)*NM_TO_FT*math.sin(math.radians(wp.target_azimuth_deg or 0.0)),
target_pos = [ (wp.target_range_nm or 0.0)*NM_TO_FT*math.cos(math.radians(wp.target_azimuth_deg or 0.0)),
(wp.target_range_nm or 0.0) * NM_TO_FT * math.sin(math.radians(wp.target_azimuth_deg or 0.0)), wp.target_altitude_ft or pos_ft[2]]
(wp.target_range_nm or 0.0) * NM_TO_FT * math.cos(math.radians(wp.target_azimuth_deg or 0.0)),
wp.target_altitude_ft or pos_ft[2]
]
time_step, num_steps = 0.1, max(1, int(duration / 0.1)) time_step, num_steps = 0.1, max(1, int(duration / 0.1))
for step in range(1, num_steps + 1): for step in range(1, num_steps + 1):
progress = step / num_steps progress = step / num_steps
@ -163,28 +146,41 @@ class Target:
keyframes.append((current_time, pos_ft[0], pos_ft[1], pos_ft[2])) keyframes.append((current_time, pos_ft[0], pos_ft[1], pos_ft[2]))
total_duration_s += duration total_duration_s += duration
if wp.target_velocity_fps is not None and wp.target_heading_deg is not None: if wp.target_velocity_fps is not None and wp.target_heading_deg is not None:
heading_rad = math.radians(wp.target_heading_deg)
speed_fps = wp.target_velocity_fps speed_fps = wp.target_velocity_fps
heading_rad = math.radians(wp.target_heading_deg)
vel_ft_s = [speed_fps * math.sin(heading_rad), speed_fps * math.cos(heading_rad), 0] vel_ft_s = [speed_fps * math.sin(heading_rad), speed_fps * math.cos(heading_rad), 0]
elif wp.maneuver_type == ManeuverType.FLY_FOR_DURATION: elif wp.maneuver_type == ManeuverType.FLY_FOR_DURATION:
speed_fps = wp.target_velocity_fps if wp.target_velocity_fps is not None else speed_fps speed_fps = wp.target_velocity_fps if wp.target_velocity_fps is not None else speed_fps
heading_rad = math.radians(wp.target_heading_deg) if wp.target_heading_deg is not None else heading_rad heading_rad = math.radians(wp.target_heading_deg) if wp.target_heading_deg is not None else heading_rad
dist_moved = speed_fps * duration dist_moved = speed_fps * duration
pos_ft[0] += dist_moved * math.sin(heading_rad) pos_ft[0] += dist_moved * math.sin(heading_rad)
pos_ft[1] += dist_moved * math.cos(heading_rad) pos_ft[1] += dist_moved * math.cos(heading_rad)
if wp.target_altitude_ft is not None: pos_ft[2] = wp.target_altitude_ft if wp.target_altitude_ft is not None: pos_ft[2] = wp.target_altitude_ft
total_duration_s += duration total_duration_s += duration
keyframes.append((total_duration_s, pos_ft[0], pos_ft[1], pos_ft[2])) keyframes.append((total_duration_s, pos_ft[0], pos_ft[1], pos_ft[2]))
elif wp.maneuver_type == ManeuverType.DYNAMIC_MANEUVER: elif wp.maneuver_type == ManeuverType.DYNAMIC_MANEUVER:
if wp.maneuver_speed_fps is not None:
speed_fps = wp.maneuver_speed_fps
current_heading_rad = math.atan2(vel_ft_s[0], vel_ft_s[1])
current_speed_2d = math.sqrt(vel_ft_s[0]**2 + vel_ft_s[1]**2)
current_pitch_rad = math.atan2(vel_ft_s[2], current_speed_2d) if current_speed_2d > 0 else 0
vel_ft_s = [speed_fps*math.cos(current_pitch_rad)*math.sin(current_heading_rad),
speed_fps*math.cos(current_pitch_rad)*math.cos(current_heading_rad),
speed_fps*math.sin(current_pitch_rad)]
time_step, num_steps = 0.1, int(duration / 0.1) time_step, num_steps = 0.1, int(duration / 0.1)
accel_lon_fps2 = (wp.longitudinal_acceleration_g or 0.0) * G_IN_FPS2 accel_lon_fps2 = (wp.longitudinal_acceleration_g or 0.0) * G_IN_FPS2
accel_lat_fps2 = (wp.lateral_acceleration_g or 0.0) * G_IN_FPS2 accel_lat_fps2 = (wp.lateral_acceleration_g or 0.0) * G_IN_FPS2
accel_ver_fps2 = (wp.vertical_acceleration_g or 0.0) * G_IN_FPS2 accel_ver_fps2 = (wp.vertical_acceleration_g or 0.0) * G_IN_FPS2
dir_multiplier = 1.0 if wp.turn_direction == TurnDirection.RIGHT else -1.0
# --- MODIFICATION START ---
# A "Right" turn from the pilot's perspective is a clockwise rotation,
# which corresponds to a NEGATIVE angle in standard math.
dir_multiplier = -1.0 if wp.turn_direction == TurnDirection.RIGHT else 1.0
# --- MODIFICATION END ---
for _ in range(num_steps): for _ in range(num_steps):
pos_ft = [pos_ft[j] + vel_ft_s[j] * time_step for j in range(3)] pos_ft = [pos_ft[j] + vel_ft_s[j] * time_step for j in range(3)]
current_speed_fps = math.sqrt(sum(v**2 for v in vel_ft_s)) current_speed_fps = math.sqrt(sum(v**2 for v in vel_ft_s))
@ -210,7 +206,7 @@ class Target:
final_path = [] final_path = []
splined_points = catmull_rom_spline(points_to_spline, num_points=max(len(keyframes), 100)) splined_points = catmull_rom_spline(points_to_spline, num_points=max(len(keyframes), 100))
for i, point in enumerate(splined_points): for i, point in enumerate(splined_points):
time = (i / (len(splined_points) - 1)) * total_duration_s if len(splined_points) > 1 else 0.0 time = (i / (len(splined_points)-1)) * total_duration_s if len(splined_points) > 1 else 0.0
final_path.append((time, point[0], point[1], point[2])) final_path.append((time, point[0], point[1], point[2]))
return final_path, total_duration_s return final_path, total_duration_s
return keyframes, total_duration_s return keyframes, total_duration_s
@ -226,7 +222,7 @@ class Target:
if self._sim_time_s >= self._total_duration_s: self.active = False if self._sim_time_s >= self._total_duration_s: self.active = False
else: else:
p1, p2 = self._path[0], self._path[-1] p1, p2 = self._path[0], self._path[-1]
for i in range(len(self._path) - 1): for i in range(len(self._path)-1):
if self._path[i][0] <= current_sim_time <= self._path[i+1][0]: if self._path[i][0] <= current_sim_time <= self._path[i+1][0]:
p1, p2 = self._path[i], self._path[i+1] p1, p2 = self._path[i], self._path[i+1]
break break
@ -237,7 +233,7 @@ class Target:
self._pos_y_ft = p1[2] + (p2[2] - p1[2]) * progress self._pos_y_ft = p1[2] + (p2[2] - p1[2]) * progress
self._pos_z_ft = p1[3] + (p2[3] - p1[3]) * progress self._pos_z_ft = p1[3] + (p2[3] - p1[3]) * progress
dx, dy, dz = self._pos_x_ft - prev_x, self._pos_y_ft - prev_y, self._pos_z_ft - prev_z dx, dy, dz = self._pos_x_ft - prev_x, self._pos_y_ft - prev_y, self._pos_z_ft - prev_z
dist_3d, dist_2d = math.sqrt(dx**2 + dy**2 + dz**2), math.sqrt(dx**2 + dy**2) dist_3d, dist_2d = math.sqrt(dx**2+dy**2+dz**2), math.sqrt(dx**2+dy**2)
if delta_time_s > 0: self.current_velocity_fps = dist_3d / delta_time_s if delta_time_s > 0: self.current_velocity_fps = dist_3d / delta_time_s
if dist_2d > 0.1: if dist_2d > 0.1:
self.current_heading_deg = math.degrees(math.atan2(dx, dy)) % 360 self.current_heading_deg = math.degrees(math.atan2(dx, dy)) % 360
@ -245,34 +241,30 @@ class Target:
self._update_current_polar_coords() self._update_current_polar_coords()
def _update_current_polar_coords(self): def _update_current_polar_coords(self):
self.current_range_nm = math.sqrt(self._pos_x_ft**2 + self._pos_y_ft**2) / NM_TO_FT self.current_range_nm = math.sqrt(self._pos_x_ft**2+self._pos_y_ft**2) / NM_TO_FT
self.current_azimuth_deg = math.degrees(math.atan2(self._pos_x_ft, self._pos_y_ft)) % 360 self.current_azimuth_deg = math.degrees(math.atan2(self._pos_x_ft, self._pos_y_ft)) % 360
self.current_altitude_ft = self._pos_z_ft self.current_altitude_ft = self._pos_z_ft
def to_dict(self) -> Dict: def to_dict(self) -> Dict:
return { "target_id": self.target_id, "active": self.active, "traceable": self.traceable, "trajectory": [wp.to_dict() for wp in self.trajectory], "use_spline": self.use_spline } return {"target_id": self.target_id, "active": self.active, "traceable": self.traceable,
"trajectory": [wp.to_dict() for wp in self.trajectory], "use_spline": self.use_spline}
class Scenario: class Scenario:
def __init__(self, name: str = "Untitled Scenario"): def __init__(self, name: str = "Untitled Scenario"):
self.name = name self.name = name
self.targets: Dict[int, Target] = {} self.targets: Dict[int, Target] = {}
def add_target(self, target: Target): def add_target(self, target: Target): self.targets[target.target_id] = target
self.targets[target.target_id] = target def get_target(self, target_id: int) -> Optional[Target]: return self.targets.get(target_id)
def get_target(self, target_id: int) -> Optional[Target]:
return self.targets.get(target_id)
def remove_target(self, target_id: int): def remove_target(self, target_id: int):
if target_id in self.targets: del self.targets[target_id] if target_id in self.targets: del self.targets[target_id]
def get_all_targets(self) -> List[Target]: def get_all_targets(self) -> List[Target]: return list(self.targets.values())
return list(self.targets.values())
def reset_simulation(self): def reset_simulation(self):
for target in self.targets.values(): target.reset_simulation() for target in self.targets.values(): target.reset_simulation()
def update_state(self, delta_time_s: float): def update_state(self, delta_time_s: float):
for target in self.targets.values(): target.update_state(delta_time_s) for target in self.targets.values(): target.update_state(delta_time_s)
def is_finished(self) -> bool: def is_finished(self) -> bool: return all(not target.active for target in self.targets.values())
return all(not target.active for target in self.targets.values())
def to_dict(self) -> Dict: def to_dict(self) -> Dict:
return { "name": self.name, "targets": [target.to_dict() for target in self.get_all_targets()] } return {"name": self.name, "targets": [t.to_dict() for t in self.get_all_targets()]}
@classmethod @classmethod
def from_dict(cls, data: Dict) -> Scenario: def from_dict(cls, data: Dict) -> Scenario:
scenario = cls(name=data.get("name", "Loaded Scenario")) scenario = cls(name=data.get("name", "Loaded Scenario"))
@ -280,21 +272,19 @@ class Scenario:
try: try:
waypoints = [] waypoints = []
for wp_data in target_data.get("trajectory", []): for wp_data in target_data.get("trajectory", []):
# --- BACKWARD COMPATIBILITY ---
# Convert old Constant Turn to new Dynamic Maneuver
if wp_data.get("maneuver_type") == "Constant Turn": if wp_data.get("maneuver_type") == "Constant Turn":
wp_data["maneuver_type"] = "Dynamic Maneuver" wp_data["maneuver_type"] = "Dynamic Maneuver"
wp_data["longitudinal_acceleration_g"] = 0.0 wp_data["longitudinal_acceleration_g"] = 0.0
wp_data["vertical_acceleration_g"] = 0.0 wp_data["vertical_acceleration_g"] = 0.0
wp_data["maneuver_type"] = ManeuverType(wp_data["maneuver_type"]) wp_data["maneuver_type"] = ManeuverType(wp_data["maneuver_type"])
if "turn_direction" in wp_data and wp_data["turn_direction"]: if "turn_direction" in wp_data and wp_data["turn_direction"]:
wp_data["turn_direction"] = TurnDirection(wp_data["turn_direction"]) wp_data["turn_direction"] = TurnDirection(wp_data["turn_direction"])
valid_keys = {f.name for f in fields(Waypoint)} valid_keys = {f.name for f in fields(Waypoint)}
filtered_wp_data = {k: v for k, v in wp_data.items() if k in valid_keys} filtered_wp_data = {k: v for k, v in wp_data.items() if k in valid_keys}
waypoints.append(Waypoint(**filtered_wp_data)) waypoints.append(Waypoint(**filtered_wp_data))
target = Target(target_id=target_data["target_id"], active=target_data.get("active", True),
target = Target(target_id=target_data["target_id"], active=target_data.get("active", True), traceable=target_data.get("traceable", True), trajectory=waypoints, use_spline=target_data.get("use_spline", False)) traceable=target_data.get("traceable", True), trajectory=waypoints,
use_spline=target_data.get("use_spline", False))
scenario.add_target(target) scenario.add_target(target)
except Exception as e: except Exception as e:
print(f"Skipping invalid target data: {target_data}. Error: {e}") print(f"Skipping invalid target data: {target_data}. Error: {e}")

View File

@ -6,8 +6,9 @@ A modal dialog window for creating or editing a single Waypoint.
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox from tkinter import ttk, messagebox
from typing import Optional from typing import Optional
import math
from target_simulator.core.models import Waypoint, ManeuverType, TurnDirection, KNOTS_TO_FPS, FPS_TO_KNOTS from target_simulator.core.models import Waypoint, ManeuverType, TurnDirection, KNOTS_TO_FPS, FPS_TO_KNOTS, G_IN_FPS2
class WaypointEditorWindow(tk.Toplevel): class WaypointEditorWindow(tk.Toplevel):
"""A dialog for entering and editing data for a single waypoint.""" """A dialog for entering and editing data for a single waypoint."""
@ -23,6 +24,7 @@ class WaypointEditorWindow(tk.Toplevel):
self.is_first_waypoint = is_first_waypoint self.is_first_waypoint = is_first_waypoint
self.result_waypoint: Optional[Waypoint] = None self.result_waypoint: Optional[Waypoint] = None
self._is_updating_turn_def = False # Flag to prevent update loops
self._setup_variables() self._setup_variables()
self._create_widgets() self._create_widgets()
@ -40,7 +42,6 @@ class WaypointEditorWindow(tk.Toplevel):
"""Initializes all tk Variables.""" """Initializes all tk Variables."""
self.maneuver_type_var = tk.StringVar() self.maneuver_type_var = tk.StringVar()
# --- Fly to Point ---
self.ftp_duration_var = tk.DoubleVar(value=10.0) self.ftp_duration_var = tk.DoubleVar(value=10.0)
self.ftp_range_var = tk.DoubleVar(value=20.0) self.ftp_range_var = tk.DoubleVar(value=20.0)
self.ftp_az_var = tk.DoubleVar(value=0.0) self.ftp_az_var = tk.DoubleVar(value=0.0)
@ -48,23 +49,23 @@ class WaypointEditorWindow(tk.Toplevel):
self.ftp_vel_var = tk.DoubleVar(value=300.0) self.ftp_vel_var = tk.DoubleVar(value=300.0)
self.ftp_hdg_var = tk.DoubleVar(value=180.0) self.ftp_hdg_var = tk.DoubleVar(value=180.0)
# --- Fly for Duration ---
self.ffd_duration_var = tk.DoubleVar(value=10.0) self.ffd_duration_var = tk.DoubleVar(value=10.0)
self.ffd_vel_var = tk.DoubleVar(value=300.0) self.ffd_vel_var = tk.DoubleVar(value=300.0)
self.ffd_hdg_var = tk.DoubleVar(value=90.0) self.ffd_hdg_var = tk.DoubleVar(value=90.0)
self.ffd_alt_var = tk.DoubleVar(value=10000.0) self.ffd_alt_var = tk.DoubleVar(value=10000.0)
# --- Dynamic Maneuver ---
self.dm_duration_var = tk.DoubleVar(value=10.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_lon_accel_g_var = tk.DoubleVar(value=0.0)
self.dm_lat_accel_g_var = tk.DoubleVar(value=0.0)
self.dm_ver_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) self.dm_turn_dir_var = tk.StringVar(value=TurnDirection.RIGHT.value)
def _load_waypoint_data(self, wp: Waypoint): def _load_waypoint_data(self, wp: Waypoint):
"""Populates the fields with data from an existing waypoint.""" """Populates the fields with data from an existing waypoint."""
self.maneuver_type_var.set(wp.maneuver_type.value) self.maneuver_type_var.set(wp.maneuver_type.value)
if wp.maneuver_type == ManeuverType.FLY_TO_POINT: if wp.maneuver_type == ManeuverType.FLY_TO_POINT:
self.ftp_duration_var.set(wp.duration_s or 10.0) 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_range_var.set(wp.target_range_nm or 0.0)
@ -72,49 +73,44 @@ class WaypointEditorWindow(tk.Toplevel):
self.ftp_alt_var.set(wp.target_altitude_ft or 10000.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_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) self.ftp_hdg_var.set(wp.target_heading_deg or 0.0)
elif wp.maneuver_type == ManeuverType.FLY_FOR_DURATION: elif wp.maneuver_type == ManeuverType.FLY_FOR_DURATION:
self.ffd_duration_var.set(wp.duration_s or 10.0) 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_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_hdg_var.set(wp.target_heading_deg or 0.0)
self.ffd_alt_var.set(wp.target_altitude_ft or 10000.0) self.ffd_alt_var.set(wp.target_altitude_ft or 10000.0)
elif wp.maneuver_type == ManeuverType.DYNAMIC_MANEUVER: elif wp.maneuver_type == ManeuverType.DYNAMIC_MANEUVER:
self.dm_duration_var.set(wp.duration_s or 10.0) 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_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_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) self.dm_ver_accel_g_var.set(wp.vertical_acceleration_g or 0.0)
if wp.turn_direction: if wp.turn_direction: self.dm_turn_dir_var.set(wp.turn_direction.value)
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): def _center_window(self):
self.update_idletasks() self.update_idletasks()
parent = self.master parent = self.master
x = parent.winfo_x() + (parent.winfo_width() // 2) - (self.winfo_width() // 2) x = parent.winfo_x() + (parent.winfo_width()//2) - (self.winfo_width()//2)
y = parent.winfo_y() + (parent.winfo_height() // 2) - (self.winfo_height() // 2) y = parent.winfo_y() + (parent.winfo_height()//2) - (self.winfo_height()//2)
self.geometry(f"+{x}+{y}") self.geometry(f"+{x}+{y}")
def _create_widgets(self): def _create_widgets(self):
main_frame = ttk.Frame(self, padding=10) main_frame = ttk.Frame(self, padding=10)
main_frame.pack(fill=tk.BOTH, expand=True) main_frame.pack(fill=tk.BOTH, expand=True)
type_frame = ttk.Frame(main_frame) type_frame = ttk.Frame(main_frame)
type_frame.pack(fill=tk.X, pady=(0, 10)) type_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(type_frame, text="Maneuver Type:").pack(side=tk.LEFT) ttk.Label(type_frame, text="Maneuver Type:").pack(side=tk.LEFT)
self.maneuver_combo = ttk.Combobox(type_frame, textvariable=self.maneuver_type_var, self.maneuver_combo = ttk.Combobox(type_frame, textvariable=self.maneuver_type_var, values=[m.value for m in ManeuverType], state="readonly")
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.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=5)
self.maneuver_combo.bind("<<ComboboxSelected>>", self._on_maneuver_type_change) 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_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.fly_for_duration_frame = self._create_fly_for_duration_frame(main_frame)
self.dynamic_maneuver_frame = self._create_dynamic_maneuver_frame(main_frame) self.dynamic_maneuver_frame = self._create_dynamic_maneuver_frame(main_frame)
button_frame = ttk.Frame(main_frame) button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=tk.X, pady=(15, 0), side=tk.BOTTOM) 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="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) ttk.Button(button_frame, text="Cancel", command=self._on_cancel).pack(side=tk.RIGHT)
if self.is_first_waypoint: if self.is_first_waypoint:
self.maneuver_type_var.set(ManeuverType.FLY_TO_POINT.value) self.maneuver_type_var.set(ManeuverType.FLY_TO_POINT.value)
self.maneuver_combo.config(state=tk.DISABLED) self.maneuver_combo.config(state=tk.DISABLED)
@ -125,7 +121,7 @@ class WaypointEditorWindow(tk.Toplevel):
frame = ttk.LabelFrame(parent, text="Position and State", padding=10) frame = ttk.LabelFrame(parent, text="Position and State", padding=10)
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
ttk.Label(frame, text="Duration to Point (s):").grid(row=0, column=0, sticky=tk.W, pady=2) 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) 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)
ttk.Label(frame, text="Target Range (NM):").grid(row=1, column=0, sticky=tk.W, pady=2) 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) ttk.Spinbox(frame, from_=0, to=1000, textvariable=self.ftp_range_var, width=10).grid(row=1, column=1, sticky=tk.EW, pady=2)
ttk.Label(frame, text="Target Azimuth (°):").grid(row=2, column=0, sticky=tk.W, pady=2) ttk.Label(frame, text="Target Azimuth (°):").grid(row=2, column=0, sticky=tk.W, pady=2)
@ -153,64 +149,96 @@ class WaypointEditorWindow(tk.Toplevel):
return frame return frame
def _create_dynamic_maneuver_frame(self, parent) -> ttk.Frame: def _create_dynamic_maneuver_frame(self, parent) -> ttk.Frame:
frame = ttk.LabelFrame(parent, text="Applied Accelerations", padding=10) frame = ttk.LabelFrame(parent, text="Dynamic Maneuver Controls", padding=10)
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
ttk.Label(frame, text="Duration (s):").grid(row=0, column=0, sticky=tk.W, pady=2) 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.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="Longitudinal Accel (g):").grid(row=1, column=0, sticky=tk.W, pady=2) ttk.Label(frame, text="Maneuver Speed (kn):").grid(row=1, 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=1, column=1, sticky=tk.EW, 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)
ttk.Label(frame, text="Lateral Accel (g):").grid(row=2, column=0, sticky=tk.W, pady=2) speed_spinbox.grid(row=1, column=1, sticky=tk.EW, pady=2)
ttk.Spinbox(frame, from_=0, to=9, format="%.2f", increment=0.1, textvariable=self.dm_lat_accel_g_var, width=10).grid(row=2, column=1, sticky=tk.EW, pady=2) turn_frame = ttk.LabelFrame(frame, text="Turn Definition", padding=5)
ttk.Label(frame, text="Turn Direction:").grid(row=3, column=0, sticky=tk.W, pady=2) turn_frame.grid(row=2, column=0, columnspan=2, sticky=tk.EW, pady=5)
turn_dir_combo = ttk.Combobox(frame, textvariable=self.dm_turn_dir_var, values=[d.value for d in TurnDirection], state="readonly", width=8) 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)
turn_dir_combo.grid(row=3, column=1, sticky=tk.W, pady=2) 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.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) 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 return frame
def _on_maneuver_type_change(self, event=None): def _update_g_from_turn_rate(self):
if self._is_updating_turn_def: return
self._is_updating_turn_def = True
try: try:
m_type = ManeuverType(self.maneuver_type_var.get()) rate_deg_s = self.dm_turn_rate_var.get()
except ValueError: return 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):
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):
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):
try: m_type = ManeuverType(self.maneuver_type_var.get())
except ValueError: return
self.fly_to_point_frame.pack_forget() self.fly_to_point_frame.pack_forget()
self.fly_for_duration_frame.pack_forget() self.fly_for_duration_frame.pack_forget()
self.dynamic_maneuver_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)
if m_type == ManeuverType.FLY_TO_POINT: elif m_type == ManeuverType.FLY_FOR_DURATION: self.fly_for_duration_frame.pack(fill=tk.BOTH, expand=True)
self.fly_to_point_frame.pack(fill=tk.BOTH, expand=True) elif m_type == ManeuverType.DYNAMIC_MANEUVER: self.dynamic_maneuver_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): def _on_ok(self):
try: try:
m_type = ManeuverType(self.maneuver_type_var.get()) m_type = ManeuverType(self.maneuver_type_var.get())
wp = Waypoint(maneuver_type=m_type) wp = Waypoint(maneuver_type=m_type)
if m_type == ManeuverType.FLY_TO_POINT: if m_type == ManeuverType.FLY_TO_POINT:
wp.duration_s = self.ftp_duration_var.get() 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()
wp.target_range_nm = self.ftp_range_var.get()
wp.target_azimuth_deg = self.ftp_az_var.get()
wp.target_altitude_ft = self.ftp_alt_var.get()
if self.is_first_waypoint: if self.is_first_waypoint:
wp.target_velocity_fps = self.ftp_vel_var.get() * KNOTS_TO_FPS wp.target_velocity_fps, wp.target_heading_deg = self.ftp_vel_var.get()*KNOTS_TO_FPS, self.ftp_hdg_var.get()
wp.target_heading_deg = self.ftp_hdg_var.get()
elif m_type == ManeuverType.FLY_FOR_DURATION: elif m_type == ManeuverType.FLY_FOR_DURATION:
wp.duration_s = self.ffd_duration_var.get() 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()
wp.target_velocity_fps = self.ffd_vel_var.get() * KNOTS_TO_FPS
wp.target_heading_deg = self.ffd_hdg_var.get()
wp.target_altitude_ft = self.ffd_alt_var.get()
elif m_type == ManeuverType.DYNAMIC_MANEUVER: elif m_type == ManeuverType.DYNAMIC_MANEUVER:
wp.duration_s = self.dm_duration_var.get() 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())
wp.longitudinal_acceleration_g = self.dm_lon_accel_g_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() wp.lateral_acceleration_g = self.dm_lat_accel_g_var.get()
wp.vertical_acceleration_g = self.dm_ver_accel_g_var.get()
wp.turn_direction = TurnDirection(self.dm_turn_dir_var.get())
self.result_waypoint = wp self.result_waypoint = wp
self.destroy() self.destroy()
except (ValueError, tk.TclError) as e: except (ValueError, tk.TclError) as e: