parent
b28727f131
commit
dad591e265
@ -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());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in new issue