tankersurf — how it works

← back to the map

Every number on the map comes from one live AIS feed and a chain of five steps. This page explains each of them, and is wired to the running configuration — the constants below are read from the server, so they can't drift away from the model actually in use.

1 · Where the data comes from

A single WebSocket feed of live AIS, filtered to a bounding box over the strait and its approaches. Ships broadcast two kinds of message we care about:

messagecarrieshow often
PositionReportposition, speed over ground, course over ground, navigation statusevery few seconds to 3 minutes, depending on speed
ShipStaticDataname, dimensions, draught, ship type, and a hand-typed destinationabout every 6 minutes

A ship is only usable once both have arrived — which is why a freshly appeared vessel can sit on the map for a few minutes before it earns a forecast.

Coverage is from shore-based receivers, so the middle of the strait is solid but the offshore approach west of Cape Flattery is patchy. Ships tend to appear as they come into range rather than at the edge of the box.

The feed's own connection limits, and how this client respects them, are in section 9.

2 · Which ships count

Kept: anything or longer. If a ship hasn't broadcast its dimensions yet, its AIS type stands in — tanker, cargo and passenger ranges are kept, everything else waits. Dropped: ferries, tugs, fishing boats, and anything making less than , which covers vessels at anchor and alongside. A typical moment in the strait has 60–90 vessels of which 5–10 survive this filter.

Filling in missing particulars

Dimensions arrive in ShipStaticData, roughly every six minutes, so a ship that has just appeared may have a position but no length — and therefore no wake estimate. There is no free vessel-particulars API worth wiring to; the lookup services are all paid or credit-metered. The cheaper answer is simply never to throw the data away. data/registry.json keeps every set of particulars the feed has ever broadcast, keyed by MMSI, and is never pruned even though the live picture is. The same ships work this strait week after week, so after a few days of listening a returning vessel is fully described the moment its first position arrives. Rows filled this way are marked fromRegistry.

3 · Snapping to the traffic lanes

Ships don't wander — they follow the traffic separation scheme. Inbound traffic keeps to the south side and outbound to the north, which is the single most useful fact in the whole model: it's why the US-side spots in the strait see inbound ships close aboard and outbound ones much further out. Past the fork the channels narrow to two or three miles, and there both directions pass within about a mile of the beach.

Lane centrelines and the watch spots, drawn from the geometry the forecaster is using right now.

Each ship is projected onto the centreline for its direction. That gives two numbers: how far along the route it sits, and how far off the centreline it is. More than 8 nm off and it isn't following that lane at all, so it's ignored.

Inbound or outbound is read the same way: the ship's course is compared with the direction of the lane at the point it snaps to. Within 60° of the lane is inbound, within 60° of the reverse is outbound, and anything in between is crossing traffic — a ferry, a pilot boat, a ship turning. This matters past the fork, where Admiralty Inlet runs south-south-east and Haro Strait runs due north; a fixed east-or-west compass rule would drop every ship in the narrows.

Where the spots come from

A spot is only useful if a ship passes close enough to paddle out to — about a mile. node index.js discover finds those places from the data rather than a chart: it takes the US shoreline from OpenStreetMap, joins each large ship's logged fixes into a track, and for every point on the shoreline counts the ships that passed within a mile. The best stretches are spaced at least 3 nm apart and listed with their ships per day, median passing distance and the direction the beach faces. The Admiralty Inlet, Haro Strait and Rosario Strait spots came out of this. In the main strait nothing qualifies — the lanes run 2–4 nm offshore — so those spots are kept for their long warning, not their distance.

The same pass is a check on the lanes: it reports how far the modelled lane runs from each candidate. Where ships demonstrably pass a beach but the lane doesn't, the forecaster would mis-time them, so the lane gets fixed before the spot is added.

4 · The fork — where kinematics runs out

At the east end, near Hein Bank, inbound traffic splits three ways: Admiralty Inlet for Puget Sound, Rosario Strait, or Haro Strait for Vancouver. This is the one genuinely uncertain thing in the forecast, and it matters enormously — the same ship passes Point Wilson at 0.8 nm if it takes Admiralty, and misses it by 12 nm if it takes Haro.

The fork is resolved by a strict priority ladder. Position beats paperwork:

  1. Geometry, . Once a ship's cross-track separation between branches exceeds , it has committed. Read straight off position — the stated destination is not consulted at all. A ship already inside Haro Strait still broadcasting "US SEATTLE" is called Haro. There's a test for exactly this.
  2. Stated destination, . Only while a ship is still west of the fork, where geometry genuinely cannot know. Matched by substring against a table of port names and UN/LOCODEs. Capped below certainty because the field is typed by hand and goes stale.
  3. Prior traffic split. When the field is blank or unrecognised: . These are estimates, not measurements — the first thing worth replacing from your own logged history.

