feat(rail): add a Rail layer with keyless live trains

Live trains from 30 keyless operator feeds: GPS positions from 29 and
Irish Rail's station reports, shown as the operators publish them. 28
work out of the box; Renfe long and medium distance and KTMB appear once
their pack is built, because their vehicles name no route.

Packs are opt-in (scripts/build-rail-paths.mjs): the operator's own
route lines, stop names, next stops and trip-update delays for live
trains. Nothing downloads when the app starts, no train is placed from a
timetable, and no request carries a deployer's key.
This commit is contained in:
Denis Groshev committed 2026-11-11 16:06:22 +01:00
1 parent 14db73f201
commit 830c080803
114 files changed
+26639 -26

No files matched your search

+16 -1
View File
@@ -37,7 +37,7 @@ test('every acquisition source can be omitted without losing catalog membership'
test('all acquisition sources may be absent, and enable fails before initializing or fetching', async (t) => {
const actual = catalog({}, t);
assert.equal(actual.layers.length, 40);
assert.equal(actual.layers.length, 41);
const manager = new LayerLifecycle(
{},
{ getSourceAvailability: actual.getSourceAvailability },
@@ -247,3 +247,18 @@ test('absent military acquisition does not poll, suppress flights, or erase supp
assert.equal(registry.isMilitaryLayerActive(), false);
assert.equal(fetches, 0);
});
test('a rail source must carry the feed catalog that keeps unbuilt packs out', () => {
// Without getFeeds the layer could never learn which schedule packs are
// built, so the live feeds that read their routes from one would never run.
assert.throws(
() =>
resolveCatalogSources({ rail: { requestSnapshot: async () => null } }),
/Invalid catalog source: rail/,
);
const source = {
requestSnapshot: async () => null,
getFeeds: async () => [],
};
assert.equal(resolveCatalogSources({ rail: source }).sources.rail, source);
});