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:
Tizian 「ティツィアーン」 2026-10-29 00:37:44 +01:00
commit aea99c68ca
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
5 changed files with 24035 additions and 12 deletions

View file

@ -376,5 +376,45 @@
"dayBreathing": "{rate} Atemzüge pro Minute",
"daySkin": "Hauttemperatur",
"dayCelsius": "{value} °C",
"daySkinDeviation": "{value} °C gegenüber deinen Nächten sonst"
"daySkinDeviation": "{value} °C gegenüber deinen Nächten sonst",
"homeRoutes": "Routen",
"routesNew": "Neue Route",
"routesEmptyTitle": "Noch keine Routen",
"routesEmptyHint": "Tipp auf die Karte, um Wegpunkte zu setzen, und deine Instanz plant den Weg dazwischen über Wege und Straßen, mit Anstieg und Untergrund.",
"routeLocked": "Eine Route, die sich hier nicht öffnet",
"routeTitle": "Route",
"routeNewTitle": "Neue Route",
"routeName": "Name",
"routeActivity": "Für",
"routeActivityAny": "Jede Aktivität",
"routeProfile": "Geplant für",
"routeProfileRun": "Laufen",
"routeProfileHike": "Wandern",
"routeProfileRoadBike": "Rennrad",
"routeProfileTrekkingBike": "Trekkingrad",
"routeProfileGravel": "Gravelbike",
"routeProfileMtb": "Mountainbike",
"routeTapHint": "Tipp auf die Karte, um einen Wegpunkt hinzuzufügen; halt einen gedrückt und zieh ihn, um ihn zu verschieben.",
"routeUndo": "Letzten Wegpunkt entfernen",
"routePlanning": "Wird geplant …",
"routeRefusedInvalid": "Ein Wegpunkt zu viel, oder einer außerhalb der Karte.",
"routeRefusedTooOften": "Zu oft geplant: versuch es in einer Minute wieder.",
"routeRefusedNoRoute": "Dort wurde kein Weg gefunden: Ein Wegpunkt liegt vielleicht abseits der Wege oder außerhalb des Gebiets, in dem deine Instanz plant.",
"routeRefusedUnreachable": "Deine Instanz ist nicht erreichbar, um den Weg zu planen.",
"routeNotOffered": "Deine Instanz plant keine Routen.",
"routeAscent": "{metres} m bergauf",
"routeDescent": "{metres} m bergab",
"routeSummary": "{distance} · {ascent}",
"routeSurfaces": "Untergrund",
"surfacePaved": "Befestigt",
"surfaceCompacted": "Verdichtet",
"surfaceGravel": "Schotter",
"surfaceNatural": "Naturboden",
"surfaceUnknown": "Unbekannt",
"routeSave": "Speichern",
"routeNotSaved": "Die Route wurde nicht gespeichert.",
"routeDelete": "Löschen",
"routeDeleteTitle": "Diese Route löschen?",
"routeDeleteMessage": "Sie wird auf all deinen Geräten gelöscht.",
"routeDeleted": "Route gelöscht"
}

View file

@ -481,5 +481,48 @@
"dayCelsius": "{value} °C",
"@dayCelsius": {"placeholders": {"value": {"type": "String", "example": "33.4"}}},
"daySkinDeviation": "{value} °C from your usual at night",
"@daySkinDeviation": {"placeholders": {"value": {"type": "String", "example": "−0.2"}}}
"@daySkinDeviation": {"placeholders": {"value": {"type": "String", "example": "−0.2"}}},
"homeRoutes": "Routes",
"routesNew": "New route",
"routesEmptyTitle": "No routes yet",
"routesEmptyHint": "Tap the map to set waypoints, and your instance plans the way between them on paths and roads, with its climb and surfaces.",
"routeLocked": "A route that doesn't open here",
"routeTitle": "Route",
"routeNewTitle": "New route",
"routeName": "Name",
"routeActivity": "For",
"routeActivityAny": "Any activity",
"routeProfile": "Planned for",
"routeProfileRun": "Running",
"routeProfileHike": "Hiking",
"routeProfileRoadBike": "Road bike",
"routeProfileTrekkingBike": "Trekking bike",
"routeProfileGravel": "Gravel bike",
"routeProfileMtb": "Mountain bike",
"routeTapHint": "Tap the map to add a waypoint; hold one and drag it to move it.",
"routeUndo": "Take the last waypoint off",
"routePlanning": "Planning…",
"routeRefusedInvalid": "A waypoint too many, or one off the map.",
"routeRefusedTooOften": "Planned too often: try again in a minute.",
"routeRefusedNoRoute": "No way found there: a waypoint may be off the paths, or outside the region your instance plans in.",
"routeRefusedUnreachable": "Your instance can't be reached to plan the way.",
"routeNotOffered": "Your instance doesn't plan routes.",
"routeAscent": "{metres} m up",
"@routeAscent": {"placeholders": {"metres": {"type": "int"}}},
"routeDescent": "{metres} m down",
"@routeDescent": {"placeholders": {"metres": {"type": "int"}}},
"routeSummary": "{distance} · {ascent}",
"@routeSummary": {"description": "A route in the list: its distance and climb.", "placeholders": {"distance": {"type": "String", "example": "12.3 km"}, "ascent": {"type": "String", "example": "120 m up"}}},
"routeSurfaces": "Surfaces",
"surfacePaved": "Paved",
"surfaceCompacted": "Compacted",
"surfaceGravel": "Gravel",
"surfaceNatural": "Natural ground",
"surfaceUnknown": "Not known",
"routeSave": "Save",
"routeNotSaved": "The route wasn't saved.",
"routeDelete": "Delete",
"routeDeleteTitle": "Delete this route?",
"routeDeleteMessage": "It's deleted on all your devices.",
"routeDeleted": "Route deleted"
}

