feat(train): a recording follows a route, its turns told (ADR 0030, D45)

The Record sheet offers the routes with a way planned; the recorder's
service follows the one chosen offline (RouteGuide.kt): turns told 50 m
before them (100 m on a bike) after a tone, off the route past 40 m for
10 s a cue with the way back, told again on rejoining, the end told.
The recording file keeps the route and the guidance; the recording
screen shows the next turn or the way back, and swipes to a map of the
route, the track and the position, north or the heading up.

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-29 08:27:27 +01:00
commit b0c0eeb3d5
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1091,6 +1091,7 @@ void main() {
WidgetTester tester, {
PolarLoop? loop,
PlanRepository? plans,
RouteRepository? routes,
}) => start(
tester,
usedBefore: true,
@ -1103,6 +1104,7 @@ void main() {
workouts: workouts,
loop: loop,
plans: plans,
routes: routes,
);
/// Lets [seconds] pass, the screen following.
@ -1365,6 +1367,89 @@ void main() {
});
});
testWidgets('a route chosen on the Record sheet chooses its activity and '
'goes to the recorder with its turns spoken; the next turn shows, '
'then the way back off it, and the map is swiped to (D45)', (
tester,
) async {
final routes = RouteRepository(MemoryRouteGateway(), keys);
addTearDown(routes.dispose);
await tester.runAsync(
() => routes.save(
const RouteContent(
name: 'River',
activity: Activity.cycling,
profile: RouteProfile.trekkingBike,
waypoints: [LatLng(50, 10), LatLng(50.005, 10.01)],
planned: RouteGeometry(
points: [
LatLng(50, 10),
LatLng(50.005, 10),
LatLng(50.005, 10.01),
],
distanceMetres: 1271,
hints: [TurnHint(index: 1, kind: TurnKind.right, metres: 715)],
),
),
),
);
await withClock(Clock(() => at), () async {
await startRecording(tester, routes: routes);
await tester.runAsync(pumpEventQueue);
await tester.tap(find.text('Record'));
await tester.pumpAndSettle();
await tester.tap(find.text('Running'));
await tester.pump();
await tester.tap(find.text('Route'));
await tester.runAsync(pumpEventQueue);
await tester.pumpAndSettle();
await tester.tap(find.text('River'));
await tester.pumpAndSettle();
expect(find.text('River'), findsOneWidget);
await tester.tap(find.text('Start'));
await tester.runAsync(pumpEventQueue);
await pass(tester, 2);
final started = platform.started.single;
expect(started.activity, Activity.cycling);
expect(started.route!['lead'], 100);
expect(started.route!['turns'], [
{'index': 1, 'say': 'In 100 metres: Turn right'},
]);
platform
..write('route @ ${started.route!['route']}')
..write('p @ 500000000 100000000 500 -');
await pass(tester);
expect(find.text('Following River'), findsOneWidget);
platform.write('guide @ next 0');
await pass(tester);
expect(find.text('Turn right'), findsOneWidget);
expect(find.text('In 560 m'), findsOneWidget);
platform.write('guide @ off 80 45');
await pass(tester);
expect(
find.text('Off the route: 80 m to the north-east'),
findsOneWidget,
);
await tester.drag(find.byType(PageView), const Offset(-500, 0));
await tester.pumpAndSettle();
expect(find.byType(FlutterMap), findsOneWidget);
expect(
find.text('Off the route: 80 m to the north-east'),
findsWidgets,
);
await tester.tap(find.text('North up'));
await tester.pump();
expect(find.text('Heading up'), findsOneWidget);
platform.write('guide @ on');
await pass(tester);
expect(find.text('Turn right'), findsWidgets);
expect(tester.takeException(), isNull);
});
});
group('with a strap (D27)', () {
const polar = FoundDevice(
address: 'A0:9E:1A:00:00:01',