feat(train): the instance's own map tiles, drawn by Train's style (ADR 0030, D50)

Protomaps' basemap cut for Europe and served by pmtiles beside Train's server
(compose.maps.yaml), drawn by Train's own style, light and dark, which the
server serves. Only people signed in get tiles: map.source hands a key that
Caddy has the server check before each tile; nothing logs a tile.

Also fixes .dockerignore, which left apps/train/ out of the build context, so
Train's server's and BRouter's images couldn't have been built.

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 16:04:05 +01:00
commit 190c3872f4
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 3726 additions and 97 deletions

View file

@ -84,8 +84,9 @@ class EndpointFirmware extends _isc.EndpointRef {
);
}
/// The map workouts are drawn on (ADR 0030, D11, D22), for the people signed
/// in: an operator's provider may have its key in the style's address.
/// The map workouts are drawn on (ADR 0030, D11, D22, D50), for the people
/// signed in: an operator's provider may have its key in the style's
/// address, and the instance's own tiles are theirs alone.
/// {@category Endpoint}
class EndpointMap extends _isc.EndpointRef {
EndpointMap(_isc.EndpointCaller caller) : super(caller);
@ -93,7 +94,8 @@ class EndpointMap extends _isc.EndpointRef {
@override
String get name => 'map';
/// The instance's map source, as its operator sets it.
/// The instance's map source, as its operator sets it, with a key for its
/// own tiles.
_ida.Future<_iebj1x2g.MapSource> source() =>
caller.callServerEndpoint<_iebj1x2g.MapSource>(
'map',

View file

@ -12,32 +12,38 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:serverpod_client/serverpod_client.dart' as _isc;
/// The map this instance's workouts are drawn on (ADR 0030, D11, D22), as
/// its operator sets it: a MapLibre style, its source's name and the credit
/// it asks for, and where the source says what it sees. OpenFreeMap where
/// nothing is set.
/// The map this instance's workouts are drawn on (ADR 0030, D11, D22, D50),
/// as its operator sets it: a MapLibre style, its source's name and the
/// credit it asks for, and where the source says what it sees. OpenFreeMap
/// where nothing is set; the instance's own tiles where its maps sidecar runs.
abstract class MapSource
implements _isc.SerializableModel, _isc.ProtocolSerialization {
MapSource._({
required this.name,
required this.styleUrl,
this.darkStyleUrl,
required this.attribution,
this.aboutUrl,
this.tileKey,
});
factory MapSource({
required String name,
required String styleUrl,
String? darkStyleUrl,
required String attribution,
String? aboutUrl,
String? tileKey,
}) = _MapSourceImpl;
factory MapSource.fromJson(Map<String, dynamic> jsonSerialization) {
return MapSource(
name: jsonSerialization['name'] as String,
styleUrl: jsonSerialization['styleUrl'] as String,
darkStyleUrl: jsonSerialization['darkStyleUrl'] as String?,
attribution: jsonSerialization['attribution'] as String,
aboutUrl: jsonSerialization['aboutUrl'] as String?,
tileKey: jsonSerialization['tileKey'] as String?,
);
}
@ -48,6 +54,10 @@ abstract class MapSource
/// provider's key may be in it, so only people signed in get it.
String styleUrl;
/// The style for the app's dark theme; null where the source has one
/// alone (D50).
String? darkStyleUrl;
/// The credit the map shows, as the source's terms ask.
String attribution;
@ -55,14 +65,21 @@ abstract class MapSource
/// where the operator names nothing.
String? aboutUrl;
/// What each tile asks with, as `Authorization: Bearer`, where the tiles
/// are the instance's own (D50): valid some 30 days, naming no one; null
/// for every other source.
String? tileKey;
/// Returns a shallow copy of this [MapSource]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
MapSource copyWith({
String? name,
String? styleUrl,
String? darkStyleUrl,
String? attribution,
String? aboutUrl,
String? tileKey,
});
@override
Map<String, dynamic> toJson() {
@ -70,8 +87,10 @@ abstract class MapSource
'__className__': 'MapSource',
'name': name,
'styleUrl': styleUrl,
if (darkStyleUrl != null) 'darkStyleUrl': darkStyleUrl,
'attribution': attribution,
if (aboutUrl != null) 'aboutUrl': aboutUrl,
if (tileKey != null) 'tileKey': tileKey,
};
}
@ -81,8 +100,10 @@ abstract class MapSource
'__className__': 'MapSource',
'name': name,
'styleUrl': styleUrl,
if (darkStyleUrl != null) 'darkStyleUrl': darkStyleUrl,
'attribution': attribution,
if (aboutUrl != null) 'aboutUrl': aboutUrl,
if (tileKey != null) 'tileKey': tileKey,
};
}
@ -98,13 +119,17 @@ class _MapSourceImpl extends MapSource {
_MapSourceImpl({
required String name,
required String styleUrl,
String? darkStyleUrl,
required String attribution,
String? aboutUrl,
String? tileKey,
}) : super._(
name: name,
styleUrl: styleUrl,
darkStyleUrl: darkStyleUrl,
attribution: attribution,
aboutUrl: aboutUrl,
tileKey: tileKey,
);
/// Returns a shallow copy of this [MapSource]
@ -114,14 +139,18 @@ class _MapSourceImpl extends MapSource {
MapSource copyWith({
String? name,
String? styleUrl,
Object? darkStyleUrl = _Undefined,
String? attribution,
Object? aboutUrl = _Undefined,
Object? tileKey = _Undefined,
}) {
return MapSource(
name: name ?? this.name,
styleUrl: styleUrl ?? this.styleUrl,
darkStyleUrl: darkStyleUrl is String? ? darkStyleUrl : this.darkStyleUrl,
attribution: attribution ?? this.attribution,
aboutUrl: aboutUrl is String? ? aboutUrl : this.aboutUrl,
tileKey: tileKey is String? ? tileKey : this.tileKey,
);
}
}