mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Merge branch 'main' into codex/shared-geodesy
This commit is contained in:
commit
64d7ed3c61
1373 files changed
+245910
-10664
No files matched your search
+158
-26
@@ -17,15 +17,28 @@ import { applicationServices } from './services/application.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { greatCircleKm } from './geoDistance.js';
|
||||
import { forward as toMGRS } from 'mgrs';
|
||||
import { CITY_POIS } from './locations.js';
|
||||
import { CITY_POIS } from './cityPois.js';
|
||||
import { composeLocalityTag } from './hudLocality.js';
|
||||
import { hudQuantity, LOCALIZED_LOCALITY } from './hudLocalityFormat.js';
|
||||
import {
|
||||
ellipsoidalToMslDisplayM,
|
||||
ensureGeoidReady,
|
||||
geoidHeight,
|
||||
} from './data/geoid.js';
|
||||
import { getBasemapLabelContext } from './voice/gevActions.js';
|
||||
import { isHudSummaryUnconfigured } from './hudSummaryResponse.js';
|
||||
import {
|
||||
hudSummaryMatchesProvenance,
|
||||
hudSummaryLayerContext,
|
||||
hudTelemetryProvenance,
|
||||
isHudSummaryUnconfigured,
|
||||
} from './hudSummaryResponse.js';
|
||||
import {
|
||||
applyHudUiTheme,
|
||||
DEFAULT_HUD_LAYOUT,
|
||||
normalizeHudLayout,
|
||||
} from './hudLayouts.js';
|
||||
import { formatNumber, msg, t } from './i18n/index.js';
|
||||
import { localizeLayerName } from './ui/layerPanel.js';
|
||||
|
||||
/** Color palettes keyed by shader mode; applied as CSS custom properties. */
|
||||
const HUD_COLORS = {
|
||||
@@ -51,11 +64,82 @@ const HUD_COLORS = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Mode names per style: the same English as STYLE_STATUS_LABELS
|
||||
* (ui/visualPresets.js), so both translate alike.
|
||||
*/
|
||||
const MODE_LABELS = Object.freeze({
|
||||
normal: msg('NORMAL'),
|
||||
retro: msg('CRT'),
|
||||
surveillance: msg('NVG'),
|
||||
thermal: msg('FLIR'),
|
||||
anime: msg('ANIME'),
|
||||
noir: msg('NOIR'),
|
||||
snow: msg('SNOW'),
|
||||
});
|
||||
|
||||
/** Region ids from _regionLabel() and the name shown for each. */
|
||||
const REGION_LABELS = Object.freeze({
|
||||
ARCTIC: msg('ARCTIC'),
|
||||
ANTARCTIC: msg('ANTARCTIC'),
|
||||
'NORTH AMERICA': msg('NORTH AMERICA'),
|
||||
'SOUTH AMERICA': msg('SOUTH AMERICA'),
|
||||
EUROPE: msg('EUROPE'),
|
||||
AFRICA: msg('AFRICA'),
|
||||
ASIA: msg('ASIA'),
|
||||
OCEANIA: msg('OCEANIA'),
|
||||
'NORTHERN OCEANIC GRID': msg('NORTHERN OCEANIC GRID'),
|
||||
'SOUTHERN OCEANIC GRID': msg('SOUTHERN OCEANIC GRID'),
|
||||
});
|
||||
|
||||
/** Feed-state ids from hudTelemetryProvenance() and the word shown for each. */
|
||||
const FEED_STATE_LABELS = Object.freeze({
|
||||
loading: msg('LOADING'),
|
||||
degraded: msg('DEGRADED'),
|
||||
stale: msg('STALE'),
|
||||
partial: msg('PARTIAL'),
|
||||
fallback: msg('FALLBACK'),
|
||||
unavailable: msg('UNAVAILABLE'),
|
||||
});
|
||||
|
||||
/**
|
||||
* The summary's feed-state suffix in the active language, e.g.
|
||||
* `UNAVAILABLE: LIVE AIS VESSELS`. State ids and layer names stay English in
|
||||
* layer data; only this line translates them.
|
||||
*/
|
||||
function localizedProvenanceTag(provenance) {
|
||||
if (!provenance) return null;
|
||||
const state = FEED_STATE_LABELS[provenance.state]
|
||||
? t(FEED_STATE_LABELS[provenance.state])
|
||||
: provenance.state.toUpperCase();
|
||||
if (!provenance.names.length) return state;
|
||||
return t('{state} {layers}', {
|
||||
state,
|
||||
layers: provenance.names
|
||||
.map((name) => localizeLayerName(name).toUpperCase())
|
||||
.join('/'),
|
||||
});
|
||||
}
|
||||
|
||||
/** The name shown for a _viewBand() id. */
|
||||
function viewBandLabel(band) {
|
||||
switch (band) {
|
||||
case 'STREET':
|
||||
return t('STREET', { context: 'view band' });
|
||||
case 'CITY':
|
||||
return t('CITY', { context: 'view band' });
|
||||
case 'METRO':
|
||||
return t('METRO', { context: 'view band' });
|
||||
case 'REGIONAL':
|
||||
return t('REGIONAL', { context: 'view band' });
|
||||
default:
|
||||
return t('GLOBAL', { context: 'view band' });
|
||||
}
|
||||
}
|
||||
|
||||
/** Shader modes that automatically show the HUD overlay. */
|
||||
const MILITARY_STYLES = new Set(['retro', 'surveillance', 'thermal']);
|
||||
|
||||
/** Allowed HUD layout variants. */
|
||||
const HUD_VARIANTS = new Set(['tactical', 'operator', 'minimal']);
|
||||
const HUD_SUMMARY_INTERVAL_MS = 15000;
|
||||
|
||||
/**
|
||||
@@ -106,7 +190,7 @@ export class IntelHUD {
|
||||
this._autoMode = true; // auto show/hide based on style
|
||||
this._currentStyle = 'normal';
|
||||
this._el = null;
|
||||
this._variant = 'tactical';
|
||||
this._variant = DEFAULT_HUD_LAYOUT;
|
||||
this._recBlinkState = true;
|
||||
this._updateInterval = null;
|
||||
this._recBlinkInterval = null;
|
||||
@@ -178,7 +262,10 @@ export class IntelHUD {
|
||||
this._el = document.getElementById('intel-hud');
|
||||
if (!this._el) return;
|
||||
|
||||
// i18n-ignore: sensor codes and a classification banner; the rest uses t()
|
||||
this._el.innerHTML = `
|
||||
<div class="hud-sonar" aria-hidden="true"></div>
|
||||
|
||||
<div class="hud-top-bar">
|
||||
<span class="hud-top-bar-left">TOP SECRET // SI-TK // NOFORN</span>
|
||||
<span class="hud-top-bar-center">${this._missionId}</span>
|
||||
@@ -190,17 +277,17 @@ export class IntelHUD {
|
||||
<div class="hud-content">
|
||||
<div class="hud-classification">TOP SECRET // SI-TK // NOFORN</div>
|
||||
<div class="hud-system">${this._missionId} ${this._sensorId}</div>
|
||||
<div class="hud-mode" id="hud-mode">NORMAL</div>
|
||||
<div class="hud-mode" id="hud-mode">${t(MODE_LABELS.normal)}</div>
|
||||
<div class="hud-summary-wrap">
|
||||
<div class="hud-summary-label">SUMMARY</div>
|
||||
<div class="hud-summary" id="hud-summary">Awaiting telemetry...</div>
|
||||
<div class="hud-summary-label">${t('SUMMARY')}</div>
|
||||
<div class="hud-summary" id="hud-summary">${t('Awaiting telemetry...')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hud-corner hud-top-right">
|
||||
<div class="hud-content" style="text-align:right">
|
||||
<div class="hud-rec"><span id="hud-rec-dot">●</span> REC <span id="hud-timestamp">2026-01-01 00:00:00Z</span></div>
|
||||
<div class="hud-rec"><span id="hud-rec-dot">●</span> ${t('REC')} <span id="hud-timestamp">2026-01-01 00:00:00Z</span></div>
|
||||
<div class="hud-orbital">ORB: ${this._orbitNum} PASS: DESC-${this._passNum}</div>
|
||||
</div>
|
||||
<div class="hud-bracket">┐</div>
|
||||
@@ -218,7 +305,7 @@ export class IntelHUD {
|
||||
<div class="hud-content" style="text-align:right">
|
||||
<div id="hud-gsd">GSD: --m NIIRS: --</div>
|
||||
<div id="hud-alt">ALT: --m SUN: --° EL</div>
|
||||
<div id="hud-ais-vessel" class="hud-ais-vessel">AIS: --</div>
|
||||
<div id="hud-ais-vessel" class="hud-ais-vessel">${t('AIS: --')}</div>
|
||||
</div>
|
||||
<div class="hud-bracket">┘</div>
|
||||
</div>
|
||||
@@ -346,9 +433,11 @@ export class IntelHUD {
|
||||
const formatted = this._formatMGRS(mgrsStr);
|
||||
mgrsLabel = formatted;
|
||||
const el = document.getElementById('hud-mgrs');
|
||||
// i18n-ignore: sensor codes
|
||||
if (el) el.textContent = `MGRS: ${formatted}`;
|
||||
} catch {
|
||||
const el = document.getElementById('hud-mgrs');
|
||||
// i18n-ignore: sensor codes
|
||||
if (el) el.textContent = 'MGRS: ---';
|
||||
}
|
||||
|
||||
@@ -357,6 +446,7 @@ export class IntelHUD {
|
||||
if (llEl) llEl.textContent = `${latDMS} ${lonDMS}`;
|
||||
const bottomEl = document.getElementById('hud-bottom-line');
|
||||
if (bottomEl) {
|
||||
// i18n-ignore: sensor codes
|
||||
bottomEl.textContent = `MGRS: ${mgrsLabel} LAT: ${latDMS} LON: ${lonDMS}`;
|
||||
}
|
||||
|
||||
@@ -372,6 +462,7 @@ export class IntelHUD {
|
||||
);
|
||||
const gsdEl = document.getElementById('hud-gsd');
|
||||
if (gsdEl)
|
||||
// i18n-ignore: sensor codes
|
||||
gsdEl.textContent = `GSD: ${gsd.toFixed(2)}m NIIRS: ${niirs.toFixed(1)}`;
|
||||
|
||||
// Altitude — reported as height above MEAN SEA LEVEL. `altM` is the raw
|
||||
@@ -384,6 +475,7 @@ export class IntelHUD {
|
||||
const altMslM = ellipsoidalToMslDisplayM(altM, geoidN);
|
||||
const sunEl = this._estimateSunElevation(latDeg, lonDeg);
|
||||
if (altEl)
|
||||
// i18n-ignore: sensor codes
|
||||
altEl.textContent = `ALT: ${Math.round(altMslM)}m SUN: ${sunEl.toFixed(1)}° EL`;
|
||||
|
||||
// Collection timestamp
|
||||
@@ -393,6 +485,7 @@ export class IntelHUD {
|
||||
const h = String(now.getUTCHours()).padStart(2, '0');
|
||||
const m = String(now.getUTCMinutes()).padStart(2, '0');
|
||||
const s = String(now.getUTCSeconds()).padStart(2, '0');
|
||||
// i18n-ignore: sensor codes
|
||||
collEl.textContent = `COLL: ${h}:${m}:${s}Z`;
|
||||
}
|
||||
|
||||
@@ -401,6 +494,7 @@ export class IntelHUD {
|
||||
const pitchDeg = Cesium.Math.toDegrees(camera.pitch);
|
||||
const ona = Math.max(0, 90 + pitchDeg);
|
||||
const onaEl = document.getElementById('hud-ona');
|
||||
// i18n-ignore: sensor codes
|
||||
if (onaEl) onaEl.textContent = `ONA: ${ona.toFixed(1)}°`;
|
||||
|
||||
// `altM` stays the raw ellipsoidal camera height the sensor model reads
|
||||
@@ -619,10 +713,10 @@ export class IntelHUD {
|
||||
*/
|
||||
_composeSummary() {
|
||||
const m = this._latestMetrics;
|
||||
if (!m) return 'Awaiting telemetry...';
|
||||
if (!m) return t('Awaiting telemetry...');
|
||||
|
||||
const modeEl = document.getElementById('hud-mode');
|
||||
const modeLabel = modeEl?.textContent || 'NORMAL';
|
||||
const modeLabel = modeEl?.textContent || t(MODE_LABELS.normal);
|
||||
const region = this._regionLabel(m.latDeg, m.lonDeg);
|
||||
const nearest = this._nearestKnownPoint(m.latDeg, m.lonDeg);
|
||||
const band = this._viewBand(m.altM);
|
||||
@@ -634,17 +728,42 @@ export class IntelHUD {
|
||||
// together, so they must never disagree. The view band above deliberately
|
||||
// keeps the ellipsoidal height: its thresholds were tuned against it.
|
||||
const altDisplayM = Number.isFinite(m.altMslM) ? m.altMslM : m.altM;
|
||||
// Display only: the AI summary request carries its own context, so this
|
||||
// line is built in the active language.
|
||||
const altTag =
|
||||
altDisplayM >= 1000
|
||||
? `${(altDisplayM / 1000).toFixed(1)}KM`
|
||||
: `${Math.round(altDisplayM)}M`;
|
||||
? hudQuantity(altDisplayM / 1000, 'km', 1)
|
||||
: hudQuantity(Math.round(altDisplayM), 'm');
|
||||
const winTag = window
|
||||
? `${Math.max(1, Math.round(window.widthKm))}x${Math.max(1, Math.round(window.heightKm))}KM`
|
||||
: 'N/A';
|
||||
? `${hudQuantity(Math.max(1, Math.round(window.widthKm)), '')}x${hudQuantity(Math.max(1, Math.round(window.heightKm)), 'km')}`
|
||||
: t('N/A');
|
||||
// NEAR the nearest catalogued POI at metro range; otherwise the lat/lon sector.
|
||||
const localityTag = composeLocalityTag(nearest, m.latDeg, m.lonDeg);
|
||||
const localityTag = composeLocalityTag(
|
||||
nearest,
|
||||
m.latDeg,
|
||||
m.lonDeg,
|
||||
LOCALIZED_LOCALITY,
|
||||
);
|
||||
|
||||
return `${modeLabel} ${band} ${localityTag} | ${region} | ALT ${altTag} | WINDOW ${winTag} | SUN ${m.sunEl.toFixed(0)}° | ONA ${m.ona.toFixed(0)}° | ${localTag}`;
|
||||
const provenance = localizedProvenanceTag(
|
||||
hudTelemetryProvenance(this._dataManager?.getAll?.() || []),
|
||||
);
|
||||
// ALT, SUN and ONA are sensor codes, as in the corner readouts.
|
||||
const line = t(
|
||||
'{mode} {band} {locality} | {region} | ALT {alt} | WINDOW {window} | SUN {sun}° | ONA {ona}° | {zone}',
|
||||
{
|
||||
mode: modeLabel,
|
||||
band: viewBandLabel(band),
|
||||
locality: localityTag,
|
||||
region: REGION_LABELS[region] ? t(REGION_LABELS[region]) : region,
|
||||
alt: altTag,
|
||||
window: winTag,
|
||||
sun: formatNumber(Math.round(m.sunEl)),
|
||||
ona: formatNumber(Math.round(m.ona)),
|
||||
zone: localTag,
|
||||
},
|
||||
);
|
||||
return provenance ? `${line} | ${provenance}` : line;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -729,7 +848,12 @@ export class IntelHUD {
|
||||
if (!response.ok || !data?.summary) {
|
||||
throw new Error(data?.error || `HTTP ${response.status}`);
|
||||
}
|
||||
this._setSummaryText(data.summary, animate);
|
||||
this._setSummaryText(
|
||||
hudSummaryMatchesProvenance(data.summary, context.feedProvenance)
|
||||
? data.summary
|
||||
: fallbackText,
|
||||
animate,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error?.name !== 'AbortError') {
|
||||
console.warn('[HUD] AI summary unavailable:', error);
|
||||
@@ -770,6 +894,7 @@ export class IntelHUD {
|
||||
streetLabels: labels.streetLabels,
|
||||
nearbyPlaceLabels: labels.nearbyPlaceLabels,
|
||||
enabledLayerLabels: enabledLayers,
|
||||
...hudSummaryLayerContext(this._dataManager?.getAll?.() || []),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -793,8 +918,9 @@ export class IntelHUD {
|
||||
// Update mode label
|
||||
const modeEl = document.getElementById('hud-mode');
|
||||
if (modeEl) {
|
||||
const modeNames = { surveillance: 'NVG', thermal: 'FLIR', retro: 'CRT' };
|
||||
modeEl.textContent = modeNames[styleName] || styleName.toUpperCase();
|
||||
modeEl.textContent = MODE_LABELS[styleName]
|
||||
? t(MODE_LABELS[styleName])
|
||||
: styleName.toUpperCase();
|
||||
}
|
||||
// Update color scheme
|
||||
const colors = HUD_COLORS[styleName] || HUD_COLORS._default;
|
||||
@@ -859,15 +985,16 @@ export class IntelHUD {
|
||||
|
||||
/**
|
||||
* Switch the HUD layout variant. Falls back to `'tactical'` if the
|
||||
* name is unrecognized.
|
||||
* @param {string} variantName - One of `'tactical'`, `'operator'`, `'minimal'`.
|
||||
* name is unrecognized. Cyber also applies a coordinated skin to the full
|
||||
* application shell; leaving Cyber restores the standard shell tokens.
|
||||
* @param {string} variantName - One of `'tactical'`, `'operator'`, `'minimal'`, `'cyber'`.
|
||||
*/
|
||||
setVariant(variantName) {
|
||||
const normalized = String(variantName || '').toLowerCase();
|
||||
this._variant = HUD_VARIANTS.has(normalized) ? normalized : 'tactical';
|
||||
this._variant = normalizeHudLayout(variantName);
|
||||
if (this._el) {
|
||||
this._el.dataset.variant = this._variant;
|
||||
}
|
||||
applyHudUiTheme(document.documentElement, this._variant);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -899,7 +1026,12 @@ export class IntelHUD {
|
||||
this._dataManager = dataManager || null;
|
||||
if (typeof this._dataManager?.subscribe === 'function') {
|
||||
this._dataManagerUnsubscribe = this._dataManager.subscribe((change) => {
|
||||
if (change?.type === 'visibility') this._markSummaryDirty();
|
||||
if (
|
||||
['visibility', 'refresh-transition', 'refresh-cancelled'].includes(
|
||||
change?.type,
|
||||
)
|
||||
)
|
||||
this._markSummaryDirty();
|
||||
});
|
||||
}
|
||||
this._markSummaryDirty();
|
||||
|
||||
Reference in new issue
Block a user