Storm cards show live GOES infrared satellite; replace the dead RainViewer satellite toggle
Publish Container / publish (push) Successful in 2m19s

RainViewer's satellite feed is empty, so the Clouds toggle never drew anything and ground
radar doesn't reach open ocean. Use NASA GIBS GOES-West/East infrared tiles instead,
auto-enabled when flying to a storm, default off otherwise.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Nate EmmertandClaude Sonnet 5.5 committed 2026-10-15 16:01:03 -04:00
1 parent a185d10e1c
commit 13985344ed
2 files changed
+41 -13

No files matched your search

+40 -12
View File
@@ -170,6 +170,14 @@ async function flyToHurricane(storm) {
if (cb) cb.checked = true;
}
// Ground radar doesn't reach open ocean, so show infrared satellite (the storm's actual cloud
// structure). Goes through the layer checkbox so state and storage stay in sync.
const sat = document.querySelector('#tab-unified-map input[data-layer="satellite"]');
if (sat && !sat.checked) {
sat.checked = true;
sat.dispatchEvent(new Event('change'));
}
await new Promise(r => setTimeout(r, 450));
const map = mapsOverview.map;
@@ -178,7 +186,7 @@ async function flyToHurricane(storm) {
// flyTo works regardless, so just make sure the canvas has its real size first.
map.resize();
map.flyTo({ center: [lon, lat], zoom: 4, duration: 900 });
map.flyTo({ center: [lon, lat], zoom: 4, duration: 900, padding: { bottom: 200 } });
setTimeout(() => {
new maplibregl.Popup({ closeButton: true, maxWidth: '260px' })
@@ -230,7 +238,7 @@ async function flyToHurricane(storm) {
const lats = allCoords.map(c => c[1]);
map.fitBounds(
[[Math.min(...lons), Math.min(...lats)], [Math.max(...lons), Math.max(...lats)]],
{ padding: 60, duration: 600, maxZoom: 5 }
{ padding: { top: 70, left: 50, right: 50, bottom: 260 }, duration: 600, maxZoom: 5 }
);
}
@@ -326,6 +334,33 @@ function mapReady(map, timeoutMs = 4000) {
});
}
// Infrared satellite from NASA GIBS (GOES-West + GOES-East, newest image, no key). Unlike ground radar it
// reaches open ocean, so a hurricane's cloud shield is visible. East draws over West where they overlap.
const GOES_LAYERS = [['goes-west', 'GOES-West_ABI_Band13_Clean_Infrared'], ['goes-east', 'GOES-East_ABI_Band13_Clean_Infrared']];
async function renderCloudOverlay(map) {
if (!map) return;
await mapReady(map);
const on = !!state.unifiedMapLayers.satellite;
const stamp = Math.floor(Date.now() / 600000); // GIBS updates ~every 10 min; bust the tile cache then
try {
GOES_LAYERS.forEach(([id, layer]) => {
if (!on) { if (map.getLayer(id)) map.setLayoutProperty(id, 'visibility', 'none'); return; }
if (map.getLayer(id)) { map.setLayoutProperty(id, 'visibility', 'visible'); }
else {
map.addSource(id, { type: 'raster', tileSize: 256, maxzoom: 6, attribution: 'NASA GIBS',
tiles: [`https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/${layer}/default/GoogleMapsCompatible_Level6/{z}/{y}/{x}.png?t=${stamp}`] });
// sit under the animated radar frames so rain still reads on top of the clouds
const beforeId = map.getStyle().layers.find(l => l.id.startsWith('rv-radar-'))?.id;
map.addLayer({ id, type: 'raster', source: id, paint: { 'raster-opacity': 0.8, 'raster-fade-duration': 0 } }, beforeId);
}
if (map.__cloudStamp !== stamp && map.getSource(id)?.setTiles) {
map.getSource(id).setTiles([`https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/${layer}/default/GoogleMapsCompatible_Level6/{z}/{y}/{x}.png?t=${stamp}`]);
}
});
if (on) map.__cloudStamp = stamp;
} catch { /* style swapped mid-update; next refresh retries */ }
}
async function renderHurricaneOverlay(map) {
if (!map) return;
const tok = (map.__hurrTok = (map.__hurrTok || 0) + 1); // a newer refresh supersedes this one
@@ -501,7 +536,7 @@ const state = {
showExtendedForecast: false,
unifiedMapLayers: {
radar: localStorage.getItem(STORAGE_KEYS.showRadarLayer) !== 'false',
satellite: localStorage.getItem(STORAGE_KEYS.showSatelliteLayer) !== 'false',
satellite: localStorage.getItem(STORAGE_KEYS.showSatelliteLayer) === 'true',
lightning: localStorage.getItem(STORAGE_KEYS.showLightningLayer) !== 'false',
wind: localStorage.getItem(STORAGE_KEYS.showWindLayer) !== 'false',
aqi: localStorage.getItem(STORAGE_KEYS.showAqiLayer) !== 'false',
@@ -1367,6 +1402,7 @@ function refreshMapsOverview() {
renderAlertMarker(state.currentCity);
renderTempMarker(state.currentCity);
renderSunMoonMarker(state.currentCity);
renderCloudOverlay(mapsOverview.map);
renderHurricaneOverlay(mapsOverview.map);
renderWildfireOverlay(mapsOverview.map);
renderSPCOverlay(mapsOverview.map);
@@ -2574,15 +2610,7 @@ document.querySelectorAll('#tab-unified-map input[type="checkbox"][data-layer]')
if (!enabled && unifiedMap?.stopPlay) unifiedMap.stopPlay();
}
if (layerName === 'satellite') {
if (enabled) {
if (unifiedMapMode !== 'satellite') unifiedMapModeButton.click();
setCloudLayerVisible(true);
} else {
setCloudLayerVisible(false);
if (unifiedMapMode !== 'radar') unifiedMapModeButton.click();
}
}
if (layerName === 'satellite') renderCloudOverlay(mapsOverview.map);
if (state.currentCity) refreshMapsOverview();
});
+1 -1
View File
@@ -292,7 +292,7 @@
</span>
</label>
<label class="toggle-control">
<span class="toggle-label">☁️ Clouds</span>
<span class="toggle-label">🛰 Satellite (IR)</span>
<span class="switch-toggle">
<input type="checkbox" id="toggle-satellite" data-layer="satellite">
<span class="toggle-slider"></span>