init
This commit is contained in:
commit
6250d23297
53 changed files with 6381 additions and 0 deletions
50
lib/weather/extras/air-quality.ts
Normal file
50
lib/weather/extras/air-quality.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { fetchJson, maybe } from "../http";
|
||||
import type { AirQuality } from "../types";
|
||||
|
||||
/**
|
||||
* Air quality from Open-Meteo's free air-quality API (CAMS data), used for
|
||||
* every provider so the AQI always means the same thing.
|
||||
* Docs: https://open-meteo.com/en/docs/air-quality-api
|
||||
*
|
||||
* The same call also tells us the location's IANA time zone, which some
|
||||
* providers (Tomorrow.io, OpenWeather free) don't give us.
|
||||
*/
|
||||
|
||||
interface AirQualityResponse {
|
||||
timezone: string;
|
||||
current: {
|
||||
us_aqi: number | null;
|
||||
pm2_5: number | null;
|
||||
pm10: number | null;
|
||||
ozone: number | null;
|
||||
nitrogen_dioxide: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LocalContext {
|
||||
timeZone: string;
|
||||
airQuality: AirQuality | null;
|
||||
}
|
||||
|
||||
export async function getLocalContext(lat: number, lon: number): Promise<LocalContext> {
|
||||
const params = new URLSearchParams({
|
||||
latitude: String(lat),
|
||||
longitude: String(lon),
|
||||
current: "us_aqi,pm2_5,pm10,ozone,nitrogen_dioxide",
|
||||
timezone: "auto",
|
||||
});
|
||||
const data = await fetchJson<AirQualityResponse>(
|
||||
`https://air-quality-api.open-meteo.com/v1/air-quality?${params}`,
|
||||
"Open-Meteo air quality",
|
||||
{ cacheSeconds: 1800 }, // air quality updates hourly at most
|
||||
);
|
||||
|
||||
const c = data.current;
|
||||
return {
|
||||
timeZone: data.timezone,
|
||||
airQuality:
|
||||
c.us_aqi === null
|
||||
? null
|
||||
: { usAqi: Math.round(c.us_aqi), pm25: maybe(c.pm2_5), pm10: maybe(c.pm10), ozone: maybe(c.ozone), no2: maybe(c.nitrogen_dioxide) },
|
||||
};
|
||||
}
|
||||
75
lib/weather/extras/nws-alerts.ts
Normal file
75
lib/weather/extras/nws-alerts.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { toSeverity } from "../describe";
|
||||
import { fetchJson } from "../http";
|
||||
import type { WeatherAlert } from "../types";
|
||||
|
||||
/**
|
||||
* Active US weather alerts from the National Weather Service (free, no key).
|
||||
* Docs: https://www.weather.gov/documentation/services-web-api
|
||||
*
|
||||
* NWS asks every app to identify itself with a User-Agent containing contact
|
||||
* info, so set NWS_CONTACT in .env.local (an email or website).
|
||||
* Outside the US this returns nothing, which is fine.
|
||||
*/
|
||||
|
||||
interface NwsResponse {
|
||||
features: {
|
||||
properties: {
|
||||
event: string;
|
||||
headline: string | null;
|
||||
severity: string;
|
||||
senderName: string;
|
||||
onset: string | null;
|
||||
effective: string;
|
||||
ends: string | null;
|
||||
expires: string;
|
||||
description: string;
|
||||
instruction: string | null;
|
||||
};
|
||||
}[];
|
||||
}
|
||||
|
||||
/** Rough boxes around US states and territories — skips pointless calls elsewhere. */
|
||||
function mightBeInUS(lat: number, lon: number): boolean {
|
||||
const mainland = lat > 24 && lat < 50 && lon > -125 && lon < -66;
|
||||
const alaska = lat > 51 && lat < 72 && (lon < -129 || lon > 172);
|
||||
const hawaii = lat > 18 && lat < 23 && lon > -161 && lon < -154;
|
||||
const caribbean = lat > 17 && lat < 19 && lon > -68 && lon < -64; // Puerto Rico, USVI
|
||||
const pacific = lat > 13 && lat < 21 && lon > 144 && lon < 146; // Guam, Northern Marianas
|
||||
return mainland || alaska || hawaii || caribbean || pacific;
|
||||
}
|
||||
|
||||
let warnedMissingContact = false;
|
||||
|
||||
function buildNwsUserAgent(): string {
|
||||
const explicit = process.env.NWS_CONTACT?.trim();
|
||||
if (explicit) return `WayfarerWeather/1.0 (${explicit})`;
|
||||
|
||||
const deployedHost = process.env.VERCEL_PROJECT_PRODUCTION_URL?.trim() || process.env.VERCEL_URL?.trim();
|
||||
if (deployedHost) return `WayfarerWeather/1.0 (https://${deployedHost})`;
|
||||
|
||||
if (process.env.NODE_ENV === "production" && !warnedMissingContact) {
|
||||
warnedMissingContact = true;
|
||||
console.warn("[Wayfarer] Set NWS_CONTACT (email or site URL) so api.weather.gov requests include contact details.");
|
||||
}
|
||||
return "WayfarerWeather/1.0 (local-dev)";
|
||||
}
|
||||
|
||||
export async function getNwsAlerts(lat: number, lon: number): Promise<WeatherAlert[]> {
|
||||
if (!mightBeInUS(lat, lon)) return [];
|
||||
|
||||
const point = `${lat.toFixed(4)},${lon.toFixed(4)}`;
|
||||
const data = await fetchJson<NwsResponse>(`https://api.weather.gov/alerts/active?point=${point}`, "National Weather Service", {
|
||||
cacheSeconds: 300, // alerts matter; refresh more often
|
||||
headers: { "User-Agent": buildNwsUserAgent(), Accept: "application/geo+json" },
|
||||
});
|
||||
|
||||
return data.features.map(({ properties: p }) => ({
|
||||
title: p.headline ?? p.event,
|
||||
severity: toSeverity(p.severity),
|
||||
source: p.senderName,
|
||||
starts: p.onset ?? p.effective,
|
||||
ends: p.ends ?? p.expires,
|
||||
description: p.description,
|
||||
instruction: p.instruction,
|
||||
}));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue