feat(train): OpenFreeMap's dark style in a dark theme, and MAP_DARK_STYLE_URL for a provider's

The maps follow the theme where the source names a dark style, which
only the instance's own map did: OpenFreeMap, the default source, had
none, so a dark phone showed Liberty's bright map. The default now
hands OpenFreeMap's Dark style beside Liberty, and an operator setting
MAP_STYLE_URL may set MAP_DARK_STYLE_URL (HTTPS, else none) beside it,
passed on by compose.train.yaml and named in .env.example.
ADR 0030, D65.

Whats-New: The map is dark in a dark theme.
Whats-New-De: Die Karte ist im dunklen Design dunkel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-04 20:44:47 +01:00
commit 1c67310c93
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
5 changed files with 39 additions and 4 deletions

View file

@ -3,10 +3,12 @@ import 'dart:io';
import 'package:halkyon_train_server/src/generated/protocol.dart'; import 'package:halkyon_train_server/src/generated/protocol.dart';
/// The map source where the operator sets none (ADR 0030, D11): OpenFreeMap, /// The map source where the operator sets none (ADR 0030, D11): OpenFreeMap,
/// which takes no key and makes no contract, its Liberty style. /// which takes no key and makes no contract, its Liberty style, and its Dark
/// one in a dark theme (D65).
final openFreeMap = MapSource( final openFreeMap = MapSource(
name: 'OpenFreeMap', name: 'OpenFreeMap',
styleUrl: 'https://tiles.openfreemap.org/styles/liberty', styleUrl: 'https://tiles.openfreemap.org/styles/liberty',
darkStyleUrl: 'https://tiles.openfreemap.org/styles/dark',
attribution: 'OpenFreeMap © OpenMapTiles Data from OpenStreetMap', attribution: 'OpenFreeMap © OpenMapTiles Data from OpenStreetMap',
aboutUrl: 'https://openfreemap.org', aboutUrl: 'https://openfreemap.org',
); );
@ -21,7 +23,8 @@ MapConfig mapConfig() => mapConfigFrom(Platform.environment);
/// The map [environment] sets. `MAP_TILES_URL` (D50), the instance's own /// The map [environment] sets. `MAP_TILES_URL` (D50), the instance's own
/// Protomaps tiles as an HTTPS `{z}/{x}/{y}` template, drawn by Train's /// Protomaps tiles as an HTTPS `{z}/{x}/{y}` template, drawn by Train's
/// style, served beside them at `/map/style/light.json` and `dark.json`; /// style, served beside them at `/map/style/light.json` and `dark.json`;
/// else `MAP_STYLE_URL`, a provider's MapLibre style over HTTPS; else /// else `MAP_STYLE_URL`, a provider's MapLibre style over HTTPS, with
/// `MAP_DARK_STYLE_URL` its dark one (HTTPS, else none, D65); else
/// [openFreeMap]. Both with `MAP_SOURCE_NAME` (else the host), /// [openFreeMap]. Both with `MAP_SOURCE_NAME` (else the host),
/// `MAP_ATTRIBUTION` (else OpenStreetMap's credit, and Protomaps' for the /// `MAP_ATTRIBUTION` (else OpenStreetMap's credit, and Protomaps' for the
/// instance's tiles) and `MAP_ABOUT_URL` (HTTPS, else none). /// instance's tiles) and `MAP_ABOUT_URL` (HTTPS, else none).
@ -59,6 +62,7 @@ MapConfig mapConfigFrom(Map<String, String> environment) {
source: MapSource( source: MapSource(
name: set('MAP_SOURCE_NAME') ?? style.host, name: set('MAP_SOURCE_NAME') ?? style.host,
styleUrl: style.toString(), styleUrl: style.toString(),
darkStyleUrl: _https(environment['MAP_DARK_STYLE_URL'])?.toString(),
attribution: set('MAP_ATTRIBUTION') ?? '© OpenStreetMap contributors', attribution: set('MAP_ATTRIBUTION') ?? '© OpenStreetMap contributors',
aboutUrl: about, aboutUrl: about,
), ),

View file

@ -11,6 +11,8 @@ void main() {
expect(source.styleUrl, 'https://tiles.openfreemap.org/styles/liberty'); expect(source.styleUrl, 'https://tiles.openfreemap.org/styles/liberty');
expect(source.attribution, contains('OpenStreetMap')); expect(source.attribution, contains('OpenStreetMap'));
expect(source.aboutUrl, 'https://openfreemap.org'); expect(source.aboutUrl, 'https://openfreemap.org');
// Its dark style in a dark theme (D65).
expect(source.darkStyleUrl, 'https://tiles.openfreemap.org/styles/dark');
}); });
test("an operator's provider is named as they set it", () { test("an operator's provider is named as they set it", () {
@ -31,6 +33,26 @@ void main() {
expect(source.aboutUrl, 'https://www.maptiler.com/privacy-policy/'); expect(source.aboutUrl, 'https://www.maptiler.com/privacy-policy/');
}); });
test("an operator's provider has a dark style where they set one over "
'HTTPS (D65), none otherwise', () {
const style = 'https://maps.example.org/light.json';
expect(
mapSourceFrom(const {
'MAP_STYLE_URL': style,
'MAP_DARK_STYLE_URL': 'https://maps.example.org/dark.json',
}).darkStyleUrl,
'https://maps.example.org/dark.json',
);
for (final dark in [null, '', 'http://maps.example.org/dark.json']) {
expect(
mapSourceFrom({'MAP_STYLE_URL': style, 'MAP_DARK_STYLE_URL': ?dark})
.darkStyleUrl,
isNull,
reason: dark,
);
}
});
test('a provider without a name is named by its host, and credits ' test('a provider without a name is named by its host, and credits '
'OpenStreetMap', () { 'OpenStreetMap', () {
final source = mapSourceFrom(const { final source = mapSourceFrom(const {

View file

@ -69,9 +69,11 @@ COMPOSE_FILE=compose.learn.yaml
# or subscriptions needs a paid plan: its key. # or subscriptions needs a paid plan: its key.
#WEATHER= #WEATHER=
#OPEN_METEO_API_KEY= #OPEN_METEO_API_KEY=
# Train's map (ADR 0030, D22): a MapLibre style over HTTPS, with its name, credit and # Train's map (ADR 0030, D22): a MapLibre style over HTTPS, with its dark one for a
# page; empty: OpenFreeMap's. # dark theme (D65; empty: the light one in both), its name, credit and page; empty:
# OpenFreeMap's, Liberty and Dark.
#MAP_STYLE_URL= #MAP_STYLE_URL=
#MAP_DARK_STYLE_URL=
#MAP_SOURCE_NAME= #MAP_SOURCE_NAME=
#MAP_ATTRIBUTION= #MAP_ATTRIBUTION=
#MAP_ABOUT_URL= #MAP_ABOUT_URL=

View file

@ -45,6 +45,7 @@ services:
OPEN_METEO_API_KEY: ${OPEN_METEO_API_KEY:-} OPEN_METEO_API_KEY: ${OPEN_METEO_API_KEY:-}
# The map source (D22); empty: OpenFreeMap. # The map source (D22); empty: OpenFreeMap.
MAP_STYLE_URL: ${MAP_STYLE_URL:-} MAP_STYLE_URL: ${MAP_STYLE_URL:-}
MAP_DARK_STYLE_URL: ${MAP_DARK_STYLE_URL:-}
MAP_SOURCE_NAME: ${MAP_SOURCE_NAME:-} MAP_SOURCE_NAME: ${MAP_SOURCE_NAME:-}
MAP_ATTRIBUTION: ${MAP_ATTRIBUTION:-} MAP_ATTRIBUTION: ${MAP_ATTRIBUTION:-}
MAP_ABOUT_URL: ${MAP_ABOUT_URL:-} MAP_ABOUT_URL: ${MAP_ABOUT_URL:-}

View file

@ -1725,6 +1725,12 @@ Strava. What was found, 27 Oct:
map showing in the corners. The bar names the activity (Running), not the map showing in the corners. The bar names the activity (Running), not the
workout, whose name is the sheet's first line: once in sight, not twice (D63 had workout, whose name is the sheet's first line: once in sight, not twice (D63 had
the bar take the sheet's title). the bar take the sheet's title).
- **A dark map on OpenFreeMap too**: the maps followed the theme where the source
has a dark style (D50), which only the instance's own map had. OpenFreeMap's
default source now hands its Dark style beside Liberty, and an operator's
provider takes one from `MAP_DARK_STYLE_URL` (HTTPS, else none: the light style
in both themes). A dark Liberty made in the app (its colours inverted) was the
alternative, a style of the app's own for a provider's tiles it can't know.
## Consequences ## Consequences
- Train's sensors are one pattern: open profiles read by the app itself, on the phone; - Train's sensors are one pattern: open profiles read by the app itself, on the phone;