Add NHC forecast track to hurricane click-to-map
Publish Container / publish (push) Successful in 2m6s
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:
1 parent
8b9d3639af
commit
3af4c1df4b
2 files changed
+130
-20
No files matched your search
+56
-11
@@ -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 {}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user