The branch_basis column in the CSV tells you which rule fired for every row.

Kinematics does all the timing; the destination only ever picks a branch. The AIS message also contains the ship's own Eta field, and this model ignores it entirely — that's an ETA to a destination port, not to your beach, and it is frequently stale.

A spot west of the fork is passed whichever branch a ship eventually takes, so those predictions merge into one row labelled branch: any and the probabilities add back up to roughly 1. A spot east of the fork carries the full branch risk.

5 · Closest approach, and the ETA

With a route chosen, the forecaster walks it forward in 0.2 nm steps looking for the point where it comes nearest the spot — the closest point of approach. That gives both the distance the ship will pass at, and the run distance to get there. Speed does the rest:

eta = now + run_distance ÷ speed_over_ground

Speed is clamped to a sane 5–25 kn, because a garbled report of 0.1 or 40 knots shouldn't produce an ETA next week or in four minutes.

6 · The two kinds of doubt

These are deliberately kept apart, because they answer different questions.

Will it pass at all? — the probability

termwhat it accounts for
branch_probthe fork, from the ladder above
nav_status under engine, less for anything else
decaythe chance per hour that a transit changes materially — anchoring off Port Angeles for a pilot or bunkers, slowing for traffic, diverting
stalenesstrust decays once the last fix is over 15 minutes old

How fast is it really going?

The broadcast speed is instantaneous speed over ground, which is not the same as progress along the route — a ship yawing, cutting a corner, or being set by the tide closes on a spot at a different rate than its speedometer suggests. So each ship's own recent track is used to measure speed made good along the lane: how far along the route it has actually travelled, divided by how long that took.

speed made good = Δ(distance along route) ÷ Δ(time) over the last

When the measurement and the broadcast figure agree, the measured one is used. When they disagree sharply, one of them is wrong and there is no way to tell which, so the row keeps the broadcast speed and widens its window instead — the speed_basis column says which of the three happened.

The leg-by-leg speeds also give the scatter, which replaces a guessed uncertainty with a measured one: a ship that has held a steady speed for twenty minutes earns a tighter window than one that has been surging, rather than both getting the same assumed .

When exactly? — the window

Separate from the above, and shown as the 80% interval on the map:

The window widens with lead time — a ship two hours out is worth planning around loosely; one twenty minutes out is worth walking down to the beach for.

7 · The wake itself

This is the part that changed most, and it splits cleanly into physics that needs no calibration and one heuristic that does.

When the wake actually arrives physics

A ship's wake is bounded by the Kelvin wedge — a wake pattern sits inside a half-angle of 19.47° either side of the track, and that angle is fixed, independent of the ship's speed or size. So the wake reaches a beach that is d off the lane well after the ship has gone past:

delay = d ÷ (V × tan 19.47°) ≈ 2.83 × d ÷ V

This matters more than anything else on this page. At New Dungeness, 2.1 nm off the inbound lane, a 13-knot ship's wake lands about 27 minutes after the ship is abeam. Off Freshwater Bay at 3.9 nm it is closer to 50 minutes. Timing a walk down to the water off the ship's own ETA gets you there far too early — so every time shown on the map and in the board is the wake arrival, with the ship's abeam time given alongside it.

What the waves look like physics

Transverse waves in a Kelvin pattern travel at the ship's own speed, which pins their wavelength and period to speed alone — nothing about the hull enters:

wavelength = 2πV² ÷ g period = 2πV ÷ g

So a ship at 13 knots throws a 4.3-second wave and one at 18 knots a 5.9-second wave. Longer-period waves shoal and break better, and lose less energy on the way in, which is why speed matters out of proportion to its effect on the ETA.

How big heuristic

Displacement is estimated from the broadcast hull dimensions:

displacement ≈ Cb × length × beam × draught × 1.025

The block coefficient Cb comes from the ship type, with a wrinkle: AIS lumps container ships and bulk carriers into one cargo code, so the length/beam ratio breaks the tie — slender hulls above 7:1 are treated as container ships at 0.65, beamier ones as bulkers at 0.82. If draught hasn't been broadcast, L/19 stands in and the row is flagged draught_estimated.

The 0–100 score itself is a ranking aid, and it is worth saying plainly why it doesn't simply follow the textbook. The published deep-water forms predict wave height relative to hull length, which makes a small fast ferry outrank a loaded 366-metre container ship. That is correct for shoreline erosion — it is why fast ferries get speed-restricted — but wrong for someone waiting on a beach, who cares about the energy in the whole train and how long the waves are. So the score leans on displacement, softens the Froude term, and rewards period:

