PageSourceSearch

https://aurora-c9c22.web.app/js/auroraDataService.js?v=6341c0fa58

js aurora-c9c22.web.app collected 2026-10-03 12:17:19 UTC 5,998 bytes, 176 lines download raw bytes

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.