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

+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' });