/** * Fetches historical NWS storm-based warnings (SVR + TOR) from the Iowa * Environmental Mesonet (IEM) for WFO FWD (Fort Worth), which covers * Collin County and the Plano TX metro area. * * Correct endpoint: /geojson/sbw.geojson with sts/ets timestamp params. * Geometry is MultiPolygon; date field is "issue"; phenomena filtered server-side. * * No API key required — IEM is a free public service (Iowa State University). * Cached 12 hours on Vercel edge. */ const IEM_BASE = 'https://mesonet.agron.iastate.edu/geojson/sbw.geojson'; const WFO = 'FWD'; // Fort Worth NWS office — covers Collin County / Plano TX // Generous bounding box: Plano, Allen, McKinney, Frisco, Richardson const BBOX = { minLat: 32.85, maxLat: 33.45, minLon: -97.15, maxLon: -96.35 }; const IEM_HEADERS = { 'User-Agent': 'LynkedUpPro-CRM/1.0 (admin@lynkedup.com)', Accept: 'application/geo+json', }; // Only keep storm-relevant phenomena const KEEP_PHENOMENA = new Set(['SV', 'TO', 'FF']); function polygonInBBox(rawCoords) { return rawCoords.some(([lon, lat]) => lat >= BBOX.minLat && lat <= BBOX.maxLat && lon >= BBOX.minLon && lon <= BBOX.maxLon ); } function extractRing(geom) { if (!geom) return null; if (geom.type === 'Polygon' && geom.coordinates?.[0]?.length >= 3) return geom.coordinates[0]; if (geom.type === 'MultiPolygon' && geom.coordinates?.[0]?.[0]?.length >= 3) return geom.coordinates[0][0]; // first ring of first polygon return null; } function derivedAreaName(rawCoords, ph) { const n = rawCoords.length; const centerLat = rawCoords.reduce((s, [, lat]) => s + lat, 0) / n; const centerLon = rawCoords.reduce((s, [lon]) => s + lon, 0) / n; let city; if (centerLat > 33.22) city = 'McKinney'; else if (centerLat > 33.16 && centerLon < -96.79) city = 'Frisco'; else if (centerLat > 33.16) city = 'N. Plano / Allen'; else if (centerLat > 33.10) city = 'Allen'; else if (centerLon < -96.84) city = 'Legacy Corridor'; else if (centerLat > 33.04) city = 'Central Plano'; else city = 'S. Plano'; const label = ph === 'TO' ? 'Tornado Warning' : ph === 'FF' ? 'Flash Flood Warning' : 'Severe Thunderstorm'; return `${city} — ${label}`; } function toStormEvent(feature, idx) { const p = feature.properties || {}; const ph = p.phenomena || ''; if (!KEEP_PHENOMENA.has(ph)) return null; const rawCoords = extractRing(feature.geometry); if (!rawCoords || !polygonInBBox(rawCoords)) return null; // GeoJSON coords are [lon, lat]; Leaflet needs [lat, lon] const polygon = rawCoords.map(([lon, lat]) => [lat, lon]); const type = ph === 'TO' ? 'tornado' : ph === 'FF' ? 'flood' : 'hail'; // hailtag stores hail size in inches (e.g. "1.75") — present on SVR warnings const hailIn = p.hailtag ? parseFloat(p.hailtag) : null; let severity; if (ph === 'TO') { severity = 'severe'; } else if (hailIn !== null) { severity = hailIn >= 2.0 ? 'severe' : hailIn >= 1.5 ? 'significant' : hailIn >= 1.0 ? 'moderate' : 'trace'; } else if (ph === 'FF') { severity = 'moderate'; } else { severity = 'significant'; // SVR without hail tag } const date = p.issue || p.polygon_begin || new Date().toISOString(); return { id: `IEM-${WFO}-${ph}-${p.year ?? ''}-${p.eventid ?? idx}`, date, type, severity, maxHailSize: hailIn, areaName: derivedAreaName(rawCoords, ph), county: 'Collin', estimatedHomes: null, description: `${type === 'tornado' ? 'Tornado Warning' : type === 'flood' ? 'Flash Flood Warning' : 'Severe Thunderstorm Warning'} — NWS ${WFO}`, source: 'IEM/NWS', polygon, }; } export default async function handler(req, res) { res.setHeader('Cache-Control', 's-maxage=43200, stale-while-revalidate=86400'); res.setHeader('Access-Control-Allow-Origin', '*'); if (req.method === 'OPTIONS') return res.status(200).end(); if (req.method !== 'GET') return res.status(405).json({ error: 'Method not allowed' }); const months = Math.min(parseInt(req.query.months ?? '36', 10), 36); const now = new Date(); const cutoff = new Date(now.getTime() - months * 30.44 * 86400000); const sts = cutoff.toISOString(); const ets = now.toISOString(); const url = `${IEM_BASE}?sts=${encodeURIComponent(sts)}&ets=${encodeURIComponent(ets)}&wfo=${WFO}`; try { const response = await fetch(url, { headers: IEM_HEADERS }); if (!response.ok) throw new Error(`IEM ${response.status}`); const geojson = await response.json(); const features = Array.isArray(geojson.features) ? geojson.features : []; const events = features .map((f, i) => toStormEvent(f, i)) .filter(Boolean) .sort((a, b) => new Date(b.date) - new Date(a.date)) .slice(0, 150); return res.status(200).json(events); } catch (err) { console.error('[storm-polygons] error:', err.message); return res.status(200).json([]); } }