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 OpenWeather (CC BY-SA 4.0)'; 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 RainViewer', }, { id: "nexrad", label: "US radar", hint: "NWS NEXRAD radar, past 50 minutes. United States only, high detail.", maxNativeZoom: 12, needsOpenWeather: false, attribution: 'NEXRAD via Iowa Environmental Mesonet (Iowa State Univ.)', }, { 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 = { "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 { 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}` }]; }