'use strict'; // TV-news-style live weather radar PiP overlay. // // node radar.js [path/to/config.json] // // Two modes (config.mode): // "always" - keep the radar overlay on screen permanently. // "on_warning" - (default) poll the NWS and only "cut to radar" when a qualifying // warning (Tornado / Severe Thunderstorm / Flash Flood / Flood, by // default) covers the configured point; clear it when none remain. // // The overlay page (radar-overlay.html) does the actual map drawing in the player's // browser: CARTO basemap + animated RainViewer radar + live NWS warning polygons. This // script just decides WHEN to show it and pushes/clears the PiP. Node 18+ (global fetch), // needs an st_ API token with the 'full' scope. const fs = require('fs'); const path = require('path'); // ---- pure, offline-testable helpers ------------------------------------------------- const EVENT_COLORS = { 'Tornado Warning': '#FF2D2D', 'Severe Thunderstorm Warning': '#FFD12E', 'Flash Flood Warning': '#25D0C0', 'Flood Warning': '#46C766', }; const DEFAULT_COLOR = '#FF8A1F'; function colorForEvent(event) { return EVENT_COLORS[event] || DEFAULT_COLOR; } // Normalise a NWS GeoJSON FeatureCollection into the minimal shape we gate on. function normaliseFeatureCollection(json) { const obj = typeof json === 'string' ? JSON.parse(json) : json; const feats = (obj && Array.isArray(obj.features)) ? obj.features : []; return feats.map((f) => { const p = (f && f.properties) || {}; const g = (f && f.geometry) || null; return { identifier: p.id || (f && f.id) || null, event: p.event || null, severity: p.severity || 'Unknown', expires: p.expires || p.ends || null, headline: p.headline || p.event || '', hasGeometry: !!(g && (g.type === 'Polygon' || g.type === 'MultiPolygon')), }; }); } function isExpired(expires, now) { if (!expires) return false; const t = Date.parse(expires); return Number.isFinite(t) && t <= now; } // Show-worthy if it's one of the configured warning events, still active, and has a // polygon we can actually draw on the map. function qualifies(alert, opts = {}) { const events = opts.events || Object.keys(EVENT_COLORS); const now = opts.now || Date.now(); if (!alert || !alert.event) return false; if (!events.includes(alert.event)) return false; if (!alert.hasGeometry) return false; if (isExpired(alert.expires, now)) return false; return true; } // Keep in lockstep with the ?v= on the