mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge branch 'main' into fix/geo-distance-dateline
# Conflicts: # CHANGELOG.md
This commit is contained in:
commit
c37e301ab7
440 files changed
+33136
-4670
No files matched your search
@@ -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(', ')}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
Vendored
+36
-7
@@ -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:
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user