From d899c04ba8cefb71aaabaeedd7f92a679e2a34c0 Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Wed, 15 Oct 2025 11:31:51 +0200 Subject: [PATCH] aggiunto le tre componenti delle accelerazioni e sistemata la preview nella simulazione --- settings.json | 24 ++- target_simulator/core/models.py | 108 ++++++------- .../gui/waypoint_editor_window.py | 142 +++++++++++------- 3 files changed, 156 insertions(+), 118 deletions(-) diff --git a/settings.json b/settings.json index b77dbc1..5ee9cc5 100644 --- a/settings.json +++ b/settings.json @@ -201,7 +201,7 @@ { "maneuver_type": "Fly to Point", "duration_s": 1.0, - "target_range_nm": 80.0, + "target_range_nm": 28.0, "target_azimuth_deg": 0.0, "target_altitude_ft": 10000.0, "target_velocity_fps": 506.343, @@ -213,7 +213,7 @@ }, { "maneuver_type": "Fly for Duration", - "duration_s": 10.0, + "duration_s": 300.0, "target_altitude_ft": 10000.0, "target_velocity_fps": 506.343, "target_heading_deg": 180.0, @@ -221,6 +221,26 @@ "lateral_acceleration_g": 0.0, "vertical_acceleration_g": 0.0, "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 diff --git a/target_simulator/core/models.py b/target_simulator/core/models.py index fe45b32..57ce826 100644 --- a/target_simulator/core/models.py +++ b/target_simulator/core/models.py @@ -15,7 +15,7 @@ MAX_TARGET_ID = 15 KNOTS_TO_FPS = 1.68781 FPS_TO_KNOTS = 1 / KNOTS_TO_FPS NM_TO_FT = 6076.12 -G_IN_FPS2 = 32.174 # Standard gravity in ft/s^2 +G_IN_FPS2 = 32.174 class ManeuverType(Enum): FLY_TO_POINT = "Fly to Point" @@ -31,39 +31,32 @@ class Waypoint: """Represents a segment of a target's trajectory, defining a maneuver.""" maneuver_type: ManeuverType = ManeuverType.FLY_FOR_DURATION - # --- Common Parameters --- duration_s: Optional[float] = 10.0 - # --- Parameters for FLY_TO_POINT --- target_range_nm: Optional[float] = None target_azimuth_deg: 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_heading_deg: Optional[float] = None - # --- Parameters for DYNAMIC_MANEUVER --- + maneuver_speed_fps: Optional[float] = None longitudinal_acceleration_g: Optional[float] = 0.0 lateral_acceleration_g: Optional[float] = 0.0 vertical_acceleration_g: Optional[float] = 0.0 turn_direction: Optional[TurnDirection] = TurnDirection.RIGHT def to_dict(self) -> Dict: - """Serializes the waypoint to a dictionary.""" data = {"maneuver_type": self.maneuver_type.value} for f in fields(self): if not f.name.startswith('_') and f.name != "maneuver_type": val = getattr(self, f.name) - if isinstance(val, Enum): - data[f.name] = val.value - elif val is not None: - data[f.name] = val + if isinstance(val, Enum): data[f.name] = val.value + elif val is not None: data[f.name] = val return data @dataclass class Target: - """Represents a dynamic 3D target with a programmable trajectory.""" target_id: int trajectory: List[Waypoint] = field(default_factory=list) active: bool = True @@ -80,7 +73,6 @@ class Target: _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_z_ft: float = field(init=False, repr=False, default=0.0) - _sim_time_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) @@ -93,15 +85,14 @@ class Target: def reset_simulation(self): self._sim_time_s = 0.0 self._generate_path() - if self._path: 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] if len(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] - 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_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 @@ -124,22 +115,17 @@ class Target: first_wp = waypoints[0] 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 = [(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] + speed_fps = first_wp.target_velocity_fps or 0.0 heading_rad = math.radians(first_wp.target_heading_deg or 0.0) pitch_rad = 0.0 - speed_fps = first_wp.target_velocity_fps or 0.0 - vel_ft_s = [ - speed_fps * math.cos(pitch_rad) * math.sin(heading_rad), - speed_fps * math.cos(pitch_rad) * math.cos(heading_rad), - speed_fps * math.sin(pitch_rad) - ] + vel_ft_s = [speed_fps*math.cos(pitch_rad)*math.sin(heading_rad), + speed_fps*math.cos(pitch_rad)*math.cos(heading_rad), + speed_fps*math.sin(pitch_rad)] 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 wp.maneuver_type == ManeuverType.FLY_TO_POINT: - start_pos = list(pos_ft) - 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)), - (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] - ] + start_pos, start_time = list(pos_ft), 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)), + (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)) for step in range(1, num_steps + 1): progress = step / num_steps @@ -163,28 +146,41 @@ class Target: keyframes.append((current_time, pos_ft[0], pos_ft[1], pos_ft[2])) total_duration_s += duration 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 + heading_rad = math.radians(wp.target_heading_deg) vel_ft_s = [speed_fps * math.sin(heading_rad), speed_fps * math.cos(heading_rad), 0] - + 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 heading_rad = math.radians(wp.target_heading_deg) if wp.target_heading_deg is not None else heading_rad - dist_moved = speed_fps * duration pos_ft[0] += dist_moved * math.sin(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 - total_duration_s += duration keyframes.append((total_duration_s, pos_ft[0], pos_ft[1], pos_ft[2])) 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) 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_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): 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)) @@ -210,7 +206,7 @@ class Target: final_path = [] splined_points = catmull_rom_spline(points_to_spline, num_points=max(len(keyframes), 100)) 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])) return final_path, 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 else: 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]: p1, p2 = self._path[i], self._path[i+1] break @@ -237,7 +233,7 @@ class Target: self._pos_y_ft = p1[2] + (p2[2] - p1[2]) * 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 - 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 dist_2d > 0.1: self.current_heading_deg = math.degrees(math.atan2(dx, dy)) % 360 @@ -245,34 +241,30 @@ class Target: self._update_current_polar_coords() 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_altitude_ft = self._pos_z_ft 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: def __init__(self, name: str = "Untitled Scenario"): self.name = name self.targets: Dict[int, Target] = {} - def add_target(self, target: Target): - self.targets[target.target_id] = target - def get_target(self, target_id: int) -> Optional[Target]: - return self.targets.get(target_id) + def add_target(self, target: Target): self.targets[target.target_id] = target + def get_target(self, target_id: int) -> Optional[Target]: return self.targets.get(target_id) def remove_target(self, target_id: int): if target_id in self.targets: del self.targets[target_id] - def get_all_targets(self) -> List[Target]: - return list(self.targets.values()) + def get_all_targets(self) -> List[Target]: return list(self.targets.values()) def reset_simulation(self): for target in self.targets.values(): target.reset_simulation() def update_state(self, delta_time_s: float): for target in self.targets.values(): target.update_state(delta_time_s) - def is_finished(self) -> bool: - return all(not target.active for target in self.targets.values()) + def is_finished(self) -> bool: return all(not target.active for target in self.targets.values()) 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 def from_dict(cls, data: Dict) -> Scenario: scenario = cls(name=data.get("name", "Loaded Scenario")) @@ -280,21 +272,19 @@ class Scenario: try: waypoints = [] 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": wp_data["maneuver_type"] = "Dynamic Maneuver" wp_data["longitudinal_acceleration_g"] = 0.0 wp_data["vertical_acceleration_g"] = 0.0 - wp_data["maneuver_type"] = ManeuverType(wp_data["maneuver_type"]) if "turn_direction" in wp_data and wp_data["turn_direction"]: wp_data["turn_direction"] = TurnDirection(wp_data["turn_direction"]) 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} waypoints.append(Waypoint(**filtered_wp_data)) - - 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)) + 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)) scenario.add_target(target) except Exception as e: print(f"Skipping invalid target data: {target_data}. Error: {e}") diff --git a/target_simulator/gui/waypoint_editor_window.py b/target_simulator/gui/waypoint_editor_window.py index c6a370e..45fcf3f 100644 --- a/target_simulator/gui/waypoint_editor_window.py +++ b/target_simulator/gui/waypoint_editor_window.py @@ -6,8 +6,9 @@ A modal dialog window for creating or editing a single Waypoint. import tkinter as tk from tkinter import ttk, messagebox 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): """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.result_waypoint: Optional[Waypoint] = None + self._is_updating_turn_def = False # Flag to prevent update loops self._setup_variables() self._create_widgets() @@ -40,7 +42,6 @@ class WaypointEditorWindow(tk.Toplevel): """Initializes all tk Variables.""" self.maneuver_type_var = tk.StringVar() - # --- Fly to Point --- 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) @@ -48,23 +49,23 @@ class WaypointEditorWindow(tk.Toplevel): self.ftp_vel_var = tk.DoubleVar(value=300.0) self.ftp_hdg_var = tk.DoubleVar(value=180.0) - # --- Fly for Duration --- 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) - # --- Dynamic Maneuver --- 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_lat_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) @@ -72,49 +73,44 @@ class WaypointEditorWindow(tk.Toplevel): 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) + 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): 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) + 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) - 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 = 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("<>", 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) @@ -125,7 +121,7 @@ class WaypointEditorWindow(tk.Toplevel): frame = ttk.LabelFrame(parent, text="Position and State", padding=10) frame.columnconfigure(1, weight=1) 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.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) @@ -153,64 +149,96 @@ class WaypointEditorWindow(tk.Toplevel): return 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) 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="Longitudinal Accel (g):").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) - ttk.Label(frame, text="Lateral Accel (g):").grid(row=2, column=0, sticky=tk.W, 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) - ttk.Label(frame, text="Turn Direction:").grid(row=3, column=0, sticky=tk.W, pady=2) - turn_dir_combo = ttk.Combobox(frame, textvariable=self.dm_turn_dir_var, values=[d.value for d in TurnDirection], state="readonly", width=8) - turn_dir_combo.grid(row=3, column=1, 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 _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: - m_type = ManeuverType(self.maneuver_type_var.get()) + 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): + 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_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) + 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.duration_s = self.ftp_duration_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() + 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 = self.ftp_vel_var.get() * KNOTS_TO_FPS - wp.target_heading_deg = self.ftp_hdg_var.get() - + 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 = self.ffd_duration_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() - + 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 = self.dm_duration_var.get() - wp.longitudinal_acceleration_g = self.dm_lon_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()) - + 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: