aggiunta su ppi la visualizzazione dell'antenna che si muove
This commit is contained in:
parent
20b7fd41ad
commit
34a6737fcc
@ -56,6 +56,10 @@ class SimulationStateHub:
|
|||||||
# Summary throttle to avoid flooding logs while still providing throughput info
|
# Summary throttle to avoid flooding logs while still providing throughput info
|
||||||
self._last_real_summary_time = time.monotonic()
|
self._last_real_summary_time = time.monotonic()
|
||||||
self._real_summary_interval_s = 1.0
|
self._real_summary_interval_s = 1.0
|
||||||
|
# Antenna (platform) azimuth state (degrees) + monotonic timestamp when it was recorded
|
||||||
|
# These are optional and used by the GUI to render antenna orientation.
|
||||||
|
self._antenna_azimuth_deg = None
|
||||||
|
self._antenna_azimuth_ts = None
|
||||||
|
|
||||||
def add_simulated_state(
|
def add_simulated_state(
|
||||||
self, target_id: int, timestamp: float, state: Tuple[float, ...]
|
self, target_id: int, timestamp: float, state: Tuple[float, ...]
|
||||||
@ -241,6 +245,53 @@ class SimulationStateHub:
|
|||||||
# On error, do nothing (silently ignore invalid heading)
|
# On error, do nothing (silently ignore invalid heading)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def set_antenna_azimuth(
|
||||||
|
self, azimuth_deg: float, timestamp: Optional[float] = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Store the latest antenna (platform) azimuth in degrees along with an
|
||||||
|
optional monotonic timestamp. The GUI can retrieve this to render the
|
||||||
|
antenna orientation and smoothly animate between updates.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
azimuth_deg: Azimuth value in degrees (0 = North, positive CCW)
|
||||||
|
timestamp: Optional monotonic timestamp (defaults to time.monotonic())
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ts = float(timestamp) if timestamp is not None else time.monotonic()
|
||||||
|
az = float(azimuth_deg) % 360
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
with self._lock:
|
||||||
|
self._antenna_azimuth_deg = az
|
||||||
|
self._antenna_azimuth_ts = ts
|
||||||
|
|
||||||
|
def get_antenna_azimuth(self) -> Tuple[Optional[float], Optional[float]]:
|
||||||
|
"""
|
||||||
|
Returns a tuple (azimuth_deg, timestamp) representing the last-known
|
||||||
|
antenna azimuth and the monotonic timestamp when it was recorded.
|
||||||
|
If not available, returns (None, None).
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return (self._antenna_azimuth_deg, self._antenna_azimuth_ts)
|
||||||
|
|
||||||
|
# Backwards-compatible aliases for existing callers. These delegate to the
|
||||||
|
# new antenna-named methods so external code using the older names continues
|
||||||
|
# to work.
|
||||||
|
def set_platform_azimuth(
|
||||||
|
self, azimuth_deg: float, timestamp: Optional[float] = None
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
self.set_antenna_azimuth(azimuth_deg, timestamp=timestamp)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_platform_azimuth(self) -> Tuple[Optional[float], Optional[float]]:
|
||||||
|
try:
|
||||||
|
return self.get_antenna_azimuth()
|
||||||
|
except Exception:
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
def get_real_heading(self, target_id: int) -> Optional[float]:
|
def get_real_heading(self, target_id: int) -> Optional[float]:
|
||||||
"""
|
"""
|
||||||
Retrieve the last stored real heading for a target, or None if not set.
|
Retrieve the last stored real heading for a target, or None if not set.
|
||||||
|
|||||||
@ -954,6 +954,39 @@ class MainView(tk.Tk):
|
|||||||
# self.logger.debug("MainView: received hub refresh. Updating real targets.")
|
# self.logger.debug("MainView: received hub refresh. Updating real targets.")
|
||||||
display_data = self._build_display_data_from_hub()
|
display_data = self._build_display_data_from_hub()
|
||||||
self.ppi_widget.update_real_targets(display_data.get("real", []))
|
self.ppi_widget.update_real_targets(display_data.get("real", []))
|
||||||
|
# Also propagate platform/antenna azimuth (if available) so the PPI
|
||||||
|
# can render the antenna orientation. The hub stores a (az_deg, ts)
|
||||||
|
# tuple via set_platform_azimuth().
|
||||||
|
try:
|
||||||
|
if (
|
||||||
|
hasattr(self, "simulation_hub")
|
||||||
|
and self.simulation_hub is not None
|
||||||
|
and hasattr(self.ppi_widget, "update_antenna_azimuth")
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
# Prefer the new API name, fall back to the legacy
|
||||||
|
if hasattr(self.simulation_hub, "get_antenna_azimuth"):
|
||||||
|
az_deg, az_ts = (
|
||||||
|
self.simulation_hub.get_antenna_azimuth()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
az_deg, az_ts = (
|
||||||
|
self.simulation_hub.get_platform_azimuth()
|
||||||
|
)
|
||||||
|
|
||||||
|
if az_deg is not None:
|
||||||
|
# pass the hub-provided timestamp if available
|
||||||
|
self.ppi_widget.update_antenna_azimuth(
|
||||||
|
az_deg, timestamp=az_ts
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
# don't allow GUI update failures to interrupt queue processing
|
||||||
|
self.logger.debug(
|
||||||
|
"Failed to propagate antenna azimuth to PPI",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# This is an update with simulated targets from the engine.
|
# This is an update with simulated targets from the engine.
|
||||||
|
|||||||
@ -272,6 +272,57 @@ class DebugPayloadRouter:
|
|||||||
struct = {"scenario": scenario_dict, "targets": targets_list}
|
struct = {"scenario": scenario_dict, "targets": targets_list}
|
||||||
json_bytes = bytearray(json.dumps(struct, indent=2).encode("utf-8"))
|
json_bytes = bytearray(json.dumps(struct, indent=2).encode("utf-8"))
|
||||||
self._update_last_payload("RIS_STATUS_JSON", json_bytes)
|
self._update_last_payload("RIS_STATUS_JSON", json_bytes)
|
||||||
|
# Propagate antenna azimuth into the hub so the GUI can render
|
||||||
|
# the antenna orientation. Prefer the RIS field `ant_nav_az` (this
|
||||||
|
# is the antenna navigation azimuth). For backward compatibility
|
||||||
|
# fall back to `platform_azimuth` if `ant_nav_az` is not present.
|
||||||
|
try:
|
||||||
|
plat = None
|
||||||
|
if "ant_nav_az" in scenario_dict:
|
||||||
|
plat = scenario_dict.get("ant_nav_az")
|
||||||
|
elif "platform_azimuth" in scenario_dict:
|
||||||
|
plat = scenario_dict.get("platform_azimuth")
|
||||||
|
|
||||||
|
if (
|
||||||
|
plat is not None
|
||||||
|
and self._hub
|
||||||
|
and hasattr(self._hub, "set_platform_azimuth")
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
val = float(plat)
|
||||||
|
# If the value looks like radians (<= ~2*pi) convert to degrees
|
||||||
|
if abs(val) <= (2 * math.pi * 1.1):
|
||||||
|
deg = math.degrees(val)
|
||||||
|
else:
|
||||||
|
deg = val
|
||||||
|
recv_ts = (
|
||||||
|
reception_timestamp
|
||||||
|
if "reception_timestamp" in locals()
|
||||||
|
else time.monotonic()
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
# New API: set_antenna_azimuth
|
||||||
|
if hasattr(self._hub, "set_antenna_azimuth"):
|
||||||
|
self._hub.set_antenna_azimuth(
|
||||||
|
deg, timestamp=recv_ts
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Fallback to legacy name if present
|
||||||
|
self._hub.set_platform_azimuth(
|
||||||
|
deg, timestamp=recv_ts
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
self._logger.debug(
|
||||||
|
"Failed to set antenna/platform azimuth on hub",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
self._logger.debug(
|
||||||
|
"Error while extracting antenna azimuth from RIS payload",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
self._logger.exception("Failed to generate text/JSON for RIS debug view.")
|
self._logger.exception("Failed to generate text/JSON for RIS debug view.")
|
||||||
|
|
||||||
|
|||||||
@ -60,6 +60,8 @@ class PPIDisplay(ttk.Frame):
|
|||||||
self.show_sim_trail_var = tk.BooleanVar(value=False)
|
self.show_sim_trail_var = tk.BooleanVar(value=False)
|
||||||
# Default: do not show real trails unless the user enables them.
|
# Default: do not show real trails unless the user enables them.
|
||||||
self.show_real_trail_var = tk.BooleanVar(value=False)
|
self.show_real_trail_var = tk.BooleanVar(value=False)
|
||||||
|
# Antenna animate toggle: when False the antenna is hidden; when True it is shown and animated
|
||||||
|
self.animate_antenna_var = tk.BooleanVar(value=True)
|
||||||
self.canvas = None
|
self.canvas = None
|
||||||
self._create_controls()
|
self._create_controls()
|
||||||
self._create_plot()
|
self._create_plot()
|
||||||
@ -67,6 +69,17 @@ class PPIDisplay(ttk.Frame):
|
|||||||
self._real_update_timestamps = collections.deque(maxlen=10000)
|
self._real_update_timestamps = collections.deque(maxlen=10000)
|
||||||
self._last_update_summary_time = time.monotonic()
|
self._last_update_summary_time = time.monotonic()
|
||||||
self._update_summary_interval_s = 1.0
|
self._update_summary_interval_s = 1.0
|
||||||
|
# Antenna/Platform visualization state used to animate a moving
|
||||||
|
# dashed line indicating current antenna azimuth.
|
||||||
|
self._antenna_state = {
|
||||||
|
"last_az_deg": None,
|
||||||
|
"last_ts": None,
|
||||||
|
"next_az_deg": None,
|
||||||
|
"next_ts": None,
|
||||||
|
"animating": False,
|
||||||
|
"tick_ms": 33, # ~30 FPS animation cadence
|
||||||
|
}
|
||||||
|
self._antenna_line_artist = None
|
||||||
|
|
||||||
def _on_display_options_changed(self):
|
def _on_display_options_changed(self):
|
||||||
# A full redraw is needed, but we don't have the last data sets.
|
# A full redraw is needed, but we don't have the last data sets.
|
||||||
@ -127,6 +140,14 @@ class PPIDisplay(ttk.Frame):
|
|||||||
command=self._on_display_options_changed,
|
command=self._on_display_options_changed,
|
||||||
)
|
)
|
||||||
cb_real_trail.grid(row=1, column=1, sticky="w", padx=5)
|
cb_real_trail.grid(row=1, column=1, sticky="w", padx=5)
|
||||||
|
# Antenna animate toggle (single Checkbutton)
|
||||||
|
cb_antenna = ttk.Checkbutton(
|
||||||
|
options_frame,
|
||||||
|
text="Animate Antenna",
|
||||||
|
variable=self.animate_antenna_var,
|
||||||
|
command=self._on_antenna_animate_changed,
|
||||||
|
)
|
||||||
|
cb_antenna.grid(row=2, column=0, columnspan=2, sticky="w", padx=5, pady=(6, 0))
|
||||||
legend_frame = ttk.Frame(top_frame)
|
legend_frame = ttk.Frame(top_frame)
|
||||||
legend_frame.pack(side=tk.RIGHT, padx=(10, 5))
|
legend_frame.pack(side=tk.RIGHT, padx=(10, 5))
|
||||||
sim_sw = tk.Canvas(legend_frame, width=16, height=12, highlightthickness=0)
|
sim_sw = tk.Canvas(legend_frame, width=16, height=12, highlightthickness=0)
|
||||||
@ -165,6 +186,12 @@ class PPIDisplay(ttk.Frame):
|
|||||||
(self._scan_line_2,) = self.ax.plot(
|
(self._scan_line_2,) = self.ax.plot(
|
||||||
[-limit_rad, -limit_rad], [0, self.max_range], "y--", linewidth=1
|
[-limit_rad, -limit_rad], [0, self.max_range], "y--", linewidth=1
|
||||||
)
|
)
|
||||||
|
# Antenna current azimuth (dashed light-gray line). It will be
|
||||||
|
# animated via update_antenna_azimuth() calls which interpolate
|
||||||
|
# between timestamped azimuth updates for smooth motion.
|
||||||
|
(self._antenna_line_artist,) = self.ax.plot(
|
||||||
|
[], [], color="lightgray", linestyle="--", linewidth=1.2, alpha=0.85
|
||||||
|
)
|
||||||
self.canvas = FigureCanvasTkAgg(fig, master=self)
|
self.canvas = FigureCanvasTkAgg(fig, master=self)
|
||||||
self.canvas.draw()
|
self.canvas.draw()
|
||||||
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
||||||
@ -374,6 +401,17 @@ class PPIDisplay(ttk.Frame):
|
|||||||
limit_rad = np.deg2rad(self.scan_limit_deg)
|
limit_rad = np.deg2rad(self.scan_limit_deg)
|
||||||
self._scan_line_1.set_data([limit_rad, limit_rad], [0, max_r])
|
self._scan_line_1.set_data([limit_rad, limit_rad], [0, max_r])
|
||||||
self._scan_line_2.set_data([-limit_rad, -limit_rad], [0, max_r])
|
self._scan_line_2.set_data([-limit_rad, -limit_rad], [0, max_r])
|
||||||
|
# Ensure antenna line extends to the updated range limit
|
||||||
|
try:
|
||||||
|
if self._antenna_line_artist is not None:
|
||||||
|
data = self._antenna_line_artist.get_data()
|
||||||
|
if data and len(data) == 2:
|
||||||
|
thetas, rs = data
|
||||||
|
if len(thetas) >= 1:
|
||||||
|
theta = thetas[0]
|
||||||
|
self._antenna_line_artist.set_data([theta, theta], [0, max_r])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _on_range_selected(self, event=None):
|
def _on_range_selected(self, event=None):
|
||||||
self.ax.set_ylim(0, self.range_var.get())
|
self.ax.set_ylim(0, self.range_var.get())
|
||||||
@ -381,12 +419,234 @@ class PPIDisplay(ttk.Frame):
|
|||||||
if self.canvas:
|
if self.canvas:
|
||||||
self.canvas.draw()
|
self.canvas.draw()
|
||||||
|
|
||||||
|
def _on_antenna_animate_changed(self):
|
||||||
|
"""
|
||||||
|
Callback when the animate checkbox toggles. When unchecked the antenna
|
||||||
|
is hidden; when checked the antenna is shown and animation resumes if
|
||||||
|
a pending interpolation exists.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
st = self._antenna_state
|
||||||
|
enabled = (
|
||||||
|
getattr(self, "animate_antenna_var", None)
|
||||||
|
and self.animate_antenna_var.get()
|
||||||
|
)
|
||||||
|
if not enabled:
|
||||||
|
# Hide the antenna and stop animating
|
||||||
|
st["animating"] = False
|
||||||
|
if self._antenna_line_artist is not None:
|
||||||
|
try:
|
||||||
|
self._antenna_line_artist.set_data([], [])
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self._antenna_line_artist.remove()
|
||||||
|
self._antenna_line_artist = None
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if self.canvas:
|
||||||
|
try:
|
||||||
|
self.canvas.draw_idle()
|
||||||
|
except Exception:
|
||||||
|
self.canvas.draw()
|
||||||
|
return
|
||||||
|
|
||||||
|
# If enabled: resume animation if there's a pending interval
|
||||||
|
last_ts = st.get("last_ts")
|
||||||
|
next_ts = st.get("next_ts")
|
||||||
|
if last_ts is not None and next_ts is not None and next_ts > last_ts:
|
||||||
|
if not st.get("animating"):
|
||||||
|
st["animating"] = True
|
||||||
|
try:
|
||||||
|
self.after(st.get("tick_ms", 33), self._antenna_animation_step)
|
||||||
|
except Exception:
|
||||||
|
st["animating"] = False
|
||||||
|
else:
|
||||||
|
# Nothing to interpolate: render the most recent azimuth immediately
|
||||||
|
cur = st.get("last_az_deg") or st.get("next_az_deg")
|
||||||
|
if cur is not None:
|
||||||
|
self._render_antenna_line(cur)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def clear_previews(self):
|
def clear_previews(self):
|
||||||
for artist in self.preview_artists:
|
for artist in self.preview_artists:
|
||||||
artist.set_data([], [])
|
artist.set_data([], [])
|
||||||
if self.canvas:
|
if self.canvas:
|
||||||
self.canvas.draw()
|
self.canvas.draw()
|
||||||
|
|
||||||
|
# -------------------- Antenna visualization & interpolation --------------------
|
||||||
|
def update_antenna_azimuth(self, az_deg: float, timestamp: float = None):
|
||||||
|
"""
|
||||||
|
Receive a new platform/antenna azimuth (degrees) with an optional
|
||||||
|
monotonic timestamp. The display will interpolate between the last
|
||||||
|
known azimuth and this new azimuth over the time interval to provide
|
||||||
|
a smooth animation.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
now = time.monotonic()
|
||||||
|
ts = float(timestamp) if timestamp is not None else now
|
||||||
|
az = float(az_deg) % 360
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
st = self._antenna_state
|
||||||
|
|
||||||
|
# If antenna animate is disabled, update stored azimuth but hide the antenna
|
||||||
|
try:
|
||||||
|
if (
|
||||||
|
getattr(self, "animate_antenna_var", None) is not None
|
||||||
|
and not self.animate_antenna_var.get()
|
||||||
|
):
|
||||||
|
st["last_az_deg"] = az
|
||||||
|
st["last_ts"] = ts
|
||||||
|
st["next_az_deg"] = az
|
||||||
|
st["next_ts"] = ts
|
||||||
|
st["animating"] = False
|
||||||
|
# Hide the visible antenna line if present
|
||||||
|
if self._antenna_line_artist is not None:
|
||||||
|
try:
|
||||||
|
self._antenna_line_artist.set_data([], [])
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self._antenna_line_artist.remove()
|
||||||
|
self._antenna_line_artist = None
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if self.canvas:
|
||||||
|
try:
|
||||||
|
self.canvas.draw_idle()
|
||||||
|
except Exception:
|
||||||
|
self.canvas.draw()
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# If no previous sample exists, initialize both last and next to this value
|
||||||
|
if st["last_az_deg"] is None or st["last_ts"] is None:
|
||||||
|
st["last_az_deg"] = az
|
||||||
|
st["last_ts"] = ts
|
||||||
|
st["next_az_deg"] = az
|
||||||
|
st["next_ts"] = ts
|
||||||
|
# Render immediately
|
||||||
|
self._render_antenna_line(az)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Compute the current interpolated azimuth at 'now' and set as last
|
||||||
|
# so the new interpolation starts from the on-screen position.
|
||||||
|
cur_az = st["last_az_deg"]
|
||||||
|
cur_ts = st["last_ts"]
|
||||||
|
next_az = st.get("next_az_deg")
|
||||||
|
next_ts = st.get("next_ts")
|
||||||
|
|
||||||
|
# If there is an outstanding next sample in the future, compute
|
||||||
|
# current interpolated value to be the new last.
|
||||||
|
try:
|
||||||
|
if next_az is not None and next_ts is not None and next_ts > cur_ts:
|
||||||
|
now_t = now
|
||||||
|
frac = 0.0
|
||||||
|
if next_ts > cur_ts:
|
||||||
|
frac = max(
|
||||||
|
0.0, min(1.0, (now_t - cur_ts) / float(next_ts - cur_ts))
|
||||||
|
)
|
||||||
|
# Shortest-angle interpolation
|
||||||
|
a0 = cur_az
|
||||||
|
a1 = next_az
|
||||||
|
diff = ((a1 - a0 + 180) % 360) - 180
|
||||||
|
interp = (a0 + diff * frac) % 360
|
||||||
|
cur_az = interp
|
||||||
|
cur_ts = now_t
|
||||||
|
except Exception:
|
||||||
|
# If interpolation fails, fall back to last known
|
||||||
|
cur_az = st["last_az_deg"]
|
||||||
|
cur_ts = st["last_ts"]
|
||||||
|
|
||||||
|
# Set new last and next values for upcoming animation
|
||||||
|
st["last_az_deg"] = cur_az
|
||||||
|
st["last_ts"] = cur_ts
|
||||||
|
st["next_az_deg"] = az
|
||||||
|
st["next_ts"] = ts
|
||||||
|
|
||||||
|
# Start the animation loop if not already running
|
||||||
|
if not st["animating"]:
|
||||||
|
st["animating"] = True
|
||||||
|
try:
|
||||||
|
self.after(st["tick_ms"], self._antenna_animation_step)
|
||||||
|
except Exception:
|
||||||
|
st["animating"] = False
|
||||||
|
|
||||||
|
def _antenna_animation_step(self):
|
||||||
|
st = self._antenna_state
|
||||||
|
try:
|
||||||
|
# If next and last are the same timestamp, snap to next
|
||||||
|
last_ts = st.get("last_ts")
|
||||||
|
next_ts = st.get("next_ts")
|
||||||
|
last_az = st.get("last_az_deg")
|
||||||
|
next_az = st.get("next_az_deg")
|
||||||
|
|
||||||
|
now = time.monotonic()
|
||||||
|
|
||||||
|
if last_az is None or next_az is None or last_ts is None or next_ts is None:
|
||||||
|
st["animating"] = False
|
||||||
|
return
|
||||||
|
|
||||||
|
if next_ts <= last_ts or abs(next_ts - last_ts) < 1e-6:
|
||||||
|
# No interval: snap
|
||||||
|
cur = next_az % 360
|
||||||
|
st["last_az_deg"] = cur
|
||||||
|
st["last_ts"] = next_ts
|
||||||
|
st["animating"] = False
|
||||||
|
self._render_antenna_line(cur)
|
||||||
|
return
|
||||||
|
|
||||||
|
frac = max(0.0, min(1.0, (now - last_ts) / float(next_ts - last_ts)))
|
||||||
|
# Shortest-angle interpolation across 0/360 boundary
|
||||||
|
a0 = last_az
|
||||||
|
a1 = next_az
|
||||||
|
diff = ((a1 - a0 + 180) % 360) - 180
|
||||||
|
cur = (a0 + diff * frac) % 360
|
||||||
|
|
||||||
|
self._render_antenna_line(cur)
|
||||||
|
|
||||||
|
if frac >= 1.0:
|
||||||
|
# Reached the target
|
||||||
|
st["last_az_deg"] = next_az % 360
|
||||||
|
st["last_ts"] = next_ts
|
||||||
|
st["animating"] = False
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
st["animating"] = False
|
||||||
|
# Schedule next tick if still animating
|
||||||
|
try:
|
||||||
|
if st.get("animating"):
|
||||||
|
self.after(st.get("tick_ms", 33), self._antenna_animation_step)
|
||||||
|
except Exception:
|
||||||
|
st["animating"] = False
|
||||||
|
|
||||||
|
def _render_antenna_line(self, az_deg: float):
|
||||||
|
"""
|
||||||
|
Render the antenna (platform) azimuth line on the PPI using the
|
||||||
|
current display conventions (theta = deg -> radians) and current range limit.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
theta = np.deg2rad(float(az_deg) % 360)
|
||||||
|
max_r = self.ax.get_ylim()[1]
|
||||||
|
if self._antenna_line_artist is None:
|
||||||
|
# Create artist lazily if missing
|
||||||
|
(self._antenna_line_artist,) = self.ax.plot(
|
||||||
|
[], [], color="lightgray", linestyle="--", linewidth=1.2, alpha=0.85
|
||||||
|
)
|
||||||
|
# Plot as a radial line from r=0 to r=max_r
|
||||||
|
self._antenna_line_artist.set_data([theta, theta], [0, max_r])
|
||||||
|
# Use draw_idle for better GUI responsiveness
|
||||||
|
if self.canvas:
|
||||||
|
try:
|
||||||
|
self.canvas.draw_idle()
|
||||||
|
except Exception:
|
||||||
|
# Fall back to immediate draw
|
||||||
|
self.canvas.draw()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def draw_trajectory_preview(self, waypoints: List[Waypoint], use_spline: bool):
|
def draw_trajectory_preview(self, waypoints: List[Waypoint], use_spline: bool):
|
||||||
self.clear_previews()
|
self.clear_previews()
|
||||||
self.clear_trails()
|
self.clear_trails()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user