View file

@ -2283,6 +2283,246 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'{value} °C from your usual at night'**
String daySkinDeviation(String value);
/// No description provided for @homeRoutes.
///
/// In en, this message translates to:
/// **'Routes'**
String get homeRoutes;
/// No description provided for @routesNew.
///
/// In en, this message translates to:
/// **'New route'**
String get routesNew;
/// No description provided for @routesEmptyTitle.
///
/// In en, this message translates to:
/// **'No routes yet'**
String get routesEmptyTitle;
/// No description provided for @routesEmptyHint.
///
/// In en, this message translates to:
/// **'Tap the map to set waypoints, and your instance plans the way between them on paths and roads, with its climb and surfaces.'**
String get routesEmptyHint;
/// No description provided for @routeLocked.
///
/// In en, this message translates to:
/// **'A route that doesn\'t open here'**
String get routeLocked;
/// No description provided for @routeTitle.
///
/// In en, this message translates to:
/// **'Route'**
String get routeTitle;
/// No description provided for @routeNewTitle.
///
/// In en, this message translates to:
/// **'New route'**
String get routeNewTitle;
/// No description provided for @routeName.
///
/// In en, this message translates to:
/// **'Name'**
String get routeName;
/// No description provided for @routeActivity.
///
/// In en, this message translates to:
/// **'For'**
String get routeActivity;
/// No description provided for @routeActivityAny.
///
/// In en, this message translates to:
/// **'Any activity'**
String get routeActivityAny;
/// No description provided for @routeProfile.
///
/// In en, this message translates to:
/// **'Planned for'**
String get routeProfile;
/// No description provided for @routeProfileRun.
///
/// In en, this message translates to:
/// **'Running'**
String get routeProfileRun;
/// No description provided for @routeProfileHike.
///
/// In en, this message translates to:
/// **'Hiking'**
String get routeProfileHike;
/// No description provided for @routeProfileRoadBike.
///
/// In en, this message translates to:
/// **'Road bike'**
String get routeProfileRoadBike;
/// No description provided for @routeProfileTrekkingBike.
///
/// In en, this message translates to:
/// **'Trekking bike'**
String get routeProfileTrekkingBike;
/// No description provided for @routeProfileGravel.
///
/// In en, this message translates to:
/// **'Gravel bike'**
String get routeProfileGravel;
/// No description provided for @routeProfileMtb.
///
/// In en, this message translates to:
/// **'Mountain bike'**
String get routeProfileMtb;
/// No description provided for @routeTapHint.
///
/// In en, this message translates to:
/// **'Tap the map to add a waypoint; hold one and drag it to move it.'**
String get routeTapHint;
/// No description provided for @routeUndo.
///
/// In en, this message translates to:
/// **'Take the last waypoint off'**
String get routeUndo;
/// No description provided for @routePlanning.
///
/// In en, this message translates to:
/// **'Planning…'**
String get routePlanning;
/// No description provided for @routeRefusedInvalid.
///
/// In en, this message translates to:
/// **'A waypoint too many, or one off the map.'**
String get routeRefusedInvalid;
/// No description provided for @routeRefusedTooOften.
///
/// In en, this message translates to:
/// **'Planned too often: try again in a minute.'**
String get routeRefusedTooOften;
/// No description provided for @routeRefusedNoRoute.
///
/// In en, this message translates to:
/// **'No way found there: a waypoint may be off the paths, or outside the region your instance plans in.'**
String get routeRefusedNoRoute;
/// No description provided for @routeRefusedUnreachable.
///
/// In en, this message translates to:
/// **'Your instance can\'t be reached to plan the way.'**
String get routeRefusedUnreachable;
/// No description provided for @routeNotOffered.
///
/// In en, this message translates to:
/// **'Your instance doesn\'t plan routes.'**
String get routeNotOffered;
/// No description provided for @routeAscent.
///
/// In en, this message translates to:
/// **'{metres} m up'**
String routeAscent(int metres);
/// No description provided for @routeDescent.
///
/// In en, this message translates to:
/// **'{metres} m down'**
String routeDescent(int metres);
/// A route in the list: its distance and climb.
///
/// In en, this message translates to:
/// **'{distance} · {ascent}'**
String routeSummary(String distance, String ascent);
/// No description provided for @routeSurfaces.
///
/// In en, this message translates to:
/// **'Surfaces'**
String get routeSurfaces;
/// No description provided for @surfacePaved.
///
/// In en, this message translates to:
/// **'Paved'**
String get surfacePaved;
/// No description provided for @surfaceCompacted.
///
/// In en, this message translates to:
/// **'Compacted'**
String get surfaceCompacted;
/// No description provided for @surfaceGravel.
///
/// In en, this message translates to:
/// **'Gravel'**
String get surfaceGravel;
/// No description provided for @surfaceNatural.
///
/// In en, this message translates to:
/// **'Natural ground'**
String get surfaceNatural;
/// No description provided for @surfaceUnknown.
///
/// In en, this message translates to:
/// **'Not known'**
String get surfaceUnknown;
/// No description provided for @routeSave.
///
/// In en, this message translates to:
/// **'Save'**
String get routeSave;
/// No description provided for @routeNotSaved.
///
/// In en, this message translates to:
/// **'The route wasn\'t saved.'**
String get routeNotSaved;
/// No description provided for @routeDelete.
///
/// In en, this message translates to:
/// **'Delete'**
String get routeDelete;
/// No description provided for @routeDeleteTitle.
///
/// In en, this message translates to:
/// **'Delete this route?'**
String get routeDeleteTitle;
/// No description provided for @routeDeleteMessage.
///
/// In en, this message translates to:
/// **'It\'s deleted on all your devices.'**
String get routeDeleteMessage;
/// No description provided for @routeDeleted.
///
/// In en, this message translates to:
/// **'Route deleted'**
String get routeDeleted;
}
class _AppLocalizationsDelegate

