mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
Fixes the 51 P0–P2 findings from the full review of the i18n branch and the issues found while checking every new string on screen in ru and es. - Translate errors and statuses that still reached ru/es in English: feed and tile errors, provider statuses, the ALPR chip, the traffic mode chip, voice plan step labels, fire feed values, receiver bands, scene save reasons and the Cesium attribution link. - Show preset city names in the UI language (src/ui/placeNames.js); data and everything sent to the model stay English. - Share layer names between the layer panel, HUD and voice card (src/ui/layerNames.js). - Keep long ru/es strings whole: voice card error lines, analyst titles, the voice tier status, the Nepal evidence button, the ALPR retry chip. English layout is unchanged. - Ask before a language switch drops unsaved work. - Catalog fixes: glossary terms (пуск, пресет), gender agreement and consistent "peticiones" in es, fewer calques in both languages. - Fix English text that was itself wrong (e.g. "Exit cockpit to fly to a weather", raw tool ids in voice plan steps). Catalogs: es/ru 2535 messages each, complete. Co-Authored-By: Claude Opus 5.5 <[email protected]>
185 lines
5.9 KiB
JavaScript
185 lines
5.9 KiB
JavaScript
/** Pure proximity helpers for Global Context. No capability modeling. */
|
|
|
|
import { t } from '../i18n/index.js';
|
|
import { UNNAMED_PLACE_NAMES } from './installationNames.js';
|
|
|
|
const UNNAMED_NAMES = new Set(UNNAMED_PLACE_NAMES);
|
|
|
|
export const AWARENESS_RADIUS_M = 250000;
|
|
export const AWARENESS_MAX_FLIGHT_SEARCH_RADIUS_M = 16000000;
|
|
export const AWARENESS_RELATIONSHIP = Object.freeze({
|
|
NEARBY: 'NEARBY',
|
|
OUTSIDE_RANGE: 'OUTSIDE_RANGE',
|
|
UNKNOWN: 'UNKNOWN',
|
|
});
|
|
|
|
/**
|
|
* Search an expanding area synchronously, doubling the radius after each miss.
|
|
* The maximum covers the farthest possible Earth-surface chord while keeping
|
|
* an empty feed bounded.
|
|
*
|
|
* @template T
|
|
* @param {(radiusM: number) => T|null|undefined} search Search callback.
|
|
* @param {{initialRadiusM?: number, maxRadiusM?: number}} [options] Bounds.
|
|
* @returns {{candidate: T, radiusM: number}|null} First match and radius used.
|
|
*/
|
|
export function findByDoublingRadius(
|
|
search,
|
|
{
|
|
initialRadiusM = AWARENESS_RADIUS_M,
|
|
maxRadiusM = AWARENESS_MAX_FLIGHT_SEARCH_RADIUS_M,
|
|
} = {},
|
|
) {
|
|
if (typeof search !== 'function') return null;
|
|
if (!Number.isFinite(initialRadiusM) || initialRadiusM <= 0) return null;
|
|
if (!Number.isFinite(maxRadiusM) || maxRadiusM < initialRadiusM) return null;
|
|
|
|
let radiusM = initialRadiusM;
|
|
while (radiusM <= maxRadiusM) {
|
|
const candidate = search(radiusM);
|
|
if (candidate) return { candidate, radiusM };
|
|
if (radiusM === maxRadiusM) break;
|
|
radiusM = Math.min(radiusM * 2, maxRadiusM);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** @param {number} meters @returns {string} */
|
|
export function formatAwarenessDistance(meters) {
|
|
if (!Number.isFinite(meters) || meters < 0) return '—';
|
|
return meters < 1000
|
|
? `${Math.round(meters)} m`
|
|
: `${(meters / 1000).toFixed(meters < 10000 ? 1 : 0)} km`;
|
|
}
|
|
|
|
/**
|
|
* Return a stable visible label without leaking missing values into UI text.
|
|
* The English name the data gives an unnamed installation ("Military area")
|
|
* is shown in the UI language; mapped names and callsigns show as they are.
|
|
*/
|
|
export function formatAwarenessLabel(value) {
|
|
if (value && typeof value === 'object') {
|
|
for (const candidate of [
|
|
value.callsign,
|
|
value.name,
|
|
value.label,
|
|
value.id,
|
|
value.icao24,
|
|
value.mmsi,
|
|
]) {
|
|
const label = formatAwarenessLabel(candidate);
|
|
if (label !== '—') return label;
|
|
}
|
|
return '—';
|
|
}
|
|
if (typeof value === 'string') {
|
|
const text = value.trim();
|
|
if (!text) return '—';
|
|
return UNNAMED_NAMES.has(text) ? t(text) : text;
|
|
}
|
|
if (Number.isFinite(value)) return String(value);
|
|
return '—';
|
|
}
|
|
|
|
/**
|
|
* Summarize sorted nearby results without promoting missing/stale feeds to a
|
|
* negative finding. `UNKNOWN` is an input-data state, never an out-of-range
|
|
* claim in this MVP.
|
|
*/
|
|
export function summarizeAwarenessCohort(
|
|
items,
|
|
{ available = true, stale = false, limit = 3 } = {},
|
|
) {
|
|
if (!available)
|
|
return {
|
|
relationship: AWARENESS_RELATIONSHIP.UNKNOWN,
|
|
count: null,
|
|
nearest: [],
|
|
reason: t('feed unavailable'),
|
|
};
|
|
if (stale)
|
|
return {
|
|
relationship: AWARENESS_RELATIONSHIP.UNKNOWN,
|
|
count: null,
|
|
nearest: [],
|
|
reason: t('feed stale'),
|
|
};
|
|
const normalized = (Array.isArray(items) ? items : [])
|
|
.filter(
|
|
(item) =>
|
|
Number.isFinite(item?.distanceM ?? item?.distance) &&
|
|
(item.distanceM ?? item.distance) >= 0,
|
|
)
|
|
.map((item) => ({ ...item, distanceM: item.distanceM ?? item.distance }))
|
|
.sort((a, b) => a.distanceM - b.distanceM);
|
|
return {
|
|
relationship: normalized.length
|
|
? AWARENESS_RELATIONSHIP.NEARBY
|
|
: AWARENESS_RELATIONSHIP.UNKNOWN,
|
|
count: normalized.length,
|
|
nearest: normalized.slice(0, limit),
|
|
reason: normalized.length
|
|
? t('observed or mapped nearby context')
|
|
: t('no observed or mapped objects in current feeds'),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Return selectable nearby targets in a stable, subject-first order.
|
|
* @param {Array<{id: string, summary?: {nearest?: Array<Object>}}>} cohorts Awareness cohorts.
|
|
* @param {{layerId?: string}|null} [subject] Current subject.
|
|
* @param {Iterable<string>} [visitedKeys] Previously visited layer/id keys.
|
|
* @returns {Array<{layerId: string, id: string, item: Object}>} Selectable targets.
|
|
*/
|
|
export function getAwarenessNavigationTargets(
|
|
cohorts,
|
|
subject = null,
|
|
visitedKeys = [],
|
|
) {
|
|
const visited = new Set(visitedKeys);
|
|
const targets = [];
|
|
for (const cohort of Array.isArray(cohorts) ? cohorts : []) {
|
|
const layerId = cohort?.id;
|
|
if (
|
|
![
|
|
'flights',
|
|
'military',
|
|
'ais-live-vessels',
|
|
'military-installations',
|
|
].includes(layerId)
|
|
)
|
|
continue;
|
|
const sourceItems = Array.isArray(cohort?.summary?.navigationNearest)
|
|
? cohort.summary.navigationNearest
|
|
: Array.isArray(cohort?.summary?.nearest)
|
|
? cohort.summary.nearest
|
|
: [];
|
|
for (const item of sourceItems) {
|
|
const id = item?.icao24 || item?.mmsi || item?.id;
|
|
if (!id) continue;
|
|
const key = `${layerId}:${id}`;
|
|
if (key === `${subject?.layerId}:${subject?.id}`) continue;
|
|
targets.push({
|
|
layerId,
|
|
id: String(id),
|
|
item,
|
|
visited: visited.has(key),
|
|
});
|
|
}
|
|
}
|
|
targets.sort((a, b) => {
|
|
// UNVISITED outranks layer affinity. Ordering subject-layer first put
|
|
// already-visited same-layer contacts ahead of unvisited contacts in every
|
|
// other layer, so NEXT ping-ponged inside the subject's own layer and never
|
|
// reached vessels, installations, or the other flight layer at all.
|
|
const visitedOrder = Number(a.visited) - Number(b.visited);
|
|
if (visitedOrder) return visitedOrder;
|
|
const subjectOrder =
|
|
Number(a.layerId !== subject?.layerId) -
|
|
Number(b.layerId !== subject?.layerId);
|
|
if (subjectOrder) return subjectOrder;
|
|
return (a.item.distanceM ?? Infinity) - (b.item.distanceM ?? Infinity);
|
|
});
|
|
return targets;
|
|
}
|