score ∝ √(displacement) × (Fr ÷ 0.18)^1.5 × (cpa ÷ 2nm)^(-1/3) × period-bonus

The distance term is the d^(-1/3) decay of the divergent waves. Treat the number as an ordering, not a wave height.

8 · Conditions when the wake lands

Knowing a wake is due at 06:40 is only half the answer; whether it is worth walking down for depends on what the water and sky are doing at that hour. Each passage therefore carries a weather lookup — taken at the wake's arrival hour, not the ship's, which can be the better part of an hour earlier.

whatwhy it matters
wind speed, direction and gustsstrength and, more importantly, which way relative to the beach
offshore / cross / onshoreoffshore wind blows from the land over the incoming wave and grooms its face; onshore chops it up. Each spot carries a facingDeg — the direction it looks out toward — and the wind is classified against it
ambient wave heightthe sea the wake has to stand out from. A 1.5 m sea at Neah Bay will swallow a wake that would be obvious in the 0.2 m water off Freshwater Bay. The weather service forecasts height for inland waters but not period
cloud, precipitation, daylight, temperaturewhether you can see it, and what it costs you to stand there

On the map these are drawn rather than written out. The wind arrow points the way the wind is blowing — downwind — which is the opposite of the meteorological convention of naming the direction it comes from, because downwind is what you can read at a glance against the shape of the coast. The shaft thickens with strength, and the whole group takes the offshore/cross/onshore colour. The sea glyph gains a line as the ambient sea builds: one line below 0.3 m, two below 1 m, three above. The sky glyph follows cloud cover, switches to a moon outside daylight hours, and becomes a rain cloud when precipitation is forecast. Hovering any of them gives the numbers, and the map legend spells the whole set out.

In the passage list the icons stack in the left column beneath the arrival time, which the row already reserves — wind on one line, sea and sky sharing the next. That fills space the layout was wasting instead of adding height, so the conditions cost nothing. The spot popup is the expanded view, where the same readings get their words and full numbers.

Forecasts come from the US National Weather Service (api.weather.gov), which is free, needs no key, and is public-domain data that may be used commercially. It covers the US only, which is why every spot is on the US side. Each spot is looked up once to find its 2.5 km forecast grid cell, and that is remembered in data/nws-points.json; after that a refresh is one call per grid cell, cached for . The grid comes in runs of varying length — three hours of one wind speed, then two of another — which are laid onto an even hourly axis, with rainfall totals spread across their run. Daylight is worked out from the sun's position rather than fetched. If a lookup fails, rows simply carry no weather rather than the forecast failing.

9 · What gets stored

No database — flat files:

fileshapepurpose
data/state.jsonsnapshot, overwritten every 30sthe current picture; lets the forecaster run as a separate process
data/tracks.jsonlappend-only, one line per vessel per minutehistory — what the lane refit reads, and what any future climatology would use
data/registry.jsonMMSI → particulars, never prunedremembers hull dimensions so returning ships are described immediately
data/nws-points.jsonspot → weather grid cellthe one lookup per spot the weather service needs, kept across restarts
data/coastline.jsonOpenStreetMap shorelinefetched once by discover to score beaches against ship tracks
data/users.jsonemail → access statuswho has asked for access and who's been let in, when sign-in is on

10 · Staying inside the feed's limits

The feed is rate-limited and carries no SLA, so the client is built to stay inside its published limits rather than discover them:

limitwhat this app does
a small number of concurrent connections per account and per IPdata/feed.lock holds the pid of whichever command owns the feed; a second listen, serve, run or check refuses to start rather than quietly opening a second connection
subscription must arrive within 3 s of connectingsent in the socket's open handler, before anything else
at most one subscription update per secondthe subscription is sent once per connection and never updated
messages are dropped if you don't read fast enoughframes are parsed synchronously; a full forecast pass costs about 0.5 ms, so the event loop is never blocked long enough to build a backlog
reconnect with exponential backoff and jitterbackoff doubles to a 60 s ceiling with ±30% jitter, and resets only once data actually flows again — not merely on a connection that opens and then sits silent
don't connect from the browserthe key stays on the server; the map is fed over server-sent events and never sees it

There is no durable replay and no uptime guarantee, so gaps are expected and every reconnection re-sends a complete subscription.

Forecasts are written to out/passages.csv, out/passages.json and out/board.txt every .

11 · Known limits

Not for navigation. This is a wake-watching toy built on a free best-effort feed.