npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

signalk-weather-router-plus

v0.1.1

Published

Signal K plugin: standalone open-water weather routing (isochrone) using ECMWF open-data forecasts decoded in-process, GSHHG land avoidance, vessel polars. Publishes routes to the Signal K Resources API and registers as a Weather API provider.

Readme

signalk-weather-router-plus

Standalone open-water weather routing as a Signal K plugin. Nothing runs outside the Signal K process: the plugin downloads ECMWF open-data forecasts by HTTP byte range, decodes the CCSDS-packed GRIB2 fields in TypeScript, reads Copernicus Marine SMOC ocean currents (worldwide, including tides) and the Copernicus Marine hourly sea level (tide height, total water level, surge) from their Zarr stores with an in-process Blosc/LZ4 decoder, avoids land with GSHHG coastline polygons, and runs an isochrone router against the vessel's polar in a worker thread. No runtime npm dependencies.

The "plus" is the overlays: wind, temperature, tide, current and more, drawn from the same data the routing uses.

What it does not do: it routes in open water and avoids land, but knows nothing about depths, channels or bridges. (US inshore routing was too much for a plugin; it lives in the separate router.zeddisplay.com.)

A finished route from the western Mediterranean through the Strait of Gibraltar to Lisbon, with wind speed, isobars and the itinerary of legs

Status: beta (0.1.1). Please report problems at https://github.com/motamman/signalk-weather-router-plus/issues.

What changed in this version: WHATSNEW.md. Full history: CHANGELOG.md.

What it provides

| Surface | Path | |---|---| | Webapp (map, compute, watch progress) | listed in the Admin UI's Webapps page as Weather Router Plus; served at /signalk-weather-router-plus/ (also /plugins/signalk-weather-router-plus/ui) | | Route job API (REST + Server-Sent Events) | /plugins/signalk-weather-router-plus/api/… | | OpenAPI | /plugins/signalk-weather-router-plus/api/openapi.json | | Finished routes | saved to /signalk/v2/api/resources/routes/{jobId} (needs a routes provider, e.g. resources-provider) | | Weather API provider | point forecasts and observations anywhere from the global forecast (read from the decoded run on disk by the data worker) via /signalk/v2/api/weather/forecasts/point?lat=&lon= and /observations, with outside.cloudCover / wind.gust when the extra fields are on, water.level / water.levelTendency (relative to mean sea level) when tides are on and water.surfaceCurrentSpeed / surfaceCurrentDirection (the set, towards) where a current source covers the point | | Notifications | notifications.weatherRouterPlus.{jobId} on completion or failure | | CLI (no Signal K) | wrp-route |

