"""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}