View file

@ -1367,4 +1367,136 @@ class AppLocalizationsDe extends AppLocalizations {
String daySkinDeviation(String value) {
return '$value °C gegenüber deinen Nächten sonst';
}
@override
String get homeRoutes => 'Routen';
@override
String get routesNew => 'Neue Route';
@override
String get routesEmptyTitle => 'Noch keine Routen';
@override
String get routesEmptyHint =>
'Tipp auf die Karte, um Wegpunkte zu setzen, und deine Instanz plant den Weg dazwischen über Wege und Straßen, mit Anstieg und Untergrund.';
@override
String get routeLocked => 'Eine Route, die sich hier nicht öffnet';
@override
String get routeTitle => 'Route';
@override
String get routeNewTitle => 'Neue Route';
@override
String get routeName => 'Name';
@override
String get routeActivity => 'Für';
@override
String get routeActivityAny => 'Jede Aktivität';
@override
String get routeProfile => 'Geplant für';
@override
String get routeProfileRun => 'Laufen';
@override
String get routeProfileHike => 'Wandern';
@override
String get routeProfileRoadBike => 'Rennrad';
@override
String get routeProfileTrekkingBike => 'Trekkingrad';
@override
String get routeProfileGravel => 'Gravelbike';
@override
String get routeProfileMtb => 'Mountainbike';
@override
String get routeTapHint =>
'Tipp auf die Karte, um einen Wegpunkt hinzuzufügen; halt einen gedrückt und zieh ihn, um ihn zu verschieben.';
@override
String get routeUndo => 'Letzten Wegpunkt entfernen';
@override
String get routePlanning => 'Wird geplant …';
@override
String get routeRefusedInvalid =>
'Ein Wegpunkt zu viel, oder einer außerhalb der Karte.';
@override
String get routeRefusedTooOften =>
'Zu oft geplant: versuch es in einer Minute wieder.';
@override
String get routeRefusedNoRoute =>
'Dort wurde kein Weg gefunden: Ein Wegpunkt liegt vielleicht abseits der Wege oder außerhalb des Gebiets, in dem deine Instanz plant.';
@override
String get routeRefusedUnreachable =>
'Deine Instanz ist nicht erreichbar, um den Weg zu planen.';
@override
String get routeNotOffered => 'Deine Instanz plant keine Routen.';
@override
String routeAscent(int metres) {
return '$metres m bergauf';
}
@override
String routeDescent(int metres) {
return '$metres m bergab';
}
@override
String routeSummary(String distance, String ascent) {
return '$distance · $ascent';
}
@override
String get routeSurfaces => 'Untergrund';
@override
String get surfacePaved => 'Befestigt';
@override
String get surfaceCompacted => 'Verdichtet';
@override
String get surfaceGravel => 'Schotter';
@override
String get surfaceNatural => 'Naturboden';
@override
String get surfaceUnknown => 'Unbekannt';
@override
String get routeSave => 'Speichern';
@override
String get routeNotSaved => 'Die Route wurde nicht gespeichert.';
@override
String get routeDelete => 'Löschen';
@override
String get routeDeleteTitle => 'Diese Route löschen?';
@override
String get routeDeleteMessage => 'Sie wird auf all deinen Geräten gelöscht.';
@override
String get routeDeleted => 'Route gelöscht';
}

