sky plot: add per-satellite trajectory trails

- Store up to 1 hour of (elevation, azimuth) history per satellite/source
- Trail column in satellite table (checkbox, default off); clicking the
  column header toggles all trails on/off
- Trail rendered in GNSS color with alpha fading from 15 (oldest) to 200
  (newest), subsampled to max 120 points for render performance
- Trail column fixed at 42 px to avoid checkbox + empty-text visual split

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 13:23:14 +02:00
co-authored by Claude Sonnet 4.6
parent bfe70aff22
commit 1522a2eb6a
2 changed files with 126 additions and 32 deletions
+36 -11
View File
@@ -45,13 +45,13 @@ class SkyRenderThread(QThread):
self._queue: Queue = Queue(maxsize=1)
self._running = True
def submit(self, sats: list, width: int, height: int):
def submit(self, sats: list, trails: dict, width: int, height: int):
# Discard any pending (not yet rendered) frame so we always render latest
try:
self._queue.get_nowait()
except Empty:
pass
self._queue.put((list(sats), width, height))
self._queue.put((list(sats), trails, width, height))
def stop(self):
self._running = False
@@ -71,14 +71,14 @@ class SkyRenderThread(QThread):
continue
if item is None:
break
sats, w, h = item
sats, trails, w, h = item
if w > 0 and h > 0:
img = self._render(sats, w, h)
img = self._render(sats, trails, w, h)
self.frame_ready.emit(img)
# ── Drawing (runs in render thread — QPainter on QImage is thread-safe) ──
def _render(self, sats: list, w: int, h: int) -> QImage:
def _render(self, sats: list, trails: dict, w: int, h: int) -> QImage:
img = QImage(w, h, QImage.Format_RGB32)
p = QPainter(img)
@@ -89,6 +89,8 @@ class SkyRenderThread(QThread):
self._draw_bg(p, w, h)
self._draw_grid(p, cx, cy, max_r)
if trails:
self._draw_trails(p, cx, cy, max_r, trails)
self._draw_satellites(p, cx, cy, max_r, sats)
self._draw_legend(p, sats)
@@ -138,6 +140,27 @@ class SkyRenderThread(QThread):
p.setPen(Qt.NoPen)
p.drawEllipse(QPointF(cx, cy), 3, 3)
def _draw_trails(self, p: QPainter, cx, cy, max_r, trails: dict):
for (_, gnss_id, _), points in trails.items():
n = len(points)
if n < 2:
continue
base_color = _GNSS_QCOLORS.get(gnss_id, _C_LABEL)
r, g, b = base_color.red(), base_color.green(), base_color.blue()
# Subsample to at most 120 points so rendering stays fast
step = max(1, n // 120)
pts = points[::step]
m = len(pts)
for i in range(1, m):
# Alpha fades from 15 (oldest) to 200 (newest)
alpha = int(15 + 185 * i / m)
p.setPen(QPen(QColor(r, g, b, alpha), 1.5))
x1, y1 = _polar_xy(cx, cy, max_r, pts[i - 1][0], pts[i - 1][1])
x2, y2 = _polar_xy(cx, cy, max_r, pts[i][0], pts[i][1])
p.drawLine(QPointF(x1, y1), QPointF(x2, y2))
def _draw_satellites(self, p: QPainter, cx, cy, max_r, sats):
f = QFont(); f.setPointSize(7)
p.setFont(f)
@@ -188,8 +211,9 @@ class SkyPlotWidget(QWidget):
def __init__(self):
super().__init__()
self._sats: list = []
self._image: QImage = None
self._sats: list = []
self._trails: dict = {}
self._image: QImage = None
self.setMinimumSize(400, 400)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
@@ -206,16 +230,17 @@ class SkyPlotWidget(QWidget):
# ── Public API ────────────────────────────────────────────────────────────
def update_data(self, sats: list):
self._sats = sats
self._thread.submit(sats, self.width(), self.height())
def update_data(self, sats: list, trails: dict = None):
self._sats = sats
self._trails = trails or {}
self._thread.submit(sats, self._trails, self.width(), self.height())
# ── Qt events ─────────────────────────────────────────────────────────────
def resizeEvent(self, event):
super().resizeEvent(event)
if self._sats:
self._thread.submit(self._sats, self.width(), self.height())
self._thread.submit(self._sats, self._trails, self.width(), self.height())
def paintEvent(self, _event):
p = QPainter(self)