This commit is contained in:
Cayden Schwechten 2026-10-31 21:20:16 -05:00
commit 6250d23297
53 changed files with 6381 additions and 0 deletions

86
lib/radar/layers.ts Normal file
View file

@ -0,0 +1,86 @@
import type { RadarFrame } from "../weather/types";
/**
* Every map overlay the radar panel can show, and how to get its frames.
*
* RainViewer — worldwide radar, last ~2 hours in 10-minute steps.
* Since Jan 2026: past frames only, max zoom 7, one color scheme,
* personal/educational use only.
* NEXRAD — US-only NWS radar via Iowa State's mesonet; much sharper,
* last 50 minutes in 5-minute steps. No key.
* OpenWeather — model maps (precipitation, clouds, temperature, wind, pressure).
* Needs OPENWEATHER_API_KEY; tiles go through /api/tiles so the key stays secret.
*/
export type RadarLayerId = "rainviewer" | "nexrad" | "owm-precip" | "owm-clouds" | "owm-temp" | "owm-wind" | "owm-pressure";
export interface RadarLayer {
id: RadarLayerId;
label: string;
hint: string; // shown under the map
maxNativeZoom: number; // beyond this, tiles are stretched rather than fetched
needsOpenWeather: boolean;
attribution: string;
}
const OWM_ATTRIBUTION =
'Weather data provided by <a href="https://openweathermap.org/" target="_blank" rel="noopener">OpenWeather</a> (<a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener">CC BY-SA 4.0</a>)';
export const RADAR_LAYERS: RadarLayer[] = [
{
id: "rainviewer",
label: "Radar",
hint: "Worldwide radar, past 2 hours. Detail stops at regional zoom.",
maxNativeZoom: 7,
needsOpenWeather: false,
attribution: 'Weather data by <a href="https://www.rainviewer.com/" target="_blank" rel="noopener">RainViewer</a>',
},
{
id: "nexrad",
label: "US radar",
hint: "NWS NEXRAD radar, past 50 minutes. United States only, high detail.",
maxNativeZoom: 12,
needsOpenWeather: false,
attribution:
'NEXRAD via <a href="https://mesonet.agron.iastate.edu/" target="_blank" rel="noopener">Iowa Environmental Mesonet (Iowa State Univ.)</a>',
},
{ id: "owm-precip", label: "Precipitation", hint: "Modelled precipitation right now.", maxNativeZoom: 9, needsOpenWeather: true, attribution: OWM_ATTRIBUTION },
{ id: "owm-clouds", label: "Clouds", hint: "Modelled cloud cover right now.", maxNativeZoom: 9, needsOpenWeather: true, attribution: OWM_ATTRIBUTION },
{ id: "owm-temp", label: "Temperature", hint: "Modelled air temperature right now.", maxNativeZoom: 9, needsOpenWeather: true, attribution: OWM_ATTRIBUTION },
{ id: "owm-wind", label: "Wind", hint: "Modelled wind speed right now.", maxNativeZoom: 9, needsOpenWeather: true, attribution: OWM_ATTRIBUTION },
{ id: "owm-pressure", label: "Pressure", hint: "Modelled sea-level pressure right now.", maxNativeZoom: 9, needsOpenWeather: true, attribution: OWM_ATTRIBUTION },
];
/** Our layer ids → OpenWeather's tile layer names. Also the allow-list for /api/tiles. */
export const OWM_TILE_LAYERS: Record<string, string> = {
"owm-precip": "precipitation_new",
"owm-clouds": "clouds_new",
"owm-temp": "temp_new",
"owm-wind": "wind_new",
"owm-pressure": "pressure_new",
};
/** NEXRAD frames are fixed offsets from "now": 50, 45 … 5 minutes ago, then the latest. */
function nexradFrames(): RadarFrame[] {
const fiveMinutes = 5 * 60_000;
const latest = Math.floor(Date.now() / fiveMinutes) * fiveMinutes;
const base = "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-n0q-900913";
return [50, 45, 40, 35, 30, 25, 20, 15, 10, 5, 0].map((minutesAgo) => ({
time: new Date(latest - minutesAgo * 60_000).toISOString(),
url: minutesAgo === 0 ? `${base}/{z}/{x}/{y}.png` : `${base}-m${String(minutesAgo).padStart(2, "0")}m/{z}/{x}/{y}.png`,
}));
}
/** Loads the frames for a layer. Single-frame layers return a list of one. */
export async function loadFrames(id: RadarLayerId): Promise<RadarFrame[]> {
if (id === "nexrad") return nexradFrames();
if (id === "rainviewer") {
const response = await fetch("/api/radar");
if (!response.ok) throw new Error("Radar frames are unavailable right now");
return (await response.json()) as RadarFrame[];
}
return [{ time: new Date().toISOString(), url: `/api/tiles/${id}/{z}/{x}/{y}` }];
}