From dad591e26578903c380ee1b45d0075d54b967c76 Mon Sep 17 00:00:00 2001 From: accius Date: Mon, 2 Feb 2026 20:02:17 -0500 Subject: [PATCH] aurora overlay --- src/plugins/layerRegistry.js | 3 +- src/plugins/layers/useAurora.js | 129 ++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 src/plugins/layers/useAurora.js diff --git a/src/plugins/layerRegistry.js b/src/plugins/layerRegistry.js index ccaace2..013e816 100644 --- a/src/plugins/layerRegistry.js +++ b/src/plugins/layerRegistry.js @@ -1,14 +1,15 @@ /** * Layer Plugin Registry - * Only Weather Radar for now */ import * as WXRadarPlugin from './layers/useWXRadar.js'; import * as EarthquakesPlugin from './layers/useEarthquakes.js'; +import * as AuroraPlugin from './layers/useAurora.js'; const layerPlugins = [ WXRadarPlugin, EarthquakesPlugin, + AuroraPlugin, ]; export function getAllLayers() { diff --git a/src/plugins/layers/useAurora.js b/src/plugins/layers/useAurora.js new file mode 100644 index 0000000..e3eaa18 --- /dev/null +++ b/src/plugins/layers/useAurora.js @@ -0,0 +1,129 @@ +import { useState, useEffect } from 'react'; + +// NOAA OVATION Aurora Forecast +// Provides 30-min forecast of auroral activity as a global image overlay +// Data: https://services.swpc.noaa.gov/products/animations/ovation_north_24h.json +// Image: https://services.swpc.noaa.gov/images/animations/ovation/north/latest.jpg + +export const metadata = { + id: 'aurora', + name: 'Aurora Forecast', + description: 'NOAA OVATION auroral oval forecast (Northern & Southern)', + icon: '🌌', + category: 'space-weather', + defaultEnabled: false, + defaultOpacity: 0.5, + version: '1.0.0' +}; + +export function useLayer({ enabled = false, opacity = 0.5, map = null }) { + const [northLayer, setNorthLayer] = useState(null); + const [southLayer, setSouthLayer] = useState(null); + const [refreshTimestamp, setRefreshTimestamp] = useState(Date.now()); + + // NOAA provides aurora forecast as a GeoJSON-like data product + // We'll use the OVATION aurora map images which cover both hemispheres + // These are pre-rendered transparent PNGs showing aurora probability + + useEffect(() => { + if (!map || typeof L === 'undefined') return; + + if (enabled) { + try { + // NOAA OVATION aurora forecast - uses a tile overlay from SWPC + // The aurora oval images are projected onto a polar view, but SWPC also + // provides an equirectangular overlay we can use with Leaflet + const t = Math.floor(Date.now() / 300000) * 300000; // 5-min cache bust + + // Northern hemisphere aurora overlay + const north = L.imageOverlay( + `https://services.swpc.noaa.gov/images/aurora-forecast-northern-hemisphere.jpg?t=${t}`, + [[0, -180], [90, 180]], + { + opacity: opacity, + zIndex: 210, + className: 'aurora-overlay' + } + ); + + // Southern hemisphere aurora overlay + const south = L.imageOverlay( + `https://services.swpc.noaa.gov/images/aurora-forecast-southern-hemisphere.jpg?t=${t}`, + [[-90, -180], [0, 180]], + { + opacity: opacity, + zIndex: 210, + className: 'aurora-overlay' + } + ); + + north.addTo(map); + south.addTo(map); + setNorthLayer(north); + setSouthLayer(south); + } catch (err) { + console.error('Aurora overlay error:', err); + } + } else { + // Remove layers when disabled + if (northLayer) { + try { map.removeLayer(northLayer); } catch (e) {} + setNorthLayer(null); + } + if (southLayer) { + try { map.removeLayer(southLayer); } catch (e) {} + setSouthLayer(null); + } + } + + return () => { + if (northLayer && map) { + try { map.removeLayer(northLayer); } catch (e) {} + } + if (southLayer && map) { + try { map.removeLayer(southLayer); } catch (e) {} + } + }; + }, [enabled, map, refreshTimestamp]); + + // Update opacity + useEffect(() => { + if (northLayer) northLayer.setOpacity(opacity); + if (southLayer) southLayer.setOpacity(opacity); + }, [opacity, northLayer, southLayer]); + + // Auto-refresh every 10 minutes (NOAA updates ~every 30 min) + useEffect(() => { + if (!enabled) return; + + const interval = setInterval(() => { + // Remove old layers and trigger re-add with new timestamp + if (northLayer && map) { + try { map.removeLayer(northLayer); } catch (e) {} + setNorthLayer(null); + } + if (southLayer && map) { + try { map.removeLayer(southLayer); } catch (e) {} + setSouthLayer(null); + } + setRefreshTimestamp(Date.now()); + }, 600000); // 10 minutes + + return () => clearInterval(interval); + }, [enabled, northLayer, southLayer, map]); + + return { + layers: [northLayer, southLayer].filter(Boolean), + refresh: () => { + if (northLayer && map) { + try { map.removeLayer(northLayer); } catch (e) {} + setNorthLayer(null); + } + if (southLayer && map) { + try { map.removeLayer(southLayer); } catch (e) {} + setSouthLayer(null); + } + setRefreshTimestamp(Date.now()); + } + }; +}