feat(storm-intel): richer event data + NWS boundary label in drawer and map tooltip
useStormEvents: - Extract windSpeed (windtag), expireTime, warningDurationMins, phenomenaCode/Label - Calculate polygonAreaSqMi via shoelace formula; estimate homes at 1800/sq mi StormDetailDrawer — new sections: - Weather Metrics: hail size with damage descriptor, wind speed with severity label - Impact Zone: warning area sq miles + estimated homes in zone - Warning Timeline: issued time, expired time, duration, issuing NWS office - NWS Boundary note: explains the shape is a meteorologist-drawn warning zone Map tooltip: - Shows phenomenaLabel, wind speed, area sq mi - Footer section labeled "NWS Warning Boundary" with shape explanation
This commit is contained in:
@@ -60,6 +60,32 @@ function derivedAreaName(rawCoords, ph) {
|
|||||||
return `${city} — ${label}`;
|
return `${city} — ${label}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shoelace formula — polygon is Leaflet [lat,lon] pairs
|
||||||
|
function polygonAreaSqMi(polygon) {
|
||||||
|
const n = polygon.length;
|
||||||
|
if (n < 3) return 0;
|
||||||
|
const avgLat = polygon.reduce((s, p) => s + p[0], 0) / n;
|
||||||
|
const mLat = 69.0;
|
||||||
|
const mLon = 69.0 * Math.cos(avgLat * Math.PI / 180);
|
||||||
|
let area = 0;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const [lat1, lon1] = polygon[i];
|
||||||
|
const [lat2, lon2] = polygon[(i + 1) % n];
|
||||||
|
area += (lon1 * mLon) * (lat2 * mLat) - (lon2 * mLon) * (lat1 * mLat);
|
||||||
|
}
|
||||||
|
return Math.abs(area) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PH_LABEL = {
|
||||||
|
SV: 'Severe Thunderstorm Warning',
|
||||||
|
TO: 'Tornado Warning',
|
||||||
|
FF: 'Flash Flood Warning',
|
||||||
|
WS: 'Winter Storm Warning',
|
||||||
|
BZ: 'Blizzard Warning',
|
||||||
|
WW: 'Winter Weather Advisory',
|
||||||
|
IS: 'Ice Storm Warning',
|
||||||
|
};
|
||||||
|
|
||||||
function featureToEvent(feature, idx) {
|
function featureToEvent(feature, idx) {
|
||||||
const p = feature.properties || {};
|
const p = feature.properties || {};
|
||||||
const ph = p.phenomena || '';
|
const ph = p.phenomena || '';
|
||||||
@@ -70,7 +96,18 @@ function featureToEvent(feature, idx) {
|
|||||||
|
|
||||||
const polygon = rawCoords.map(([lon, lat]) => [lat, lon]); // → Leaflet [lat,lon]
|
const polygon = rawCoords.map(([lon, lat]) => [lat, lon]); // → Leaflet [lat,lon]
|
||||||
|
|
||||||
const hailIn = p.hailtag ? parseFloat(p.hailtag) : null;
|
const hailIn = p.hailtag ? parseFloat(p.hailtag) : null;
|
||||||
|
const windMph = p.windtag ? parseInt(p.windtag, 10) : null;
|
||||||
|
const issueTime = p.issue || p.polygon_begin || null;
|
||||||
|
const expireTime = p.expire || p.polygon_end || null;
|
||||||
|
|
||||||
|
const warningDurationMins = (issueTime && expireTime)
|
||||||
|
? Math.round((new Date(expireTime) - new Date(issueTime)) / 60000)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const areaSqMi = Math.round(polygonAreaSqMi(polygon));
|
||||||
|
// Suburban DFW density ~1 800 homes/sq mi — conservative (polygons often include open land)
|
||||||
|
const estHomes = Math.round(areaSqMi * 1800);
|
||||||
|
|
||||||
const type = ph === 'TO' ? 'tornado'
|
const type = ph === 'TO' ? 'tornado'
|
||||||
: ph === 'FF' ? 'flood'
|
: ph === 'FF' ? 'flood'
|
||||||
@@ -78,6 +115,7 @@ function featureToEvent(feature, idx) {
|
|||||||
: ph === 'WW' || ph === 'IS' ? 'ice'
|
: ph === 'WW' || ph === 'IS' ? 'ice'
|
||||||
: hailIn !== null ? 'hail'
|
: hailIn !== null ? 'hail'
|
||||||
: 'wind';
|
: 'wind';
|
||||||
|
|
||||||
const severity = ph === 'TO' ? 'severe'
|
const severity = ph === 'TO' ? 'severe'
|
||||||
: hailIn !== null
|
: hailIn !== null
|
||||||
? (hailIn >= 2.0 ? 'severe' : hailIn >= 1.5 ? 'significant' : hailIn >= 1.0 ? 'moderate' : 'trace')
|
? (hailIn >= 2.0 ? 'severe' : hailIn >= 1.5 ? 'significant' : hailIn >= 1.0 ? 'moderate' : 'trace')
|
||||||
@@ -85,16 +123,23 @@ function featureToEvent(feature, idx) {
|
|||||||
: 'significant';
|
: 'significant';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `IEM-${WFO}-${ph}-${p.year ?? ''}-${p.eventid ?? idx}`,
|
id: `IEM-${WFO}-${ph}-${p.year ?? ''}-${p.eventid ?? idx}`,
|
||||||
date: p.issue || p.polygon_begin || new Date().toISOString(),
|
date: issueTime || new Date().toISOString(),
|
||||||
|
expireTime,
|
||||||
|
warningDurationMins,
|
||||||
type,
|
type,
|
||||||
severity,
|
severity,
|
||||||
maxHailSize: hailIn,
|
phenomenaCode: ph,
|
||||||
areaName: derivedAreaName(rawCoords, ph),
|
phenomenaLabel: PH_LABEL[ph] ?? ph,
|
||||||
county: 'Collin',
|
maxHailSize: hailIn,
|
||||||
estimatedHomes: null,
|
windSpeed: windMph,
|
||||||
description: `${type === 'tornado' ? 'Tornado Warning' : type === 'flood' ? 'Flash Flood Warning' : 'Severe Thunderstorm Warning'} — NWS ${WFO}`,
|
areaName: derivedAreaName(rawCoords, ph),
|
||||||
source: 'IEM/NWS',
|
county: 'Collin',
|
||||||
|
areaSqMi,
|
||||||
|
estimatedHomes: estHomes > 0 ? estHomes : null,
|
||||||
|
description: `${PH_LABEL[ph] ?? ph} — NWS ${WFO}`,
|
||||||
|
source: 'IEM/NWS',
|
||||||
|
wfo: WFO,
|
||||||
polygon,
|
polygon,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+132
-43
@@ -331,13 +331,13 @@ const StormMap = memo(({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip direction="top" opacity={1} className="storm-tooltip" sticky>
|
<Tooltip direction="top" opacity={1} className="storm-tooltip" sticky>
|
||||||
<div style={{ fontFamily: 'system-ui,sans-serif', minWidth: 160 }}>
|
<div style={{ fontFamily: 'system-ui,sans-serif', minWidth: 190, maxWidth: 230 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 5, marginBottom: 4 }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||||
<span style={{ fontSize: 10, fontWeight: 800, color: sty.dot, textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
<span style={{ fontSize: 10, fontWeight: 800, color: sty.dot, textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||||||
{sty.label} Hail
|
{sty.label} · {storm.phenomenaLabel ?? storm.type}
|
||||||
</span>
|
</span>
|
||||||
{storm.maxHailSize && (
|
{storm.maxHailSize && (
|
||||||
<span style={{ fontSize: 10, fontWeight: 700, color: '#6B7280' }}>
|
<span style={{ fontSize: 10, fontWeight: 700, color: '#6B7280', background: '#F4F4F5', padding: '1px 5px', borderRadius: 4 }}>
|
||||||
{storm.maxHailSize}"
|
{storm.maxHailSize}"
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -345,9 +345,19 @@ const StormMap = memo(({
|
|||||||
<p style={{ fontSize: 12, fontWeight: 700, color: '#18181b', marginBottom: 2 }}>
|
<p style={{ fontSize: 12, fontWeight: 700, color: '#18181b', marginBottom: 2 }}>
|
||||||
{storm.areaName}
|
{storm.areaName}
|
||||||
</p>
|
</p>
|
||||||
<p style={{ fontSize: 10, color: '#71717a' }}>
|
<p style={{ fontSize: 10, color: '#71717a', marginBottom: 6 }}>
|
||||||
{Math.floor((Date.now() - new Date(storm.date).getTime()) / 86400000)}d ago · ~{(storm.estimatedHomes ?? '?').toLocaleString()} homes
|
{Math.floor((Date.now() - new Date(storm.date).getTime()) / 86400000)}d ago
|
||||||
|
{storm.windSpeed ? ` · ${storm.windSpeed} mph` : ''}
|
||||||
|
{storm.areaSqMi ? ` · ${storm.areaSqMi} sq mi` : ''}
|
||||||
</p>
|
</p>
|
||||||
|
<div style={{ borderTop: '1px solid #E4E4E7', paddingTop: 5 }}>
|
||||||
|
<p style={{ fontSize: 9, fontWeight: 700, color: '#9CA3AF', textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 2 }}>
|
||||||
|
NWS Warning Boundary
|
||||||
|
</p>
|
||||||
|
<p style={{ fontSize: 9, color: '#A1A1AA', lineHeight: 1.4 }}>
|
||||||
|
Official zone drawn by NWS meteorologist — covers the projected storm track, not the exact cloud outline.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Polygon>
|
</Polygon>
|
||||||
@@ -700,61 +710,140 @@ const StormDetailDrawer = ({ storm, open, onClose, onFlyTo, onAssignZone, canMan
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto pl-5 pr-4 py-4 space-y-4">
|
<div className="flex-1 overflow-y-auto pl-5 pr-4 py-4 space-y-5">
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-black text-zinc-900 dark:text-white leading-tight">
|
<h2 className="text-lg font-black text-zinc-900 dark:text-white leading-tight">{storm.areaName}</h2>
|
||||||
{storm.areaName}
|
|
||||||
</h2>
|
|
||||||
<p className="text-[12px] text-zinc-500 dark:text-zinc-400 mt-0.5">{storm.county} County, TX</p>
|
<p className="text-[12px] text-zinc-500 dark:text-zinc-400 mt-0.5">{storm.county} County, TX</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`flex items-center gap-2 px-3 py-2 rounded-xl border ${conv.bg} ${conv.border}`}>
|
{/* Canvassing window */}
|
||||||
{conv.label === 'Hot Zone' && <Flame size={14} className={conv.color} />}
|
<div className={`flex items-start gap-2.5 px-3 py-2.5 rounded-xl border ${conv.bg} ${conv.border}`}>
|
||||||
{conv.label === 'Warm' && <TrendingUp size={14} className={conv.color} />}
|
<div className="mt-0.5 shrink-0">
|
||||||
{conv.label === 'Cold' && <Snowflake size={14} className={conv.color} />}
|
{conv.label === 'Hot Zone' && <Flame size={15} className={conv.color} />}
|
||||||
{conv.label === 'Too Fresh' && <Clock size={14} className={conv.color} />}
|
{conv.label === 'Warm' && <TrendingUp size={15} className={conv.color} />}
|
||||||
|
{conv.label === 'Cold' && <Snowflake size={15} className={conv.color} />}
|
||||||
|
{conv.label === 'Too Fresh' && <Clock size={15} className={conv.color} />}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className={`text-[12px] font-bold ${conv.color}`}>{conv.label}</p>
|
<p className={`text-[12px] font-bold ${conv.color}`}>{conv.label}</p>
|
||||||
<p className="text-[10px] text-zinc-500 dark:text-zinc-400">
|
<p className="text-[11px] text-zinc-500 dark:text-zinc-400 leading-relaxed mt-0.5">
|
||||||
{conv.label === 'Hot Zone' && 'Peak canvassing window — highest conversion likelihood'}
|
{conv.label === 'Hot Zone' && 'Peak canvassing window. Insurance claims actively being filed — homeowners are receptive. Lead with free inspection.'}
|
||||||
{conv.label === 'Warm' && 'Still worth canvassing — moderate conversion rate'}
|
{conv.label === 'Warm' && 'Claims in process. Many have had adjuster visits. Lead with supplement expertise and contractor credentials.'}
|
||||||
{conv.label === 'Cold' && 'Low conversion — storm damage assessment mostly done'}
|
{conv.label === 'Cold' && 'Most claims settled. Final window before statute expires. Lead with "late claim" and supplemental services.'}
|
||||||
{conv.label === 'Too Fresh' && 'Too recent — wait for claims process to begin'}
|
{conv.label === 'Too Fresh' && 'Adjuster process hasn\'t started. Build rapport, collect contacts, follow up in 2–3 weeks when claims open.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
{/* Weather metrics */}
|
||||||
{storm.maxHailSize && (
|
<div>
|
||||||
|
<p className="text-[10px] font-black uppercase tracking-wider text-zinc-400 mb-2">Weather Metrics</p>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="rounded-xl bg-zinc-50 dark:bg-zinc-800/60 border border-zinc-100 dark:border-white/[0.06] p-3">
|
<div className="rounded-xl bg-zinc-50 dark:bg-zinc-800/60 border border-zinc-100 dark:border-white/[0.06] p-3">
|
||||||
<p className="text-[10px] font-bold uppercase tracking-wider text-zinc-400 mb-1">Max Hail</p>
|
<p className="text-[10px] font-bold uppercase tracking-wider text-zinc-400 mb-1">Hail Size</p>
|
||||||
<p className="text-xl font-black" style={{ color: sty.dot }}>{storm.maxHailSize}"</p>
|
{storm.maxHailSize ? (
|
||||||
<p className="text-[10px] text-zinc-400">{sty.label} category</p>
|
<>
|
||||||
|
<p className="text-xl font-black" style={{ color: sty.dot }}>{storm.maxHailSize}"</p>
|
||||||
|
<p className="text-[10px] text-zinc-400">
|
||||||
|
{storm.maxHailSize >= 2.0 ? 'Golf ball — major roof damage' :
|
||||||
|
storm.maxHailSize >= 1.5 ? 'Ping pong — significant bruising' :
|
||||||
|
storm.maxHailSize >= 1.0 ? 'Quarter — moderate impact' :
|
||||||
|
'Dime — cosmetic impact'}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-xl font-black text-zinc-400">—</p>
|
||||||
|
<p className="text-[10px] text-zinc-400">not reported</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{storm.estimatedHomes && (
|
|
||||||
<div className="rounded-xl bg-zinc-50 dark:bg-zinc-800/60 border border-zinc-100 dark:border-white/[0.06] p-3">
|
<div className="rounded-xl bg-zinc-50 dark:bg-zinc-800/60 border border-zinc-100 dark:border-white/[0.06] p-3">
|
||||||
<p className="text-[10px] font-bold uppercase tracking-wider text-zinc-400 mb-1">Est. Homes</p>
|
<p className="text-[10px] font-bold uppercase tracking-wider text-zinc-400 mb-1">Wind Speed</p>
|
||||||
<p className="text-xl font-black text-zinc-900 dark:text-white">{storm.estimatedHomes.toLocaleString()}</p>
|
{storm.windSpeed ? (
|
||||||
<p className="text-[10px] text-zinc-400">in impact zone</p>
|
<>
|
||||||
|
<p className="text-xl font-black text-zinc-900 dark:text-white">{storm.windSpeed}</p>
|
||||||
|
<p className="text-[10px] text-zinc-400">
|
||||||
|
mph — {storm.windSpeed >= 65 ? 'destructive' : storm.windSpeed >= 58 ? 'significant damage' : 'moderate damage'}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-xl font-black text-zinc-400">—</p>
|
||||||
|
<p className="text-[10px] text-zinc-400">not reported</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl bg-zinc-50 dark:bg-zinc-800/60 border border-zinc-100 dark:border-white/[0.06] p-3">
|
||||||
|
<p className="text-[10px] font-bold uppercase tracking-wider text-zinc-400 mb-1">Days Ago</p>
|
||||||
|
<p className="text-xl font-black text-zinc-900 dark:text-white">{days}</p>
|
||||||
|
<p className="text-[10px] text-zinc-400">since impact</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl bg-zinc-50 dark:bg-zinc-800/60 border border-zinc-100 dark:border-white/[0.06] p-3">
|
||||||
|
<p className="text-[10px] font-bold uppercase tracking-wider text-zinc-400 mb-1">Severity</p>
|
||||||
|
<p className="text-xl font-black" style={{ color: sty.dot }}>{sty.label}</p>
|
||||||
|
<p className="text-[10px] text-zinc-400">{storm.phenomenaCode} warning</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className="rounded-xl bg-zinc-50 dark:bg-zinc-800/60 border border-zinc-100 dark:border-white/[0.06] p-3">
|
|
||||||
<p className="text-[10px] font-bold uppercase tracking-wider text-zinc-400 mb-1">Days Ago</p>
|
|
||||||
<p className="text-xl font-black text-zinc-900 dark:text-white">{days}</p>
|
|
||||||
<p className="text-[10px] text-zinc-400">since impact</p>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-xl bg-zinc-50 dark:bg-zinc-800/60 border border-zinc-100 dark:border-white/[0.06] p-3">
|
|
||||||
<p className="text-[10px] font-bold uppercase tracking-wider text-zinc-400 mb-1">Source</p>
|
|
||||||
<p className="text-base font-black text-zinc-900 dark:text-white">{storm.source}</p>
|
|
||||||
<p className="text-[10px] text-zinc-400">data origin</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Impact zone */}
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[11px] font-bold uppercase tracking-wider text-zinc-400 mb-1.5">Intel</p>
|
<p className="text-[10px] font-black uppercase tracking-wider text-zinc-400 mb-2">Impact Zone</p>
|
||||||
<p className="text-[13px] text-zinc-600 dark:text-zinc-300 leading-relaxed">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{storm.description}
|
{storm.areaSqMi > 0 && (
|
||||||
|
<div className="rounded-xl bg-zinc-50 dark:bg-zinc-800/60 border border-zinc-100 dark:border-white/[0.06] p-3">
|
||||||
|
<p className="text-[10px] font-bold uppercase tracking-wider text-zinc-400 mb-1">Warning Area</p>
|
||||||
|
<p className="text-xl font-black text-zinc-900 dark:text-white">{storm.areaSqMi.toLocaleString()}</p>
|
||||||
|
<p className="text-[10px] text-zinc-400">sq miles covered</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{storm.estimatedHomes > 0 && (
|
||||||
|
<div className="rounded-xl bg-zinc-50 dark:bg-zinc-800/60 border border-zinc-100 dark:border-white/[0.06] p-3">
|
||||||
|
<p className="text-[10px] font-bold uppercase tracking-wider text-zinc-400 mb-1">Est. Homes</p>
|
||||||
|
<p className="text-xl font-black text-zinc-900 dark:text-white">{storm.estimatedHomes.toLocaleString()}</p>
|
||||||
|
<p className="text-[10px] text-zinc-400">in warning zone</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Warning timeline */}
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] font-black uppercase tracking-wider text-zinc-400 mb-2">Warning Timeline</p>
|
||||||
|
<div className="rounded-xl bg-zinc-50 dark:bg-zinc-800/60 border border-zinc-100 dark:border-white/[0.06] divide-y divide-zinc-100 dark:divide-white/[0.05]">
|
||||||
|
<div className="flex items-center justify-between px-3 py-2">
|
||||||
|
<span className="text-[11px] text-zinc-500 dark:text-zinc-400">Issued</span>
|
||||||
|
<span className="text-[11px] font-semibold text-zinc-700 dark:text-zinc-200">
|
||||||
|
{storm.date ? new Date(storm.date).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between px-3 py-2">
|
||||||
|
<span className="text-[11px] text-zinc-500 dark:text-zinc-400">Expired</span>
|
||||||
|
<span className="text-[11px] font-semibold text-zinc-700 dark:text-zinc-200">
|
||||||
|
{storm.expireTime ? new Date(storm.expireTime).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between px-3 py-2">
|
||||||
|
<span className="text-[11px] text-zinc-500 dark:text-zinc-400">Duration</span>
|
||||||
|
<span className="text-[11px] font-semibold text-zinc-700 dark:text-zinc-200">
|
||||||
|
{storm.warningDurationMins ? `${storm.warningDurationMins} min` : '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between px-3 py-2">
|
||||||
|
<span className="text-[11px] text-zinc-500 dark:text-zinc-400">Issuing Office</span>
|
||||||
|
<span className="text-[11px] font-semibold text-zinc-700 dark:text-zinc-200">NWS {storm.wfo ?? 'FWD'} — Fort Worth</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* NWS boundary note */}
|
||||||
|
<div className="rounded-xl border border-zinc-200 dark:border-white/[0.07] bg-zinc-50 dark:bg-zinc-800/40 px-3 py-2.5">
|
||||||
|
<p className="text-[10px] font-black uppercase tracking-wider text-zinc-400 mb-1">NWS Warning Boundary</p>
|
||||||
|
<p className="text-[11px] text-zinc-500 dark:text-zinc-400 leading-relaxed">
|
||||||
|
The polygon on the map is the official NWS warning zone — drawn by a meteorologist to cover the storm's projected track and affected area, not the exact cloud outline. Shape is typical for the storm's direction of travel.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user