feat(train): routes planned through the instance's BRouter (ADR 0030, D44)
The routing endpoint plans a route through the waypoints for one of six profiles, for the people signed in, at most 50 waypoints and 30 plans a person a minute, nothing kept or logged; refused without BROUTER_URL. BRouter's GeoJSON is read into points, elevations, climb and descent above 3 m, turn hints and stretches by surface. Train's running profile for BRouter, halkyon-run.brf, is a first guess not yet tried on BRouter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
8325d2a08b
commit
781576726a
31 changed files with 2563 additions and 1 deletions
|
|
@ -3,7 +3,7 @@
|
|||
What holds for every app and package of this repository is in the [root AGENTS.md](../../AGENTS.md); it applies here in full, and Learn's patterns (`apps/learn/AGENTS.md`) wherever Train needs the same thing.
|
||||
|
||||
- **A shell on the shared packages**, Todo's copied and renamed (decided 2026-10-28, ADR 0030, D12; what Train is to do: ADR 0030): signing in, the instances, the account and the look work, and since step 5a (28 Oct 2026) a person's workouts are kept and synced with `serverpod_offline_sync`, every row sealed (ADR 0030, D7, D14, D15), and on Android a workout is recorded (5b: ADR 0030, D13, D16, D18 to D21), and since 5c home lists them and each has its page with its map (D22 to D24). Packages: `halkyon_train_server`, `halkyon_train_client` (generated, never edited), `halkyon_train_flutter`; app ID `app.halkyon.train` on every platform; no web build. Licences as Learn's (server SSPL, client and app AGPL; `LICENSE.md`).
|
||||
- **The server** includes `halkyon_base` (ADR 0014) and configures it in `server.dart` (`trainBase()`, `lib/src/train_base.dart`): its basics say it serves `train` (never change it once apps use it), sign-in mails name Halkyon Train, the source is this repository, `minimumAppBuild` 1, and `deleteTrainData` (`lib/src/train_data.dart`) as its `deletePersonData` hook, which deletes the person's sync spaces and so, by the sync engine's cascades, every row of theirs (`test/integration/workout/person_deletion_test.dart`). Authentication through the module's builders: a password, SimpleLogin on the register, email codes proving an address, server-side sessions; no anonymous accounts (ADR 0020, D25), nor GitHub (D36). **Accounts** (ADR 0016): Train holds its own register when it runs alone, and is a member of the instance's register (Learn's server on learn.halkyon.eu) where the operator sets `ACCOUNT_REGISTER_URL` (`trainBase(accounts: AccountsMode.fromEnvironment())`) with the instance's shared `INSTANCE_SECRET` (`instanceSecret`); then sign-in, handles and profiles are the register's, the email settings are the register's alone, and an identified person's deletion goes to the register first. `addInstanceRoutes` serves the calls between the services on its web server, and the server doesn't start without the secret where it needs it. `schedulePruning` (`lib/src/pruning.dart`) keeps the module's pruning scheduled, one entry each however often the server starts (and cancels the one under the pruning's name before ADR 0020, D25): the anonymous accounts left from before daily, sign-up attempts and codes hourly, as the register the deletions its members haven't confirmed hourly, as a member the people the register no longer knows daily (each a no-op in the other mode). Session logs are never persisted, in any run mode. **The Loop's firmware relay** (ADR 0030, D10, D40, `lib/src/firmware/`): the `firmware` endpoint asks Polar's firmware service in the person's place and hands on the file, kept in memory a day per hardware and version (`FirmwareRelay`); on by default, off where the operator sets `LOOP_FIRMWARE_RELAY=off`, the app then offering the direct way alone; tested on a fake Polar (`test/integration/firmware/`). Migrations (2026-10-28): an initial one with Serverpod's tables, its auth modules' and the module's as they stand (Todo's grew one by one with the module; Train starts where they ended), then `workouts`, purely additive: the sync engine's tables and Train's first three, then `profile`, `days` (Train's `day`, `day_series` and `night`) and `plans` (`plan`, D42), each purely additive. **Synced models** (`database: sync`, the module `offline_sync`, `experimental_features: databaseSync` in `config/generator.yaml`): `content_key` (the person's key sealed to the account), `workout` and `workout_series` (a workout's series in chunks of at most ten minutes, deleted with it), `profile` (the body's figures, one row a person), `day`, `day_series` (a day's 24/7 heart rate in chunks of three hours, deleted with it) and `night` (one row a person and date each, ADR 0030, D37) and `plan` (an interval plan, D42), each a sealed `payload` with its `keyId` and `nonce`, nothing else readable. Tests (`test/integration/train_server_test.dart`) call the module's endpoints through the server's own dispatch (`halkyon_base.<endpoint>`), since the generated test tools expose the project's endpoints alone.
|
||||
- **The server** includes `halkyon_base` (ADR 0014) and configures it in `server.dart` (`trainBase()`, `lib/src/train_base.dart`): its basics say it serves `train` (never change it once apps use it), sign-in mails name Halkyon Train, the source is this repository, `minimumAppBuild` 1, and `deleteTrainData` (`lib/src/train_data.dart`) as its `deletePersonData` hook, which deletes the person's sync spaces and so, by the sync engine's cascades, every row of theirs (`test/integration/workout/person_deletion_test.dart`). Authentication through the module's builders: a password, SimpleLogin on the register, email codes proving an address, server-side sessions; no anonymous accounts (ADR 0020, D25), nor GitHub (D36). **Accounts** (ADR 0016): Train holds its own register when it runs alone, and is a member of the instance's register (Learn's server on learn.halkyon.eu) where the operator sets `ACCOUNT_REGISTER_URL` (`trainBase(accounts: AccountsMode.fromEnvironment())`) with the instance's shared `INSTANCE_SECRET` (`instanceSecret`); then sign-in, handles and profiles are the register's, the email settings are the register's alone, and an identified person's deletion goes to the register first. `addInstanceRoutes` serves the calls between the services on its web server, and the server doesn't start without the secret where it needs it. `schedulePruning` (`lib/src/pruning.dart`) keeps the module's pruning scheduled, one entry each however often the server starts (and cancels the one under the pruning's name before ADR 0020, D25): the anonymous accounts left from before daily, sign-up attempts and codes hourly, as the register the deletions its members haven't confirmed hourly, as a member the people the register no longer knows daily (each a no-op in the other mode). Session logs are never persisted, in any run mode. **The Loop's firmware relay** (ADR 0030, D10, D40, `lib/src/firmware/`): the `firmware` endpoint asks Polar's firmware service in the person's place and hands on the file, kept in memory a day per hardware and version (`FirmwareRelay`); on by default, off where the operator sets `LOOP_FIRMWARE_RELAY=off`, the app then offering the direct way alone; tested on a fake Polar (`test/integration/firmware/`). **Routes** (ADR 0030, D11, D44, `lib/src/routing/`): the `routing` endpoint plans a route through the instance's BRouter (`BROUTER_URL`; none: `offered` is false and plans are refused) for the people signed in, at most 50 waypoints and 30 plans a person a minute, nothing kept or logged; BRouter's GeoJSON read in `brouter.dart` (points, elevations, climbs above 3 m, turn hints, stretches by surface), tested on its format (`test/unit/routing/`) and on a fake BRouter (`test/integration/routing/`). Train's running profile is `apps/train/brouter/halkyon-run.brf`, which the sidecar's profiles hold beside BRouter's own. Migrations (2026-10-28): an initial one with Serverpod's tables, its auth modules' and the module's as they stand (Todo's grew one by one with the module; Train starts where they ended), then `workouts`, purely additive: the sync engine's tables and Train's first three, then `profile`, `days` (Train's `day`, `day_series` and `night`) and `plans` (`plan`, D42), each purely additive. **Synced models** (`database: sync`, the module `offline_sync`, `experimental_features: databaseSync` in `config/generator.yaml`): `content_key` (the person's key sealed to the account), `workout` and `workout_series` (a workout's series in chunks of at most ten minutes, deleted with it), `profile` (the body's figures, one row a person), `day`, `day_series` (a day's 24/7 heart rate in chunks of three hours, deleted with it) and `night` (one row a person and date each, ADR 0030, D37) and `plan` (an interval plan, D42), each a sealed `payload` with its `keyId` and `nonce`, nothing else readable. Tests (`test/integration/train_server_test.dart`) call the module's endpoints through the server's own dispatch (`halkyon_base.<endpoint>`), since the generated test tools expose the project's endpoints alone.
|
||||
- **The app** is halkyon_app's `InstanceGate` and `SessionGate` with Train's frame (`trainFrame`: "Train" under "Halkyon", its icon, `train.example.org`, its own sentence) and `ServerpodInstanceChecker(app: 'train')`; `TrainDependencies` (`AppDependencies<TrainPerson>`) on one instance, `TrainPerson` per person: their own sync database in their folder (`sync.sqlite`, halkyon_app's `PersonFolders`, one person's at a time), halkyon_app's `SyncService` behind `AppCompatibility` (the module's basics' `minimumAppBuild`, against the build `package_info_plus` reads), their key (`TrainKeys`, `lib/src/core/privacy/`: one key per person, IDs `train1.<ms>.<nonce>`, the oldest-made writing, kept in `content_key` and opened only in Train's form, ADR 0030, D15) and their workouts (`WorkoutRepository`, `lib/src/feature/workout/`: halkyon_app's `PayloadEnvelope` seals a workout for its row and a series chunk for its row and its workout; payloads are versioned JSON, a series' columns delta-coded, gzipped before sealing; a deletion blanks every payload first); the gateways' tests run on a sync database opened without a server, the repository's on memory gateways and real libsodium; a person's app whose home lists their workouts (`WorkoutsCubit`, with `TrainPerson.deletions`, halkyon_app's `DelayedDeletions`) and opens each one's page (`WorkoutCubit`: `analyse` in `workout_analysis.dart` reads its series into the track's pieces, splits, elevation and charts, drawn by `workout_chart.dart`; the map with flutter_map on the instance's source, `MapSourceRepository` asking `map.source`, its tiles from `TrainPerson.mapTiles`, vector_map_tiles in the app and none in widget tests), and the account sheet: halkyon_app's `AccountTiles` (whose account on which instance, Switch account, Sign out, Delete account, which asks first) in Train's words, and halkyon_app's About (`showAppAbout`: the source of what the instance runs, from `TrainDependencies.sourceCodeUrl`, and the licence list). Texts through gen-l10n in British English (the template), German (informal du), an empty `en_US` and `en_GB` (`test/l10n/arb_files_test.dart` keeps them in step). The look follows the system's theme mode and language until Train has settings. Countries in where an instance keeps its data are named by halkyon_app's CLDR names (`AssetCountryNames`), its language picked by halkyon_app's `resolveAppLocale`. Widget tests start Train's whole tree with halkyon_app's fakes (`test/app/train_app_test.dart`).
|
||||
- **Recording** (ADR 0030, D13, D16, D18 to D21, D26 to D28) is Android's alone for now (iOS waits for a Mac, D25) (`TrainPerson.recording` null elsewhere): `RecorderService` (Kotlin, `android/app/src/main/kotlin/app/halkyon/train/`), a foreground service of the types `location` and `health` started from the open app, takes fixes from LocationManager's GPS (no Play services), steps from the step detector the heart rate from the strap and a row from the rower the person chose (ADR 0030, D26 to D28: `NotifyLink.kt` on Android's own Bluetooth API, no package, `HeartRateMeasurement.kt` parsing 0x2A37 and `RowerData.kt` FTMS's Rower Data 0x2AD1; `DeviceScanner.kt` finds them for the sheets over `…/recorder/straps` and `…/recorder/rowers`; each remembered in the person's `strap.json` and `rower.json`, `DeviceStore`), decides auto-pause (`AutoPause.kt`) and appends each sample as a line to the person's `recording.txt` (format in `recording_file.dart`), flushed every 5 s; `MainActivity` asks the permissions (never background location) and bridges `app.halkyon.train/recorder` (methods) and `…/recorder/lines` (the file so far, then each line). The app (`lib/src/feature/recording/`) follows those lines with `RecordingBloc`, works out what the screen and the notification show (`liveOf`, the saved totals' functions), and saves a recording as a workout and its series (`recordedWorkout`) or throws it away; one cut off, or stopped and not saved, is found as the person's app opens. Tests: the Dart side with `test/support/fake_recorder_platform.dart`; the Kotlin auto-pause and the strap's and the rower's parsers on the JVM, `JAVA_HOME=<Android Studio's jbr> ./gradlew :app:testDebugUnitTest` in `android/` after a first `flutter build apk` (which writes `gradlew`), not part of `tool/check.dart`, as CI's runner has no Android SDK.
|
||||
- **Health Connect** (ADR 0030, D30, D31) is Android's (`TrainPerson.health` null elsewhere): `HealthConnectBridge.kt` on AndroidX's `connect-client` 1.1.0 over `app.halkyon.train/health` (its records' mapping in `HealthRecords.kt`, tested on the JVM; the rationale's links from Health Connect in the manifest, `…/health/rationale` to the app), `HealthPlatform` in Dart (`lib/src/feature/health/`), `HealthConnect` per person: with writing on, every workout written as `healthRecordsOf` makes it (client IDs from the workout's ID, a version counted up as it changes, a fingerprint deciding whether it did), written again or deleted as the workouts change and as the app opens, what was written kept in the person's `health_connect.json` (`HealthStore`, never synced); with reading on, the sessions of the apps the person turned on brought in as workouts through the changes token (`importedWorkout` in `health_import.dart`, the workout's `origin` in its payload; a double of one Train has brings at most its heart rate, `isDouble`; one deleted in Train is remembered so it stays out; a track only when asked for on its page, `bringInTrack`), as the app opens and comes to the front (`HealthConnectListener`, which opens the rationale too); the screen from the account sheet (`HealthScreen`, `HealthCubit`). Tests on `test/support/fake_health_platform.dart`. Train needs Android 8 (API 26) for it.
|
||||
|
|
|
|||
135
apps/train/brouter/halkyon-run.brf
Normal file
135
apps/train/brouter/halkyon-run.brf
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
# Halkyon Train's running profile for BRouter (ADR 0030, D44): BRouter has
|
||||
# none for running, its hiking profiles sending runners up steps and rough
|
||||
# paths, its cycling ones off footpaths. Paths, footways, tracks and quiet
|
||||
# streets first, any surface; big roads only where nothing else goes, steps
|
||||
# and scrambles avoided, motorways never. Written on BRouter's `shortest.brf`
|
||||
# (its access rules and its voice hints' classifiers kept), 28 Oct 2026; not
|
||||
# yet tried on a BRouter: the costs are a first guess, to be tuned on runs.
|
||||
|
||||
---context:global # following code refers to global config
|
||||
|
||||
# A climb costs a little, as it does a runner; going down costs nothing.
|
||||
assign downhillcost 0
|
||||
assign downhillcutoff 1.5
|
||||
assign uphillcost 15
|
||||
assign uphillcutoff 1.5
|
||||
|
||||
assign turnInstructionMode = 2 # locus-style, as Train asks for
|
||||
assign validForFoot 1
|
||||
|
||||
---context:way # following code refers to way-tags
|
||||
|
||||
assign turncost 0
|
||||
|
||||
assign initialclassifier =
|
||||
if route=ferry then 1
|
||||
else 0
|
||||
|
||||
assign initialcost switch route=ferry 10000 0
|
||||
|
||||
#
|
||||
# implicit access here just from the motorroad tag
|
||||
#
|
||||
assign defaultaccess
|
||||
switch access=
|
||||
(
|
||||
if motorroad=yes then false
|
||||
else if highway=motorway|motorway_link then false
|
||||
else true
|
||||
)
|
||||
switch or access=private access=no
|
||||
false
|
||||
true
|
||||
|
||||
#
|
||||
# foot access
|
||||
#
|
||||
assign footaccess
|
||||
switch foot=
|
||||
defaultaccess
|
||||
not or foot=private or foot=no foot=use_sidepath
|
||||
|
||||
# A big road is run along on its sidewalk where it has one.
|
||||
assign hassidewalk = or sidewalk=both|left|right|yes or sidewalk:both=yes or sidewalk:left=yes sidewalk:right=yes
|
||||
|
||||
# Scrambles and climbing routes are no running.
|
||||
assign isscramble = sac_scale=demanding_mountain_hiking|alpine_hiking|demanding_alpine_hiking|difficult_alpine_hiking
|
||||
|
||||
assign costfactor
|
||||
add switch footaccess 0 100000
|
||||
|
||||
switch and highway= not route=ferry 100000
|
||||
|
||||
switch route=ferry 5.67
|
||||
switch or highway=motorway highway=motorway_link 100000
|
||||
switch or highway=proposed highway=abandoned 100000
|
||||
switch isscramble 20
|
||||
|
||||
switch highway=path|footway|pedestrian|track|bridleway 1
|
||||
switch highway=living_street|residential|service|cycleway 1.15
|
||||
switch highway=steps 3
|
||||
switch highway=unclassified|road 1.3
|
||||
switch highway=tertiary|tertiary_link switch hassidewalk 1.3 1.8
|
||||
switch highway=secondary|secondary_link switch hassidewalk 1.5 2.5
|
||||
switch highway=primary|primary_link switch hassidewalk 1.8 4
|
||||
switch highway=trunk|trunk_link switch hassidewalk 3 10
|
||||
1.5
|
||||
|
||||
# way priorities used for voice hint generation (as BRouter's own)
|
||||
|
||||
assign isresidentialorliving = or highway=residential|living_street living_street=yes
|
||||
|
||||
assign priorityclassifier =
|
||||
|
||||
if ( highway=motorway ) then 30
|
||||
else if ( highway=motorway_link ) then 29
|
||||
else if ( highway=trunk ) then 28
|
||||
else if ( highway=trunk_link ) then 27
|
||||
else if ( highway=primary ) then 26
|
||||
else if ( highway=primary_link ) then 25
|
||||
else if ( highway=secondary ) then 24
|
||||
else if ( highway=secondary_link ) then 23
|
||||
else if ( highway=tertiary ) then 22
|
||||
else if ( highway=tertiary_link ) then 21
|
||||
else if ( highway=unclassified ) then 20
|
||||
else if ( isresidentialorliving ) then 6
|
||||
else if ( highway=service ) then 6
|
||||
else if ( highway=cycleway ) then 6
|
||||
else if ( highway=track ) then if tracktype=grade1 then 6 else 4
|
||||
else if ( highway=bridleway|road|path|footway ) then 4
|
||||
else if ( highway=steps ) then 2
|
||||
else if ( highway=pedestrian ) then 2
|
||||
else 0
|
||||
|
||||
assign isroundabout = junction=roundabout
|
||||
assign islinktype = highway=motorway_link|trunk_link|primary_link|secondary_link|tertiary_link
|
||||
assign isgoodforcars = if greater priorityclassifier 6 then true
|
||||
else if ( or isresidentialorliving highway=service ) then true
|
||||
else if ( and highway=track tracktype=grade1 ) then true
|
||||
else false
|
||||
|
||||
assign classifiermask add multiply isroundabout 4
|
||||
add multiply islinktype 8
|
||||
multiply isgoodforcars 16
|
||||
|
||||
# The surface and smoothness in the answer's way tags, for the planner's
|
||||
# stretches.
|
||||
assign dummyUsage = or surface= smoothness=
|
||||
|
||||
---context:node # following code refers to node tags
|
||||
|
||||
assign defaultaccess
|
||||
switch access=
|
||||
1
|
||||
switch or access=private access=no
|
||||
0
|
||||
1
|
||||
|
||||
assign footaccess
|
||||
switch foot=
|
||||
defaultaccess
|
||||
switch or foot=private foot=no
|
||||
0
|
||||
1
|
||||
|
||||
assign initialcost switch footaccess 0 1000000
|
||||
|
|
@ -18,6 +18,12 @@ import 'package:halkyon_train_client/src/protocol/firmware/loop_firmware.dart'
|
|||
as _ikc7xqe5;
|
||||
import 'package:halkyon_train_client/src/protocol/map/map_source.dart'
|
||||
as _iebj1x2g;
|
||||
import 'package:halkyon_train_client/src/protocol/routing/planned_route.dart'
|
||||
as _i0hhs3f4;
|
||||
import 'package:halkyon_train_client/src/protocol/routing/route_point.dart'
|
||||
as _iq1flhug;
|
||||
import 'package:halkyon_train_client/src/protocol/routing/route_profile.dart'
|
||||
as _isvxstnj;
|
||||
import 'package:http/http.dart' as _i85jenna;
|
||||
import 'package:serverpod_auth_core_client/serverpod_auth_core_client.dart'
|
||||
as _iacc;
|
||||
|
|
@ -96,6 +102,41 @@ class EndpointMap extends _isc.EndpointRef {
|
|||
);
|
||||
}
|
||||
|
||||
/// Routes planned by the instance's BRouter (ADR 0030, D11, D44), for those
|
||||
/// signed in: BRouter has no sign-in of its own, so the server alone reaches
|
||||
/// it. Neither the waypoints nor the route are kept or logged. Refusals are
|
||||
/// `RouteRefused`.
|
||||
/// {@category Endpoint}
|
||||
class EndpointRouting extends _isc.EndpointRef {
|
||||
EndpointRouting(_isc.EndpointCaller caller) : super(caller);
|
||||
|
||||
@override
|
||||
String get name => 'routing';
|
||||
|
||||
/// Whether the instance plans routes.
|
||||
_ida.Future<bool> offered() => caller.callServerEndpoint<bool>(
|
||||
'routing',
|
||||
'offered',
|
||||
{},
|
||||
);
|
||||
|
||||
/// The route through [waypoints], in order, for [profile]. Refused where
|
||||
/// the instance has no BRouter or it fails, for fewer than two or more
|
||||
/// than `Routing.maxWaypoints` waypoints, and beyond `Routing.perMinute`
|
||||
/// plans a minute.
|
||||
_ida.Future<_i0hhs3f4.PlannedRoute> plan(
|
||||
List<_iq1flhug.RoutePoint> waypoints,
|
||||
_isvxstnj.RouteProfile profile,
|
||||
) => caller.callServerEndpoint<_i0hhs3f4.PlannedRoute>(
|
||||
'routing',
|
||||
'plan',
|
||||
{
|
||||
'waypoints': waypoints,
|
||||
'profile': profile,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class Modules {
|
||||
Modules(Client client) {
|
||||
base = _ihk4p5t7.Caller(client);
|
||||
|
|
@ -145,6 +186,7 @@ class Client extends _isc.ServerpodClientShared {
|
|||
) {
|
||||
firmware = EndpointFirmware(this);
|
||||
map = EndpointMap(this);
|
||||
routing = EndpointRouting(this);
|
||||
modules = Modules(this);
|
||||
}
|
||||
|
||||
|
|
@ -152,12 +194,15 @@ class Client extends _isc.ServerpodClientShared {
|
|||
|
||||
late final EndpointMap map;
|
||||
|
||||
late final EndpointRouting routing;
|
||||
|
||||
late final Modules modules;
|
||||
|
||||
@override
|
||||
Map<String, _isc.EndpointRef> get endpointRefLookup => {
|
||||
'firmware': firmware,
|
||||
'map': map,
|
||||
'routing': routing,
|
||||
};
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:halkyon_base_client/halkyon_base_client.dart' as _ihk4p5t7;
|
||||
import 'package:halkyon_oidc_client/halkyon_oidc_client.dart' as _ibkpwzgl;
|
||||
import 'package:halkyon_train_client/src/protocol/routing/route_point.dart'
|
||||
as _iq1flhug;
|
||||
import 'package:serverpod_auth_core_client/serverpod_auth_core_client.dart'
|
||||
as _iacc;
|
||||
import 'package:serverpod_auth_idp_client/serverpod_auth_idp_client.dart'
|
||||
|
|
@ -30,6 +32,12 @@ import 'map/map_source.dart' as _ikw0qg8x;
|
|||
import 'plan/plan.dart' as _i45prw73;
|
||||
import 'privacy/content_key.dart' as _imed3x5d;
|
||||
import 'profile/profile.dart' as _ia5j8fch;
|
||||
import 'routing/planned_route.dart' as _ikjgxraq;
|
||||
import 'routing/route_hint.dart' as _if6974nb;
|
||||
import 'routing/route_point.dart' as _isqtfus6;
|
||||
import 'routing/route_profile.dart' as _ifl4j5ep;
|
||||
import 'routing/route_refused.dart' as _ix4aepwk;
|
||||
import 'routing/route_stretch.dart' as _ic7f85bc;
|
||||
import 'workout/workout.dart' as _i5b9b17z;
|
||||
import 'workout/workout_series.dart' as _i92mefto;
|
||||
export 'day/day.dart';
|
||||
|
|
@ -41,6 +49,12 @@ export 'map/map_source.dart';
|
|||
export 'plan/plan.dart';
|
||||
export 'privacy/content_key.dart';
|
||||
export 'profile/profile.dart';
|
||||
export 'routing/planned_route.dart';
|
||||
export 'routing/route_hint.dart';
|
||||
export 'routing/route_point.dart';
|
||||
export 'routing/route_profile.dart';
|
||||
export 'routing/route_refused.dart';
|
||||
export 'routing/route_stretch.dart';
|
||||
export 'workout/workout.dart';
|
||||
export 'workout/workout_series.dart';
|
||||
export 'client.dart';
|
||||
|
|
@ -720,6 +734,24 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
if (t == _ia5j8fch.Profile) {
|
||||
return _ia5j8fch.Profile.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ikjgxraq.PlannedRoute) {
|
||||
return _ikjgxraq.PlannedRoute.fromJson(data) as T;
|
||||
}
|
||||
if (t == _if6974nb.RouteHint) {
|
||||
return _if6974nb.RouteHint.fromJson(data) as T;
|
||||
}
|
||||
if (t == _isqtfus6.RoutePoint) {
|
||||
return _isqtfus6.RoutePoint.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ifl4j5ep.RouteProfile) {
|
||||
return _ifl4j5ep.RouteProfile.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ix4aepwk.RouteRefused) {
|
||||
return _ix4aepwk.RouteRefused.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ic7f85bc.RouteStretch) {
|
||||
return _ic7f85bc.RouteStretch.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i5b9b17z.Workout) {
|
||||
return _i5b9b17z.Workout.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -756,6 +788,24 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
if (t == _isc.getType<_ia5j8fch.Profile?>()) {
|
||||
return (data != null ? _ia5j8fch.Profile.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _isc.getType<_ikjgxraq.PlannedRoute?>()) {
|
||||
return (data != null ? _ikjgxraq.PlannedRoute.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _isc.getType<_if6974nb.RouteHint?>()) {
|
||||
return (data != null ? _if6974nb.RouteHint.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _isc.getType<_isqtfus6.RoutePoint?>()) {
|
||||
return (data != null ? _isqtfus6.RoutePoint.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _isc.getType<_ifl4j5ep.RouteProfile?>()) {
|
||||
return (data != null ? _ifl4j5ep.RouteProfile.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _isc.getType<_ix4aepwk.RouteRefused?>()) {
|
||||
return (data != null ? _ix4aepwk.RouteRefused.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _isc.getType<_ic7f85bc.RouteStretch?>()) {
|
||||
return (data != null ? _ic7f85bc.RouteStretch.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _isc.getType<_i5b9b17z.Workout?>()) {
|
||||
return (data != null ? _i5b9b17z.Workout.fromJson(data) : null) as T;
|
||||
}
|
||||
|
|
@ -763,6 +813,33 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
return (data != null ? _i92mefto.WorkoutSeries.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == List<_isqtfus6.RoutePoint>) {
|
||||
return (data as List)
|
||||
.map((e) => deserialize<_isqtfus6.RoutePoint>(e))
|
||||
.toList()
|
||||
as T;
|
||||
}
|
||||
if (t == List<double>) {
|
||||
return (data as List).map((e) => deserialize<double>(e)).toList() as T;
|
||||
}
|
||||
if (t == List<_if6974nb.RouteHint>) {
|
||||
return (data as List)
|
||||
.map((e) => deserialize<_if6974nb.RouteHint>(e))
|
||||
.toList()
|
||||
as T;
|
||||
}
|
||||
if (t == List<_ic7f85bc.RouteStretch>) {
|
||||
return (data as List)
|
||||
.map((e) => deserialize<_ic7f85bc.RouteStretch>(e))
|
||||
.toList()
|
||||
as T;
|
||||
}
|
||||
if (t == List<_iq1flhug.RoutePoint>) {
|
||||
return (data as List)
|
||||
.map((e) => deserialize<_iq1flhug.RoutePoint>(e))
|
||||
.toList()
|
||||
as T;
|
||||
}
|
||||
try {
|
||||
return _ihk4p5t7.Protocol().deserialize<T>(data, t);
|
||||
} on _isc.DeserializationTypeNotFoundException catch (_) {}
|
||||
|
|
@ -792,6 +869,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
_i45prw73.Plan => 'Plan',
|
||||
_imed3x5d.ContentKey => 'ContentKey',
|
||||
_ia5j8fch.Profile => 'Profile',
|
||||
_ikjgxraq.PlannedRoute => 'PlannedRoute',
|
||||
_if6974nb.RouteHint => 'RouteHint',
|
||||
_isqtfus6.RoutePoint => 'RoutePoint',
|
||||
_ifl4j5ep.RouteProfile => 'RouteProfile',
|
||||
_ix4aepwk.RouteRefused => 'RouteRefused',
|
||||
_ic7f85bc.RouteStretch => 'RouteStretch',
|
||||
_i5b9b17z.Workout => 'Workout',
|
||||
_i92mefto.WorkoutSeries => 'WorkoutSeries',
|
||||
_ => null,
|
||||
|
|
@ -829,6 +912,18 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
return 'ContentKey';
|
||||
case _ia5j8fch.Profile():
|
||||
return 'Profile';
|
||||
case _ikjgxraq.PlannedRoute():
|
||||
return 'PlannedRoute';
|
||||
case _if6974nb.RouteHint():
|
||||
return 'RouteHint';
|
||||
case _isqtfus6.RoutePoint():
|
||||
return 'RoutePoint';
|
||||
case _ifl4j5ep.RouteProfile():
|
||||
return 'RouteProfile';
|
||||
case _ix4aepwk.RouteRefused():
|
||||
return 'RouteRefused';
|
||||
case _ic7f85bc.RouteStretch():
|
||||
return 'RouteStretch';
|
||||
case _i5b9b17z.Workout():
|
||||
return 'Workout';
|
||||
case _i92mefto.WorkoutSeries():
|
||||
|
|
@ -896,6 +991,24 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
if (dataClassName == 'Profile') {
|
||||
return deserialize<_ia5j8fch.Profile>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'PlannedRoute') {
|
||||
return deserialize<_ikjgxraq.PlannedRoute>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'RouteHint') {
|
||||
return deserialize<_if6974nb.RouteHint>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'RoutePoint') {
|
||||
return deserialize<_isqtfus6.RoutePoint>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'RouteProfile') {
|
||||
return deserialize<_ifl4j5ep.RouteProfile>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'RouteRefused') {
|
||||
return deserialize<_ix4aepwk.RouteRefused>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'RouteStretch') {
|
||||
return deserialize<_ic7f85bc.RouteStretch>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'Workout') {
|
||||
return deserialize<_i5b9b17z.Workout>(data['data']);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,169 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:halkyon_train_client/src/protocol/protocol.dart' as _ip4ob8rr;
|
||||
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
||||
import '../routing/route_hint.dart' as _iocqct4j;
|
||||
import '../routing/route_point.dart' as _iafawe19;
|
||||
import '../routing/route_stretch.dart' as _i8kofibb;
|
||||
|
||||
/// A route the instance's BRouter planned (ADR 0030, D44): its points in
|
||||
/// order, their elevations in metres (empty where BRouter knows none; a point
|
||||
/// without one takes the one before), its length, its climb and descent
|
||||
/// (climbs above 3 m, as a workout's), its turns, and its stretches in order.
|
||||
/// Nothing of it is kept on the instance: the app seals what it saves.
|
||||
abstract class PlannedRoute
|
||||
implements _isc.SerializableModel, _isc.ProtocolSerialization {
|
||||
PlannedRoute._({
|
||||
required this.points,
|
||||
required this.elevations,
|
||||
required this.distanceMetres,
|
||||
required this.ascentMetres,
|
||||
required this.descentMetres,
|
||||
required this.hints,
|
||||
required this.stretches,
|
||||
});
|
||||
|
||||
factory PlannedRoute({
|
||||
required List<_iafawe19.RoutePoint> points,
|
||||
required List<double> elevations,
|
||||
required int distanceMetres,
|
||||
required int ascentMetres,
|
||||
required int descentMetres,
|
||||
required List<_iocqct4j.RouteHint> hints,
|
||||
required List<_i8kofibb.RouteStretch> stretches,
|
||||
}) = _PlannedRouteImpl;
|
||||
|
||||
factory PlannedRoute.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return PlannedRoute(
|
||||
points: _ip4ob8rr.Protocol().deserialize<List<_iafawe19.RoutePoint>>(
|
||||
jsonSerialization['points'],
|
||||
),
|
||||
elevations: _ip4ob8rr.Protocol().deserialize<List<double>>(
|
||||
jsonSerialization['elevations'],
|
||||
),
|
||||
distanceMetres: jsonSerialization['distanceMetres'] as int,
|
||||
ascentMetres: jsonSerialization['ascentMetres'] as int,
|
||||
descentMetres: jsonSerialization['descentMetres'] as int,
|
||||
hints: _ip4ob8rr.Protocol().deserialize<List<_iocqct4j.RouteHint>>(
|
||||
jsonSerialization['hints'],
|
||||
),
|
||||
stretches: _ip4ob8rr.Protocol().deserialize<List<_i8kofibb.RouteStretch>>(
|
||||
jsonSerialization['stretches'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<_iafawe19.RoutePoint> points;
|
||||
|
||||
List<double> elevations;
|
||||
|
||||
int distanceMetres;
|
||||
|
||||
int ascentMetres;
|
||||
|
||||
int descentMetres;
|
||||
|
||||
List<_iocqct4j.RouteHint> hints;
|
||||
|
||||
List<_i8kofibb.RouteStretch> stretches;
|
||||
|
||||
/// Returns a shallow copy of this [PlannedRoute]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
PlannedRoute copyWith({
|
||||
List<_iafawe19.RoutePoint>? points,
|
||||
List<double>? elevations,
|
||||
int? distanceMetres,
|
||||
int? ascentMetres,
|
||||
int? descentMetres,
|
||||
List<_iocqct4j.RouteHint>? hints,
|
||||
List<_i8kofibb.RouteStretch>? stretches,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'PlannedRoute',
|
||||
'points': points.toJson(valueToJson: (v) => v.toJson()),
|
||||
'elevations': elevations.toJson(),
|
||||
'distanceMetres': distanceMetres,
|
||||
'ascentMetres': ascentMetres,
|
||||
'descentMetres': descentMetres,
|
||||
'hints': hints.toJson(valueToJson: (v) => v.toJson()),
|
||||
'stretches': stretches.toJson(valueToJson: (v) => v.toJson()),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'PlannedRoute',
|
||||
'points': points.toJson(valueToJson: (v) => v.toJsonForProtocol()),
|
||||
'elevations': elevations.toJson(),
|
||||
'distanceMetres': distanceMetres,
|
||||
'ascentMetres': ascentMetres,
|
||||
'descentMetres': descentMetres,
|
||||
'hints': hints.toJson(valueToJson: (v) => v.toJsonForProtocol()),
|
||||
'stretches': stretches.toJson(valueToJson: (v) => v.toJsonForProtocol()),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _isc.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _PlannedRouteImpl extends PlannedRoute {
|
||||
_PlannedRouteImpl({
|
||||
required List<_iafawe19.RoutePoint> points,
|
||||
required List<double> elevations,
|
||||
required int distanceMetres,
|
||||
required int ascentMetres,
|
||||
required int descentMetres,
|
||||
required List<_iocqct4j.RouteHint> hints,
|
||||
required List<_i8kofibb.RouteStretch> stretches,
|
||||
}) : super._(
|
||||
points: points,
|
||||
elevations: elevations,
|
||||
distanceMetres: distanceMetres,
|
||||
ascentMetres: ascentMetres,
|
||||
descentMetres: descentMetres,
|
||||
hints: hints,
|
||||
stretches: stretches,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [PlannedRoute]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
@override
|
||||
PlannedRoute copyWith({
|
||||
List<_iafawe19.RoutePoint>? points,
|
||||
List<double>? elevations,
|
||||
int? distanceMetres,
|
||||
int? ascentMetres,
|
||||
int? descentMetres,
|
||||
List<_iocqct4j.RouteHint>? hints,
|
||||
List<_i8kofibb.RouteStretch>? stretches,
|
||||
}) {
|
||||
return PlannedRoute(
|
||||
points: points ?? this.points.map((e0) => e0.copyWith()).toList(),
|
||||
elevations: elevations ?? this.elevations.map((e0) => e0).toList(),
|
||||
distanceMetres: distanceMetres ?? this.distanceMetres,
|
||||
ascentMetres: ascentMetres ?? this.ascentMetres,
|
||||
descentMetres: descentMetres ?? this.descentMetres,
|
||||
hints: hints ?? this.hints.map((e0) => e0.copyWith()).toList(),
|
||||
stretches:
|
||||
stretches ?? this.stretches.map((e0) => e0.copyWith()).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
||||
|
||||
/// A turn of a route planned, as BRouter hints it (ADR 0030, D45): at the
|
||||
/// point [index] of the route, BRouter's command (1 straight on, 2 left, 3
|
||||
/// slightly left, 4 sharply left, 5 right, 6 slightly right, 7 sharply
|
||||
/// right, 8 keep left, 9 keep right, 10 and 11 U-turns, 13 and 14
|
||||
/// roundabouts, 15 a 180° turn, 17 and 18 exits, 16 a beeline), the exit of
|
||||
/// a roundabout, the metres to the next hint and the turn's angle.
|
||||
abstract class RouteHint
|
||||
implements _isc.SerializableModel, _isc.ProtocolSerialization {
|
||||
RouteHint._({
|
||||
required this.index,
|
||||
required this.command,
|
||||
required this.exit,
|
||||
required this.metres,
|
||||
required this.angle,
|
||||
});
|
||||
|
||||
factory RouteHint({
|
||||
required int index,
|
||||
required int command,
|
||||
required int exit,
|
||||
required int metres,
|
||||
required int angle,
|
||||
}) = _RouteHintImpl;
|
||||
|
||||
factory RouteHint.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return RouteHint(
|
||||
index: jsonSerialization['index'] as int,
|
||||
command: jsonSerialization['command'] as int,
|
||||
exit: jsonSerialization['exit'] as int,
|
||||
metres: jsonSerialization['metres'] as int,
|
||||
angle: jsonSerialization['angle'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
int index;
|
||||
|
||||
int command;
|
||||
|
||||
int exit;
|
||||
|
||||
int metres;
|
||||
|
||||
int angle;
|
||||
|
||||
/// Returns a shallow copy of this [RouteHint]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
RouteHint copyWith({
|
||||
int? index,
|
||||
int? command,
|
||||
int? exit,
|
||||
int? metres,
|
||||
int? angle,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'RouteHint',
|
||||
'index': index,
|
||||
'command': command,
|
||||
'exit': exit,
|
||||
'metres': metres,
|
||||
'angle': angle,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'RouteHint',
|
||||
'index': index,
|
||||
'command': command,
|
||||
'exit': exit,
|
||||
'metres': metres,
|
||||
'angle': angle,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _isc.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _RouteHintImpl extends RouteHint {
|
||||
_RouteHintImpl({
|
||||
required int index,
|
||||
required int command,
|
||||
required int exit,
|
||||
required int metres,
|
||||
required int angle,
|
||||
}) : super._(
|
||||
index: index,
|
||||
command: command,
|
||||
exit: exit,
|
||||
metres: metres,
|
||||
angle: angle,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [RouteHint]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
@override
|
||||
RouteHint copyWith({
|
||||
int? index,
|
||||
int? command,
|
||||
int? exit,
|
||||
int? metres,
|
||||
int? angle,
|
||||
}) {
|
||||
return RouteHint(
|
||||
index: index ?? this.index,
|
||||
command: command ?? this.command,
|
||||
exit: exit ?? this.exit,
|
||||
metres: metres ?? this.metres,
|
||||
angle: angle ?? this.angle,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
||||
|
||||
/// A point on the earth, in degrees (WGS 84): a waypoint asked for, or a
|
||||
/// point of a route planned (ADR 0030, D44).
|
||||
abstract class RoutePoint
|
||||
implements _isc.SerializableModel, _isc.ProtocolSerialization {
|
||||
RoutePoint._({
|
||||
required this.lat,
|
||||
required this.lon,
|
||||
});
|
||||
|
||||
factory RoutePoint({
|
||||
required double lat,
|
||||
required double lon,
|
||||
}) = _RoutePointImpl;
|
||||
|
||||
factory RoutePoint.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return RoutePoint(
|
||||
lat: (jsonSerialization['lat'] as num).toDouble(),
|
||||
lon: (jsonSerialization['lon'] as num).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
double lat;
|
||||
|
||||
double lon;
|
||||
|
||||
/// Returns a shallow copy of this [RoutePoint]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
RoutePoint copyWith({
|
||||
double? lat,
|
||||
double? lon,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'RoutePoint',
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'RoutePoint',
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _isc.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoutePointImpl extends RoutePoint {
|
||||
_RoutePointImpl({
|
||||
required double lat,
|
||||
required double lon,
|
||||
}) : super._(
|
||||
lat: lat,
|
||||
lon: lon,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [RoutePoint]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
@override
|
||||
RoutePoint copyWith({
|
||||
double? lat,
|
||||
double? lon,
|
||||
}) {
|
||||
return RoutePoint(
|
||||
lat: lat ?? this.lat,
|
||||
lon: lon ?? this.lon,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
||||
|
||||
/// How a route is planned (ADR 0030, D44): running on Train's own BRouter
|
||||
/// profile, hiking (`hiking-mountain`), a road bike (`fastbike`), a trekking
|
||||
/// bike (`trekking`), gravel (`gravel`) or a mountain bike (`mtb`).
|
||||
enum RouteProfile implements _isc.SerializableModel {
|
||||
run,
|
||||
hike,
|
||||
roadBike,
|
||||
trekkingBike,
|
||||
gravel,
|
||||
mtb;
|
||||
|
||||
static RouteProfile fromJson(String name) {
|
||||
switch (name) {
|
||||
case 'run':
|
||||
return RouteProfile.run;
|
||||
case 'hike':
|
||||
return RouteProfile.hike;
|
||||
case 'roadBike':
|
||||
return RouteProfile.roadBike;
|
||||
case 'trekkingBike':
|
||||
return RouteProfile.trekkingBike;
|
||||
case 'gravel':
|
||||
return RouteProfile.gravel;
|
||||
case 'mtb':
|
||||
return RouteProfile.mtb;
|
||||
default:
|
||||
throw ArgumentError(
|
||||
'Value "$name" cannot be converted to "RouteProfile"',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toJson() => name;
|
||||
|
||||
@override
|
||||
String toString() => name;
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
||||
|
||||
/// Why the instance plans no route (ADR 0030, D44): `off` where it has no
|
||||
/// BRouter, `invalid` for waypoints it doesn't take (fewer than two, more
|
||||
/// than `Routing.maxWaypoints`, off the earth), `tooOften` beyond
|
||||
/// `Routing.perMinute` plans a minute, `noRoute` where BRouter found none
|
||||
/// (outside the region it holds, or no way between), `unreachable` where it
|
||||
/// didn't answer.
|
||||
abstract class RouteRefused
|
||||
implements
|
||||
_isc.SerializableException,
|
||||
_isc.SerializableModel,
|
||||
_isc.ProtocolSerialization {
|
||||
RouteRefused._({required this.reason});
|
||||
|
||||
factory RouteRefused({required String reason}) = _RouteRefusedImpl;
|
||||
|
||||
factory RouteRefused.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return RouteRefused(reason: jsonSerialization['reason'] as String);
|
||||
}
|
||||
|
||||
String reason;
|
||||
|
||||
/// Returns a shallow copy of this [RouteRefused]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
RouteRefused copyWith({String? reason});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'RouteRefused',
|
||||
'reason': reason,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'RouteRefused',
|
||||
'reason': reason,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RouteRefused(reason: $reason)';
|
||||
}
|
||||
}
|
||||
|
||||
class _RouteRefusedImpl extends RouteRefused {
|
||||
_RouteRefusedImpl({required String reason}) : super._(reason: reason);
|
||||
|
||||
/// Returns a shallow copy of this [RouteRefused]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
@override
|
||||
RouteRefused copyWith({String? reason}) {
|
||||
return RouteRefused(reason: reason ?? this.reason);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
||||
|
||||
/// A stretch of a route planned with what it runs on (ADR 0030, D44): its
|
||||
/// length, its OpenStreetMap `surface` and `highway` where tagged.
|
||||
abstract class RouteStretch
|
||||
implements _isc.SerializableModel, _isc.ProtocolSerialization {
|
||||
RouteStretch._({
|
||||
required this.metres,
|
||||
this.surface,
|
||||
this.highway,
|
||||
});
|
||||
|
||||
factory RouteStretch({
|
||||
required int metres,
|
||||
String? surface,
|
||||
String? highway,
|
||||
}) = _RouteStretchImpl;
|
||||
|
||||
factory RouteStretch.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return RouteStretch(
|
||||
metres: jsonSerialization['metres'] as int,
|
||||
surface: jsonSerialization['surface'] as String?,
|
||||
highway: jsonSerialization['highway'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
int metres;
|
||||
|
||||
String? surface;
|
||||
|
||||
String? highway;
|
||||
|
||||
/// Returns a shallow copy of this [RouteStretch]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
RouteStretch copyWith({
|
||||
int? metres,
|
||||
String? surface,
|
||||
String? highway,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'RouteStretch',
|
||||
'metres': metres,
|
||||
if (surface != null) 'surface': surface,
|
||||
if (highway != null) 'highway': highway,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'RouteStretch',
|
||||
'metres': metres,
|
||||
if (surface != null) 'surface': surface,
|
||||
if (highway != null) 'highway': highway,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _isc.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _RouteStretchImpl extends RouteStretch {
|
||||
_RouteStretchImpl({
|
||||
required int metres,
|
||||
String? surface,
|
||||
String? highway,
|
||||
}) : super._(
|
||||
metres: metres,
|
||||
surface: surface,
|
||||
highway: highway,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [RouteStretch]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
@override
|
||||
RouteStretch copyWith({
|
||||
int? metres,
|
||||
Object? surface = _Undefined,
|
||||
Object? highway = _Undefined,
|
||||
}) {
|
||||
return RouteStretch(
|
||||
metres: metres ?? this.metres,
|
||||
surface: surface is String? ? surface : this.surface,
|
||||
highway: highway is String? ? highway : this.highway,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,10 @@ import 'package:halkyon_base_server/halkyon_base_server.dart' as _ik8molhd;
|
|||
import 'package:halkyon_oidc_server/halkyon_oidc_server.dart' as _ipoqfiqr;
|
||||
import 'package:halkyon_train_server/src/generated/future_calls.dart'
|
||||
as _i59xasdy;
|
||||
import 'package:halkyon_train_server/src/generated/routing/route_point.dart'
|
||||
as _is4pcfub;
|
||||
import 'package:halkyon_train_server/src/generated/routing/route_profile.dart'
|
||||
as _iex9meso;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
import 'package:serverpod_auth_core_server/serverpod_auth_core_server.dart'
|
||||
as _iacs;
|
||||
|
|
@ -23,6 +27,7 @@ import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart
|
|||
as _izehhkf5;
|
||||
import '../firmware/firmware_endpoint.dart' as _id2y0j3h;
|
||||
import '../map/map_endpoint.dart' as _i35zr6r3;
|
||||
import '../routing/routing_endpoint.dart' as _ivjydyye;
|
||||
export 'future_calls.dart' show ServerpodFutureCallsGetter;
|
||||
|
||||
class Endpoints extends _is.EndpointDispatch {
|
||||
|
|
@ -41,6 +46,12 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
'map',
|
||||
null,
|
||||
),
|
||||
'routing': _ivjydyye.RoutingEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
'routing',
|
||||
null,
|
||||
),
|
||||
};
|
||||
connectors['firmware'] = _is.EndpointConnector(
|
||||
name: 'firmware',
|
||||
|
|
@ -130,6 +141,47 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
),
|
||||
},
|
||||
);
|
||||
connectors['routing'] = _is.EndpointConnector(
|
||||
name: 'routing',
|
||||
endpoint: endpoints['routing']!,
|
||||
methodConnectors: {
|
||||
'offered': _is.MethodConnector(
|
||||
name: 'offered',
|
||||
params: {},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async => (endpoints['routing'] as _ivjydyye.RoutingEndpoint)
|
||||
.offered(session),
|
||||
),
|
||||
'plan': _is.MethodConnector(
|
||||
name: 'plan',
|
||||
params: {
|
||||
'waypoints': _is.ParameterDescription(
|
||||
name: 'waypoints',
|
||||
type: _is.getType<List<_is4pcfub.RoutePoint>>(),
|
||||
nullable: false,
|
||||
),
|
||||
'profile': _is.ParameterDescription(
|
||||
name: 'profile',
|
||||
type: _is.getType<_iex9meso.RouteProfile>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['routing'] as _ivjydyye.RoutingEndpoint).plan(
|
||||
session,
|
||||
params['waypoints'],
|
||||
params['profile'],
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
modules['halkyon_base'] = _ik8molhd.Endpoints()
|
||||
..initializeEndpoints(server);
|
||||
modules['serverpod_auth_idp'] = _iais.Endpoints()
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:halkyon_base_server/halkyon_base_server.dart' as _ik8molhd;
|
||||
import 'package:halkyon_oidc_server/halkyon_oidc_server.dart' as _ipoqfiqr;
|
||||
import 'package:halkyon_train_server/src/generated/routing/route_point.dart'
|
||||
as _is4pcfub;
|
||||
import 'package:serverpod/protocol.dart' as _isp;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
import 'package:serverpod_auth_core_server/serverpod_auth_core_server.dart'
|
||||
|
|
@ -30,6 +32,12 @@ import 'map/map_source.dart' as _ikw0qg8x;
|
|||
import 'plan/plan.dart' as _i45prw73;
|
||||
import 'privacy/content_key.dart' as _imed3x5d;
|
||||
import 'profile/profile.dart' as _ia5j8fch;
|
||||
import 'routing/planned_route.dart' as _ikjgxraq;
|
||||
import 'routing/route_hint.dart' as _if6974nb;
|
||||
import 'routing/route_point.dart' as _isqtfus6;
|
||||
import 'routing/route_profile.dart' as _ifl4j5ep;
|
||||
import 'routing/route_refused.dart' as _ix4aepwk;
|
||||
import 'routing/route_stretch.dart' as _ic7f85bc;
|
||||
import 'workout/workout.dart' as _i5b9b17z;
|
||||
import 'workout/workout_series.dart' as _i92mefto;
|
||||
export 'day/day.dart';
|
||||
|
|
@ -41,6 +49,12 @@ export 'map/map_source.dart';
|
|||
export 'plan/plan.dart';
|
||||
export 'privacy/content_key.dart';
|
||||
export 'profile/profile.dart';
|
||||
export 'routing/planned_route.dart';
|
||||
export 'routing/route_hint.dart';
|
||||
export 'routing/route_point.dart';
|
||||
export 'routing/route_profile.dart';
|
||||
export 'routing/route_refused.dart';
|
||||
export 'routing/route_stretch.dart';
|
||||
export 'workout/workout.dart';
|
||||
export 'workout/workout_series.dart';
|
||||
export 'sync_tables.dart';
|
||||
|
|
@ -705,6 +719,24 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (t == _ia5j8fch.Profile) {
|
||||
return _ia5j8fch.Profile.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ikjgxraq.PlannedRoute) {
|
||||
return _ikjgxraq.PlannedRoute.fromJson(data) as T;
|
||||
}
|
||||
if (t == _if6974nb.RouteHint) {
|
||||
return _if6974nb.RouteHint.fromJson(data) as T;
|
||||
}
|
||||
if (t == _isqtfus6.RoutePoint) {
|
||||
return _isqtfus6.RoutePoint.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ifl4j5ep.RouteProfile) {
|
||||
return _ifl4j5ep.RouteProfile.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ix4aepwk.RouteRefused) {
|
||||
return _ix4aepwk.RouteRefused.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ic7f85bc.RouteStretch) {
|
||||
return _ic7f85bc.RouteStretch.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i5b9b17z.Workout) {
|
||||
return _i5b9b17z.Workout.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -741,6 +773,24 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (t == _is.getType<_ia5j8fch.Profile?>()) {
|
||||
return (data != null ? _ia5j8fch.Profile.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _is.getType<_ikjgxraq.PlannedRoute?>()) {
|
||||
return (data != null ? _ikjgxraq.PlannedRoute.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _is.getType<_if6974nb.RouteHint?>()) {
|
||||
return (data != null ? _if6974nb.RouteHint.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _is.getType<_isqtfus6.RoutePoint?>()) {
|
||||
return (data != null ? _isqtfus6.RoutePoint.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _is.getType<_ifl4j5ep.RouteProfile?>()) {
|
||||
return (data != null ? _ifl4j5ep.RouteProfile.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _is.getType<_ix4aepwk.RouteRefused?>()) {
|
||||
return (data != null ? _ix4aepwk.RouteRefused.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _is.getType<_ic7f85bc.RouteStretch?>()) {
|
||||
return (data != null ? _ic7f85bc.RouteStretch.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _is.getType<_i5b9b17z.Workout?>()) {
|
||||
return (data != null ? _i5b9b17z.Workout.fromJson(data) : null) as T;
|
||||
}
|
||||
|
|
@ -748,6 +798,33 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return (data != null ? _i92mefto.WorkoutSeries.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == List<_isqtfus6.RoutePoint>) {
|
||||
return (data as List)
|
||||
.map((e) => deserialize<_isqtfus6.RoutePoint>(e))
|
||||
.toList()
|
||||
as T;
|
||||
}
|
||||
if (t == List<double>) {
|
||||
return (data as List).map((e) => deserialize<double>(e)).toList() as T;
|
||||
}
|
||||
if (t == List<_if6974nb.RouteHint>) {
|
||||
return (data as List)
|
||||
.map((e) => deserialize<_if6974nb.RouteHint>(e))
|
||||
.toList()
|
||||
as T;
|
||||
}
|
||||
if (t == List<_ic7f85bc.RouteStretch>) {
|
||||
return (data as List)
|
||||
.map((e) => deserialize<_ic7f85bc.RouteStretch>(e))
|
||||
.toList()
|
||||
as T;
|
||||
}
|
||||
if (t == List<_is4pcfub.RoutePoint>) {
|
||||
return (data as List)
|
||||
.map((e) => deserialize<_is4pcfub.RoutePoint>(e))
|
||||
.toList()
|
||||
as T;
|
||||
}
|
||||
try {
|
||||
return _ik8molhd.Protocol().deserialize<T>(data, t);
|
||||
} on _is.DeserializationTypeNotFoundException catch (_) {}
|
||||
|
|
@ -780,6 +857,12 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
_i45prw73.Plan => 'Plan',
|
||||
_imed3x5d.ContentKey => 'ContentKey',
|
||||
_ia5j8fch.Profile => 'Profile',
|
||||
_ikjgxraq.PlannedRoute => 'PlannedRoute',
|
||||
_if6974nb.RouteHint => 'RouteHint',
|
||||
_isqtfus6.RoutePoint => 'RoutePoint',
|
||||
_ifl4j5ep.RouteProfile => 'RouteProfile',
|
||||
_ix4aepwk.RouteRefused => 'RouteRefused',
|
||||
_ic7f85bc.RouteStretch => 'RouteStretch',
|
||||
_i5b9b17z.Workout => 'Workout',
|
||||
_i92mefto.WorkoutSeries => 'WorkoutSeries',
|
||||
_ => null,
|
||||
|
|
@ -817,6 +900,18 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return 'ContentKey';
|
||||
case _ia5j8fch.Profile():
|
||||
return 'Profile';
|
||||
case _ikjgxraq.PlannedRoute():
|
||||
return 'PlannedRoute';
|
||||
case _if6974nb.RouteHint():
|
||||
return 'RouteHint';
|
||||
case _isqtfus6.RoutePoint():
|
||||
return 'RoutePoint';
|
||||
case _ifl4j5ep.RouteProfile():
|
||||
return 'RouteProfile';
|
||||
case _ix4aepwk.RouteRefused():
|
||||
return 'RouteRefused';
|
||||
case _ic7f85bc.RouteStretch():
|
||||
return 'RouteStretch';
|
||||
case _i5b9b17z.Workout():
|
||||
return 'Workout';
|
||||
case _i92mefto.WorkoutSeries():
|
||||
|
|
@ -888,6 +983,24 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (dataClassName == 'Profile') {
|
||||
return deserialize<_ia5j8fch.Profile>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'PlannedRoute') {
|
||||
return deserialize<_ikjgxraq.PlannedRoute>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'RouteHint') {
|
||||
return deserialize<_if6974nb.RouteHint>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'RoutePoint') {
|
||||
return deserialize<_isqtfus6.RoutePoint>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'RouteProfile') {
|
||||
return deserialize<_ifl4j5ep.RouteProfile>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'RouteRefused') {
|
||||
return deserialize<_ix4aepwk.RouteRefused>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'RouteStretch') {
|
||||
return deserialize<_ic7f85bc.RouteStretch>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'Workout') {
|
||||
return deserialize<_i5b9b17z.Workout>(data['data']);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,3 +4,6 @@ firmware:
|
|||
- download:
|
||||
map:
|
||||
- source:
|
||||
routing:
|
||||
- offered:
|
||||
- plan:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,169 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:halkyon_train_server/src/generated/protocol.dart' as _iw76w3x7;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
import '../routing/route_hint.dart' as _iocqct4j;
|
||||
import '../routing/route_point.dart' as _iafawe19;
|
||||
import '../routing/route_stretch.dart' as _i8kofibb;
|
||||
|
||||
/// A route the instance's BRouter planned (ADR 0030, D44): its points in
|
||||
/// order, their elevations in metres (empty where BRouter knows none; a point
|
||||
/// without one takes the one before), its length, its climb and descent
|
||||
/// (climbs above 3 m, as a workout's), its turns, and its stretches in order.
|
||||
/// Nothing of it is kept on the instance: the app seals what it saves.
|
||||
abstract class PlannedRoute
|
||||
implements _is.SerializableModel, _is.ProtocolSerialization {
|
||||
PlannedRoute._({
|
||||
required this.points,
|
||||
required this.elevations,
|
||||
required this.distanceMetres,
|
||||
required this.ascentMetres,
|
||||
required this.descentMetres,
|
||||
required this.hints,
|
||||
required this.stretches,
|
||||
});
|
||||
|
||||
factory PlannedRoute({
|
||||
required List<_iafawe19.RoutePoint> points,
|
||||
required List<double> elevations,
|
||||
required int distanceMetres,
|
||||
required int ascentMetres,
|
||||
required int descentMetres,
|
||||
required List<_iocqct4j.RouteHint> hints,
|
||||
required List<_i8kofibb.RouteStretch> stretches,
|
||||
}) = _PlannedRouteImpl;
|
||||
|
||||
factory PlannedRoute.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return PlannedRoute(
|
||||
points: _iw76w3x7.Protocol().deserialize<List<_iafawe19.RoutePoint>>(
|
||||
jsonSerialization['points'],
|
||||
),
|
||||
elevations: _iw76w3x7.Protocol().deserialize<List<double>>(
|
||||
jsonSerialization['elevations'],
|
||||
),
|
||||
distanceMetres: jsonSerialization['distanceMetres'] as int,
|
||||
ascentMetres: jsonSerialization['ascentMetres'] as int,
|
||||
descentMetres: jsonSerialization['descentMetres'] as int,
|
||||
hints: _iw76w3x7.Protocol().deserialize<List<_iocqct4j.RouteHint>>(
|
||||
jsonSerialization['hints'],
|
||||
),
|
||||
stretches: _iw76w3x7.Protocol().deserialize<List<_i8kofibb.RouteStretch>>(
|
||||
jsonSerialization['stretches'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<_iafawe19.RoutePoint> points;
|
||||
|
||||
List<double> elevations;
|
||||
|
||||
int distanceMetres;
|
||||
|
||||
int ascentMetres;
|
||||
|
||||
int descentMetres;
|
||||
|
||||
List<_iocqct4j.RouteHint> hints;
|
||||
|
||||
List<_i8kofibb.RouteStretch> stretches;
|
||||
|
||||
/// Returns a shallow copy of this [PlannedRoute]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
PlannedRoute copyWith({
|
||||
List<_iafawe19.RoutePoint>? points,
|
||||
List<double>? elevations,
|
||||
int? distanceMetres,
|
||||
int? ascentMetres,
|
||||
int? descentMetres,
|
||||
List<_iocqct4j.RouteHint>? hints,
|
||||
List<_i8kofibb.RouteStretch>? stretches,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'PlannedRoute',
|
||||
'points': points.toJson(valueToJson: (v) => v.toJson()),
|
||||
'elevations': elevations.toJson(),
|
||||
'distanceMetres': distanceMetres,
|
||||
'ascentMetres': ascentMetres,
|
||||
'descentMetres': descentMetres,
|
||||
'hints': hints.toJson(valueToJson: (v) => v.toJson()),
|
||||
'stretches': stretches.toJson(valueToJson: (v) => v.toJson()),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'PlannedRoute',
|
||||
'points': points.toJson(valueToJson: (v) => v.toJsonForProtocol()),
|
||||
'elevations': elevations.toJson(),
|
||||
'distanceMetres': distanceMetres,
|
||||
'ascentMetres': ascentMetres,
|
||||
'descentMetres': descentMetres,
|
||||
'hints': hints.toJson(valueToJson: (v) => v.toJsonForProtocol()),
|
||||
'stretches': stretches.toJson(valueToJson: (v) => v.toJsonForProtocol()),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _PlannedRouteImpl extends PlannedRoute {
|
||||
_PlannedRouteImpl({
|
||||
required List<_iafawe19.RoutePoint> points,
|
||||
required List<double> elevations,
|
||||
required int distanceMetres,
|
||||
required int ascentMetres,
|
||||
required int descentMetres,
|
||||
required List<_iocqct4j.RouteHint> hints,
|
||||
required List<_i8kofibb.RouteStretch> stretches,
|
||||
}) : super._(
|
||||
points: points,
|
||||
elevations: elevations,
|
||||
distanceMetres: distanceMetres,
|
||||
ascentMetres: ascentMetres,
|
||||
descentMetres: descentMetres,
|
||||
hints: hints,
|
||||
stretches: stretches,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [PlannedRoute]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
PlannedRoute copyWith({
|
||||
List<_iafawe19.RoutePoint>? points,
|
||||
List<double>? elevations,
|
||||
int? distanceMetres,
|
||||
int? ascentMetres,
|
||||
int? descentMetres,
|
||||
List<_iocqct4j.RouteHint>? hints,
|
||||
List<_i8kofibb.RouteStretch>? stretches,
|
||||
}) {
|
||||
return PlannedRoute(
|
||||
points: points ?? this.points.map((e0) => e0.copyWith()).toList(),
|
||||
elevations: elevations ?? this.elevations.map((e0) => e0).toList(),
|
||||
distanceMetres: distanceMetres ?? this.distanceMetres,
|
||||
ascentMetres: ascentMetres ?? this.ascentMetres,
|
||||
descentMetres: descentMetres ?? this.descentMetres,
|
||||
hints: hints ?? this.hints.map((e0) => e0.copyWith()).toList(),
|
||||
stretches:
|
||||
stretches ?? this.stretches.map((e0) => e0.copyWith()).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// A turn of a route planned, as BRouter hints it (ADR 0030, D45): at the
|
||||
/// point [index] of the route, BRouter's command (1 straight on, 2 left, 3
|
||||
/// slightly left, 4 sharply left, 5 right, 6 slightly right, 7 sharply
|
||||
/// right, 8 keep left, 9 keep right, 10 and 11 U-turns, 13 and 14
|
||||
/// roundabouts, 15 a 180° turn, 17 and 18 exits, 16 a beeline), the exit of
|
||||
/// a roundabout, the metres to the next hint and the turn's angle.
|
||||
abstract class RouteHint
|
||||
implements _is.SerializableModel, _is.ProtocolSerialization {
|
||||
RouteHint._({
|
||||
required this.index,
|
||||
required this.command,
|
||||
required this.exit,
|
||||
required this.metres,
|
||||
required this.angle,
|
||||
});
|
||||
|
||||
factory RouteHint({
|
||||
required int index,
|
||||
required int command,
|
||||
required int exit,
|
||||
required int metres,
|
||||
required int angle,
|
||||
}) = _RouteHintImpl;
|
||||
|
||||
factory RouteHint.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return RouteHint(
|
||||
index: jsonSerialization['index'] as int,
|
||||
command: jsonSerialization['command'] as int,
|
||||
exit: jsonSerialization['exit'] as int,
|
||||
metres: jsonSerialization['metres'] as int,
|
||||
angle: jsonSerialization['angle'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
int index;
|
||||
|
||||
int command;
|
||||
|
||||
int exit;
|
||||
|
||||
int metres;
|
||||
|
||||
int angle;
|
||||
|
||||
/// Returns a shallow copy of this [RouteHint]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
RouteHint copyWith({
|
||||
int? index,
|
||||
int? command,
|
||||
int? exit,
|
||||
int? metres,
|
||||
int? angle,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'RouteHint',
|
||||
'index': index,
|
||||
'command': command,
|
||||
'exit': exit,
|
||||
'metres': metres,
|
||||
'angle': angle,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'RouteHint',
|
||||
'index': index,
|
||||
'command': command,
|
||||
'exit': exit,
|
||||
'metres': metres,
|
||||
'angle': angle,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _RouteHintImpl extends RouteHint {
|
||||
_RouteHintImpl({
|
||||
required int index,
|
||||
required int command,
|
||||
required int exit,
|
||||
required int metres,
|
||||
required int angle,
|
||||
}) : super._(
|
||||
index: index,
|
||||
command: command,
|
||||
exit: exit,
|
||||
metres: metres,
|
||||
angle: angle,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [RouteHint]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
RouteHint copyWith({
|
||||
int? index,
|
||||
int? command,
|
||||
int? exit,
|
||||
int? metres,
|
||||
int? angle,
|
||||
}) {
|
||||
return RouteHint(
|
||||
index: index ?? this.index,
|
||||
command: command ?? this.command,
|
||||
exit: exit ?? this.exit,
|
||||
metres: metres ?? this.metres,
|
||||
angle: angle ?? this.angle,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// A point on the earth, in degrees (WGS 84): a waypoint asked for, or a
|
||||
/// point of a route planned (ADR 0030, D44).
|
||||
abstract class RoutePoint
|
||||
implements _is.SerializableModel, _is.ProtocolSerialization {
|
||||
RoutePoint._({
|
||||
required this.lat,
|
||||
required this.lon,
|
||||
});
|
||||
|
||||
factory RoutePoint({
|
||||
required double lat,
|
||||
required double lon,
|
||||
}) = _RoutePointImpl;
|
||||
|
||||
factory RoutePoint.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return RoutePoint(
|
||||
lat: (jsonSerialization['lat'] as num).toDouble(),
|
||||
lon: (jsonSerialization['lon'] as num).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
double lat;
|
||||
|
||||
double lon;
|
||||
|
||||
/// Returns a shallow copy of this [RoutePoint]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
RoutePoint copyWith({
|
||||
double? lat,
|
||||
double? lon,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'RoutePoint',
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'RoutePoint',
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoutePointImpl extends RoutePoint {
|
||||
_RoutePointImpl({
|
||||
required double lat,
|
||||
required double lon,
|
||||
}) : super._(
|
||||
lat: lat,
|
||||
lon: lon,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [RoutePoint]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
RoutePoint copyWith({
|
||||
double? lat,
|
||||
double? lon,
|
||||
}) {
|
||||
return RoutePoint(
|
||||
lat: lat ?? this.lat,
|
||||
lon: lon ?? this.lon,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// How a route is planned (ADR 0030, D44): running on Train's own BRouter
|
||||
/// profile, hiking (`hiking-mountain`), a road bike (`fastbike`), a trekking
|
||||
/// bike (`trekking`), gravel (`gravel`) or a mountain bike (`mtb`).
|
||||
enum RouteProfile implements _is.SerializableModel {
|
||||
run,
|
||||
hike,
|
||||
roadBike,
|
||||
trekkingBike,
|
||||
gravel,
|
||||
mtb;
|
||||
|
||||
static RouteProfile fromJson(String name) {
|
||||
switch (name) {
|
||||
case 'run':
|
||||
return RouteProfile.run;
|
||||
case 'hike':
|
||||
return RouteProfile.hike;
|
||||
case 'roadBike':
|
||||
return RouteProfile.roadBike;
|
||||
case 'trekkingBike':
|
||||
return RouteProfile.trekkingBike;
|
||||
case 'gravel':
|
||||
return RouteProfile.gravel;
|
||||
case 'mtb':
|
||||
return RouteProfile.mtb;
|
||||
default:
|
||||
throw ArgumentError(
|
||||
'Value "$name" cannot be converted to "RouteProfile"',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toJson() => name;
|
||||
|
||||
@override
|
||||
String toString() => name;
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// Why the instance plans no route (ADR 0030, D44): `off` where it has no
|
||||
/// BRouter, `invalid` for waypoints it doesn't take (fewer than two, more
|
||||
/// than `Routing.maxWaypoints`, off the earth), `tooOften` beyond
|
||||
/// `Routing.perMinute` plans a minute, `noRoute` where BRouter found none
|
||||
/// (outside the region it holds, or no way between), `unreachable` where it
|
||||
/// didn't answer.
|
||||
abstract class RouteRefused
|
||||
implements
|
||||
_is.SerializableException,
|
||||
_is.SerializableModel,
|
||||
_is.ProtocolSerialization {
|
||||
RouteRefused._({required this.reason});
|
||||
|
||||
factory RouteRefused({required String reason}) = _RouteRefusedImpl;
|
||||
|
||||
factory RouteRefused.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return RouteRefused(reason: jsonSerialization['reason'] as String);
|
||||
}
|
||||
|
||||
String reason;
|
||||
|
||||
/// Returns a shallow copy of this [RouteRefused]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
RouteRefused copyWith({String? reason});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'RouteRefused',
|
||||
'reason': reason,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'RouteRefused',
|
||||
'reason': reason,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RouteRefused(reason: $reason)';
|
||||
}
|
||||
}
|
||||
|
||||
class _RouteRefusedImpl extends RouteRefused {
|
||||
_RouteRefusedImpl({required String reason}) : super._(reason: reason);
|
||||
|
||||
/// Returns a shallow copy of this [RouteRefused]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
RouteRefused copyWith({String? reason}) {
|
||||
return RouteRefused(reason: reason ?? this.reason);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// A stretch of a route planned with what it runs on (ADR 0030, D44): its
|
||||
/// length, its OpenStreetMap `surface` and `highway` where tagged.
|
||||
abstract class RouteStretch
|
||||
implements _is.SerializableModel, _is.ProtocolSerialization {
|
||||
RouteStretch._({
|
||||
required this.metres,
|
||||
this.surface,
|
||||
this.highway,
|
||||
});
|
||||
|
||||
factory RouteStretch({
|
||||
required int metres,
|
||||
String? surface,
|
||||
String? highway,
|
||||
}) = _RouteStretchImpl;
|
||||
|
||||
factory RouteStretch.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return RouteStretch(
|
||||
metres: jsonSerialization['metres'] as int,
|
||||
surface: jsonSerialization['surface'] as String?,
|
||||
highway: jsonSerialization['highway'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
int metres;
|
||||
|
||||
String? surface;
|
||||
|
||||
String? highway;
|
||||
|
||||
/// Returns a shallow copy of this [RouteStretch]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
RouteStretch copyWith({
|
||||
int? metres,
|
||||
String? surface,
|
||||
String? highway,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'RouteStretch',
|
||||
'metres': metres,
|
||||
if (surface != null) 'surface': surface,
|
||||
if (highway != null) 'highway': highway,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'RouteStretch',
|
||||
'metres': metres,
|
||||
if (surface != null) 'surface': surface,
|
||||
if (highway != null) 'highway': highway,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _RouteStretchImpl extends RouteStretch {
|
||||
_RouteStretchImpl({
|
||||
required int metres,
|
||||
String? surface,
|
||||
String? highway,
|
||||
}) : super._(
|
||||
metres: metres,
|
||||
surface: surface,
|
||||
highway: highway,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [RouteStretch]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
RouteStretch copyWith({
|
||||
int? metres,
|
||||
Object? surface = _Undefined,
|
||||
Object? highway = _Undefined,
|
||||
}) {
|
||||
return RouteStretch(
|
||||
metres: metres ?? this.metres,
|
||||
surface: surface is String? ? surface : this.surface,
|
||||
highway: highway is String? ? highway : this.highway,
|
||||
);
|
||||
}
|
||||
}
|
||||
243
apps/train/halkyon_train_server/lib/src/routing/brouter.dart
Normal file
243
apps/train/halkyon_train_server/lib/src/routing/brouter.dart
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:halkyon_train_server/src/generated/protocol.dart';
|
||||
|
||||
/// BRouter's name for each of Train's profiles (ADR 0030, D44): its own
|
||||
/// files, and `halkyon-run`, Train's, which the sidecar's profiles hold.
|
||||
String brouterProfileOf(RouteProfile profile) => switch (profile) {
|
||||
RouteProfile.run => 'halkyon-run',
|
||||
RouteProfile.hike => 'hiking-mountain',
|
||||
RouteProfile.roadBike => 'fastbike',
|
||||
RouteProfile.trekkingBike => 'trekking',
|
||||
RouteProfile.gravel => 'gravel',
|
||||
RouteProfile.mtb => 'mtb',
|
||||
};
|
||||
|
||||
/// BRouter found no route: outside the segments it holds, or no way.
|
||||
class NoRoute() implements Exception;
|
||||
|
||||
/// BRouter didn't answer, or answered nothing it reads.
|
||||
class BRouterFailed(final String message) implements Exception {
|
||||
@override
|
||||
String toString() => 'BRouterFailed: $message';
|
||||
}
|
||||
|
||||
/// The router (ADR 0030, D11, D44): the instance's BRouter.
|
||||
abstract interface class RoutePlanner() {
|
||||
/// The route through [waypoints] for [profile], as BRouter's GeoJSON.
|
||||
/// Throws [NoRoute] where there is none, [BRouterFailed] where it didn't
|
||||
/// answer.
|
||||
Future<Map<String, Object?>> route(
|
||||
List<RoutePoint> waypoints,
|
||||
RouteProfile profile,
|
||||
);
|
||||
}
|
||||
|
||||
/// [RoutePlanner] on BRouter's HTTP server at [address] (`http://brouter:17777/`),
|
||||
/// which only the server reaches; what it is asked is never logged.
|
||||
class HttpBRouter(final Uri address) implements RoutePlanner {
|
||||
/// How long a route may take: a long one across a country takes seconds.
|
||||
static const _timeout = Duration(seconds: 30);
|
||||
|
||||
@override
|
||||
Future<Map<String, Object?>> route(
|
||||
List<RoutePoint> waypoints,
|
||||
RouteProfile profile,
|
||||
) async {
|
||||
final client = HttpClient()..connectionTimeout = _timeout;
|
||||
try {
|
||||
final uri = address
|
||||
.resolve('brouter')
|
||||
.replace(
|
||||
queryParameters: {
|
||||
'lonlats': [
|
||||
for (final point in waypoints) '${point.lon},${point.lat}',
|
||||
].join('|'),
|
||||
'profile': brouterProfileOf(profile),
|
||||
'alternativeidx': '0',
|
||||
'format': 'geojson',
|
||||
// Locus' turn hints: exits told apart from keeping left or right.
|
||||
'timode': '2',
|
||||
},
|
||||
);
|
||||
final HttpClientResponse response;
|
||||
final String body;
|
||||
try {
|
||||
response = await (await client.getUrl(uri)).close().timeout(_timeout);
|
||||
body = await utf8.decodeStream(response).timeout(_timeout);
|
||||
} on Object catch (error) {
|
||||
throw BRouterFailed(error.runtimeType.toString());
|
||||
}
|
||||
// BRouter answers a route it can't find with an error and a message.
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
throw NoRoute();
|
||||
}
|
||||
try {
|
||||
return jsonDecode(body) as Map<String, Object?>;
|
||||
} on Object {
|
||||
throw BRouterFailed('no GeoJSON');
|
||||
}
|
||||
} finally {
|
||||
client.close(force: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The climb that counts, as a workout's (D24): GPS's or the elevation
|
||||
/// model's noise is a few metres.
|
||||
const _climbThreshold = 3;
|
||||
|
||||
/// The route [geojson] holds, as [HttpBRouter] answers it: BRouter's one
|
||||
/// feature, its coordinates `[lon, lat, elevation]`, its `voicehints`
|
||||
/// `[index, command, exit, metres to the next, angle]`, and its `messages`,
|
||||
/// a table of stretches whose first row names the columns (`Distance`,
|
||||
/// `WayTags`). Numbers in its properties come as strings.
|
||||
PlannedRoute plannedRouteOf(Map<String, Object?> geojson) {
|
||||
final feature = switch (geojson['features']) {
|
||||
[final Map<String, Object?> feature, ...] => feature,
|
||||
_ => throw BRouterFailed('no feature'),
|
||||
};
|
||||
final properties = feature['properties'] as Map<String, Object?>? ?? {};
|
||||
final coordinates = switch (feature['geometry']) {
|
||||
{'coordinates': final List<Object?> coordinates} => coordinates,
|
||||
_ => throw BRouterFailed('no coordinates'),
|
||||
};
|
||||
final points = <RoutePoint>[];
|
||||
final elevations = <double>[];
|
||||
var anyElevation = false;
|
||||
for (final coordinate in coordinates) {
|
||||
if (coordinate case [final num lon, final num lat, ...final rest]) {
|
||||
points.add(RoutePoint(lat: lat.toDouble(), lon: lon.toDouble()));
|
||||
final elevation = switch (rest) {
|
||||
[final num elevation, ...] => elevation.toDouble(),
|
||||
_ => null,
|
||||
};
|
||||
anyElevation |= elevation != null;
|
||||
elevations.add(elevation ?? elevations.lastOrNull ?? double.nan);
|
||||
}
|
||||
}
|
||||
if (points.length < 2) {
|
||||
throw BRouterFailed('no route');
|
||||
}
|
||||
// A start without an elevation takes the first known.
|
||||
final known = elevations.firstWhere((e) => !e.isNaN, orElse: () => 0);
|
||||
final filled = [
|
||||
for (final e in elevations)
|
||||
if (e.isNaN) known else e,
|
||||
];
|
||||
final (ascent, descent) = _climbs(filled);
|
||||
return PlannedRoute(
|
||||
points: points,
|
||||
elevations: anyElevation ? filled : const [],
|
||||
distanceMetres: _int(properties['track-length']) ?? 0,
|
||||
ascentMetres: anyElevation ? ascent : 0,
|
||||
descentMetres: anyElevation ? descent : 0,
|
||||
hints: [
|
||||
for (final hint in properties['voicehints'] as List<Object?>? ?? [])
|
||||
if (hint case [
|
||||
final Object index,
|
||||
final Object command,
|
||||
final Object exit,
|
||||
final Object metres,
|
||||
final Object angle,
|
||||
...,
|
||||
])
|
||||
if ((
|
||||
_int(index),
|
||||
_int(command),
|
||||
_int(exit),
|
||||
_int(metres),
|
||||
_int(angle),
|
||||
)
|
||||
case (
|
||||
final index?,
|
||||
final command?,
|
||||
final exit?,
|
||||
final metres?,
|
||||
final angle?,
|
||||
)
|
||||
when index >= 0 && index < points.length)
|
||||
RouteHint(
|
||||
index: index,
|
||||
command: command,
|
||||
exit: exit,
|
||||
metres: metres,
|
||||
angle: angle,
|
||||
),
|
||||
],
|
||||
stretches: _stretches(properties['messages']),
|
||||
);
|
||||
}
|
||||
|
||||
/// The climbs and descents above [_climbThreshold], each added up.
|
||||
(int, int) _climbs(List<double> elevations) {
|
||||
if (elevations.isEmpty) {
|
||||
return (0, 0);
|
||||
}
|
||||
var reference = elevations.first;
|
||||
var (up, down) = (0.0, 0.0);
|
||||
for (final elevation in elevations.skip(1)) {
|
||||
if (elevation > reference + _climbThreshold) {
|
||||
up += elevation - reference;
|
||||
reference = elevation;
|
||||
} else if (elevation < reference - _climbThreshold) {
|
||||
down += reference - elevation;
|
||||
reference = elevation;
|
||||
}
|
||||
}
|
||||
return (up.round(), down.round());
|
||||
}
|
||||
|
||||
/// The stretches of BRouter's messages: each row's length and way's tags,
|
||||
/// rows of the same surface and highway joined.
|
||||
List<RouteStretch> _stretches(Object? messages) {
|
||||
if (messages is! List<Object?> || messages.isEmpty) {
|
||||
return const [];
|
||||
}
|
||||
final header = messages.first;
|
||||
if (header is! List<Object?>) {
|
||||
return const [];
|
||||
}
|
||||
final (distance, tags) = (
|
||||
header.indexOf('Distance'),
|
||||
header.indexOf('WayTags'),
|
||||
);
|
||||
if (distance < 0 || tags < 0) {
|
||||
return const [];
|
||||
}
|
||||
final stretches = <RouteStretch>[];
|
||||
for (final row in messages.skip(1)) {
|
||||
if (row is! List<Object?> ||
|
||||
row.length <= (distance > tags ? distance : tags)) {
|
||||
continue;
|
||||
}
|
||||
final metres = _int(row[distance]);
|
||||
if (metres == null || metres <= 0) {
|
||||
continue;
|
||||
}
|
||||
final way = {
|
||||
for (final tag in '${row[tags]}'.split(' '))
|
||||
if (tag.split('=') case [final key, final value]) key: value,
|
||||
};
|
||||
final (surface, highway) = (way['surface'], way['highway']);
|
||||
final last = stretches.lastOrNull;
|
||||
if (last != null && last.surface == surface && last.highway == highway) {
|
||||
stretches[stretches.length - 1] = last.copyWith(
|
||||
metres: last.metres + metres,
|
||||
);
|
||||
} else {
|
||||
stretches.add(
|
||||
RouteStretch(metres: metres, surface: surface, highway: highway),
|
||||
);
|
||||
}
|
||||
}
|
||||
return stretches;
|
||||
}
|
||||
|
||||
int? _int(Object? value) => switch (value) {
|
||||
final int value => value,
|
||||
final num value => value.round(),
|
||||
final String value => num.tryParse(value)?.round(),
|
||||
_ => null,
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue