From c502404e1d8d5d1089a8e15282dd0f224e0a8d78 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Thu, 5 Nov 2026 12:29:45 +0400 Subject: [PATCH] Show installation and ALPR camera labels on Navara the way Cesium does An installation card on Navara now names an unnamed feature and its class in the UI language, and an ALPR camera keeps its context label as English data that is translated when shown. --- src/layers/alpr/render/navara.js | 4 +- src/layers/alpr/render/navara.test.mjs | 12 +++ src/layers/installations/render/navara.js | 7 +- .../installations/render/navara.test.mjs | 81 +++++++++++++++++++ 4 files changed, 98 insertions(+), 6 deletions(-) diff --git a/src/layers/alpr/render/navara.js b/src/layers/alpr/render/navara.js index fcb5e8fd2..534413527 100644 --- a/src/layers/alpr/render/navara.js +++ b/src/layers/alpr/render/navara.js @@ -1,4 +1,4 @@ -import { t } from '../../../i18n/index.js'; +import { msg } from '../../../i18n/index.js'; import * as geo from '../../../geo/index.js'; import { distanceScale } from '../../../globe/cpuPicks.js'; import { selectionDispatcherFor } from '../../../data/selectionDispatcher.js'; @@ -341,7 +341,7 @@ export function createAlprNavaraRenderer(engine, { state, services, source }) { layerName: 'ALPR Cameras', source: source.attribution?.description || source.label || 'Camera source', - label: t('ALPR camera'), + label: msg('ALPR camera'), latitude: record.latitude, longitude: record.longitude, properties: { diff --git a/src/layers/alpr/render/navara.test.mjs b/src/layers/alpr/render/navara.test.mjs index a0cdb8249..d4234829d 100644 --- a/src/layers/alpr/render/navara.test.mjs +++ b/src/layers/alpr/render/navara.test.mjs @@ -1,6 +1,8 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import * as geo from '../../../geo/index.js'; +import { setLocale } from '../../../i18n/index.js'; +import { shownEntityLabel } from '../../../voice/speech.js'; import { createAlprNavaraRenderer } from './navara.js'; function event() { @@ -311,6 +313,16 @@ test('ALPR preserves selected metadata and releases an evicted or replaced selec assert.equal(h.contexts.has('alpr:1'), false); }); +test('the camera label stays English data in a translated interface, as on Cesium', (t) => { + const h = harness(t); + setLocale('ru'); + t.after(() => setLocale('en')); + h.load(records(1)); + const { label } = h.contexts.get('alpr:0'); + assert.equal(label, 'ALPR camera'); + assert.equal(shownEntityLabel(label), 'Камера ALPR'); +}); + test('ten ALPR toggles clear contexts, picks and credits without allocating new sets', (t) => { const h = harness(t); for (let i = 0; i < 10; i++) { diff --git a/src/layers/installations/render/navara.js b/src/layers/installations/render/navara.js index 9ccc6cebb..a6a82aa02 100644 --- a/src/layers/installations/render/navara.js +++ b/src/layers/installations/render/navara.js @@ -7,6 +7,7 @@ import { MILITARY_LABEL_CAP, MILITARY_POINT_CAP, } from '../../../data/militaryNames.js'; +import { installationClassLabel, installationTitle } from '../classLabel.js'; import { COLOR_BY_CLASS, LAYER_ID, MAX_RENDERED } from '../policy.js'; import { createInstallationDrapedMesh, @@ -278,11 +279,9 @@ export function createInstallationsNavaraRenderer( } context.gevContextId = record.id; context.gevLabelModel = { - title: record.name || t('MAPPED INSTALLATION'), + title: installationTitle(record.name) || t('MAPPED INSTALLATION'), details: [ - String(record.class || 'installation') - .replaceAll('_', ' ') - .toUpperCase(), + installationClassLabel(record.class), ...(record.memberNames || []).slice(0, 3), ], accent: COLOR_BY_CLASS[record.class] || '#9ca6b0', diff --git a/src/layers/installations/render/navara.test.mjs b/src/layers/installations/render/navara.test.mjs index cb962ceb6..9c92a1855 100644 --- a/src/layers/installations/render/navara.test.mjs +++ b/src/layers/installations/render/navara.test.mjs @@ -1,6 +1,8 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { Mesh } from 'three'; +import { setLocale } from '../../../i18n/index.js'; +import { installationClassLabel, installationTitle } from '../classLabel.js'; import { createInstallationsNavaraRenderer } from './navara.js'; test('selected installation fill is reused and removed with its scene resources', () => { @@ -157,3 +159,82 @@ test('selected installation fill is reused and removed with its scene resources' assert.equal(deletedEffects, 1, 'destroy deletes the public effect handle'); assert.equal(effectConfig.gevInstallationsDrape.owner.meshes.size, 0); }); + +test('the card label reads in the UI language, as on Cesium', () => { + const record = { + id: 'osm:node:4', + longitude: 10, + latitude: 50, + class: 'naval_base', + name: 'Firing range', + }; + const state = { + enabled: true, + records: [record], + recordById: new Map([[record.id, record]]), + selectedId: null, + }; + const contexts = new Map(); + const set = { setCount() {}, set() {}, commit() {}, destroy() {} }; + const renderer = createInstallationsNavaraRenderer( + { + movers: { createSet: () => set }, + camera: { height: () => 1_000 }, + render: { request() {} }, + }, + { + state, + services: { + context: { + getSelectedEntityContext: () => null, + registerEntityContext: (entity, value) => + contexts.set(value.id, entity), + selectEntityContext() {}, + removeEntityContextsForLayer() {}, + clearSelectedEntityContextForLayer() {}, + }, + picking: { registerPickOwner() {}, unregisterPickOwner() {} }, + }, + parts: { + model: { + colorFor: () => ({ + red: 0.6, + green: 0.6, + blue: 0.6, + alpha: 1, + toCssColorString: () => '#9ca6b0', + }), + installationSurfaceHeightM: () => 0, + installationSourceLabel: () => 'OpenStreetMap', + }, + namedMarkers: { sync() {}, enable() {}, hide() {}, destroy() {} }, + viewport: { clearUnavailableRetry() {} }, + ingestion: { setInstallationStatus() {} }, + }, + source: {}, + overlayHost: { setVisible() {}, setEntries() {}, clearSource() {} }, + view: { + canvas: { clientHeight: 800 }, + camera: { raw: { fov: 60 } }, + registerEffect() {}, + addEffect: () => ({ update() {}, delete() {} }), + }, + EffectDesc: class {}, + NavaraEffect: class {}, + DrapedMesh: Mesh, + setupMaterialForDrape() {}, + }, + ); + try { + setLocale('ru'); + renderer.lifecycle.methods.enable(); + const label = contexts.get(record.id).gevLabelModel; + assert.equal(label.title, installationTitle('Firing range')); + assert.notEqual(label.title, 'Firing range'); + assert.equal(label.details[0], installationClassLabel('naval_base')); + assert.notEqual(label.details[0], 'NAVAL BASE'); + } finally { + setLocale('en'); + renderer.lifecycle.methods.destroy(); + } +});