Merge branch 'main' into fix/geo-distance-dateline

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
Sameh Khamis committed 2026-10-16 14:59:29 -07:00
commit c37e301ab7
440 files changed
+33136 -4670

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
import { t } from '../../i18n/index.js';
import { UNNAMED_PLACE_NAMES } from '../../data/installationNames.js';
const UNNAMED_NAMES = new Set(UNNAMED_PLACE_NAMES);
/**
* An installation's card title: a mapped name as it is; the English label
* the data gives an unnamed feature ("Firing range", "Military area") in the
* UI language. The record keeps the English name.
* @param {string} name
* @returns {string} Empty when the record has no name.
*/
export function installationTitle(name) {
const text = String(name ?? '');
return UNNAMED_NAMES.has(text.trim()) ? t(text.trim()) : text;
}
/**
* An installation class as a map card shows it: AIRFIELD, NAVAL BASE…, in
* the UI language. The class itself stays an English identifier in data.
* @param {string} klass
* @returns {string}
*/
export function installationClassLabel(klass) {
const key = String(klass || 'installation');
switch (key) {
case 'airfield':
return t('AIRFIELD', { context: 'installation class' });
case 'naval_base':
return t('NAVAL BASE', { context: 'installation class' });
case 'range':
return t('RANGE', { context: 'installation class' });
case 'military_land':
return t('MILITARY LAND', { context: 'installation class' });
case 'base':
return t('BASE', { context: 'installation class' });
case 'barracks':
return t('BARRACKS', { context: 'installation class' });
case 'training_area':
return t('TRAINING AREA', { context: 'installation class' });
case 'places_candidate':
return t('PLACES CANDIDATE', { context: 'installation class' });
case 'installation':
return t('INSTALLATION', { context: 'installation class' });
default:
return key.replaceAll('_', ' ').toUpperCase();
}
}
@@ -0,0 +1,36 @@
// Every class the bundled OSM names pack can put on a record has a
// catalog entry, so the selected card shows it in the UI language.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { setLocale, getCatalog } from '../../i18n/index.js';
import { installationClassLabel } from './classLabel.js';
const pack = JSON.parse(
readFileSync(
new URL(
'../../data/local_data/osm_military_names/names.json',
import.meta.url,
),
'utf8',
),
);
setLocale('en');
const englishLabel = Object.fromEntries(
pack.classes.map((c) => [c, installationClassLabel(c)]),
);
for (const locale of ['es', 'ru']) {
test(`every names-pack class is in the ${locale} catalog`, () => {
const catalog = getCatalog(locale);
const missing = pack.classes.filter(
(c) => catalog[`installation class|${englishLabel[c]}`] === undefined,
);
assert.deepEqual(
missing,
[],
`no ${locale} entry for: ${missing.join(', ')}`,
);
});
}
+36 -7
View File
@@ -1,10 +1,37 @@
import * as Cesium from 'cesium';
import { installationFeedback } from '../../data/installationFeedback.js';
import { describeInstallationFeedback } from '../../data/installationFeedback.js';
import {
LAYER_ID,
DISTANCE_PREFILTER_MARGIN_M,
ANCHOR_REFRESH_M,
} from './policy.js';
import { formatNumber, t, tn } from '../../i18n/index.js';
// installationFeedback.js is shared with the HUD (src/loadingFeedback.js) and
// hands back msg()-marked English plus values; getStats translates them here.
function localizeInstallationFeedback(feedback) {
if (feedback.text !== undefined) {
const values = {};
if (feedback.reason !== undefined) values.reason = t(feedback.reason);
if (feedback.seconds !== undefined) values.seconds = feedback.seconds;
return t(feedback.text, values);
}
const { count, km } = feedback;
if (km) {
const kmText = formatNumber(km);
return count === 0
? t('No mapped sites within {km} km of the contact', { km: kmText })
: tn(
count,
'{count} mapped site within {km} km of the contact',
'{count} mapped sites within {km} km of the contact',
{ km: kmText },
);
}
return count === 0
? t('No mapped sites in view')
: tn(count, '{count} mapped site in view', '{count} mapped sites in view');
}
export function createControls({ state: layerState, services, parts, source }) {
const methods = {
@@ -225,13 +252,15 @@ export function createControls({ state: layerState, services, parts, source }) {
retryAt: layerState.retryAt,
retrying: layerState.loading && Boolean(layerState.failureReason),
failureReason: layerState.failureReason,
statusMessage: installationFeedback({
...layerState,
count: layerState.lastUpdate ? mappedCount : undefined,
retrying: layerState.loading && Boolean(layerState.failureReason),
}),
statusMessage: localizeInstallationFeedback(
describeInstallationFeedback({
...layerState,
count: layerState.lastUpdate ? mappedCount : undefined,
retrying: layerState.loading && Boolean(layerState.failureReason),
}),
),
loadingLabel: layerState.loading
? 'loading mapped installation context'
? t('loading mapped installation context')
: '',
coverage: layerState.coverage,
coverageLabel:
+3 -2
View File
@@ -1,5 +1,6 @@
import { militaryOsmKey } from '../../data/militaryNames.js';
import { isUnavailableCapability } from '../../sources/capability.js';
import { msg } from '../../i18n/index.js';
/** Keep named point coverage until its matching polygon takes over. */
export function retainNamedHandoff(previous, records, box) {
const named = new Map(
@@ -256,7 +257,7 @@ export function createIngestion({
payload.status === 'stale'
? `Serving cached mapped context · ${payload.records[0]?.retrievedAt || 'date unknown'}`
: saturated
? 'Too many mapped sites in view to list them all'
? msg('Too many mapped sites in view to list them all')
: placesError,
);
parts.rendering.renderRecords();
@@ -292,7 +293,7 @@ export function createIngestion({
layerState.failureReason = error?.failureReason || 'unavailable';
setInstallationStatus(
'unavailable',
error?.message || 'Installation context unavailable',
error?.message || msg('Installation context unavailable'),
);
if (!isUnavailableCapability(error))
parts.viewport.scheduleUnavailableRetry(error?.retryAfterMs);
@@ -0,0 +1,79 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { setLocale, translateKnown } from '../../i18n/index.js';
import { createControls } from './controls.js';
function stats(state) {
const layerState = { records: [], enabled: true, ...state };
return createControls({
state: layerState,
services: {},
parts: {},
source: {},
}).methods.getStats();
}
for (const locale of ['es', 'ru']) {
test(`${locale}: installation feedback is in the UI language`, () => {
setLocale(locale);
const empty = stats({ status: 'empty', lastUpdate: 1 }).statusMessage;
assert.notEqual(empty, 'No mapped sites in view');
const found = stats({
status: 'ready',
lastUpdate: 1,
records: [{ kind: 'installation' }, { kind: 'installation' }],
}).statusMessage;
assert.notEqual(found, '2 mapped sites in view');
const tiles = stats({
status: 'unavailable',
failureReason: 'tiles_unavailable',
}).statusMessage;
assert.notEqual(tiles, 'Map tiles temporarily unavailable');
const retry = stats({
status: 'unavailable',
failureReason: 'names_unavailable',
retryAt: Date.now() + 5000,
}).statusMessage;
assert.doesNotMatch(retry, /^Mapped names temporarily unavailable/);
for (const text of [
'Too many mapped sites in view to list them all',
'Installation context unavailable',
]) {
assert.notEqual(translateKnown(text), text);
}
});
}
test('en: installation feedback keeps its English wording', () => {
setLocale('en');
assert.equal(
stats({ status: 'empty', lastUpdate: 1 }).statusMessage,
'No mapped sites in view',
);
assert.equal(
stats({
status: 'ready',
lastUpdate: 1,
records: [{ kind: 'installation' }],
}).statusMessage,
'1 mapped site in view',
);
assert.equal(
stats({
status: 'ready',
lastUpdate: 1,
wide: true,
coverage: { kind: 'subject', radiusM: 25000 },
records: [{ kind: 'installation' }, { kind: 'installation' }],
}).statusMessage,
'2 mapped sites within 25 km of the contact',
);
assert.equal(
stats({
status: 'unavailable',
failureReason: 'names_unavailable',
retryAt: Date.now() + 5000,
}).statusMessage,
'Mapped names temporarily unavailable — retrying in 5s',
);
});
+4 -5
View File
@@ -1,3 +1,4 @@
import { installationClassLabel, installationTitle } from './classLabel.js';
import * as Cesium from 'cesium';
import {
MILITARY_LABEL_CAP,
@@ -162,7 +163,7 @@ export function createNamedMarkers({ state, services, parts, overlayHost }) {
id: record.id,
source: LAYER_ID,
position,
title: record.name,
title: installationTitle(record.name),
priority: record.areaM2 || 0,
accent: COLOR_BY_CLASS[record.class] || '#9ca6b0',
}),
@@ -177,14 +178,12 @@ export function createNamedMarkers({ state, services, parts, overlayHost }) {
marker.entry.selected = record.id === state.selectedId;
marker.entry.details = marker.entry.selected
? [
String(record.class || 'installation')
.replaceAll('_', ' ')
.toUpperCase(),
installationClassLabel(record.class),
...(record.memberNames || []).slice(0, 3),
]
: [];
marker.entry.position = position;
marker.entry.title = record.name;
marker.entry.title = installationTitle(record.name);
marker.entry.priority = record.areaM2 || 0;
entries.push(marker.entry);
ordered.push(marker);
+4 -4
View File
@@ -1,4 +1,6 @@
import { installationClassLabel, installationTitle } from './classLabel.js';
import * as Cesium from 'cesium';
import { t } from '../../i18n/index.js';
import { LAYER_ID, MAX_RENDERED, COLOR_BY_CLASS } from './policy.js';
export function createRendering({
@@ -270,11 +272,9 @@ export function createRendering({
entity.gevTrackedId = `installations:${record.id}`;
entity.gevDisplayPosition = () => displayPosition;
entity.gevLabelModel = {
title: record.name || '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',
@@ -0,0 +1,85 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { normalizeMilitaryInstallations } from '../../data/militaryInstallationData.js';
import { UNNAMED_MILITARY_AREA } from '../../sources/openFreeMap.js';
import { setLocale } from '../../i18n/index.js';
import { installationTitle } from './classLabel.js';
import { formatAwarenessLabel } from '../../data/militaryAwarenessEngine.js';
// Unnamed installations get an English label in data ("Firing range",
// "Military land", "Military airfield"; OpenFreeMap polygons: "Military area").
// The data stays English; the map card title reads in the UI language.
const { records } = normalizeMilitaryInstallations({
elements: [
{ type: 'node', id: 1, lat: 1, lon: 1, tags: { military: 'range' } },
{
type: 'way',
id: 2,
center: { lat: 2, lon: 2 },
tags: { landuse: 'military' },
},
{ type: 'node', id: 3, lat: 3, lon: 3, tags: { military: 'airfield' } },
{ type: 'node', id: 4, lat: 4, lon: 4, tags: { military: 'naval_base' } },
],
});
const names = [...records.map((r) => r.name), UNNAMED_MILITARY_AREA];
test('unnamed installations keep English names in data', () => {
assert.deepEqual(names, [
'Firing range',
'Military land',
'Military airfield',
'Naval base',
'Military area',
]);
});
test('unnamed installation card title reads in the UI language', () => {
try {
for (const locale of ['es', 'ru']) {
setLocale(locale);
for (const name of names) {
const shown = installationTitle(name);
assert.ok(shown, `${locale}: "${name}" has a title`);
assert.notEqual(
shown,
name,
`${locale}: "${name}" is shown in English`,
);
}
// A mapped name is shown as it is.
assert.equal(installationTitle('Fort Bragg'), 'Fort Bragg');
}
} finally {
setLocale('en');
}
});
test('English card titles are unchanged', () => {
setLocale('en');
for (const name of names) assert.equal(installationTitle(name), name);
assert.equal(installationTitle(''), '');
});
test('the Contacts line names an unnamed installation in the UI language', () => {
try {
setLocale('en');
for (const name of names)
assert.equal(formatAwarenessLabel({ id: 'osm-1', name }), name);
for (const locale of ['es', 'ru']) {
setLocale(locale);
for (const name of names)
assert.equal(
formatAwarenessLabel({ id: 'osm-1', name }),
installationTitle(name),
);
assert.equal(
formatAwarenessLabel({ id: 'osm-2', name: 'Fort Bragg' }),
'Fort Bragg',
);
assert.equal(formatAwarenessLabel({ callsign: 'RCH123 ' }), 'RCH123');
}
} finally {
setLocale('en');
}
});