feat: AR-GPS initial commit — Python + JavaScript PyQt5 (standalone desktop app) + FastAPI (charts REST router) + OpenLayers (frontend map)
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
'use strict';
|
||||
/* Sky plot (azimuth/elevation) + SNR signal bars.
|
||||
Mode-aware: lee data-mode del <html> y ajusta colores del canvas.
|
||||
HiDPI-aware: escala internamente por devicePixelRatio.
|
||||
Labels HTML: todo el texto se pone en divs overlay para máxima nitidez.
|
||||
Usage: SkyPlot.update(sats) donde sats = [{prn,system,el,az,snr,used}, ...] */
|
||||
|
||||
const SkyPlot = (function () {
|
||||
|
||||
/* Colores base por sistema */
|
||||
const SYS_COLOR = {
|
||||
GPS: '#00c8e8',
|
||||
GLONASS: '#f45050',
|
||||
Galileo: '#28d870',
|
||||
BeiDou: '#f89030',
|
||||
GNSS: '#a070f8',
|
||||
QZSS: '#f8c820',
|
||||
};
|
||||
|
||||
const THEMES = {
|
||||
day: {
|
||||
bg: '#0a1628',
|
||||
ring: '#1e3a58',
|
||||
ring2: '#0e2040',
|
||||
label: '#4878a8',
|
||||
cardinal: '#5888b8',
|
||||
snrBg: '#0a1628',
|
||||
snrLabel: '#486888',
|
||||
unusedAlpha: '55',
|
||||
satText: '#e0f0ff',
|
||||
satTextDim: '#3868a0',
|
||||
snrUsedText: '#c0deff',
|
||||
snrDimText: '#3868a0',
|
||||
},
|
||||
dusk: {
|
||||
bg: '#050912',
|
||||
ring: '#12202e',
|
||||
ring2: '#080e18',
|
||||
label: '#304060',
|
||||
cardinal: '#3a5070',
|
||||
snrBg: '#050912',
|
||||
snrLabel: '#283848',
|
||||
unusedAlpha: '44',
|
||||
satText: '#b0c8e0',
|
||||
satTextDim: '#283848',
|
||||
snrUsedText: '#98b8d0',
|
||||
snrDimText: '#283848',
|
||||
},
|
||||
night: {
|
||||
bg: '#0e0000',
|
||||
ring: '#280808',
|
||||
ring2: '#180404',
|
||||
label: '#582010',
|
||||
cardinal: '#703020',
|
||||
snrBg: '#0e0000',
|
||||
snrLabel: '#401808',
|
||||
unusedAlpha: '44',
|
||||
override: '#c83020',
|
||||
satText: '#ffb090',
|
||||
satTextDim: '#582010',
|
||||
snrUsedText: '#ff9878',
|
||||
snrDimText: '#401808',
|
||||
},
|
||||
dayplus: {
|
||||
bg: '#dce8f6',
|
||||
ring: '#7aa4c8',
|
||||
ring2: '#c4d8ec',
|
||||
label: '#3060a0',
|
||||
cardinal: '#003888',
|
||||
snrBg: '#dce8f6',
|
||||
snrLabel: '#2060a0',
|
||||
unusedAlpha: '70',
|
||||
satText: '#00204a',
|
||||
satTextDim: '#406090',
|
||||
snrUsedText: '#001030',
|
||||
snrDimText: '#406090',
|
||||
},
|
||||
};
|
||||
|
||||
function _theme() {
|
||||
const m = document.documentElement.getAttribute('data-mode') || 'day';
|
||||
return THEMES[m] || THEMES.day;
|
||||
}
|
||||
|
||||
/* ── HiDPI setup ──────────────────────────────────────────────────────────
|
||||
Guarda dimensiones lógicas en dataset (primera vez) para evitar el
|
||||
feedback loop canvas.width → offsetWidth → canvas.width... */
|
||||
function _setupCanvas(canvas) {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
if (!canvas.dataset.logicalW) {
|
||||
const lw = canvas.offsetWidth || parseInt(canvas.getAttribute('width'), 10) || 258;
|
||||
const lh = canvas.offsetHeight || parseInt(canvas.getAttribute('height'), 10) || 258;
|
||||
canvas.dataset.logicalW = lw;
|
||||
canvas.dataset.logicalH = lh;
|
||||
canvas.style.width = lw + 'px';
|
||||
canvas.style.height = lh + 'px';
|
||||
}
|
||||
|
||||
const W = parseInt(canvas.dataset.logicalW, 10);
|
||||
const H = parseInt(canvas.dataset.logicalH, 10);
|
||||
const wPx = Math.round(W * dpr);
|
||||
const hPx = Math.round(H * dpr);
|
||||
if (canvas.width !== wPx || canvas.height !== hPx) {
|
||||
canvas.width = wPx;
|
||||
canvas.height = hPx;
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
return { ctx, W, H };
|
||||
}
|
||||
|
||||
/* ── Overlay HTML labels ───────────────────────────────────────────────── */
|
||||
function _clearLabels(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.innerHTML = '';
|
||||
return el;
|
||||
}
|
||||
|
||||
/* Agrega un <span> al overlay en coordenadas lógicas del canvas.
|
||||
ox/oy = offset de alineación (ej. -0.5 para centrar horizontalmente).
|
||||
Las coords x,y son equivalentes a las de ctx.fillText en el canvas. */
|
||||
function _addLabel(overlay, text, x, y, color, opts) {
|
||||
if (!overlay) return;
|
||||
const s = document.createElement('span');
|
||||
s.textContent = text;
|
||||
s.style.left = Math.round(x) + 'px';
|
||||
s.style.top = Math.round(y - 9) + 'px'; /* 9px ≈ ascent de 9px font */
|
||||
s.style.color = color;
|
||||
if (opts && opts.bold) s.style.fontWeight = '700';
|
||||
if (opts && opts.size) s.style.fontSize = opts.size + 'px';
|
||||
if (opts && opts.centerX) s.style.transform = 'translateX(-50%)';
|
||||
overlay.appendChild(s);
|
||||
}
|
||||
|
||||
/* ── Sky plot ─────────────────────────────────────────────────────────── */
|
||||
function drawSky(sats) {
|
||||
const canvas = document.getElementById('sky-canvas');
|
||||
if (!canvas) return;
|
||||
const t = _theme();
|
||||
const { ctx, W, H } = _setupCanvas(canvas);
|
||||
const cx = W / 2, cy = H / 2;
|
||||
const R = Math.min(cx, cy) - 12;
|
||||
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
|
||||
/* Fondo circular */
|
||||
const bgGrad = ctx.createRadialGradient(cx, cy, R * 0.05, cx, cy, R + 10);
|
||||
bgGrad.addColorStop(0, t.ring2 || t.bg);
|
||||
bgGrad.addColorStop(1, t.bg);
|
||||
ctx.beginPath(); ctx.arc(cx, cy, R + 10, 0, Math.PI * 2);
|
||||
ctx.fillStyle = bgGrad; ctx.fill();
|
||||
|
||||
/* Anillos de elevación — solo formas, sin texto */
|
||||
[0, 30, 60].forEach(el => {
|
||||
const r = R * (1 - el / 90);
|
||||
ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = t.ring;
|
||||
ctx.lineWidth = el === 0 ? 1.2 : 0.7;
|
||||
ctx.setLineDash(el === 0 ? [] : [3, 4]);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
});
|
||||
|
||||
/* Líneas cardinales */
|
||||
ctx.strokeStyle = t.ring; ctx.lineWidth = 0.7;
|
||||
ctx.setLineDash([4, 5]);
|
||||
ctx.beginPath(); ctx.moveTo(cx, cy - R); ctx.lineTo(cx, cy + R); ctx.stroke();
|
||||
ctx.beginPath(); ctx.moveTo(cx - R, cy); ctx.lineTo(cx + R, cy); ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
|
||||
/* Satélites — solo puntos y halos */
|
||||
(sats || []).forEach(s => {
|
||||
if (s.el == null || s.az == null) return;
|
||||
const r = R * (1 - s.el / 90);
|
||||
const ang = (s.az - 90) * Math.PI / 180;
|
||||
const x = cx + r * Math.cos(ang);
|
||||
const y = cy + r * Math.sin(ang);
|
||||
|
||||
const baseCol = t.override || SYS_COLOR[s.system] || '#94a3b8';
|
||||
const dotCol = s.used ? baseCol : baseCol + t.unusedAlpha;
|
||||
const rad = s.used ? 8 : 5;
|
||||
|
||||
if (s.used) {
|
||||
ctx.beginPath(); ctx.arc(x, y, rad + 5, 0, Math.PI * 2);
|
||||
ctx.fillStyle = baseCol + '22'; ctx.fill();
|
||||
}
|
||||
|
||||
ctx.beginPath(); ctx.arc(x, y, rad, 0, Math.PI * 2);
|
||||
ctx.fillStyle = dotCol; ctx.fill();
|
||||
|
||||
if (s.used) {
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.80)';
|
||||
ctx.lineWidth = 1.5; ctx.stroke();
|
||||
} else {
|
||||
ctx.strokeStyle = baseCol + '55';
|
||||
ctx.lineWidth = 0.7; ctx.stroke();
|
||||
}
|
||||
});
|
||||
|
||||
/* ── Labels HTML (nítidos) ─────────────────────────────────────────── */
|
||||
const ov = _clearLabels('sky-labels');
|
||||
|
||||
/* Cardinales */
|
||||
_addLabel(ov, 'N', cx, cy - R - 3, t.cardinal, { bold: true, size: 11, centerX: true });
|
||||
_addLabel(ov, 'S', cx, cy + R + 13, t.cardinal, { bold: true, size: 11, centerX: true });
|
||||
_addLabel(ov, 'E', cx + R + 4, cy + 4, t.cardinal, { bold: true, size: 11 });
|
||||
_addLabel(ov, 'W', cx - R - 13, cy + 4, t.cardinal, { bold: true, size: 11 });
|
||||
|
||||
/* Etiquetas de elevación (30° y 60°) */
|
||||
[30, 60].forEach(el => {
|
||||
const r = R * (1 - el / 90);
|
||||
_addLabel(ov, el + '°', cx + r + 3, cy - 1, t.label, { size: 8 });
|
||||
});
|
||||
|
||||
/* PRN de satélites */
|
||||
(sats || []).forEach(s => {
|
||||
if (s.el == null || s.az == null) return;
|
||||
const r = R * (1 - s.el / 90);
|
||||
const ang = (s.az - 90) * Math.PI / 180;
|
||||
const x = cx + r * Math.cos(ang);
|
||||
const y = cy + r * Math.sin(ang);
|
||||
const rad = s.used ? 8 : 5;
|
||||
const col = s.used ? t.satText : t.satTextDim;
|
||||
_addLabel(ov, s.prn, x + rad + 2, y + 4, col, { size: 8 });
|
||||
});
|
||||
|
||||
/* Leyenda de sistemas */
|
||||
let lx = 6, ly = H - 4;
|
||||
Object.entries(SYS_COLOR).forEach(([sys, col]) => {
|
||||
const c = t.override || col;
|
||||
/* Cuadrito de color en canvas (sin texto) */
|
||||
ctx.fillStyle = c;
|
||||
ctx.fillRect(lx, ly - 7, 7, 7);
|
||||
/* Letra del sistema en overlay */
|
||||
_addLabel(ov, sys[0], lx + 9, ly, t.label, { size: 8 });
|
||||
lx += 22;
|
||||
});
|
||||
}
|
||||
|
||||
/* ── SNR bars ─────────────────────────────────────────────────────────── */
|
||||
function drawSNR(sats) {
|
||||
const canvas = document.getElementById('snr-canvas');
|
||||
if (!canvas) return;
|
||||
const t = _theme();
|
||||
const { ctx, W, H } = _setupCanvas(canvas);
|
||||
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
ctx.fillStyle = t.snrBg; ctx.fillRect(0, 0, W, H);
|
||||
|
||||
const visible = (sats || []).filter(s => s.snr != null).slice(0, 24);
|
||||
if (!visible.length) { _clearLabels('snr-labels'); return; }
|
||||
|
||||
const barW = Math.floor((W - 4) / visible.length) - 1;
|
||||
const maxH = H - 18;
|
||||
|
||||
const ov = _clearLabels('snr-labels');
|
||||
|
||||
visible.forEach((s, i) => {
|
||||
const snr = s.snr || 0;
|
||||
const bh = Math.round((snr / 50) * maxH);
|
||||
const x = 2 + i * (barW + 1);
|
||||
const y = H - 16 - bh;
|
||||
const baseCol = t.override || SYS_COLOR[s.system] || '#94a3b8';
|
||||
|
||||
if (bh > 0) {
|
||||
const grad = ctx.createLinearGradient(x, y, x, y + bh);
|
||||
grad.addColorStop(0, baseCol);
|
||||
grad.addColorStop(1, baseCol + (s.used ? '80' : '28'));
|
||||
ctx.fillStyle = s.used ? grad : baseCol + '38';
|
||||
ctx.fillRect(x, y, barW, bh);
|
||||
|
||||
if (s.used) {
|
||||
ctx.fillStyle = baseCol;
|
||||
ctx.fillRect(x, y, barW, 2);
|
||||
}
|
||||
}
|
||||
|
||||
/* Labels HTML */
|
||||
if (snr > 0) {
|
||||
const snrCol = s.used ? (t.snrUsedText || baseCol) : t.snrDimText;
|
||||
_addLabel(ov, snr, x + barW / 2, y - 1, snrCol, { size: 7, centerX: true });
|
||||
}
|
||||
|
||||
/* PRN debajo */
|
||||
const prnCol = s.used ? t.snrUsedText : t.snrDimText;
|
||||
_addLabel(ov, s.prn, x + barW / 2, H - 1, prnCol, { size: 7, centerX: true });
|
||||
});
|
||||
}
|
||||
|
||||
/* ── API pública ──────────────────────────────────────────────────────── */
|
||||
let _lastSats = [];
|
||||
|
||||
function update(sats) {
|
||||
_lastSats = sats || [];
|
||||
drawSky(_lastSats);
|
||||
drawSNR(_lastSats);
|
||||
|
||||
const used = _lastSats.filter(s => s.used).length;
|
||||
const view = _lastSats.length;
|
||||
const u = document.getElementById('sat-used');
|
||||
const v = document.getElementById('sat-view');
|
||||
if (u) u.textContent = used;
|
||||
if (v) v.textContent = view;
|
||||
}
|
||||
|
||||
function redraw() { drawSky(_lastSats); drawSNR(_lastSats); }
|
||||
|
||||
return { update, redraw };
|
||||
})();
|
||||
Reference in New Issue
Block a user