'use strict'; /* Sky plot (azimuth/elevation) + SNR signal bars. Mode-aware: lee data-mode del 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 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 }; })();