V3-01: activity type per ride
Adds Activity (motorcycle/bicycle/scooter/skateboard/running/walking/other) as a column on Trip, and threads it through as real behaviour rather than a label: ActivityProfile (domain/activity_profile.dart) carries a noise floor, accuracy gate, histogram bucket width, and elevation smoothing window/threshold per activity, consumed by sanitizeSpeedKmh, isUsableFix, computeSummary, Accumulator and speedHistogram. The motorcycle profile reproduces the exact constants the app shipped with before this existed, and a test asserts they never drift apart. This is the port's first real migration: schemaVersion 1 -> 2, m.addColumn(trips, trips.activity) with a motorcycle default so every existing row survives unmodified. Proven with a hand-built v1 SQLite file (raw sqlite3, not drift_dev's schema tooling) that a real database with real rides upgrades and keeps every trip, segment and point. No picker in front of Start: a new trip defaults to whichever activity was most recently used, derived live from the trips table rather than duplicated into a Config field. Editable afterwards on trip detail, which recomputes aggregates under the new profile immediately -- a walking pace that reads as noise under a motorcycle's floor reads as real movement once the activity is corrected, and there's a test proving exactly that transition. The widget-test pass for the activity-picker sheet caught a real overflow bug: seven options overflowed a Column-based bottom sheet the same way the record screen once did. Fixed with a scrollable ListView + isScrollControlled, same shape as that earlier fix. 188 tests (171 -> 188): 2 migration, 5 ActivityProfile, 3 profile-threading proofs in computeSummary, 5 repository, 2 widget. Analyze clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:rippr/src/domain/activity_profile.dart';
|
||||
import 'package:rippr/src/domain/models.dart';
|
||||
import 'package:rippr/src/stats/ride_statistics.dart';
|
||||
|
||||
@@ -299,4 +300,57 @@ void main() {
|
||||
expect(elevationProfile([point(ts: 0)]).length, 1);
|
||||
});
|
||||
});
|
||||
|
||||
group('activity profile changes real output (V3-01)', () {
|
||||
test('a slow walk reads as stationary under the motorcycle floor, but not under '
|
||||
"walking's own", () {
|
||||
// 20 fixes a second apart, each ~0.7 km/h -- real walking pace, comfortably under
|
||||
// the motorcycle noise floor (1.5) but above the walking one (0.3). This is the
|
||||
// whole point of activity profiles: without one, a recorded walk would show
|
||||
// 00:00:00 moving time despite every fix showing genuine movement.
|
||||
final points = List.generate(
|
||||
20,
|
||||
(i) => point(ts: i * 1000, lat: 51.0 + i * 0.0000065, speed: 0.7),
|
||||
);
|
||||
|
||||
final asMotorcycle =
|
||||
computeSummary(points, profile: ActivityProfile.motorcycle);
|
||||
final asWalking = computeSummary(points, profile: ActivityProfile.walking);
|
||||
|
||||
expect(asMotorcycle.movingMillis, 0,
|
||||
reason: 'the motorcycle floor must reject real walking speed as noise');
|
||||
expect(asWalking.movingMillis, greaterThan(0),
|
||||
reason: "the walking profile must recognise its own pace as movement");
|
||||
});
|
||||
|
||||
test('elevation window size changes what counts as a real climb', () {
|
||||
// A short, shallow rise across only a handful of samples. A wider averaging
|
||||
// window (running/walking) smooths it away as noise; the narrower motorcycle
|
||||
// window banks more of it as real.
|
||||
final points = List.generate(
|
||||
10,
|
||||
(i) => point(ts: i * 1000, alt: 1000.0 + i * 0.5),
|
||||
);
|
||||
|
||||
final motorcycle =
|
||||
computeSummary(points, profile: ActivityProfile.motorcycle).elevationGainM;
|
||||
final running =
|
||||
computeSummary(points, profile: ActivityProfile.running).elevationGainM;
|
||||
|
||||
expect(running, lessThanOrEqualTo(motorcycle),
|
||||
reason: "running's wider smoothing window must not report MORE gain than "
|
||||
"motorcycle's narrower one on the same climb");
|
||||
});
|
||||
|
||||
test('defaults to the motorcycle profile when none is given', () {
|
||||
final points = List.generate(
|
||||
5,
|
||||
(i) => point(ts: i * 1000, speed: 40.0),
|
||||
);
|
||||
expect(
|
||||
computeSummary(points),
|
||||
computeSummary(points, profile: ActivityProfile.motorcycle),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user