1import { normalizeLon180 } from './utils/colorMap.js'; 2import { storageGetItem, storageSetItem } from './utils/consentStorage.js'; 3 4// --- Constants --- 5// GCS mirror of NOAA's ovation_aurora_latest.json (refreshed by updateAuroraData) 6// index.html starts this same download early; tests/unit/markup.test.mjs keeps the two in step. 7export const BUCKET_URL = "https://storage.googleapis.com/auroramap-public-data/ovation_aurora_latest.json"; 8 9const CACHE_KEY = "auroraDataCache"; 10const CACHE_TIME_KEY = "auroraDataCacheTime"; 11const CACHE_DURATION_MS = 2 * 60 * 1000; // 2 minutes (Local browser cache) 12const EQUATOR_MASK_DEG = 5; 13 14// --- STATE --- 15// The points of the frame on the globe; set by drawAuroraFrame. 16let _currentPoints = []; 17 18/** 19 * Main entry point: Get aurora data (checks cache first, then network) 20 */ 21export async function fetchAuroraDataWithCache() { 22 // 1. Try Cache 23 const cached = loadFromCache(); 24 if (cached) { 25 console.log("â¡ Loaded aurora data from local cache"); 26 return cached; 27 } 28 29 // 2. Try Network (Your Bucket) 30 console.log("ð¡ Fetching fresh aurora data from Cloud Storage..."); 31 const freshData = await fetchAuroraData(); 32 33 if (freshData) { 34 saveToCache(freshData.points); 35 return freshData; 36 } 37 38 // 3. Fallback 39 return { points: [], timestamp: new Date() }; 40} 41 42/** 43 * Set the point set that probability lookups (pins, the info card) read from. 44 */ 45export function setCurrentPoints(points) { 46 if (Array.isArray(points)) { 47 _currentPoints = points; 48 } 49} 50 51// Max distance (degrees, squared) for a data point to be considered "at" a location. 52// Forecast data has 1° resolution so max in-band distance is ~0.71°; 2° gives comfortable margin. 53// Without this cap, forecast JSONs (which omit 0%-probability points) return aurora band 54// probability for every point on Earth because the nearest stored point always has prob >= 1. 55const MAX_LOOKUP_DIST_SQ = 4; // 2° radius 56 57/** 58 * Synchronously get probability for a lat/lon 59 */ 60export function getAuroraProbability(lat, lon) { 61 if (!_currentPoints || _currentPoints.length === 0) return 'N/A'; 62 63 const qLon = normalizeLon180(lon); 64 let closest = null; 65 let minDist = Infinity; 66 67 for (const p of _currentPoints) { 68 const dLat = p.lat - lat; 69 // Across the antimeridian, 179.8 and -179.9 are neighbours. 70 let dLon = Math.abs(p.lon - qLon); 71 if (dLon > 180) dLon = 360 - dLon; 72 const dist = dLat * dLat + dLon * dLon; 73 if (dist < minDist) { 74 minDist = dist; 75 closest = p; 76 } 77 } 78 79 if (closest && minDist <= MAX_LOOKUP_DIST_SQ) { 80 if (Math.abs(closest.lat) <= EQUATOR_MASK_DEG) return '0.0%'; 81 82 let prob = Number(closest.p) || 0; 83 prob = Math.max(0, Math.min(100, prob)); 84 return prob.toFixed(1) + '%'; 85 } 86 87 return '0.0%'; 88} 89 90/** 91 * Fetch from YOUR BUCKET (Not NOAA) 92 */ 93export async function fetchAuroraData() { 94 try { 95 // We append ?t=... to force the browser to ignore its internal cache 96 // and actually hit Google Storage (which respects the 5-min max-age we set in the backend). 97 const url = `${BUCKET_URL}?t=${Date.now()}`; 98 99 // index.html starts this download before any module has loaded; use it 100 // once, and fetch afresh if it failed or has already been used. 101 const early = globalThis.__earlyAuroraFetch; 102 globalThis.__earlyAuroraFetch = null; 103 const resp = (early && await early) || await fetch(url); 104 if (!resp.ok) throw new Error(`Bucket Error: ${resp.status}`); 105 106 const data = await resp.json(); 107 108 // The structure from the bucket is identical to NOAA's, so this logic stays the same. 109 const points = processAuroraCoordinates(data.coordinates || []); 110 111 return { 112 points: points, 113 timestamp: new Date() 114 }; 115 116 } catch (err) { 117 console.error("â Failed to load aurora data:", err); 118 return null; 119 } 120} 121 122/** 123 * NOAA's [lon (0..359), lat, probability] rows -> {lon (-180..180), lat, p}. 124 */ 125export function processAuroraCoordinates(rawCoords) { 126 if (!Array.isArray(rawCoords)) return []; 127 128 // Safety check: The raw data is a flat array of [lon, lat, val] 129 // We map it into objects for easier usage. 130 const mapped = rawCoords.map(c => { 131 const rawLon = Number(c[0]); 132 const rawLat = Number(c[1]); 133 let pval = Number(c[2] ?? 0); 134 135 if (!isFinite(rawLon) || !isFinite(rawLat) || !isFinite(pval)) return null; 136 137 // Normalize longitude to -180...180 138 const lon = normalizeLon180(rawLon); 139 const lat = Math.max(-90, Math.min(90, rawLat)); 140 141 // Clean up noise near equator 142 if (Math.abs(lat) <= EQUATOR_MASK_DEG) pval = 0.0; 143 144 return { lon, lat, p: pval }; 145 }
145).filter(Boolean); // Filter out nulls 146 147 return mapped; 148} 149 150function loadFromCache() { 151 try { 152 const raw = storageGetItem(CACHE_KEY); 153 const timeStr = storageGetItem(CACHE_TIME_KEY); 154 if (!raw || !timeStr) return null; 155 if (Date.now() - Number(timeStr) < CACHE_DURATION_MS) { 156 const points = JSON.parse(raw); 157 return { points, timestamp: new Date(Number(timeStr)) }; 158 } 159 } catch (e) { 160 // Corrupt or unreadable cache. Not fatal â the caller falls back to the 161 // network â but silence here hid stale-cache bugs for a long time. 162 console.warn('[aurora-data] Ignoring unreadable cache:', e.message); 163 } 164 return null; 165} 166 167function saveToCache(points) { 168 try { 169 storageSetItem(CACHE_KEY, JSON.stringify(points)); 170 storageSetItem(CACHE_TIME_KEY, String(Date.now())); 171 } catch (e) { 172 // Usually a quota error or private browsing. The site still works, it 173 // just re-fetches every load. 174 console.warn('[aurora-data] Could not cache aurora data:', e.message); 175 } 176}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.