feat(train): routes kept as sealed rows, planned on the map (ADR 0030, D44)
Routes are synced `route` rows (the model `SavedRoute`, as Flutter has a `Route`), sealed as plans are; the migration `routes` only creates the table, its space index and its cascade to the sync engine's spaces. A route's payload holds its name, activity, profile, waypoints and the way planned, points and elevations delta-coded and gzipped. Home gets a fourth tab, Routes, and the planner: waypoints tapped on the map, held and dragged, the last undone, a new route's first where the phone is (GPS through the recorder's channel), the way planned by the instance's BRouter after each change, an earlier answer dropped; its distance, climb, descent, elevation and surfaces under the map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
a8f7c26e69
commit
aea99c68ca
3 changed files with 24035 additions and 12 deletions
|
|
@ -131,6 +131,13 @@ class FakeRecorderPlatform({
|
|||
@override
|
||||
Future<bool> isRecording() async => _file != null;
|
||||
|
||||
/// Where the phone is; null: no fix.
|
||||
({double lat, double lon})? at;
|
||||
|
||||
@override
|
||||
Future<({double lat, double lon})?> position() async =>
|
||||
allows.contains(RecorderPermission.location) ? at : null;
|
||||
|
||||
/// A recording that runs on, as when the app comes back to one.
|
||||
void runOn(String path) => _file = File(path);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:halkyon_train_flutter/src/feature/route/data/routing_gateway.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/route/model/route.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
/// [RoutingGateway] planning straight lines between the waypoints, 100 m of
|
||||
/// climb a waypoint after the first; or refusing with [refusal], or holding
|
||||
/// its answers until [release] where [held].
|
||||
class FakeRoutingGateway({
|
||||
var bool isOffered = true,
|
||||
var RoutingRefusal? refusal,
|
||||
var bool held = false,
|
||||
}) implements RoutingGateway {
|
||||
/// What was asked for, in order.
|
||||
final asked = <({List<LatLng> waypoints, RouteProfile profile})>[];
|
||||
|
||||
final _held = <Completer<void>>[];
|
||||
|
||||
/// Answers every plan held so far.
|
||||
void release() {
|
||||
for (final answer in _held) {
|
||||
answer.complete();
|
||||
}
|
||||
_held.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> offered() async => isOffered;
|
||||
|
||||
@override
|
||||
Future<RouteGeometry> plan(
|
||||
List<LatLng> waypoints,
|
||||
RouteProfile profile,
|
||||
) async {
|
||||
asked.add((waypoints: [...waypoints], profile: profile));
|
||||
final refused = refusal;
|
||||
if (held) {
|
||||
final answer = Completer<void>();
|
||||
_held.add(answer);
|
||||
await answer.future;
|
||||
}
|
||||
if (refused != null) {
|
||||
throw RoutingRefused(refused);
|
||||
}
|
||||
return straightRoute(waypoints);
|
||||
}
|
||||
}
|
||||
|
||||
/// The straight lines through [waypoints], as [FakeRoutingGateway] plans.
|
||||
RouteGeometry straightRoute(List<LatLng> waypoints) {
|
||||
const distance = Distance();
|
||||
var metres = 0.0;
|
||||
for (var i = 1; i < waypoints.length; i++) {
|
||||
metres += distance(waypoints[i - 1], waypoints[i]);
|
||||
}
|
||||
return RouteGeometry(
|
||||
points: waypoints,
|
||||
elevations: [for (var i = 0; i < waypoints.length; i++) 500.0 + 100 * i],
|
||||
distanceMetres: metres.round(),
|
||||
ascentMetres: 100 * (waypoints.length - 1),
|
||||
hints: [
|
||||
for (var i = 1; i < waypoints.length - 1; i++)
|
||||
TurnHint(index: i, kind: TurnKind.left, metres: 0),
|
||||
],
|
||||
stretches: [
|
||||
RouteStretch(metres: metres.round(), surface: 'asphalt'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/route/data/route_gateway.dart';
|
||||
|
||||
/// [RouteGateway] in memory, whose rows a test changes as an instance could.
|
||||
class MemoryRouteGateway() implements RouteGateway {
|
||||
final rows = <String, Envelope>{};
|
||||
final _remote = StreamController<void>.broadcast();
|
||||
|
||||
/// As a sync bringing others' changes.
|
||||
void bringRemoteChanges() => _remote.add(null);
|
||||
|
||||
@override
|
||||
Future<List<RouteRow>> routes() async => [
|
||||
for (final MapEntry(:key, :value) in rows.entries)
|
||||
(id: key, envelope: value),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<void> insert(RouteRow route) async => rows[route.id] = route.envelope;
|
||||
|
||||
@override
|
||||
Future<void> update(String id, Envelope envelope) async {
|
||||
if (rows.containsKey(id)) {
|
||||
rows[id] = envelope;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(String id) async => rows.remove(id);
|
||||
|
||||
@override
|
||||
Stream<void> get remoteChanges => _remote.stream;
|
||||
}
|
||||
Loading…
Reference in a new issue