Sailing calculations¶
| Garmin WatchApp | Wear OS WatchApp | Apple Watch | Garmin DataField | Server | |
|---|---|---|---|---|---|
| Status | ✅ | ✅ | ⏳ | 🟡 (COG and baro arrow only) | 🟡 (own rules, see below) |
| Code | source/CogCalculator.mc, source/BaroIndicator.mc, source/SunCalc.mc, source/MoonCalc.mc, source/DataModel.mc, source/TripContext.mc |
core/…/CogCalculator.kt, core/…/BaroIndicator.kt, core/…/SunCalc.kt (SunCalc, MoonCalc), wear/…/recording/TripEngine.kt |
— (SailEverCore) |
source/CogCalculator.mc, source/BaroIndicator.mc, source/DataModel.mc |
app/services/sailever/sltb/kinematics.rb, app/services/ingestion/derivation.rb, app/services/ingestion/track_recompute.rb, app/services/trip_bridges/rebuild.rb, app/services/engine_detector.rb |
Verified against code: 2026-10-11
Purpose¶
The sailing maths that more than one product uses. Each section says the rule, then which products implement it and where they differ. Where a value is shown is in the screen specs.
Units¶
- 1 nm = 1852 m. Knots = m/s × 3600 / 1852.
- Pressure in hPa (sensors give Pa on Garmin, hPa on Wear OS, kPa on Apple).
- Angles in whole degrees [0, 360), 0 = north, clockwise.
COG and turn detection¶
Source. COG is the GPS direction of travel, never the compass. It is used only at SOG ≥ 1.0 kn and with GPS quality ≥ 2; below that the GPS course is noise or switches to the compass.
Smoothing. The shown COG is the circular mean of the last 5 headings (1 Hz, about 5 s). Angles are averaged as unit vectors (sum of sin, sum of cos, then atan2), so 350° and 10° give 0°, not 180°. Result truncated to whole degrees.
Reset. Whenever there is no valid heading (too slow, no fix), COG shows nothing, the turn angle is 0, and the whole history is dropped. Neither the mean nor a turn spans a gap.
Turn detection (tack or jibe):
- The last 30 raw headings are kept (1 Hz, about 30 s). No turn until 30 are in.
- before = circular mean of the oldest two, after = circular mean of the newest two, each rounded to whole degrees.
- Turn = shortest signed difference after − before, in [−180, 180]; positive = clockwise (to starboard).
- Reported only when |turn| > 30°, otherwise 0.
The track view shows the turn: ../watchapp/track-view.md. The DataField shows it in its own layout: ../datafield/screen.md.
| Product | Heading source | Differences |
|---|---|---|
| Garmin WatchApp | Position.Info.heading |
— (reference) |
| Wear OS WatchApp | Location.bearing, or derived from the previous fix when missing (../watchapp/recording.md) |
GPS quality is mapped from accuracy in metres |
| DataField | Position.Info.heading |
same results; older code layout |
| Server | bearing between neighbouring points | no smoothing, no turns; see SOG and COG on the server |
| Apple Watch | ⏳ | — |
Barometer trend¶
Sampling. Pressure is read every second, but only kept as a trend sample at an interval that grows with the time since recording began:
| Phase (time since start) | Sample every | Max samples | Span | Sensitivity |
|---|---|---|---|---|
| 0–30 min | 2 min | 15 | 30 min | 0.2 hPa |
| 30–120 min | 5 min | 18 | 90 min | 0.4 hPa |
| from 120 min | 10 min | 36 | 6 h | 0.6 hPa |
Each sample keeps the sensitivity of its phase. When the buffer is full, the oldest sample goes. The first sample is taken one interval after start. Samples are raw readings, not smoothed.
Arrow. Compares the newest sample with every older one: - With fewer than 3 samples: sideways, blue. - For older sample i (0 = oldest) of n: threshold = sensitivity of the newest sample × (0.7 + 0.3 × (i + 1) / (n − 1)). Newer samples get a higher threshold. - Count the older samples clearly below (newest − older > threshold) and clearly above (< −threshold). - ≥ 60 % below → arrow up; ≥ 60 % above → arrow down. From 80 % the arrow is red, otherwise yellow. - Otherwise sideways, blue.
Arrow colours: red 0xFF0000, yellow 0xFFAA00, blue 0x00AAFF (Connect IQ named colours). Shapes: up = triangle pointing up; down = triangle pointing down; sideways = triangle pointing right, half the height.
Storm drop (WatchApp alert): pressure of the oldest sample − pressure of the newest > limit (user setting, default 1.5 hPa), with at least 2 samples. See ../watchapp/alerts-and-auto-stats.md.
Server storm warning: a different rule. Any two stored points with a baro value at most 3 h apart where the later one is more than 1.5 hPa lower. The first such point gets a storm_warning event and the track a storm flag. The threshold is fixed. SLTB points carry the watch's smoothed pressure (EMA α = 0.2); FIT points carry the raw value.
| Product | Arrow | Storm drop | Notes |
|---|---|---|---|
| Garmin WatchApp | ✅ | ✅ alert | phases restart when the app is reopened |
| Wear OS WatchApp | ✅ | ✅ alert | monotonic clock; no barometer → no samples |
| DataField | ✅ | — | phases count from when the DataField is loaded |
| Server | — | ✅ own 3 h rule | — |
| Apple Watch | ⏳ | ⏳ | CMAltimeter |
Sunrise and sunset¶
The NOAA low-precision solar algorithm, computed on the watch from the position and the local date. No network. Good to about a minute at sailing latitudes.
- Zenith 90.833° (refraction and the sun's radius).
- Day of year from the local calendar date.
- The UTC result is shifted by the watch's current time-zone offset.
- When the sun does not cross the horizon (midnight sun, polar night) there is no time.
- Text:
HH:MM, both zero-padded, minutes truncated (not rounded);--:--when there is no time.
Implemented in Garmin and Wear OS WatchApp (identical). Not in the DataField or the server. Apple ⏳. Display: ../watchapp/environment.md.
Moon phase¶
- Phase fraction = (days since the new moon of 2024-01-11 11:57 UTC, Unix 1704974220) / 29.53058867, fractional part, 0 = new moon.
- Names by fraction:
| Fraction | Name | Short name |
|---|---|---|
| < 0.03 or ≥ 0.97 | NEW MOON |
NEW |
| 0.03 – 0.22 | WAXING CRESCENT |
WAX CRESC |
| 0.22 – 0.28 | FIRST QUARTER |
FIRST QTR |
| 0.28 – 0.47 | WAXING GIBBOUS |
WAX GIBB |
| 0.47 – 0.53 | FULL MOON |
FULL |
| 0.53 – 0.72 | WANING GIBBOUS |
WAN GIBB |
| 0.72 – 0.78 | LAST QUARTER |
LAST QTR |
| 0.78 – 0.97 | WANING CRESCENT |
WAN CRESC |
Lower bound inclusive. Quarters and full get a narrow band because they are only true for about a day. Short names are for small screens. Implemented in Garmin and Wear OS WatchApp (identical). Apple ⏳.
Distance¶
| Where | Rule |
|---|---|
| WatchApp, Garmin | the device odometer of the FIT session (elapsedDistance) |
| WatchApp, Wear OS | computed from fixes: accuracy ≤ 30 m, and speed ≥ 0.3 kn or no speed |
| DataField | Garmin's elapsedDistance of the host activity |
| Server, track | END distance_m when > 0 and the track was saved (not recovered), or the FIT session distance of an uploaded file, distance_source = device; otherwise the haversine sum over the points (computed) |
| Server, trimmed track | the device distance × (haversine of the kept part / haversine of the whole track), distance_source = device_scaled; computed when there is no device distance |
| Server, trip | sum of the tracks plus the bridges |
Details of the watch: ../watchapp/recording.md. Trim (IN / OUT): ../server/track-editor.md; a bridge connects one track's last kept point to the next track's first and counts as distance, never as time; rules: ../server/ingestion.md.
Sailing time and engine time¶
WatchApp (shown on the watch, sent in END): - Each unpaused second counts for the current vessel state: sailed (Sail), motored (Engine), motorsailed (Motorsail) or stationary (all others). - Distance gains count the same way; stationary distance counts only in the total. - No speed threshold.
Server (logbook numbers) from consecutive point pairs of the kept part:
- Pairs more than 300 s apart are skipped (no time, no split distance).
- Engine when the first point is engine_on or in Engine / Motorsail: the pair's time and distance are engine.
- Otherwise the distance is sailing distance, and the time is sailing time only when the first point's SOG ≥ 0.5 kn.
- Sailing and engine distance are scaled by track distance / haversine distance, so they never add up to more than the track.
- The server ignores the watch's END time totals.
| Rule | WatchApp | Server |
|---|---|---|
| Motorsail | own bucket, added to "motored" in trip totals | engine |
| Stationary states | own bucket | sailing bucket (see Known deviations) |
| < 0.5 kn | counted | left out of sailing time |
Engine detection¶
Server only, for the track editor's Analyze track (EngineDetector): a suggestion of where a track was under engine and where under sail, from the positions alone. The skipper checks and corrects it; it is never applied without saving the editor. It reads the speed the way a skipper does: under engine the speed is dead even, and the engine going on or off shows as a step in the speed; under sail the speed varies and builds or fades with the wind.
- Positions resampled to every 10 s (linear, not across gaps over 120 s); speed from the positions (central differences, 3-sample mean). Then minutes: the mean speed of each minute.
- Variation of each minute: over the ±5 min around it, the speed's standard deviation after removing a straight-line trend, divided by the mean speed (at least 0.5 kn).
- Step at each minute: |mean speed of the 5 minutes before − of the 5 after| / the higher of the two (at least 1 kn). A step of 0.375 or more is a speed step (engine on or off).
- Per moving minute (mean > 1.5 kn, not in a gap): z = 0.1058 − 1.1858 · (ln(variation + 0.001) + 3.3395) / 1.2219, P = 1 / (1 + e^−z); below about 4 % variation the minute leans towards engine.
- A two-state smoother (forward-backward) over the moving minutes, the minute scores raised to the power 0.3. Switching state has a chance of 0.3 per minute within 2 minutes of a speed step and 0.001 elsewhere, so a slow build-up of speed under sail stays sail. Prior 0.3 for engine, 0.8 in the first and last 10 moving minutes (leaving and entering harbour).
- The likelier state per minute; runs shorter than 5 min join the one before. The first change is at the track's first point.
- Reliability = mean of max(P, 1 − P) over the moving minutes: the expected share of right minutes.
Fitted on 7 days with the skipper's own sail/motor times (a handwritten logbook, Cyclades 2023, Garmin FIT every ~20 s; and the owner's reading of a Mallorca day, 2025, 1 s): 97 % balanced accuracy on days left out of the fit, the reliability matching the observed share. The thresholds were chosen on the same days, so expect less on other boats. Harder cases: motoring into waves, and sailing that starts without a drop in speed. More features (jitter, variation relative to the trip, cruising speed, model wind) did worse. Details: ADR 011.
Average speed¶
| Where | Rule |
|---|---|
| WatchApp track | track distance × 3600 / track timer |
| WatchApp auto-stats | interval distance × 3600 / interval seconds |
| WatchApp state period | mean of SOG samples above 0 |
| DataField | elapsedDistance / timerTime of the host activity |
| Server | mean SOG of all kept points |
Max speed¶
| Where | Rule |
|---|---|
| WatchApp | highest 1 Hz SOG, unfiltered |
| Server | highest 30 s rolling mean of point SOG, capped at 999.99 kn |
SOG and COG on the server¶
SLTB carries no speed or course. The server derives them per point (Sailever::Sltb::Kinematics):
- SOG: central difference: haversine distance from the previous to the next point / time between them. A one-sided difference at track ends and across gaps longer than 3 × the sample interval. A point with no neighbour in reach gets none. Values above 100 kn are dropped as GPS jumps. Then a median-of-3 filter. Rounded to 0.01 kn.
- COG: initial bearing from the previous to the next point, rounded to 0.1°. None when SOG < 0.3 kn.
- Earth radius 6 371 008.8 m. Measured against device speed: median difference 0.19 kn.
FIT uploads keep the speed and course from the file.
Known deviations¶
- Server sailing bucket includes stationary states. Distance moved at anchor, on a buoy or adrift counts as sailing distance, and time there with SOG ≥ 0.5 kn counts as sailing time. The glossary says sailing time is time under sail.
- Average and max speed differ between products (table above), so the watch, the DataField and the logbook show different numbers for the same track.
- Baro storm rule differs between the watch alert (adaptive buffer, user limit) and the server warning (fixed 3 h, 1.5 hPa).