mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
Replace the semantic keys with the English source text, so code reads as the
UI does and English needs no catalog: t('Save keys'), tn(count, one, other)
for plurals, msg() for text stored in data, and bare data-i18n* markup that
translates the element's own English. Other languages are one flat JSON each.
- Simplify src/i18n: drop namespaces, the configurable locale pair
(GEV_DEFAULT_LOCALE / GEV_SECONDARY_LOCALE) and its build defines. Locale
comes from ?lang=, the stored choice, then browser languages.
- Add scripts/i18n.mjs (npm run i18n): extracts every message from t/tn/msg
and markup, and fails on missing, unused, placeholder-mismatched or
wrongly-shaped plural entries. npm test runs it via catalog.test.mjs.
- Add Russian (src/i18n/locales/ru.json, 833 messages), started from the
community catalog in #312 and reviewed against the current English.
- Fix data-i18n placed on parents that would wipe their children (cockpit
LEVEL and local weather cells, first-run EXPLORE MANUALLY tile), and a
doubled degree sign in the TRACK ACQUIRED signal.
- Rewrite docs/TRANSLATORS.md for the new workflow; drop sections the earlier
merge resurrected in docs/CURRENT-STATE.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
259 lines
9.2 KiB
JavaScript
259 lines
9.2 KiB
JavaScript
/** Tracking identity, entry, exit and contact navigation for the Cockpit controller. */
|
|
import { t } from '../i18n/index.js';
|
|
import {
|
|
normalizeHeading,
|
|
resolveTrackedAircraftInfo,
|
|
} from '../cockpitMath.js';
|
|
|
|
export function readAircraftInfo() {
|
|
// In cockpit mode the controller takes the entity off `viewer.trackedEntity`
|
|
// (see update()), so the cockpit's own handle is the tracked identity there.
|
|
const trackedEntity = this.viewer?.trackedEntity || this.trackedEntity;
|
|
return resolveTrackedAircraftInfo({
|
|
civilian: this.services.flightsLayer.getTrackedInfo?.() || null,
|
|
military: this.services.militaryFlightsLayer.getTrackedInfo?.() || null,
|
|
trackedId: trackedEntity?.gevTrackedId || '',
|
|
});
|
|
}
|
|
|
|
export function dispatchCockpitModeChanged(active, info = null) {
|
|
const subjectId = active
|
|
? String(info?.icao24 || '')
|
|
.trim()
|
|
.toLowerCase() || null
|
|
: null;
|
|
const layerId =
|
|
active && ['flights', 'military'].includes(info?.layerId)
|
|
? info.layerId
|
|
: null;
|
|
window.dispatchEvent(
|
|
new CustomEvent('gev:cockpit-mode-changed', {
|
|
detail: { active: active === true, subjectId, layerId },
|
|
}),
|
|
);
|
|
}
|
|
|
|
export function toggleTrackedTr3b() {
|
|
if (this.destroyed) return false;
|
|
const info = this.readAircraftInfo();
|
|
const icao24 = String(info?.icao24 || '').trim();
|
|
if (!icao24) return false;
|
|
this.services.toggleTr3b(icao24);
|
|
const layer =
|
|
info.layerId === 'military'
|
|
? this.services.militaryFlightsLayer
|
|
: this.services.flightsLayer;
|
|
layer.refreshTr3b?.(icao24);
|
|
this._tr3bSignature = null; // force the chip to repaint on the next sync
|
|
this.syncTr3bToggle(info);
|
|
return true;
|
|
}
|
|
|
|
export function syncTr3bToggle(info) {
|
|
if (!this.tr3bToggle) return;
|
|
const icao24 = String(info?.icao24 || '').trim();
|
|
const converted = !!icao24 && this.services.isTr3b(icao24);
|
|
const signature = icao24 ? `${icao24}:${converted ? 1 : 0}` : '';
|
|
if (this._tr3bSignature === signature) return;
|
|
this._tr3bSignature = signature;
|
|
this.tr3bToggle.hidden = !icao24;
|
|
this.tr3bToggle.setAttribute('aria-pressed', converted ? 'true' : 'false');
|
|
this.tr3bToggle.title = converted
|
|
? t('Restore real aircraft')
|
|
: t('Reclassify as TR-3B');
|
|
}
|
|
|
|
export function syncEntry() {
|
|
if (this.destroyed) return false;
|
|
if (this.active) return;
|
|
const info = this.readAircraftInfo();
|
|
const trackedContact = !!(info && this.viewer.trackedEntity?.position);
|
|
this.syncTr3bToggle(trackedContact ? info : null);
|
|
const available = !!(this.isEntryAllowed() && trackedContact);
|
|
// Change-only DOM writes: this runs on a preUpdate cadence, and
|
|
// unconditional `hidden` assignments invalidate style/layout every frame
|
|
// even when nothing changed. (perf item 9)
|
|
if (this._entryAvailable === available) return;
|
|
this._entryAvailable = available;
|
|
if (this.entry) this.entry.hidden = !available;
|
|
if (this.mapViewButton) this.mapViewButton.hidden = true;
|
|
if (this.resetGlobeButton) this.resetGlobeButton.hidden = true;
|
|
}
|
|
|
|
export function navigateContext(direction, options = {}) {
|
|
if (this.destroyed) return false;
|
|
const method = direction < 0 ? 'navigatePrevious' : 'navigateNext';
|
|
const wasActive = this.active;
|
|
if (wasActive) this.contextNavigationDeadlineMs = performance.now() + 1500;
|
|
const navigationOptions = wasActive
|
|
? { ...options, aircraftOnly: true }
|
|
: options;
|
|
const changed = Boolean(
|
|
this.services.militaryAwarenessLayer?.[method]?.(navigationOptions),
|
|
);
|
|
if (!changed) {
|
|
this.contextNavigationDeadlineMs = 0;
|
|
return false;
|
|
}
|
|
if (wasActive) this._adoptTrackedEntity(performance.now());
|
|
return true;
|
|
}
|
|
|
|
export function _adoptTrackedEntity(nowMs, suppliedInfo = null) {
|
|
const nextEntity = this.viewer.trackedEntity;
|
|
if (
|
|
!this.active ||
|
|
!nextEntity?.position ||
|
|
nextEntity === this.trackedEntity
|
|
)
|
|
return false;
|
|
const info = suppliedInfo || this.readAircraftInfo();
|
|
if (!info) return false;
|
|
if (this.trackedEntity && this.viewer.entities.contains(this.trackedEntity)) {
|
|
this.trackedEntity.show = this.trackedEntityWasShown;
|
|
}
|
|
this.trackedEntity = nextEntity;
|
|
this.trackedEntityWasShown = nextEntity.show;
|
|
nextEntity.show = false;
|
|
this.viewer.trackedEntity = undefined;
|
|
this.cockpitAnchorValid = false;
|
|
this.heading = normalizeHeading(info.track ?? 0);
|
|
this.lastFrameMs = nowMs;
|
|
this.lastHudUpdateMs = 0;
|
|
this.lastContextUpdateMs = 0;
|
|
this.lastCameraUpdateMs = 0;
|
|
this.contextNavigationDeadlineMs = 0;
|
|
this.dispatchCockpitModeChanged(true, info);
|
|
return true;
|
|
}
|
|
|
|
export function enter() {
|
|
if (this.destroyed) return false;
|
|
if (this.active) return false;
|
|
if (!this.isEntryAllowed()) return false;
|
|
const info = this.readAircraftInfo();
|
|
const entity = this.viewer.trackedEntity;
|
|
if (!info || !entity?.position) return false;
|
|
this.entryFocusOrigin =
|
|
document.activeElement instanceof HTMLElement
|
|
? document.activeElement
|
|
: null;
|
|
// Retire deferred navigation before cancelFlight can run its callbacks.
|
|
this.onCameraTakeover?.();
|
|
this.viewer.camera.cancelFlight();
|
|
this.trackedEntity = entity;
|
|
this.trackedEntityWasShown = entity.show;
|
|
entity.show = false;
|
|
this.heading = normalizeHeading(info.track ?? 0);
|
|
this.lastFrameMs = performance.now();
|
|
this.lastCameraUpdateMs = 0;
|
|
this.lastHudUpdateMs = 0;
|
|
this.lastContextUpdateMs = 0;
|
|
this.contextNavigationDeadlineMs = 0;
|
|
this.lastGroundProbeMs = 0;
|
|
this.surfaceWaitStartedMs = performance.now();
|
|
this.surfaceAcquiring = false;
|
|
this.surfaceFallback = false;
|
|
this.lastCompassSignature = '';
|
|
this.cockpitAnchorValid = false;
|
|
this.lastCameraUpdateMs = 0;
|
|
this.active = true;
|
|
// Cockpit animates the camera from preUpdate every frame — preUpdate only
|
|
// runs on rendered frames, so idle mode would freeze the cockpit solid.
|
|
// (perf wave 2)
|
|
this.services.holdContinuousRender('cockpit');
|
|
this.viewer.trackedEntity = undefined;
|
|
this.viewer.scene.screenSpaceCameraController.enableInputs = false;
|
|
document.body.classList.add('cockpit-mode');
|
|
// Activation writes entry/quick/map visibility directly, bypassing
|
|
// syncEntry's change-only cache — invalidate it so the exit-path
|
|
// syncEntry re-applies every write (notably re-hiding mapViewButton).
|
|
this._entryAvailable = undefined;
|
|
if (this.entry) this.entry.hidden = true;
|
|
if (this.tr3bToggle) {
|
|
this.tr3bToggle.hidden = true;
|
|
this._tr3bSignature = null;
|
|
}
|
|
if (this.mapViewButton) this.mapViewButton.hidden = false;
|
|
if (this.resetGlobeButton) this.resetGlobeButton.hidden = false;
|
|
if (this.hud) this.hud.hidden = false;
|
|
if (this.signalStream) this.signalStream.hidden = false;
|
|
this.hud?.classList.add('signals-active');
|
|
this.signalItems = [];
|
|
this.signalSignatures.clear();
|
|
this.showBriefPage(0);
|
|
this.startBriefRotation();
|
|
const trackLabel =
|
|
info.callsign || info.registration || info.icao24 || t('AIRCRAFT');
|
|
const trackHeading = String(
|
|
Math.round(normalizeHeading(info.track ?? 0)),
|
|
).padStart(3, '0');
|
|
this.pushCockpitSignal(
|
|
'track',
|
|
'track',
|
|
t('TRACK ACQUIRED'),
|
|
t('{label} · COURSE {heading}°', {
|
|
label: trackLabel,
|
|
heading: trackHeading,
|
|
}),
|
|
);
|
|
this.updateHud(info, performance.now(), true);
|
|
this.setVisionMode(this.getInitialVisionMode());
|
|
this.scheduleContextLayout();
|
|
this.mapViewButton?.focus({ preventScroll: true });
|
|
this.onEntered?.();
|
|
this.dispatchCockpitModeChanged(true, info);
|
|
return true;
|
|
}
|
|
|
|
export function exit({ restoreTracking = true } = {}) {
|
|
if (!this.active) return false;
|
|
const entity = this.trackedEntity;
|
|
this.active = false;
|
|
this.services.releaseContinuousRender('cockpit');
|
|
this.trackedEntity = null;
|
|
this.heading = null;
|
|
this.cockpitAnchorValid = false;
|
|
this.surfaceWaitStartedMs = 0;
|
|
this.surfaceAcquiring = false;
|
|
this.surfaceFallback = false;
|
|
this.lastHudUpdateMs = 0;
|
|
this.lastContextUpdateMs = 0;
|
|
this.contextNavigationDeadlineMs = 0;
|
|
this.lastCompassSignature = '';
|
|
this.stopBriefRotation();
|
|
this.regionalBriefAbort?.abort();
|
|
this.regionalBriefAbort = null;
|
|
this.regionalBriefRequestToken += 1;
|
|
this.regionalBriefSubjectId = null;
|
|
document.body.classList.remove('cockpit-mode');
|
|
this.onExited?.();
|
|
this.hud?.style.removeProperty('--cockpit-utility-top');
|
|
this.hud?.style.removeProperty('--cockpit-utility-max-height');
|
|
if (this.hud) this.hud.hidden = true;
|
|
if (this.route) this.route.hidden = true;
|
|
this.clearPredictiveRoute();
|
|
this.setVisionMode('optical');
|
|
if (this.signalStream) this.signalStream.hidden = true;
|
|
this.hud?.classList.remove('signals-active');
|
|
this.viewer.scene.screenSpaceCameraController.enableInputs = true;
|
|
if (entity && this.viewer.entities.contains(entity))
|
|
entity.show = this.trackedEntityWasShown;
|
|
this.trackedEntityWasShown = true;
|
|
this.dispatchCockpitModeChanged(false);
|
|
if (restoreTracking && entity && this.viewer.entities.contains(entity)) {
|
|
this.viewer.trackedEntity = entity;
|
|
this.restoreTrackingFrame(entity);
|
|
}
|
|
this.syncEntry();
|
|
const restoreTarget =
|
|
this.entryFocusOrigin === this.entry
|
|
? this.entry
|
|
: this.entry || this.entryFocusOrigin;
|
|
this.entryFocusOrigin = null;
|
|
if (!this.destroyed && restoreTarget?.isConnected && !restoreTarget.hidden) {
|
|
restoreTarget.focus({ preventScroll: true });
|
|
}
|
|
return true;
|
|
}
|