add NM
This commit is contained in:
parent
07bbfdddc0
commit
e5895f1163
@ -25,59 +25,156 @@ from .simulation_manager import SimulationManager
|
||||
from ..utils import logger
|
||||
|
||||
# --- Helper Dialog for Adding/Editing Targets ---
|
||||
# Sostituisci l'intera classe AddTargetDialog nel file gui/gui.py con questa
|
||||
|
||||
class AddTargetDialog(tk.Toplevel):
|
||||
"""Dialog window for adding or editing target parameters."""
|
||||
def __init__(self, parent, target_data=None):
|
||||
"""Dialog window for adding or editing target parameters with dual input modes."""
|
||||
def __init__(self, parent, target_data_si=None):
|
||||
super().__init__(parent)
|
||||
self.title("Add New Target" if target_data is None else "Edit Target")
|
||||
self.title("Add New Target" if target_data_si is None else "Edit Target")
|
||||
self.transient(parent)
|
||||
self.grab_set()
|
||||
self.result = None
|
||||
self.result_si = None
|
||||
|
||||
frame = ttk.Frame(self, padding="10")
|
||||
frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
main_frame = ttk.Frame(self, padding="10")
|
||||
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
self.vars = {
|
||||
"pos_x": tk.DoubleVar(value=target_data["pos_x"] if target_data else radar_math.meters_to_nm(5000.0)),
|
||||
"pos_y": tk.DoubleVar(value=target_data["pos_y"] if target_data else radar_math.meters_to_nm(0.0)),
|
||||
"pos_z": tk.DoubleVar(value=target_data["pos_z"] if target_data else radar_math.meters_to_nm(0.0)),
|
||||
"vel_x": tk.DoubleVar(value=target_data["vel_x"] if target_data else radar_math.mps_to_knots(-150.0)),
|
||||
"vel_y": tk.DoubleVar(value=target_data["vel_y"] if target_data else radar_math.mps_to_knots(0.0)),
|
||||
"vel_z": tk.DoubleVar(value=target_data["vel_z"] if target_data else radar_math.mps_to_knots(0.0)),
|
||||
"rcs": tk.DoubleVar(value=target_data["rcs"] if target_data else 1.0)
|
||||
}
|
||||
# --- Mode Selection ---
|
||||
self.input_mode = tk.StringVar(value="Cartesian")
|
||||
mode_frame = ttk.Frame(main_frame)
|
||||
mode_frame.grid(row=0, column=0, columnspan=2, pady=(0, 10), sticky=tk.W)
|
||||
ttk.Label(mode_frame, text="Position Input Mode:").pack(side=tk.LEFT, padx=(0, 10))
|
||||
ttk.Radiobutton(mode_frame, text="Cartesian (X, Y, Z)", variable=self.input_mode, value="Cartesian", command=self._update_input_mode).pack(side=tk.LEFT)
|
||||
ttk.Radiobutton(mode_frame, text="Spherical (R, Az, El)", variable=self.input_mode, value="Spherical", command=self._update_input_mode).pack(side=tk.LEFT)
|
||||
|
||||
labels = ["Initial Position X (NM):", "Initial Position Y (NM):", "Initial Position Z (NM):",
|
||||
"Velocity X (knots):", "Velocity Y (knots):", "Velocity Z (knots):", "RCS (m^2):"]
|
||||
keys = ["pos_x", "pos_y", "pos_z", "vel_x", "vel_y", "vel_z", "rcs"]
|
||||
# --- Data Initialization ---
|
||||
self._initialize_vars(target_data_si)
|
||||
|
||||
for i, (label_text, key) in enumerate(zip(labels, keys)):
|
||||
ttk.Label(frame, text=label_text).grid(row=i, column=0, sticky=tk.W, pady=2)
|
||||
spinbox = ttk.Spinbox(frame, from_=-1e6, to=1e6, textvariable=self.vars[key])
|
||||
spinbox.grid(row=i, column=1, sticky=(tk.W, tk.E), pady=2)
|
||||
if key == "rcs": spinbox.config(from_=0.01)
|
||||
# --- Input Frames ---
|
||||
self.cartesian_frame = ttk.Frame(main_frame)
|
||||
self.cartesian_frame.grid(row=1, column=0, columnspan=2, sticky='ew')
|
||||
self.spherical_frame = ttk.Frame(main_frame)
|
||||
self.spherical_frame.grid(row=1, column=0, columnspan=2, sticky='ew')
|
||||
|
||||
button_frame = ttk.Frame(frame)
|
||||
button_frame.grid(row=len(labels), column=0, columnspan=2, pady=10)
|
||||
self._populate_cartesian_frame()
|
||||
self._populate_spherical_frame()
|
||||
|
||||
# --- Common Velocity and RCS Frame ---
|
||||
common_frame = ttk.Frame(main_frame)
|
||||
common_frame.grid(row=2, column=0, columnspan=2, pady=(10, 0), sticky='ew')
|
||||
self._populate_common_frame(common_frame)
|
||||
|
||||
# --- Buttons ---
|
||||
button_frame = ttk.Frame(main_frame)
|
||||
button_frame.grid(row=3, column=0, columnspan=2, 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.destroy).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self._update_input_mode() # Set initial visibility
|
||||
|
||||
def _initialize_vars(self, target_data_si):
|
||||
"""Initialize all tk variables from SI data, calculating both coordinate systems."""
|
||||
# Default values in SI
|
||||
pos_x_m, pos_y_m, pos_z_m = 5000.0, 0.0, 0.0
|
||||
vel_x_mps, vel_y_mps, vel_z_mps = -150.0, 0.0, 0.0
|
||||
rcs = 1.0
|
||||
|
||||
if target_data_si:
|
||||
pos_x_m, pos_y_m, pos_z_m = target_data_si["pos_x"], target_data_si["pos_y"], target_data_si["pos_z"]
|
||||
vel_x_mps, vel_y_mps, vel_z_mps = target_data_si["vel_x"], target_data_si["vel_y"], target_data_si["vel_z"]
|
||||
rcs = target_data_si["rcs"]
|
||||
|
||||
# Calculate spherical coordinates from SI cartesian
|
||||
range_m, az_deg, el_deg = radar_math.cartesian_to_spherical(pos_x_m, pos_y_m, pos_z_m)
|
||||
|
||||
self.vars = {
|
||||
# Display units
|
||||
"pos_x_nm": tk.DoubleVar(value=radar_math.meters_to_nm(pos_x_m)),
|
||||
"pos_y_nm": tk.DoubleVar(value=radar_math.meters_to_nm(pos_y_m)),
|
||||
"pos_z_nm": tk.DoubleVar(value=radar_math.meters_to_nm(pos_z_m)),
|
||||
"range_nm": tk.DoubleVar(value=radar_math.meters_to_nm(range_m)),
|
||||
"azimuth_deg": tk.DoubleVar(value=az_deg),
|
||||
"elevation_deg": tk.DoubleVar(value=el_deg),
|
||||
"vel_x_knots": tk.DoubleVar(value=radar_math.mps_to_knots(vel_x_mps)),
|
||||
"vel_y_knots": tk.DoubleVar(value=radar_math.mps_to_knots(vel_y_mps)),
|
||||
"vel_z_knots": tk.DoubleVar(value=radar_math.mps_to_knots(vel_z_mps)),
|
||||
"rcs": tk.DoubleVar(value=rcs)
|
||||
}
|
||||
|
||||
def _create_spinbox(self, parent, label_text, var_key, from_=-1e6, to=1e6):
|
||||
ttk.Label(parent, text=label_text).pack(side=tk.LEFT)
|
||||
spinbox = ttk.Spinbox(parent, from_=from_, to=to, textvariable=self.vars[var_key], width=10)
|
||||
spinbox.pack(side=tk.RIGHT, fill=tk.X, expand=True)
|
||||
|
||||
def _populate_cartesian_frame(self):
|
||||
labels = ["Position X (NM):", "Position Y (NM):", "Position Z (NM):"]
|
||||
keys = ["pos_x_nm", "pos_y_nm", "pos_z_nm"]
|
||||
for label, key in zip(labels, keys):
|
||||
frame = ttk.Frame(self.cartesian_frame)
|
||||
frame.pack(fill=tk.X, pady=1)
|
||||
self._create_spinbox(frame, label, key)
|
||||
|
||||
def _populate_spherical_frame(self):
|
||||
frame_r = ttk.Frame(self.spherical_frame); frame_r.pack(fill=tk.X, pady=1)
|
||||
self._create_spinbox(frame_r, "Range (NM):", "range_nm", from_=0.0)
|
||||
|
||||
frame_az = ttk.Frame(self.spherical_frame); frame_az.pack(fill=tk.X, pady=1)
|
||||
self._create_spinbox(frame_az, "Azimuth (deg):", "azimuth_deg", from_=-180.0, to=180.0)
|
||||
|
||||
frame_el = ttk.Frame(self.spherical_frame); frame_el.pack(fill=tk.X, pady=1)
|
||||
self._create_spinbox(frame_el, "Elevation (deg):", "elevation_deg", from_=-90.0, to=90.0)
|
||||
|
||||
def _populate_common_frame(self, parent):
|
||||
ttk.Separator(parent).pack(fill=tk.X, pady=5)
|
||||
labels = ["Velocity X (knots):", "Velocity Y (knots):", "Velocity Z (knots):", "RCS (m^2):"]
|
||||
keys = ["vel_x_knots", "vel_y_knots", "vel_z_knots", "rcs"]
|
||||
for label, key in zip(labels, keys):
|
||||
frame = ttk.Frame(parent)
|
||||
frame.pack(fill=tk.X, pady=1)
|
||||
from_val = 0.01 if key == "rcs" else -1e6
|
||||
self._create_spinbox(frame, label, key, from_=from_val)
|
||||
|
||||
def _update_input_mode(self):
|
||||
"""Show the correct frame based on the selected radio button."""
|
||||
if self.input_mode.get() == "Cartesian":
|
||||
self.spherical_frame.grid_remove()
|
||||
self.cartesian_frame.grid()
|
||||
else: # Spherical
|
||||
self.cartesian_frame.grid_remove()
|
||||
self.spherical_frame.grid()
|
||||
|
||||
def on_ok(self):
|
||||
result_nm_knots = {key: var.get() for key, var in self.vars.items()}
|
||||
self.result = {
|
||||
"pos_x": radar_math.nm_to_meters(result_nm_knots["pos_x"]),
|
||||
"pos_y": radar_math.nm_to_meters(result_nm_knots["pos_y"]),
|
||||
"pos_z": radar_math.nm_to_meters(result_nm_knots["pos_z"]),
|
||||
"vel_x": radar_math.knots_to_mps(result_nm_knots["vel_x"]),
|
||||
"vel_y": radar_math.knots_to_mps(result_nm_knots["vel_y"]),
|
||||
"vel_z": radar_math.knots_to_mps(result_nm_knots["vel_z"]),
|
||||
"rcs": result_nm_knots["rcs"]
|
||||
"""Convert data from the active input mode back to SI and store it."""
|
||||
# Get common values
|
||||
vel_x_mps = radar_math.knots_to_mps(self.vars["vel_x_knots"].get())
|
||||
vel_y_mps = radar_math.knots_to_mps(self.vars["vel_y_knots"].get())
|
||||
vel_z_mps = radar_math.knots_to_mps(self.vars["vel_z_knots"].get())
|
||||
rcs = self.vars["rcs"].get()
|
||||
|
||||
# Get position based on the active mode
|
||||
if self.input_mode.get() == "Cartesian":
|
||||
pos_x_m = radar_math.nm_to_meters(self.vars["pos_x_nm"].get())
|
||||
pos_y_m = radar_math.nm_to_meters(self.vars["pos_y_nm"].get())
|
||||
pos_z_m = radar_math.nm_to_meters(self.vars["pos_z_nm"].get())
|
||||
else: # Spherical
|
||||
range_nm = self.vars["range_nm"].get()
|
||||
azimuth_deg = self.vars["azimuth_deg"].get()
|
||||
elevation_deg = self.vars["elevation_deg"].get()
|
||||
|
||||
range_m = radar_math.nm_to_meters(range_nm)
|
||||
pos_x_m, pos_y_m, pos_z_m = radar_math.spherical_to_cartesian(range_m, azimuth_deg, elevation_deg)
|
||||
|
||||
self.result_si = {
|
||||
"pos_x": pos_x_m, "pos_y": pos_y_m, "pos_z": pos_z_m,
|
||||
"vel_x": vel_x_mps, "vel_y": vel_y_mps, "vel_z": vel_z_mps,
|
||||
"rcs": rcs
|
||||
}
|
||||
self.destroy()
|
||||
|
||||
def show(self):
|
||||
"""Show the dialog and return the result in SI units."""
|
||||
self.wait_window()
|
||||
return self.result
|
||||
return self.result_si
|
||||
|
||||
# --- Main Application Class ---
|
||||
class App(tk.Tk):
|
||||
@ -219,6 +316,8 @@ class App(tk.Tk):
|
||||
}
|
||||
self.profiles = config_manager.load_profiles()
|
||||
self.selected_profile = tk.StringVar()
|
||||
self.scenarios = config_manager.load_scenarios()
|
||||
self.selected_scenario = tk.StringVar()
|
||||
for key in ["prf", "carrier_frequency", "duty_cycle", "beamwidth_az_deg", "scan_mode", "scan_speed_deg_s", "min_az_deg", "max_az_deg"]:
|
||||
self.vars[key].trace_add("write", self.update_derived_parameters)
|
||||
|
||||
@ -290,7 +389,28 @@ class App(tk.Tk):
|
||||
ttk.Label(derived_group, textvariable=self.vars[key]).pack(anchor=tk.W)
|
||||
|
||||
def _populate_target_tab(self, tab):
|
||||
target_group = ttk.LabelFrame(tab, text="Target Management", padding=10)
|
||||
"""Populates the Target tab with scenario management and the target table."""
|
||||
# --- NUOVO: Scenario Management Group ---
|
||||
scenario_group = ttk.LabelFrame(tab, text="Scenario Management", padding=10)
|
||||
scenario_group.pack(fill=tk.X, padx=5, pady=5)
|
||||
|
||||
scenario_frame = ttk.Frame(scenario_group)
|
||||
scenario_frame.pack(fill=tk.X, pady=2)
|
||||
|
||||
ttk.Label(scenario_frame, text="Scenario:").pack(side=tk.LEFT, padx=(0, 5))
|
||||
self.scenario_combobox = ttk.Combobox(scenario_frame, textvariable=self.selected_scenario, state='readonly')
|
||||
self.scenario_combobox.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||
self.scenario_combobox.bind('<<ComboboxSelected>>', self.on_scenario_select)
|
||||
|
||||
btn_frame_scenario = ttk.Frame(scenario_group)
|
||||
btn_frame_scenario.pack(fill=tk.X, pady=5)
|
||||
ttk.Button(btn_frame_scenario, text="Save Current...", command=self.save_scenario).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(btn_frame_scenario, text="Delete Selected", command=self.delete_scenario).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.refresh_scenario_list()
|
||||
|
||||
# --- Target Management Group (la tabella esistente) ---
|
||||
target_group = ttk.LabelFrame(tab, text="Target List", padding=10)
|
||||
target_group.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
self._create_target_table(target_group)
|
||||
|
||||
@ -307,14 +427,16 @@ class App(tk.Tk):
|
||||
self.analysis_text.config(yscrollcommand=analysis_text_scroll.set)
|
||||
|
||||
def _create_target_table(self, parent):
|
||||
"""Creates and configures the target Treeview table with display units."""
|
||||
frame = ttk.Frame(parent)
|
||||
frame.pack(fill=tk.BOTH, expand=True)
|
||||
cols = ("Pos X (m)", "Pos Y (m)", "Pos Z (m)", "Vel X (m/s)", "Vel Y (m/s)", "Vel Z (m/s)", "RCS (m^2)")
|
||||
# --- MODIFICA: Aggiornamento dei nomi delle colonne ---
|
||||
cols = ("Pos X (NM)", "Pos Y (NM)", "Pos Z (NM)", "Vel X (knots)", "Vel Y (knots)", "Vel Z (knots)", "RCS (m^2)")
|
||||
self.target_table = ttk.Treeview(frame, columns=cols, show="headings")
|
||||
self.target_table.column("#0", width=0, stretch=tk.NO)
|
||||
for col in cols:
|
||||
self.target_table.heading(col, text=col)
|
||||
self.target_table.column(col, width=60, anchor=tk.CENTER)
|
||||
self.target_table.column(col, width=80, anchor=tk.CENTER) # Increased width
|
||||
self.target_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
scrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.target_table.yview)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
@ -414,9 +536,23 @@ class App(tk.Tk):
|
||||
if self.plot_manager:
|
||||
self.plot_manager.update_ppi_range()
|
||||
|
||||
def add_target_to_table(self, data):
|
||||
# data is in meters and m/s from the dialog
|
||||
self.target_table.insert("", tk.END, values=[f"{v:.2f}" for v in data.values()])
|
||||
def add_target_to_table(self, data_si):
|
||||
"""
|
||||
Adds a target to the table, converting SI data to display units.
|
||||
Args:
|
||||
data_si (dict): Target data in SI units (meters, m/s).
|
||||
"""
|
||||
# --- MODIFICA: Conversione da SI a unità di visualizzazione ---
|
||||
values_to_display = [
|
||||
f"{radar_math.meters_to_nm(data_si['pos_x']):.2f}",
|
||||
f"{radar_math.meters_to_nm(data_si['pos_y']):.2f}",
|
||||
f"{radar_math.meters_to_nm(data_si['pos_z']):.2f}",
|
||||
f"{radar_math.mps_to_knots(data_si['vel_x']):.2f}",
|
||||
f"{radar_math.mps_to_knots(data_si['vel_y']):.2f}",
|
||||
f"{radar_math.mps_to_knots(data_si['vel_z']):.2f}",
|
||||
f"{data_si['rcs']:.2f}"
|
||||
]
|
||||
self.target_table.insert("", tk.END, values=values_to_display)
|
||||
self.check_target_warnings()
|
||||
if self.plot_manager:
|
||||
self.plot_manager.redraw_ppi_targets(self.get_targets_from_gui())
|
||||
@ -435,18 +571,30 @@ class App(tk.Tk):
|
||||
return RadarConfig(carrier_frequency=self.vars["carrier_frequency"].get(), prf=self.vars["prf"].get(), duty_cycle=self.vars["duty_cycle"].get(), sample_rate=self.vars["sample_rate"].get(), antenna_config=antenna_cfg, scan_config=scan_cfg)
|
||||
|
||||
def get_targets_from_gui(self) -> list[Target]:
|
||||
"""
|
||||
Extracts target data from the Treeview, converts it from display units back to SI,
|
||||
and returns a list of Target objects.
|
||||
"""
|
||||
targets = []
|
||||
for item in self.target_table.get_children():
|
||||
values = self.target_table.item(item)['values']
|
||||
values_display_units = self.target_table.item(item)['values']
|
||||
try:
|
||||
float_values = [float(v) for v in values]
|
||||
pos = np.array(float_values[0:3])
|
||||
vel = np.array(float_values[3:6])
|
||||
rcs = float_values[6]
|
||||
# --- MODIFICA: Conversione da unità di visualizzazione a SI ---
|
||||
pos = np.array([
|
||||
radar_math.nm_to_meters(float(values_display_units[0])),
|
||||
radar_math.nm_to_meters(float(values_display_units[1])),
|
||||
radar_math.nm_to_meters(float(values_display_units[2]))
|
||||
])
|
||||
vel = np.array([
|
||||
radar_math.knots_to_mps(float(values_display_units[3])),
|
||||
radar_math.knots_to_mps(float(values_display_units[4])),
|
||||
radar_math.knots_to_mps(float(values_display_units[5]))
|
||||
])
|
||||
rcs = float(values_display_units[6])
|
||||
targets.append(Target(initial_position=pos, velocity=vel, rcs=rcs))
|
||||
except (ValueError, IndexError) as e:
|
||||
self.log.error(f"Skipping invalid target data: {values}. Error: {e}")
|
||||
messagebox.showwarning("Invalid Data", f"Skipping invalid target data: {values}. Error: {e}", parent=self)
|
||||
self.log.error(f"Skipping invalid target data from table: {values_display_units}. Error: {e}")
|
||||
messagebox.showwarning("Invalid Data", f"Skipping invalid target data: {values_display_units}. Error: {e}", parent=self)
|
||||
return targets
|
||||
|
||||
def _update_analysis_text(self, radar_cfg, targets, current_az_deg, iq_data_cpi):
|
||||
@ -570,23 +718,41 @@ class App(tk.Tk):
|
||||
if result: self.add_target_to_table(result)
|
||||
|
||||
def on_target_double_click(self, event):
|
||||
"""Handles double-click on target table to edit a target."""
|
||||
selected_item = self.target_table.selection()
|
||||
if not selected_item: return
|
||||
item_data = self.target_table.item(selected_item[0])['values']
|
||||
# Data in table is in m and m/s, convert to NM and knots for dialog
|
||||
target_data = {
|
||||
"pos_x": radar_math.meters_to_nm(float(item_data[0])),
|
||||
"pos_y": radar_math.meters_to_nm(float(item_data[1])),
|
||||
"pos_z": radar_math.meters_to_nm(float(item_data[2])),
|
||||
"vel_x": radar_math.mps_to_knots(float(item_data[3])),
|
||||
"vel_y": radar_math.mps_to_knots(float(item_data[4])),
|
||||
"vel_z": radar_math.mps_to_knots(float(item_data[5])),
|
||||
"rcs": float(item_data[6])
|
||||
item_data_display_units = self.target_table.item(selected_item[0])['values']
|
||||
|
||||
# --- MODIFICA: Conversione da unità di visualizzazione a SI per il dialogo ---
|
||||
try:
|
||||
target_data_si = {
|
||||
"pos_x": radar_math.nm_to_meters(float(item_data_display_units[0])),
|
||||
"pos_y": radar_math.nm_to_meters(float(item_data_display_units[1])),
|
||||
"pos_z": radar_math.nm_to_meters(float(item_data_display_units[2])),
|
||||
"vel_x": radar_math.knots_to_mps(float(item_data_display_units[3])),
|
||||
"vel_y": radar_math.knots_to_mps(float(item_data_display_units[4])),
|
||||
"vel_z": radar_math.knots_to_mps(float(item_data_display_units[5])),
|
||||
"rcs": float(item_data_display_units[6])
|
||||
}
|
||||
dialog = AddTargetDialog(self, target_data=target_data)
|
||||
result = dialog.show()
|
||||
if result:
|
||||
self.target_table.item(selected_item[0], values=[f"{v:.2f}" for v in result.values()])
|
||||
except (ValueError, IndexError):
|
||||
self.log.error("Could not parse target data from table for editing.")
|
||||
return
|
||||
|
||||
dialog = AddTargetDialog(self, target_data_si=target_data_si)
|
||||
result_si = dialog.show()
|
||||
|
||||
if result_si:
|
||||
# Result is already in SI. Convert to display units to update the table row.
|
||||
values_to_display = [
|
||||
f"{radar_math.meters_to_nm(result_si['pos_x']):.2f}",
|
||||
f"{radar_math.meters_to_nm(result_si['pos_y']):.2f}",
|
||||
f"{radar_math.meters_to_nm(result_si['pos_z']):.2f}",
|
||||
f"{radar_math.mps_to_knots(result_si['vel_x']):.2f}",
|
||||
f"{radar_math.mps_to_knots(result_si['vel_y']):.2f}",
|
||||
f"{radar_math.mps_to_knots(result_si['vel_z']):.2f}",
|
||||
f"{result_si['rcs']:.2f}"
|
||||
]
|
||||
self.target_table.item(selected_item[0], values=values_to_display)
|
||||
self.check_target_warnings()
|
||||
if self.plot_manager: self.plot_manager.redraw_ppi_targets(self.get_targets_from_gui())
|
||||
|
||||
@ -643,6 +809,95 @@ class App(tk.Tk):
|
||||
if hasattr(self, 'scan_speed_spinbox'): self.scan_speed_spinbox.config(state=state)
|
||||
self.update_derived_parameters()
|
||||
|
||||
def on_scenario_select(self, event=None):
|
||||
"""Loads the selected target scenario into the target table, converting for display."""
|
||||
scenario_name = self.selected_scenario.get()
|
||||
if not scenario_name or scenario_name not in self.scenarios: return
|
||||
|
||||
self.log.info(f"Loading scenario: '{scenario_name}'")
|
||||
for item in self.target_table.get_children(): self.target_table.delete(item)
|
||||
|
||||
target_list_si = self.scenarios[scenario_name]
|
||||
for target_data_si in target_list_si:
|
||||
# --- MODIFICA: Conversione da SI a unità di visualizzazione ---
|
||||
values_to_display = [
|
||||
f"{radar_math.meters_to_nm(target_data_si['initial_position'][0]):.2f}",
|
||||
f"{radar_math.meters_to_nm(target_data_si['initial_position'][1]):.2f}",
|
||||
f"{radar_math.meters_to_nm(target_data_si['initial_position'][2]):.2f}",
|
||||
f"{radar_math.mps_to_knots(target_data_si['velocity'][0]):.2f}",
|
||||
f"{radar_math.mps_to_knots(target_data_si['velocity'][1]):.2f}",
|
||||
f"{radar_math.mps_to_knots(target_data_si['velocity'][2]):.2f}",
|
||||
f"{target_data_si['rcs']:.2f}"
|
||||
]
|
||||
self.target_table.insert("", tk.END, values=values_to_display)
|
||||
|
||||
self.check_target_warnings()
|
||||
if self.plot_manager: self.plot_manager.redraw_ppi_targets(self.get_targets_from_gui())
|
||||
messagebox.showinfo("Scenario Loaded", f"Scenario '{scenario_name}' has been loaded.", parent=self)
|
||||
|
||||
def save_scenario(self):
|
||||
"""Saves the current list of targets as a new scenario."""
|
||||
scenario_name = simpledialog.askstring("Save Scenario", "Enter a name for this scenario:", parent=self)
|
||||
if not scenario_name or not scenario_name.strip():
|
||||
return
|
||||
|
||||
scenario_name = scenario_name.strip()
|
||||
if scenario_name in self.scenarios:
|
||||
if not messagebox.askyesno("Overwrite Scenario", f"Scenario '{scenario_name}' already exists. Overwrite it?", parent=self):
|
||||
return
|
||||
|
||||
current_targets_data = []
|
||||
for item in self.target_table.get_children():
|
||||
values = self.target_table.item(item)['values']
|
||||
try:
|
||||
float_values = [float(v) for v in values]
|
||||
target_dict = {
|
||||
"initial_position": float_values[0:3],
|
||||
"velocity": float_values[3:6],
|
||||
"rcs": float_values[6]
|
||||
}
|
||||
current_targets_data.append(target_dict)
|
||||
except (ValueError, IndexError):
|
||||
self.log.error(f"Could not parse target data for saving: {values}")
|
||||
continue # Skip invalid rows
|
||||
|
||||
if not current_targets_data:
|
||||
messagebox.showwarning("No Targets", "Cannot save an empty scenario.", parent=self)
|
||||
return
|
||||
|
||||
self.scenarios[scenario_name] = current_targets_data
|
||||
if config_manager.save_scenarios(self.scenarios):
|
||||
self.refresh_scenario_list()
|
||||
self.selected_scenario.set(scenario_name)
|
||||
self.log.info(f"Scenario '{scenario_name}' saved successfully.")
|
||||
messagebox.showinfo("Scenario Saved", f"Scenario '{scenario_name}' saved successfully.", parent=self)
|
||||
else:
|
||||
self.log.error("Failed to save scenarios to file.")
|
||||
messagebox.showerror("Error", "Could not save scenarios to file.", parent=self)
|
||||
|
||||
def delete_scenario(self):
|
||||
"""Deletes the selected scenario."""
|
||||
scenario_name = self.selected_scenario.get()
|
||||
if not scenario_name:
|
||||
messagebox.showwarning("No Scenario Selected", "Please select a scenario to delete.", parent=self)
|
||||
return
|
||||
|
||||
if messagebox.askyesno("Delete Scenario", f"Are you sure you want to delete the scenario '{scenario_name}'?", parent=self):
|
||||
if scenario_name in self.scenarios:
|
||||
del self.scenarios[scenario_name]
|
||||
if config_manager.save_scenarios(self.scenarios):
|
||||
self.refresh_scenario_list()
|
||||
self.log.info(f"Scenario '{scenario_name}' has been deleted.")
|
||||
messagebox.showinfo("Scenario Deleted", f"Scenario '{scenario_name}' has been deleted.", parent=self)
|
||||
else:
|
||||
self.log.error("Failed to save scenarios to file after deletion.")
|
||||
messagebox.showerror("Error", "Could not save scenarios to file.", parent=self)
|
||||
|
||||
def refresh_scenario_list(self):
|
||||
"""Updates the scenario combobox with current saved scenarios."""
|
||||
self.scenario_combobox['values'] = sorted(list(self.scenarios.keys()))
|
||||
self.selected_scenario.set('') # Clear selection
|
||||
|
||||
def start_gui():
|
||||
"""Entry point to launch the Tkinter GUI application."""
|
||||
app = App()
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
"""
|
||||
Handles loading and saving of radar configuration profiles from a JSON file.
|
||||
Handles loading and saving of radar configuration profiles and target scenarios
|
||||
from JSON files.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import numpy as np
|
||||
|
||||
# Use a file in the user's home directory to persist profiles
|
||||
# Use files in the user's home directory to persist data
|
||||
# across different project locations or versions.
|
||||
PROFILES_FILE = os.path.join(os.path.expanduser("~"), ".radar_simulator_profiles.json")
|
||||
SCENARIOS_FILE = os.path.join(os.path.expanduser("~"), ".radar_simulator_scenarios.json")
|
||||
|
||||
# --- Profile Management (Radar Configuration) ---
|
||||
|
||||
def load_profiles():
|
||||
"""
|
||||
@ -18,7 +23,6 @@ def load_profiles():
|
||||
try:
|
||||
with open(PROFILES_FILE, 'r') as f:
|
||||
profiles = json.load(f)
|
||||
# Basic validation
|
||||
if isinstance(profiles, dict):
|
||||
return profiles
|
||||
return {}
|
||||
@ -36,3 +40,43 @@ def save_profiles(profiles):
|
||||
return True
|
||||
except IOError:
|
||||
return False
|
||||
|
||||
# --- Scenario Management (Target Lists) - NUOVE FUNZIONI ---
|
||||
|
||||
def load_scenarios():
|
||||
"""
|
||||
Loads target scenarios (lists of targets) from the JSON file.
|
||||
"""
|
||||
if not os.path.exists(SCENARIOS_FILE):
|
||||
return {}
|
||||
try:
|
||||
with open(SCENARIOS_FILE, 'r') as f:
|
||||
scenarios = json.load(f)
|
||||
# Basic validation to ensure it's a dictionary
|
||||
if isinstance(scenarios, dict):
|
||||
return scenarios
|
||||
return {}
|
||||
except (IOError, json.JSONDecodeError):
|
||||
return {}
|
||||
|
||||
def save_scenarios(scenarios):
|
||||
"""
|
||||
Saves the given scenarios dictionary to the JSON file.
|
||||
|
||||
The data structure is:
|
||||
{
|
||||
"ScenarioName1": [
|
||||
{"initial_position": [x,y,z], "velocity": [vx,vy,vz], "rcs": rcs_val},
|
||||
...
|
||||
],
|
||||
...
|
||||
}
|
||||
"""
|
||||
try:
|
||||
with open(SCENARIOS_FILE, 'w') as f:
|
||||
# np.ndarray is not JSON serializable, so convert to lists if needed
|
||||
# This is handled in the GUI layer before calling this function
|
||||
json.dump(scenarios, f, indent=4)
|
||||
return True
|
||||
except (IOError, TypeError):
|
||||
return False
|
||||
@ -4,11 +4,9 @@ Utility functions for radar-related mathematical calculations.
|
||||
import numpy as np
|
||||
from scipy.constants import c
|
||||
|
||||
# Conversion Constants
|
||||
# --- Conversion Constants ---
|
||||
METERS_PER_NAUTICAL_MILE = 1852.0
|
||||
METERS_PER_SECOND_TO_KNOTS = 1.94384
|
||||
DEGREES_TO_RADIANS = np.pi / 180.0
|
||||
RADIANS_TO_DEGREES = 180.0 / np.pi
|
||||
|
||||
def calculate_max_unambiguous_range(prf: float) -> float:
|
||||
"""
|
||||
@ -85,9 +83,10 @@ def calculate_gaussian_gain(angle_off_boresight_deg: float, beamwidth_deg: float
|
||||
# The standard deviation (sigma) of the Gaussian beam is related to the 3dB beamwidth
|
||||
sigma = beamwidth_deg / (2 * np.sqrt(2 * np.log(2)))
|
||||
|
||||
# We use angle^2 / (2 * sigma^2) for the one-way gain pattern.
|
||||
# This provides a more realistic falloff than a squared (two-way) model.
|
||||
gain = np.exp(-(angle_off_boresight_deg**2) / (2 * sigma**2))
|
||||
# We use angle^2 / (2 * sigma^2) for the one-way power gain pattern.
|
||||
# The two-way voltage gain is the square of the one-way voltage gain,
|
||||
# which is equivalent to the one-way power gain.
|
||||
gain = np.exp(-(angle_off_boresight_deg**2) / (sigma**2))
|
||||
return gain
|
||||
|
||||
# --- Unit Conversion Functions ---
|
||||
@ -108,14 +107,42 @@ def knots_to_mps(knots: float) -> float:
|
||||
"""Converts knots to meters per second."""
|
||||
return knots / METERS_PER_SECOND_TO_KNOTS
|
||||
|
||||
def mps_to_kmh(mps: float) -> float:
|
||||
"""Converts meters per second to kilometers per hour."""
|
||||
return mps * 3.6
|
||||
def cartesian_to_spherical(x: float, y: float, z: float) -> tuple[float, float, float]:
|
||||
"""
|
||||
Converts Cartesian coordinates (x, y, z) to Spherical coordinates.
|
||||
|
||||
def deg_to_rad(deg: float) -> float:
|
||||
"""Converts degrees to radians."""
|
||||
return deg * DEGREES_TO_RADIANS
|
||||
Args:
|
||||
x, y, z: Coordinates in meters.
|
||||
|
||||
def rad_to_deg(rad: float) -> float:
|
||||
"""Converts radians to degrees."""
|
||||
return rad * RADIANS_TO_DEGREES
|
||||
Returns:
|
||||
A tuple containing (range_m, azimuth_deg, elevation_deg).
|
||||
"""
|
||||
range_m = np.sqrt(x**2 + y**2 + z**2)
|
||||
if range_m == 0:
|
||||
return 0.0, 0.0, 0.0
|
||||
|
||||
azimuth_deg = np.rad2deg(np.arctan2(y, x))
|
||||
elevation_deg = np.rad2deg(np.arcsin(z / range_m))
|
||||
|
||||
return range_m, azimuth_deg, elevation_deg
|
||||
|
||||
def spherical_to_cartesian(range_m: float, azimuth_deg: float, elevation_deg: float) -> list[float]:
|
||||
"""
|
||||
Converts Spherical coordinates to Cartesian coordinates (x, y, z).
|
||||
|
||||
Args:
|
||||
range_m: Range in meters.
|
||||
azimuth_deg: Azimuth in degrees.
|
||||
elevation_deg: Elevation in degrees.
|
||||
|
||||
Returns:
|
||||
A list containing [x, y, z] coordinates in meters.
|
||||
"""
|
||||
az_rad = np.deg2rad(azimuth_deg)
|
||||
el_rad = np.deg2rad(elevation_deg)
|
||||
|
||||
x = range_m * np.cos(el_rad) * np.cos(az_rad)
|
||||
y = range_m * np.cos(el_rad) * np.sin(az_rad)
|
||||
z = range_m * np.sin(el_rad)
|
||||
|
||||
return [x, y, z]
|
||||
Loading…
Reference in New Issue
Block a user