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:
parent
ca62ed1aee
commit
9b9b8d08ca
4 changed files with 127 additions and 19 deletions
File diff suppressed because one or more lines are too long
|
|
@ -168,9 +168,10 @@ int? _wheelTurns(Recording recording) {
|
||||||
|
|
||||||
/// The wheel's circumference, in millimetres, [recording] measured by GPS
|
/// The wheel's circumference, in millimetres, [recording] measured by GPS
|
||||||
/// (D49): its track's length over the wheel's revolutions, where 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
|
/// covered the ride's moving time (`_gpsCovered`; a tunnel's straight line
|
||||||
/// as a smaller wheel), it went 2 km at least by both and they make a wheel
|
/// would read as another wheel), it went 2 km at least and they make a
|
||||||
/// (1.5 to 2.5 m); else null.
|
/// 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) {
|
int? measuredWheelMm(Recording recording) {
|
||||||
final gps = _metres(recording);
|
final gps = _metres(recording);
|
||||||
final turns = _wheelTurns(recording);
|
final turns = _wheelTurns(recording);
|
||||||
|
|
@ -178,11 +179,79 @@ int? measuredWheelMm(Recording recording) {
|
||||||
turns == null ||
|
turns == null ||
|
||||||
turns == 0 ||
|
turns == 0 ||
|
||||||
gps < 2000 ||
|
gps < 2000 ||
|
||||||
usesWheel(gps, wheelMetresOf(recording))) {
|
!_gpsCovered(recording)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final mm = (gps * 1000 / turns).round();
|
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
|
/// The metres the rower counted while not paused: each frame's growth over
|
||||||
|
|
|
||||||
|
|
@ -122,14 +122,21 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
group("the wheel's distance (D49)", () {
|
group("the wheel's distance (D49)", () {
|
||||||
/// A ride of [seconds] with GPS [gpsMetres] a second and the wheel
|
/// A ride of [seconds] with GPS [gpsMetres] a second, no fix between
|
||||||
/// turning twice a second, its 2.1 m round.
|
/// the seconds [lostFrom] and [lostTo], and the wheel turning twice a
|
||||||
Recording ride({required int seconds, required num gpsMetres}) => Recording(
|
/// second, taken for 2.1 m round.
|
||||||
|
Recording ride({
|
||||||
|
required int seconds,
|
||||||
|
required num gpsMetres,
|
||||||
|
int? lostFrom,
|
||||||
|
int? lostTo,
|
||||||
|
}) => Recording(
|
||||||
activity: Activity.cycling,
|
activity: Activity.cycling,
|
||||||
startedAt: start,
|
startedAt: start,
|
||||||
fixes: [
|
fixes: [
|
||||||
for (var s = 0; s <= seconds; s += 10)
|
for (var s = 0; s <= seconds; s += 10)
|
||||||
north(s, (s * gpsMetres).round()),
|
if (lostFrom == null || s <= lostFrom || s >= lostTo!)
|
||||||
|
north(s, (s * gpsMetres).round()),
|
||||||
],
|
],
|
||||||
wheel: [
|
wheel: [
|
||||||
for (var s = 0; s <= seconds; s++) Reading(time: at(s), value: s * 2),
|
for (var s = 0; s <= seconds; s++) Reading(time: at(s), value: s * 2),
|
||||||
|
|
@ -162,15 +169,39 @@ void main() {
|
||||||
() {
|
() {
|
||||||
expect(measuredWheelMm(ride(seconds: 200, gpsMetres: 4.2)), isNull);
|
expect(measuredWheelMm(ride(seconds: 200, gpsMetres: 4.2)), isNull);
|
||||||
expect(measuredWheelMm(ride(seconds: 600, gpsMetres: 8)), isNull);
|
expect(measuredWheelMm(ride(seconds: 600, gpsMetres: 8)), isNull);
|
||||||
|
expect(measuredWheelMm(ride(seconds: 1200, gpsMetres: 1.6)), isNull);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
test("one whose GPS fell short of the wheel's way (a tunnel) measures "
|
test('a smaller wheel than the one taken is measured all the same: '
|
||||||
'none: its quotient would read as a smaller wheel', () {
|
"GPS's track is held to time, not to the wheel taken", () {
|
||||||
// 3.6 m a second by GPS, 4.2 by the wheel: 86 %, a 1.8 m wheel.
|
// 3 m a second by GPS, two turns: a 20" wheel's 1.5 m.
|
||||||
final tunnel = ride(seconds: 1500, gpsMetres: 3.6);
|
final small = ride(seconds: 900, gpsMetres: 3);
|
||||||
expect(rodeByWheel(tunnel), isTrue);
|
expect(measuredWheelMm(small), closeTo(1500, 5));
|
||||||
|
// A child's 16" wheel, 1.25 m.
|
||||||
|
expect(
|
||||||
|
measuredWheelMm(ride(seconds: 900, gpsMetres: 2.5)),
|
||||||
|
closeTo(1250, 5),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('one whose GPS was lost for a while (a tunnel) measures none: the '
|
||||||
|
'straight line across would read as another wheel', () {
|
||||||
|
final tunnel = ride(
|
||||||
|
seconds: 1500,
|
||||||
|
gpsMetres: 4.2,
|
||||||
|
lostFrom: 600,
|
||||||
|
lostTo: 720,
|
||||||
|
);
|
||||||
expect(measuredWheelMm(tunnel), isNull);
|
expect(measuredWheelMm(tunnel), isNull);
|
||||||
|
// A fix lost now and then, as on any ride, still measures.
|
||||||
|
final patchy = ride(
|
||||||
|
seconds: 1500,
|
||||||
|
gpsMetres: 4.2,
|
||||||
|
lostFrom: 600,
|
||||||
|
lostTo: 620,
|
||||||
|
);
|
||||||
|
expect(measuredWheelMm(patchy), closeTo(2100, 5));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1187,10 +1187,10 @@ Strava. What was found, 27 Oct:
|
||||||
progress and live speed are the wheel's where GPS made less than 90 % of the wheel's
|
progress and live speed are the wheel's where GPS made less than 90 % of the wheel's
|
||||||
way (a turbo trainer, a tunnel, no fix), else GPS's (`usesWheel`, `rodeByWheel`).
|
way (a turbo trainer, a tunnel, no fix), else GPS's (`usesWheel`, `rodeByWheel`).
|
||||||
The circumference starts at 2105 mm (700×25C) and is learnt from rides with at least
|
The circumference starts at 2105 mm (700×25C) and is learnt from rides with at least
|
||||||
2 km of GPS whose quotient falls between 1.5 and 2.5 m: the first sets it, each later
|
2 km of GPS covering the ride (D56) whose quotient falls between 1 and 3 m: the
|
||||||
one moves it a quarter of the way (`measuredWheelMm`); one typed on the Record sheet
|
first sets it, each later one moves it a quarter of the way (`measuredWheelMm`); one
|
||||||
(1000 to 3000 mm) stays until cleared, which measures again. Auto-pause still follows
|
typed on the Record sheet (1000 to 3000 mm) stays until cleared, which measures
|
||||||
GPS alone on a trainer.
|
again. Auto-pause still follows GPS alone on a trainer.
|
||||||
- **D50 The instance's own map and searching for places** (recommended, the developer's
|
- **D50 The instance's own map and searching for places** (recommended, the developer's
|
||||||
but the search's region; 29 Oct; step 12, what D22, D23 and D44 left for later):
|
but the search's region; 29 Oct; step 12, what D22, D23 and D44 left for later):
|
||||||
- **Tiles** (12a): Protomaps' basemap (schema v4, zooms 0 to 15, ODbL, OpenStreetMap's
|
- **Tiles** (12a): Protomaps' basemap (schema v4, zooms 0 to 15, ODbL, OpenStreetMap's
|
||||||
|
|
@ -1416,6 +1416,14 @@ Strava. What was found, 27 Oct:
|
||||||
written, and the other version is offered instead (`LoopFailed('another update')`).
|
written, and the other version is offered instead (`LoopFailed('another update')`).
|
||||||
The relay's calls to Polar have timeouts, caps on their size and https on every
|
The relay's calls to Polar have timeouts, caps on their size and https on every
|
||||||
hop, one fetch per file at a time, and a limit per person.
|
hop, one fetch per file at a time, and a limit per person.
|
||||||
|
- **D56 The wheel learnt from GPS that covered the ride** (recommended, the developer's;
|
||||||
|
30 Oct; revises D49's measure): a ride measures the wheel where precise fixes followed
|
||||||
|
it for 95 % of its moving time (gaps of 30 s at most count, a pause cuts), not where
|
||||||
|
GPS made 90 % of the wheel's way at the circumference taken: that held every wheel to
|
||||||
|
the one taken, so a real wheel under some 1.9 m (20", 24", a child's 16") was never
|
||||||
|
learnt from the 2105 mm a sensor starts with. Its quotient may fall between 1 and 3 m,
|
||||||
|
as one typed may (`measuredWheelMm`, `_gpsCovered`). A tunnel's straight line still
|
||||||
|
measures nothing. The distance still follows `usesWheel`, against the wheel as learnt.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue