S1005403_RisCC/target_simulator/gui/ppi_display.py
2025-10-01 14:17:56 +02:00

111 lines
4.0 KiB
Python

# target_simulator/gui/ppi_display.py
"""
A Tkinter widget that displays a Plan Position Indicator (PPI) using Matplotlib.
"""
import tkinter as tk
from tkinter import ttk
import math
import numpy as np
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from core.models import Target
from typing import List
class PPIDisplay(ttk.Frame):
"""A custom widget for the PPI radar display."""
def __init__(self, master, max_range_nm: int = 100, scan_limit_deg: int = 60):
super().__init__(master)
self.max_range = max_range_nm
self.scan_limit_deg = scan_limit_deg
self._create_controls()
self._create_plot()
def _create_controls(self):
"""Creates the control widgets for the PPI display."""
controls_frame = ttk.Frame(self)
controls_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)
ttk.Label(controls_frame, text="Range (NM):").pack(side=tk.LEFT)
# Generate range steps
steps = list(range(self.max_range, 19, -20))
if 10 not in steps:
steps.append(10)
self.range_var = tk.IntVar(value=self.max_range)
self.range_selector = ttk.Combobox(
controls_frame,
textvariable=self.range_var,
values=steps,
state="readonly",
width=5
)
self.range_selector.pack(side=tk.LEFT)
self.range_selector.bind("<<ComboboxSelected>>", self._on_range_selected)
def _create_plot(self):
"""Initializes the Matplotlib polar plot."""
# Dark theme for the plot
fig = Figure(figsize=(5, 5), dpi=100, facecolor='#3E3E3E')
fig.subplots_adjust(left=0.05, right=0.95, top=0.85, bottom=0.05)
self.ax = fig.add_subplot(111, projection='polar', facecolor='#2E2E2E')
# Configure polar axes
self.ax.set_theta_zero_location('N') # 0 degrees at the top
self.ax.set_theta_direction(-1) # Clockwise rotation
self.ax.set_rlabel_position(90)
self.ax.set_ylim(0, self.max_range)
# Style the plot
self.ax.tick_params(axis='x', colors='white')
self.ax.tick_params(axis='y', colors='white')
self.ax.grid(color='white', linestyle='--', linewidth=0.5, alpha=0.5)
self.ax.spines['polar'].set_color('white')
self.ax.set_title("PPI Display", color='white', y=1.08)
# Draw scan sector limits
limit_rad = np.deg2rad(self.scan_limit_deg)
self.ax.plot([limit_rad, limit_rad], [0, self.max_range], color='yellow', linestyle='--', linewidth=1)
self.ax.plot([-limit_rad, -limit_rad], [0, self.max_range], color='yellow', linestyle='--', linewidth=1)
# Initial plot for targets (an empty plot to be updated later)
self.target_plot, = self.ax.plot([], [], 'ro', markersize=6) # 'ro' = red circle
# Embed the plot in a Tkinter canvas
self.canvas = FigureCanvasTkAgg(fig, master=self)
self.canvas.draw()
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
def _on_range_selected(self, event):
"""Handles the selection of a new range from the combobox."""
new_range = self.range_var.get()
self.ax.set_ylim(0, new_range)
self.canvas.draw()
def update_targets(self, targets: List[Target]):
"""
Updates the positions of targets on the PPI display.
Args:
targets: A list of Target objects to display.
"""
active_targets = [t for t in targets if t.active]
if not active_targets:
self.target_plot.set_data([], [])
else:
# Matplotlib polar plot needs angles in radians
theta = [math.radians(t.azimuth_deg) for t in active_targets]
r = [t.range_nm for t in active_targets]
self.target_plot.set_data(theta, r)
# Redraw the canvas to show the changes
self.canvas.draw()