Skip to content

SPEC-P1: Extended Weather Fields & Current-Adjusted Routing#25

Merged
SL-Mar merged 15 commits intodevelopmentfrom
feat/extended-weather-fields
Feb 12, 2026
Merged

SPEC-P1: Extended Weather Fields & Current-Adjusted Routing#25
SL-Mar merged 15 commits intodevelopmentfrom
feat/extended-weather-fields

Conversation

@SL-Mar
Copy link
Owner

@SL-Mar SL-Mar commented Feb 12, 2026

Summary

  • Add SST, visibility, ice concentration, and swell decomposition to weather pipeline, both optimization engines (A* and VISIR Dijkstra), and frontend map overlays
  • Implement SST-corrected Holtrop-Mennen resistance via UNESCO 1983 density and Sharqawy 2010 viscosity correlations
  • Add COLREG Rule 6 tiered visibility speed caps, ice penalty zone (5% SIC = 2x cost), and swell fallback derivation (0.8/0.6 × total Hs)
  • New API endpoints (GET /api/weather/swell, SST piggyback on /api/weather/wind), extended per-leg route response fields
  • 4 new frontend map overlay layers (Ice, Visibility, SST, Swell) with canvas tile color scales and legends
  • 21 new unit tests covering all SPEC-P1 requirements (all passing)

Test plan

  • pytest tests/unit/test_spec_p1.py — 21/21 passing
  • pytest tests/unit/test_vessel_model.py — 15/16 passing (1 pre-existing failure unrelated to this PR)
  • next build — compiles successfully with no TypeScript errors
  • Manual: toggle each new overlay layer (Ice, Visibility, SST, Swell) on the map
  • Manual: run optimization with ice/visibility in the viewport and verify extended leg fields in response
  • Manual: verify swell endpoint returns decomposition data

🤖 Generated with Claude Code

SL-Mar and others added 15 commits February 12, 2026 11:11
Add SST, visibility, ice concentration, and swell decomposition to the
weather data pipeline, optimization engines, and frontend map overlays.

Backend:
- seawater_density (UNESCO 1983) and seawater_viscosity (Sharqawy 2010)
  feed SST-corrected resistance into Holtrop-Mennen model
- Tiered COLREG Rule 6 visibility speed caps (fog/poor/moderate)
- Ice penalty zone (5% = 2x cost) alongside existing 15% exclusion
- Swell decomposition fallback (0.8/0.6 × total Hs) in GridWeatherProvider
- GET /api/weather/swell endpoint, SST piggyback on /api/weather/wind
- Extended OptimizationLegModel with per-leg swell/ice/vis/sst fields
- CMEMS SST (thetao), ice (siconc), GFS visibility fetch + synthetic fallback

Frontend:
- 4 new map overlay buttons (Ice, Visibility, SST, Swell) with toggle
- Canvas tile color scales for each extended layer in WeatherGridLayer
- WeatherLegend supports all 7 layer types with correct gradient ranges
- API client methods and TypeScript interfaces for extended field data

