mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
style: apply current runtime formatter to voice integration
This commit is contained in:
1 parent
3385e6b713
commit
d1a72a35af
4 files changed
+414
-249
No files matched your search
+71
-14
@@ -29,14 +29,54 @@ import { pointInRing } from './naturalEarthRegions.js';
|
|||||||
|
|
||||||
/** Layers the engine understands, with the fields queries may reference. */
|
/** Layers the engine understands, with the fields queries may reference. */
|
||||||
export const ANALYST_LAYERS = {
|
export const ANALYST_LAYERS = {
|
||||||
flights: { numeric: ['altitudeM', 'speedMps', 'verticalRateMps'], text: ['callsign', 'icao24', 'originCountry', 'operator', 'routeOrigin', 'routeDestination', 'aircraftClass'], flags: ['military', 'onGround'] },
|
flights: {
|
||||||
military: { numeric: ['altitudeM', 'speedMps', 'verticalRateMps'], text: ['callsign', 'icao24', 'originCountry', 'operator', 'aircraftClass'], flags: ['military', 'onGround'] },
|
numeric: ['altitudeM', 'speedMps', 'verticalRateMps'],
|
||||||
'ais-live-vessels': { numeric: ['speedKts', 'courseDeg'], text: ['name', 'mmsi', 'shipType', 'destination', 'navStatus'], flags: [] },
|
text: [
|
||||||
'local-firms': { numeric: ['frp'], text: ['confidence', 'satellite'], flags: [] },
|
'callsign',
|
||||||
earthquakes: { numeric: ['magnitude', 'depthKm'], text: ['place'], flags: [] },
|
'icao24',
|
||||||
satellites: { numeric: ['altitudeM', 'speedMps'], text: ['name', 'noradId', 'satelliteClass', 'group'], flags: [] },
|
'originCountry',
|
||||||
'local-datacenters': { numeric: [], text: ['name', 'operator', 'capacity'], flags: [] },
|
'operator',
|
||||||
'local-dams': { numeric: [], text: ['name', 'operator', 'river', 'output'], flags: [] },
|
'routeOrigin',
|
||||||
|
'routeDestination',
|
||||||
|
'aircraftClass',
|
||||||
|
],
|
||||||
|
flags: ['military', 'onGround'],
|
||||||
|
},
|
||||||
|
military: {
|
||||||
|
numeric: ['altitudeM', 'speedMps', 'verticalRateMps'],
|
||||||
|
text: ['callsign', 'icao24', 'originCountry', 'operator', 'aircraftClass'],
|
||||||
|
flags: ['military', 'onGround'],
|
||||||
|
},
|
||||||
|
'ais-live-vessels': {
|
||||||
|
numeric: ['speedKts', 'courseDeg'],
|
||||||
|
text: ['name', 'mmsi', 'shipType', 'destination', 'navStatus'],
|
||||||
|
flags: [],
|
||||||
|
},
|
||||||
|
'local-firms': {
|
||||||
|
numeric: ['frp'],
|
||||||
|
text: ['confidence', 'satellite'],
|
||||||
|
flags: [],
|
||||||
|
},
|
||||||
|
earthquakes: {
|
||||||
|
numeric: ['magnitude', 'depthKm'],
|
||||||
|
text: ['place'],
|
||||||
|
flags: [],
|
||||||
|
},
|
||||||
|
satellites: {
|
||||||
|
numeric: ['altitudeM', 'speedMps'],
|
||||||
|
text: ['name', 'noradId', 'satelliteClass', 'group'],
|
||||||
|
flags: [],
|
||||||
|
},
|
||||||
|
'local-datacenters': {
|
||||||
|
numeric: [],
|
||||||
|
text: ['name', 'operator', 'capacity'],
|
||||||
|
flags: [],
|
||||||
|
},
|
||||||
|
'local-dams': {
|
||||||
|
numeric: [],
|
||||||
|
text: ['name', 'operator', 'river', 'output'],
|
||||||
|
flags: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const EARTH_R_KM = 6371;
|
const EARTH_R_KM = 6371;
|
||||||
@@ -131,7 +171,7 @@ export function createAnalystEngine(providers) {
|
|||||||
async function query(spec = {}) {
|
async function query(spec = {}) {
|
||||||
const layers =
|
const layers =
|
||||||
spec.followUp && lastResult
|
spec.followUp && lastResult
|
||||||
? null // follow-up: re-filter the remembered set, no re-snapshot
|
? null // follow-up: re-filter the remembered set, no re-snapshot
|
||||||
: Array.isArray(spec.layers) && spec.layers.length
|
: Array.isArray(spec.layers) && spec.layers.length
|
||||||
? spec.layers
|
? spec.layers
|
||||||
: ['flights'];
|
: ['flights'];
|
||||||
@@ -161,7 +201,20 @@ export function createAnalystEngine(providers) {
|
|||||||
const rows = providers.getRecords(key) || [];
|
const rows = providers.getRecords(key) || [];
|
||||||
const snapshot = providers.getLayerSnapshot?.(key);
|
const snapshot = providers.getLayerSnapshot?.(key);
|
||||||
if (snapshot) queriedSnapshots.push(snapshot);
|
if (snapshot) queriedSnapshots.push(snapshot);
|
||||||
layersQueried.push({ layerKey: key, records: rows.length, ...(snapshot ? { feedState: snapshot.feedState, source: snapshot.source, lastUpdate: snapshot.lastUpdate, enabled: snapshot.enabled, error: snapshot.error } : {}), ...providers.getRecordCoverage?.(key, rows) });
|
layersQueried.push({
|
||||||
|
layerKey: key,
|
||||||
|
records: rows.length,
|
||||||
|
...(snapshot
|
||||||
|
? {
|
||||||
|
feedState: snapshot.feedState,
|
||||||
|
source: snapshot.source,
|
||||||
|
lastUpdate: snapshot.lastUpdate,
|
||||||
|
enabled: snapshot.enabled,
|
||||||
|
error: snapshot.error,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...providers.getRecordCoverage?.(key, rows),
|
||||||
|
});
|
||||||
for (const row of rows) records.push({ layerKey: key, ...row });
|
for (const row of rows) records.push({ layerKey: key, ...row });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,8 +259,8 @@ export function createAnalystEngine(providers) {
|
|||||||
// nothing in the payload to show which.
|
// nothing in the payload to show which.
|
||||||
const subjectCenter =
|
const subjectCenter =
|
||||||
Number.isFinite(subject?.lat) && Number.isFinite(subject?.lon)
|
Number.isFinite(subject?.lat) && Number.isFinite(subject?.lon)
|
||||||
? { lat: subject.lat, lon: subject.lon }
|
? { lat: subject.lat, lon: subject.lon }
|
||||||
: null;
|
: null;
|
||||||
const center = explicitCenter ||
|
const center = explicitCenter ||
|
||||||
subjectCenter || { lat: view.lat, lon: view.lon };
|
subjectCenter || { lat: view.lat, lon: view.lon };
|
||||||
resolvedScope = { center, km: Number(scope.km) || 100 };
|
resolvedScope = { center, km: Number(scope.km) || 100 };
|
||||||
@@ -273,9 +326,13 @@ export function createAnalystEngine(providers) {
|
|||||||
coverage: {
|
coverage: {
|
||||||
layersQueried,
|
layersQueried,
|
||||||
scope: scopeNote,
|
scope: scopeNote,
|
||||||
...(queriedSnapshots.length ? { feedProvenance: feedProvenanceEnvelope(queriedSnapshots) } : {}),
|
...(queriedSnapshots.length
|
||||||
|
? { feedProvenance: feedProvenanceEnvelope(queriedSnapshots) }
|
||||||
|
: {}),
|
||||||
followUp: Boolean(spec.followUp && lastResult),
|
followUp: Boolean(spec.followUp && lastResult),
|
||||||
note: layersQueried.some((layer) => layer.basis === 'bounded-loaded-records')
|
note: layersQueried.some(
|
||||||
|
(layer) => layer.basis === 'bounded-loaded-records',
|
||||||
|
)
|
||||||
? 'Counts and ranks cover the bounded examined loaded records only; omitted records may change the nearest item or count. Satellite distance is ground distance, not slant range.'
|
? 'Counts and ranks cover the bounded examined loaded records only; omitted records may change the nearest item or count. Satellite distance is ground distance, not slant range.'
|
||||||
: 'client-side data only — answers cover what the enabled layers currently hold',
|
: 'client-side data only — answers cover what the enabled layers currently hold',
|
||||||
},
|
},
|
||||||
|
|||||||
+25
-8
@@ -24,7 +24,12 @@ import {
|
|||||||
geoidHeight,
|
geoidHeight,
|
||||||
} from './data/geoid.js';
|
} from './data/geoid.js';
|
||||||
import { getBasemapLabelContext } from './voice/gevActions.js';
|
import { getBasemapLabelContext } from './voice/gevActions.js';
|
||||||
import { hudSummaryMatchesProvenance, hudSummaryLayerContext, hudTelemetryProvenanceTag, isHudSummaryUnconfigured } from './hudSummaryResponse.js';
|
import {
|
||||||
|
hudSummaryMatchesProvenance,
|
||||||
|
hudSummaryLayerContext,
|
||||||
|
hudTelemetryProvenanceTag,
|
||||||
|
isHudSummaryUnconfigured,
|
||||||
|
} from './hudSummaryResponse.js';
|
||||||
|
|
||||||
/** Color palettes keyed by shader mode; applied as CSS custom properties. */
|
/** Color palettes keyed by shader mode; applied as CSS custom properties. */
|
||||||
const HUD_COLORS = {
|
const HUD_COLORS = {
|
||||||
@@ -641,15 +646,17 @@ export class IntelHUD {
|
|||||||
const altDisplayM = Number.isFinite(m.altMslM) ? m.altMslM : m.altM;
|
const altDisplayM = Number.isFinite(m.altMslM) ? m.altMslM : m.altM;
|
||||||
const altTag =
|
const altTag =
|
||||||
altDisplayM >= 1000
|
altDisplayM >= 1000
|
||||||
? `${(altDisplayM / 1000).toFixed(1)}KM`
|
? `${(altDisplayM / 1000).toFixed(1)}KM`
|
||||||
: `${Math.round(altDisplayM)}M`;
|
: `${Math.round(altDisplayM)}M`;
|
||||||
const winTag = window
|
const winTag = window
|
||||||
? `${Math.max(1, Math.round(window.widthKm))}x${Math.max(1, Math.round(window.heightKm))}KM`
|
? `${Math.max(1, Math.round(window.widthKm))}x${Math.max(1, Math.round(window.heightKm))}KM`
|
||||||
: 'N/A';
|
: 'N/A';
|
||||||
// NEAR the nearest catalogued POI at metro range; otherwise the lat/lon sector.
|
// 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);
|
||||||
|
|
||||||
const provenance = hudTelemetryProvenanceTag(this._dataManager?.getAll?.() || []);
|
const provenance = hudTelemetryProvenanceTag(
|
||||||
|
this._dataManager?.getAll?.() || [],
|
||||||
|
);
|
||||||
const line = `${modeLabel} ${band} ${localityTag} | ${region} | ALT ${altTag} | WINDOW ${winTag} | SUN ${m.sunEl.toFixed(0)}° | ONA ${m.ona.toFixed(0)}° | ${localTag}`;
|
const line = `${modeLabel} ${band} ${localityTag} | ${region} | ALT ${altTag} | WINDOW ${winTag} | SUN ${m.sunEl.toFixed(0)}° | ONA ${m.ona.toFixed(0)}° | ${localTag}`;
|
||||||
return provenance ? `${line} | ${provenance}` : line;
|
return provenance ? `${line} | ${provenance}` : line;
|
||||||
}
|
}
|
||||||
@@ -736,7 +743,12 @@ export class IntelHUD {
|
|||||||
if (!response.ok || !data?.summary) {
|
if (!response.ok || !data?.summary) {
|
||||||
throw new Error(data?.error || `HTTP ${response.status}`);
|
throw new Error(data?.error || `HTTP ${response.status}`);
|
||||||
}
|
}
|
||||||
this._setSummaryText(hudSummaryMatchesProvenance(data.summary, context.feedProvenance) ? data.summary : fallbackText, animate);
|
this._setSummaryText(
|
||||||
|
hudSummaryMatchesProvenance(data.summary, context.feedProvenance)
|
||||||
|
? data.summary
|
||||||
|
: fallbackText,
|
||||||
|
animate,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.name !== 'AbortError') {
|
if (error?.name !== 'AbortError') {
|
||||||
console.warn('[HUD] AI summary unavailable:', error);
|
console.warn('[HUD] AI summary unavailable:', error);
|
||||||
@@ -770,8 +782,8 @@ export class IntelHUD {
|
|||||||
const enabledLayers =
|
const enabledLayers =
|
||||||
this._dataManager
|
this._dataManager
|
||||||
?.getAll?.()
|
?.getAll?.()
|
||||||
?.filter((layer) => layer.enabled)
|
?.filter((layer) => layer.enabled)
|
||||||
.map((layer) => layer.name) || [];
|
.map((layer) => layer.name) || [];
|
||||||
return {
|
return {
|
||||||
placeLabels: labels.placeLabels,
|
placeLabels: labels.placeLabels,
|
||||||
streetLabels: labels.streetLabels,
|
streetLabels: labels.streetLabels,
|
||||||
@@ -907,7 +919,12 @@ export class IntelHUD {
|
|||||||
this._dataManager = dataManager || null;
|
this._dataManager = dataManager || null;
|
||||||
if (typeof this._dataManager?.subscribe === 'function') {
|
if (typeof this._dataManager?.subscribe === 'function') {
|
||||||
this._dataManagerUnsubscribe = this._dataManager.subscribe((change) => {
|
this._dataManagerUnsubscribe = this._dataManager.subscribe((change) => {
|
||||||
if (['visibility', 'refresh-transition', 'refresh-cancelled'].includes(change?.type)) this._markSummaryDirty();
|
if (
|
||||||
|
['visibility', 'refresh-transition', 'refresh-cancelled'].includes(
|
||||||
|
change?.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
this._markSummaryDirty();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this._markSummaryDirty();
|
this._markSummaryDirty();
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export function keylessHudSummaryResponse(apiKey) {
|
|||||||
export function isHudSummaryUnconfigured(status, data) {
|
export function isHudSummaryUnconfigured(status, data) {
|
||||||
const keys =
|
const keys =
|
||||||
data !== null && typeof data === 'object' && !Array.isArray(data)
|
data !== null && typeof data === 'object' && !Array.isArray(data)
|
||||||
? Object.keys(data)
|
? Object.keys(data)
|
||||||
: [];
|
: [];
|
||||||
return (
|
return (
|
||||||
keys.length === 4 &&
|
keys.length === 4 &&
|
||||||
status === 200 &&
|
status === 200 &&
|
||||||
@@ -93,7 +93,9 @@ export function hudTelemetryProvenanceTag(layers = [], options) {
|
|||||||
const envelope = feedProvenanceEnvelope(snapshots, options);
|
const envelope = feedProvenanceEnvelope(snapshots, options);
|
||||||
if (!envelope.overall || envelope.overall === 'nominal') return null;
|
if (!envelope.overall || envelope.overall === 'nominal') return null;
|
||||||
const names = envelope.layers
|
const names = envelope.layers
|
||||||
.filter((s) => s.feedState && s.feedState !== 'nominal' && s.feedState !== 'off')
|
.filter(
|
||||||
|
(s) => s.feedState && s.feedState !== 'nominal' && s.feedState !== 'off',
|
||||||
|
)
|
||||||
.map((s) => String(s.name || s.id || 'LAYER').toUpperCase())
|
.map((s) => String(s.name || s.id || 'LAYER').toUpperCase())
|
||||||
.slice(0, 2);
|
.slice(0, 2);
|
||||||
return `${envelope.overall.toUpperCase()}${names.length ? ` ${names.join('/')}` : ''}`;
|
return `${envelope.overall.toUpperCase()}${names.length ? ` ${names.join('/')}` : ''}`;
|
||||||
@@ -103,5 +105,8 @@ export function hudTelemetryProvenanceTag(layers = [], options) {
|
|||||||
export function hudSummaryMatchesProvenance(summary, provenance) {
|
export function hudSummaryMatchesProvenance(summary, provenance) {
|
||||||
const state = provenance?.overall;
|
const state = provenance?.overall;
|
||||||
if (!state || state === 'nominal' || state === 'off') return true;
|
if (!state || state === 'nominal' || state === 'off') return true;
|
||||||
return String(summary || '').toUpperCase().split(/\W+/).includes(String(state).toUpperCase());
|
return String(summary || '')
|
||||||
|
.toUpperCase()
|
||||||
|
.split(/\W+/)
|
||||||
|
.includes(String(state).toUpperCase());
|
||||||
}
|
}
|
||||||
+309
-223
@@ -1,4 +1,8 @@
|
|||||||
import { layerSnapshot, layerSnapshots, feedProvenanceEnvelope } from '../data/layerSnapshot.js';
|
import {
|
||||||
|
layerSnapshot,
|
||||||
|
layerSnapshots,
|
||||||
|
feedProvenanceEnvelope,
|
||||||
|
} from '../data/layerSnapshot.js';
|
||||||
import { readLayerLifecycleSummary } from './layerSummary.js';
|
import { readLayerLifecycleSummary } from './layerSummary.js';
|
||||||
export { readLayerLifecycleSummary } from './layerSummary.js';
|
export { readLayerLifecycleSummary } from './layerSummary.js';
|
||||||
import { defaultGeospatial } from '../search/defaults.js';
|
import { defaultGeospatial } from '../search/defaults.js';
|
||||||
@@ -300,7 +304,7 @@ function cachesFor(service) {
|
|||||||
service.signal?.addEventListener(
|
service.signal?.addEventListener(
|
||||||
'abort',
|
'abort',
|
||||||
() => {
|
() => {
|
||||||
for (const cache of Object.values(caches)) cache.clear();
|
for (const cache of Object.values(caches)) cache.clear();
|
||||||
},
|
},
|
||||||
{ once: true },
|
{ once: true },
|
||||||
);
|
);
|
||||||
@@ -519,16 +523,16 @@ export function createGevActionRunner({
|
|||||||
...(args.locationQuery ? { query: args.locationQuery } : {}),
|
...(args.locationQuery ? { query: args.locationQuery } : {}),
|
||||||
...(hasCoordinates
|
...(hasCoordinates
|
||||||
? {
|
? {
|
||||||
latitude: Number(args.latitude),
|
latitude: Number(args.latitude),
|
||||||
longitude: Number(args.longitude),
|
longitude: Number(args.longitude),
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
};
|
};
|
||||||
const layer = await runGevAction(
|
const layer = await runGevAction(
|
||||||
'set_layer_visibility',
|
'set_layer_visibility',
|
||||||
{
|
{
|
||||||
layerId,
|
layerId,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
runOptions,
|
runOptions,
|
||||||
);
|
);
|
||||||
@@ -712,10 +716,10 @@ export function createGevActionRunner({
|
|||||||
}
|
}
|
||||||
const cancellationState = () =>
|
const cancellationState = () =>
|
||||||
withContextModeVocabulary(
|
withContextModeVocabulary(
|
||||||
typeof styleManager.getContextModeState === 'function'
|
typeof styleManager.getContextModeState === 'function'
|
||||||
? styleManager.getContextModeState()
|
? styleManager.getContextModeState()
|
||||||
: {},
|
: {},
|
||||||
);
|
);
|
||||||
if (!current()) {
|
if (!current()) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -731,8 +735,8 @@ export function createGevActionRunner({
|
|||||||
const result = await styleManager.setContextMode(
|
const result = await styleManager.setContextMode(
|
||||||
mode === 'off' ? null : mode,
|
mode === 'off' ? null : mode,
|
||||||
{
|
{
|
||||||
signal: runOptions.signal,
|
signal: runOptions.signal,
|
||||||
isCurrent: runOptions.isCurrent,
|
isCurrent: runOptions.isCurrent,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (!current() && result?.ok !== true) {
|
if (!current() && result?.ok !== true) {
|
||||||
@@ -803,8 +807,8 @@ export function createGevActionRunner({
|
|||||||
rollbackTarget = styleManager.getAircraftTrackingTarget?.() || null;
|
rollbackTarget = styleManager.getAircraftTrackingTarget?.() || null;
|
||||||
const contextState =
|
const contextState =
|
||||||
typeof styleManager.getContextModeState === 'function'
|
typeof styleManager.getContextModeState === 'function'
|
||||||
? styleManager.getContextModeState()
|
? styleManager.getContextModeState()
|
||||||
: {};
|
: {};
|
||||||
priorContextMode = contextState?.mode || null;
|
priorContextMode = contextState?.mode || null;
|
||||||
const contactsReady =
|
const contactsReady =
|
||||||
contextState?.mode === 'flights' &&
|
contextState?.mode === 'flights' &&
|
||||||
@@ -824,8 +828,8 @@ export function createGevActionRunner({
|
|||||||
if (contextResult?.ok !== true || !current()) {
|
if (contextResult?.ok !== true || !current()) {
|
||||||
const contextRollback = contextChangedForEntry
|
const contextRollback = contextChangedForEntry
|
||||||
? await styleManager.setContextMode(priorContextMode, {
|
? await styleManager.setContextMode(priorContextMode, {
|
||||||
claimVisualAuthority: false,
|
claimVisualAuthority: false,
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -873,12 +877,12 @@ export function createGevActionRunner({
|
|||||||
const contextRollback = await styleManager.setContextMode(
|
const contextRollback = await styleManager.setContextMode(
|
||||||
priorContextMode,
|
priorContextMode,
|
||||||
{
|
{
|
||||||
// Undoing this action's own precondition — still choreography.
|
// Undoing this action's own precondition — still choreography.
|
||||||
claimVisualAuthority: false,
|
claimVisualAuthority: false,
|
||||||
...(current()
|
...(current()
|
||||||
? {
|
? {
|
||||||
signal: runOptions.signal,
|
signal: runOptions.signal,
|
||||||
isCurrent: runOptions.isCurrent,
|
isCurrent: runOptions.isCurrent,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
@@ -896,8 +900,8 @@ export function createGevActionRunner({
|
|||||||
setPanelOpen(styleManager, 'data-panel', true);
|
setPanelOpen(styleManager, 'data-panel', true);
|
||||||
const focusedLayer =
|
const focusedLayer =
|
||||||
layerId && dataManager.layers.has(layerId)
|
layerId && dataManager.layers.has(layerId)
|
||||||
? focusDataLayerRow(layerId)
|
? focusDataLayerRow(layerId)
|
||||||
: null;
|
: null;
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
action: 'show_data_layers_menu',
|
action: 'show_data_layers_menu',
|
||||||
@@ -907,10 +911,10 @@ export function createGevActionRunner({
|
|||||||
.getAll()
|
.getAll()
|
||||||
.filter((layer) => layer.showInTogglePanel !== false)
|
.filter((layer) => layer.showInTogglePanel !== false)
|
||||||
.map((layer) => ({
|
.map((layer) => ({
|
||||||
id: layer.id,
|
id: layer.id,
|
||||||
name: layer.name,
|
name: layer.name,
|
||||||
enabled: layer.enabled,
|
enabled: layer.enabled,
|
||||||
count: layer.stats?.count || 0,
|
count: layer.stats?.count || 0,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -924,16 +928,16 @@ export function createGevActionRunner({
|
|||||||
typeof styleManager?.runImmediateLocationNavigation === 'function'
|
typeof styleManager?.runImmediateLocationNavigation === 'function'
|
||||||
? (navigate) =>
|
? (navigate) =>
|
||||||
styleManager.runImmediateLocationNavigation(navigate)
|
styleManager.runImmediateLocationNavigation(navigate)
|
||||||
: null,
|
: null,
|
||||||
beginDeferred:
|
beginDeferred:
|
||||||
typeof styleManager?.beginDeferredLocationNavigation === 'function'
|
typeof styleManager?.beginDeferredLocationNavigation === 'function'
|
||||||
? () => styleManager.beginDeferredLocationNavigation()
|
? () => styleManager.beginDeferredLocationNavigation()
|
||||||
: null,
|
: null,
|
||||||
reassertDeferred:
|
reassertDeferred:
|
||||||
typeof styleManager?.reassertDeferredLocationNavigation === 'function'
|
typeof styleManager?.reassertDeferredLocationNavigation === 'function'
|
||||||
? (generation) =>
|
? (generation) =>
|
||||||
styleManager.reassertDeferredLocationNavigation(generation)
|
styleManager.reassertDeferredLocationNavigation(generation)
|
||||||
: null,
|
: null,
|
||||||
onStart: () => {
|
onStart: () => {
|
||||||
if (typeof styleManager?.beginLocationNavigation === 'function') {
|
if (typeof styleManager?.beginLocationNavigation === 'function') {
|
||||||
styleManager.beginLocationNavigation();
|
styleManager.beginLocationNavigation();
|
||||||
@@ -1180,8 +1184,8 @@ function sanitizeAnnotationSpec(spec) {
|
|||||||
out.points = out.points
|
out.points = out.points
|
||||||
.slice(0, MAX_ROUTE_POINTS)
|
.slice(0, MAX_ROUTE_POINTS)
|
||||||
.map((p) =>
|
.map((p) =>
|
||||||
p && typeof p === 'object' && typeof p.target === 'string'
|
p && typeof p === 'object' && typeof p.target === 'string'
|
||||||
? { ...p, target: clampStr(p.target, MAX_TARGET_LEN) }
|
? { ...p, target: clampStr(p.target, MAX_TARGET_LEN) }
|
||||||
: p,
|
: p,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1644,8 +1648,8 @@ export async function controlRadio(
|
|||||||
// viewport's nearest station.
|
// viewport's nearest station.
|
||||||
const action =
|
const action =
|
||||||
requestedAction === 'play' && hasSelectionCriteria
|
requestedAction === 'play' && hasSelectionCriteria
|
||||||
? 'select'
|
? 'select'
|
||||||
: requestedAction;
|
: requestedAction;
|
||||||
|
|
||||||
const readRadioLifecycle = () =>
|
const readRadioLifecycle = () =>
|
||||||
readLayerLifecycleSummary(dataManager, 'radio');
|
readLayerLifecycleSummary(dataManager, 'radio');
|
||||||
@@ -1666,11 +1670,11 @@ export async function controlRadio(
|
|||||||
const intentSummary = () =>
|
const intentSummary = () =>
|
||||||
lastIntentOutcome
|
lastIntentOutcome
|
||||||
? {
|
? {
|
||||||
phase: lastIntentOutcome.phase,
|
phase: lastIntentOutcome.phase,
|
||||||
cancellationReason: lastIntentOutcome.cancellationReason || null,
|
cancellationReason: lastIntentOutcome.cancellationReason || null,
|
||||||
successorIntentEpoch: lastIntentOutcome.successorIntentEpoch ?? null,
|
successorIntentEpoch: lastIntentOutcome.successorIntentEpoch ?? null,
|
||||||
successorEnabled: lastIntentOutcome.successorEnabled ?? null,
|
successorEnabled: lastIntentOutcome.successorEnabled ?? null,
|
||||||
successorOrigin: lastIntentOutcome.successorOrigin ?? null,
|
successorOrigin: lastIntentOutcome.successorOrigin ?? null,
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
@@ -1832,7 +1836,7 @@ export async function controlRadio(
|
|||||||
{ volume: volumePct / 100 },
|
{ volume: volumePct / 100 },
|
||||||
{ origin: 'voice' },
|
{ origin: 'voice' },
|
||||||
)
|
)
|
||||||
: radio.setVolume(volumePct / 100);
|
: radio.setVolume(volumePct / 100);
|
||||||
if (volumeApplied === false) {
|
if (volumeApplied === false) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -1970,12 +1974,12 @@ export async function controlRadio(
|
|||||||
if (!radioActionIsCurrent(options)) return cancelled(summarize);
|
if (!radioActionIsCurrent(options)) return cancelled(summarize);
|
||||||
const station = radio.selectRequestedStation?.(
|
const station = radio.selectRequestedStation?.(
|
||||||
{
|
{
|
||||||
categoryId: String(args.category || 'all'),
|
categoryId: String(args.category || 'all'),
|
||||||
anchor: location ? { lat: location.lat, lon: location.lon } : null,
|
anchor: location ? { lat: location.lat, lon: location.lon } : null,
|
||||||
country: normalizedCountry.empty
|
country: normalizedCountry.empty
|
||||||
? String(location?.country || '')
|
? String(location?.country || '')
|
||||||
: normalizedCountry.code,
|
: normalizedCountry.code,
|
||||||
stationQuery: String(args.stationQuery || ''),
|
stationQuery: String(args.stationQuery || ''),
|
||||||
},
|
},
|
||||||
{ autoplay: false },
|
{ autoplay: false },
|
||||||
);
|
);
|
||||||
@@ -2097,23 +2101,23 @@ async function trackEntity(viewer, dataManager, styleManager, args = {}) {
|
|||||||
'fire',
|
'fire',
|
||||||
'track_entity',
|
'track_entity',
|
||||||
() => {
|
() => {
|
||||||
flyToLandmark(viewer, strongest.latitude, strongest.longitude, {
|
flyToLandmark(viewer, strongest.latitude, strongest.longitude, {
|
||||||
range: 14000,
|
range: 14000,
|
||||||
pitch: -50,
|
pitch: -50,
|
||||||
heading: 0,
|
heading: 0,
|
||||||
buildingHeight: 0,
|
buildingHeight: 0,
|
||||||
duration: 2.2,
|
duration: 2.2,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
action: 'track_entity',
|
action: 'track_entity',
|
||||||
kind: 'fire',
|
kind: 'fire',
|
||||||
layerId: 'local-firms',
|
layerId: 'local-firms',
|
||||||
label: strongest.label || 'Strongest fire',
|
label: strongest.label || 'Strongest fire',
|
||||||
latitude: strongest.latitude,
|
latitude: strongest.latitude,
|
||||||
longitude: strongest.longitude,
|
longitude: strongest.longitude,
|
||||||
frp: strongest.frp ?? null,
|
frp: strongest.frp ?? null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2152,39 +2156,39 @@ async function trackEntity(viewer, dataManager, styleManager, args = {}) {
|
|||||||
family.kind,
|
family.kind,
|
||||||
'track_entity',
|
'track_entity',
|
||||||
() => {
|
() => {
|
||||||
let trackedOk = false;
|
let trackedOk = false;
|
||||||
if (family.kind === 'vessel') {
|
if (family.kind === 'vessel') {
|
||||||
trackedOk = !!module.selectById?.(found.mmsi);
|
trackedOk = !!module.selectById?.(found.mmsi);
|
||||||
flyToLandmark(viewer, found.latitude, found.longitude, {
|
flyToLandmark(viewer, found.latitude, found.longitude, {
|
||||||
range: 6000,
|
range: 6000,
|
||||||
pitch: -45,
|
pitch: -45,
|
||||||
heading: 0,
|
heading: 0,
|
||||||
buildingHeight: 0,
|
buildingHeight: 0,
|
||||||
duration: 2.0,
|
duration: 2.0,
|
||||||
});
|
});
|
||||||
} else if (family.kind === 'satellite') {
|
} else if (family.kind === 'satellite') {
|
||||||
trackedOk = !!module.trackById?.(found.noradId, { origin: 'voice' });
|
trackedOk = !!module.trackById?.(found.noradId, { origin: 'voice' });
|
||||||
} else {
|
} else {
|
||||||
trackedOk = !!module.trackById?.(found.icao24, { origin: 'voice' });
|
trackedOk = !!module.trackById?.(found.icao24, { origin: 'voice' });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: trackedOk,
|
ok: trackedOk,
|
||||||
action: 'track_entity',
|
action: 'track_entity',
|
||||||
layerId: family.layerId,
|
layerId: family.layerId,
|
||||||
kind: family.kind,
|
kind: family.kind,
|
||||||
// Aircraft follow the flight layers' label convention (callsign →
|
// Aircraft follow the flight layers' label convention (callsign →
|
||||||
// registration → icao24) so the spoken name matches what the UI shows;
|
// registration → icao24) so the spoken name matches what the UI shows;
|
||||||
// `registration` is absent on vessels/satellites and simply falls
|
// `registration` is absent on vessels/satellites and simply falls
|
||||||
// through to their own name/id links.
|
// through to their own name/id links.
|
||||||
label: formatTrackedEntityLabel(found, query),
|
label: formatTrackedEntityLabel(found, query),
|
||||||
latitude: found.latitude ?? null,
|
latitude: found.latitude ?? null,
|
||||||
longitude: found.longitude ?? null,
|
longitude: found.longitude ?? null,
|
||||||
altitudeM: Number.isFinite(found.altitudeM)
|
altitudeM: Number.isFinite(found.altitudeM)
|
||||||
? Math.round(found.altitudeM)
|
? Math.round(found.altitudeM)
|
||||||
: null,
|
: null,
|
||||||
error: trackedOk ? null : 'Match found but tracking failed',
|
error: trackedOk ? null : 'Match found but tracking failed',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2337,40 +2341,40 @@ async function frameOverhead(viewer, dataManager, styleManager, args = {}) {
|
|||||||
'frame',
|
'frame',
|
||||||
'frame_overhead',
|
'frame_overhead',
|
||||||
() => {
|
() => {
|
||||||
viewer.camera.flyToBoundingSphere(sphere, {
|
viewer.camera.flyToBoundingSphere(sphere, {
|
||||||
duration: 2.0,
|
duration: 2.0,
|
||||||
offset: new Cesium.HeadingPitchRange(
|
offset: new Cesium.HeadingPitchRange(
|
||||||
viewer.camera.heading,
|
viewer.camera.heading,
|
||||||
pitch,
|
pitch,
|
||||||
sphere.radius * 2.4,
|
sphere.radius * 2.4,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
let detectionEnabled = false;
|
let detectionEnabled = false;
|
||||||
try {
|
try {
|
||||||
const detectionState = styleManager?.getDetectionState?.();
|
const detectionState = styleManager?.getDetectionState?.();
|
||||||
if (detectionState?.detectionMode === 'OFF') {
|
if (detectionState?.detectionMode === 'OFF') {
|
||||||
const detectionResult = styleManager.setDetection({ mode: 'dense' });
|
const detectionResult = styleManager.setDetection({ mode: 'dense' });
|
||||||
detectionEnabled = detectionResult?.ok === true;
|
detectionEnabled = detectionResult?.ok === true;
|
||||||
} else if (detectionState) {
|
} else if (detectionState) {
|
||||||
detectionEnabled = true;
|
detectionEnabled = true;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// detection facade unavailable; framing still succeeded
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// detection facade unavailable; framing still succeeded
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
action: 'frame_overhead',
|
action: 'frame_overhead',
|
||||||
layerId,
|
layerId,
|
||||||
radiusKm: Math.round(radiusKm),
|
radiusKm: Math.round(radiusKm),
|
||||||
count: entries.length,
|
count: entries.length,
|
||||||
detectionEnabled,
|
detectionEnabled,
|
||||||
nearest: entries.slice(0, 5).map((entry) => ({
|
nearest: entries.slice(0, 5).map((entry) => ({
|
||||||
id: entry.id || entry.icao24 || entry.mmsi || null,
|
id: entry.id || entry.icao24 || entry.mmsi || null,
|
||||||
label: entry.label || entry.callsign || entry.name || null,
|
label: entry.label || entry.callsign || entry.name || null,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2491,8 +2495,8 @@ export async function getBasemapLabelContext(
|
|||||||
]).slice(0, 16),
|
]).slice(0, 16),
|
||||||
nearbyPlaceLabels: uniqueStrings(
|
nearbyPlaceLabels: uniqueStrings(
|
||||||
(nearbyPlaces || []).flatMap((nearbyPlace) => [
|
(nearbyPlaces || []).flatMap((nearbyPlace) => [
|
||||||
nearbyPlace.name,
|
nearbyPlace.name,
|
||||||
nearbyPlace.address,
|
nearbyPlace.address,
|
||||||
]),
|
]),
|
||||||
).slice(0, 24),
|
).slice(0, 24),
|
||||||
};
|
};
|
||||||
@@ -2661,7 +2665,8 @@ function nextIssPass(viewer, dataManager, args) {
|
|||||||
peakElevationDeg: Math.round(pass.maxElevDeg),
|
peakElevationDeg: Math.round(pass.maxElevDeg),
|
||||||
riseDirection: compassDir(pass.riseAzDeg),
|
riseDirection: compassDir(pass.riseAzDeg),
|
||||||
visible: typeof pass.visible === 'boolean' ? pass.visible : null,
|
visible: typeof pass.visible === 'boolean' ? pass.visible : null,
|
||||||
visibilityNote: 'Geometric illumination estimate only; weather, brightness and orbital-element age affect actual visibility.',
|
visibilityNote:
|
||||||
|
'Geometric illumination estimate only; weather, brightness and orbital-element age affect actual visibility.',
|
||||||
setIso: new Date(pass.setMs).toISOString(),
|
setIso: new Date(pass.setMs).toISOString(),
|
||||||
peakIso: new Date(pass.maxElevMs).toISOString(),
|
peakIso: new Date(pass.maxElevMs).toISOString(),
|
||||||
};
|
};
|
||||||
@@ -2669,22 +2674,55 @@ function nextIssPass(viewer, dataManager, args) {
|
|||||||
|
|
||||||
function nextSatellitePass(viewer, dataManager, args) {
|
function nextSatellitePass(viewer, dataManager, args) {
|
||||||
const layer = dataManager?.layers?.get('satellites')?.module;
|
const layer = dataManager?.layers?.get('satellites')?.module;
|
||||||
const identity = layer?.resolveSatelliteForPass?.(args.target) || { status: 'not-found' };
|
const identity = layer?.resolveSatelliteForPass?.(args.target) || {
|
||||||
if (identity.status !== 'ok') return {
|
status: 'not-found',
|
||||||
ok: false, action: 'next_satellite_pass', ...identity,
|
|
||||||
error: identity.status === 'ambiguous' ? 'Several loaded satellites match. Choose a NORAD ID from candidates.' : 'No loaded satellite matches. Enable satellites and use an exact name or NORAD ID.',
|
|
||||||
};
|
};
|
||||||
|
if (identity.status !== 'ok')
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
action: 'next_satellite_pass',
|
||||||
|
...identity,
|
||||||
|
error:
|
||||||
|
identity.status === 'ambiguous'
|
||||||
|
? 'Several loaded satellites match. Choose a NORAD ID from candidates.'
|
||||||
|
: 'No loaded satellite matches. Enable satellites and use an exact name or NORAD ID.',
|
||||||
|
};
|
||||||
// Reuse the legacy location fallback and result formatting, substituting only
|
// Reuse the legacy location fallback and result formatting, substituting only
|
||||||
// this explicitly resolved catalog identity and the optional visibility filter.
|
// this explicitly resolved catalog identity and the optional visibility filter.
|
||||||
const adapter = { layers: new Map([['satellites', { module: {
|
const adapter = {
|
||||||
getNextIssPass: (options) => layer.getNextSatellitePass(identity.noradId, { ...options, requireVisible: args.visibleOnly === true }),
|
layers: new Map([
|
||||||
} }]]) };
|
[
|
||||||
|
'satellites',
|
||||||
|
{
|
||||||
|
module: {
|
||||||
|
getNextIssPass: (options) =>
|
||||||
|
layer.getNextSatellitePass(identity.noradId, {
|
||||||
|
...options,
|
||||||
|
requireVisible: args.visibleOnly === true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
};
|
||||||
const result = nextIssPass(viewer, adapter, args);
|
const result = nextIssPass(viewer, adapter, args);
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
result.error = result.error.replace(/ISS/g, identity.name || String(identity.noradId));
|
result.error = result.error.replace(
|
||||||
if (args.visibleOnly === true) result.error += ' Search required estimated illumination under a dark sky.';
|
/ISS/g,
|
||||||
|
identity.name || String(identity.noradId),
|
||||||
|
);
|
||||||
|
if (args.visibleOnly === true)
|
||||||
|
result.error +=
|
||||||
|
' Search required estimated illumination under a dark sky.';
|
||||||
}
|
}
|
||||||
return { ...result, action: 'next_satellite_pass', noradId: identity.noradId, name: identity.name, visibleOnly: args.visibleOnly === true, horizonHours: 24 };
|
return {
|
||||||
|
...result,
|
||||||
|
action: 'next_satellite_pass',
|
||||||
|
noradId: identity.noradId,
|
||||||
|
name: identity.name,
|
||||||
|
visibleOnly: args.visibleOnly === true,
|
||||||
|
horizonHours: 24,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePanelId(value) {
|
function normalizePanelId(value) {
|
||||||
@@ -2716,21 +2754,21 @@ function normalizeCockpitNavigationHints(rawAction) {
|
|||||||
|
|
||||||
const targetLayer =
|
const targetLayer =
|
||||||
raw.includes('vessel') || raw.includes('ship') || raw.includes('ais')
|
raw.includes('vessel') || raw.includes('ship') || raw.includes('ais')
|
||||||
? 'ais-live-vessels'
|
? 'ais-live-vessels'
|
||||||
: raw.includes('installation') ||
|
: raw.includes('installation') ||
|
||||||
raw.includes('facility') ||
|
raw.includes('facility') ||
|
||||||
raw.includes('base')
|
raw.includes('base')
|
||||||
? 'military-installations'
|
? 'military-installations'
|
||||||
: raw.includes('military')
|
: raw.includes('military')
|
||||||
? 'military'
|
? 'military'
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const aircraftClass =
|
const aircraftClass =
|
||||||
raw.includes('helicopter') ||
|
raw.includes('helicopter') ||
|
||||||
raw.includes('helo') ||
|
raw.includes('helo') ||
|
||||||
raw.includes('chopper')
|
raw.includes('chopper')
|
||||||
? 'helicopter'
|
? 'helicopter'
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
targetLayer,
|
targetLayer,
|
||||||
@@ -2839,10 +2877,10 @@ async function flyToRequestedLocation(
|
|||||||
placeSearch = unavailablePlaceSearch,
|
placeSearch = unavailablePlaceSearch,
|
||||||
searchNavigation = searchAndFlyTo,
|
searchNavigation = searchAndFlyTo,
|
||||||
signal,
|
signal,
|
||||||
onStart = null,
|
onStart = null,
|
||||||
runImmediate = null,
|
runImmediate = null,
|
||||||
beginDeferred = null,
|
beginDeferred = null,
|
||||||
reassertDeferred = null,
|
reassertDeferred = null,
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
const requestedRangeM = Number(args.rangeM);
|
const requestedRangeM = Number(args.rangeM);
|
||||||
@@ -2865,11 +2903,11 @@ async function flyToRequestedLocation(
|
|||||||
? new Promise((resolve) => {
|
? new Promise((resolve) => {
|
||||||
settleArrival = resolve;
|
settleArrival = resolve;
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
const arrivalHooks = arrival
|
const arrivalHooks = arrival
|
||||||
? {
|
? {
|
||||||
onComplete: () => settleArrival?.('arrived'),
|
onComplete: () => settleArrival?.('arrived'),
|
||||||
onCancel: () => settleArrival?.('cancelled'),
|
onCancel: () => settleArrival?.('cancelled'),
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
const afterArrival = async (result, label) => {
|
const afterArrival = async (result, label) => {
|
||||||
@@ -2884,12 +2922,12 @@ async function flyToRequestedLocation(
|
|||||||
if (locationId) {
|
if (locationId) {
|
||||||
const result = immediate(() =>
|
const result = immediate(() =>
|
||||||
flyToPresetLocation(viewer, locationId, {
|
flyToPresetLocation(viewer, locationId, {
|
||||||
...(rangeM || args.viewMode === 'close'
|
...(rangeM || args.viewMode === 'close'
|
||||||
? { range: rangeM || 250 }
|
? { range: rangeM || 250 }
|
||||||
: { viewMode: 'overview' }),
|
: { viewMode: 'overview' }),
|
||||||
duration: 2.2,
|
duration: 2.2,
|
||||||
onStart: immediateOnStart,
|
onStart: immediateOnStart,
|
||||||
...arrivalHooks,
|
...arrivalHooks,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (result === false)
|
if (result === false)
|
||||||
@@ -2914,13 +2952,13 @@ async function flyToRequestedLocation(
|
|||||||
if (Number.isFinite(latitude) && Number.isFinite(longitude)) {
|
if (Number.isFinite(latitude) && Number.isFinite(longitude)) {
|
||||||
const result = immediate(() =>
|
const result = immediate(() =>
|
||||||
flyToLandmark(viewer, latitude, longitude, {
|
flyToLandmark(viewer, latitude, longitude, {
|
||||||
range: rangeM || 250,
|
range: rangeM || 250,
|
||||||
pitch: -35,
|
pitch: -35,
|
||||||
heading: 0,
|
heading: 0,
|
||||||
buildingHeight: 0,
|
buildingHeight: 0,
|
||||||
duration: 2.2,
|
duration: 2.2,
|
||||||
onStart: immediateOnStart,
|
onStart: immediateOnStart,
|
||||||
...arrivalHooks,
|
...arrivalHooks,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (result === false)
|
if (result === false)
|
||||||
@@ -2946,10 +2984,10 @@ async function flyToRequestedLocation(
|
|||||||
if (poiMatch) {
|
if (poiMatch) {
|
||||||
const result = immediate(() =>
|
const result = immediate(() =>
|
||||||
flyToPOI(viewer, poiMatch.cityId, poiMatch.index, {
|
flyToPOI(viewer, poiMatch.cityId, poiMatch.index, {
|
||||||
duration: 2.2,
|
duration: 2.2,
|
||||||
onStart: immediateOnStart,
|
onStart: immediateOnStart,
|
||||||
...arrivalHooks,
|
...arrivalHooks,
|
||||||
...(rangeM ? { range: rangeM } : {}),
|
...(rangeM ? { range: rangeM } : {}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const poi = CITY_POIS[poiMatch.cityId]?.pois?.[poiMatch.index];
|
const poi = CITY_POIS[poiMatch.cityId]?.pois?.[poiMatch.index];
|
||||||
@@ -3029,19 +3067,19 @@ function getCurrentViewState(
|
|||||||
style: styleManager.activeStyle || 'normal',
|
style: styleManager.activeStyle || 'normal',
|
||||||
context:
|
context:
|
||||||
typeof styleManager.getContextModeState === 'function'
|
typeof styleManager.getContextModeState === 'function'
|
||||||
? {
|
? {
|
||||||
...withContextModeVocabulary(styleManager.getContextModeState()),
|
...withContextModeVocabulary(styleManager.getContextModeState()),
|
||||||
// The numbers on the operator's Contacts panel, so a window/count
|
// The numbers on the operator's Contacts panel, so a window/count
|
||||||
// question can be answered from what they are looking at.
|
// question can be answered from what they are looking at.
|
||||||
...(activeContactsWindow(dataManager)
|
...(activeContactsWindow(dataManager)
|
||||||
? { contactsWindow: activeContactsWindow(dataManager) }
|
? { contactsWindow: activeContactsWindow(dataManager) }
|
||||||
: {}),
|
: {}),
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
cockpit:
|
cockpit:
|
||||||
typeof styleManager.getCockpitState === 'function'
|
typeof styleManager.getCockpitState === 'function'
|
||||||
? styleManager.getCockpitState()
|
? styleManager.getCockpitState()
|
||||||
: null,
|
: null,
|
||||||
controls:
|
controls:
|
||||||
typeof styleManager.getControlState === 'function'
|
typeof styleManager.getControlState === 'function'
|
||||||
? styleManager.getControlState()
|
? styleManager.getControlState()
|
||||||
@@ -3058,7 +3096,9 @@ function getCurrentViewState(
|
|||||||
source: layerSnapshot(layer).source,
|
source: layerSnapshot(layer).source,
|
||||||
lastUpdate: layerSnapshot(layer).lastUpdate,
|
lastUpdate: layerSnapshot(layer).lastUpdate,
|
||||||
})),
|
})),
|
||||||
feedProvenance: feedProvenanceEnvelope(layerSnapshots(dataManager.getAll()).filter((layer) => layer.enabled)),
|
feedProvenance: feedProvenanceEnvelope(
|
||||||
|
layerSnapshots(dataManager.getAll()).filter((layer) => layer.enabled),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3092,7 +3132,7 @@ async function getEntityContext(
|
|||||||
limit,
|
limit,
|
||||||
target: viewTarget,
|
target: viewTarget,
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
const scene = await scenePromise;
|
const scene = await scenePromise;
|
||||||
|
|
||||||
if ((scope === 'selected' || scope === 'auto') && selected) {
|
if ((scope === 'selected' || scope === 'auto') && selected) {
|
||||||
@@ -3178,14 +3218,14 @@ function aircraftProximityWindowForQuery(dataManager, args, result) {
|
|||||||
items: items
|
items: items
|
||||||
.slice(0, Math.round(clampNumber(args.limit, 1, 50, 12)))
|
.slice(0, Math.round(clampNumber(args.limit, 1, 50, 12)))
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
layerKey: item.layerKey,
|
layerKey: item.layerKey,
|
||||||
id: item.id,
|
id: item.id,
|
||||||
...(item.icao24 ? { icao24: item.icao24 } : {}),
|
...(item.icao24 ? { icao24: item.icao24 } : {}),
|
||||||
...(item.callsign ? { callsign: item.callsign } : {}),
|
...(item.callsign ? { callsign: item.callsign } : {}),
|
||||||
...(Number.isFinite(item.distance)
|
...(Number.isFinite(item.distance)
|
||||||
? { distanceKm: Math.round(item.distance / 100) / 10 }
|
? { distanceKm: Math.round(item.distance / 100) / 10 }
|
||||||
: {}),
|
: {}),
|
||||||
})),
|
})),
|
||||||
summary: { count },
|
summary: { count },
|
||||||
coverage: {
|
coverage: {
|
||||||
layersQueried: result?.coverage?.layersQueried || [],
|
layersQueried: result?.coverage?.layersQueried || [],
|
||||||
@@ -3283,15 +3323,15 @@ function visibleEntityContexts(
|
|||||||
insertNearestRecord(
|
insertNearestRecord(
|
||||||
nearbyRecords,
|
nearbyRecords,
|
||||||
{
|
{
|
||||||
record,
|
record,
|
||||||
distanceScore: target
|
distanceScore: target
|
||||||
? approximateCoordinateDistanceSq(
|
? approximateCoordinateDistanceSq(
|
||||||
targetLat,
|
targetLat,
|
||||||
targetLon,
|
targetLon,
|
||||||
record.latitude,
|
record.latitude,
|
||||||
record.longitude,
|
record.longitude,
|
||||||
)
|
)
|
||||||
: 0,
|
: 0,
|
||||||
},
|
},
|
||||||
VISIBLE_ENTITY_SHORTLIST,
|
VISIBLE_ENTITY_SHORTLIST,
|
||||||
);
|
);
|
||||||
@@ -3448,16 +3488,16 @@ async function getBasemapContext(
|
|||||||
const cachedNearbyPlaces =
|
const cachedNearbyPlaces =
|
||||||
shouldFetchNearbyPlaces(cameraHeightM) &&
|
shouldFetchNearbyPlaces(cameraHeightM) &&
|
||||||
nearbyPlacesCache.has(nearbyCacheKey)
|
nearbyPlacesCache.has(nearbyCacheKey)
|
||||||
? nearbyPlacesCache.get(nearbyCacheKey)
|
? nearbyPlacesCache.get(nearbyCacheKey)
|
||||||
: null;
|
: null;
|
||||||
const placePromise =
|
const placePromise =
|
||||||
shouldReverseGeocode(cameraHeightM) && !cachedPlace
|
shouldReverseGeocode(cameraHeightM) && !cachedPlace
|
||||||
? reverseGeocode(latitude, longitude, service)
|
? reverseGeocode(latitude, longitude, service)
|
||||||
: Promise.resolve(cachedPlace);
|
: Promise.resolve(cachedPlace);
|
||||||
const nearbyPlacesPromise =
|
const nearbyPlacesPromise =
|
||||||
shouldFetchNearbyPlaces(cameraHeightM) && !cachedNearbyPlaces
|
shouldFetchNearbyPlaces(cameraHeightM) && !cachedNearbyPlaces
|
||||||
? fetchNearbyPlaces(latitude, longitude, cameraHeightM, service)
|
? fetchNearbyPlaces(latitude, longitude, cameraHeightM, service)
|
||||||
: Promise.resolve(cachedNearbyPlaces);
|
: Promise.resolve(cachedNearbyPlaces);
|
||||||
const [viewportPlaces, resolvedPlace, resolvedNearbyPlaces] =
|
const [viewportPlaces, resolvedPlace, resolvedNearbyPlaces] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
resolveWithin(
|
resolveWithin(
|
||||||
@@ -3465,13 +3505,13 @@ async function getBasemapContext(
|
|||||||
BASEMAP_CONTEXT_WAIT_MS,
|
BASEMAP_CONTEXT_WAIT_MS,
|
||||||
cachedViewportPlaces,
|
cachedViewportPlaces,
|
||||||
),
|
),
|
||||||
resolveWithin(placePromise, BASEMAP_CONTEXT_WAIT_MS, cachedPlace),
|
resolveWithin(placePromise, BASEMAP_CONTEXT_WAIT_MS, cachedPlace),
|
||||||
resolveWithin(
|
resolveWithin(
|
||||||
nearbyPlacesPromise,
|
nearbyPlacesPromise,
|
||||||
BASEMAP_CONTEXT_WAIT_MS,
|
BASEMAP_CONTEXT_WAIT_MS,
|
||||||
cachedNearbyPlaces,
|
cachedNearbyPlaces,
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
const place = resolvedPlace || fallbackPlace;
|
const place = resolvedPlace || fallbackPlace;
|
||||||
const nearbyPlaces = resolvedNearbyPlaces || [];
|
const nearbyPlaces = resolvedNearbyPlaces || [];
|
||||||
return {
|
return {
|
||||||
@@ -3516,12 +3556,12 @@ function nearbyKnownLandmarks(latitude, longitude, cameraHeightM) {
|
|||||||
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return [];
|
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return [];
|
||||||
const maxDistanceKm =
|
const maxDistanceKm =
|
||||||
cameraHeightM <= 5000
|
cameraHeightM <= 5000
|
||||||
? 2
|
? 2
|
||||||
: cameraHeightM <= 50000
|
: cameraHeightM <= 50000
|
||||||
? 10
|
? 10
|
||||||
: cameraHeightM <= 250000
|
: cameraHeightM <= 250000
|
||||||
? 35
|
? 35
|
||||||
: 0;
|
: 0;
|
||||||
if (maxDistanceKm <= 0) return [];
|
if (maxDistanceKm <= 0) return [];
|
||||||
|
|
||||||
const matches = [];
|
const matches = [];
|
||||||
@@ -3752,9 +3792,9 @@ function inferCountry(latitude, longitude) {
|
|||||||
];
|
];
|
||||||
const region = regions.find(
|
const region = regions.find(
|
||||||
(item) =>
|
(item) =>
|
||||||
latitude >= item.south &&
|
latitude >= item.south &&
|
||||||
latitude <= item.north &&
|
latitude <= item.north &&
|
||||||
longitude >= item.west &&
|
longitude >= item.west &&
|
||||||
longitude <= item.east,
|
longitude <= item.east,
|
||||||
);
|
);
|
||||||
return region?.name || null;
|
return region?.name || null;
|
||||||
@@ -3802,18 +3842,18 @@ async function reverseGeocodeViewportSamples(samples, cameraHeightM, service) {
|
|||||||
sample.longitude,
|
sample.longitude,
|
||||||
service,
|
service,
|
||||||
);
|
);
|
||||||
if (!place) return null;
|
if (!place) return null;
|
||||||
return {
|
return {
|
||||||
latitude: sample.latitude,
|
latitude: sample.latitude,
|
||||||
longitude: sample.longitude,
|
longitude: sample.longitude,
|
||||||
formattedAddress: place.formattedAddress,
|
formattedAddress: place.formattedAddress,
|
||||||
locality: place.locality,
|
locality: place.locality,
|
||||||
region: place.region,
|
region: place.region,
|
||||||
country: place.country,
|
country: place.country,
|
||||||
types: place.types,
|
types: place.types,
|
||||||
labels: place.labels,
|
labels: place.labels,
|
||||||
streetLabels: place.streetLabels,
|
streetLabels: place.streetLabels,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
).filter(Boolean);
|
).filter(Boolean);
|
||||||
@@ -3834,21 +3874,21 @@ function viewportPlacesFromCache(samples, cameraHeightM, service) {
|
|||||||
const place = reverseGeocodeCache.get(
|
const place = reverseGeocodeCache.get(
|
||||||
reverseGeocodeKey(sample.latitude, sample.longitude, service),
|
reverseGeocodeKey(sample.latitude, sample.longitude, service),
|
||||||
);
|
);
|
||||||
if (!place) return [];
|
if (!place) return [];
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
latitude: sample.latitude,
|
latitude: sample.latitude,
|
||||||
longitude: sample.longitude,
|
longitude: sample.longitude,
|
||||||
formattedAddress: place.formattedAddress,
|
formattedAddress: place.formattedAddress,
|
||||||
locality: place.locality,
|
locality: place.locality,
|
||||||
region: place.region,
|
region: place.region,
|
||||||
country: place.country,
|
country: place.country,
|
||||||
types: place.types,
|
types: place.types,
|
||||||
labels: place.labels,
|
labels: place.labels,
|
||||||
streetLabels: place.streetLabels,
|
streetLabels: place.streetLabels,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
return summarizeViewportPlaces(places);
|
return summarizeViewportPlaces(places);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4008,12 +4048,12 @@ function summarizeEntity(viewer, entity, { includeProperties = false } = {}) {
|
|||||||
const tags = props.tags || {};
|
const tags = props.tags || {};
|
||||||
const label = cleanText(
|
const label = cleanText(
|
||||||
props.name ||
|
props.name ||
|
||||||
tags.name ||
|
tags.name ||
|
||||||
tags['name:en'] ||
|
tags['name:en'] ||
|
||||||
tags.official_name ||
|
tags.official_name ||
|
||||||
tags.operator ||
|
tags.operator ||
|
||||||
props.operator ||
|
props.operator ||
|
||||||
entity.name ||
|
entity.name ||
|
||||||
layerTitle(layerId),
|
layerTitle(layerId),
|
||||||
);
|
);
|
||||||
const position =
|
const position =
|
||||||
@@ -4073,8 +4113,8 @@ function unwrapProperties(value) {
|
|||||||
for (const [key, entry] of Object.entries(value)) {
|
for (const [key, entry] of Object.entries(value)) {
|
||||||
out[key] =
|
out[key] =
|
||||||
entry && typeof entry.getValue === 'function'
|
entry && typeof entry.getValue === 'function'
|
||||||
? unwrapProperties(entry.getValue(Cesium.JulianDate.now()))
|
? unwrapProperties(entry.getValue(Cesium.JulianDate.now()))
|
||||||
: unwrapProperties(entry);
|
: unwrapProperties(entry);
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -4189,10 +4229,15 @@ function analystProviders(
|
|||||||
const row = dataManager.getAll?.().find((layer) => layer.id === layerKey);
|
const row = dataManager.getAll?.().find((layer) => layer.id === layerKey);
|
||||||
if (row) return layerSnapshot(row);
|
if (row) return layerSnapshot(row);
|
||||||
const module = dataManager.layers?.get(layerKey)?.module;
|
const module = dataManager.layers?.get(layerKey)?.module;
|
||||||
return layerSnapshot({ id: layerKey, enabled: dataManager.isEnabled?.(layerKey), stats: module?.getStats?.() || {} });
|
return layerSnapshot({
|
||||||
|
id: layerKey,
|
||||||
|
enabled: dataManager.isEnabled?.(layerKey),
|
||||||
|
stats: module?.getStats?.() || {},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
getRecordCoverage(layerKey, rows) {
|
getRecordCoverage(layerKey, rows) {
|
||||||
if (!['satellites', 'local-datacenters', 'local-dams'].includes(layerKey)) return null;
|
if (!['satellites', 'local-datacenters', 'local-dams'].includes(layerKey))
|
||||||
|
return null;
|
||||||
const module = dataManager.layers.get(layerKey)?.module;
|
const module = dataManager.layers.get(layerKey)?.module;
|
||||||
const loaded = module?.getStats?.().count;
|
const loaded = module?.getStats?.().count;
|
||||||
return {
|
return {
|
||||||
@@ -4271,7 +4316,36 @@ async function runAnalystQuery(
|
|||||||
// model burned the turn on retries (owner field session 2026-08-21, 23:48).
|
// model burned the turn on retries (owner field session 2026-08-21, 23:48).
|
||||||
const items = result.items.map((r) => {
|
const items = result.items.map((r) => {
|
||||||
const compact = { layerKey: r.layerKey, id: r.id };
|
const compact = { layerKey: r.layerKey, id: r.id };
|
||||||
for (const k of ['icao24', 'mmsi', 'registration', 'label', 'callsign', 'name', 'altitudeM', 'speedMps', 'speedKts', 'frp', 'magnitude', 'shipType', 'destination', 'operator', 'routeOrigin', 'routeDestination', 'aircraftClass', 'military', 'onGround', 'distanceKm', 'confidence', 'place', 'noradId', 'satelliteClass', 'group', 'river', 'output', 'capacity']) {
|
for (const k of [
|
||||||
|
'icao24',
|
||||||
|
'mmsi',
|
||||||
|
'registration',
|
||||||
|
'label',
|
||||||
|
'callsign',
|
||||||
|
'name',
|
||||||
|
'altitudeM',
|
||||||
|
'speedMps',
|
||||||
|
'speedKts',
|
||||||
|
'frp',
|
||||||
|
'magnitude',
|
||||||
|
'shipType',
|
||||||
|
'destination',
|
||||||
|
'operator',
|
||||||
|
'routeOrigin',
|
||||||
|
'routeDestination',
|
||||||
|
'aircraftClass',
|
||||||
|
'military',
|
||||||
|
'onGround',
|
||||||
|
'distanceKm',
|
||||||
|
'confidence',
|
||||||
|
'place',
|
||||||
|
'noradId',
|
||||||
|
'satelliteClass',
|
||||||
|
'group',
|
||||||
|
'river',
|
||||||
|
'output',
|
||||||
|
'capacity',
|
||||||
|
]) {
|
||||||
if (r[k] !== null && r[k] !== undefined) compact[k] = r[k];
|
if (r[k] !== null && r[k] !== undefined) compact[k] = r[k];
|
||||||
}
|
}
|
||||||
return compact;
|
return compact;
|
||||||
@@ -4305,10 +4379,22 @@ async function runAnalystQuery(
|
|||||||
// differ. The generic record/scope engine still owns explicit regions and
|
// differ. The generic record/scope engine still owns explicit regions and
|
||||||
// arbitrary points — only "how many aircraft around <this contact>" is
|
// arbitrary points — only "how many aircraft around <this contact>" is
|
||||||
// unified, because that is the question the panel is already answering.
|
// unified, because that is the question the panel is already answering.
|
||||||
const entityWindow = aircraftProximityWindowForQuery(dataManager, args, result);
|
const entityWindow = aircraftProximityWindowForQuery(
|
||||||
|
dataManager,
|
||||||
|
args,
|
||||||
|
result,
|
||||||
|
);
|
||||||
if (entityWindow) {
|
if (entityWindow) {
|
||||||
const provenance = feedProvenanceEnvelope(layerSnapshots(dataManager.getAll?.() || []).filter((layer) => layer.enabled && ['flights', 'military'].includes(layer.id)));
|
const provenance = feedProvenanceEnvelope(
|
||||||
return { ...entityWindow, feedProvenance: provenance, feedState: provenance.overall };
|
layerSnapshots(dataManager.getAll?.() || []).filter(
|
||||||
|
(layer) => layer.enabled && ['flights', 'military'].includes(layer.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...entityWindow,
|
||||||
|
feedProvenance: provenance,
|
||||||
|
feedState: provenance.overall,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const contactsWindow = activeContactsWindow(dataManager);
|
const contactsWindow = activeContactsWindow(dataManager);
|
||||||
@@ -4336,7 +4422,7 @@ async function runAnalystQuery(
|
|||||||
`${contactsWindow.radiusKm} km of ${contactsWindow.centeredOn}, and that is the answer to a bare ` +
|
`${contactsWindow.radiusKm} km of ${contactsWindow.centeredOn}, and that is the answer to a bare ` +
|
||||||
`"how many aircraft are nearby". This query measured something else — ${result.count} ${result.scopeLabel}. ` +
|
`"how many aircraft are nearby". This query measured something else — ${result.count} ${result.scopeLabel}. ` +
|
||||||
'Give this one only if the operator asked about that specific area, and name both scopes if you give both.'
|
'Give this one only if the operator asked about that specific area, and name both scopes if you give both.'
|
||||||
: null;
|
: null;
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
action: 'analyst_query',
|
action: 'analyst_query',
|
||||||
@@ -4356,10 +4442,10 @@ async function runAnalystQuery(
|
|||||||
// missed inside a nested shape.
|
// missed inside a nested shape.
|
||||||
...(contactsWindow && aircraftQueried
|
...(contactsWindow && aircraftQueried
|
||||||
? {
|
? {
|
||||||
contactsWindow,
|
contactsWindow,
|
||||||
contactsWindowCount: windowAircraft,
|
contactsWindowCount: windowAircraft,
|
||||||
contactsWindowSubject: contactsWindow.centeredOn || null,
|
contactsWindowSubject: contactsWindow.centeredOn || null,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...(countsReconciliation ? { countsReconciliation } : {}),
|
...(countsReconciliation ? { countsReconciliation } : {}),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user