/** * IEM Local Storm Reports (LSR) hook. * Fetches spotter-verified ground truth reports from the same provider as SBW. * CORS-open, no auth needed. */ import { useState, useEffect } from 'react'; const LSR_URL = 'https://mesonet.agron.iastate.edu/geojson/lsr.geojson'; const WFO = 'FWD'; const KEEP_TYPES = new Set(['H', 'G', 'W', 'T', 'F', 'D', '4']); const TYPE_TEXT = { H: 'HAIL', '4': 'HAIL', G: 'WIND GUST', W: 'HIGH WIND', T: 'TORNADO', F: 'FLASH FLOOD', D: 'STRUCTURAL DAMAGE', }; // Plano / Collin County bounding box const BBOX = { minLat: 32.85, maxLat: 33.45, minLon: -97.15, maxLon: -96.35 }; function inBBox(lat, lon) { return lat >= BBOX.minLat && lat <= BBOX.maxLat && lon >= BBOX.minLon && lon <= BBOX.maxLon; } export function useStormReports() { const [reports, setReports] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { let cancelled = false; const load = async () => { setLoading(true); try { const now = new Date(); const cutoff = new Date(now.getTime() - 36 * 30.44 * 86400000); const sts = encodeURIComponent(cutoff.toISOString()); const ets = encodeURIComponent(now.toISOString()); const url = `${LSR_URL}?sts=${sts}&ets=${ets}&wfo=${WFO}`; const res = await fetch(url); if (!res.ok) throw new Error(`LSR ${res.status}`); const geojson = await res.json(); const parsed = (geojson.features || []) .map(feature => { const p = feature.properties || {}; const type = p.typetext?.charAt(0)?.toUpperCase() || p.type || ''; // Some feeds use numeric string type code instead const typeKey = KEEP_TYPES.has(type) ? type : KEEP_TYPES.has(p.type) ? p.type : null; if (!typeKey) return null; const coords = feature.geometry?.coordinates; if (!coords || coords.length < 2) return null; const [lon, lat] = coords; if (!inBBox(lat, lon)) return null; const valid = p.valid || p.utc_valid || null; const magnitude = (p.magnitude != null && p.magnitude !== '') ? parseFloat(p.magnitude) : null; const city = p.city || p.st || ''; const county = p.county || ''; const source = p.source || p.remark_source || ''; const remark = p.remark || ''; return { id: `LSR-${typeKey}-${valid}-${Math.round(lat * 1000)}-${Math.round(lon * 1000)}`, lat, lon, type: typeKey, typetext: TYPE_TEXT[typeKey] ?? typeKey, magnitude: isNaN(magnitude) ? null : magnitude, city, county, source, remark, valid, }; }) .filter(Boolean) .sort((a, b) => new Date(b.valid) - new Date(a.valid)) .slice(0, 500); if (!cancelled) setReports(parsed); } catch { // LSR unreachable — silently show empty if (!cancelled) setReports([]); } finally { if (!cancelled) setLoading(false); } }; load(); return () => { cancelled = true; }; }, []); return { reports, loading }; }