mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
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:
commit
48e90c3780
32 files changed
+2390
-36
No files matched your search
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
Reference in new issue
Block a user