Merge origin/main: CCTV model specs, transit Ontario, Brazil GBFS, camera packs

Conflicts:
- CCTV HUD: main's datasheet FOV, capability range and spec summary,
  with their labels translated; specFovCapabilityToken takes a format
  so the HUD can pass a translated one. Datasheet resolution labels are
  camera data (i18n-ignore).
- Transit: main's route labels and operator mode words, with the
  generic words still translated; an operator's own word stays a name.

Catalogs: es/ru 2566 messages each, complete.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-16 12:58:23 +04:00
commit 48e90c3780
32 files changed
+2390 -36

No files matched your search

+6 -2
View File
@@ -221,8 +221,12 @@ export function createIngestion({ state, services, parts, source }) {
for (const record of records) {
if (isStaleVehicleFix(record, now, fetchedAt)) continue;
const key = transitVehicleKey(feed.id, record.id);
const mode = transitModeFor(feed, record.routeId);
const modeInferred = !transitModeResolved(feed, record.routeId);
const mode = transitModeFor(feed, record.routeId, record.id);
const modeInferred = !transitModeResolved(
feed,
record.routeId,
record.id,
);
// A stamp ahead of the fetch is a clock fault; the fetch time bounds it.
const stamped = vehicleReportTimeMs(record, fetchedAt);
const reportAt =
+54 -8
View File
@@ -4,7 +4,10 @@
* scene state, no network: everything here is exercised by node:test.
*/
import { TRANSIT_MODE_ICON } from '../../data/transitFeeds.js';
import {
TRANSIT_MODE_ICON,
transitRouteLabel,
} from '../../data/transitFeeds.js';
import { displayMotion, playbackPosition } from './movement.js';
export const TRANSIT_SELECTED_OVERLAY_SOURCE_ID = 'transit-selected';
@@ -379,13 +382,16 @@ export function buildTransitSelectionCopy(
) {
const icon = TRANSIT_MODE_ICON[mode] || TRANSIT_MODE_ICON.unknown;
const routeLabel = record.routeId
? t('Route {route}', { route: record.routeId })
? t('Route {route}', { route: transitRouteLabel(feed, record.routeId) })
: t('Vehicle {vehicle}', { vehicle: record.label || record.id });
const title = `${icon} ${routeLabel}`;
// The mode is said in words on its own line. A glyph and a route id do not
// tell a reader that the dot on Tremont Street is a subway train, and a
// subway drawn on a street reads as a bug until the card says what it is.
const kindWord = t(TRANSIT_MODE_WORD[mode] || TRANSIT_MODE_WORD.unknown);
// An operator's own mode word (feed.modeWords) is a name and stays as is.
const kindWord =
feed?.modeWords?.[mode]?.word ||
t(TRANSIT_MODE_WORD[mode] || TRANSIT_MODE_WORD.unknown);
const details = [`${kindWord} · ${feed.name} · ${feed.region}`];
const motion = [];
// What the SCREEN is doing leads, because that is what the reader can check.
@@ -534,17 +540,50 @@ export const TRANSIT_MODE_ABBR = Object.freeze({
unknown: 'TRANSIT',
});
/**
* The mode in the operator's own vocabulary. A Toronto 501 and a Helsinki 1010
* are both the `tram` mode, but only one of them is called a tram by the people
* who ride it. A feed may name the modes it runs; everything else falls through
* to the shared table, so an override renames one word and nothing else.
* @param {string} mode Resolved transit mode.
* @param {object|null} [feed] Registry entry.
* @returns {string}
*/
export function transitModeWord(mode, feed = null) {
return (
feed?.modeWords?.[mode]?.word ||
TRANSIT_MODE_WORD[mode] ||
TRANSIT_MODE_WORD.unknown
);
}
/**
* The operator's short mode word for the fixed-width detection fields.
* @param {string} mode Resolved transit mode.
* @param {object|null} [feed] Registry entry.
* @returns {string}
*/
export function transitModeAbbr(mode, feed = null) {
return (
feed?.modeWords?.[mode]?.abbr ||
TRANSIT_MODE_ABBR[mode] ||
TRANSIT_MODE_ABBR.unknown
);
}
/**
* The bright first line of a vehicle's detection label: its route when the feed
* names one, else the fleet number, else the raw id. What a person standing at
* the stop would call it.
* @param {object} record Normalized vehicle record.
* @param {object|null} [feed] Registry entry, for feeds whose route ids are
* internal keys rather than the number on the vehicle.
* @returns {string}
*/
export function transitDetectionId(record) {
export function transitDetectionId(record, feed = null) {
const route =
typeof record?.routeId === 'string' ? record.routeId.trim() : '';
if (route) return elideRouteText(route);
if (route) return elideRouteText(transitRouteLabel(feed, route));
const label = typeof record?.label === 'string' ? record.label.trim() : '';
if (label) return elideRouteText(label);
return elideRouteText(String(record?.id || 'TRANSIT'));
@@ -576,7 +615,7 @@ export function elideRouteText(text) {
* @returns {string}
*/
export function transitDetectionClass(mode, feed) {
const kind = TRANSIT_MODE_ABBR[mode] || TRANSIT_MODE_ABBR.unknown;
const kind = transitModeAbbr(mode, feed);
const operator =
typeof feed?.name === 'string' ? feed.name.toUpperCase() : '';
return operator ? `${kind} ${operator}`.slice(0, 20) : kind;
@@ -593,10 +632,17 @@ export function transitDetectionClass(mode, feed) {
*
* @param {object} record Normalized vehicle record.
* @param {string} mode Resolved transit mode.
* @param {number} [nowMs]
* @param {object|null} [feed] Registry entry, for its own mode vocabulary.
* @returns {string}
*/
export function transitDetectionMetric(entry, mode, nowMs = Date.now()) {
const kind = TRANSIT_MODE_ABBR[mode] || TRANSIT_MODE_ABBR.unknown;
export function transitDetectionMetric(
entry,
mode,
nowMs = Date.now(),
feed = null,
) {
const kind = transitModeAbbr(mode, feed);
const state = transitVehicleState(entry, nowMs);
return state ? `${kind} ${state}` : kind;
}
+29 -6
View File
@@ -12,6 +12,20 @@ import {
} from '../../data/transitFeeds.js';
import { transitModeTier } from '../../data/transitPresetStyle.js';
/**
* The registry entry behind a vehicle. Live feeds answer first so a feed the
* session disabled stops supplying vocabulary; the registry is the fallback,
* because a vehicle can outlive the poll that brought it.
* @param {object} state Layer state.
* @param {object} entry Vehicle entry.
* @returns {object|null}
*/
function feedForEntry(state, entry) {
const id = entry?.feedId;
if (!id) return null;
return state._activeFeeds.get(id) || getRegisteredTransitFeed(id) || null;
}
/**
* The layer row's answer to "what is on screen and is it honest?".
* @param {object} context
@@ -101,6 +115,7 @@ export function createQueries({ state, parts }) {
contact._entry,
contact._entry.mode,
now,
feedForEntry(state, contact._entry),
);
return;
}
@@ -128,14 +143,17 @@ export function createQueries({ state, parts }) {
// Route and operator text change with the record and the mode, both of
// which arrive on polls — the same events that rebuild this list.
if (!entry.labelId || entry.labelRecord !== entry.record) {
entry.labelId = transitDetectionId(entry.record);
entry.labelId = transitDetectionId(
entry.record,
feedForEntry(state, entry),
);
entry.labelRecord = entry.record;
}
if (!entry.labelClass || entry.labelMode !== entry.mode) {
const feed =
state._activeFeeds.get(entry.feedId) ||
getRegisteredTransitFeed(entry.feedId);
entry.labelClass = transitDetectionClass(entry.mode, feed);
entry.labelClass = transitDetectionClass(
entry.mode,
feedForEntry(state, entry),
);
entry.labelMode = entry.mode;
}
const contact = {
@@ -146,7 +164,12 @@ export function createQueries({ state, parts }) {
id: entry.labelId,
type: 'VEH',
klass: entry.labelClass,
metric: transitDetectionMetric(entry, entry.mode, now),
metric: transitDetectionMetric(
entry,
entry.mode,
now,
feedForEntry(state, entry),
),
// The bracket carries the mode colour in every preset: the detection
// canvas sits above the post-FX chain, where the sprites' own colour
// is lost under NVG and FLIR. Keyless too — the feeds are keyless.
+76
View File
@@ -0,0 +1,76 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
buildTransitSelectionCopy,
transitDetectionClass,
transitDetectionId,
transitDetectionMetric,
transitModeAbbr,
transitModeWord,
} from './policy.js';
import { getTransitFeed } from '../../data/transitFeeds.js';
const ttc = () => getTransitFeed('ttc-toronto');
const hsl = () => getTransitFeed('hsl-helsinki');
test('an operator that names its own vehicles overrides the shared mode word', () => {
// A Toronto 501 is a streetcar. A Helsinki 1010 is a tram. Both are the
// `tram` mode, so the word a reader sees has to come from the operator,
// not from the mode.
assert.equal(transitModeWord('tram', ttc()), 'Streetcar');
assert.equal(transitModeAbbr('tram', ttc()), 'STREETCAR');
assert.equal(transitModeWord('tram', hsl()), 'Tram');
assert.equal(transitModeAbbr('tram', hsl()), 'TRAM');
});
test('an override renames one mode, never the rest of the feed', () => {
assert.equal(transitModeWord('bus', ttc()), 'Bus');
assert.equal(transitModeWord('subway', ttc()), 'Subway');
assert.equal(transitModeAbbr('bus', ttc()), 'BUS');
});
test('a missing feed or unknown mode falls back to the shared table', () => {
assert.equal(transitModeWord('tram', null), 'Tram');
assert.equal(transitModeWord('spaceship', ttc()), 'Transit vehicle');
assert.equal(transitModeAbbr('spaceship', ttc()), 'TRANSIT');
assert.equal(transitModeWord('tram', { name: 'No override' }), 'Tram');
});
test('the selection card calls a Toronto 501 a streetcar', () => {
const record = { id: '4400', routeId: '501', timestamp: 0 };
const copy = buildTransitSelectionCopy(ttc(), record, 'tram', 0, null, null);
assert.match(copy.details.join(' | '), /Streetcar · TTC/);
assert.doesNotMatch(copy.details.join(' | '), /Tram/);
});
test('detection labels carry the operator vocabulary within the field width', () => {
const klass = transitDetectionClass('tram', ttc());
assert.equal(klass, 'STREETCAR TTC');
assert.ok(klass.length <= 20, 'fits the detection class field');
assert.equal(transitDetectionClass('tram', hsl()), 'TRAM HSL');
const metric = transitDetectionMetric({ mode: 'tram' }, 'tram', 0, ttc());
assert.match(metric, /^STREETCAR/);
});
test('a Guelph vehicle is labelled with the number on the bus, not the database key', () => {
const guelph = getTransitFeed('guelph-transit');
const record = { id: '196', routeId: '2991', timestamp: 0 };
const copy = buildTransitSelectionCopy(guelph, record, 'bus', 0, null, null);
assert.match(copy.title, /Route 1\b/);
assert.doesNotMatch(copy.title, /2991/);
assert.equal(transitDetectionId(record, guelph), '1');
});
test('a feed that already speaks rider route numbers is untouched', () => {
const record = { id: '4400', routeId: '501', timestamp: 0 };
assert.equal(transitDetectionId(record, ttc()), '501');
const copy = buildTransitSelectionCopy(ttc(), record, 'tram', 0, null, null);
assert.match(copy.title, /Route 501/);
// No feed at all still renders the raw route rather than throwing.
assert.equal(transitDetectionId(record), '501');
});
test('a vehicle with no route still falls back to its fleet number', () => {
const guelph = getTransitFeed('guelph-transit');
assert.equal(transitDetectionId({ id: '196', label: '196' }, guelph), '196');
});