Data

  • Forecast: ECMWF IFS 0.25° open data, oper/wave streams for every cycle: 00z/12z to 360 h (every 3 h to 144 h, then every 6 h), 06z/18z to 144 h. Fields: 10u, 10v, msl, swh, mwp, mwd. Only those fields are fetched (byte-range requests against the published .index files, roughly 4.7 MB per step instead of 140 MB) and cached on disk under the plugin's data directory (ecmwf/). The whole globe is decoded at full Float32 precision (exactly the decoded values), so overlays, conditions, the Weather API and routing work anywhere: 1440 × 721 cells × 4 B = 4.15 MB per field per step. A 72 h horizon (25 steps) is 623 MB with the six base fields and 1.35 GB with the extra fields (2t, tprate, skt, 2d, ptype, tcc, 10fg).

    Decoded once per update, kept on disk, read per request. The decoded forecast is not kept in memory. When a new ECMWF run arrives the data worker decodes it one step at a time into one reusable one-step block (54.0 MB with the extra fields) plus decode buffers (20.8 MB: one global field as Uint32 + Float64 + Float32 + Int32) and writes each step to forecast/<yyyymmddHH>/ under the plugin data directory: one raw Float32 file per field and step (<step>-<param>.f32, rows from the south, 1440 × 721, NaN kept, the exact in-memory layout) and an index.json (cycle, steps and valid times, parameters, grid, format version, complete). The run is written into a temporary directory, every file fsync'd, and renamed into place only when complete, so a crash never leaves a run that looks complete. It stays there until the next run replaces it (forecast.keepCycles applies, as for the GRIB cache). At start-up a complete decoded run of the current cycle is used as it is, without decoding. Each request then reads only what it needs, for as long as it needs it: a map layer reads its view (plus two cells) at the two steps around the map time; the conditions popup, the Weather API and /api/forecast?lat=&lon= read a few cells around the point for every step; a route reads its corridor box plus 5° (wind and waves, every step) into one block before it runs and drops it when it ends. Inside what was read every sample is bit for bit the value of the whole global store (the unit tests check map grids, arrows, isobars, conditions, Weather API points and corridor sampling against it). Nothing is cached in the process beyond that: warm reads come from the OS page cache (1–2 ms for a map view, below), so an in-process cache of field-steps was not added.

    Disk: one run of 72 h with the extra fields is 1,349,712,000 B of .f32 files plus a 3.5 kB index.json; with keepCycles 2 up to two runs are kept, next to the GRIB cache (212.5 MB for one 72 h cycle with the extra fields). An update writes the run once: 1,144,471,552 B written by the plugin process during a forced reload (/proc/<pid>/io write_bytes, measured on a build with the previous extra-field set). The GRIB cache stays, so a settings change (horizon, extra fields) can decode again without downloading.

    Measured on a Pi 5 (8 GB, NVMe), 2026-09-28, the installed build (whole forecast in memory) against this design, same settings (72 h, extra fields, SMOC, RTOFS, tides), the plugin run with a stand-in Signal K app, process RSS sampled every second from /proc/<pid>/status:

    | | whole forecast in memory | decoded on disk | |---|---|---| | RSS after start-up, all loads + 60 s | 1644.0 MiB | 533.8 MiB | | peak RSS during a forced forecast reload | 3059.6 MiB | 891.3 MiB | | RSS 60 s after the reload | 1987.3 MiB | 806.3 MiB | | start-up to forecast ready | 64.6 s (decode from the GRIB cache) | 1.3 s (decoded run on disk); 68.6 s when it has to decode | | forced reload (decode from the GRIB cache) | 66.9 s | 69.4 s (includes writing and fsyncing 1.14 GB) |

    Forecast reads from the decoded run (DecodedRun.window), cold = page cache dropped first: a map view (10° × 8°, wind, 2 steps) 7.1 ms cold, 1.6–1.7 ms warm; the North Atlantic (70° × 40°) 10.5 / 2.1–2.4 ms; the whole world 37.9 / 8.4–9.2 ms (16.6 MB); a point series (11 fields × 25 steps) 34.7 / 25–29 ms; a route corridor (5 fields × 25 steps) 108.6–148.4 ms cold, 27–35 ms warm (3.7–7.4 MB). Route jobs Newport RI → Bermuda and → Horta (motor and sail_max) took 196–238 s with the whole forecast in memory and 199–239 s decoded on disk; Lisbon → Palma 3.2–7.2 s and 6.8–12.9 s (the first Lisbon route of the second run downloaded 19.2 MB of SMOC chunks the first run already had on disk). Route areas are released when a route ends, so each route decodes its CMEMS SMOC area again from the disk cache (2.1–3.4 s measured).

  • Land: GSHHG shorelines as shapefiles (GSHHS_f_L1.shp for full resolution; add GSHHS_f_L6.shp for Antarctica) or the OSM land-polygons export. Overlay land flags use a raster built on demand for each requested bbox at a resolution matched to the request's sample spacing (a quarter of it, 0.002° to 0.25°, at most 4 M cells), from an in-memory index of the shapefile records; the last 8 rasters are kept. Conditions is_land uses the exact polygons. For routing, land is loaded for the box around the route's corridor (see Global water grid) plus 1° and rasterised at the finest resolution that fits the configured cell budget (0.5 m arc-seconds to 0.01°). The raster is conservative: cells crossed by a coastline edge count as land (an exact supercover of every edge, so a water cell contains no coastline). Where the corridor passes a passage only a few cells wide, finer patches (down to 0.0005°, about 55 m) are rasterised locally. A leg is checked against every raster cell its path crosses (the finest patch where one covers it), not points along it, so land narrower than the spacing of points cannot be stepped over. Endpoints and every leg of the finished route are checked against the exact polygons: a leg crossing or touching any coastline edge is a land crossing, whatever the width of the land (legs whose cells are all water need no polygon test).

  • Depth: none in this version. There is no bathymetry: depths, draught and clearances play no part in the route.

  • Currents: a stack of sources; where several cover a point the highest priority with data wins, and exactly (0, 0) from a source means "no data here" (the next one is asked):

    | Priority | Source | Coverage | |---|---|---| | 10 (from the file) | user-installed tidal harmonics .npz, e.g. NECOFS GoM3 | its grid | | 3 | Copernicus Marine SMOC (below) | worldwide, 80°S–90°N | | 2 | NOAA Global RTOFS, depth-averaged ubaro/vbaro, one regional product | the product's box | | 0 (from the file) | FES2014 harmonic extract .npz | its grid |

    SMOC is product GLOBAL_ANALYSISFORECAST_PHY_001_024, dataset cmems_mod_glo_phy_anfc_merged-uv_PT1H-i_202211: hourly surface currents merging the Mercator 1/12° circulation model, FES2014 tidal currents and Stokes drift (utotal/vtotal, m/s), from 2020-11-01 to about 10 days ahead, updated once a day. It is read anonymously (no account) from the Copernicus Marine ARCO Zarr v2 stores on s3.waw3-1.cloudferro.com (timeChunked.zarr: 1 h × 512 × 2048-cell chunks; geoChunked.zarr: 4272 h × 16 × 8-cell chunks; downsampled4.zarr: 1/3°, one chunk per hour for the globe), with the chunks' Blosc/LZ4 compression decoded in TypeScript (src/data/blosc.ts, src/data/zarr.ts). Each load takes the layout with the lower estimated download (a small box over many hours comes from geoChunked, a wide area from timeChunked).

    • Resident area: the vessel's position (Signal K navigation.position) ± the configured half-width (15°), every step from the current hour to the SMOC horizon (72 h) at 3 h (or 1 h) spacing, cropped from the decoded chunks and held as Float32 in SharedArrayBuffers: the data worker loads it and the route worker uses the same memory. It is rebuilt when the window moves on a step, when the vessel has moved more than a third of the half-width, or for a new run.
    • On demand: before a route (route worker) or an overlay / conditions query (data worker) whose box the resident area does not cover, the plugin loads that box first: all window steps for a route or a conditions series, only the one or two steps around the requested hour for a map overlay (from the 1/3° store for zoomed-out views, lattice ≥ 0.25°). A single area is capped at 128 MB (a route box too large at 1/12° is loaded at 1/3°). On-demand areas are not kept for long: the route worker releases its route areas when the route ends, and the data worker keeps at most 16 MB of areas loaded for map / conditions queries (least recently used first; measured on a Pi 5 NVMe, 2026-09-28: a map view's area is 0.0–0.2 MB decoded, and decoding one again from the disk cache takes 25–89 ms). Overlay queries wait at most 60 s; a slower load finishes in the background and serves the next request. Without a vessel position nothing is resident and everything loads on demand.
    • Runs and cache: compressed chunks are cached under <data dir>/smoc/<run>/, where the run is the last hour on the store's time axis (it advances by 24 h each day). On every refresh tick the plugin reads the 12 kB .zmetadata and the STAC record; a new run is used once STAC reports the update finished (admp_updated_data later than the metadata rewrite, no admp_updating_start_date), the resident area is downloaded again, and the previous run's cache is deleted. Offline, the newest cached run is used.
    • Sampling: bilinear on the 1/12° grid (seamless across the antimeridian), linear in time between steps with a ±1 h grace at the ends, like RTOFS. Cells the model leaves empty (land, and roughly the first cell off the coast) are "no data".
    • Measured (2026-09-28, ±15° box = 367 × 367 cells, 72 h): 26 steps at 3 h: 181.8 MB download (English Channel, 208 chunks, 9 s on a fast line) or 110.9 MB (US East Coast, 104 chunks), 28 MB resident; 74 steps at 1 h: 219.5 MB (English Channel, from geoChunked) or 315.6 MB (US East Coast), 79.7 MB resident. A one-hour overlay of a 1.5° × 1° box outside the resident area: 2.7 MB (2 chunks); a conditions series at a point: 0.64 MB (4 geoChunked chunks, which hold every hour of the run). Decoding runs at about 400 MB/s.

    Coastal display extension. A ~9 km model has no value in the cells next to the coast, so drawn currents would stop short of the shoreline. For the map layers only (/api/field?layer=current and /api/currents), SMOC and RTOFS fill an empty grid cell that has valid cells within 2 grid cells from those cells (inverse-distance² weights, no fade; valid cells never change), so colour and arrows reach the coast, where the map's coastline tiles cut them. Routing, the conditions popup and the sea-state layer use the raw values only.

    Attribution and licence. SMOC is Generated using E.U. Copernicus Marine Service Information; https://doi.org/10.48670/moi-00016. The Copernicus Marine licence (https://marine.copernicus.eu/user-corner/service-commitments-and-licence) grants the licence free of charge (section 2.1) as a worldwide, non-exclusive, royalty-free, perpetual licence to use the products and to create and distribute value-added products or derivative works "for any purpose" (2.2), with the credit above, which the page shows in the map's attribution while a current layer is on (2.3–2.4). The products come without warranty (4). The service commitments state the service is free of charge until the end of the current Copernicus Marine Service phase, planned for 30 June 2028.

  • Tides and water level: Copernicus Marine hourly sea level. Same product (GLOBAL_ANALYSISFORECAST_PHY_001_024), dataset cmems_mod_glo_phy_anfc_merged-sl_PT1H-i_202411: 1/12°, 80°S–90°N, hourly from 2022-09-01 to about 10 days ahead, updated daily (source attribute "MERCATOR GLO12, FES2014"). Read anonymously from the same ARCO Zarr stores with the same run detection, disk cache and layouts as SMOC (shared code in src/data/arco.ts; src/tides/). Variables used, in metres:

    • ocean_tide: the FES2014 ocean tide, "tidal sea surface height above mean sea level", i.e. the tide relative to the sea floor, as a tide gauge records it. tide_loading (sea-floor displacement under the tidal load) is not added: a gauge and the land move with the loaded crust, so the height a mariner sees is the ocean tide alone; ocean + load tide (geocentric) matters only for satellite altimetry. total_sea_level does not include it either.
    • total_sea_level: height above the geoid = ocean_tide + invert_barometer + sea_surface_height (GLO12 dynamic sea level, which includes the mean dynamic topography) + global_mean_steric_variation + global_mean_mass_volume_variation (product user manual CMEMS-GLO-PUM-001-024 issue 2.4 and the variable's long_name; checked on the data: the sum matches to the product's 1 mm quantisation).

    Derived quantities, SI metres relative to local mean sea level:

    | Field | Formula | |---|---| | tide height tide_m | ocean_tide | | MSL offset | mean(total_sea_level − ocean_tide) over the mean window | | surge (non-tidal residual) surge_m | total_sea_level − ocean_tide − offset | | total water level water_level_m | total_sea_level − offset = tide + surge |

    The mean window is every hourly sample of the geoChunked time chunks covering the last 60 days of the run (60 to ~210 days: a geo chunk holds 3648 h and is downloaded whole anyway), so the offset is fixed for a run and place. It removes the geoid-to-MSL separation (mean dynamic topography, e.g. −0.44 m at Newport RI, +0.15 m at Sydney) and the seasonal mean; the surge is the departure from that recent mean: weather set-up, inverse barometer and shorter dynamic signals. Over the last 130 days its standard deviation was ~8 cm at Newport and Sydney, correlating with the inverse barometer (r = 0.37 and 0.68).

    Point series (conditions popup, Weather API): from the geoChunked store, bilinear from the 4 surrounding cells; a corner that is model land takes the IDW² mean of valid cells within 2 cells and the value is flagged tide_extrapolated. No valid cell within 2 cells (~18 km) → no tide data (e.g. Southampton: the Solent is land at 1/12°). High and low waters are those of the tide height: local extrema of the hourly samples (pairs less than 3 cm apart dropped), refined with a parabola through each extremum and its neighbours; range = mean of consecutive high−low differences. Tendency: rising / falling, steady within ±2 cm/h. Measured: 0.9–6.7 MB and 0.2–1.5 s per new place (2 variables × 1–4 chunks of ~0.3–1.6 MB; the whole 3120-hour chunk), then served from memory (8 places) or disk for the rest of the run.

    Tide-height map layer (/api/field?layer=tide): ocean_tide only, hourly (a 3-hourly step would err by up to ~30 % of the amplitude mid-step). A resident area around the vessel (± tides.halfWidth, default 15°) over now → tides.horizon (default 24 h), its start aligned to 6 h so it is rebuilt four times a day; views elsewhere load their hour on demand (1/3° grid for zoomed-out views); at most 16 MB of those on-demand hours are kept between queries. Measured at 15° around Newport: 368 × 368 cells × 31 hourly steps, 16.8 MB in memory, 62 timeChunked chunks = 39.9 MB downloaded in 3.5 s (about 1.3 MB per hour of window; a new daily run re-downloads it). One on-demand hour: 0.74 MB (1/12°, Sydney) or 0.53 MB (1/3°, most of the North Atlantic). The same 2-cell coastal extension as the current layers is applied for display; the page's land mask clips it.

    Datum and accuracy caveats. Heights are relative to mean sea level, not chart datum (LAT / MLLW): add the local chart-datum-to-MSL difference yourself; not for under-keel clearance. The model is ~9 km: in bays, estuaries and harbours the tide can be earlier and smaller than local tide tables (see the Newport check under Verification: highs 14–17 cm low and ~1 h early; lows within 2 cm and ~30 min early), and small basins may not exist in the model at all. Credit: Generated using E.U. Copernicus Marine Service Information; https://doi.org/10.48670/moi-00016 (shown in the map attribution while the tide layer is on, and under the Tide chart).

Using the webapp

  • Planning. Click the map for the menu: set or move the start, set or move the destination, Add waypoint here (the clicked point becomes the destination and the old destination becomes the last waypoint, so waypoints stay in placing order), or Conditions here. Holding on the map does the direct action (start, then destination, then extend the course). Drag any pin to move it. Holding on a computed route pins that point as a waypoint. The button under the zoom buttons (⌖) centres the map on the boat's Signal K position (navigation.position), keeping the zoom; a first visit opens on it (the page never asks the browser for its location). The route summary (distance, time, arrival, sail and motor time, waypoints, highest waves, validation) shows on the Route tab and at the top of the Itinerary tab. The plan (start, destination, waypoints with their radii, departure) is kept in the browser and restored on reload; a departure that has passed is set to now, and the status line says so.

  • Header. One line per quantity with the source it comes from: wind (the ECMWF run, plus any regional model), waves, currents, tides, and the job queue. Only the sources that apply to the route on the map, or to the map view when there is none, are listed; times are local, with UTC as a tooltip.

  • Saved routes (Saved tab): opening one replaces the route on the map (its own start, destination, waypoints and precision) and offers Recompute with the current forecast and settings, or Keep as saved. Publish in the itinerary bar saves it to Signal K's Resources; a published route carries each point's leg (time, mode, distance, SOG, COG, wind, current, waves) as the point's description, in your units, which chartplotters show with the point. GPX in the same bar downloads the route as a GPX 1.1 file for plotters and apps that import GPX: one route whose points carry their name (Start, WP1 … End), time and leg, and the route's summary as its description.

  • Units and times. Every quantity follows the Signal K user's unit preferences, with one deliberate exception: a duration of a day or more (a passage, the sailing or motoring time, a forecast horizon) is written as days, hours and minutes, Signal K's own duration-compact format ("9d 7h"), whatever the time unit is set to, as "223.0 hour" reads badly; shorter durations follow the preference. Clock times are shown in ship's time when the Signal K server publishes it (environment.time.timezoneRegion, an IANA zone such as Pacific/Tongatapu, else environment.time.timezoneOffset, e.g. -930; signalk-ships-time sets them), else in the browser's time zone, and the departure field is read in the same zone. A time a day or more away carries its date ("Tue 13 Oct 21:58"), as a weekday alone is ambiguous on a long passage.

  • Points of sail in the itinerary come from the route's polar at each leg's wind speed: in irons tighter than the polar's no-go angle (the tightest angle it gives any speed, with the tightest sailable angle setting applied), close hauled from there to its best upwind (VMG) angle, close reach to 75°, beam reach to 105°, broad reach to 15° short of its best downwind angle, then downwind.

  • LIVE and SIMULATE (Route tab). LIVE follows the real boat along the route on the map; it is available when the route starts where the boat is. The itinerary card of the point the boat is heading to is highlighted with live figures, and each point passed keeps those at its closest approach. Off the route by more than the cross-track threshold for the sustain time, or at a waypoint, the page computes a re-plan from the boat's position through the remaining waypoints and offers it in a banner (Accept, Dismiss). SIMULATE sails a simulated boat along the route at a chosen speed-up, with Start, Stop and Rewind to start, and draws its track. Both run only while the page is open; the re-plan is not published and does not change Freeboard's active course.

    Planning: start, destination and waypoints on the map, with the click menu open

  • Waypoint behaviour (Setup tab): Precision Precise (each leg ends exactly at its waypoint) or Approximate (one search carries the route through the circle around each waypoint instead of stopping at it), and Waypoint radius 50–2000 m, default 200 (Approximate only). See Waypoints.

  • Signal K notes (Layers → Base, on by default): notes from the Signal K Resources API that have a position (hazards, warnings and remarks, e.g. the area warnings signalk-passage-briefing adds) as markers in the map view. Click one for its title, text, link, who wrote it and when, and its bearing and distance from the boat, with Edit and Delete (a second press confirms); drag one to move it. Add note here in the map's click menu writes a new note at that point. Notes are saved to Signal K's Resources API, so Freeboard and other apps see them; writing needs a Signal K login with write access.

  • Areas to avoid. A note can mark the area around it to avoid: tick Avoid this area in the note's form and give a radius. The note turns red with a dashed circle, and the router treats the circle as land: no candidate or leg may enter it, and a start, waypoint or destination inside one is refused with a message naming the note. The radius is stored in the note as properties.avoid.radius_m (metres), so a note another plugin wrote (a passage briefing's area warning) can be marked too, and other apps can read it. Avoid marked areas in the Plan tab (on by default; avoid_areas in a request) turns this off for a route; the job log lists the areas used. The land-avoiding corridor that guides the search does not know the areas, so a circle across a narrow passage can leave the search with no way through.

  • Layers (Base / Weather / Water): each layer is named for the quantity it shows. Colour layers are exclusive (one at a time). Colour layers, wind barbs and current arrows are drawn tile by tile from /api/tile (web-map tiles at whole hours, saved on the server, see Map tiles); each colour tile is cut at the coastline with its own 256 × 256 coastline tile. Their time is the overlay time rounded to the hour. Isobars and flow lines are drawn for the whole view, from the same saved tiles (see Map layers). The tide colour scale stretches to the largest tide in the tiles loaded (at least ±0.5 m). On the tide and current layers, water the source model has no value for (narrower than its ~9 km grid) is hatched and labelled "no model data".

    Current speed and direction in the Aegean, cut at the coastline, with "no model data" hatching in the Euboean Gulf

  • Seas along the route (Layers → Base, on by default): an arrow on each leg of the route, pointing the way the waves travel, coloured by the encounter index (the sea as the boat meets it, see Comfort). The leg cards and the Freeboard panel show a Seas row for the next leg: where the waves come from ("head seas", "on the starboard bow") and the encounter index's band and value ("rough (112)").

  • Current against the waves (Layers → Water, off by default): what the current does to the sea, from /api/tile/seas, coloured by the sea state there (the sea-state heatmap's scale, each band in the colour of its legend swatch, shifted slightly darker and outlined so the glyphs stay visible over the heatmap). Three styles, chosen under the layer's checkbox and remembered in the browser (a trial, to settle on one):

    • A arrows: an arrow at every point along the way the waves travel: two heads meeting in the middle where the current runs against the waves (drawn larger the more it steepens them), a double chevron where it runs with them, a thin arrow where there is little current along them.
    • B rips & breakers: a mark only where the current against the waves steepens a choppy or rougher sea (index 75 or more): the short wavy lines charts use for tide rips when the waves are 10–25 % steeper, a breaking wave at 25–50 %, a breaking wave with spray from 50 %. Nothing elsewhere, and no direction (the wave and current arrows carry that).
    • C wind, waves, current: three arrows from each point, each pointing where it is going: wind thin with a feather at the tail, waves wavy, current thick and solid. Where two point at each other the sea is rough, and the glyph shows which pair (wind against current, waves against current, or wind against waves). Arms within 20° are spread apart; an arm is left out below 1 m/s of wind, 0.1 m of waves or 0.1 m/s of current.

    A and B choose their glyph from the waves against the current only; wind against current is in the sea state (the colour) but not in their glyph. C shows all three. The thresholds are named constants in public/rp-layers.js.

  • Wave direction (arrows) (Layers → Water, off by default): an arrow per point along the way the waves travel, from the same /api/tile/seas points, coloured by the significant wave height on the wave-height heatmap's scale (shifted darker and outlined, as above) and longer the longer the mean wave period (12 px at 4 s and below to 34 px at 16 s and above), so long swell and short chop read apart.

  • Conditions popup (shift-click, or the menu): 72-hour charts for Wind, Waves, Sea state (index / Beaufort / Douglas), Tide & current (tide height, total water level and surge on the left axis; current speed as a filled area on the right axis; the current's set as arrows; high and low water marked), Pressure, Temp, Precip, and a Raw table. Click the chart to move every map layer to that hour.

    Conditions popup, Tide & current tab: tide height, total water level and surge against current speed and set, with high and low waters

  • Polars: the picker lists the default polar and the polars directory; "Create polar from boat specs…" generates one.

    Polar diagram of the selected polar, and the sailing strategy modes

  • Decision lines (the switch beside Find Route, and the same one in Layers → Base; off by default, remembered): the router's search drawn as it runs, one line per stage front (the candidates kept after pruning, blue → amber by stage; every point has its own arrival time, so they are not isochrones) and the best path so far, dashed. The fronts are always streamed and kept with the job; the switch only shows or hides them, so turning it on after a run shows the search that was made. The finished route's fronts stay, faintly, also for a past route opened from the log.

  • Settings tab: the web-app settings below, in the selected units.

  • While the server gets its forecast (a first start, or after the forecast settings changed, when the forecast is decoded again): a notice under the header says what it is doing ("Loading the forecast: decoding the 06Z cycle, step 12 of 37"), with a progress bar and, once measured from the decode itself, the time left. The map layers wait quietly meanwhile (no error notes), a route started meanwhile waits for the forecast and its log shows the same progress, and when the forecast is ready every layer and the units load by themselves. The Freeboard panel shows the same in its status line.

  • The page references its scripts with ?v=<tag>, a tag that changes whenever a file in public/ changes, so browsers and proxies in front of Signal K always load the current scripts after an update.

In Freeboard-SK

The plugin is also a plotter extension (Signal K Plotter Extensions API, version 1), so weather routing is available inside Freeboard-SK 3.0 or later without any change to Freeboard: Freeboard finds the extension through the plotterExtensions resource collection the plugin provides, and the panel runs in a sandboxed iframe served from /signalk-weather-router-plus/plotterext/.

The Weather Router Plus panel in Freeboard-SK, with a draft route on the chart

  • Tap the grid icon at the top right of the chart to show the extension toolbar, then Weather route. The panel slides in on the right.
  • Route on the chart (the usual way): draw the route with Freeboard's own Draw Route tool (pencil menu), tapping the start, any waypoints and the destination on the chart, then Finish; or tick a saved route in the Routes list. The panel lists the routes shown on the chart (one is picked by itself). Weather-route it sends the first point as the start, the last as the destination and the points between as precise waypoints, and rewrites that route's geometry in place with the result, which stays Freeboard's editable draft (or an unsaved edit of a saved route). Drag a point and press the button again to re-route; Restore drawn route puts the drawn points back.
  • From the boat to a position (the quick way): From is the vessel's position, kept up to date through Freeboard's own Signal K connection (editable; Use the vessel position snaps back); To is typed, Use the map centre, or one of your saved waypoints. Find route places the result on the chart as a new draft route.
  • Both use the chosen polar (the same list as the web app), mode (Sail max, Fastest, Motor) and, under sail, the min sail speed (the boat speed under sail below which the router motors; the plugin's routing setting by default, in Freeboard's speed unit), and the Limits: a maximum wind speed and wave height that no leg may exceed (empty = no limit; the routing settings' values by default). Progress is shown; Cancel stops the job. The map is fitted to the result; the panel shows distance and time in Freeboard's unit preferences, the sailing/motoring split, the arrival time and an itinerary: one card per leg with its time, waypoint, mode and tack, then distance, time, SOG and COG, then the wind (with the point of sail from the route's polar), the current (fair or foul) and the waves. A tap on a card centres the chart on its waypoint. Each point's description also carries its leg, which Freeboard shows in the route's points sheet (the ⇅ icon beside Points); your own point names are kept.
  • A saved weather route shows its legs without routing again. Ticking a saved route that carries the plugin's weather (one computed in the web app, or here and saved) in Freeboard's Routes list opens the panel on that route's legs (a hidden background page of the extension watches the routes shown; routes Freeboard shows again in its first seconds after starting do not open it).
  • Following the boat. The card of the leg the boat is on is outlined and tagged "boat" and kept in view: the leg nearest the boat's position, within 10 nautical miles of the route, else the leg of Freeboard's active course when it is this route. Freeboard has no event for a tap on a route point, so the cards cannot follow a tap on the chart.
  • Units. Speed, distance and depth are in Freeboard's own units (its Settings → Units; wave height follows depth), so nothing changes unit from one part of the Freeboard screen to another; angles, times and data sizes, which Freeboard has no setting for, follow your Signal K unit preferences, except that a duration of a day or more is written as days, hours and minutes ("9d 7h"), and clock times are in ship's time when the server publishes it, as in the web app; wave periods are in seconds. Freeboard's units can differ from your Signal K preferences, which the web app uses, until Freeboard adopts them (PR-8 in docs/plans/freeboard-sk-integration.md).
  • Save route… opens Freeboard's Route Details dialog and stores the route in Signal K's Resources (the plugin does not publish it itself in this case, so there is one copy); for a saved route, Save changes updates it. Discard removes a new draft or restores a rewritten route.
  • The panel keeps running while closed, so a long route finishes in the background. Routes started here are ordinary jobs: they appear in the web app's run log and in GET /api/routes.

Map overlays. The eight colour layers (wind speed, wave height, current speed, sea state, precipitation, air and sea temperature, tide height) are published as Signal K chart resources, served as PNG tiles (/api/tile/<layer>/{z}/{x}/{y}.png, see Map layers) with a time block covering the forecast hours. In Freeboard's Chart list they appear as "Wind speed (Weather Router Plus)" and so on: tick one to show it, set its opacity and order like any chart, and use the clock action on its row for Freeboard's Time palette (scrub, step, loop, play through the forecast; NOW returns to the current hour). A layer is listed only while its data is there: currents need a current source, tides the tide data, waves and the temperatures the forecast fields. The colours are the web app's; Freeboard has no legend, so the scale is in the web app's layer legend (GET /api/legends). The tide layer uses its fixed ±3 m scale here. Five glyph layers come with them: Wind barbs, Current arrows, Isobars (4 hPa, bold every 20 hPa; highs and lows as blue and red dots; no pressure labels, as the server has no font), Current against the waves (the web app's layer in style A) and Wave arrows (coloured by wave height, longer for a longer period); the last two need wave data. The layers are also organised as Freeboard Groups (resources menu → Groups), one colour layer each with the glyphs that belong with it, shown in one tap: Wind (speed, barbs), Waves (height, wave arrows), Currents (speed, arrows), Pressure (isobars, barbs), Sea state (index, current against the waves), Tide (height, arrows), Rain (precipitation, isobars), Air temperature and Sea temperature (with isobars and arrows). Two colour layers over each other are unreadable, so no group has more than one. A group holds the layers whose data is there and is rewritten when the forecast is reloaded; it needs the server's groups collection, which Freeboard creates.

Not yet available in the panel: waypoints in the "from the boat" flow (draw a route on the chart for those), a departure time other than now, polar performance and the other web-app settings (they apply as set in the web app's Settings tab). A "weather route to here" entry in Freeboard's map menu needs a change to Freeboard; see docs/plans/freeboard-sk-integration.md.

Routing engine

A port of the routePlanning OceanPropagator (subsector isochrone, Hagiwara 1989 / Chen & Mao 2024), guided by a corridor from a global water grid:

  1. Corridor. A* on the global water grid finds a land-avoiding corridor from the start to the end of each leg (see Waypoints), wherever the water path goes (Lisbon → Palma goes south through the Strait of Gibraltar, far outside the box around the endpoints). The route's land raster, the CMEMS SMOC area loaded for the route and the first-boot forecast crop cover the corridor's box plus 1°.
  2. Consistency with the route raster. A flood fill on the route's (conservative) land raster, inside a band of grid cells along the corridor, must connect start and end. Where it stops, because the raster's resolution closes a passage the grid keeps open, the raster is refined locally (a finer patch, down to 0.0005°) and the fill repeated. A passage still closed at 0.0005° is not navigable for this router: its grid cells are blocked and A* runs again (up to 12 times). Narrow stretches of the corridor (a passage under 10 raster cells wide) are refined the same way so the isochrones have room, and stretches under 8 km wide are re-traced on the route raster (a fine A* kept to mid-channel), because the grid's 2 km cells cannot place the skeleton inside a 700 m strait.
  3. Isochrones. From each retained parent, 2m+1 candidate headings are projected one stage step ahead, aimed at the corridor point one step ahead; candidates whose great-circle leg touches land are dropped; survivors are timed by a leg simulator that samples wind and the polar every simStepM metres (mode policy sail_max, fastest or motor). Candidates are binned by cross-track offset into 2k subsectors and the cheapest per bin is kept.
  4. Narrow passages. The corridor carries the across-track water width at every point. A parent's step never jumps past a point where the passage is narrower than a quarter of the step: it may step up to that point, and inside the passage it steps at most 4 × the local width (not below 1 km); the stage budget grows by the stages this costs. Inside a stretch narrower than one subsector bin, candidates are binned across the passage (6 bins over its width) instead of by the start → end offset, so several branches get through a strait (in the test runs Madeira → Cartagena kept 8–12 branches through Gibraltar, where it used to get down to 1).
  5. Automatic vias. Where the corridor crosses a narrow passage the grid build recorded (below) that is narrower than one stage step, a soft via (a pass-through disc of radius half the width + 500 m, at least 1 km) is placed at its narrowest point, so every branch is pulled through the passage instead of drifting against the coast beside it. Progress messages name them ("auto via at Strait of Gibraltar, width 14.2 km"); the GeoJSON lists them in the auto_vias property and the job summary in auto_vias. They are not route waypoints and never carry role: "via".
  6. Finish. The search stops when a branch that crossed every automatic via is within one (local) stage step of the leg's end with a land-free straight final leg (or, for an approximate waypoint, as soon as a branch is inside the waypoint's circle); the terminal is chosen among those with a clear final leg. If the planned stages run out first, up to K/2 more run.

Comfort (rough water)

The sea-state index describes the water at a point (wind against current, swell steepened by an opposing current); it knows nothing of the boat. It is the sum of a wind term (50 in any breeze without a current, more where wind and current oppose) and a wave term, 10 × swh² × 5/max(period, 5) × steepening (SWELL_COEFF, src/plugin/conditions.ts), on the bands smooth < 35 ≤ good < 50 ≤ slight < 75 ≤ choppy < 100 ≤ rough < 150 ≤ extreme: a 2 m wind sea is choppy, 3.5 m rough, 5 m and more extreme, and long swell reads milder than a wind sea of the same height. Heading into the waves is harder than running before them, so the router weights the index by the angle between the course and the direction the waves come from: × 1.3 in head seas, × 1.05 abeam, × 0.8 in following seas, a cosine between (the encounter index, src/engine/seas.ts). The weights are a judgement, not derived from physics, and are kept in one place so they can be tuned.

With a comfort weight above 0 (setting routing.comfortWeight, default 1; request field comfort_weight), each second the leg simulator sails in water above an encounter index of 75 (the top of the "slight" band: with no current the index's wind term alone is 50, which head seas weight to 65) counts extra in the search's choices: weight × (index − 75) / 100 extra seconds per second, at most 2 × weight. With weight 1, choppy water at 100 adds 25 %, rough at 125 adds 50 %, extreme at 200 adds 125 %. The cost is used for pruning, the choice of the terminal, and (when it runs) the smoother; the route's times, ETAs and summary stay the real times. It needs wave data in the forecast; without it the cost is 0. Each waypoint carries the sea on the leg into it (sea_index, seas_angle_deg, seas_side, seas_sector, encounter_index).

Waypoints (legs)

A waypoint is an end point and a start point by another name: it ends one leg and starts the next (port of the routePlanning compute_multi_leg_route). Each leg is routed as its own route, with its own corridor, land raster, isochrone search (K stages per leg) and retries, departing at the previous leg's arrival time so wind, current and waves move on with the boat. The legs are then stitched: the duplicate junction point is dropped, distances and sailing/motoring times are summed, and the junction point of each waypoint carries role: "via" in the GeoJSON (automatic vias stay in auto_vias and are never role: "via"). Progress messages are prefixed leg 2/4: ….

A route with waypoints off Rhode Island: an approximate waypoint circle, legs coloured by tack, and the itinerary cards

precision decides where an intermediate leg ends:

  • precise (default): exactly on the waypoint (a straight final leg from the last stage to the point, checked against land and simulated like the final leg to the destination).
  • approximate: the route only has to pass through the waypoint's circle (arrival_radius_m, default 200 m, or the waypoint's own radius_m). As in the reference (hybrid.py, collapsed ocean runs), consecutive legs joined by approximate waypoints are routed as one search from the run's start to its end, with each waypoint circle as a via the winning branch must pass through in order. The track carries on through the waypoint instead of ending there and restarting, and the point where it passes the circle carries role: "via". Progress messages for such a run read legs 1–3/3: … through 2 waypoint circle(s) … in one search.

The final destination is always exact. Routes without waypoints are one leg, unchanged. Where this differs from the reference:

  • after a precise waypoint the next leg starts where the previous one ended (the reference restarts from the canonical waypoint and trims the stitch), so the track is continuous;
  • a branch whose next waypoint circle is closer than one stage step also gets a candidate that steps straight into the circle. Without it a branch reaches a small circle only if a full stage step (tens of km) happens to cross it, which failed where the course turns at a waypoint (the Baja route in docs/plans/waypoints-multi-leg.md);
  • if a one-search run still finds no branch through every circle, that run's legs are routed one by one (each ends on entering its circle and the next starts there) and the log says so, instead of the route failing.

The forecast area and the CMEMS SMOC area are read per leg (the leg's corridor box plus the margin) and released after the leg; everything is released when the route ends. On brain (Pi 5) one area for all legs took the same time (Baja, 4 legs: 11.9 / 12.0 s against 11.5 / 11.8 s per leg) and held more forecast (3.0 MB against at most 2.0 MB per leg) and SMOC (2.5 MB against at most 0.9 MB).

One deliberate difference from the reference: the stage budget is sized to the corridor length, not the straight-line distance, so detours around land fit within the configured number of stages.

If a route arrives after the last forecast step, conditions are held at the last step. The GeoJSON carries forecast_valid_to, forecast_horizon_exceeded_s and legs_beyond_forecast, every point after the last step has beyond_forecast: true, and the web app shows it: an amber badge in the result strip with the end time, the legs after it drawn dashed with a "forecast ends" marker on the map, a chip on the itinerary cards, and a note in the saved route's description. The Freeboard panel shows the same note. The Forecast horizon setting (Settings tab, Forecast group, 3 h to 360 h) decides how far the forecast reaches.

Regional wind (optional). With the signalk-grib-downloader plugin installed (AROME, ARPEGE, ICON-EU, GFS), the plugin decodes the 10 m wind of each complete run whose grid is finer than ECMWF's and layers it over ECMWF: the regional model where it covers the point and the time, blended over five grid cells at its border and over its last 3 hours, ECMWF elsewhere; waves stay ECMWF. A source no finer than ECMWF (GFS at 0.25°, ECMWF's own spacing) is not decoded or used; the header lists it as "not used: not finer than the global forecast". A regional grid on either side of 180° works for routes across it. The job log and the summary's regional_wind say which model answered how much. wind_model: "ecmwf" (or unticking "Regional wind where available" in the Plan tab) routes on ECMWF alone. Install only the downloader; its companion signalk-grib-weather-provider is not needed.

Where the corridor is open water, each candidate aims the centre of its heading sweep one step along the skeleton's direction from where it is, so branches can spread across the ocean to find a detour; in narrow water it aims at the skeleton itself, which keeps the search in the channel.

When the search stops, the final leg of every branch with a clear straight hop to the waypoint (the nearest 64) is simulated, straight or as a beat, and the branch with the earliest predicted arrival is taken, not the nearest one: branches advance a fixed distance per stage, so a slow branch crawling straight at the waypoint is nearest while faster branches that tacked are further out but ahead in time.

Every stop (start, waypoints, destination) is tested against the exact coastline polygons before routing. One on land, or within 150 m of the shore, is moved to the nearest point with 150 m of water around it, within 1,000 m, and reported: in the log (which point, how far), on the route (snaps, stop_count, the start_/end_ original, anchor and snap-distance fields) and on the moved point (snap_distance_m, original); the web app draws the tie from the drawn point to the water and the itinerary says "moved N m". With no water within 1,000 m the route fails naming the point.

Polar rows closer to the wind than the Tightest sailable angle setting (Settings tab, Routing group, default 30°, 0 = off) are ignored for routing: many library polars carry small boat speeds at 5°–25° off the wind, which would send a route dead upwind at a crawl instead of tacking. The polar files themselves are not changed.

When a parent's primary heading sweep yields nothing (its headings in the polar's no-go angle, on land or over a limit), that parent alone gets the wider sweeps (±120°, then half step, then the full circle at a quarter step) while its siblings keep their primary candidates; the reference implementation widened only when the whole stage's sweep was empty, which left a front beating to windward tacking in place.

The candidate nearest each goal always survives a stage's subsector pruning (the bin cost prices the remaining distance at motor speed, which is optimistic to windward and could drop the leading branch), so the best remaining distance never increases from one stage to the next.

A search that stops making progress once its planned stages are used (three stages in a row without any candidate coming closer to the destination; when the front is beating, a tenth or more of its water candidates dead upwind, the check waits for the hard ceiling of planned stages plus half the configured count; progress is measured towards the deepest branch's next via, or the destination once every via is crossed) fails with "the search is boxed in" (or, with a via still uncrossed, the vias-not-crossed error that makes the router retry without automatic vias), counting how many of the last stage's candidates were over the wind/wave limit, crossed land or had no boat speed, and naming the forecast end when the search had run past it. Each stage's progress line also carries those counts.

Global water grid

data/water-grid-0.02.bin.gz (shipped, 1.49 MB) is a navigability graph of the whole world at 0.02° (18000 × 9000 cells), built from GSHHG full resolution L1 (GSHHS_f_L1.shp):

  • Water and edges. The coastline is rasterised at 0.005° (4 × 4 fine cells per grid cell) in 10° tiles with a 1.2° halo, so edges on tile borders and across the antimeridian see the neighbouring tile. A fine cell is water when its centre is outside every polygon. Per grid cell the file stores a water bit and two edge bits (east, north). An edge is open when a 4-connected path of fine water cells inside the two cells crosses it, i.e. when some fine row (or column) has water on both sides. A plain "any water in the cell" rule closed the Bosphorus (a one-cell thread crossing cells corner to corner); the edge rule keeps it open. Diagonal fine contacts do not count: on the conservative raster the Bosphorus is closed even with diagonal connectivity, while centre sampling with 4-connectivity keeps it open and keeps every isthmus in the checks below closed.
  • Split cells. Where a cell's fine water forms two components that both touch its border (the two shores of a spit or isthmus thinner than a cell), the grid stores the component of each border fine cell and which fine rows cross to each neighbour (57 759 cells worldwide), and the search follows components through them. Without this the grid leaked across such strips.
  • Moves. A* moves to the four neighbours through open edges, and diagonally only where both L-shaped paths through the two side cells are open (never through a split cell), so a diagonal never cuts a land corner. Cost is distance times a coast penalty (up to 1.4× next to land, fading out 4 cells off), with a heuristic weight of 1.1 (corridor cost at most 10 % above optimal; measured +0.3 %). The search window grows from the legs' box until the path is found (at most 12 M cells, about 84 MB while it runs) and wraps round the antimeridian.
  • Narrow passages. Per grid cell the build takes the largest distance to land of its fine water cells (the clearance) and runs a merge tree in local windows (2° cores, 1° margin): cells are added from the widest water down, and a cell that joins two basins whose widest water is at least 1.5× its own clearance (and 500 m wider, and basins at least 2 km wide) is a passage's narrowest point. Windows are local on purpose: Messina joins the Tyrrhenian and the Ionian, which also connect round Sicily. 4987 passages up to 40 km wide are stored with position, width and channel axis; names come from a table of well-known straits.
  • Canals. Known ship canals (Corinth, Cape Cod, Chesapeake and Delaware, Kiel, Suez, Panama) are stored as the edges their cut lines cross, closed unless Allow canals is on. With GSHHG none of them is open water at 0.005° (Cape Cod and Corinth only look open when a test box lets the water go round the cape or the Peloponnese); three edges near the Panama Canal's approaches are recorded, but the canal is closed anyway. The setting matters with coastline data that includes canals (e.g. OSM land polygons).
  • Memory and loading. The route worker loads it once (about 20 ms to decompress, into one buffer): 62.9 MB of arrays (three 20.25 MB bit planes, 1.3 MB of split cells, the passage list) plus 2.3 MB of lookup maps; measured process RSS +73 MB. The data worker does not load it.
  • Rebuilding. The file records the shapefiles it was built from (name, size, modification time and a SHA-256 of the size and the first and last MiB). When the configured landShapefiles differ (another GSHHG resolution, L6 Antarctica added, OSM land polygons), the route worker keeps routing with the shipped grid and rebuilds a matching one in a background thread into the plugin data directory, then switches to it. A rebuild needs about 400 MB while it runs (checked against "memory kept free" first) and took 74–79 s for GSHHG full L1 and 47 s for GSHHG high on an Apple M3; not measured on a Raspberry Pi 5 (expect several minutes). To rebuild the shipped file: npm run build:water-grid -- --land /path/GSHHS_f_L1.shp; npm run check:water-grid runs the connectivity checks.

Install

Requirements

  • Signal K server 2.24.0 or later (the configuration panel uses the React 19 Admin UI that came with 2.24.0); tested on 2.33.0. Read-only users can use the web app on servers that support per-route access (see API); on older servers every route is admin-only.
  • Node.js 20.10 or later (the server's own Node).
  • Internet access for the forecast, current, tide and coastline downloads.
  • Disk in the Signal K data directory: the decoded forecast (about 1.35 GB for 72 h with the extra fields, 4.6 GB at 360 h), the GRIB files of the cached cycles, the coastline (about 156 MB when downloaded), current and tide caches, and the saved map tiles (up to the configured cap, 20 GB by default).
  • Memory: tested on a Raspberry Pi 5 with 8 GB. The resource guard keeps the configured amount free (default 1 GB) and refuses a forecast or a route that would not fit.

From the Signal K App Store

Admin UI → Appstore → Available, search for Weather Router Plus, Install, then restart the server. Enable the plugin in Server → Plugin Config; the web app appears on the Webapps page.

From source (development)

git clone https://github.com/motamman/signalk-weather-router-plus.git
cd signalk-weather-router-plus
npm install
npm run build

Then add it to the server as a local package: in ~/.signalk/package.json add "signalk-weather-router-plus": "file:/path/to/signalk-weather-router-plus" to dependencies, run npm install in ~/.signalk, and restart the server. Avoid npm link and npm install <tarball> in ~/.signalk: both can remove other plugins that are not listed in its package.json. After a code change, npm run build and restart.

First start

Coastline: with no coastline shapefile configured, the plugin downloads GSHHG 2.3.7 (Wessel & Smith, LGPL) once from the authors' site, https://www.soest.hawaii.edu/pwessel/gshhg/gshhg-shp-2.3.7.zip (149 MB; if that fails, the identical copy at https://router.zeddisplay.com/downloads/gshhg-shp-2.3.7.zip; the archive's SHA-256 is checked either way), extracts the full-resolution level-1 shoreline (GSHHS_f_L1.shp with its .shx and .prj, about 156 MB) into coastline/gshhg-2.3.7/ in the plugin data directory, deletes the archive and starts; the plugin status shows the progress. The global water grid shipped with the plugin was built from this same file, so it is used as is. The download does not hold up the server's start-up; if it fails (offline, server error, short file) the plugin status says why and it is tried again every 10 minutes (or at once with Download coastline in the plugin's configuration panel); stopping the plugin cancels it. To use another coastline, or an existing GSHHG copy, set its path in the plugin configuration. A configured coastline is never replaced by the download: if a configured file is missing or unreadable, the plugin does not start and its status names the file.

The package carries the signalk-webapp keyword and a public/ folder, so after the restart the webapp appears on the Admin UI's Webapps page. Writes (computing, cancelling, publishing) need a readwrite login; the page redirects to the server login when it gets a 401. A polar file (.csv or .pol, knots) enables sailing; without one every route is motor-only.

Configuration

Settings are split in two.

Signal K plugin configuration (Admin UI → Server → Plugin Config): installation settings only. The plugin ships its own configuration panel (keyword signalk-plugin-configurator, public/remoteEntry.js), which the Admin UI shows in place of the generated form: the coastline with a Download coastline button and its progress (and Use the downloaded coastline when a path is set), the map overlay cache with the radius and window in the Signal K user's distance and time units (stored in m and s; a unit that cannot be read shows "—" and cannot be edited there), and the other options below. Save stores the configuration and restarts the plugin. The panel is a hand-written Module Federation container that uses the Admin UI's own React, so the package carries no React and needs no build step for it.

| Field | Notes | |---|---| | landShapefiles | comma-separated absolute paths; blank = download GSHHG 2.3.7 full-resolution level 1 once (see Install) | | polarFile | .csv (twa/tws,4,6,…) or .pol (tab-delimited); the default polar (token default). Blank = the bundled Catalina 36 | | polarsDir | directory of .pol/.csv polars listed by /api/polars. Blank = the library bundled with the plugin (data/polars/: the ~700 polars of the OpenCPN weather_routing_pi library, GPL-3.0), with user polars (generated ones included) kept in polars/user/ in the plugin data directory, so an update never removes them. Set, user polars are in <polarsDir>/user/ | | currents.harmonicDir | directory of tidal-harmonic .npz files | | forecast.mirror | ecmwf, aws or google | | weatherProvider.enabled | register with the Weather API (default on) | | overlayCache.enabled | build map tiles ahead of time (default on) | | overlayCache.radius | m, around the boat and the map view at zoom 8 and below; halved at each deeper zoom (default 250000, 1000–2000000) | | overlayCache.window | s, how far ahead tiles are built from now; 0 = the whole forecast (default 0, max 1296000) | | overlayCache.maxZoom | deepest zoom built ahead (default 15, 6–18) | | overlayCache.diskCap | bytes for saved tiles, least recently used removed first (default 20e9 = 20 GB, min 100e6) | | overlayCache.workers | threads building tiles ahead (default 2, 1–8) | | overlayCache.followView | also build around the area the map shows (default on) |

Stored in SI (metres, seconds, bytes); the configuration panel shows the radius and window in the user's units and the disk cap in GB. Tiles the page asks for are saved, whether or not tiles are built ahead, except those computed while an on-demand current or tide area is still loading: those are answered but not saved. What is built ahead of time: the colour layers the page draws (wind, waves, sea state, current, rain, air temperature, sea temperature, tide height), wind barbs, current arrows and coastline tiles, for every hour from now to the end of the window (tide height: to the end of the tide run), at zooms 6 to maxZoom. Order: the map view's area, then the boat's; within each, nearest hour first, then shallower zoom, then nearest the centre. Tiles already saved are skipped. A new forecast cycle, currents run or tide run removes the tiles made from the old one and the walk starts again; so do a new hour, a boat move of more than 1 km, a new view and a settings change. No new tile is started while a route runs or the map's own queries are waiting. The boat's last position is kept in last-position.json in the plugin data directory, so the boat's area is known after a restart before a fix arrives.

Web-app settings (the webapp's Settings tab, or GET/PUT /api/settings): stored on the server in settings.json in the plugin data directory and shared by every client. Values are SI on the wire (m, m/s, s; degrees for the heading increment); the page shows them in the Signal K user's unit preferences. Saving needs a readwrite login.

| Group | Settings (default) | A change… | |---|---|---| | vessel | speed under power (6 kt = 3.087 m/s), polar performance (1 = 100%, 0.3–1.2) | applies to the next route | | forecast | horizon (72 h = 259200 s, 3–360 h; above 144 h only 00z/12z cycles qualify), check interval (60 min), cached cycles kept (2), extra fields (on), memory kept free (1 GB = 1e9 B) | horizon / extra fields / memory kept free reload the forecast; the interval restarts the timer | | currents | SMOC on, SMOC horizon (72 h = 259200 s, 6–240 h), SMOC step (3 h = 10800 s; 1 h or 3 h only), SMOC area half-width (15°, 2–30°), RTOFS on, RTOFS product (west_atl, …), RTOFS horizon (72 h), RTOFS step (3 h) | reloads currents | | tides | Copernicus Marine sea level on, tide map area half-width (15°, 1–30°), tide map horizon (24 h = 86400 s, 6–240 h) | reloads tides only | | routing | stages (20), subsectors (30), headings (30), heading increment (1°), sail threshold (4.9 kt), simulation step (200 m), land raster cell budget (25 M), allow canals (off), route simplification (10 m, 0 = off), shortcut smoother (off), comfort weight (1, 0 = off), shortcut may be slower by (0.05 = 5%), finished routes kept (50), max wind (none), max wave height (none) | applies to the next route | | publish | save to the Resources API (on), route name prefix (WRP), notifications (on) | applies to the next route |

Resource guard. The decoded forecast is on disk, so the guard checks what actually needs memory. Before a forecast update: the streaming decoder's one-step block and buffers (66 MB with the extra fields) against the memory available now (Linux MemAvailable, bounded by a cgroup (container) limit, or reclaimable pages from vm_stat on macOS), leaving "memory kept free"; and the decoded run's exact size (fields × steps × 4.15 MB) against the free disk space, leaving 1 GB. Before a route: its corridor store (area × 5 fields × steps × 4 B) aga