gui: add checkable constellation legend to filter sky plot by GNSS system

Replaces the static painted legend with real checkboxes (one per
GNSS system, color-swatched to match GNSS_COLORS) so users can
toggle which constellations show in the sky plot and satellite
table, mirroring the existing receiver-source filter checkboxes.
This commit is contained in:
2026-07-17 15:52:30 +02:00
parent eb18b1485f
commit f7af4e5b31
2 changed files with 35 additions and 24 deletions
+34 -4
View File
@@ -13,7 +13,7 @@ from PyQt5.QtWidgets import (
QSplitter, QGroupBox, QFrame, QCheckBox, QSpinBox, QSplitter, QGroupBox, QFrame, QCheckBox, QSpinBox,
) )
from PyQt5.QtCore import Qt, QTimer from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QColor, QBrush from PyQt5.QtGui import QColor, QBrush, QIcon, QPixmap
from data_model import ReceiverManager, GNSS_NAMES, GNSS_COLORS from data_model import ReceiverManager, GNSS_NAMES, GNSS_COLORS
from sky_widget import SkyPlotWidget from sky_widget import SkyPlotWidget
@@ -37,6 +37,8 @@ class SkyTab(QWidget):
self._name_labels: dict = {} # rid -> QLabel (receiver display name) self._name_labels: dict = {} # rid -> QLabel (receiver display name)
self._trail: dict = {} # (rid, gnss_id, sv_id) -> deque[(elev, azim)] self._trail: dict = {} # (rid, gnss_id, sv_id) -> deque[(elev, azim)]
self._hi_timers: dict = {} # (rid, gnss_id, sv_id) -> QTimer self._hi_timers: dict = {} # (rid, gnss_id, sv_id) -> QTimer
self._gnss_checks: dict = {} # gnss_id -> QCheckBox
self._enabled_gnss: set = set(GNSS_NAMES.keys())
outer = QVBoxLayout(self) outer = QVBoxLayout(self)
outer.setContentsMargins(4, 4, 4, 4) outer.setContentsMargins(4, 4, 4, 4)
@@ -68,7 +70,28 @@ class SkyTab(QWidget):
self._trail_spin.setEnabled(False) self._trail_spin.setEnabled(False)
self._trail_spin.valueChanged.connect(self._on_trail_toggled) self._trail_spin.valueChanged.connect(self._on_trail_toggled)
src_bl.addWidget(self._trail_spin) src_bl.addWidget(self._trail_spin)
outer.addWidget(src_box)
# ── Constellation legend / filter ─────────────────────────────────────
const_box = QGroupBox("Constellations (check to show)")
const_box.setMaximumHeight(80)
const_bl = QHBoxLayout(const_box)
const_bl.setContentsMargins(4, 2, 4, 2)
const_bl.setSpacing(8)
for gid in sorted(GNSS_NAMES):
cb = QCheckBox(GNSS_NAMES[gid])
cb.setChecked(True)
pix = QPixmap(10, 10)
pix.fill(QColor(GNSS_COLORS.get(gid, '#9E9E9E')))
cb.setIcon(QIcon(pix))
cb.toggled.connect(lambda checked, g=gid: self._on_gnss_toggled(g, checked))
self._gnss_checks[gid] = cb
const_bl.addWidget(cb)
const_bl.addStretch()
filter_row = QHBoxLayout()
filter_row.addWidget(src_box)
filter_row.addWidget(const_box)
outer.addLayout(filter_row)
# ── Info bar (one card per receiver) ────────────────────────────────── # ── Info bar (one card per receiver) ──────────────────────────────────
info_frame = QFrame() info_frame = QFrame()
@@ -181,7 +204,7 @@ class SkyTab(QWidget):
def _visible_sats(self) -> list: def _visible_sats(self) -> list:
selected = self._selected_rids() selected = self._selected_rids()
return [(k[0], self._store[k]) return [(k[0], self._store[k])
for k in self._store if k[0] in selected] for k in self._store if k[0] in selected and k[1] in self._enabled_gnss]
def _active_trails(self) -> dict: def _active_trails(self) -> dict:
if not self._trail_cb.isChecked(): if not self._trail_cb.isChecked():
@@ -192,7 +215,14 @@ class SkyTab(QWidget):
selected = self._selected_rids() selected = self._selected_rids()
return {key: list(dq)[-n_points:] return {key: list(dq)[-n_points:]
for key, dq in self._trail.items() for key, dq in self._trail.items()
if key[0] in selected} if key[0] in selected and key[1] in self._enabled_gnss}
def _on_gnss_toggled(self, gid: int, checked: bool):
if checked:
self._enabled_gnss.add(gid)
else:
self._enabled_gnss.discard(gid)
self._refresh_immediate()
# ── Trail toggle ────────────────────────────────────────────────────────── # ── Trail toggle ──────────────────────────────────────────────────────────
+1 -20
View File
@@ -11,7 +11,7 @@ from PyQt5.QtWidgets import QWidget, QSizePolicy, QApplication
from PyQt5.QtGui import (QPainter, QColor, QPen, QBrush, QFont, QImage) from PyQt5.QtGui import (QPainter, QColor, QPen, QBrush, QFont, QImage)
from PyQt5.QtCore import Qt, QPointF, QRectF, QThread, pyqtSignal from PyQt5.QtCore import Qt, QPointF, QRectF, QThread, pyqtSignal
from data_model import GNSS_NAMES, GNSS_COLORS, GNSS_SHORT from data_model import GNSS_COLORS, GNSS_SHORT
# ── Static colors (created once in main thread, read-only in render thread) ── # ── Static colors (created once in main thread, read-only in render thread) ──
_C_BG = QColor('#1a1a2e') _C_BG = QColor('#1a1a2e')
@@ -92,7 +92,6 @@ class SkyRenderThread(QThread):
if trails: if trails:
self._draw_trails(p, cx, cy, max_r, trails) self._draw_trails(p, cx, cy, max_r, trails)
self._draw_satellites(p, cx, cy, max_r, sats) self._draw_satellites(p, cx, cy, max_r, sats)
self._draw_legend(p, sats)
p.end() p.end()
return img return img
@@ -184,24 +183,6 @@ class SkyRenderThread(QThread):
p.drawText(QRectF(x + size + 1, y - 6, 28, 12), p.drawText(QRectF(x + size + 1, y - 6, 28, 12),
Qt.AlignLeft, f"{short}{sat.sv_id}") Qt.AlignLeft, f"{short}{sat.sv_id}")
def _draw_legend(self, p: QPainter, sats):
used = {sat.gnss_id for _, sat in sats}
if not used:
return
f = QFont(); f.setPointSize(8)
p.setFont(f)
lx, ly = 8, 8
for gid in sorted(used):
color = _GNSS_QCOLORS.get(gid)
if color is None:
continue
p.setBrush(QBrush(color))
p.setPen(Qt.NoPen)
p.drawEllipse(QPointF(lx + 5, ly + 6), 5, 5)
p.setPen(QPen(_C_SAT_LBL))
p.drawText(lx + 14, ly + 11, GNSS_NAMES.get(gid, f'SYS{gid}'))
ly += 18
# ── Widget ──────────────────────────────────────────────────────────────────── # ── Widget ────────────────────────────────────────────────────────────────────