View file

@ -1360,6 +1360,137 @@ class AppLocalizationsEn extends AppLocalizations {
String daySkinDeviation(String value) {
return '$value °C from your usual at night';
}
@override
String get homeRoutes => 'Routes';
@override
String get routesNew => 'New route';
@override
String get routesEmptyTitle => 'No routes yet';
@override
String get routesEmptyHint =>
'Tap the map to set waypoints, and your instance plans the way between them on paths and roads, with its climb and surfaces.';
@override
String get routeLocked => 'A route that doesn\'t open here';
@override
String get routeTitle => 'Route';
@override
String get routeNewTitle => 'New route';
@override
String get routeName => 'Name';
@override
String get routeActivity => 'For';
@override
String get routeActivityAny => 'Any activity';
@override
String get routeProfile => 'Planned for';
@override
String get routeProfileRun => 'Running';
@override
String get routeProfileHike => 'Hiking';
@override
String get routeProfileRoadBike => 'Road bike';
@override
String get routeProfileTrekkingBike => 'Trekking bike';
@override
String get routeProfileGravel => 'Gravel bike';
@override
String get routeProfileMtb => 'Mountain bike';
@override
String get routeTapHint =>
'Tap the map to add a waypoint; hold one and drag it to move it.';
@override
String get routeUndo => 'Take the last waypoint off';
@override
String get routePlanning => 'Planning…';
@override
String get routeRefusedInvalid => 'A waypoint too many, or one off the map.';
@override
String get routeRefusedTooOften =>
'Planned too often: try again in a minute.';
@override
String get routeRefusedNoRoute =>
'No way found there: a waypoint may be off the paths, or outside the region your instance plans in.';
@override
String get routeRefusedUnreachable =>
'Your instance can\'t be reached to plan the way.';
@override
String get routeNotOffered => 'Your instance doesn\'t plan routes.';
@override
String routeAscent(int metres) {
return '$metres m up';
}
@override
String routeDescent(int metres) {
return '$metres m down';
}
@override
String routeSummary(String distance, String ascent) {
return '$distance · $ascent';
}
@override
String get routeSurfaces => 'Surfaces';
@override
String get surfacePaved => 'Paved';
@override
String get surfaceCompacted => 'Compacted';
@override
String get surfaceGravel => 'Gravel';
@override
String get surfaceNatural => 'Natural ground';
@override
String get surfaceUnknown => 'Not known';
@override
String get routeSave => 'Save';
@override
String get routeNotSaved => 'The route wasn\'t saved.';
@override
String get routeDelete => 'Delete';
@override
String get routeDeleteTitle => 'Delete this route?';
@override
String get routeDeleteMessage => 'It\'s deleted on all your devices.';
@override
String get routeDeleted => 'Route deleted';
}
/// The translations for English, as used in the United Kingdom (`en_GB`).