#!/usr/bin/env python3 """ PLFM Radar Dashboard - PyQt6 Edition with Embedded Leaflet Map =============================================================== A professional-grade radar tracking GUI using PyQt6 with embedded web-based Leaflet.js maps for real-time target visualization. Features: - Embedded interactive Leaflet map with OpenStreetMap tiles - Real-time target tracking and visualization - Python-to-JavaScript bridge for seamless updates - Dark theme UI matching existing radar dashboard style - Support for multiple tile servers (OSM, Google, satellite) - Marker clustering for dense target environments - Coverage area visualization - Target trails/history Author: PLFM Radar Team Version: 1.0.0 """ import sys import json import math import time import random import logging from dataclasses import dataclass, asdict from enum import Enum # PyQt6 imports from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, QLabel, QPushButton, QComboBox, QSpinBox, QDoubleSpinBox, QGroupBox, QGridLayout, QSplitter, QFrame, QStatusBar, QCheckBox, QTableWidget, QTableWidgetItem, QHeaderView ) from PyQt6.QtCore import ( Qt, QTimer, pyqtSignal, pyqtSlot, QObject ) from PyQt6.QtGui import ( QFont, QColor, QPalette ) from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebChannel import QWebChannel # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # ============================================================================= # Dark Theme Colors (matching existing radar dashboard) # ============================================================================= DARK_BG = "#2b2b2b" DARK_FG = "#e0e0e0" DARK_ACCENT = "#3c3f41" DARK_HIGHLIGHT = "#4e5254" DARK_BORDER = "#555555" DARK_TEXT = "#cccccc" DARK_BUTTON = "#3c3f41" DARK_BUTTON_HOVER = "#4e5254" DARK_SUCCESS = "#4CAF50" DARK_WARNING = "#FFC107" DARK_ERROR = "#F44336" DARK_INFO = "#2196F3" # ============================================================================= # Data Classes # ============================================================================= @dataclass class RadarTarget: """Represents a detected radar target""" id: int range: float # Range in meters velocity: float # Velocity in m/s (positive = approaching) azimuth: float # Azimuth angle in degrees elevation: float # Elevation angle in degrees latitude: float = 0.0 longitude: float = 0.0 snr: float = 0.0 # Signal-to-noise ratio in dB timestamp: float = 0.0 track_id: int = -1 classification: str = "unknown" def to_dict(self) -> dict: """Convert to dictionary for JSON serialization""" return asdict(self) @dataclass class GPSData: """GPS position and orientation data""" latitude: float longitude: float altitude: float pitch: float # Pitch angle in degrees heading: float = 0.0 # Heading in degrees (0 = North) timestamp: float = 0.0 def to_dict(self) -> dict: return asdict(self) @dataclass class RadarSettings: """Radar system configuration""" system_frequency: float = 10.5e9 # Hz (PLFM TX LO) chirp_duration_1: float = 30e-6 # Long chirp duration (s) chirp_duration_2: float = 0.5e-6 # Short chirp duration (s) chirps_per_position: int = 32 freq_min: float = 10e6 # Hz freq_max: float = 30e6 # Hz prf1: float = 1000 # PRF 1 (Hz) prf2: float = 2000 # PRF 2 (Hz) max_distance: float = 1536 # Max detection range (m) -- 64 bins x 24 m coverage_radius: float = 1536 # Map coverage radius (m) class TileServer(Enum): """Available map tile servers""" OPENSTREETMAP = "osm" GOOGLE_MAPS = "google" GOOGLE_SATELLITE = "google_sat" GOOGLE_HYBRID = "google_hybrid" ESRI_SATELLITE = "esri_sat" # ============================================================================= # JavaScript Bridge - Enables Python <-> JavaScript communication # ============================================================================= class MapBridge(QObject): """ Bridge object exposed to JavaScript for bidirectional communication. This allows Python to call JavaScript functions and vice versa. """ # Signals emitted when JS calls Python mapClicked = pyqtSignal(float, float) # lat, lon markerClicked = pyqtSignal(int) # target_id mapReady = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) self._map_ready = False @pyqtSlot(float, float) def onMapClick(self, lat: float, lon: float): """Called from JavaScript when map is clicked""" logger.debug(f"Map clicked at: {lat}, {lon}") self.mapClicked.emit(lat, lon) @pyqtSlot(int) def onMarkerClick(self, target_id: int): """Called from JavaScript when a target marker is clicked""" logger.debug(f"Marker clicked: Target #{target_id}") self.markerClicked.emit(target_id) @pyqtSlot() def onMapReady(self): """Called from JavaScript when map is fully initialized""" logger.info("Map is ready") self._map_ready = True self.mapReady.emit() @pyqtSlot(str) def logFromJS(self, message: str): """Receive log messages from JavaScript""" logger.debug(f"[JS] {message}") @property def is_ready(self) -> bool: return self._map_ready # ============================================================================= # Map Widget - Embedded Leaflet Map # ============================================================================= class RadarMapWidget(QWidget): """ Custom widget embedding a Leaflet.js map via QWebEngineView. Provides methods for updating radar position, targets, and coverage. """ targetSelected = pyqtSignal(int) # Emitted when a target is selected def __init__(self, parent=None): super().__init__(parent) # State self._radar_position = GPSData( latitude=40.7128, # Default: New York City longitude=-74.0060, altitude=100.0, pitch=0.0 ) self._targets: list[RadarTarget] = [] self._coverage_radius = 1536 # meters (64 bins x 24 m, 3 km mode) self._tile_server = TileServer.OPENSTREETMAP self._show_coverage = True self._show_trails = False self._target_history: dict[int, list[tuple[float, float]]] = {} # Setup UI self._setup_ui() # Setup bridge self._bridge = MapBridge(self) self._bridge.mapReady.connect(self._on_map_ready) self._bridge.markerClicked.connect(self._on_marker_clicked) # Setup web channel self._channel = QWebChannel() self._channel.registerObject("bridge", self._bridge) self._web_view.page().setWebChannel(self._channel) # Load map self._load_map() def _setup_ui(self): """Setup the widget UI""" layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) # Control bar control_bar = QFrame() control_bar.setStyleSheet(f"background-color: {DARK_ACCENT}; border-radius: 4px;") control_layout = QHBoxLayout(control_bar) control_layout.setContentsMargins(8, 4, 8, 4) # Tile server selector self._tile_combo = QComboBox() self._tile_combo.addItem("OpenStreetMap", TileServer.OPENSTREETMAP) self._tile_combo.addItem("Google Maps", TileServer.GOOGLE_MAPS) self._tile_combo.addItem("Google Satellite", TileServer.GOOGLE_SATELLITE) self._tile_combo.addItem("Google Hybrid", TileServer.GOOGLE_HYBRID) self._tile_combo.addItem("ESRI Satellite", TileServer.ESRI_SATELLITE) self._tile_combo.currentIndexChanged.connect(self._on_tile_server_changed) self._tile_combo.setStyleSheet(f""" QComboBox {{ background-color: {DARK_BUTTON}; color: {DARK_FG}; border: 1px solid {DARK_BORDER}; padding: 4px 8px; border-radius: 4px; }} """) control_layout.addWidget(QLabel("Tiles:")) control_layout.addWidget(self._tile_combo) # Coverage toggle self._coverage_check = QCheckBox("Show Coverage") self._coverage_check.setChecked(True) self._coverage_check.stateChanged.connect(self._on_coverage_toggled) control_layout.addWidget(self._coverage_check) # Trails toggle self._trails_check = QCheckBox("Show Trails") self._trails_check.setChecked(False) self._trails_check.stateChanged.connect(self._on_trails_toggled) control_layout.addWidget(self._trails_check) # Center on radar button center_btn = QPushButton("Center on Radar") center_btn.clicked.connect(self._center_on_radar) center_btn.setStyleSheet(f""" QPushButton {{ background-color: {DARK_BUTTON}; color: {DARK_FG}; border: 1px solid {DARK_BORDER}; padding: 4px 12px; border-radius: 4px; }} QPushButton:hover {{ background-color: {DARK_BUTTON_HOVER}; }} """) control_layout.addWidget(center_btn) # Fit all button fit_btn = QPushButton("Fit All Targets") fit_btn.clicked.connect(self._fit_all_targets) fit_btn.setStyleSheet(center_btn.styleSheet()) control_layout.addWidget(fit_btn) control_layout.addStretch() # Status label self._status_label = QLabel("Initializing map...") self._status_label.setStyleSheet(f"color: {DARK_INFO};") control_layout.addWidget(self._status_label) layout.addWidget(control_bar) # Web view for map self._web_view = QWebEngineView() self._web_view.setMinimumSize(400, 300) layout.addWidget(self._web_view, stretch=1) def _get_map_html(self) -> str: """Generate the complete HTML for the Leaflet map""" return f''' Radar Map
''' def _load_map(self): """Load the map HTML into the web view""" html = self._get_map_html() self._web_view.setHtml(html) logger.info("Map HTML loaded") def _on_map_ready(self): """Called when the map is fully initialized""" self._status_label.setText(f"Map ready - {len(self._targets)} targets") self._status_label.setStyleSheet(f"color: {DARK_SUCCESS};") logger.info("Map widget ready") def _on_marker_clicked(self, target_id: int): """Handle marker click events""" self.targetSelected.emit(target_id) def _on_tile_server_changed(self, _index: int): """Handle tile server change""" server = self._tile_combo.currentData() self._tile_server = server self._run_js(f"setTileServer('{server.value}')") def _on_coverage_toggled(self, state: int): """Handle coverage visibility toggle""" visible = state == Qt.CheckState.Checked.value self._show_coverage = visible self._run_js(f"setCoverageVisible({str(visible).lower()})") def _on_trails_toggled(self, state: int): """Handle trails visibility toggle""" visible = state == Qt.CheckState.Checked.value self._show_trails = visible self._run_js(f"setTrailsVisible({str(visible).lower()})") def _center_on_radar(self): """Center map view on radar position""" self._run_js("centerOnRadar()") def _fit_all_targets(self): """Fit map view to show all targets""" self._run_js("fitAllTargets()") def _run_js(self, script: str): """Execute JavaScript in the web view""" self._web_view.page().runJavaScript(script) # Public API def set_radar_position(self, gps_data: GPSData): """Update the radar position on the map""" self._radar_position = gps_data self._run_js( f"updateRadarPosition({gps_data.latitude}, {gps_data.longitude}, " f"{gps_data.altitude}, {gps_data.pitch}, {gps_data.heading})" ) def set_targets(self, targets: list[RadarTarget]): """Update all targets on the map""" self._targets = targets # Convert targets to JSON targets_data = [t.to_dict() for t in targets] targets_json = json.dumps(targets_data) # Update status self._status_label.setText(f"{len(targets)} targets tracked") # Call JavaScript update function self._run_js(f"updateTargets('{targets_json}')") def set_coverage_radius(self, radius: float): """Set the coverage circle radius in meters""" self._coverage_radius = radius self._run_js(f"setCoverageRadius({radius})") def set_zoom(self, level: int): """Set map zoom level (0-19)""" level = max(0, min(19, level)) self._run_js(f"setZoom({level})") # ============================================================================= # Utility Functions # ============================================================================= def polar_to_geographic( radar_lat: float, radar_lon: float, range_m: float, azimuth_deg: float ) -> tuple[float, float]: """ Convert polar coordinates (range, azimuth) relative to radar to geographic coordinates (latitude, longitude). Args: radar_lat: Radar latitude in degrees radar_lon: Radar longitude in degrees range_m: Range from radar in meters azimuth_deg: Azimuth angle in degrees (0 = North, clockwise) Returns: Tuple of (latitude, longitude) for the target """ # Earth's radius in meters R = 6371000 # Convert to radians lat1 = math.radians(radar_lat) lon1 = math.radians(radar_lon) bearing = math.radians(azimuth_deg) # Calculate new position lat2 = math.asin( math.sin(lat1) * math.cos(range_m / R) + math.cos(lat1) * math.sin(range_m / R) * math.cos(bearing) ) lon2 = lon1 + math.atan2( math.sin(bearing) * math.sin(range_m / R) * math.cos(lat1), math.cos(range_m / R) - math.sin(lat1) * math.sin(lat2) ) return (math.degrees(lat2), math.degrees(lon2)) # ============================================================================= # Target Simulator (Demo Mode) # ============================================================================= class TargetSimulator(QObject): """Simulates radar targets for demonstration purposes""" targetsUpdated = pyqtSignal(list) # Emits list of RadarTarget def __init__(self, radar_position: GPSData, parent=None): super().__init__(parent) self._radar_position = radar_position self._targets: list[RadarTarget] = [] self._next_id = 1 self._timer = QTimer() self._timer.timeout.connect(self._update_targets) # Initialize some targets self._initialize_targets() def _initialize_targets(self, count: int = 8): """Create initial set of simulated targets""" for _ in range(count): self._add_random_target() def _add_random_target(self): """Add a new random target""" # Random range between 5km and 40km range_m = random.uniform(5000, 40000) # Random azimuth azimuth = random.uniform(0, 360) # Random velocity (-100 to +100 m/s) velocity = random.uniform(-100, 100) # Random elevation elevation = random.uniform(-5, 45) # Calculate geographic position lat, lon = polar_to_geographic( self._radar_position.latitude, self._radar_position.longitude, range_m, azimuth ) target = RadarTarget( id=self._next_id, range=range_m, velocity=velocity, azimuth=azimuth, elevation=elevation, latitude=lat, longitude=lon, snr=random.uniform(10, 35), timestamp=time.time(), track_id=self._next_id, classification=random.choice(["aircraft", "drone", "bird", "unknown"]) ) self._next_id += 1 self._targets.append(target) def _update_targets(self): """Update target positions (called by timer)""" updated_targets = [] for target in self._targets: # Update range based on velocity new_range = target.range - target.velocity * 0.5 # 0.5 second update # Check if target is still in range if new_range < 50 or new_range > 1536: # Remove this target and add a new one continue # Slightly vary velocity new_velocity = target.velocity + random.uniform(-2, 2) new_velocity = max(-150, min(150, new_velocity)) # Slightly vary azimuth (simulate turning) new_azimuth = (target.azimuth + random.uniform(-0.5, 0.5)) % 360 # Calculate new geographic position lat, lon = polar_to_geographic( self._radar_position.latitude, self._radar_position.longitude, new_range, new_azimuth ) updated_target = RadarTarget( id=target.id, range=new_range, velocity=new_velocity, azimuth=new_azimuth, elevation=target.elevation + random.uniform(-0.1, 0.1), latitude=lat, longitude=lon, snr=target.snr + random.uniform(-1, 1), timestamp=time.time(), track_id=target.track_id, classification=target.classification ) updated_targets.append(updated_target) # Occasionally add new targets if len(updated_targets) < 5 or (random.random() < 0.05 and len(updated_targets) < 15): self._add_random_target() updated_targets.append(self._targets[-1]) self._targets = updated_targets self.targetsUpdated.emit(updated_targets) def start(self, interval_ms: int = 500): """Start the simulation""" self._timer.start(interval_ms) def stop(self): """Stop the simulation""" self._timer.stop() def set_radar_position(self, gps_data: GPSData): """Update radar position""" self._radar_position = gps_data # ============================================================================= # Main Dashboard Window # ============================================================================= class RadarDashboard(QMainWindow): """Main application window for the radar dashboard""" def __init__(self): super().__init__() # State self._radar_position = GPSData( latitude=40.7128, longitude=-74.0060, altitude=100.0, pitch=0.0, heading=0.0, timestamp=time.time() ) self._settings = RadarSettings() self._simulator: TargetSimulator | None = None self._demo_mode = True # Setup UI self._setup_window() self._setup_dark_theme() self._setup_ui() self._setup_statusbar() # Start demo mode self._start_demo_mode() def _setup_window(self): """Configure main window properties""" self.setWindowTitle("PLFM Radar Dashboard - PyQt6 Edition") self.setMinimumSize(1200, 800) self.resize(1400, 900) def _setup_dark_theme(self): """Apply dark theme to the application""" palette = QPalette() palette.setColor(QPalette.ColorRole.Window, QColor(DARK_BG)) palette.setColor(QPalette.ColorRole.WindowText, QColor(DARK_FG)) palette.setColor(QPalette.ColorRole.Base, QColor(DARK_ACCENT)) palette.setColor(QPalette.ColorRole.AlternateBase, QColor(DARK_HIGHLIGHT)) palette.setColor(QPalette.ColorRole.ToolTipBase, QColor(DARK_ACCENT)) palette.setColor(QPalette.ColorRole.ToolTipText, QColor(DARK_FG)) palette.setColor(QPalette.ColorRole.Text, QColor(DARK_FG)) palette.setColor(QPalette.ColorRole.Button, QColor(DARK_BUTTON)) palette.setColor(QPalette.ColorRole.ButtonText, QColor(DARK_FG)) palette.setColor(QPalette.ColorRole.BrightText, QColor(DARK_FG)) palette.setColor(QPalette.ColorRole.Highlight, QColor(DARK_INFO)) palette.setColor(QPalette.ColorRole.HighlightedText, QColor(DARK_FG)) self.setPalette(palette) # Global stylesheet self.setStyleSheet(f""" QMainWindow {{ background-color: {DARK_BG}; }} QTabWidget::pane {{ border: 1px solid {DARK_BORDER}; background-color: {DARK_BG}; }} QTabBar::tab {{ background-color: {DARK_ACCENT}; color: {DARK_FG}; padding: 8px 20px; margin-right: 2px; border-top-left-radius: 4px; border-top-right-radius: 4px; }} QTabBar::tab:selected {{ background-color: {DARK_HIGHLIGHT}; border-bottom: 2px solid {DARK_INFO}; }} QTabBar::tab:hover {{ background-color: {DARK_BUTTON_HOVER}; }} QGroupBox {{ font-weight: bold; border: 1px solid {DARK_BORDER}; border-radius: 6px; margin-top: 12px; padding-top: 10px; background-color: {DARK_ACCENT}; }} QGroupBox::title {{ subcontrol-origin: margin; left: 10px; padding: 0 8px; color: {DARK_INFO}; }} QPushButton {{ background-color: {DARK_BUTTON}; color: {DARK_FG}; border: 1px solid {DARK_BORDER}; padding: 6px 16px; border-radius: 4px; font-weight: 500; }} QPushButton:hover {{ background-color: {DARK_BUTTON_HOVER}; }} QPushButton:pressed {{ background-color: {DARK_HIGHLIGHT}; }} QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox {{ background-color: {DARK_ACCENT}; color: {DARK_FG}; border: 1px solid {DARK_BORDER}; padding: 4px 8px; border-radius: 4px; }} QLabel {{ color: {DARK_FG}; }} QTableWidget {{ background-color: {DARK_ACCENT}; color: {DARK_FG}; gridline-color: {DARK_BORDER}; border: 1px solid {DARK_BORDER}; }} QTableWidget::item {{ padding: 4px; }} QTableWidget::item:selected {{ background-color: {DARK_INFO}; }} QHeaderView::section {{ background-color: {DARK_HIGHLIGHT}; color: {DARK_FG}; padding: 6px; border: none; border-right: 1px solid {DARK_BORDER}; }} QScrollBar:vertical {{ background-color: {DARK_ACCENT}; width: 12px; margin: 0; }} QScrollBar::handle:vertical {{ background-color: {DARK_HIGHLIGHT}; border-radius: 6px; min-height: 20px; }} QStatusBar {{ background-color: {DARK_ACCENT}; color: {DARK_FG}; }} """) def _setup_ui(self): """Setup the main UI layout""" # Central widget central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) main_layout.setContentsMargins(8, 8, 8, 8) main_layout.setSpacing(8) # Tab widget self._tabs = QTabWidget() main_layout.addWidget(self._tabs) # Create tabs self._create_map_tab() self._create_targets_tab() self._create_settings_tab() def _create_map_tab(self): """Create the map visualization tab""" tab = QWidget() layout = QHBoxLayout(tab) layout.setContentsMargins(4, 4, 4, 4) # Splitter for map and sidebar splitter = QSplitter(Qt.Orientation.Horizontal) # Map widget (main area) self._map_widget = RadarMapWidget() self._map_widget.targetSelected.connect(self._on_target_selected) splitter.addWidget(self._map_widget) # Sidebar sidebar = QWidget() sidebar.setMaximumWidth(320) sidebar.setMinimumWidth(280) sidebar_layout = QVBoxLayout(sidebar) sidebar_layout.setContentsMargins(8, 8, 8, 8) # Radar Position Group pos_group = QGroupBox("Radar Position") pos_layout = QGridLayout(pos_group) self._lat_spin = QDoubleSpinBox() self._lat_spin.setRange(-90, 90) self._lat_spin.setDecimals(6) self._lat_spin.setValue(self._radar_position.latitude) self._lat_spin.valueChanged.connect(self._on_position_changed) self._lon_spin = QDoubleSpinBox() self._lon_spin.setRange(-180, 180) self._lon_spin.setDecimals(6) self._lon_spin.setValue(self._radar_position.longitude) self._lon_spin.valueChanged.connect(self._on_position_changed) self._alt_spin = QDoubleSpinBox() self._alt_spin.setRange(0, 50000) self._alt_spin.setDecimals(1) self._alt_spin.setValue(self._radar_position.altitude) self._alt_spin.setSuffix(" m") pos_layout.addWidget(QLabel("Latitude:"), 0, 0) pos_layout.addWidget(self._lat_spin, 0, 1) pos_layout.addWidget(QLabel("Longitude:"), 1, 0) pos_layout.addWidget(self._lon_spin, 1, 1) pos_layout.addWidget(QLabel("Altitude:"), 2, 0) pos_layout.addWidget(self._alt_spin, 2, 1) sidebar_layout.addWidget(pos_group) # Coverage Group coverage_group = QGroupBox("Coverage") coverage_layout = QGridLayout(coverage_group) self._coverage_spin = QDoubleSpinBox() self._coverage_spin.setRange(1, 100) self._coverage_spin.setDecimals(1) self._coverage_spin.setValue(self._settings.coverage_radius / 1000) self._coverage_spin.setSuffix(" km") self._coverage_spin.valueChanged.connect(self._on_coverage_changed) coverage_layout.addWidget(QLabel("Radius:"), 0, 0) coverage_layout.addWidget(self._coverage_spin, 0, 1) sidebar_layout.addWidget(coverage_group) # Demo Controls demo_group = QGroupBox("Demo Mode") demo_layout = QVBoxLayout(demo_group) self._demo_btn = QPushButton("Stop Demo") self._demo_btn.setCheckable(True) self._demo_btn.setChecked(True) self._demo_btn.clicked.connect(self._toggle_demo_mode) demo_layout.addWidget(self._demo_btn) add_target_btn = QPushButton("Add Random Target") add_target_btn.clicked.connect(self._add_demo_target) demo_layout.addWidget(add_target_btn) sidebar_layout.addWidget(demo_group) # Target Info info_group = QGroupBox("Selected Target") info_layout = QVBoxLayout(info_group) self._target_info_label = QLabel("No target selected") self._target_info_label.setWordWrap(True) self._target_info_label.setStyleSheet(f"color: {DARK_TEXT}; padding: 8px;") info_layout.addWidget(self._target_info_label) sidebar_layout.addWidget(info_group) sidebar_layout.addStretch() splitter.addWidget(sidebar) splitter.setSizes([900, 300]) layout.addWidget(splitter) self._tabs.addTab(tab, "Map View") def _create_targets_tab(self): """Create the targets table tab""" tab = QWidget() layout = QVBoxLayout(tab) layout.setContentsMargins(8, 8, 8, 8) # Targets table self._targets_table = QTableWidget() self._targets_table.setColumnCount(9) self._targets_table.setHorizontalHeaderLabels([ "ID", "Track", "Range (m)", "Velocity (m/s)", "Azimuth (°)", "Elevation (°)", "SNR (dB)", "Classification", "Status" ]) header = self._targets_table.horizontalHeader() header.setSectionResizeMode(QHeaderView.ResizeMode.Stretch) self._targets_table.setSelectionBehavior( QTableWidget.SelectionBehavior.SelectRows ) self._targets_table.setAlternatingRowColors(True) layout.addWidget(self._targets_table) self._tabs.addTab(tab, "Targets") def _create_settings_tab(self): """Create the settings tab""" tab = QWidget() layout = QVBoxLayout(tab) layout.setContentsMargins(8, 8, 8, 8) # Radar Settings Group radar_group = QGroupBox("Radar Parameters") radar_layout = QGridLayout(radar_group) radar_layout.addWidget(QLabel("System Frequency:"), 0, 0) freq_spin = QDoubleSpinBox() freq_spin.setRange(1, 100) freq_spin.setValue(self._settings.system_frequency / 1e9) freq_spin.setSuffix(" GHz") radar_layout.addWidget(freq_spin, 0, 1) radar_layout.addWidget(QLabel("Max Range:"), 1, 0) range_spin = QDoubleSpinBox() range_spin.setRange(1, 200) range_spin.setValue(self._settings.max_distance / 1000) range_spin.setSuffix(" km") radar_layout.addWidget(range_spin, 1, 1) radar_layout.addWidget(QLabel("PRF 1:"), 2, 0) prf1_spin = QSpinBox() prf1_spin.setRange(100, 10000) prf1_spin.setValue(int(self._settings.prf1)) prf1_spin.setSuffix(" Hz") radar_layout.addWidget(prf1_spin, 2, 1) radar_layout.addWidget(QLabel("PRF 2:"), 3, 0) prf2_spin = QSpinBox() prf2_spin.setRange(100, 10000) prf2_spin.setValue(int(self._settings.prf2)) prf2_spin.setSuffix(" Hz") radar_layout.addWidget(prf2_spin, 3, 1) layout.addWidget(radar_group) # About about_group = QGroupBox("About") about_layout = QVBoxLayout(about_group) about_text = QLabel( "PLFM Radar Dashboard
" "PyQt6 Edition with Embedded Leaflet Map

" "Version: 1.0.0
" "Map: OpenStreetMap + Leaflet.js
" "Framework: PyQt6 + QWebEngine" ) about_text.setStyleSheet(f"color: {DARK_TEXT}; padding: 12px;") about_layout.addWidget(about_text) layout.addWidget(about_group) layout.addStretch() self._tabs.addTab(tab, "Settings") def _setup_statusbar(self): """Setup the status bar""" self._statusbar = QStatusBar() self.setStatusBar(self._statusbar) self._status_label = QLabel("Ready") self._statusbar.addWidget(self._status_label) self._target_count_label = QLabel("Targets: 0") self._statusbar.addPermanentWidget(self._target_count_label) self._mode_label = QLabel("Demo Mode") self._mode_label.setStyleSheet(f"color: {DARK_INFO}; font-weight: bold;") self._statusbar.addPermanentWidget(self._mode_label) def _start_demo_mode(self): """Start the demo mode with simulated targets""" self._simulator = TargetSimulator(self._radar_position, self) self._simulator.targetsUpdated.connect(self._on_targets_updated) self._simulator.start(500) # Update every 500ms self._demo_mode = True self._demo_btn.setChecked(True) self._demo_btn.setText("Stop Demo") self._mode_label.setText("Demo Mode") self._status_label.setText("Demo mode active") logger.info("Demo mode started") def _toggle_demo_mode(self, checked: bool): """Toggle demo mode on/off""" if checked: self._start_demo_mode() else: if self._simulator: self._simulator.stop() self._demo_mode = False self._demo_btn.setText("Start Demo") self._mode_label.setText("Idle") self._status_label.setText("Demo mode stopped") logger.info("Demo mode stopped") def _add_demo_target(self): """Add a random target in demo mode""" if self._simulator: self._simulator._add_random_target() logger.info("Added random target") def _on_targets_updated(self, targets: list[RadarTarget]): """Handle updated target list from simulator""" # Update map self._map_widget.set_targets(targets) # Update status bar self._target_count_label.setText(f"Targets: {len(targets)}") # Update table self._update_targets_table(targets) def _update_targets_table(self, targets: list[RadarTarget]): """Update the targets table""" self._targets_table.setRowCount(len(targets)) for row, target in enumerate(targets): # ID self._targets_table.setItem(row, 0, QTableWidgetItem(str(target.id))) # Track ID self._targets_table.setItem(row, 1, QTableWidgetItem(str(target.track_id))) # Range self._targets_table.setItem(row, 2, QTableWidgetItem(f"{target.range:.1f}")) # Velocity vel_item = QTableWidgetItem(f"{target.velocity:+.1f}") if target.velocity > 1: vel_item.setForeground(QColor(DARK_ERROR)) elif target.velocity < -1: vel_item.setForeground(QColor(DARK_INFO)) self._targets_table.setItem(row, 3, vel_item) # Azimuth self._targets_table.setItem(row, 4, QTableWidgetItem(f"{target.azimuth:.1f}")) # Elevation self._targets_table.setItem(row, 5, QTableWidgetItem(f"{target.elevation:.1f}")) # SNR self._targets_table.setItem(row, 6, QTableWidgetItem(f"{target.snr:.1f}")) # Classification self._targets_table.setItem(row, 7, QTableWidgetItem(target.classification)) # Status status = "Approaching" if target.velocity > 1 else ( "Receding" if target.velocity < -1 else "Stationary" ) status_item = QTableWidgetItem(status) if status == "Approaching": status_item.setForeground(QColor(DARK_ERROR)) elif status == "Receding": status_item.setForeground(QColor(DARK_INFO)) self._targets_table.setItem(row, 8, status_item) def _on_target_selected(self, target_id: int): """Handle target selection from map""" # Find target if self._simulator: for target in self._simulator._targets: if target.id == target_id: self._show_target_info(target) break def _show_target_info(self, target: RadarTarget): """Display target information in sidebar""" status = "Approaching" if target.velocity > 1 else ( "Receding" if target.velocity < -1 else "Stationary" ) info = f""" Target #{target.id}

Track ID: {target.track_id}
Range: {target.range:.1f} m
Velocity: {target.velocity:+.1f} m/s
Azimuth: {target.azimuth:.1f}°
Elevation: {target.elevation:.1f}°
SNR: {target.snr:.1f} dB
Classification: {target.classification}
Status: {status} """ self._target_info_label.setText(info) def _on_position_changed(self): """Handle radar position change from UI""" self._radar_position.latitude = self._lat_spin.value() self._radar_position.longitude = self._lon_spin.value() self._radar_position.altitude = self._alt_spin.value() self._map_widget.set_radar_position(self._radar_position) if self._simulator: self._simulator.set_radar_position(self._radar_position) def _on_coverage_changed(self, value: float): """Handle coverage radius change""" radius_m = value * 1000 self._settings.coverage_radius = radius_m self._map_widget.set_coverage_radius(radius_m) def closeEvent(self, event): """Handle window close""" if self._simulator: self._simulator.stop() event.accept() # ============================================================================= # Main Entry Point # ============================================================================= def main(): """Application entry point""" # Create application app = QApplication(sys.argv) app.setApplicationName("PLFM Radar Dashboard") app.setApplicationVersion("1.0.0") # Set font font = QFont("Segoe UI", 10) app.setFont(font) # Create and show main window window = RadarDashboard() window.show() logger.info("Application started") # Run event loop sys.exit(app.exec()) if __name__ == "__main__": main()