""" Main GUI module for the Radar Scenario Simulator (Tkinter version). This module is responsible for building the main application window, laying out widgets, and coordinating the simulation and plotting managers. """ import tkinter as tk from tkinter import ttk, messagebox, simpledialog import queue import numpy as np from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure # Import core simulation engine and utility functions from ..core.simulation_engine import RadarConfig, AntennaConfig, ScanConfig, Target from ..utils import radar_math, config_manager # Import new manager classes from .plot_manager import PlotManager from .simulation_manager import SimulationManager # --- Helper Dialog for Adding/Editing Targets --- class AddTargetDialog(tk.Toplevel): """Dialog window for adding or editing target parameters.""" def __init__(self, parent, target_data=None): super().__init__(parent) self.title("Add New Target" if target_data is None else "Edit Target") self.transient(parent) self.grab_set() self.result = None frame = ttk.Frame(self, padding="10") 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 5000.0), "pos_y": tk.DoubleVar(value=target_data["pos_y"] if target_data else 0.0), "pos_z": tk.DoubleVar(value=target_data["pos_z"] if target_data else 0.0), "vel_x": tk.DoubleVar(value=target_data["vel_x"] if target_data else -150.0), "vel_y": tk.DoubleVar(value=target_data["vel_y"] if target_data else 0.0), "vel_z": tk.DoubleVar(value=target_data["vel_z"] if target_data else 0.0), "rcs": tk.DoubleVar(value=target_data["rcs"] if target_data else 1.0) } labels = ["Initial Position X (m):", "Initial Position Y (m):", "Initial Position Z (m):", "Velocity X (m/s):", "Velocity Y (m/s):", "Velocity Z (m/s):", "RCS (m^2):"] keys = ["pos_x", "pos_y", "pos_z", "vel_x", "vel_y", "vel_z", "rcs"] 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) button_frame = ttk.Frame(frame) button_frame.grid(row=len(labels), 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) def on_ok(self): self.result = {key: var.get() for key, var in self.vars.items()} self.destroy() def show(self): self.wait_window() return self.result # --- Main Application Class --- class App(tk.Tk): """Main application window for the Radar Scenario Simulator.""" def __init__(self): super().__init__() self.title("Radar Scenario Simulator") self.attributes('-fullscreen', True) self.bind("", lambda event: self.attributes('-fullscreen', False)) # --- Main Layout --- main_paned_window = ttk.PanedWindow(self, orient=tk.HORIZONTAL) main_paned_window.pack(fill=tk.BOTH, expand=True) left_column_frame = ttk.Frame(main_paned_window, width=600) main_paned_window.add(left_column_frame, weight=1) right_frame = ttk.Frame(main_paned_window) main_paned_window.add(right_frame, weight=2) left_paned_window = ttk.PanedWindow(left_column_frame, orient=tk.VERTICAL) left_paned_window.pack(fill=tk.BOTH, expand=True) notebook_frame = ttk.Frame(left_paned_window, height=450) left_paned_window.add(notebook_frame, weight=1) bottom_left_frame = ttk.Frame(left_paned_window) left_paned_window.add(bottom_left_frame, weight=1) # --- Initialize UI Variables --- self._init_vars() # --- Create UI Components --- notebook = ttk.Notebook(notebook_frame) notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.config_tab = ttk.Frame(notebook) self.target_tab = ttk.Frame(notebook) self.scenario_tab = ttk.Frame(notebook) notebook.add(self.config_tab, text="Configuration") notebook.add(self.target_tab, text="Target") notebook.add(self.scenario_tab, text="Scenario") self._populate_config_tab(self.config_tab) self._populate_target_tab(self.target_tab) self._populate_scenario_tab(self.scenario_tab) # --- Setup Plotting Area --- ppi_plot_frame = ttk.LabelFrame(bottom_left_frame, text="PPI Display") ppi_plot_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(5, 0), pady=5) scan_controls_frame = ttk.LabelFrame(bottom_left_frame, text="Scan & PPI Control") scan_controls_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 5), pady=5) self._populate_scan_ppi_controls(scan_controls_frame) self.rd_figure = Figure(figsize=(8, 6), dpi=100, facecolor='#3a3a3a') self.rd_canvas = FigureCanvasTkAgg(self.rd_figure, master=right_frame) self.rd_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) self.ppi_figure = Figure(figsize=(4, 4), dpi=100, facecolor='#3a3a3a') self.ppi_canvas = FigureCanvasTkAgg(self.ppi_figure, master=ppi_plot_frame) self.ppi_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) # --- Initialize Managers --- self.plot_manager = PlotManager(self.rd_figure, self.rd_canvas, self.ppi_figure, self.ppi_canvas, self.vars['ppi_range_m']) self.simulation_manager = SimulationManager() # --- Finalize Init --- self.toggle_amplitude_controls() self.update_derived_parameters() self.protocol("WM_DELETE_WINDOW", self.on_closing) def on_closing(self): if self.simulation_manager.is_running(): self.stop_simulation() self.destroy() def _init_vars(self): self.vars = { "carrier_frequency": tk.DoubleVar(value=9.5e9), "prf": tk.DoubleVar(value=2000.0), "duty_cycle": tk.DoubleVar(value=10.0), "sample_rate": tk.DoubleVar(value=5e6), "beamwidth_az_deg": tk.DoubleVar(value=3.0), "beamwidth_el_deg": tk.DoubleVar(value=3.0), "scan_mode": tk.StringVar(value='staring'), "min_az_deg": tk.DoubleVar(value=-30.0), "max_az_deg": tk.DoubleVar(value=30.0), "scan_speed_deg_s": tk.DoubleVar(value=20.0), "num_pulses_cpi": tk.IntVar(value=256), "simulation_duration_s": tk.DoubleVar(value=10.0), "min_db": tk.DoubleVar(value=-60.0), "max_db": tk.DoubleVar(value=0.0), "auto_scale": tk.BooleanVar(value=True), "pulse_width_text": tk.StringVar(), "listening_time_text": tk.StringVar(), "max_range_text": tk.StringVar(), "max_velocity_text": tk.StringVar(), "dwell_time_text": tk.StringVar(), "pulses_on_target_text": tk.StringVar(), "ppi_range_m": tk.DoubleVar(value=15000.0), "scan_info_text": tk.StringVar(value="Mode: Staring") } self.profiles = config_manager.load_profiles() self.selected_profile = tk.StringVar() for key in ["prf", "carrier_frequency", "duty_cycle", "beamwidth_az_deg", "scan_mode", "scan_speed_deg_s"]: self.vars[key].trace_add("write", self.update_derived_parameters) def _create_labeled_spinbox(self, parent, text, var, from_, to, increment=1.0, is_db=False, scientific=False, command=None): frame = ttk.Frame(parent) frame.pack(fill=tk.X, pady=2) ttk.Label(frame, text=text, width=25).pack(side=tk.LEFT) if scientific: spinbox = ttk.Spinbox(frame, from_=from_, to=to, increment=increment, textvariable=var, format="%.2e", command=command) else: spinbox = ttk.Spinbox(frame, from_=from_, to=to, increment=increment, textvariable=var, command=command) spinbox.pack(side=tk.LEFT, fill=tk.X, expand=True) if is_db: if "min" in text.lower(): self.min_db_spinbox = spinbox else: self.max_db_spinbox = spinbox return spinbox def _populate_config_tab(self, tab): profile_group = ttk.LabelFrame(tab, text="Radar Profiles", padding=10) profile_group.pack(fill=tk.X, padx=5, pady=5) profile_frame = ttk.Frame(profile_group) profile_frame.pack(fill=tk.X, pady=2) ttk.Label(profile_frame, text="Profile:").pack(side=tk.LEFT, padx=(0, 5)) self.profile_combobox = ttk.Combobox(profile_frame, textvariable=self.selected_profile, state='readonly') self.profile_combobox.pack(side=tk.LEFT, fill=tk.X, expand=True) self.profile_combobox.bind('<>', self.on_profile_select) btn_frame = ttk.Frame(profile_group) btn_frame.pack(fill=tk.X, pady=5) ttk.Button(btn_frame, text="Save Current...", command=self.save_profile).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="Delete Selected", command=self.delete_profile).pack(side=tk.LEFT, padx=5) self.refresh_profile_list() radar_group = ttk.LabelFrame(tab, text="Radar Configuration", padding=10) radar_group.pack(fill=tk.X, padx=5, pady=5) self._create_labeled_spinbox(radar_group, "Carrier Frequency (Hz):", self.vars["carrier_frequency"], 1e6, 100e9, scientific=True) self._create_labeled_spinbox(radar_group, "PRF (Hz):", self.vars["prf"], 1, 100000) self._create_labeled_spinbox(radar_group, "Duty Cycle (%):", self.vars["duty_cycle"], 0.01, 99.99, increment=0.1) self._create_labeled_spinbox(radar_group, "Sample Rate (Hz):", self.vars["sample_rate"], 1e3, 100e6, scientific=True) antenna_group = ttk.LabelFrame(tab, text="Antenna Configuration", padding=10) antenna_group.pack(fill=tk.X, padx=5, pady=5) self._create_labeled_spinbox(antenna_group, "Azimuth Beamwidth (deg):", self.vars["beamwidth_az_deg"], 0.1, 90.0, increment=0.1) scan_group = ttk.LabelFrame(tab, text="Scan Strategy", padding=10) scan_group.pack(fill=tk.X, padx=5, pady=5) mode_frame = ttk.Frame(scan_group) mode_frame.pack(fill=tk.X, pady=2) ttk.Label(mode_frame, text="Scan Mode:", width=25).pack(side=tk.LEFT) ttk.Radiobutton(mode_frame, text="Staring", variable=self.vars["scan_mode"], value="staring", command=self.update_scan_mode_controls).pack(side=tk.LEFT) ttk.Radiobutton(mode_frame, text="Sector", variable=self.vars["scan_mode"], value="sector", command=self.update_scan_mode_controls).pack(side=tk.LEFT) self.min_az_spinbox = self._create_labeled_spinbox(scan_group, "Min Azimuth (deg):", self.vars["min_az_deg"], -180, 180, increment=1.0) self.max_az_spinbox = self._create_labeled_spinbox(scan_group, "Max Azimuth (deg):", self.vars["max_az_deg"], -180, 180, increment=1.0) self.scan_speed_spinbox = self._create_labeled_spinbox(scan_group, "Scan Speed (deg/s):", self.vars["scan_speed_deg_s"], 0.1, 1000.0, increment=1.0) self.update_scan_mode_controls() derived_group = ttk.LabelFrame(tab, text="Derived & Calculated Values", padding=10) derived_group.pack(fill=tk.X, padx=5, pady=5) for key in ["pulse_width_text", "listening_time_text"]: ttk.Label(derived_group, textvariable=self.vars[key]).pack(anchor=tk.W) ttk.Separator(derived_group, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=5) self.max_range_label = ttk.Label(derived_group, textvariable=self.vars["max_range_text"]) self.max_range_label.pack(anchor=tk.W) self.max_velocity_label = ttk.Label(derived_group, textvariable=self.vars["max_velocity_text"]) self.max_velocity_label.pack(anchor=tk.W) ttk.Separator(derived_group, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=5) for key in ["dwell_time_text", "pulses_on_target_text"]: 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) target_group.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self._create_target_table(target_group) def _populate_scenario_tab(self, tab): sim_group = ttk.LabelFrame(tab, text="Simulation & Plotting Control", padding=10) sim_group.pack(fill=tk.X, padx=5, pady=5) self._create_sim_controls(sim_group) self.analysis_frame = ttk.LabelFrame(tab, text="Simulation Analysis & Warnings", padding=10) self.analysis_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.analysis_text = tk.Text(self.analysis_frame, wrap=tk.WORD, height=10, state=tk.DISABLED, font=('TkDefaultFont', 10)) self.analysis_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) analysis_text_scroll = ttk.Scrollbar(self.analysis_frame, command=self.analysis_text.yview) analysis_text_scroll.pack(side=tk.RIGHT, fill=tk.Y) self.analysis_text.config(yscrollcommand=analysis_text_scroll.set) def _create_target_table(self, parent): frame = ttk.Frame(parent) frame.pack(fill=tk.BOTH, expand=True) cols = ("Pos X", "Pos Y", "Pos Z", "Vel X", "Vel Y", "Vel Z", "RCS") 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.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) self.target_table.configure(yscrollcommand=scrollbar.set) self.target_table.bind("", self.on_target_double_click) btn_frame = ttk.Frame(parent) btn_frame.pack(fill=tk.X, pady=5) ttk.Button(btn_frame, text="Add Target", command=self.open_add_target_dialog).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="Remove Selected", command=self.remove_selected_target).pack(side=tk.LEFT, padx=5) def _create_sim_controls(self, parent): self._create_labeled_spinbox(parent, "Pulses per CPI:", self.vars["num_pulses_cpi"], 1, 8192) self._create_labeled_spinbox(parent, "Simulation Duration (s):", self.vars["simulation_duration_s"], 0.1, 300.0, increment=0.1) auto_scale_check = ttk.Checkbutton(parent, text="Auto-Scale Amplitude", variable=self.vars["auto_scale"], command=self.toggle_amplitude_controls) auto_scale_check.pack(fill=tk.X, pady=5) self._create_labeled_spinbox(parent, "Min Display Amplitude (dB):", self.vars["min_db"], -200, 100, is_db=True) self._create_labeled_spinbox(parent, "Max Display Amplitude (dB):", self.vars["max_db"], -200, 100, is_db=True) self.generate_button = ttk.Button(parent, text="Start Simulation", command=self.start_simulation) self.generate_button.pack(pady=5) self.stop_button = ttk.Button(parent, text="Stop Simulation", command=self.stop_simulation, state=tk.DISABLED) self.stop_button.pack(pady=5) def _populate_scan_ppi_controls(self, parent): self._create_labeled_spinbox(parent, "PPI Range (m):", self.vars['ppi_range_m'], 100, 150000, command=self.on_ppi_range_change) ttk.Label(parent, textvariable=self.vars['scan_info_text'], wraplength=180, justify=tk.LEFT).pack(anchor=tk.W, padx=5, pady=5) def start_simulation(self): if self.simulation_manager.is_running(): return self.generate_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) radar_cfg = self.get_radar_config_from_gui() targets = self.get_targets_from_gui() if not targets: messagebox.showwarning("No Targets", "Please add at least one target to simulate.", parent=self) self.stop_simulation() return self.plot_manager.setup_plots_for_simulation( self.vars["num_pulses_cpi"].get(), self.vars["min_db"].get(), self.vars["max_db"].get(), targets, radar_cfg ) self.simulation_manager.start( radar_cfg, targets, self.vars["simulation_duration_s"].get(), self.vars["num_pulses_cpi"].get() ) self.after(100, self._check_simulation_queue) def stop_simulation(self): self.simulation_manager.stop() self.generate_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) self.plot_manager.clear_rd_plot() self.analysis_text.config(state=tk.NORMAL) self.analysis_text.delete(1.0, tk.END) self.analysis_text.insert(tk.END, "Simulation stopped or not started.") self.analysis_text.config(state=tk.DISABLED) def _check_simulation_queue(self): try: frame_data = self.simulation_manager.data_queue.get_nowait() if frame_data is None: self.stop_simulation() return radar_cfg = self.get_radar_config_from_gui() self.plot_manager.update_plots( frame_data, radar_cfg, self.vars["auto_scale"].get(), self.vars["min_db"].get(), self.vars["max_db"].get() ) self._update_analysis_text(radar_cfg, self.get_targets_from_gui(), frame_data[0], frame_data[1]) except queue.Empty: pass finally: if self.simulation_manager.is_running(): self.after(50, self._check_simulation_queue) def on_ppi_range_change(self, *args): if self.plot_manager: self.plot_manager.update_ppi_range() def add_target_to_table(self, data): self.target_table.insert("", tk.END, values=[f"{v:.2f}" for v in data.values()]) self.check_target_warnings() if self.plot_manager: self.plot_manager.redraw_ppi_targets(self.get_targets_from_gui()) def remove_selected_target(self): selected_items = self.target_table.selection() for i in selected_items: self.target_table.delete(i) self.check_target_warnings() if self.plot_manager: self.plot_manager.redraw_ppi_targets(self.get_targets_from_gui()) def get_radar_config_from_gui(self) -> RadarConfig: antenna_cfg = AntennaConfig( beamwidth_az_deg=self.vars["beamwidth_az_deg"].get(), beamwidth_el_deg=self.vars["beamwidth_el_deg"].get() ) scan_cfg = ScanConfig( mode=self.vars["scan_mode"].get(), min_az_deg=self.vars["min_az_deg"].get(), max_az_deg=self.vars["max_az_deg"].get(), scan_speed_deg_s=self.vars["scan_speed_deg_s"].get() ) 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]: targets = [] for item in self.target_table.get_children(): values = 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] targets.append(Target(initial_position=pos, velocity=vel, rcs=rcs)) except (ValueError, IndexError) as e: messagebox.showwarning("Invalid Data", f"Skipping invalid target data: {values}. Error: {e}", parent=self) return targets def _update_analysis_text(self, radar_cfg, targets, current_az_deg, iq_data_cpi): report = [] max_range = radar_math.calculate_max_unambiguous_range(radar_cfg.prf) max_vel = radar_math.calculate_max_unambiguous_velocity(radar_cfg.carrier_frequency, radar_cfg.prf) report.append(f"--- Radar Configuration Analysis ---") report.append(f"Max Unambiguous Range: {max_range:,.0f} m") report.append(f"Max Unambiguous Velocity: \u00b1{max_vel:,.1f} m/s") report.append(f"Current Antenna Azimuth: {current_az_deg:,.1f} deg") if radar_cfg.scan_config.mode != 'staring' and radar_cfg.scan_config.scan_speed_deg_s > 0: dwell_time = radar_math.calculate_dwell_time(radar_cfg.antenna_config.beamwidth_az_deg, radar_cfg.scan_config.scan_speed_deg_s) pulses_on_target = radar_math.calculate_pulses_on_target(dwell_time, radar_cfg.prf) report.append(f"Calculated Dwell Time: {dwell_time * 1e3:,.2f} ms") report.append(f"Calculated Pulses on Target: {pulses_on_target:,}") if pulses_on_target < self.vars["num_pulses_cpi"].get(): report.append("WARNING: Pulses per CPI > Pulses on Target") report.append(f"\n--- Target Analysis ---") if not targets: report.append("No targets defined.") else: for i, target in enumerate(targets): report.append(f"\nTarget {i+1} (RCS={target.rcs:.1f} m^2) - Analysis based on initial state.") self.analysis_text.config(state=tk.NORMAL) self.analysis_text.delete(1.0, tk.END) self.analysis_text.insert(tk.END, "\n".join(report)) self.analysis_text.config(state=tk.DISABLED) self.analysis_text.see(tk.END) def update_derived_parameters(self, *args): try: prf = self.vars["prf"].get() carrier_freq = self.vars["carrier_frequency"].get() duty_cycle = self.vars["duty_cycle"].get() beamwidth = self.vars["beamwidth_az_deg"].get() scan_mode = self.vars["scan_mode"].get() scan_speed = self.vars["scan_speed_deg_s"].get() pri = 1.0 / prf pulse_width = pri * (duty_cycle / 100.0) listening_time = pri self.vars["pulse_width_text"].set(f"Pulse Width: {pulse_width * 1e6:,.2f} µs") self.vars["listening_time_text"].set(f"Listening Window: {listening_time * 1e6:,.2f} µs (Max Range Time)") max_range = radar_math.calculate_max_unambiguous_range(prf) max_vel = radar_math.calculate_max_unambiguous_velocity(carrier_freq, prf) self.vars["max_range_text"].set(f"Max Unambiguous Range: {max_range:,.0f} m") self.vars["max_velocity_text"].set(f"Max Unambiguous Velocity: \u00b1{max_vel:,.1f} m/s") if scan_mode == 'staring': self.vars["dwell_time_text"].set("Dwell Time: N/A (Staring)") self.vars["pulses_on_target_text"].set("Pulses on Target: N/A (Staring)") else: dwell_time = radar_math.calculate_dwell_time(beamwidth, scan_speed) pulses_on_target = radar_math.calculate_pulses_on_target(dwell_time, prf) self.vars["dwell_time_text"].set(f"Dwell Time: {dwell_time * 1e3:,.2f} ms") self.vars["pulses_on_target_text"].set(f"Pulses on Target: {pulses_on_target:,}") self.check_target_warnings() self._update_scan_info_text() except (tk.TclError, ValueError, ZeroDivisionError): pass def _update_scan_info_text(self): mode = self.vars['scan_mode'].get() if mode == 'staring': self.vars['scan_info_text'].set("Mode: Staring") else: min_az = self.vars['min_az_deg'].get() max_az = self.vars['max_az_deg'].get() speed = self.vars['scan_speed_deg_s'].get() self.vars['scan_info_text'].set(f"Mode: Sector Scan\nAz: [{min_az}°, {max_az}°] @ {speed}°/s") def check_target_warnings(self): if not hasattr(self, 'target_table') or not hasattr(self, 'max_range_label'): return try: prf = self.vars["prf"].get() carrier_freq = self.vars["carrier_frequency"].get() max_range = radar_math.calculate_max_unambiguous_range(prf) max_vel = radar_math.calculate_max_unambiguous_velocity(carrier_freq, prf) range_warning, vel_warning = False, False for item in self.target_table.get_children(): values = [float(v) for v in self.target_table.item(item)['values']] target_initial_range = np.linalg.norm(values[0:3]) target_radial_vel = np.dot(values[3:6], values[0:3] / target_initial_range) if target_initial_range > 0 else 0 if target_initial_range > max_range: range_warning = True if abs(target_radial_vel) > max_vel: vel_warning = True self.max_range_label.config(foreground='orange' if range_warning else 'black') self.max_velocity_label.config(foreground='orange' if vel_warning else 'black') except (tk.TclError, ValueError, ZeroDivisionError): if hasattr(self, 'max_range_label'): self.max_range_label.config(foreground='black') if hasattr(self, 'max_velocity_label'): self.max_velocity_label.config(foreground='black') def open_add_target_dialog(self): dialog = AddTargetDialog(self) result = dialog.show() if result: self.add_target_to_table(result) def on_target_double_click(self, event): selected_item = self.target_table.selection() if not selected_item: return item_data = self.target_table.item(selected_item[0])['values'] target_data = { "pos_x": float(item_data[0]), "pos_y": float(item_data[1]), "pos_z": float(item_data[2]), "vel_x": float(item_data[3]), "vel_y": float(item_data[4]), "vel_z": float(item_data[5]), "rcs": float(item_data[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()]) self.check_target_warnings() if self.plot_manager: self.plot_manager.redraw_ppi_targets(self.get_targets_from_gui()) def on_profile_select(self, event=None): profile_name = self.selected_profile.get() if profile_name in self.profiles: profile_data = self.profiles[profile_name] for key, value in profile_data.items(): if key in self.vars: self.vars[key].set(value) self.update_scan_mode_controls() messagebox.showinfo("Profile Loaded", f"Profile '{profile_name}' has been loaded.", parent=self) def save_profile(self): profile_name = simpledialog.askstring("Save Profile", "Enter a name for this profile:", parent=self) if not profile_name or not profile_name.strip(): return profile_name = profile_name.strip() if profile_name in self.profiles: if not messagebox.askyesno("Overwrite Profile", f"Profile '{profile_name}' already exists. Overwrite it?", parent=self): return current_config = {key: var.get() for key, var in self.vars.items() if isinstance(var, (tk.DoubleVar, tk.StringVar, tk.IntVar))} self.profiles[profile_name] = current_config if config_manager.save_profiles(self.profiles): self.refresh_profile_list() self.selected_profile.set(profile_name) messagebox.showinfo("Profile Saved", f"Profile '{profile_name}' saved successfully.", parent=self) else: messagebox.showerror("Error", "Could not save profiles to file.", parent=self) def delete_profile(self): profile_name = self.selected_profile.get() if not profile_name: messagebox.showwarning("No Profile Selected", "Please select a profile to delete.", parent=self); return if messagebox.askyesno("Delete Profile", f"Are you sure you want to delete the profile '{profile_name}'?", parent=self): if profile_name in self.profiles: del self.profiles[profile_name] if config_manager.save_profiles(self.profiles): self.refresh_profile_list() messagebox.showinfo("Profile Deleted", f"Profile '{profile_name}' has been deleted.", parent=self) else: messagebox.showerror("Error", "Could not save profiles to file.", parent=self) def refresh_profile_list(self): self.profile_combobox['values'] = sorted(list(self.profiles.keys())) self.selected_profile.set('') def toggle_amplitude_controls(self): state = tk.DISABLED if self.vars["auto_scale"].get() else tk.NORMAL self.min_db_spinbox.config(state=state) self.max_db_spinbox.config(state=state) def update_scan_mode_controls(self, *args): mode = self.vars["scan_mode"].get() state = tk.NORMAL if mode != 'staring' else tk.DISABLED if mode == 'staring': self.vars["scan_speed_deg_s"].set(0.0001) elif self.vars["scan_speed_deg_s"].get() <= 0.0001: self.vars["scan_speed_deg_s"].set(20.0) self.min_az_spinbox.config(state=state) self.max_az_spinbox.config(state=state) self.scan_speed_spinbox.config(state=state) self.update_derived_parameters() def start_gui(): """Entry point to launch the Tkinter GUI application.""" app = App() app.mainloop()