Tests: 21 new unit tests covering all SPEC-P1 requirements (all passing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace NaN values from CMEMS/GFS with sensible defaults on the API side,
add client-side NaN transparency guard, fix visibility km→m unit mismatch,
clear stale layer data on switch, and add data source labels for new layers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch CMEMS_ICE_DATASET from the hourly physics dataset (PT1H-m) to the
daily one (P1D-m) which actually contains the siconc variable. The hourly
dataset caused a silent fallback to synthetic latitude-band ice data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ice layer now uses WMO/TD-No. 1215 6-stop ramp (blue→green→yellow→orange→red)
instead of 3-band cutoff at 15%, fixing flat red blob on Baltic 82% ice data.
SST adds near-freezing 2°C stop for Nordic cold-water differentiation. Visibility
and swell refactored to shared interpolateColorRamp() utility. Wind/wave unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ice layer now supports the same forecast timeline as wind, waves, and
currents. The CMEMS P1D-m dataset provides daily ice concentration
(siconc) forecasts for 10 days, fetched on demand and stored compressed
in PostgreSQL for persistence across restarts.

Backend: fetch_ice_forecast() in copernicus.py, ingest/retrieve via
weather_ingestion.py and db_weather_provider.py, three new API endpoints
(status/prefetch/frames) in main.py with file cache + DB rebuild.

Frontend: IceForecastFrames types in api.ts, ice mode in
ForecastTimeline with daily slider (Day 0-9), pass-through in
MapComponent, frame extraction callback in page.tsx.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of hardcoding forecast hours, the timeline now derives
available hours from the frame keys returned by the backend. This
ensures the slider range, labels, and play loop match the actual
forecast data in the database for all layers (wind, waves, currents,
ice).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…dcoded 10

CMEMS ice dataset may have fewer than 10 days of forecast data available.
The status endpoint was hardcoding total_hours=10, so when only 9 frames
existed the completion check (9 >= 10) never passed and the frontend
polling loop ran forever.

Now total_hours reflects the actual number of frames, and complete is
determined by whether the prefetch task has finished.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of calling Leaflet's redraw() which destroys all tiles and
recreates them (causing a visible flash), the WeatherGridLayer now
uses a refreshTiles() method that iterates existing tile canvases
and repaints their pixels in-place. DOM elements stay in the tree
so there is no blank frame between updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The wind forecast handler only updated particle velocity data but never
reconstructed WindFieldData for the heatmap grid layer. Now each
velocity frame is converted to a 2D u/v grid (preserving the ocean
mask from the initial load) so the heatmap colors change per hour,
matching the behavior of wave/ice forecast layers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: pygrib was missing from requirements.txt — GRIB files downloaded
fine but couldn't be parsed, so fetch_wind_data returned None for all frames.

Also fixes GFS 6-hour cycle rollover: when the latest GFS run changes between
prefetch and frame retrieval, endpoints now fall back to the best cached run
by scanning the GRIB cache directory. Frontend passes captured viewport bounds
to loadWindFrames to prevent bbox mismatch from map panning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All tile rendering (heatmap, wind arrows, wave crests) now happens on an
offscreen composite canvas. The visible tile only updates via a single
atomic drawImage call at the end, so old content stays visible while the
new frame renders — no more clear-then-repaint flash.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The composite approach created an extra 256x256 canvas per tile causing
freezes. Instead, simply move clearRect from the start of paintTile to
right before drawImage so old content stays visible during offscreen
rendering — same flicker fix without the overhead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wind frames endpoint now serves from a pre-built JSON file cache (~68ms)
instead of re-parsing 41 GRIB files on every request (~5s+). The cache
is built once at the end of the prefetch background task, matching the
pattern used by wave/current/ice forecast endpoints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The VelocityParticleLayer had data in its useEffect dependency array,
causing React's cleanup to destroy and recreate the entire leaflet-velocity
layer on every forecast frame change. The setData smooth-update path was
dead code — never reachable because cleanup always nulled the layer ref.

Split into two effects (matching WeatherGridLayer pattern):
- Effect 1: layer lifecycle on [type, zoom, hasData] — no data content dep
- Effect 2: smooth data update on [data] — calls setData() on existing layer

Also replace broken clearRect suppression with persistent snapshot bridge:
snapshot is created once on first setData call, kept visible during rapid
scrubbing (debounced removal), faded out 350ms after last call.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- page.tsx: Cache WindFieldData per (GFS run, hour) to avoid O(ny*nx)
  array reconstruction on every frame change — instant on replay loops
- ForecastTimeline: rAF-throttle slider input so rapid scrubbing renders
  once per browser frame; replace setInterval playback with rAF loop
  aligned to paint cycle (prevents queue buildup)
- WeatherGridLayer: rAF-throttle refreshTiles to collapse multiple rapid
  data changes into a single tile repaint per frame
- VelocityParticleLayer: Track and cancel both fade/remove timers on
  each setData; reset mid-fade snapshots to opaque; reduce fade delay
  from 350ms to 200ms for better particle visibility during playback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@SL-Mar SL-Mar merged commit 45e23ae into development Feb 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant