179 lines
6.8 KiB
Python
179 lines
6.8 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
|
|
import matplotlib.transforms as transforms
|
|
import matplotlib.markers as markers
|
|
|
|
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.target_artists = []
|
|
|
|
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)
|
|
|
|
# Set custom theta labels for sector scan view
|
|
angles = np.arange(0, 360, 30)
|
|
labels = []
|
|
for angle in angles:
|
|
if angle == 0:
|
|
labels.append("0°")
|
|
elif angle < 180:
|
|
labels.append(f"+{angle}°")
|
|
elif angle == 180:
|
|
labels.append("±180°")
|
|
else: # angle > 180
|
|
labels.append(f"-{360 - angle}°")
|
|
self.ax.set_thetagrids(angles, labels)
|
|
|
|
# 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)
|
|
|
|
# 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)
|
|
# Update heading vector lengths on zoom
|
|
self.update_targets([]) # Pass empty list to redraw existing ones with new scale
|
|
self.canvas.draw()
|
|
|
|
def update_targets(self, targets: List[Target]):
|
|
"""
|
|
Aggiorna la posizione dei target sulla PPI e mostra l'ID accanto al punto. Tooltip con ID al passaggio del mouse.
|
|
"""
|
|
if targets:
|
|
self.active_targets = [t for t in targets if t.active]
|
|
|
|
# Rimuovi artisti precedenti
|
|
for artist in self.target_artists:
|
|
artist.remove()
|
|
self.target_artists.clear()
|
|
|
|
# Rimuovi eventuali tooltips precedenti
|
|
if hasattr(self, 'tooltips'):
|
|
for tip in self.tooltips:
|
|
tip.remove()
|
|
self.tooltips = []
|
|
|
|
vector_len = self.range_var.get() / 25
|
|
self._target_dots = []
|
|
|
|
for target in getattr(self, 'active_targets', []):
|
|
r1 = target.current_range_nm
|
|
th1 = np.deg2rad(target.current_azimuth_deg)
|
|
# Punto del target
|
|
dot, = self.ax.plot(th1, r1, 'o', markersize=5, color='red', picker=5)
|
|
self.target_artists.append(dot)
|
|
self._target_dots.append((dot, target))
|
|
|
|
# Vettore heading
|
|
x1 = r1 * np.sin(th1)
|
|
y1 = r1 * np.cos(th1)
|
|
h_rad = np.deg2rad(target.current_heading_deg)
|
|
dx = vector_len * np.sin(h_rad)
|
|
dy = vector_len * np.cos(h_rad)
|
|
x2, y2 = x1 + dx, y1 + dy
|
|
r2 = np.sqrt(x2**2 + y2**2)
|
|
th2 = np.arctan2(x2, y2)
|
|
line, = self.ax.plot([th1, th2], [r1, r2], color='red', linewidth=1.2)
|
|
self.target_artists.append(line)
|
|
|
|
# Gestione tooltip
|
|
def on_motion(event):
|
|
# Mostra hint se il mouse è vicino a un punto target
|
|
if event.inaxes != self.ax:
|
|
if hasattr(self, '_tooltip_label') and self._tooltip_label:
|
|
self._tooltip_label.place_forget()
|
|
self._tooltip_label = None
|
|
return
|
|
found = False
|
|
for dot, target in self._target_dots:
|
|
cont, _ = dot.contains(event)
|
|
if cont:
|
|
# Usa la posizione del mouse relativa al widget Tkinter
|
|
self._show_tooltip(event.x + 10, event.y + 10, f"ID: {target.target_id}")
|
|
found = True
|
|
break
|
|
if not found and hasattr(self, '_tooltip_label') and self._tooltip_label:
|
|
self._tooltip_label.place_forget()
|
|
self._tooltip_label = None
|
|
|
|
self.canvas.mpl_connect('motion_notify_event', on_motion)
|
|
self._tooltip_label = None
|
|
self.canvas.draw()
|
|
|
|
def _show_tooltip(self, x, y, text):
|
|
# Mostra un tooltip Tkinter sopra la canvas, centrato sul punto target
|
|
if self._tooltip_label:
|
|
self._tooltip_label.place_forget()
|
|
self._tooltip_label = tk.Label(self.canvas.get_tk_widget(), text=text, bg='yellow', fg='black', font=('Consolas', 9), relief='solid', borderwidth=1)
|
|
self._tooltip_label.place(x=x, y=y) |