// Shared helpers used across layers. Cesium and satellite are CDN globals. const Cesium = window.Cesium; // Build an offscreen canvas glyph for a BillboardCollection. export function makeGlyphCanvas(size, drawFn) { const c = document.createElement('canvas'); c.width = size; c.height = size; drawFn(c.getContext('2d'), size); return c; } // White upward-pointing triangle (aircraft). Tinted per-billboard via .color. export function aircraftGlyph() { return makeGlyphCanvas(32, (g) => { g.fillStyle = '#ffffff'; g.beginPath(); g.moveTo(16, 2); g.lineTo(28, 30); g.lineTo(16, 23); g.lineTo(4, 30); g.closePath(); g.fill(); }); } // Upward teardrop/flame (wildfires). Drawn directly in `hex` with a dark // outline — one shared canvas reused across every fire billboard. export function flameGlyph(hex) { return makeGlyphCanvas(28, (g, s) => { const cx = s / 2; g.beginPath(); g.moveTo(cx, 3); // pointed top g.bezierCurveTo(s * 0.86, s * 0.34, s * 0.80, s * 0.72, cx, s - 4); g.bezierCurveTo(s * 0.20, s * 0.72, s * 0.14, s * 0.34, cx, 3); g.closePath(); g.fillStyle = hex; g.fill(); g.lineWidth = 2; g.strokeStyle = '#0a0f14'; g.stroke(); }); } // White diamond (ships). export function shipGlyph() { return makeGlyphCanvas(28, (g) => { g.fillStyle = '#ffffff'; g.beginPath(); g.moveTo(14, 2); g.lineTo(26, 14); g.lineTo(14, 26); g.lineTo(2, 14); g.closePath(); g.fill(); }); } // Great-circle initial bearing, degrees (0 = north, clockwise). export function bearingDeg(lat1, lon1, lat2, lon2) { const toR = Math.PI / 180; const p1 = lat1 * toR; const p2 = lat2 * toR; const dl = (lon2 - lon1) * toR; const y = Math.sin(dl) * Math.cos(p2); const x = Math.cos(p1) * Math.sin(p2) - Math.sin(p1) * Math.cos(p2) * Math.cos(dl); return (Math.atan2(y, x) * 180 / Math.PI + 360) % 360; } // Aircraft altitude → band color. export function altitudeColor(CONFIG, altMeters) { const c = CONFIG.colors; if (altMeters == null) return c.aircraftHigh; if (altMeters < 3000) return c.aircraftLow; if (altMeters < 9000) return c.aircraftMid; return c.aircraftHigh; } // Escape a string for safe interpolation into InfoBox description HTML. // Feeds like USGS/EONET are trusted, but this is defense-in-depth for the // public deploy (and the InfoBox is a real HTML sink). export function escapeHtml(s) { return String(s == null ? '' : s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // Only allow http(s) URLs into href attributes (blocks javascript:/data: URLs). export function safeUrl(u) { const s = String(u == null ? '' : u).trim(); return /^https?:\/\//i.test(s) ? s : ''; } // CSS hex → Cesium.Color, optional alpha. export function cz(hex, alpha) { const col = Cesium.Color.fromCssColorString(hex); return alpha == null ? col : col.withAlpha(alpha); } // Billboard screen-space rotation (CCW) for a compass heading (CW from north). export function headingToRotation(headingDeg) { return -Cesium.Math.toRadians(headingDeg || 0); }