diff --git a/app/public/app.js b/app/public/app.js index 96fd2e5..ba947f7 100644 --- a/app/public/app.js +++ b/app/public/app.js @@ -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(); }); diff --git a/app/public/index.html b/app/public/index.html index 554fac3..b78cc99 100644 --- a/app/public/index.html +++ b/app/public/index.html @@ -292,7 +292,7 @@