Add NHC forecast track to hurricane click-to-map
Publish Container / publish (push) Successful in 2m6s

- Parse NHC forecast advisory text (FORECAST VALID / OUTLOOK VALID lines)
  to extract real forecast positions at 9–117h intervals with wind speeds
- Render forecast track as dashed line + clickable dots (wind on click)
- Auto-fit map bounds to show the full 5-day track after loading
- Fix buildStyle to use OpenFreeMap vector tile URLs without API key

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Nate EmmertandClaude Sonnet 4.6 committed 2026-09-02 10:11:22 -04:00
1 parent 8b9d3639af
commit 3af4c1df4b
2 files changed
+130 -20

No files matched your search

+56 -11
View File
@@ -127,7 +127,7 @@ async function flyToHurricane(storm) {
if (!map) return;
await renderHurricaneOverlay(map);
map.flyTo({ center: [lon, lat], zoom: 5, duration: 900 });
map.flyTo({ center: [lon, lat], zoom: 4, duration: 900 });
setTimeout(() => {
const cat = (storm.classification || '').toUpperCase();
@@ -140,17 +140,62 @@ async function flyToHurricane(storm) {
if (storm.id) {
try {
const trackData = await fetch(`/api/hurricanes/track?stormId=${encodeURIComponent(storm.id)}`).then(r => r.json());
if (trackData?.type === 'FeatureCollection' && trackData.features?.length) {
['hurricane-track'].forEach(id => {
if (map.getLayer(id)) map.removeLayer(id);
if (map.getSource(id)) map.removeSource(id);
});
map.addSource('hurricane-track', { type: 'geojson', data: trackData });
map.addLayer({
id: 'hurricane-track', type: 'line', source: 'hurricane-track',
paint: { 'line-color': '#ff5722', 'line-width': 2.5, 'line-dasharray': [4, 2] },
const { lin, pts, pgn } = await fetch(`/api/hurricanes/track?stormId=${encodeURIComponent(storm.id)}`).then(r => r.json());
['hurricane-track-fill', 'hurricane-track-line', 'hurricane-track-pts'].forEach(id => {
if (map.getLayer(id)) map.removeLayer(id);
});
['hurricane-track-pgn', 'hurricane-track-lin', 'hurricane-track-pts-src'].forEach(id => {
if (map.getSource(id)) map.removeSource(id);
});
if (pgn?.type === 'FeatureCollection' && pgn.features?.length) {
map.addSource('hurricane-track-pgn', { type: 'geojson', data: pgn });
map.addLayer({ id: 'hurricane-track-fill', type: 'fill', source: 'hurricane-track-pgn',
paint: { 'fill-color': '#ff5722', 'fill-opacity': 0.12 } });
}
if (lin?.type === 'FeatureCollection' && lin.features?.length) {
map.addSource('hurricane-track-lin', { type: 'geojson', data: lin });
map.addLayer({ id: 'hurricane-track-line', type: 'line', source: 'hurricane-track-lin',
paint: { 'line-color': '#ff5722', 'line-width': 2.5, 'line-dasharray': [4, 2] } });
}
if (pts?.type === 'FeatureCollection' && pts.features?.length) {
map.addSource('hurricane-track-pts-src', { type: 'geojson', data: pts });
map.addLayer({ id: 'hurricane-track-pts', type: 'circle', source: 'hurricane-track-pts-src',
paint: {
'circle-radius': 7,
'circle-color': ['match', ['get', 'TCDVLP'],
'Hurricane', '#f44336', 'HU', '#f44336',
'Tropical Storm', '#9c27b0', 'TS', '#9c27b0',
'#2196f3'],
'circle-stroke-width': 2, 'circle-stroke-color': '#fff',
} });
// Fit map to show full track
const allCoords = pts.features.map(f => f.geometry.coordinates);
if (allCoords.length > 1) {
const lons = allCoords.map(c => c[0]);
const lats = allCoords.map(c => c[1]);
map.fitBounds(
[[Math.min(...lons), Math.min(...lats)], [Math.max(...lons), Math.max(...lats)]],
{ padding: 60, duration: 600, maxZoom: 5 }
);
}
map.on('click', 'hurricane-track-pts', e => {
const p = e.features[0].properties;
const tau = p.TAU != null ? `+${p.TAU}h` : '';
const knots = p.MAXWIND || p.INTENSITY;
const wind = knots ? `${Math.round(knots * 1.15)} mph` : '--';
const type = p.TCDVLP || p.STORMTYPE || '';
new maplibregl.Popup().setLngLat(e.lngLat)
.setHTML(`<b>${tau}</b> ${type}<br>Winds: ${wind}${p.MSLP ? '<br>Pressure: ' + p.MSLP + ' mb' : ''}`)
.addTo(map);
});
map.on('mouseenter', 'hurricane-track-pts', () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'hurricane-track-pts', () => { map.getCanvas().style.cursor = ''; });
}
} catch {}
}