S1005403_RisCC/target_simulator/gui/ppi_adapter.py
2025-11-13 10:57:10 +01:00

178 lines
7.2 KiB
Python

"""PPI adapter utilities.
This module converts absolute states stored in the SimulationStateHub into
lightweight Target objects positioned relative to ownship and suitable for
consumption by the :class:`target_simulator.gui.ppi_display.PPIDisplay`.
Public API:
- build_display_data(simulation_hub, ...): returns a dict with keys
``'simulated'`` and ``'real'`` containing lists of targets for display.
"""
from typing import Dict, List, Optional, TYPE_CHECKING
if TYPE_CHECKING:
# Avoid runtime imports; enable type checking for forward references
from target_simulator.core.simulation_engine import SimulationEngine
from target_simulator.gui.ppi_display import PPIDisplay
from target_simulator.core.models import Scenario
from logging import Logger
import math
from target_simulator.core.models import Target
from target_simulator.analysis.simulation_state_hub import SimulationStateHub
def build_display_data(
simulation_hub: SimulationStateHub,
scenario: Optional["Scenario"] = None,
engine: Optional["SimulationEngine"] = None,
ppi_widget: Optional["PPIDisplay"] = None,
logger: Optional["Logger"] = None,
) -> Dict[str, List[Target]]:
"""
Builds PPI display data from the simulation hub, converting absolute
'real' coordinates to relative coordinates based on the ownship's position.
Returns a dict with keys 'simulated' and 'real' containing lightweight
Target objects suitable for passing to PPIDisplay.
"""
simulated_targets_for_ppi: List[Target] = []
real_targets_for_ppi: List[Target] = []
if not simulation_hub:
return {"simulated": [], "real": []}
# Get ownship state for coordinate transformation
ownship_state = simulation_hub.get_ownship_state()
ownship_pos_xy_ft = ownship_state.get("position_xy_ft")
# Get the fixed simulation origin state (ownship state at T=0)
simulation_origin = simulation_hub.get_simulation_origin()
target_ids = simulation_hub.get_all_target_ids()
for tid in target_ids:
history = simulation_hub.get_target_history(tid)
if not history:
continue
# --- Process Simulated Data (transforming from simulation frame to world frame) ---
if history.get("simulated"):
last_sim_state = history["simulated"][-1]
if len(last_sim_state) >= 6:
_ts, x_sim_ft, y_sim_ft, z_sim_ft, vel_fps, vert_vel_fps = (
last_sim_state[:6]
)
else:
_ts, x_sim_ft, y_sim_ft, z_sim_ft = last_sim_state
vel_fps, vert_vel_fps = 0.0, 0.0
# Default to identity transformation if origin is not set
x_origin_ft, y_origin_ft = 0.0, 0.0
heading_origin_rad = 0.0
if simulation_origin:
origin_pos = simulation_origin.get("position_xy_ft", (0.0, 0.0))
x_origin_ft, y_origin_ft = origin_pos
heading_origin_rad = math.radians(
simulation_origin.get("heading_deg", 0.0)
)
# 1. Rotate the simulated position by the initial heading of the ownship.
# This aligns the simulation frame with the world frame (North-up).
cos_h = math.cos(heading_origin_rad)
sin_h = math.sin(heading_origin_rad)
x_rotated = x_sim_ft * cos_h - y_sim_ft * sin_h
y_rotated = x_sim_ft * sin_h + y_sim_ft * cos_h
# 2. Translate by the initial position of the ownship.
# This gives the absolute world position of the simulated target.
x_abs_ft = x_rotated + x_origin_ft
y_abs_ft = y_rotated + y_origin_ft
# 3. Convert absolute world coordinates to coordinates relative to current ownship.
rel_x_ft, rel_y_ft = x_abs_ft, y_abs_ft
if ownship_pos_xy_ft:
rel_x_ft = x_abs_ft - ownship_pos_xy_ft[0]
rel_y_ft = y_abs_ft - ownship_pos_xy_ft[1]
sim_target = Target(target_id=tid, trajectory=[])
setattr(sim_target, "_pos_x_ft", rel_x_ft)
setattr(sim_target, "_pos_y_ft", rel_y_ft)
setattr(sim_target, "_pos_z_ft", z_sim_ft)
sim_target.current_velocity_fps = vel_fps
sim_target.current_vertical_velocity_fps = vert_vel_fps
sim_target._update_current_polar_coords()
try:
heading = None
if engine and getattr(engine, "scenario", None):
t = engine.scenario.get_target(tid)
if t:
# The target's heading is also in the simulation frame.
# It must be rotated by the origin heading to be in the world frame.
sim_heading_deg = getattr(t, "current_heading_deg", 0.0)
world_heading_deg = (
sim_heading_deg + math.degrees(heading_origin_rad)
) % 360
heading = world_heading_deg
if heading is None and scenario:
t2 = scenario.get_target(tid)
if t2:
sim_heading_deg = getattr(t2, "current_heading_deg", 0.0)
world_heading_deg = (
sim_heading_deg + math.degrees(heading_origin_rad)
) % 360
heading = world_heading_deg
if heading is not None:
sim_target.current_heading_deg = float(heading)
except Exception:
pass
sim_target.active = True
if engine and getattr(engine, "scenario", None):
t_engine = engine.scenario.get_target(tid)
if t_engine is not None:
sim_target.active = bool(getattr(t_engine, "active", True))
simulated_targets_for_ppi.append(sim_target)
# --- Process Real Data (transforming from absolute to relative) ---
if history.get("real"):
last_real_state = history["real"][-1]
_ts, abs_x_ft, abs_y_ft, abs_z_ft = last_real_state[:4]
rel_x_ft, rel_y_ft = abs_x_ft, abs_y_ft
if ownship_pos_xy_ft:
rel_x_ft = abs_x_ft - ownship_pos_xy_ft[0]
rel_y_ft = abs_y_ft - ownship_pos_xy_ft[1]
ownship_alt_ft = ownship_state.get("altitude_ft", 0.0)
rel_z_ft = abs_z_ft - ownship_alt_ft
real_target = Target(target_id=tid, trajectory=[])
setattr(real_target, "_pos_x_ft", rel_x_ft)
setattr(real_target, "_pos_y_ft", rel_y_ft)
setattr(real_target, "_pos_z_ft", rel_z_ft)
real_target._update_current_polar_coords()
hdg = simulation_hub.get_real_heading(tid)
if hdg is not None:
real_target.current_heading_deg = float(hdg) % 360
real_target.active = True
real_targets_for_ppi.append(real_target)
if logger:
logger.debug(
"PPI Adapter: Built display data (simulated=%d, real=%d)",
len(simulated_targets_for_ppi),
len(real_targets_for_ppi),
)
return {"simulated": simulated_targets_for_ppi, "real": real_targets_for_ppi}