Radar: switch to RainViewer (IEM blocks heavy tile use), cleaner Map tab
Publish Container / publish (push) Successful in 2m40s

- /api/radar/times serves RainViewer frames (cached 2 min), IEM NEXRAD only as fallback;
  sources honour maxNativeZoom so tiles overzoom instead of going blank
- Map tab opens with radar + location dot only; wind/AQI/alerts/temp/sun/lightning are opt-in,
  readout chips follow their toggles
- Bolder location dot
- Satellite (GIBS) layer: new storage key, never persisted by storm taps, radar always draws above it

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:38:00 -04:00
1 parent 13985344ed
commit 2efc945192
3 files changed
+62 -33

No files matched your search

+6
View File
@@ -342,3 +342,9 @@ body.map-fs #tab-unified-map { padding: 0; }
.sticky-head { background: color-mix(in srgb, var(--paper) 96%, transparent) !important; }
.hero-hilo { background: rgba(0, 0, 0, 0.24) !important; }
.topbar, .bottom-nav { -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); }
/* readout chips follow their layer toggle (author display rules would otherwise beat [hidden]) */
#tab-unified-map .map-overlay-readouts[hidden], #tab-unified-map .map-readout-chip[hidden] { display: none !important; }
/* Location dot: the one thing on the map by default, so make it unmistakable (static halo, no animation) */
.map-location-dot { width: 20px; height: 20px; background: #2563eb; border: 3px solid #fff; border-radius: 50%; box-shadow: 0 0 0 7px rgba(37, 99, 235, 0.28), 0 2px 8px rgba(0, 0, 0, 0.55); }
+26 -8
View File
@@ -176,6 +176,8 @@ async function flyToHurricane(storm) {
if (sat && !sat.checked) {
sat.checked = true;
sat.dispatchEvent(new Event('change'));
// a storm tap is a one-off look, not a change to the user's saved map layers
try { localStorage.removeItem(STORAGE_KEYS.showSatelliteLayer); } catch {}
}
await new Promise(r => setTimeout(r, 450));
@@ -358,6 +360,8 @@ async function renderCloudOverlay(map) {
}
});
if (on) map.__cloudStamp = stamp;
// Radar frames must always draw above the clouds (they may have been added after this layer), or rain disappears
map.getStyle().layers.filter(l => l.id.startsWith('rv-radar-')).forEach(l => map.moveLayer(l.id));
} catch { /* style swapped mid-update; next refresh retries */ }
}
@@ -490,7 +494,7 @@ const STORAGE_KEYS = {
showLightning: 'weather.showLightning',
showWind: 'weather.showWind',
showRadarLayer: 'weather.map.radar',
showSatelliteLayer: 'weather.map.satellite',
showSatelliteLayer: 'weather.map.goesIR', // new key: the old 'satellite' toggle never drew anything, ignore its stale value
showLightningLayer: 'weather.map.lightning',
showWindLayer: 'weather.map.wind',
showAqiLayer: 'weather.map.aqi',
@@ -537,12 +541,12 @@ const state = {
unifiedMapLayers: {
radar: localStorage.getItem(STORAGE_KEYS.showRadarLayer) !== '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',
alerts: localStorage.getItem(STORAGE_KEYS.showAlertsLayer) !== 'false',
temp: localStorage.getItem(STORAGE_KEYS.showTempMarker) !== 'false',
sunMoon: localStorage.getItem(STORAGE_KEYS.showSunMoonMarkers) !== 'false',
lightning: localStorage.getItem(STORAGE_KEYS.showLightningLayer) === 'true',
wind: localStorage.getItem(STORAGE_KEYS.showWindLayer) === 'true',
aqi: localStorage.getItem(STORAGE_KEYS.showAqiLayer) === 'true',
alerts: localStorage.getItem(STORAGE_KEYS.showAlertsLayer) === 'true',
temp: localStorage.getItem(STORAGE_KEYS.showTempMarker) === 'true',
sunMoon: localStorage.getItem(STORAGE_KEYS.showSunMoonMarkers) === 'true',
hurricane: localStorage.getItem(STORAGE_KEYS.showHurricaneLayer) === 'true',
wildfire: localStorage.getItem(STORAGE_KEYS.showWildfireLayer) === 'true',
spc: localStorage.getItem(STORAGE_KEYS.showSpcLayer) === 'true',
@@ -1048,7 +1052,7 @@ function createRadar(mapEl, timelineEl, playBtn, tsEl, layerBtn) {
function addFrameLayer(frame, forMode) {
const id = `rv-${forMode}-${frame.time}`;
if (!map.getSource(id)) {
map.addSource(id, { type: 'raster', tiles: [frameTileUrl(frame, forMode)], tileSize: 256 });
map.addSource(id, { type: 'raster', tiles: [frameTileUrl(frame, forMode)], tileSize: 256, ...(frame.maxNativeZoom ? { maxzoom: frame.maxNativeZoom } : {}) });
map.addLayer({
id, type: 'raster', source: id,
paint: {
@@ -2611,11 +2615,25 @@ document.querySelectorAll('#tab-unified-map input[type="checkbox"][data-layer]')
}
if (layerName === 'satellite') renderCloudOverlay(mapsOverview.map);
syncReadoutChips();
if (state.currentCity) refreshMapsOverview();
});
});
// The top-left readout chips only show for layers the user switched on
function syncReadoutChips() {
const L = state.unifiedMapLayers;
[['overlay-temp', L.temp], ['overlay-aqi', L.aqi], ['overlay-sunmoon', L.sunMoon]].forEach(([id, on]) => {
const el = document.getElementById(id);
if (el) el.hidden = !on;
});
const box = document.getElementById('map-overlay-readouts');
if (box) box.hidden = !(L.temp || L.aqi || L.sunMoon);
}
syncReadoutChips();
const baseMapSelect = document.getElementById('base-map-select');
if (baseMapSelect) {
baseMapSelect.value = state.unifiedMapBaseLayer;
+30 -25
View File
@@ -909,12 +909,38 @@ app.get('/api/alerts', async (req, res) => {
}
});
// Radar/satellite tile configuration
// Radar tile configuration.
// Primary: RainViewer (global composite, 10-min frames, free, tiles load straight from their CDN).
// Fallback: IEM NEXRAD (US only). IEM blocks IPs that pull too many tiles and answers with a "sorry"
// redirect, so it must not be the only source. Satellite imagery is separate (NASA GIBS, client-side).
let rainviewerCache = { at: 0, data: null };
async function rainviewerFrames() {
if (rainviewerCache.data && Date.now() - rainviewerCache.at < 120000) return rainviewerCache.data;
const r = await fetchWithRetry('https://api.rainviewer.com/public/weather-maps.json', { attempts: 2, timeoutMs: 8000 });
if (!r.ok) throw new Error(`RainViewer ${r.status}`);
const j = await r.json();
const host = j.host || 'https://tilecache.rainviewer.com';
const frames = (j.radar?.past || []).slice(-12).map(f => ({
time: f.time,
url: `${host}${f.path}/256/{z}/{x}/{y}/2/1_1.png`, // colour scheme 2, smoothed, snow shown
isForecast: false,
maxNativeZoom: 7, // RainViewer's free tiles stop at z7; MapLibre overzooms beyond that
}));
if (!frames.length) throw new Error('RainViewer returned no frames');
rainviewerCache = { at: Date.now(), data: frames };
return frames;
}
app.get('/api/radar/times', async (req, res) => {
try {
const now = Math.floor(Date.now() / 1000);
try {
const past = await rainviewerFrames();
return res.json({ provider: 'rainviewer', radar: { past }, satellite: { infrared: [] } });
} catch (e) {
console.error('RainViewer unavailable, falling back to IEM:', e.message);
}
// IEM NEXRAD tile service (US radar, public/free) with historical offsets for animation.
const now = Math.floor(Date.now() / 1000);
const radarLayerBase = 'https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0';
const radarOffsetsMin = [55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5, 0];
const radarPast = radarOffsetsMin.map(offset => {
@@ -926,28 +952,7 @@ app.get('/api/radar/times', async (req, res) => {
maxNativeZoom: 12,
};
});
// IEM GOES East cloud/satellite frames for animation.
const satOffsetsMin = [30, 25, 20, 15, 10, 5, 0];
const satelliteInfrared = satOffsetsMin.map(offset => {
const layer = offset === 0 ? 'goes_east' : `goes_east_m${String(offset).padStart(2, '0')}m`;
return {
time: now - (offset * 60),
url: `https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/${layer}/{z}/{x}/{y}.png`,
isForecast: false,
maxNativeZoom: 10,
};
});
res.json({
provider: 'iem-radar-sat',
radar: {
past: radarPast,
},
satellite: {
infrared: satelliteInfrared,
},
});
res.json({ provider: 'iem-radar', radar: { past: radarPast }, satellite: { infrared: [] } });
} catch (e) {
console.error('Radar config error:', e.message);
res.status(500).json({ error: 'Radar times failed' });