87 lines
4.1 KiB
TypeScript
87 lines
4.1 KiB
TypeScript
|
|
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}` }];
|
||
|
|
}
|