Skip to content

crossing detection

Greg Wilkins edited this page Oct 7, 2026 · 3 revisions

Crossing detection

How a boat decides, from raw GPS and with no network, that it crossed a line, which way, and at what instant. This is the part of the system that decides a race, and it runs on the boat.

The code is client/www/crossing.js (the detector and quality gates) and raceclient.js (RaceClient.accept, one fix through the whole pipeline). Its executable specification is crossing-test.js, run in a browser and in the Maven build. There is deliberately no server-side copy: the server stores what the boat decided and never re-runs it.

A mark is a line

A virtual mark is a line between two points, port and starboard. Either end may be infinite: the given point is then a bearing the line runs out through, not where it stops. A line replaces each kind of physical mark:

Mark As a line
start / finish a finite line, exactly as a committee boat and pin would set it
contested rounding a finite line across the approaching leg, crossed like a start line
lightly contested rounding a half-infinite line from where the buoy would be, running away from the approaching leg
passing mark a line parallel to the leg, off to one side
gate two lines either side of the gate, a boat crossing either
island a half-infinite line from the shore
a point rounded to port or starboard a short half-infinite line from the mark, out of the turn, resolved by the server (course-model)

The ends' names say which way to cross. Forward leaves the port end to port and the starboard end to starboard; reverse is the opposite. The names are relative to a forward crossing, so a line crossed both ways — a leeward line that is the start, mark 2 and the finish — is forward once and reverse twice.

Two tests: sense and extent

Test Question Involves the ends?
Sense Which way did the boat cross? No — the sign of a cross product against the port→starboard vector
Extent Did it cross the line itself, or its extension past a finite end? Only finite ends

Keeping these apart is what makes infinite ends work: an infinite end is always on the side its name says, so only a finite end can be missed. The approach screen's three states are the combinations — approaching (neither settled), crossed (both passed), missed (sense passed, extent failed).

A crossing is a segment intersection, not proximity. The segment between two successive good fixes either cuts the line or it does not. Testing proximity would miss a fast crossing of a short line and could not give a crossing instant at all.

The pipeline

Each fix goes through these stages, in order:

fix ─► quality control ─► relocation watch ─► local metres ─► live step's detectors ─► advance

1. Quality control: reject, never smooth

One bad fix is especially dangerous to a line test. A flyer — a single wild position — that lands on the far side makes the segment out and the segment back both cut the line, manufacturing a pair of crossings the boat never made. Fixes are gated, outermost first:

Gate Rejects Notes
Metadata too few satellites (minSatellites), stated accuracy worse than maxAccuracyM necessary but not sufficient: a wrong fix often still claims a good accuracy. A missing satellite count — which every phone browser gives — counts as no evidence either way
Kinematic a fix further from the last good one than the boat could have gone the primary defence. The budget is the distance at maxSpeedKn over the time between fixes, plus three standard deviations of the two fixes' stated accuracies combined in quadrature, so a fast fix rate does not make the gate reject honest noise

Nothing is smoothed. A filter strong enough to remove outliers also lags the true position and biases the crossing instant, which is the one number a result depends on. Rejected fixes are kept with their reason for the record.

2. Relocation: a jump the boat really made

The kinematic gate measures from the last accepted fix, so after a genuine jump — a dropout below decks or under a bridge, a phone asleep in a pocket — it would reject every fix forever. The relocation watch tells the two cases apart: a flyer disagrees with its neighbours; a relocation agrees with itself. N consecutive kinematically-rejected fixes that are each plausible from the one before are accepted as the boat's new position.

A relocation is never a crossing. The jump may sweep across any number of lines, so the detectors are reset and the track discarded. The live step does not advance: a crossing made during the blackout was not seen and has to be made again. Only a kinematic refusal can start a relocation; a run of two-satellite fixes is not evidence of anything.

3. One origin for the whole course

Every line is projected into one local tangent plane, in metres east and north, with its origin fixed when the boat joins. Distances measured against one origin and compared against another would be wrong by however far apart the origins are.

4. The detector: N and N, with one fix clear

Only the live step sees fixes. On a two-lap windward/leeward the leeward line is the start, mark 2 and the finish — one line, three steps, each with its own detector — so crossing it on the way to the windward mark cannot latch the finish early. Both sides of a gate have a detector and see every fix; the first to latch is the side taken. Before a cycle starts, each entry line can be the live one.

A crossing is valid when there are:

  1. at least N consecutive fixes on one side (confirmFixes, default 3),
  2. a segment that cuts the line, then
  3. at least N consecutive fixes on the other side, at least one of them clear of the line — further from it than half that fix's stated accuracy, or than accuracyBandM if the series sets one. The far-side run keeps counting until one is.

A single flyer cannot produce N fixes on the far side. A boat sitting on the line in the receiver's noise never produces a clear fix, so it never latches. A boat that dips over and comes back is logged as far side not confirmed. Once the far side is confirmed the candidate is settled, sense first:

Outcome Recorded as
wrong sense rejected, wrong sense — logged, not scored, and not a miss
right sense, outside the extent rejected, past the port/starboard end — the missed state
right sense, within the extent latched

A latch is permanent. Once a crossing in the required sense is confirmed it stands, and nothing later can undo it or move its time, so a late bad fix cannot corrupt anything.

5. The instant is interpolated

Whether a crossing happened and when are separate questions. The instant is never the time of a fix: it is the moment the segment between the fixes either side cut the line, interpolated along that segment. Taking a confirming fix's time instead would bias it early or late by up to a fix interval.

Elapsed time runs from the interpolated start crossing to the interpolated finish. Both come from the boat's own clock, so an offset in that clock cancels out.

Which side a fix is on, and whether it is clear

Test Used for
side() the sign of the distance to the line, with no band the runs the latch counts, and what the screens draw
clear() further from the line than accuracyBandM, or than half the fix's stated accuracy the one far-side fix the latch needs as evidence

A fix two metres over with five metres of stated accuracy is over, and counts towards the run, but it is not evidence of a crossing — the error alone could put it there. Half the accuracy, because the stated accuracy is a radius around the fix and the question is only whether the boat could still be on the near side.

One metre, a hard edge, and a warning

The whole system resolves to one metre (RESOLUTION_M, the same value in crossing.js and in Geo.java). GPS on a phone does not honestly resolve better, and a scoring edge that moved with the twelfth decimal place of a float could not be argued in front of a protest committee.

So a finite end is a hard edge: if you miss, you miss. There is no band of doubt at an end. Instead the boat is owed a warning: the boat's course over ground is projected onto the line, and the margin from that cut to the nearer finite end is reported, flagged near the end inside 50 m and beyond the end past it. The approach screen shows that as a ring that turns amber, then red, long before the boat gets there. An infinite end raises no warning, because it cannot be missed.

Tuning

The thresholds belong to the water and the devices on it, so they are set per series, travel inside every snapshot, and are read by the boat:

defaults:
  confirmFixes: 3          # N in the N-and-N rule
  accuracyBandM: null      # how far over one far-side fix must be; null for half its stated accuracy
  qc:
    minSatellites: 4
    maxAccuracyM: 25
    maxSpeedKn: 40         # the kinematic gate

Every one is a default set without data from the water — see open questions 1, 2 and 6.

What reaches the record

Each latched crossing is recorded with its interpolated instant, position and confirmation counts. Each quality-control rejection and relocation is recorded too, uncounted, with its reason — for example REJECTED_KINEMATIC: implied ground speed 63 kn — so a contested result has the evidence behind it.