Files
AR-GPS/frontend/js/skyplot.js
T

312 lines
11 KiB
JavaScript

'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 };
})();