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:
+36
-11
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user