* Add zoom-aware symbology, fix globe popovers, expand geo coverage Map rendering improvements based on user feedback: - Globe markers now scale with camera altitude (onZoom hook) - Priority-based visibility culls low-priority markers at world view - Globe popovers use getScreenCoords for accurate positioning - Flat map labels hidden at low zoom, revealed progressively - Default globe altitude lowered from 2.5 to 1.8 for better fill - Americas region zoom tightened to CONUS focus Geographic coverage expansion: - 4 new OpenSky air theaters: Caribbean, Gulf of Guinea, Cape Route, Horn of Africa - Flight corridors now span Americas and Africa - NOAA alerts extract centroid lat/lon from GeoJSON geometry - EPA RadNet stations geocoded with hardcoded coords for 10 US cities - ISS + Tiangong positions estimated from TLE orbital elements - GDELT geoEvents() now called in briefing for mapped event points - New legend entries: Weather Alert, EPA RadNet, Space Station, GDELT Event * Fix null-safe coordinate checks and remove injected data blob - Use `!= null` instead of truthy checks for lat/lon in noaa.mjs and inject.mjs so valid 0-coordinates (equator/prime meridian) are not silently dropped - Reset jarvis.html `let D` back to null placeholder so generated runtime data is not part of the PR diff * Remove re-injected data blob from jarvis.html Reset let D back to null — previous commit was correct but inject.mjs build verification re-injected the payload.
94 lines
3.4 KiB
JavaScript
94 lines
3.4 KiB
JavaScript
// NOAA / National Weather Service — Severe weather alerts & climate events
|
|
// No auth required. Real-time alerts.
|
|
|
|
import { safeFetch } from '../utils/fetch.mjs';
|
|
|
|
const NWS_BASE = 'https://api.weather.gov';
|
|
|
|
// Get all active weather alerts (US)
|
|
export async function getActiveAlerts(opts = {}) {
|
|
const {
|
|
severity = null, // Extreme, Severe, Moderate, Minor
|
|
urgency = null, // Immediate, Expected, Future
|
|
event = null, // e.g. "Tornado Warning", "Hurricane Warning"
|
|
limit = 50,
|
|
} = opts;
|
|
|
|
const params = new URLSearchParams({ limit: String(limit), status: 'actual' });
|
|
if (severity) params.set('severity', severity);
|
|
if (urgency) params.set('urgency', urgency);
|
|
if (event) params.set('event', event);
|
|
|
|
return safeFetch(`${NWS_BASE}/alerts/active?${params}`, {
|
|
headers: { 'Accept': 'application/geo+json' },
|
|
});
|
|
}
|
|
|
|
// Get severe alerts only
|
|
export async function getSevereAlerts() {
|
|
return getActiveAlerts({ severity: 'Extreme,Severe' });
|
|
}
|
|
|
|
// Briefing — severe weather events that could impact markets/supply chains
|
|
export async function briefing() {
|
|
const alerts = await getSevereAlerts();
|
|
const features = alerts?.features || [];
|
|
|
|
// Categorize by impact type
|
|
const hurricanes = features.filter(f => /hurricane|typhoon|tropical/i.test(f.properties?.event));
|
|
const tornadoes = features.filter(f => /tornado/i.test(f.properties?.event));
|
|
const floods = features.filter(f => /flood/i.test(f.properties?.event));
|
|
const winter = features.filter(f => /blizzard|ice storm|winter/i.test(f.properties?.event));
|
|
const fire = features.filter(f => /fire/i.test(f.properties?.event));
|
|
const other = features.filter(f => {
|
|
const e = f.properties?.event || '';
|
|
return !/hurricane|typhoon|tropical|tornado|flood|blizzard|ice storm|winter|fire/i.test(e);
|
|
});
|
|
|
|
return {
|
|
source: 'NOAA/NWS',
|
|
timestamp: new Date().toISOString(),
|
|
totalSevereAlerts: features.length,
|
|
summary: {
|
|
hurricanes: hurricanes.length,
|
|
tornadoes: tornadoes.length,
|
|
floods: floods.length,
|
|
winterStorms: winter.length,
|
|
wildfires: fire.length,
|
|
other: other.length,
|
|
},
|
|
topAlerts: features.slice(0, 15).map(f => {
|
|
// Extract centroid from GeoJSON geometry
|
|
let lat = null, lon = null;
|
|
const geo = f.geometry;
|
|
if (geo?.type === 'Polygon' && geo.coordinates?.[0]?.length) {
|
|
const coords = geo.coordinates[0];
|
|
lat = coords.reduce((s, c) => s + c[1], 0) / coords.length;
|
|
lon = coords.reduce((s, c) => s + c[0], 0) / coords.length;
|
|
} else if (geo?.type === 'MultiPolygon' && geo.coordinates?.length) {
|
|
const coords = geo.coordinates[0][0];
|
|
lat = coords.reduce((s, c) => s + c[1], 0) / coords.length;
|
|
lon = coords.reduce((s, c) => s + c[0], 0) / coords.length;
|
|
} else if (geo?.type === 'Point') {
|
|
[lon, lat] = geo.coordinates;
|
|
}
|
|
return {
|
|
event: f.properties?.event,
|
|
severity: f.properties?.severity,
|
|
urgency: f.properties?.urgency,
|
|
headline: f.properties?.headline,
|
|
areas: f.properties?.areaDesc,
|
|
onset: f.properties?.onset,
|
|
expires: f.properties?.expires,
|
|
lat: lat != null ? +lat.toFixed(3) : null,
|
|
lon: lon != null ? +lon.toFixed(3) : null,
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
|
|
if (process.argv[1]?.endsWith('noaa.mjs')) {
|
|
const data = await briefing();
|
|
console.log(JSON.stringify(data, null, 2));
|
|
}
|