fix(train): the wheel is learnt from rides GPS covered, whatever its size (ADR 0030, D56)

The measure was held to GPS making 90 % of the wheel's way at the
circumference taken, 2105 mm to start: a real wheel under some 1.9 m fell
short of it on every ride and was never learnt. A ride now measures where
precise fixes followed it for 95 % of its moving time, its quotient between
1 and 3 m as one typed may be.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-30 16:01:40 +01:00
commit 9b9b8d08ca
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -168,9 +168,10 @@ int? _wheelTurns(Recording recording) {
/// The wheel's circumference, in millimetres, [recording] measured by GPS
/// (D49): its track's length over the wheel's revolutions, where GPS
/// covered the ride (its distance is GPS's: a tunnel's stretch would read
/// as a smaller wheel), it went 2 km at least by both and they make a wheel
/// (1.5 to 2.5 m); else null.
/// covered the ride's moving time (`_gpsCovered`; a tunnel's straight line
/// would read as another wheel), it went 2 km at least and they make a
/// wheel (1 to 3 m, as one typed may be); else null. Held to time, not to
/// the wheel taken: a smaller wheel's ride falls short of it.
int? measuredWheelMm(Recording recording) {
final gps = _metres(recording);
final turns = _wheelTurns(recording);
@ -178,11 +179,79 @@ int? measuredWheelMm(Recording recording) {
turns == null ||
turns == 0 ||
gps < 2000 ||
usesWheel(gps, wheelMetresOf(recording))) {
!_gpsCovered(recording)) {
return null;
}
final mm = (gps * 1000 / turns).round();
return mm >= 1500 && mm <= 2500 ? mm : null;
return mm >= 1000 && mm <= 3000 ? mm : null;
}
/// The longest GPS may go without a precise fix while moving, its track
/// still the way it went.
const _longestGap = Duration(seconds: 30);
/// The share of the moving time GPS must cover for its track to measure
/// the wheel.
const _coveredShare = 0.95;
/// Whether precise fixes followed [recording] for [_coveredShare] of its
/// moving time, from its first wheel reading to its last: the time between
/// two fixes counts where it is at most [_longestGap] and no pause falls
/// between them.
bool _gpsCovered(Recording recording) {
final wheel = recording.wheel;
if (wheel.length < 2) {
return false;
}
final from = wheel.first.time;
final to = wheel.last.time;
final pauses = recording.pauses;
var next = 0;
var paused = false;
Fix? last;
var covered = Duration.zero;
for (final fix in recording.fixes) {
if (fix.time.isBefore(from) || fix.time.isAfter(to)) {
continue;
}
while (next < pauses.length && !pauses[next].time.isAfter(fix.time)) {
paused = pauses[next++].paused;
last = null;
}
if (paused || fix.accuracyCm > maxAccuracyCm) {
continue;
}
if (last != null) {
final gap = fix.time.difference(last.time);
if (gap <= _longestGap) {
covered += gap;
}
}
last = fix;
}
final moving = _movingBetween(recording, from, to);
return moving > Duration.zero &&
covered.inMilliseconds >= moving.inMilliseconds * _coveredShare;
}
/// How long [recording] moved between [from] and [to], its pauses left out.
Duration _movingBetween(Recording recording, DateTime from, DateTime to) {
DateTime clamp(DateTime time) =>
time.isBefore(from) ? from : (time.isAfter(to) ? to : time);
var moving = to.difference(from);
DateTime? pausedAt;
for (final mark in recording.pauses) {
if (mark.paused) {
pausedAt ??= clamp(mark.time);
} else if (pausedAt != null) {
moving -= clamp(mark.time).difference(pausedAt);
pausedAt = null;
}
}
if (pausedAt != null) {
moving -= to.difference(pausedAt);
}
return moving;
}
/// The metres the rower counted while not paused: each frame's growth over