import { interruptGlobeRotation } from '../cameraVerbs.js'; import { layerSnapshot, feedProvenanceEnvelope, } from '../data/layerSnapshot.js'; import { readLayerLifecycleSummary } from './layerSummary.js'; import { SPEECH_BUILDERS, attachVoiceResult } from './speech.js'; import { resolveDeicticArgs } from './deixis.js'; export { readLayerLifecycleSummary } from './layerSummary.js'; import { defaultGeospatial } from '../search/defaults.js'; import { msg } from '../i18n/index.js'; import * as Cesium from 'cesium'; import { CITY_POIS } from '../cityPois.js'; import { greatCircleKm } from '../geoDistance.js'; import { hudViewportSamples, placeLookupView, placeLookupViewChanged, } from '../hudPlaceLookup.js'; import { findPoiByName, flyToGlobeView, flyToLandmark, flyToPOI, flyToPresetLocation, GLOBE_VIEW, searchAndFlyTo, } from '../locations.js'; import { getContextStore, getSelectedEntityContext, isContextRecordActive, } from '../data/contextStore.js'; import { CCTV_FOCUS_RESULT } from '../layers/cctv/index.js'; import { createAnalystEngine, normalizeAnalystSpec, selectAnalystRows, } from '../data/analystEngine.js'; import { voiceLayer, voiceLayerAliasMap } from './layerManifest.js'; import { layerFeedState } from '../data/feedState.js'; import { initCameraVerbs, moveCamera, flyRoute, interruptCameraMotion, adjustOrbitRange, } from '../cameraVerbs.js'; import * as defaultFloorServices from '../data/groundFloor.js'; import { isPickedWorldPosition } from '../data/scenePick.js'; import { unavailablePlaceSearch } from '../search/placeSearch.js'; import * as defaultAnnotationResolver from '../annotations/annotationResolver.js'; import { normalizeRadioCountryInput } from '../data/radioCountry.js'; import { TR3B_CLASS } from '../data/tr3bRegistry.js'; import { getWorldPacket, withContextModeVocabulary, activeContactsWindow, TRACKABLE_FAMILIES, } from '../agent/worldPacket.js'; const ALLOWED_STYLES = new Set([ 'normal', 'retro', 'surveillance', 'thermal', 'anime', 'noir', ]); const PANEL_ALIASES = new Map([ ['data', 'data-panel'], ['data layers', 'data-panel'], ['layers', 'data-panel'], ['layer menu', 'data-panel'], ['data layer menu', 'data-panel'], ['locations', 'location-bar'], ['location', 'location-bar'], ['styles', 'control-panel'], ['filters', 'control-panel'], ['visual styles', 'control-panel'], ['visual preset', 'control-panel'], ['visual presets', 'control-panel'], ['provider settings', 'provider-settings'], ['cctv', 'cctv-panel'], ['cameras', 'cctv-panel'], ['voice settings', 'voice-settings'], ['voice-settings', 'voice-settings'], ['voice preview', 'voice-preview'], ['voice-preview', 'voice-preview'], ['voice control', 'voice-preview'], ['voice controls', 'voice-preview'], ['radio', 'radio-panel'], ['internet radio', 'radio-panel'], ['radio stations', 'radio-panel'], ['context', 'global-context-panel'], ['context panel', 'global-context-panel'], ['global context', 'global-context-panel'], ['right context', 'global-context-panel'], ['context right panel', 'global-context-panel'], ['scenes', 'scene-panel'], ['scene', 'scene-panel'], ['post processing', 'pp-toggles'], ['hud controls', 'pp-toggles'], ['map stack', 'control-panel'], ['stack', 'control-panel'], ['basemap', 'control-panel'], ['map sources', 'control-panel'], ['sources', 'control-panel'], ['weather', 'weather-panel'], ['weather panel', 'weather-panel'], ['recent imagery', 'recent-imagery-panel'], ['imagery panel', 'recent-imagery-panel'], ]); const PANEL_IDS = new Set([ 'data-panel', 'location-bar', 'control-panel', 'cctv-panel', 'radio-panel', 'global-context-panel', 'scene-panel', 'pp-toggles', 'weather-panel', 'recent-imagery-panel', 'provider-settings', ]); const CONTEXT_MODE_ALIASES = new Map([ ['off', 'off'], ['none', 'off'], ['clear', 'off'], ['contacts', 'flights'], ['contact', 'flights'], ['flights', 'flights'], ['space missions', 'space-missions'], ['space-mission', 'space-missions'], ['space mission', 'space-missions'], ['space-missions', 'space-missions'], ['missions', 'space-missions'], ]); const COCKPIT_ACTION_ALIASES = new Map([ ['next', 'next'], ['previous', 'previous'], ['prev', 'previous'], ['enter', 'enter'], ['exit', 'exit'], ['status', 'status'], ['state', 'status'], ['next military', 'next'], ['next military aircraft', 'next'], ['next helicopter', 'next'], ['next closest', 'next'], ['next closest helicopter', 'next'], ['next closest military', 'next'], ['go to next', 'next'], ]); const COCKPIT_TARGET_LAYERS = new Set([ 'flights', 'military', 'ais-live-vessels', 'military-installations', ]); // Spoken layer names come from the voice layer manifest, the same source the // Realtime tool enums are generated from. const LAYER_ALIASES = voiceLayerAliasMap(); const CITY_ALIASES = new Map([ ['new york', 'nyc'], ['new york city', 'nyc'], ['san francisco', 'sf'], ['washington', 'dc'], ['washington dc', 'dc'], ['washington d.c.', 'dc'], ]); // Basemap stack vocabulary. Switching requires an explicit stack name // ("Bing aerial", "road map", "OSM", "Google 3D") — any "satellite(s)" // phrasing ALWAYS means the satellites DATA LAYER, never a basemap; the // session instructions carry the decision table. // // Road phrasings resolve to OSM, the one shipped road basemap. Every alias // must name a live `MAP_STACKS` id: an alias for a retired stack would resolve // cleanly and then fail at the controller with "Unknown map stack", which reads // to the operator as a broken command rather than a retired source. const STACK_ALIASES = new Map([ ['photoreal', 'photoreal'], ['google 3d', 'photoreal'], ['google', 'photoreal'], ['3d', 'photoreal'], ['photorealistic', 'photoreal'], ['bing-aerial', 'bing-aerial'], ['bing aerial', 'bing-aerial'], ['bing-labels', 'bing-labels'], ['bing labels', 'bing-labels'], ['labels', 'bing-labels'], ['aerial with labels', 'bing-labels'], ['esri-imagery', 'esri-imagery'], ['esri', 'esri-imagery'], ['esri imagery', 'esri-imagery'], ['esri satellite', 'esri-imagery'], ['osm', 'osm'], ['openstreetmap', 'osm'], ['open street map', 'osm'], ['road', 'osm'], ['roads', 'osm'], ['road map', 'osm'], ]); const FRAME_TARGETS = new Map([ ['flights', 'flights'], ['planes', 'flights'], ['aircraft', 'flights'], ['military', 'military'], ['military flights', 'military'], ['satellites', 'satellites'], ['vessels', 'ais-live-vessels'], ['ships', 'ais-live-vessels'], ]); const serviceCaches = new WeakMap(); function cachesFor(service) { service.signal?.throwIfAborted(); let caches = serviceCaches.get(service); if (!caches) { caches = { reverseGeocodeCache: new Map(), reverseGeocodeInFlight: new Map(), nearbyPlacesCache: new Map(), nearbyPlacesInFlight: new Map(), }; serviceCaches.set(service, caches); service.signal?.addEventListener( 'abort', () => { for (const cache of Object.values(caches)) cache.clear(); }, { once: true }, ); } return caches; } const VISIBLE_ENTITY_SHORTLIST = 64; const BASEMAP_CONTEXT_WAIT_MS = 1500; const viewTargetCache = new WeakMap(); /** Create application actions over the supplied scene and services. */ export function createGevActionRunner({ viewer, styleManager, dataManager, sceneDirector = null, annotations = null, placeSearch = unavailablePlaceSearch, floorServices = defaultFloorServices, annotationResolver = defaultAnnotationResolver, searchNavigation = searchAndFlyTo, speechBuilders = null, deixis = null, readVoicePanelOperation = null, }) { // Voice enable times and analyst follow-up memory belong to this runner. // Speech builders turn results into say/display envelopes; callers extend // them (e.g. an analyst_query builder) without replacing the defaults. const builders = speechBuilders ? { ...SPEECH_BUILDERS, ...speechBuilders } : SPEECH_BUILDERS; const _layerEnabledAt = new Map(); let analystEngine; const resolveRegionRing = (name, signal) => annotationResolver.resolveRegionRingForQuery(name, signal, placeSearch); const ensureAnalystEngine = () => (analystEngine ||= createAnalystEngine( analystProviders(viewer, dataManager, { placeSearch, resolveRegionRing, isWarming: (layerKey) => layerIsWarming(_layerEnabledAt, layerKey), }), )); installViewTargetPrewarm(viewer); initCameraVerbs(viewer, getViewTargetCartesian, { isCockpitActive: () => Boolean(styleManager?.getCockpitState?.()?.active), }); // Nested calls (select_nearest_aircraft composing layer + flight) use the // raw runner; only the outer call gets a voice envelope. const voiceUnavailable = (action) => ({ ok: false, action, applied: false, unavailable: true, error: 'Voice panel controls are unavailable in this host.', say: 'Voice panel controls are unavailable in this host.', }); async function showVoicePanel( view, open, options, operation = readVoicePanelOperation?.(), ) { const current = () => !options.signal?.aborted && (!options.isCurrent || options.isCurrent()) && operation?.isCurrent(); if (!operation) return voiceUnavailable('set_panel_open'); if (!current()) return { ok: false, action: 'set_panel_open', applied: false, cancelled: true, }; let exit = null; if (open) { if (typeof styleManager?.getCockpitState !== 'function') return voiceUnavailable('set_panel_open'); if (styleManager.getCockpitState()?.active) { if (typeof styleManager.controlCockpit !== 'function') return voiceUnavailable('set_panel_open'); exit = await styleManager.controlCockpit('exit'); if (styleManager.getCockpitState()?.active) return { ok: false, action: 'set_panel_open', applied: false, exit, error: exit?.error || 'Cockpit did not exit.', }; } } if (!current()) return { ok: false, action: 'set_panel_open', applied: false, cancelled: true, exit, cockpitExited: Boolean(exit && !styleManager.getCockpitState()?.active), say: exit && !styleManager.getCockpitState()?.active ? 'Cockpit exited; the voice panel was left as you last set it.' : 'The voice panel was left as you last set it.', }; const result = operation.show(view, open); return { ...result, action: 'set_panel_open', panelId: view === 'settings' ? 'voice-settings' : 'voice-preview', exit, state: styleManager?.getCockpitState?.() ?? null, say: !result.ok ? 'The voice panel was not changed.' : result.settingsOpen ? 'Voice Settings is open.' : result.panelOpen ? result.previewEnabled ? 'Voice Preview is open.' : 'Voice control is open; Preview remains off.' : 'The voice panel is closed.', }; } const runGevAction = async function runGevAction( name, rawArgs = {}, runOptions = {}, ) { const args = rawArgs && typeof rawArgs === 'object' ? rawArgs : {}; const current = () => !runOptions.signal?.aborted && (typeof runOptions.isCurrent !== 'function' || runOptions.isCurrent()); // Step reports for voice narration and the voice card; never fatal. const reportProgress = (step, label = '') => { try { runOptions.progress?.({ step, label: String(label || '') }); } catch { /* Progress is advisory. */ } }; // Navigation tools interrupt any continuous camera motion (spec §1.1) — // checked FIRST because each handler returns. if (name === 'zoom_to_globe') { interruptCameraMotion(`nav:${name}`); } // Explicit navigation while TRACKING supersedes the follow camera — // otherwise the tracker drags the view back and "I flew there but can't // do anything" (field finding). track_entity manages its own handoff. if (name === 'zoom_to_globe' && viewer.trackedEntity) { stopAllTracking(viewer, dataManager); } // Zoom during an active orbit adjusts the orbit RADIUS (spiral in/out) — // a straight camera move would be snapped back by the per-frame lookAt. if (name === 'adjust_camera_zoom') { interruptGlobeRotation('voice-zoom-intent'); const zoomOut = String(args.direction || '').toLowerCase() === 'out'; const amt = String(args.amount || 'medium').toLowerCase(); const factor = { little: 1.25, medium: 1.6, lot: 2.4 }[amt] || 1.6; if (adjustOrbitRange(zoomOut ? factor : 1 / factor)) { return { ok: true, action: 'adjust_camera_zoom', direction: zoomOut ? 'out' : 'in', amount: amt, orbitRadiusAdjusted: true, }; } } if (name === 'set_layer_visibility') { const layerId = normalizeLayerId(args.layerId); if (!layerId) { throw new Error(`Unknown data layer: ${args.layerId || 'missing'}`); } if (!dataManager.layers.has(layerId)) { if (layerId === 'radio') { return { ok: false, action: 'set_layer_visibility', layerId, error: 'Radio layer unavailable', ...readLayerLifecycleSummary(dataManager, layerId), }; } throw new Error(`Unknown data layer: ${args.layerId || 'missing'}`); } const enabled = Boolean(args.enabled); if (enabled) { const pending = dataManager.getAll?.().find((l) => l.id === layerId); reportProgress('layer', pending?.name || layerId); } const changeOptions = { origin: 'voice' }; if (runOptions.signal) changeOptions.signal = runOptions.signal; let changed = false; let changeError = null; let intentOutcome = null; try { if (typeof dataManager._setEnabledWithIntent === 'function') { const intent = dataManager._setEnabledWithIntent( layerId, enabled, changeOptions, ); changed = await intent.promise; if (Number.isInteger(intent.intentEpoch)) { intentOutcome = await dataManager._waitForVisibilityIntent?.( layerId, intent.intentEpoch, ); } } else { changed = await dataManager.setEnabled( layerId, enabled, changeOptions, ); } if (layerId === 'rocket-launches' || layerId === 'satellites') { await styleManager?._waitForContextLayerSettlement?.(); } } catch (error) { changeError = error; } const lifecycleSummary = readLayerLifecycleSummary(dataManager, layerId); if ( intentOutcome?.succeeded === false && intentOutcome.cancellationReason ) { return { ok: false, action: 'set_layer_visibility', layerId, cancelled: true, phase: intentOutcome.phase, cancellationReason: intentOutcome.cancellationReason, successorIntentEpoch: intentOutcome.successorIntentEpoch, successorEnabled: intentOutcome.successorEnabled, successorOrigin: intentOutcome.successorOrigin, ...lifecycleSummary, }; } const intentCommitted = intentOutcome?.succeeded === true; const current = !runOptions.signal?.aborted && (typeof runOptions.isCurrent !== 'function' || runOptions.isCurrent()); if (!current && !intentCommitted) { return { ok: false, action: 'set_layer_visibility', layerId, cancelled: true, error: 'Layer request was superseded by a newer voice turn', ...lifecycleSummary, }; } const settledEnabled = lifecycleSummary.enabled; const lifecycleSettled = lifecycleSummary.lifecycleState === (enabled ? 'enabled' : 'disabled') && !lifecycleSummary.lifecycleUncertain; if (changeError) { return { ok: false, action: 'set_layer_visibility', layerId, error: changeError?.message || `Could not ${enabled ? 'enable' : 'disable'} the requested layer`, ...lifecycleSummary, }; } if ( changed === false || settledEnabled !== enabled || !lifecycleSettled ) { return { ok: false, action: 'set_layer_visibility', layerId, error: `Could not ${enabled ? 'enable' : 'disable'} the requested layer`, ...lifecycleSummary, }; } if (enabled) _layerEnabledAt.set(layerId, Date.now()); const layer = dataManager.getAll().find((item) => item.id === layerId); const snapshot = layer && enabled ? layerSnapshot(layer) : null; return { ok: true, action: 'set_layer_visibility', layerId, label: layer?.name || layerId, ...lifecycleSummary, ...(snapshot?.feedState ? { feedState: snapshot.feedState } : {}), ...(snapshot?.source ? { source: snapshot.source } : {}), }; } if (name === 'select_nearest_aircraft') { const layerId = normalizeLayerId(args.layerId || 'flights'); if (!['flights', 'military'].includes(layerId)) { return { ok: false, action: 'select_nearest_aircraft', error: 'Nearest-aircraft selection supports Flights or Military Flights only', }; } const hasLocationId = Boolean(String(args.locationId || '').trim()); const hasLocationQuery = Boolean(String(args.locationQuery || '').trim()); const hasCoordinates = args.latitude != null && args.longitude != null && Number.isFinite(Number(args.latitude)) && Number.isFinite(Number(args.longitude)); if (!hasLocationId && !hasLocationQuery && !hasCoordinates) { return { ok: false, action: 'select_nearest_aircraft', stage: 'location', error: 'Nearest-aircraft selection needs a preset, place name, or latitude and longitude', }; } const locationArgs = { waitForArrival: true, ...(args.locationId ? { locationId: args.locationId } : {}), ...(args.locationQuery ? { query: args.locationQuery } : {}), ...(hasCoordinates ? { latitude: Number(args.latitude), longitude: Number(args.longitude), } : {}), }; const layer = await runGevAction( 'set_layer_visibility', { layerId, enabled: true, }, runOptions, ); if (layer?.ok !== true || !current()) { return { ok: false, action: 'select_nearest_aircraft', stage: 'layer', cancelled: !current() || Boolean(layer?.cancelled), error: layer?.error || `${layerId} could not be enabled`, layer, }; } const location = await runGevAction( 'fly_to_location', locationArgs, runOptions, ); if (location?.ok !== true || !current()) { return { ok: false, action: 'select_nearest_aircraft', stage: 'location', cancelled: !current() || Boolean(location?.cancelled), error: location?.error || `Could not arrive at ${location?.label || args.locationQuery || args.locationId || 'the requested place'}`, location, layer, }; } const layerModule = dataManager.layers.get(layerId)?.module || null; let refreshed = false; reportProgress('refresh', location.label); try { if (typeof dataManager.refreshLayer === 'function') { refreshed = await dataManager.refreshLayer(layerId, { signal: runOptions.signal, }); } else if (typeof layerModule?.update === 'function') { refreshed = (await layerModule.update(viewer, { signal: runOptions.signal, })) !== false; } } catch { refreshed = false; } if (!refreshed || !current()) { return { ok: false, action: 'select_nearest_aircraft', stage: 'refresh', cancelled: !current(), error: !current() ? 'Nearest-aircraft refresh was cancelled' : `${layer.label || layerId} is enabled, but its destination refresh did not complete`, location, layer, }; } const stats = layerModule?.getStats?.() || {}; const source = String(stats.source || layerModule?.source || '').trim() || null; const feed = { state: layerFeedState({ ...stats, source }), source, count: Number.isFinite(Number(stats.count)) ? Number(stats.count) : null, }; reportProgress('nearest', location.label); const nearest = await createAnalystEngine( analystProviders(viewer, dataManager, { recordLimitByLayer: { [layerId]: Number.MAX_SAFE_INTEGER }, placeSearch, resolveRegionRing, }), ).query({ layers: [layerId], scope: { kind: 'view' }, filters: [{ field: 'onGround', op: 'eq', value: false }], sortBy: 'distance', sortDir: 'asc', limit: 1, }); const aircraft = nearest?.items?.[0] || null; if (!aircraft || !current()) { return { ok: false, action: 'select_nearest_aircraft', stage: 'nearest', cancelled: !current(), error: !current() ? 'Nearest-aircraft selection was cancelled' : feed.state === 'unavailable' ? `${layer.label || layerId} is enabled, but ${feed.source || 'its aircraft feed'} is unavailable` : `${layer.label || layerId} is enabled${feed.state === 'fallback' ? ` on the ${feed.source || 'fallback'} feed` : ''}, but no airborne aircraft is loaded in the ${location.label || 'destination'} view yet`, location, layer, feed, count: nearest?.count || 0, }; } const stableAircraftId = aircraft.icao24 || aircraft.id; const selection = await trackEntity(viewer, dataManager, styleManager, { query: stableAircraftId, layerId, }); if (selection?.ok !== true) { return { ok: false, action: 'select_nearest_aircraft', stage: 'selection', error: selection?.error || 'The nearest airborne aircraft could not be selected', location, layer, feed, selection, }; } return { ok: true, action: 'select_nearest_aircraft', location: location.label, layerId, label: selection.label, feed, aircraft: { id: stableAircraftId, callsign: aircraft.callsign || null, altitudeM: aircraft.altitudeM ?? null, distanceKm: aircraft.distanceKm ?? null, onGround: false, }, }; } if (name === 'set_visual_style') { const style = normalizeStyle(args.style); if (!style) throw new Error(`Unknown visual style: ${args.style || 'missing'}`); styleManager.setStyle(style, { userInitiated: true }); return { ok: true, action: 'set_visual_style', style }; } if (name === 'set_voice_inactivity_timeout') { const operation = readVoicePanelOperation?.(); if (!operation) return voiceUnavailable(name); if (!current() || !operation.isCurrent()) return { ok: false, action: name, applied: false, cancelled: true }; const result = operation.setMinutes(args.minutes); const duration = result.minutes === 0 ? 'disabled' : `${result.minutes} minutes`; return { ...result, action: name, say: !result.applied ? 'Voice inactivity was not changed.' : result.persisted ? `Voice inactivity is ${duration}, saved in this browser.` : `Voice inactivity is ${duration} for now, but could not be saved in this browser.`, }; } if (name === 'set_panel_open') { const panelId = normalizePanelId(args.panelId || args.panel); if (!panelId) throw new Error( `Unknown panel: ${args.panelId || args.panel || 'missing'}`, ); const open = args.open !== false; if (panelId === 'voice-settings' || panelId === 'voice-preview') return showVoicePanel( panelId === 'voice-settings' ? 'settings' : 'preview', open, runOptions, ); if (panelId === 'provider-settings') { if (!current()) return { ok: false, action: name, panelId, applied: false, cancelled: true, }; const doc = globalThis.document; const panel = doc?.getElementById?.('key-setup'); const button = open ? doc?.getElementById?.('key-setup-chip') : panel?.querySelector?.('[data-key-setup-close]'); const concealed = open && [ 'ui-clean-view', 'recording-mode', 'cockpit-mode', 'scene-playback-mode', ].some((mode) => doc?.body?.classList?.contains(mode)); if ( concealed || panel?.dataset?.ready !== 'true' || typeof button?.click !== 'function' ) { const error = concealed ? 'Provider Settings is hidden in the current view. Leave that view before opening it.' : 'Provider Settings is unavailable in this runtime.'; return { ok: false, action: name, panelId, applied: false, unavailable: true, error, say: error, }; } button.click(); // Opening unhides immediately; closing removes visible before its fade ends. const applied = open ? !panel.hidden : !panel.classList.contains('visible'); const say = applied ? `Provider Settings is ${open ? 'open' : 'closed'}.` : 'Provider Settings did not change.'; return { ok: applied, action: name, panelId, applied, open: applied ? open : !panel.hidden, say, ...(!applied ? { error: say } : {}), }; } setPanelOpen(styleManager, panelId, open); return { ok: true, action: 'set_panel_open', panelId, open }; } if (name === 'set_context_mode') { if (!styleManager?.setContextMode) { return { ok: false, action: 'set_context_mode', error: 'Context mode control unavailable', }; } const mode = normalizeContextMode(args.mode || args.contextMode); if ( mode === null && args.mode != null && String(args.mode || '').trim() !== 'off' ) { return { ok: false, action: 'set_context_mode', error: `Unknown context mode: ${args.mode || 'missing'}`, }; } const cancellationState = () => withContextModeVocabulary( typeof styleManager.getContextModeState === 'function' ? styleManager.getContextModeState() : {}, ); if (!current()) { return { ok: false, action: 'set_context_mode', cancelled: true, error: 'Context request was cancelled before it could run', ...cancellationState(), }; } if (mode && mode !== 'off') { setPanelOpen(styleManager, 'global-context-panel', true); } const result = await styleManager.setContextMode( mode === 'off' ? null : mode, { signal: runOptions.signal, isCurrent: runOptions.isCurrent, }, ); if (!current() && result?.ok !== true) { return { ...withContextModeVocabulary(result), ok: false, action: 'set_context_mode', cancelled: true, error: result?.error || 'Context request was cancelled before it completed', ...cancellationState(), }; } const contactsWindow = ['contacts', 'flights'].includes(mode) ? activeContactsWindow(dataManager) : null; return { ...withContextModeVocabulary(result), ...(contactsWindow ? { contactsWindow } : {}), }; } if (name === 'control_cockpit') { if (!styleManager?.controlCockpit) { return { ok: false, action: 'control_cockpit', error: 'Cockpit control unavailable', }; } const rawAction = args.action || args.command; const action = normalizeCockpitAction(rawAction); const notificationToken = args.notificationToken || null; if (!action) { return { ok: false, action: 'control_cockpit', error: `Unknown cockpit action: ${args.action || args.command || 'missing'}`, }; } if (args.voicePanel !== undefined) { if ( action !== 'exit' || !['preview', 'settings'].includes(args.voicePanel) ) return { ok: false, action: 'control_cockpit', applied: false, error: 'Voice panel reveal requires exit and a valid view.', }; const result = await showVoicePanel(args.voicePanel, true, runOptions); return { ...result, action: 'control_cockpit', cockpitAction: 'exit', voicePanel: args.voicePanel, }; } const inferred = normalizeCockpitNavigationHints(rawAction); const targetLayer = normalizeCockpitTargetLayer( args.targetLayer || inferred.targetLayer || args.layer || args.layerId, ); const aircraftClass = normalizeAircraftClassFilter( args.aircraftClass || inferred.aircraftClass || args.type || args.filterType, ); let contextChangedForEntry = false; let priorContextMode = null; let rollbackTarget = null; if ( action === 'enter' && typeof styleManager.setContextMode === 'function' ) { if (!current()) { return { ok: false, action: 'control_cockpit', cancelled: true, error: 'Cockpit entry was cancelled before it could run', state: styleManager.getCockpitState?.() || null, }; } rollbackTarget = styleManager.getAircraftTrackingTarget?.() || null; const contextState = typeof styleManager.getContextModeState === 'function' ? styleManager.getContextModeState() : {}; priorContextMode = contextState?.mode || null; const contactsReady = contextState?.mode === 'flights' && contextState?.active !== false && contextState?.changing !== true; if (!contactsReady) { const contextResult = await styleManager.setContextMode('flights', { signal: runOptions.signal, isCurrent: runOptions.isCurrent, // Cockpit entry establishes Contacts as its own precondition. That // is internal choreography, not an operator Context request, so it // must stay inert: claiming here would cancel a pending shared // style/detection restore the operator never overrode. claimVisualAuthority: false, }); contextChangedForEntry = contextResult?.ok === true; if (contextResult?.ok !== true || !current()) { const contextRollback = contextChangedForEntry ? await styleManager.setContextMode(priorContextMode, { claimVisualAuthority: false, }) : null; return { ok: false, action: 'control_cockpit', cancelled: !current() || Boolean(contextResult?.cancelled), error: contextResult?.error || 'Contacts context could not be established for Cockpit entry', context: contextResult ? withContextModeVocabulary(contextResult) : null, contextRollback: withContextModeVocabulary(contextRollback), state: styleManager.getCockpitState?.() || null, }; } } } // Contacts activation can adopt a newer explicit aircraft selection. // Sample only after that transaction settles so an older voice snapshot // cannot overwrite the operator's newer choice. const selectedTarget = action === 'enter' ? selectedCockpitTarget(dataManager) : null; let cockpitResult; try { cockpitResult = await styleManager.controlCockpit(action, { notificationToken, targetLayer, aircraftClass, selectedTarget, rollbackTarget, }); } catch (error) { cockpitResult = { ok: false, action: 'control_cockpit', error: error instanceof Error ? error.message : String(error), state: styleManager.getCockpitState?.() || null, }; } if ( action === 'enter' && cockpitResult?.ok !== true && contextChangedForEntry ) { const contextRollback = await styleManager.setContextMode( priorContextMode, { // Undoing this action's own precondition — still choreography. claimVisualAuthority: false, ...(current() ? { signal: runOptions.signal, isCurrent: runOptions.isCurrent, } : {}), }, ); return { ...cockpitResult, contextRollback: withContextModeVocabulary(contextRollback), }; } return cockpitResult; } if (name === 'show_data_layers_menu') { const layerId = normalizeLayerId(args.layerId || args.layer); setPanelOpen(styleManager, 'data-panel', true); const focusedLayer = layerId && dataManager.layers.has(layerId) ? focusDataLayerRow(layerId) : null; return { ok: true, action: 'show_data_layers_menu', panelId: 'data-panel', focusedLayer, layers: dataManager .getAll() .filter((layer) => layer.showInTogglePanel !== false) .map((layer) => ({ id: layer.id, name: layer.name, enabled: layer.enabled, count: layer.stats?.count || 0, })), }; } if (name === 'fly_to_location') { // A referenced contact moves: locate it now, not from the old list. if (args.entity?.layerId && args.entity?.id) { const located = locateLayerEntity(dataManager, args.entity); if (!located) { return { ok: false, action: 'fly_to_location', error: `${args.entity.label || args.entity.id} is no longer loaded`, }; } const { entity: _entity, ...rest } = args; return runGevAction( name, { ...rest, latitude: located.lat, longitude: located.lon }, runOptions, ); } return flyToRequestedLocation(viewer, args, { onProgress: reportProgress, placeSearch, searchNavigation, signal: runOptions.signal, runImmediate: typeof styleManager?.runImmediateLocationNavigation === 'function' ? (navigate) => styleManager.runImmediateLocationNavigation(navigate) : null, beginDeferred: typeof styleManager?.beginDeferredLocationNavigation === 'function' ? () => styleManager.beginDeferredLocationNavigation() : null, reassertDeferred: typeof styleManager?.reassertDeferredLocationNavigation === 'function' ? (generation) => styleManager.reassertDeferredLocationNavigation(generation) : null, onStart: () => { if (typeof styleManager?.beginLocationNavigation === 'function') { styleManager.beginLocationNavigation(); return; } interruptCameraMotion('nav:fly_to_location'); if (viewer.trackedEntity) stopAllTracking(viewer, dataManager); }, }); } if (name === 'adjust_camera_zoom') { return adjustCameraZoom(viewer, args); } if (name === 'zoom_to_globe') { if (typeof styleManager?.resetToGlobeView === 'function') { return styleManager.resetToGlobeView(); } const result = flyToGlobeView(viewer); return { ok: true, action: 'zoom_to_globe', heightKm: Math.round(GLOBE_VIEW.heightM / 1000), centeredOn: { latitude: Number(result.latitude.toFixed(2)), longitude: Number(result.longitude.toFixed(2)), }, }; } if (name === 'next_satellite_pass') { return nextSatellitePass(viewer, dataManager, args); } if (name === 'next_iss_pass') { return nextIssPass(viewer, dataManager, args); } if (name === 'analyst_query') { return runAnalystQuery( ensureAnalystEngine(), dataManager, args, _layerEnabledAt, current, runOptions.signal, ); } if (name === 'move_camera') { return moveCamera(args, (navigate, releaseOptions) => runManagedVoiceNavigation( styleManager, 'camera', 'move_camera', navigate, releaseOptions, ), ); } if (name === 'fly_route') { return flyRoute( annotations?.list?.() || [], args, (lat, lon) => floorServices.cachedGroundFloor(lat, lon), (navigate) => runManagedVoiceNavigation( styleManager, 'route', 'fly_route', navigate, ), (cells) => floorServices.warmGroundFloor(cells), ); } if (name === 'get_entity_context') { return getEntityContext( viewer, dataManager, styleManager, args, placeSearch, ); } if (name === 'get_current_view_state') { return getCurrentViewState( viewer, styleManager, dataManager, sceneDirector, ); } if (name === 'set_hud') { const out = { ok: true, action: 'set_hud' }; if (args.layout != null) { const result = styleManager.setHudLayout(args.layout); if (!result.ok) return { ...result, action: 'set_hud' }; Object.assign(out, result); } if (args.visible != null) { const result = styleManager.setHudVisible(args.visible); if (!result.ok) return { ...result, action: 'set_hud' }; Object.assign(out, result); } return { ...out, hud: styleManager.getControlState().hud }; } if (name === 'set_cyber_sonar') { if (typeof styleManager?.setCyberSonar !== 'function') { return { ok: false, action: 'set_cyber_sonar', error: 'Cyber sonar controls are unavailable.', }; } return { action: 'set_cyber_sonar', ...styleManager.setCyberSonar(args) }; } if (name === 'set_detection') { const result = styleManager.setDetection({ enabled: typeof args.enabled === 'boolean' ? args.enabled : undefined, mode: typeof args.mode === 'string' ? args.mode : undefined, densityPct: Number.isFinite(Number(args.densityPct)) ? Number(args.densityPct) : undefined, allocationStrategy: typeof args.allocationStrategy === 'string' ? args.allocationStrategy : undefined, }); return { action: 'set_detection', ...result }; } if (name === 'set_map_stack') { const stackId = normalizeStackId(args.stack); if (!stackId) throw new Error(`Unknown map stack: ${args.stack || 'missing'}`); const result = await styleManager.setMapStack(stackId); return { action: 'set_map_stack', requested: stackId, ...result }; } if (name === 'set_post_processing') { const out = { ok: true, action: 'set_post_processing' }; if (args.bloom && typeof args.bloom === 'object') { Object.assign( out, styleManager.setBloom({ enabled: typeof args.bloom.enabled === 'boolean' ? args.bloom.enabled : undefined, intensityPct: Number.isFinite(Number(args.bloom.intensityPct)) ? Number(args.bloom.intensityPct) : undefined, }), ); } if (args.sharpen && typeof args.sharpen === 'object') { Object.assign( out, styleManager.setSharpen({ enabled: typeof args.sharpen.enabled === 'boolean' ? args.sharpen.enabled : undefined, intensityPct: Number.isFinite(Number(args.sharpen.intensityPct)) ? Number(args.sharpen.intensityPct) : undefined, }), ); } return out; } if (name === 'control_scene') { return controlScene(sceneDirector, args); } if (name === 'control_cctv') { const action = String(args.action || '').toLowerCase(); if ( (action === 'enable' || action === 'disable') && dataManager.layers.get('cctv')?.module ) { const result = await runGevAction( 'set_layer_visibility', { layerId: 'cctv', enabled: action === 'enable' }, runOptions, ); return { ...result, action: 'control_cctv' }; } return controlCctv(dataManager, args, styleManager); } if (name === 'control_radio') { return controlRadio(viewer, dataManager, args, { ...runOptions, placeSearch, }); } if (name === 'track_entity') { return trackEntity(viewer, dataManager, styleManager, args, { isCurrent: current, }); } if (name === 'stop_tracking') { return stopAllTracking(viewer, dataManager); } if (name === 'frame_overhead') { return frameOverhead(viewer, dataManager, styleManager, args); } if (name === 'annotate_map') { return annotateMap(annotations, args, reportProgress); } if (name === 'clear_annotations') { return clearAnnotations(annotations); } throw new Error(`Unknown GEV tool: ${name}`); }; // Lifetimes: the runner lives for the page; conversation state — the // analyst's follow-up memory and any write still in flight — lives for one // voice session and is dropped by resetConversation(). let conversation = 0; let disposed = false; async function runVoiceAction(name, rawArgs = {}, callerOptions = {}) { if (disposed) return { ok: false, action: name, cancelled: true, error: 'Voice ended.', }; const epoch = conversation; const runOptions = { ...callerOptions, isCurrent: () => epoch === conversation && !disposed && (typeof callerOptions.isCurrent !== 'function' || callerOptions.isCurrent()), }; const input = rawArgs && typeof rawArgs === 'object' ? rawArgs : {}; // Point-and-ask: 'pointer' and referent:n become real targets here, from // the turn's pointer snapshot and the latest numbered result. const deictic = resolveDeicticArgs(name, input, { pointer: deixis?.pointer?.activeSnapshot?.() || null, referents: deixis?.referents || null, imageFrame: deixis?.imageFrame?.() || null, cameraKey: deixis?.cameraKey?.() || null, }); if (deictic.error) { return attachVoiceResult( name, { ok: false, action: name, error: deictic.error }, input, builders, ); } const referentGeneration = deixis?.referents?.generation?.(); let result = await runGevAction(name, deictic.args, runOptions); if (deictic.used && result && typeof result === 'object') result = { ...result, resolvedFrom: deictic.used }; const out = attachVoiceResult(name, result, deictic.args, builders); // Only an accepted, still-current result may become "the last list": a // cancelled turn or one finishing after the session ended must not // repopulate a cleared registry. const stillCurrent = !runOptions.signal?.aborted && (typeof runOptions.isCurrent !== 'function' || runOptions.isCurrent()); if (stillCurrent) deixis?.referents?.recordResult?.(name, out, { since: referentGeneration, }); if (stillCurrent && deictic.used?.source === 'pointer') deixis?.onPointerUsed?.(); return out; } /** * A voice session ended: invalidate every write still in flight, then * forget the analyst's "last answer". */ runVoiceAction.resetConversation = () => { conversation++; analystEngine?.reset(); }; /** Voice was removed: nothing may run or write again. */ runVoiceAction.dispose = () => { if (disposed) return; runVoiceAction.resetConversation(); disposed = true; }; return runVoiceAction; } function selectedCockpitTarget(dataManager) { const selected = getSelectedEntityContext({ dataManager }); if (!selected || !['flights', 'military'].includes(selected.layerId)) return null; const module = dataManager?.layers?.get(selected.layerId)?.module; if (!module?.trackById || typeof module.trackById !== 'function') return null; const id = String(selected.id || '').trim(); return id ? { layerId: selected.layerId, id } : null; } // Abuse guards: a single tool call may not request more than this many marks, // each route no more waypoints than this, and free-text fields are clamped so a // runaway model call can't drive unbounded geocode/Overpass/OSRM/DOM work. These // mirror the tool-schema caps (defense-in-depth: a direct or schema-ignoring call // is still bounded here). const MAX_ANNOTATIONS_PER_CALL = 24; const MAX_ROUTE_POINTS = 12; const MAX_TARGET_LEN = 200; const MAX_LABEL_LEN = 120; function clampStr(value, max) { if (typeof value !== 'string') return value; const trimmed = value.trim(); return trimmed.length > max ? trimmed.slice(0, max) : trimmed; } /** Bound the free-text + array sizes inside one annotation spec before it reaches the engine. */ function sanitizeAnnotationSpec(spec) { if (!spec || typeof spec !== 'object') return spec; const out = { ...spec }; if (typeof out.target === 'string') out.target = clampStr(out.target, MAX_TARGET_LEN); if (typeof out.toTarget === 'string') out.toTarget = clampStr(out.toTarget, MAX_TARGET_LEN); if (typeof out.label === 'string') out.label = clampStr(out.label, MAX_LABEL_LEN); if (Array.isArray(out.points)) { out.points = out.points .slice(0, MAX_ROUTE_POINTS) .map((p) => p && typeof p === 'object' && typeof p.target === 'string' ? { ...p, target: clampStr(p.target, MAX_TARGET_LEN) } : p, ); } return out; } /** * Draw "whiteboard" annotations on the 3D world to point out what the agent is * talking about. Place names are resolved to real-world coordinates (and OSM * footprints) by the annotation engine, so the agent never has to guess pixels. */ async function annotateMap(annotations, args = {}, reportProgress = () => {}) { if (!annotations || typeof annotations.annotate !== 'function') { return { ok: false, action: 'annotate_map', error: 'Annotation engine unavailable', }; } const raw = Array.isArray(args.annotations) ? args.annotations : []; if (!raw.length) { return { ok: false, action: 'annotate_map', error: 'No annotations supplied', }; } if (raw.length > MAX_ANNOTATIONS_PER_CALL) { return { ok: false, action: 'annotate_map', error: `Too many annotations in one call (${raw.length}); max ${MAX_ANNOTATIONS_PER_CALL}. Mark fewer places, or split across calls.`, }; } const requests = raw.map(sanitizeAnnotationSpec); const firstTarget = requests.find((spec) => spec?.target || spec?.label); reportProgress('resolve', firstTarget?.target || firstTarget?.label || ''); const result = await annotations.annotate(requests, { // C1 invariant enforced in CODE (not just the prompt): the VOICE path NEVER clears as // a side effect of drawing — annotations accumulate/persist, and only an explicit // clear_annotations tool call wipes the board. (clearPrevious is intentionally ignored // here and removed from the annotate_map schema; the console/demo API still has it.) clearPrevious: false, persist: args.persist !== false, flyTo: Boolean(args.flyTo), }); // Honesty: surface partial failure explicitly so the agent can tell the user // which place(s) it couldn't mark instead of implying everything appeared. const drewSome = result.drawn > 0; const someFailed = result.failed > 0; const failedLabels = []; const pendingOutline = (result.results || []).find( (r) => r.ok && r.outlinePending, ); if (pendingOutline) reportProgress('outline', pendingOutline.label || pendingOutline.target); for (const r of result.results || []) { if (r.ok) continue; // Route failures carry the specific missing waypoint name(s) in failedTargets; // everything else names its own label/target. if (Array.isArray(r.failedTargets) && r.failedTargets.length) failedLabels.push(...r.failedTargets); else failedLabels.push(r.target || r.label || 'an unnamed place'); // target (the place) before caption } return { ok: drewSome, action: 'annotate_map', drawn: result.drawn, failed: result.failed, partial: drewSome && someFailed, failedLabels: someFailed ? failedLabels : undefined, // A drawn route whose street routing was unavailable is a straight direct line, // not a real walking/driving route — flag it so the voice layer stays honest. routeFallback: (result.results || []).some((r) => r.ok && r.fallback), capped: Boolean(result.capped), // Progressive outlines: anchors are placed and returned immediately; footprints for // these items are still being traced and will appear on their own (or the mark // honestly stays a point). NOT a failure — the voice layer must not report it as one. outlinePending: (result.results || []).some((r) => r.ok && r.outlinePending) || undefined, items: result.results, // Keep `error` populated whenever ANYTHING failed (partial or total) so the // result never reads as a clean success — but keep it STATIC (no raw place text); // the actual names live only in the structured failedLabels DATA field, so the // model-facing prose can't carry injected instructions from a place name. error: someFailed ? 'Could not place one or more annotations' : null, }; } function clearAnnotations(annotations) { if (!annotations || typeof annotations.clear !== 'function') { return { ok: false, action: 'clear_annotations', error: 'Annotation engine unavailable', }; } annotations.clear(); return { ok: true, action: 'clear_annotations' }; } export function normalizeStackId(value) { const raw = String(value || '') .trim() .toLowerCase(); if (!raw) return null; return STACK_ALIASES.get(raw) || null; } /** * Voice scene playback control. Playback is fire-and-forget: startScene * sequences shots for minutes and must not block the realtime tool loop. */ function controlScene(sceneDirector, args = {}) { if (!sceneDirector) { return { ok: false, action: 'control_scene', error: 'Scene director unavailable', }; } const sceneAction = String(args.action || '').toLowerCase(); if (sceneAction === 'list') { return { ok: true, action: 'control_scene', scenes: sceneDirector.listScenes(), ...sceneDirector.getPlaybackStatus(), }; } if (sceneAction === 'status') { return { ok: true, action: 'control_scene', ...sceneDirector.getPlaybackStatus(), }; } if (sceneAction === 'stop') { sceneDirector.stopScene(msg('Stopped by voice')); return { ok: true, action: 'control_scene', running: false }; } if (sceneAction === 'next') { sceneDirector.runNextScene(); return { ok: true, action: 'control_scene', advanced: true }; } if (sceneAction === 'play') { if (sceneDirector.running) { return { ok: false, action: 'control_scene', error: 'A scene is already running — stop it first', }; } const scene = args.sceneId ? sceneDirector.findSceneByQuery(args.sceneId) : sceneDirector.listScenes()[0] || null; if (!scene) { return { ok: false, action: 'control_scene', error: `No scene matched "${args.sceneId || ''}"`, scenes: sceneDirector.listScenes(), }; } void sceneDirector.startScene(scene.id, { single: true }); return { ok: true, action: 'control_scene', playing: scene.title, shots: scene.shots, }; } throw new Error(`Unknown scene action: ${args.action || 'missing'}`); } /** Voice CCTV control over the cctv layer module's public surface. */ export async function controlCctv(dataManager, args = {}, styleManager = null) { const action = String(args.action || '').toLowerCase(); const cctv = dataManager.layers.get('cctv')?.module; if (!cctv) { return { ok: false, action: 'control_cctv', error: 'CCTV layer unavailable', }; } if (action === 'enable' || action === 'disable') { return { ok: false, action: 'control_cctv', error: 'Enable or disable CCTV through set_layer_visibility', }; } if (!dataManager.isEnabled('cctv')) { return { ok: false, action: 'control_cctv', error: 'CCTV layer is off — enable it first', }; } const summarize = () => { const ui = cctv.getUIState?.() || {}; return { activeCameraId: ui.activeCameraId || null, activeCamera: ui.activeCamera?.name || ui.activeCamera?.id || null, cameraCount: Array.isArray(ui.cameras) ? ui.cameras.length : ui.count || 0, showCoverage: !!ui.showCoverage, coverageMode: ui.coverageMode || (ui.showCoverage ? 'on' : 'off'), showProjection: !!ui.showProjection, calibrationMode: !!ui.calibrationMode, autoHop: !!ui.autoHop, }; }; if (action === 'select') { const query = String(args.cameraQuery || '') .trim() .toLowerCase(); if (!query) throw new Error('control_cctv select needs cameraQuery'); const cams = cctv.getUIState?.()?.cameras || []; const match = cams.find((cam) => String(cam.id || '').toLowerCase() === query) || cams.find((cam) => String(cam.name || '').toLowerCase() === query) || cams.find((cam) => String(cam.name || '') .toLowerCase() .includes(query), ); if (!match) { return { ok: false, action: 'control_cctv', error: `No camera matched "${args.cameraQuery}"`, ...summarize(), }; } styleManager?.supersedeDeferredNavigation?.(); const selected = cctv.selectCamera(match.id); const focusResult = selected ? cctv.focusCamera(match.id, 1.8) : CCTV_FOCUS_RESULT.NO_ACTIVE_CAMERA; return { action: 'control_cctv', selected: match.name || match.id, ...summarize(), ...cctvVoiceFocusOutcome(focusResult, { cameraSelected: !!selected }), }; } if (action === 'next' || action === 'prev') { styleManager?.supersedeDeferredNavigation?.(); const nextId = cctv.cycleCamera(action === 'next' ? 1 : -1); const focusResult = nextId ? cctv.focusCamera(nextId, 1.8) : CCTV_FOCUS_RESULT.NO_ACTIVE_CAMERA; return { action: 'control_cctv', ...summarize(), ...cctvVoiceFocusOutcome(focusResult, { cameraSelected: !!nextId }), }; } if (action === 'nearest') { styleManager?.supersedeDeferredNavigation?.(); const nearestId = cctv.focusNearest({ focus: false }); const focusResult = nearestId ? cctv.focusCamera(nearestId, 1.8) : CCTV_FOCUS_RESULT.NO_ACTIVE_CAMERA; return { action: 'control_cctv', ...summarize(), ...cctvVoiceFocusOutcome(focusResult, { cameraSelected: !!nearestId }), }; } if (action === 'focus') { const activeId = cctv.getUIState?.()?.activeCameraId; if (activeId) styleManager?.supersedeDeferredNavigation?.(); const focusResult = activeId ? cctv.focusCamera(activeId) : CCTV_FOCUS_RESULT.NO_ACTIVE_CAMERA; return { action: 'control_cctv', ...summarize(), ...cctvVoiceFocusOutcome(focusResult), }; } if (action === 'viewshed') { // Color-coded coverage volumes; enabled:false drops back to plain // coverage wireframes (not off — "hide coverage" is the coverage action). const next = typeof args.enabled === 'boolean' && !args.enabled ? 'on' : 'viewshed'; dataManager.setLayerParams( 'cctv', { coverageMode: next }, { origin: 'voice' }, ); return { ok: true, action: 'control_cctv', ...summarize() }; } if (action === 'adjust') { const current = summarize(); const next = typeof args.enabled === 'boolean' ? args.enabled : !current.calibrationMode; dataManager.setLayerParams( 'cctv', { calibrationMode: next }, { origin: 'voice' }, ); return { ok: true, action: 'control_cctv', ...summarize() }; } if (action === 'coverage') { const current = summarize(); const next = typeof args.enabled === 'boolean' ? args.enabled : !current.showCoverage; dataManager.setLayerParams( 'cctv', { coverageMode: next ? 'on' : 'off' }, { origin: 'voice' }, ); return { ok: true, action: 'control_cctv', ...summarize() }; } if (action === 'projection' || action === 'autohop') { const key = action === 'projection' ? 'showProjection' : 'autoHop'; const current = summarize(); const next = typeof args.enabled === 'boolean' ? args.enabled : !current[key]; dataManager.setLayerParams('cctv', { [key]: next }, { origin: 'voice' }); return { ok: true, action: 'control_cctv', ...summarize() }; } throw new Error(`Unknown CCTV action: ${args.action || 'missing'}`); } const RADIO_COUNTRY_CENTERS = new Map([ // i18n-ignore: place name in tool data ['us', { lat: 39.8, lon: -98.6, country: 'US', label: 'United States' }], // i18n-ignore: place name in tool data ['usa', { lat: 39.8, lon: -98.6, country: 'US', label: 'United States' }], [ 'united states', // i18n-ignore: place name in tool data { lat: 39.8, lon: -98.6, country: 'US', label: 'United States' }, ], [ 'united states of america', // i18n-ignore: place name in tool data { lat: 39.8, lon: -98.6, country: 'US', label: 'United States' }, ], ]); /** Resolve curated cities and common country requests without moving the camera. */ export function knownRadioLocation(query, locationId = '') { const requestedId = normalizeLocationId(locationId) || normalizeLocationId(query); const city = requestedId ? CITY_POIS[requestedId] : null; if (city) { const bounds = city.viewBounds; return { lat: bounds ? (bounds.southwest.lat + bounds.northeast.lat) / 2 : city.pois[0]?.lat, lon: bounds ? (bounds.southwest.lng + bounds.northeast.lng) / 2 : city.pois[0]?.lon, label: city.name, country: '', }; } return ( RADIO_COUNTRY_CENTERS.get( String(query || '') .trim() .toLowerCase(), ) || null ); } function radioCoordinatePair(args = {}) { const latitudeProvided = Object.hasOwn(args, 'latitude'); const longitudeProvided = Object.hasOwn(args, 'longitude'); const provided = latitudeProvided || longitudeProvided; const latitude = args.latitude; const longitude = args.longitude; const valid = latitudeProvided && longitudeProvided && typeof latitude === 'number' && typeof longitude === 'number' && Number.isFinite(latitude) && Number.isFinite(longitude) && latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180; return { provided, valid, latitude, longitude }; } function radioActionIsCurrent(options = {}) { return ( !options.signal?.aborted && (typeof options.isCurrent !== 'function' || options.isCurrent()) ); } function radioAbortError() { const error = new Error('Radio request was superseded by a newer voice turn'); error.name = 'AbortError'; return error; } async function resolveRadioLocation( args = {}, coordinates = radioCoordinatePair(args), options = {}, ) { if (!radioActionIsCurrent(options)) throw radioAbortError(); if (coordinates.valid) { const { latitude, longitude } = coordinates; return { lat: latitude, lon: longitude, label: `${latitude.toFixed(3)}, ${longitude.toFixed(3)}`, country: '', }; } const query = String(args.locationQuery || '').trim(); const known = knownRadioLocation(query, args.locationId); if (known) return known; if (!query) return null; const { placeSearch = unavailablePlaceSearch, signal } = options; const { place } = await placeSearch.geocode(query, { signal }); if (!radioActionIsCurrent(options)) throw radioAbortError(); if (!place) return null; // Localized provider country labels must not become station country filters. return { lat: place.lat, lon: place.lng, label: place.label || query, country: '', }; } /** Voice Radio controls over the Radio layer's public player surface. */ export async function controlRadio( viewer, dataManager, args = {}, options = {}, ) { const requestedAction = String(args.action || '') .trim() .toLowerCase(); const coordinates = radioCoordinatePair(args); const hasSelectionCriteria = Boolean( args.category || args.locationId || args.locationQuery || coordinates.provided || args.country || args.stationQuery, ); // Realtime models can reasonably interpret "play news near Austin" as Play // plus qualifiers. Play cannot honor those qualifiers, so normalize that // equivalent tool shape to Select instead of silently choosing the current // viewport's nearest station. const action = requestedAction === 'play' && hasSelectionCriteria ? 'select' : requestedAction; const readRadioLifecycle = () => readLayerLifecycleSummary(dataManager, 'radio'); const radio = dataManager.layers.get('radio')?.module; if (!radio) { return { ok: false, action: 'control_radio', error: 'Radio layer unavailable', ...readRadioLifecycle(), }; } const normalizedCountry = normalizeRadioCountryInput(args.country); let lastIntentOutcome = null; let lastIntentError = null; const intentSummary = () => lastIntentOutcome ? { phase: lastIntentOutcome.phase, cancellationReason: lastIntentOutcome.cancellationReason || null, successorIntentEpoch: lastIntentOutcome.successorIntentEpoch ?? null, successorEnabled: lastIntentOutcome.successorEnabled ?? null, successorOrigin: lastIntentOutcome.successorOrigin ?? null, } : {}; const cancelled = (summarize) => ({ ok: false, action: 'control_radio', cancelled: true, error: 'Radio request was superseded by a newer voice turn', ...intentSummary(), ...summarize(), }); const radioLifecycleIsSettled = (shouldEnable) => { const lifecycle = readRadioLifecycle(); return ( lifecycle.enabled === shouldEnable && lifecycle.lifecycleState === (shouldEnable ? 'enabled' : 'disabled') && !lifecycle.lifecycleUncertain ); }; const setRadioEnabled = async (shouldEnable) => { lastIntentOutcome = null; lastIntentError = null; if (!radioActionIsCurrent(options)) return false; const enableOptions = { origin: 'voice' }; // Both lifecycle directions can await module work. Let the manager own // the complete transaction so barge-in cancels it before a settled // visibility event can record explicit Context intent. if (options.signal) enableOptions.signal = options.signal; let result = false; try { if (typeof dataManager._setEnabledWithIntent === 'function') { const intent = dataManager._setEnabledWithIntent( 'radio', shouldEnable, enableOptions, ); result = await intent.promise; if (Number.isInteger(intent.intentEpoch)) { lastIntentOutcome = await dataManager._waitForVisibilityIntent?.( 'radio', intent.intentEpoch, ); } } else { result = await dataManager.setEnabled( 'radio', shouldEnable, enableOptions, ); } } catch (error) { lastIntentError = error; return false; } if (lastIntentOutcome?.succeeded === false) return false; if (!radioActionIsCurrent(options) && lastIntentOutcome?.succeeded !== true) return false; return result !== false && radioLifecycleIsSettled(shouldEnable); }; const lifecycleFailure = (message, summarize) => ({ ok: false, action: 'control_radio', cancelled: Boolean(lastIntentOutcome?.cancellationReason), error: lastIntentError?.message || message, ...intentSummary(), ...summarize(), }); const authorizeRadioPlayerMutation = async ({ enableIfOff = false } = {}) => { const lifecycle = readRadioLifecycle(); if (radioLifecycleIsSettled(true)) return true; if ( !enableIfOff && !lifecycle.enabled && lifecycle.lifecycleState === 'disabled' && !lifecycle.lifecycleUncertain ) return false; const reconciled = await setRadioEnabled(true); return reconciled && radioLifecycleIsSettled(true); }; const summarize = () => { const state = radio.getUIState?.() || {}; return { radioAction: action, ...readRadioLifecycle(), stationId: state.selected?.id || null, category: state.filter || 'all', audioState: state.audioState || 'stopped', volumePct: Math.round((state.volume ?? 0.8) * 100), mutedForVoice: Boolean(state.voiceDucked), }; }; if (!normalizedCountry.valid) { return { ok: false, action: 'control_radio', error: 'Radio country must be a recognized code or country name (80 characters maximum)', ...summarize(), }; } if (coordinates.provided && !coordinates.valid) { return { ok: false, action: 'control_radio', error: 'Radio coordinates require a complete numeric latitude/longitude pair in range', ...summarize(), }; } if (!radioActionIsCurrent(options)) return cancelled(summarize); if (action === 'enable' || action === 'disable') { const shouldEnable = action === 'enable'; const changed = await setRadioEnabled(shouldEnable); if (!radioActionIsCurrent(options) && lastIntentOutcome?.succeeded !== true) return cancelled(summarize); if (!changed) { return lifecycleFailure( `Radio could not be ${shouldEnable ? 'enabled' : 'disabled'}`, summarize, ); } return { ok: true, action: 'control_radio', ...summarize() }; } if (action === 'status') return { ok: true, action: 'control_radio', ...summarize() }; if (action === 'volume') { const volumePct = Number(args.volumePct); if (!Number.isFinite(volumePct) || volumePct < 0 || volumePct > 100) { return { ok: false, action: 'control_radio', error: 'Radio volume must be from 0 to 100', ...summarize(), }; } const authorized = await authorizeRadioPlayerMutation(); if (!radioActionIsCurrent(options)) return cancelled(summarize); if (!authorized) { return lifecycleFailure( 'Radio must be fully enabled before changing volume', summarize, ); } if (!radioActionIsCurrent(options)) return cancelled(summarize); const volumeApplied = typeof dataManager.setLayerParams === 'function' ? dataManager.setLayerParams( 'radio', { volume: volumePct / 100 }, { origin: 'voice' }, ) : radio.setVolume(volumePct / 100); if (volumeApplied === false) { return { ok: false, action: 'control_radio', error: 'Radio must be fully enabled before changing volume', ...summarize(), }; } return { ok: true, action: 'control_radio', ...summarize() }; } if (action === 'stop') { if (!radioActionIsCurrent(options)) return cancelled(summarize); let stopped = false; try { stopped = await radio.stopPlayback({ origin: 'voice' }); } catch (error) { return { ok: false, action: 'control_radio', error: error?.message || 'Radio could not be stopped', ...summarize(), }; } if (!radioActionIsCurrent(options)) return cancelled(summarize); if (stopped === false) { return { ok: false, action: 'control_radio', error: 'Radio could not be stopped', ...summarize(), }; } return { ok: true, action: 'control_radio', ...summarize() }; } if (action === 'pause') { // Pause is a playback-only control. In particular, a Pause sibling must // never resurrect a layer that an explicit Disable just turned off. Keep // its established cancellation authority while an enable is in flight: // the controller commits a successful Pause by aborting that older work. const lifecycle = readRadioLifecycle(); if (!lifecycle.enabled && lifecycle.lifecycleState !== 'enabling') { return { ok: true, action: 'control_radio', changed: false, ...summarize(), }; } if (!radioActionIsCurrent(options)) return cancelled(summarize); const paused = radio.pause?.({ origin: 'voice' }) || false; if (!paused) { return { ok: false, action: 'control_radio', error: 'Radio could not be paused', ...summarize(), }; } return { ok: true, action: 'control_radio', changed: true, ...summarize() }; } let resolvedLocation = null; if (action === 'select') { try { // Resolve asynchronous user input before enabling Radio. That keeps an // interrupted lookup from mutating layer or station state after barge-in. resolvedLocation = await resolveRadioLocation(args, coordinates, options); } catch (error) { if (error?.name === 'AbortError' || !radioActionIsCurrent(options)) return cancelled(summarize); throw error; } if (!radioActionIsCurrent(options)) return cancelled(summarize); if ((args.locationQuery || args.locationId) && !resolvedLocation) { return { ok: false, action: 'control_radio', error: `Could not resolve Radio location "${args.locationQuery || args.locationId}"`, ...summarize(), }; } } const authorized = await authorizeRadioPlayerMutation({ enableIfOff: true }); if (!radioActionIsCurrent(options)) return cancelled(summarize); if (!authorized) { return lifecycleFailure('Radio could not be enabled', summarize); } const state = radio.getUIState?.() || {}; if (!radioActionIsCurrent(options)) return cancelled(summarize); if (!state.stationCount) { return { ok: false, action: 'control_radio', error: state.error || 'No healthy Radio stations are available', ...summarize(), }; } if (action === 'play' || action === 'resume') { if (!radioActionIsCurrent(options)) return cancelled(summarize); const prepared = state.selected ? true : radio.cycleStation?.(1, { focus: false, autoplay: false }); return { ok: Boolean(prepared), action: 'control_radio', radioPlaybackRequested: Boolean(prepared), ...summarize(), }; } if (action === 'next' || action === 'previous') { if (!radioActionIsCurrent(options)) return cancelled(summarize); if (args.category) { if (typeof dataManager.setLayerParams === 'function') { dataManager.setLayerParams( 'radio', { filter: String(args.category) }, { origin: 'voice' }, ); } else { radio.setFilter(String(args.category)); } } const selected = radio.cycleStation?.(action === 'next' ? 1 : -1, { focus: false, autoplay: false, }); return { ok: Boolean(selected), action: 'control_radio', radioPlaybackRequested: Boolean(selected), ...summarize(), }; } if (action === 'select') { const location = resolvedLocation; if (!radioActionIsCurrent(options)) return cancelled(summarize); const station = radio.selectRequestedStation?.( { categoryId: String(args.category || 'all'), anchor: location ? { lat: location.lat, lon: location.lon } : null, country: normalizedCountry.empty ? String(location?.country || '') : normalizedCountry.code, stationQuery: String(args.stationQuery || ''), }, { autoplay: false }, ); if (!station) { return { ok: false, action: 'control_radio', error: 'No Radio station matched that location and category', ...summarize(), }; } return { ok: true, action: 'control_radio', radioPlaybackRequested: true, requestedLocation: location?.label || null, ...summarize(), }; } throw new Error(`Unknown Radio action: ${args.action || 'missing'}`); } /** * Maps a CCTV focus code to an honest voice-tool result. * @param {string|boolean} focusResult CCTV focus result code. * @param {Object} [options] * @param {boolean} [options.cameraSelected=false] Whether this action first selected a camera. * @returns {{ok: boolean, error: string|null}} Voice-facing result fields. */ export function cctvVoiceFocusOutcome( focusResult, { cameraSelected = false } = {}, ) { if (focusResult === CCTV_FOCUS_RESULT.FOCUSED || focusResult === true) { return { ok: true, error: null }; } if (focusResult === CCTV_FOCUS_RESULT.TRACKING_HOLDS_VIEW) { return { ok: false, error: cameraSelected ? 'Camera selected; tracking holds the view — say untrack to fly' : 'Camera active; tracking holds the view — say untrack first', }; } if (focusResult === CCTV_FOCUS_RESULT.COCKPIT_ACTIVE) { return { ok: false, error: cameraSelected ? 'Camera selected; in cockpit — exit cockpit to fly to it' : 'In cockpit — exit cockpit to fly to a camera', }; } return { ok: false, error: 'No active camera to focus' }; } /** * Spoken name for a tracked entity descriptor. * * Aircraft follow the flight layers' label convention — callsign → * registration → icao24 — so the narrated name matches the readout and the * detection card instead of speaking a raw hex at a contact the UI is calling * `N123AB`. Vessels and satellites carry no `registration`, so that link * simply falls through to their own name/mmsi/noradId links. * @param {object} found - Layer descriptor from `findByQuery`. * @param {string} query - The spoken query, used as the last resort. * @returns {string} A non-empty display name. */ export function formatTrackedEntityLabel(found, query = '') { const text = (v) => String(v ?? '').trim(); return ( text(found?.callsign) || text(found?.registration) || text(found?.name) || text(found?.icao24) || text(found?.mmsi) || text(found?.noradId) || String(query) ); } /** Finds and tracks/selects an entity by spoken query across layer families. */ async function trackEntity( viewer, dataManager, styleManager, args = {}, { isCurrent = () => true } = {}, ) { const cancelled = () => ({ ok: false, cancelled: true, action: 'track_entity', error: 'Tracking request was superseded by a newer voice turn', }); if (!isCurrent()) return cancelled(); const query = String(args.query || '').trim(); if (!query) throw new Error('track_entity needs a query'); // Fire queries route to the FIRMS layer's strongest detection if (/\bfires?\b/i.test(query)) { if (!dataManager.isEnabled('local-firms')) { return { ok: false, action: 'track_entity', query, error: 'The FIRMS fires layer is not enabled', }; } const firms = dataManager.layers.get('local-firms')?.module; const strongest = firms?.getStrongestFire?.(); if (!strongest) { return { ok: false, action: 'track_entity', query, error: 'No fire detections loaded yet', }; } if ( !Number.isFinite(strongest.latitude) || !Number.isFinite(strongest.longitude) ) { return { ok: false, action: 'track_entity', query, error: 'The strongest fire has no usable position', }; } if (!isCurrent()) return cancelled(); return runManagedVoiceNavigation( styleManager, 'fire', 'track_entity', () => { flyToLandmark(viewer, strongest.latitude, strongest.longitude, { range: 14000, pitch: -50, heading: 0, buildingHeight: 0, duration: 2.2, }); return { ok: true, action: 'track_entity', kind: 'fire', layerId: 'local-firms', // i18n-ignore: entity name in a tool result for the voice model label: strongest.label || 'Strongest fire', latitude: strongest.latitude, longitude: strongest.longitude, frp: strongest.frp ?? null, }; }, ); } const requested = args.layerId ? normalizeLayerId(args.layerId) : null; const families = TRACKABLE_FAMILIES.filter( (family) => !requested || family.layerId === requested, ); const skippedDisabled = []; for (const family of families) { if (!dataManager.isEnabled(family.layerId)) { skippedDisabled.push(family.layerId); continue; } const module = dataManager.layers.get(family.layerId)?.module; if (!module || typeof module.findByQuery !== 'function') continue; const found = module.findByQuery(query); if (!found) continue; if (!isCurrent()) return cancelled(); if ( family.kind === 'vessel' && (!Number.isFinite(found.latitude) || !Number.isFinite(found.longitude)) ) { return { ok: false, action: 'track_entity', layerId: family.layerId, kind: family.kind, error: 'The matched vessel has no usable position', }; } return runManagedVoiceNavigation( styleManager, family.kind, 'track_entity', () => { let trackedOk = false; if (family.kind === 'vessel') { trackedOk = !!module.selectById?.(found.mmsi); flyToLandmark(viewer, found.latitude, found.longitude, { range: 6000, pitch: -45, heading: 0, buildingHeight: 0, duration: 2.0, }); } else if (family.kind === 'satellite') { trackedOk = !!module.trackById?.(found.noradId, { origin: 'voice' }); } else { trackedOk = !!module.trackById?.(found.icao24, { origin: 'voice' }); } return { ok: trackedOk, action: 'track_entity', layerId: family.layerId, kind: family.kind, // Aircraft follow the flight layers' label convention (callsign → // registration → icao24) so the spoken name matches what the UI shows; // `registration` is absent on vessels/satellites and simply falls // through to their own name/id links. label: formatTrackedEntityLabel(found, query), latitude: found.latitude ?? null, longitude: found.longitude ?? null, altitudeM: Number.isFinite(found.altitudeM) ? Math.round(found.altitudeM) : null, error: trackedOk ? null : 'Match found but tracking failed', }; }, ); } const disabledNote = skippedDisabled.length ? ` (disabled layers skipped: ${skippedDisabled.join(', ')})` : ''; return { ok: false, action: 'track_entity', query, error: `Nothing matched "${query}"${disabledNote}`, }; } /** Releases tracking/selection on every entity layer family. */ function stopAllTracking(viewer, dataManager) { const released = []; const failed = new Set(); for (const family of TRACKABLE_FAMILIES) { const module = dataManager.layers.get(family.layerId)?.module; if (!module) continue; try { if (family.kind === 'vessel') { if (module.getSelectedInfo?.()) { if ( typeof module.clearSelection !== 'function' || module.clearSelection() === false ) { failed.add(family.layerId); } else { released.push(family.layerId); } } } else if (module.getTrackedInfo?.()) { if ( typeof module.stopTracking !== 'function' || module.stopTracking({ origin: 'voice' }) === false ) { failed.add(family.layerId); } else { released.push(family.layerId); } } } catch { failed.add(family.layerId); } } for (const [layerId, key] of [ ['flights', 'selectedFlightsTrackingId'], ['military', 'selectedMilitaryTrackingId'], ['satellites', 'selectedSatTrackingId'], ]) { try { if ( dataManager.setLayerParams( layerId, { [key]: null }, { origin: 'voice' }, ) === false ) { failed.add(layerId); } } catch { failed.add(layerId); } } if (viewer) viewer.trackedEntity = undefined; if (failed.size) { const failedLayerIds = [...failed]; return { ok: false, action: 'stop_tracking', released, failedLayerIds, error: `Tracking could not be cleared for: ${failedLayerIds.join(', ')}`, }; } return { ok: true, action: 'stop_tracking', released }; } /** * Frames entities near the current view target with a cinematic pull-back: * oblique high pitch for aircraft/ships, shallow wide pitch for satellites. * When entries are found and detection is OFF, auto-enables panoptic * detection so the framed entities are labeled, and reports * detectionEnabled so the voice agent can mention labels are on. */ async function frameOverhead(viewer, dataManager, styleManager, args = {}) { const targetRaw = String(args.target || 'flights').toLowerCase(); const layerId = FRAME_TARGETS.get(targetRaw) || normalizeLayerId(targetRaw) || 'flights'; if (!dataManager.layers.has(layerId)) { return { ok: false, action: 'frame_overhead', error: `Unknown target layer: ${args.target}`, }; } if (!dataManager.isEnabled(layerId)) { return { ok: false, action: 'frame_overhead', layerId, error: `The ${layerId} layer is not enabled`, }; } const module = dataManager.layers.get(layerId)?.module; const isSatellites = layerId === 'satellites'; const defaultRadiusKm = isSatellites ? 3000 : layerId === 'ais-live-vessels' ? 120 : 150; const radiusKm = clampNumber(args.radiusKm, 10, 20000, defaultRadiusKm); const center = getViewTargetCartesian(viewer) || viewer.camera.positionWC; let entries = []; if (typeof module.getNearby === 'function') { entries = module.getNearby(center, radiusKm * 1000, 80) || []; } else if (typeof module.getAllPositions === 'function') { entries = (module.getAllPositions(800) || []) .filter((entry) => entry.position) .map((entry) => ({ ...entry, distance: Cesium.Cartesian3.distance(center, entry.position), })) .filter((entry) => entry.distance <= radiusKm * 1000) .sort((a, b) => a.distance - b.distance) .slice(0, 80); } if (!entries.length) { return { ok: false, action: 'frame_overhead', layerId, radiusKm: Math.round(radiusKm), count: 0, error: `No ${targetRaw} within ${Math.round(radiusKm)} km of the current view`, }; } const sphere = Cesium.BoundingSphere.fromPoints( entries.map((entry) => entry.position), ); sphere.radius = Math.max(sphere.radius * 1.25, 8000); const pitch = Cesium.Math.toRadians(isSatellites ? -35 : -62); return runManagedVoiceNavigation( styleManager, 'frame', 'frame_overhead', () => { viewer.camera.flyToBoundingSphere(sphere, { duration: 2.0, offset: new Cesium.HeadingPitchRange( viewer.camera.heading, pitch, sphere.radius * 2.4, ), }); let detectionEnabled = false; try { const detectionState = styleManager?.getDetectionState?.(); if (detectionState?.detectionMode === 'OFF') { const detectionResult = styleManager.setDetection({ mode: 'dense' }); detectionEnabled = detectionResult?.ok === true; } else if (detectionState) { detectionEnabled = true; } } catch { // detection facade unavailable; framing still succeeded } return { ok: true, action: 'frame_overhead', layerId, radiusKm: Math.round(radiusKm), count: entries.length, detectionEnabled, nearest: entries.slice(0, 5).map((entry) => ({ id: entry.id || entry.icao24 || entry.mmsi || null, // Canonical identity: `id` is a display label for aircraft. ...(entry.icao24 ? { icao24: String(entry.icao24) } : {}), ...(entry.mmsi ? { mmsi: String(entry.mmsi) } : {}), label: entry.label || entry.callsign || entry.name || null, })), }; }, ); } /** Run one validated voice camera mutation through the UI-owned authority seam. */ function runManagedVoiceNavigation( styleManager, noun, action, navigate, releaseOptions = undefined, ) { if (typeof styleManager?.runImmediateNavigation !== 'function') { return { ok: false, action, error: 'Camera navigation policy unavailable' }; } const result = styleManager.runImmediateNavigation( noun, navigate, releaseOptions, ); if (result !== false) return result; return { ok: false, action, error: 'Camera navigation is unavailable in the current view', }; } /** * Place labels for the HUD's passive summary. It geocodes the view center * and one more point. While the view still shows about the same area (see * hudPlaceLookup.js) it answers for the points the last lookup asked about * instead, so a settled camera does not buy a new set of Google requests for * every small pan; those points are cache hits, and only what the last * lookup did not get (a timeout, a failure, an empty answer) is asked again. * `cachedOnly` reads the shared caches and makes no request. */ export async function getBasemapLabelContext( viewer, service = defaultGeospatial, { cachedOnly = false, previous = null } = {}, ) { const cameraHeightM = viewer.camera.positionCartographic.height; const target = getViewTargetCartographic(viewer); if (!target) { return { placeLabels: [], streetLabels: [], nearbyPlaceLabels: [], view: null, lookup: null, }; } const latitude = Number(Cesium.Math.toDegrees(target.latitude).toFixed(6)); const longitude = Number(Cesium.Math.toDegrees(target.longitude).toFixed(6)); const view = placeLookupView({ latitude, longitude, rangeM: viewTargetRangeM(viewer, target, cameraHeightM), fovRad: viewer.camera.frustum?.fov, aspectRatio: viewer.camera.frustum?.aspectRatio, tier: basemapLookupTier(cameraHeightM), }); if ( !cachedOnly && previous?.lookup && !placeLookupViewChanged(previous.view, view) ) { // The same points again: what the last lookup got is a cache hit, and // only what it did not (a timeout, a failure, an empty answer) is asked. const labels = await basemapLabelsAt(previous.lookup, service, { cachedOnly: false, }); return { ...labels, view: previous.view, lookup: previous.lookup }; } const samples = sampleViewportCartographics(viewer); const lookup = { latitude, longitude, cameraHeightM, // Cache reads are free, so a cached-only host keeps every sample. samples: cachedOnly ? samples : hudViewportSamples(samples, shouldReverseGeocode(cameraHeightM)), }; return { ...(await basemapLabelsAt(lookup, service, { cachedOnly })), view, lookup, }; } /** Which of the height-gated lookups a camera height calls for. */ function basemapLookupTier(cameraHeightM) { return [ shouldReverseGeocode(cameraHeightM), shouldReverseGeocodeViewport(cameraHeightM) && cameraHeightM > 10000, shouldFetchNearbyPlaces(cameraHeightM) && nearbyPlacesRadiusM(cameraHeightM), ].join(':'); } /** Camera distance to the view target, or the camera height without one. */ function viewTargetRangeM(viewer, target, cameraHeightM) { const position = viewer.camera.positionWC; if (!position) return cameraHeightM; const range = Cesium.Cartesian3.distance( position, Cesium.Cartographic.toCartesian(target), ); return Number.isFinite(range) ? range : cameraHeightM; } async function basemapLabelsAt( { latitude, longitude, cameraHeightM, samples }, service, { cachedOnly }, ) { const { reverseGeocodeCache, nearbyPlacesCache } = cachesFor(service); const cachedViewportPlaces = viewportPlacesFromCache( samples, cameraHeightM, service, ); const viewportPromise = cachedViewportPlaces ? Promise.resolve(cachedViewportPlaces) : cachedOnly ? Promise.resolve(null) : reverseGeocodeViewportSamples(samples, cameraHeightM, service); const placePromise = shouldReverseGeocode(cameraHeightM) ? cachedOnly ? Promise.resolve( reverseGeocodeCache.get( reverseGeocodeKey(latitude, longitude, service), ) || null, ) : reverseGeocode(latitude, longitude, service) : Promise.resolve(null); const nearbyPromise = shouldFetchNearbyPlaces(cameraHeightM) ? cachedOnly ? Promise.resolve( nearbyPlacesCache.get( nearbyPlacesCacheKey(latitude, longitude, cameraHeightM, service), ) || [], ) : fetchNearbyPlaces(latitude, longitude, cameraHeightM, service) : Promise.resolve([]); const [viewportPlaces, place, nearbyPlaces] = await Promise.all([ resolveWithin( viewportPromise, BASEMAP_CONTEXT_WAIT_MS, cachedViewportPlaces, ), resolveWithin(placePromise, BASEMAP_CONTEXT_WAIT_MS, null), resolveWithin(nearbyPromise, BASEMAP_CONTEXT_WAIT_MS, []), ]); return { placeLabels: uniqueStrings([ place?.formattedAddress, place?.locality, place?.region, place?.country, ...(place?.labels || []), ...(viewportPlaces?.visibleLabels || []), ]).slice(0, 24), streetLabels: uniqueStrings([ ...(place?.streetLabels || []), ...(viewportPlaces?.streetLabels || []), ]).slice(0, 16), nearbyPlaceLabels: uniqueStrings( (nearbyPlaces || []).flatMap((nearbyPlace) => [ nearbyPlace.name, nearbyPlace.address, ]), ).slice(0, 24), }; } function installViewTargetPrewarm(viewer) { if (viewer.__gevViewTargetPrewarmInstalled) return; viewer.__gevViewTargetPrewarmInstalled = true; let timer = null; let reportedPrewarmFailure = false; viewer.camera.moveEnd.addEventListener(() => { if (timer) window.clearTimeout(timer); timer = window.setTimeout(() => { timer = null; // Belt and braces. Validating the pick is the fix; this catch exists // because an idle callback is an UNCAUGHT context — nothing above it can // handle a surprise from the scene graph, and a red console error on a // plain camera flight is worse than a cold cache. Reported once per // viewer so a repeating cause cannot spam the console. const warm = () => { try { getViewTargetCartographic(viewer); } catch (error) { if (reportedPrewarmFailure) return; reportedPrewarmFailure = true; console.debug( '[Voice] view-target prewarm skipped:', error?.message || error, ); } }; if (typeof window.requestIdleCallback === 'function') { window.requestIdleCallback(warm, { timeout: 500 }); } else { warm(); } }, 120); }); } function adjustCameraZoom(viewer, args) { const direction = String(args.direction || '').toLowerCase(); if (direction !== 'in' && direction !== 'out') { throw new Error('adjust_camera_zoom direction must be "in" or "out"'); } const amount = String(args.amount || 'little').toLowerCase(); const fraction = { little: 0.25, medium: 0.55, lot: 1.0, }[amount]; if (!fraction) throw new Error(`Unknown zoom amount: ${args.amount}`); const camera = viewer.camera; const beforePosition = Cesium.Cartesian3.clone(camera.positionWC); const beforeHeightM = camera.positionCartographic.height; const target = getViewTargetCartesian(viewer); const targetDistanceM = target ? Cesium.Cartesian3.distance(beforePosition, target) : Math.max(100, beforeHeightM); const minimumDistanceM = direction === 'in' ? 20 : 50; const movementM = Math.max(minimumDistanceM, targetDistanceM * fraction); camera.cancelFlight(); if (direction === 'out') { camera.zoomOut(movementM); } else { const safeMovementM = Math.min( movementM, Math.max(0, targetDistanceM - 25), ); if (safeMovementM <= 0) { return { ok: false, action: 'adjust_camera_zoom', direction, amount, error: 'Camera is already at the minimum target distance', }; } camera.zoomIn(safeMovementM); } viewer.scene.requestRender(); const afterPosition = camera.positionWC; const movedM = Cesium.Cartesian3.distance(beforePosition, afterPosition); const afterHeightM = camera.positionCartographic.height; const moved = movedM >= 0.5; return { ok: moved, action: 'adjust_camera_zoom', direction, amount, movementRequestedM: Math.round(movementM), movementActualM: Math.round(movedM), beforeHeightM: Math.round(beforeHeightM), afterHeightM: Math.round(afterHeightM), error: moved ? null : 'Cesium camera position did not change', }; } const COMPASS_16 = [ 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW', ]; function compassDir(azDeg) { return COMPASS_16[Math.round((((azDeg % 360) + 360) % 360) / 22.5) % 16]; } function nextIssPass(viewer, dataManager, args) { let latDeg = Number.isFinite(args.latitude) ? args.latitude : null; let lonDeg = Number.isFinite(args.longitude) ? args.longitude : null; if (latDeg == null || lonDeg == null) { const carto = viewer?.camera?.positionCartographic; if (!carto) throw new Error('Camera position unavailable'); latDeg = Cesium.Math.toDegrees(carto.latitude); lonDeg = Cesium.Math.toDegrees(carto.longitude); } const minElevDeg = Number.isFinite(args.minElevationDeg) ? args.minElevationDeg : 10; const result = dataManager?.layers ?.get('satellites') ?.module?.getNextIssPass?.({ latDeg, lonDeg, minElevDeg }) ?? { status: 'no-tle', }; if (result.status === 'no-tle') { return { ok: false, action: 'next_iss_pass', error: 'ISS orbital elements not loaded yet — enable the satellites layer once, then ask again.', }; } if (result.status === 'none') { return { ok: false, action: 'next_iss_pass', error: `No ISS pass above ${minElevDeg}° in the next 24 hours for this location.`, }; } const { pass } = result; return { ok: true, action: 'next_iss_pass', observer: { latitude: latDeg, longitude: lonDeg }, riseIso: new Date(pass.riseMs).toISOString(), minutesFromNow: Math.round((pass.riseMs - Date.now()) / 60000), durationMin: Math.max(1, Math.round((pass.setMs - pass.riseMs) / 60000)), peakElevationDeg: Math.round(pass.maxElevDeg), riseDirection: compassDir(pass.riseAzDeg), visible: typeof pass.visible === 'boolean' ? pass.visible : null, visibilityNote: 'Geometric illumination estimate only; weather, brightness and orbital-element age affect actual visibility.', setIso: new Date(pass.setMs).toISOString(), peakIso: new Date(pass.maxElevMs).toISOString(), }; } function nextSatellitePass(viewer, dataManager, args) { const layer = dataManager?.layers?.get('satellites')?.module; const identity = layer?.resolveSatelliteForPass?.(args.target) || { status: 'not-found', }; 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 // this explicitly resolved catalog identity and the optional visibility filter. const adapter = { layers: new Map([ [ 'satellites', { module: { getNextIssPass: (options) => layer.getNextSatellitePass(identity.noradId, { ...options, requireVisible: args.visibleOnly === true, }), }, }, ], ]), }; const result = nextIssPass(viewer, adapter, args); if (result.error) { result.error = result.error.replace( /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, }; } function normalizePanelId(value) { const raw = String(value || '').trim(); if (!raw) return null; if (PANEL_IDS.has(raw)) return raw; return PANEL_ALIASES.get(raw.toLowerCase()) || null; } function normalizeLayerId(value) { const raw = String(value || '').trim(); if (!raw) return null; if (LAYER_ALIASES.has(raw.toLowerCase())) return LAYER_ALIASES.get(raw.toLowerCase()); return raw; } function normalizeCockpitTargetLayer(value) { const layerId = normalizeLayerId(value); if (!layerId || !COCKPIT_TARGET_LAYERS.has(layerId)) return null; return layerId; } function normalizeCockpitNavigationHints(rawAction) { const raw = String(rawAction || '') .trim() .toLowerCase(); if (!raw) return {}; const targetLayer = raw.includes('vessel') || raw.includes('ship') || raw.includes('ais') ? 'ais-live-vessels' : raw.includes('installation') || raw.includes('facility') || raw.includes('base') ? 'military-installations' : raw.includes('military') ? 'military' : null; const aircraftClass = raw.includes('helicopter') || raw.includes('helo') || raw.includes('chopper') ? 'helicopter' : null; return { targetLayer, aircraftClass, }; } /** * Normalize a spoken/typed aircraft-class filter to the class id the analyst * records carry. * * Every real `classifyAircraft()` id is a single unpunctuated word, so callers * already say them exactly and a plain lower-case is enough. The one exception * is the TR-3B Easter egg (`tr3b`): people write and say it hyphenated, so * "TR-3B" / "tr 3b" / "tr 3 b" would otherwise reach the analyst as a value no * record matches. Collapsing spaces and hyphens and comparing against THAT ONE * id keeps this surgical — no general alias table, and no other class id * collapses to `tr3b`, so nothing else can be caught by it. * * App-side only: the voice tool schema and the model instructions are * untouched, so this costs no prompt-cache churn. * @param {*} value Raw class filter from the tool call or an utterance hint. * @returns {string|null} Class id, or null when nothing was supplied. */ function normalizeAircraftClassFilter(value) { const raw = String(value || '') .trim() .toLowerCase(); if (!raw) return null; return raw.replace(/[\s-]+/g, '') === TR3B_CLASS ? TR3B_CLASS : raw; } function setPanelOpen(styleManager, panelId, open) { if (styleManager && typeof styleManager.setPanelCollapsed === 'function') { styleManager.setPanelCollapsed(panelId, !open, { explicit: true }); } else { const panel = document.getElementById(panelId); if (panel) panel.classList.toggle('collapsed', !open); } } function normalizeContextMode(value) { const raw = String(value || '') .trim() .toLowerCase(); if (!raw) return null; return CONTEXT_MODE_ALIASES.get(raw) || null; } function normalizeCockpitAction(value) { const raw = String(value || '') .trim() .toLowerCase(); if (!raw) return null; const direct = COCKPIT_ACTION_ALIASES.get(raw); if (direct) return direct; const normalized = raw .replace(/[^a-z0-9]+/g, ' ') .trim() .replace(/\s+/g, ' '); const directNormalized = COCKPIT_ACTION_ALIASES.get(normalized); if (directNormalized) return directNormalized; if (/\bprevious\b|\bprev\b/.test(normalized)) return 'previous'; if (/\bstatus\b|\bstate\b/.test(normalized)) return 'status'; if (/\bexit\b|\bleave\b|\bquit\b/.test(normalized)) return 'exit'; if (/\benter\b|\bopen\b|\bstart\b/.test(normalized)) return 'enter'; if (/\bnext\b|\bclosest\b|\bnearby\b|\bnearest\b/.test(normalized)) return 'next'; return null; } function focusDataLayerRow(layerId) { const row = document.querySelector( `#data-toggles [data-layer-id="${CSS.escape(layerId)}"]`, ); if (!row) return null; row.scrollIntoView({ block: 'center', behavior: 'smooth' }); row.classList.remove('gev-voice-focus'); void row.offsetWidth; row.classList.add('gev-voice-focus'); window.setTimeout(() => row.classList.remove('gev-voice-focus'), 3000); // The English name: tool results stay English whatever the UI language. const name = row.dataset.layerName || row.querySelector('.data-name')?.textContent?.trim() || layerId; return { id: layerId, name }; } function normalizeStyle(value) { const raw = String(value || '') .trim() .toLowerCase(); if (raw === 'filter off' || raw === 'off' || raw === 'default') return 'normal'; if (raw === 'night vision' || raw === 'nvg') return 'surveillance'; if (raw === 'flir') return 'thermal'; if (ALLOWED_STYLES.has(raw)) return raw; return null; } async function flyToRequestedLocation( viewer, args, { placeSearch = unavailablePlaceSearch, searchNavigation = searchAndFlyTo, signal, onProgress = () => {}, onStart = null, runImmediate = null, beginDeferred = null, reassertDeferred = null, } = {}, ) { const requestedRangeM = Number(args.rangeM); const rangeM = Number.isFinite(requestedRangeM) ? clampNumber(requestedRangeM, 100, 20000000, 900) : null; const locationId = normalizeLocationId(args.locationId || args.query); const immediate = (navigate) => typeof runImmediate === 'function' ? runImmediate(navigate) : navigate(); const immediateOnStart = typeof runImmediate === 'function' ? null : onStart; const cancelled = (label) => ({ ok: false, cancelled: true, action: 'fly_to_location', label, }); let settleArrival = null; const arrival = args.waitForArrival === true ? new Promise((resolve) => { settleArrival = resolve; }) : null; const arrivalHooks = arrival ? { onComplete: () => settleArrival?.('arrived'), onCancel: () => settleArrival?.('cancelled'), } : {}; const afterArrival = async (result, label) => { if (!arrival || result?.ok !== true) return result; onProgress('fly', label); const status = await arrival; settleArrival = null; return status === 'arrived' ? { ...result, arrived: true } : cancelled(label); }; if (locationId) { const result = immediate(() => flyToPresetLocation(viewer, locationId, { ...(rangeM || args.viewMode === 'close' ? { range: rangeM || 250 } : { viewMode: 'overview' }), duration: 2.2, onStart: immediateOnStart, ...arrivalHooks, }), ); if (result === false) return cancelled(CITY_POIS[locationId]?.name || locationId); const response = { ok: Boolean(result), action: 'fly_to_location', locationId, label: CITY_POIS[locationId]?.name || locationId, rangeM: result?.range ? Math.round(result.range) : rangeM || null, navigationMode: rangeM ? 'explicit-range' : args.viewMode === 'close' ? 'city-close' : result?.navigationMode || 'city-overview', }; return afterArrival(response, response.label); } const latitude = Number(args.latitude); const longitude = Number(args.longitude); if (Number.isFinite(latitude) && Number.isFinite(longitude)) { const result = immediate(() => flyToLandmark(viewer, latitude, longitude, { range: rangeM || 250, pitch: -35, heading: 0, buildingHeight: 0, duration: 2.2, onStart: immediateOnStart, ...arrivalHooks, }), ); if (result === false) return cancelled(`${latitude.toFixed(4)}, ${longitude.toFixed(4)}`); const response = { ok: true, action: 'fly_to_location', latitude, longitude, label: `${latitude.toFixed(4)}, ${longitude.toFixed(4)}`, rangeM: Math.round(rangeM || 250), navigationMode: rangeM ? 'explicit-range' : 'close-coordinate', }; return afterArrival(response, response.label); } const query = String(args.query || '').trim(); if (query) { // A query that names a curated preset POI ("the Texas State Capitol", "Golden Gate Bridge") // flies to its hand-tuned camera pose — the same beautiful framing as clicking the LOCATIONS // panel button — instead of generic geocode framing. An explicit rangeM still overrides distance. const poiMatch = findPoiByName(query); if (poiMatch) { const result = immediate(() => flyToPOI(viewer, poiMatch.cityId, poiMatch.index, { duration: 2.2, onStart: immediateOnStart, ...arrivalHooks, ...(rangeM ? { range: rangeM } : {}), }), ); const poi = CITY_POIS[poiMatch.cityId]?.pois?.[poiMatch.index]; if (result === false) return cancelled(poi?.name || query); const response = { ok: Boolean(result), action: 'fly_to_location', query, label: poi?.name || query, navigationMode: rangeM ? 'preset-poi-range' : 'preset-poi', rangeM: result?.range ? Math.round(result.range) : rangeM || null, }; return afterArrival(response, response.label); } const generation = typeof beginDeferred === 'function' ? beginDeferred() : null; if (generation === false) return cancelled(query); onProgress('search', query); const managedDeferred = typeof reassertDeferred === 'function'; const destination = await searchNavigation(viewer, query, { placeSearch, signal, ...(rangeM ? { range: rangeM } : {}), forceClose: args.viewMode === 'close', // 'overview' frames the geocode viewport even for precise-place results — // previously dropped here, so "overview of Zilker Park" flew to a rooftop. viewMode: args.viewMode || null, duration: 2.2, ...arrivalHooks, beforeFly: managedDeferred ? () => reassertDeferred(generation) : null, onStart: managedDeferred ? null : onStart, }); if (destination?.cancelled) return cancelled(query); const response = { ok: Boolean(destination), action: 'fly_to_location', query, label: destination?.label || query, navigationMode: destination?.navigationMode || null, rangeM: destination?.rangeM || rangeM || null, }; return afterArrival(response, response.label); } throw new Error( 'fly_to_location needs a locationId, query, or latitude/longitude', ); } function normalizeLocationId(value) { const raw = String(value || '') .trim() .toLowerCase(); if (!raw) return null; if (CITY_POIS[raw]) return raw; if (CITY_ALIASES.has(raw)) return CITY_ALIASES.get(raw); return null; } function getCurrentViewState( viewer, styleManager, dataManager, sceneDirector = null, ) { return getWorldPacket({ viewer, styleManager, dataManager, sceneDirector, }); } async function getEntityContext( viewer, dataManager, styleManager, args = {}, service = defaultGeospatial, ) { const startedAt = performance.now(); const scope = String(args.scope || 'auto').toLowerCase(); if (scope === 'target' && args.target) { return getTargetContext(viewer, dataManager, styleManager, args, service); } const layerId = normalizeLayerId(args.layerId || args.layer); const limit = Math.round(clampNumber(args.limit, 1, 12, 5)); const selected = selectedEntityContext(dataManager); const cameraHeightM = viewer.camera.positionCartographic.height; const viewTarget = getViewTargetCartographic(viewer); const scenePromise = getSceneContext( viewer, styleManager, dataManager, viewTarget, service, ); const selectedWillBeReturned = selected && (scope === 'selected' || scope === 'auto'); const visible = !selectedWillBeReturned && shouldScanVisibleEntities(cameraHeightM) ? visibleEntityContexts(viewer, dataManager, { layerId, limit, target: viewTarget, }) : []; const scene = await scenePromise; if ((scope === 'selected' || scope === 'auto') && selected) { logSlowContext(startedAt, 'selected'); return { ok: true, action: 'get_entity_context', scope: 'selected', scene, selected, }; } logSlowContext(startedAt, 'in_view'); return { ok: true, action: 'get_entity_context', scope: 'in_view', scene, selected: selected || null, visible, count: visible.length, visibleScanSkipped: !shouldScanVisibleEntities(cameraHeightM), }; } const AIRCRAFT_LAYERS = new Set(['flights', 'military']); /** Live position of a layer record by id, or null when it is gone. */ function locateLayerEntity(dataManager, { layerId, id, lat, lon }) { if (!layerId || !id) return null; if (dataManager?.isEnabled && !dataManager.isEnabled(layerId)) return null; let found = null; try { found = dataManager?.layers?.get(layerId)?.module?.findByQuery?.(String(id)) || null; } catch { found = null; } if (Number.isFinite(found?.latitude) && Number.isFinite(found?.longitude)) return { lat: found.latitude, lon: found.longitude, found }; const record = getContextStore().entities.get(String(id)); if ( record?.layerId === layerId && Number.isFinite(record.latitude) && Number.isFinite(record.longitude) ) return { lat: record.latitude, lon: record.longitude, record }; // Analyst-only layers (e.g. bikeshare) have no pick/context lookup. Read // their current snapshot, so a removed or moved record is not resurrected. try { const matches = dataManager?.layers ?.get(layerId) ?.module?.getAnalystRecords?.() .filter( (item) => String(item.icao24 ?? item.mmsi ?? item.noradId ?? item.id) === String(id), ) || []; // Bikeshare uses station names as IDs: retain the coordinate identity // even if a namesake disappears. Moving records use their fresh fix. const row = matches.length === 1 && layerId !== 'bikeshare' ? matches[0] : matches.find((item) => item.lat === lat && item.lon === lon); if (Number.isFinite(row?.lat) && Number.isFinite(row?.lon)) return { lat: row.lat, lon: row.lon, found: { ...row, latitude: row.lat, longitude: row.lon }, }; } catch { // A missing or unavailable snapshot cannot establish a live position. } return null; } /** * Describe one pointed or referenced thing without changing the selection: * a live aircraft descriptor for contacts, the context-store record for * everything else. Aircraft fields use the same names as a selected contact, * so the spoken identity line is identical. */ function describeTargetEntity(dataManager, target) { if (!target?.layerId) return null; const located = locateLayerEntity(dataManager, target); if (located?.record) return summarizeContextRecord(located.record, { includeProperties: true }); const found = located?.found; if (!found) { const record = getContextStore().entities.get(String(target.id)); return record ? summarizeContextRecord(record, { includeProperties: true }) : null; } const name = formatTrackedEntityLabel(found, target.label || target.id); const aircraft = AIRCRAFT_LAYERS.has(target.layerId); const route = found.origin && found.destination ? `${found.origin} → ${found.destination}` : ''; return { id: String(target.id), name, layerId: target.layerId, layerName: layerTitle(target.layerId), source: null, latitude: found.latitude ?? null, longitude: found.longitude ?? null, properties: compactProperties( aircraft ? { name, callsign: found.callsign || '', registration: found.registration || '', operator: found.airline || '', type: found.typeName || found.typeCode || '', route, altitude: Number.isFinite(found.altitudeM) ? `${Math.round(found.altitudeM * 3.28084)} ft` : '', } : { name, mmsi: found.mmsi || '', noradId: found.noradId || '' }, ), active: true, }; } /** get_entity_context for a pointed/referenced target (scope 'target'). */ async function getTargetContext( viewer, dataManager, styleManager, args, service, ) { const target = args.target; // Number(null) is 0: a coordinate-less referent must not become (0, 0). const coordinate = (value) => value === null || value === undefined || value === '' ? NaN : Number(value); let lat = coordinate(target.lat); let lon = coordinate(target.lon); if (!Number.isFinite(lat) || !Number.isFinite(lon)) { const located = locateLayerEntity(dataManager, target); lat = located?.lat ?? NaN; lon = located?.lon ?? NaN; } const hasPosition = Number.isFinite(lat) && Number.isFinite(lon); const viewTarget = hasPosition ? Cesium.Cartographic.fromDegrees(lon, lat) : getViewTargetCartographic(viewer); const selected = describeTargetEntity(dataManager, target); const scene = await getSceneContext( viewer, styleManager, dataManager, viewTarget, service, ); if (selected) { return { ok: true, action: 'get_entity_context', scope: 'pointer', scene, selected, }; } const cameraHeightM = viewer.camera.positionCartographic.height; const visible = shouldScanVisibleEntities(cameraHeightM) ? visibleEntityContexts(viewer, dataManager, { layerId: normalizeLayerId(args.layerId || args.layer), limit: Math.round(clampNumber(args.limit, 1, 12, 5)), target: viewTarget, }) : []; return { ok: true, action: 'get_entity_context', scope: 'pointer', scene, selected: null, ...(hasPosition ? { pointer: { latitude: Number(lat.toFixed(5)), longitude: Number(lon.toFixed(5)), }, } : {}), visible, count: visible.length, }; } /** * Answer an entity-centred "how many aircraft nearby" from the Contacts * engine, or null when the question is not that. * * Applies only when the requested radius centre IS the active Contacts * subject: that is precisely the case where the operator can see a number on * the panel, so the spoken answer must be that number. Everything else — an * explicit region, an arbitrary point, a named place — keeps the general * record engine, which is what those questions actually mean. * @param {object} args Tool arguments. * @param {object} result The general engine's result, reused for scope text. * @returns {object|null} A unified-count payload, or null. */ async function aircraftProximityWindowForQuery( dataManager, args, result, isCurrent = () => true, ) { // Follow-ups re-filter the engine's retained answer. Re-projecting a fresh // Contacts snapshot here would restore rows a previous filter removed. if (result?.coverage?.followUp) return null; const spec = normalizeAnalystSpec(args, args?.layers || []); if (!spec) return null; const scope = args?.scope; if (String(scope?.kind || '').toLowerCase() !== 'radius') return null; const layers = Array.isArray(args.layers) ? args.layers : []; if (!layers.some((layer) => layer === 'flights' || layer === 'military')) return null; const awareness = dataManager?.layers?.get('military-awareness')?.module; const snapshot = awareness?.getAircraftQuerySnapshot?.(); const subject = snapshot?.subject; if (!subject?.position) return null; // An explicit centre only qualifies when it IS the subject; otherwise the // operator asked about somewhere else and must get that answer. if (scope.center && !centerMatchesSubject(scope.center, subject.position)) return null; const radiusM = Number.isFinite(scope.km) ? scope.km * 1000 : snapshot.radiusM || 250_000; // A different radius is a different question. Let the general analyst // engine answer it instead of relabelling the panel's 250 km snapshot. if ( Number.isFinite(snapshot.radiusM) && Math.abs(radiusM - snapshot.radiusM) > 1 ) return null; const label = subject.label || subject.id || 'the selected contact'; const radiusKm = Math.round(radiusM / 1000); const wanted = layers.filter( (layer) => layer === 'flights' || layer === 'military', ); // Provenance belongs to the Awareness evaluation, not the current layer // registry. The panel deliberately retains a cohort between render-cadence // refreshes, so a live registry read here could already describe a newer // source state than the count and items the operator is looking at. const retainedProvenance = wanted .map((key) => snapshot.cohorts?.[key]?.provenance) .filter(Boolean); const feedProvenance = feedProvenanceEnvelope(retainedProvenance); const sourceSnapshotsById = new Map( feedProvenance.layers.map((layer) => [layer.id, layer]), ); // The cohort is the panel's; each contact is joined to its loaded analyst // record so the same filters, ranking and item shape apply as everywhere // else. The cohort still owns membership and distance. const distanceKm = new WeakMap(); const groups = wanted.map((key) => { const records = new Map(); for (const record of dataManager.layers .get(key) ?.module?.getAnalystRecords?.(ANALYST_RECORD_CAP) || []) if (record?.icao24) records.set(record.icao24, record); const rows = (snapshot.cohorts?.[key]?.items || []).map((item) => { const record = records.get(item.icao24); let lat = record?.lat; let lon = record?.lon; if (!Number.isFinite(lat) || !Number.isFinite(lon)) { const carto = item.position ? Cesium.Cartographic.fromCartesian(item.position) : null; lat = carto ? Cesium.Math.toDegrees(carto.latitude) : null; lon = carto ? Cesium.Math.toDegrees(carto.longitude) : null; } const row = { ...(record || { id: item.id, icao24: item.icao24, callsign: item.callsign ?? null, altitudeM: item.altitudeM ?? null, speedMps: item.velocityMps ?? null, aircraftClass: item.aircraftClass ?? null, }), lat, lon, }; // Rank the retained cohort at its original precision; display rounding // must not turn nearby contacts into artificial ID-sorted ties. if (Number.isFinite(item.distance)) distanceKm.set(row, item.distance / 1000); return row; }); return { key, rows }; }); const selection = await selectAnalystRows(groups, { filters: spec.filters, sortBy: spec.sortBy, sortDir: spec.sortDir, limit: Math.round(clampNumber(args.limit, 1, 50, 12)), layerKeys: wanted, distanceOf: (row) => distanceKm.get(row), isCurrent, }); if (!selection || !isCurrent()) return null; const items = selection.items.map(compactAnalystItem); const complete = wanted.every((key) => { const cohort = snapshot.cohorts?.[key]; return Number.isFinite(cohort?.count) && cohort.complete !== false; }); const genericLayers = new Map( (result?.coverage?.layersQueried || []).map((layer) => [ layer.layerKey, layer, ]), ); const layersQueried = wanted.map((key) => { const cohort = snapshot.cohorts?.[key] || {}; const generic = genericLayers.get(key) || {}; const sourceSnapshot = sourceSnapshotsById.get(key); const returned = Number.isFinite(cohort.count) ? cohort.count : cohort.items?.length || 0; return { layerKey: key, ...(generic.status ? { status: generic.status } : {}), ...(generic.note ? { note: generic.note } : {}), ...(generic.window ? { window: generic.window } : {}), returned, total: cohort.complete === false ? null : returned, truncated: cohort.complete === false, ...(cohort.source ? { source: cohort.source } : {}), ...(sourceSnapshot ? { feedState: sourceSnapshot.feedState, source: sourceSnapshot.source, lastUpdate: sourceSnapshot.lastUpdate, enabled: sourceSnapshot.enabled, error: sourceSnapshot.error, } : {}), }; }); const coverage = { layersQueried, records: { returned: selection.count, total: complete ? selection.count : null, truncated: !complete, }, scope: `window:${radiusKm}km@${label}`, followUp: false, feedProvenance, note: `Contacts panel snapshot${snapshot.evaluatedAt ? ` at ${new Date(snapshot.evaluatedAt).toISOString()}` : ''} — the same retained cohort supplies the card, speech, and follow-up memory${complete ? '' : '; the count is a lower bound because at least one layer reached its retained limit'}.`, }; const answer = { ok: true, action: 'analyst_query', count: selection.count, complete, scopeLabel: `within ${radiusKm} km of ${label}`, listed: items.length, items, summary: selection.summary, ...(result?.partial ? { partial: true, unanswered: [...(result.unanswered || [])] } : {}), display: { scope: `within ${radiusKm} km of ${label} (Contacts window)`, }, coverage, // (D) The answer always says whose window it is and which engine produced it. window: { engine: 'contacts-window', centeredOn: label, radiusKm, flights: snapshot.cohorts?.flights?.count ?? 'unknown', military: snapshot.cohorts?.military?.count ?? 'unknown', aircraft: Number.isFinite(snapshot.cohorts?.flights?.count) && Number.isFinite(snapshot.cohorts?.military?.count) ? snapshot.cohorts.flights.count + snapshot.cohorts.military.count : 'unknown', complete, evaluatedAt: snapshot.evaluatedAt || null, }, ...(snapshot.contactsWindow ? { contactsWindow: { ...snapshot.contactsWindow } } : {}), feedProvenance, feedState: feedProvenance.overall, }; return { answer, memory: { matched: selection.matched, // Follow-up ranking belongs to this retained Contacts evaluation, not // the camera's current position. Keep the accessor with its row owners. distanceOf: (row) => distanceKm.get(row), layerKeys: wanted, coverage, unanswered: [...(result?.unanswered || [])], scopeLabel: answer.scopeLabel, scopeDetail: answer.display.scope, }, }; } /** * Radius within which a requested centre counts as the Contacts subject's own * position. Generous enough to absorb the fix-vs-display offset between what * the model read off a card and where the contact is now, tight enough that a * neighbouring landmark is never mistaken for the subject. */ const SUBJECT_CENTER_TOLERANCE_KM = 1; /** * Is this requested centre the Contacts subject's own position? * * Measured as true ground distance. A lat/lon delta box was wrong in a way * that hid at the equator and widened toward it: a degree of longitude is * ~111 km at the equator and ~78 km at 45°, so a fixed 0.01° box spans a * different real distance at every latitude, and its diagonal admitted centres * ~1.5 km away — far enough to be a different place, close enough to slip * through and get answered as the subject's window. * @param {{lat: number, lon: number}} center Requested centre. * @param {Cesium.Cartesian3} subjectPosition Subject's world position. * @returns {boolean} True when the two are the same place. */ function centerMatchesSubject(center, subjectPosition) { if (!Number.isFinite(center?.lat) || !Number.isFinite(center?.lon)) return false; const carto = Cesium.Cartographic.fromCartesian(subjectPosition); if (!carto) return false; const subjectLat = Cesium.Math.toDegrees(carto.latitude); const subjectLon = Cesium.Math.toDegrees(carto.longitude); return ( greatCircleKm( { lat: subjectLat, lon: subjectLon }, { lat: center.lat, lon: center.lon }, ) <= SUBJECT_CENTER_TOLERANCE_KM ); } function shouldScanVisibleEntities(cameraHeightM) { return cameraHeightM <= 100000; } function selectedEntityContext(dataManager) { const record = getSelectedEntityContext({ dataManager }); if (!record) return null; return summarizeContextRecord(record, { includeProperties: true }); } function visibleEntityContexts( viewer, dataManager, { layerId = null, limit = 5, target = null } = {}, ) { const nearbyRecords = []; const canvas = viewer.scene.canvas; const width = canvas.clientWidth || canvas.width || 0; const height = canvas.clientHeight || canvas.height || 0; const centerX = width / 2; const centerY = height / 2; const targetLat = target ? Cesium.Math.toDegrees(target.latitude) : null; const targetLon = target ? Cesium.Math.toDegrees(target.longitude) : null; const store = getContextStore(); const enabledLayerIds = new Set( dataManager .getAll() .filter((layer) => layer.enabled) .map((layer) => layer.id), ); for (const record of store.entities.values()) { if (layerId && record.layerId !== layerId) continue; if (record.layerId && !enabledLayerIds.has(record.layerId)) continue; if (record.entity?.show === false || record.dataSource?.show === false) continue; if (!Number.isFinite(record.latitude) || !Number.isFinite(record.longitude)) continue; insertNearestRecord( nearbyRecords, { record, distanceScore: target ? approximateCoordinateDistanceSq( targetLat, targetLon, record.latitude, record.longitude, ) : 0, }, VISIBLE_ENTITY_SHORTLIST, ); } const candidates = []; for (const { record } of nearbyRecords) { const position = record.entity?.__localBaseCartesian; if (!position) continue; const screen = Cesium.SceneTransforms.worldToWindowCoordinates( viewer.scene, position, ); if ( !screen || screen.x < 0 || screen.y < 0 || screen.x > width || screen.y > height ) continue; const dx = screen.x - centerX; const dy = screen.y - centerY; candidates.push({ summary: summarizeContextRecord(record, { includeProperties: true }), distancePx: Math.sqrt(dx * dx + dy * dy), }); } return candidates .sort((a, b) => a.distancePx - b.distancePx) .slice(0, limit) .map((item) => item.summary); } function insertNearestRecord(records, candidate, limit) { if (records.length < limit) { records.push(candidate); records.sort((a, b) => a.distanceScore - b.distanceScore); return; } if (candidate.distanceScore >= records[records.length - 1].distanceScore) return; let low = 0; let high = records.length; while (low < high) { const middle = (low + high) >> 1; if (records[middle].distanceScore <= candidate.distanceScore) low = middle + 1; else high = middle; } records.splice(low, 0, candidate); records.pop(); } async function getSceneContext( viewer, styleManager, dataManager, viewTarget = null, service = defaultGeospatial, ) { const cartographic = Cesium.Cartographic.fromCartesian( viewer.camera.positionWC, ); const basemap = await getBasemapContext(viewer, viewTarget, service); const enabledLayers = dataManager .getAll() .filter((layer) => layer.enabled) .map((layer) => ({ id: layer.id, name: layer.name, count: layer.stats?.count || 0, source: layer.source, })); return { camera: { latitude: Number(Cesium.Math.toDegrees(cartographic.latitude).toFixed(6)), longitude: Number( Cesium.Math.toDegrees(cartographic.longitude).toFixed(6), ), heightM: Math.round(cartographic.height), }, basemap, style: styleManager.activeStyle || 'normal', enabledLayers, }; } async function getBasemapContext( viewer, viewTarget = null, service = defaultGeospatial, ) { const { reverseGeocodeCache, nearbyPlacesCache } = cachesFor(service); const target = viewTarget; const samples = sampleViewportCartographics(viewer); const cameraCartographic = Cesium.Cartographic.fromCartesian( viewer.camera.positionWC, ); const cameraHeightM = cameraCartographic.height; const viewScale = classifyViewScale(cameraHeightM); const cachedViewportPlaces = viewportPlacesFromCache( samples, cameraHeightM, service, ); const viewportPlacesPromise = cachedViewportPlaces ? Promise.resolve(cachedViewportPlaces) : reverseGeocodeViewportSamples(samples, cameraHeightM, service); if (!target) { const viewportPlaces = await resolveWithin( viewportPlacesPromise, BASEMAP_CONTEXT_WAIT_MS, cachedViewportPlaces, ); return { source: 'Google Photorealistic 3D Tiles / Cesium basemap', hasGoogle3DTiles: Boolean(window.__godsEyeView?.tileset), viewScale, viewportSamples: samples, viewportPlaces, target: null, place: null, }; } const latitude = Number(Cesium.Math.toDegrees(target.latitude).toFixed(6)); const longitude = Number(Cesium.Math.toDegrees(target.longitude).toFixed(6)); const inferredCountry = inferCountryFromSamples(samples); const knownLandmarks = nearbyKnownLandmarks( latitude, longitude, cameraHeightM, ); const fallbackPlace = coarseBasemapPlace( viewScale, latitude, longitude, inferredCountry, ); const cachedPlace = shouldReverseGeocode(cameraHeightM) ? reverseGeocodeCache.get( reverseGeocodeKey(latitude, longitude, service), ) || null : null; const nearbyCacheKey = nearbyPlacesCacheKey( latitude, longitude, cameraHeightM, service, ); const cachedNearbyPlaces = shouldFetchNearbyPlaces(cameraHeightM) && nearbyPlacesCache.has(nearbyCacheKey) ? nearbyPlacesCache.get(nearbyCacheKey) : null; const placePromise = shouldReverseGeocode(cameraHeightM) && !cachedPlace ? reverseGeocode(latitude, longitude, service) : Promise.resolve(cachedPlace); const nearbyPlacesPromise = shouldFetchNearbyPlaces(cameraHeightM) && !cachedNearbyPlaces ? fetchNearbyPlaces(latitude, longitude, cameraHeightM, service) : Promise.resolve(cachedNearbyPlaces); const [viewportPlaces, resolvedPlace, resolvedNearbyPlaces] = await Promise.all([ resolveWithin( viewportPlacesPromise, BASEMAP_CONTEXT_WAIT_MS, cachedViewportPlaces, ), resolveWithin(placePromise, BASEMAP_CONTEXT_WAIT_MS, cachedPlace), resolveWithin( nearbyPlacesPromise, BASEMAP_CONTEXT_WAIT_MS, cachedNearbyPlaces, ), ]); const place = resolvedPlace || fallbackPlace; const nearbyPlaces = resolvedNearbyPlaces || []; return { source: 'Google Photorealistic 3D Tiles / Cesium basemap', hasGoogle3DTiles: Boolean(window.__godsEyeView?.tileset), viewScale, viewportSamples: samples, viewportPlaces, target: { latitude, longitude, heightM: Math.round(target.height || 0), }, knownLandmarks, nearbyPlaces, place, }; } function classifyViewScale(cameraHeightM) { if (cameraHeightM > 12000000) return 'global'; if (cameraHeightM > 3000000) return 'continental'; if (cameraHeightM > 750000) return 'regional'; if (cameraHeightM > 100000) return 'metro'; if (cameraHeightM > 10000) return 'city'; return 'local'; } function shouldReverseGeocode(cameraHeightM) { return cameraHeightM <= 750000; } function shouldReverseGeocodeViewport(cameraHeightM) { return cameraHeightM <= 3000000; } function shouldFetchNearbyPlaces(cameraHeightM) { return cameraHeightM <= 25000; } function nearbyKnownLandmarks(latitude, longitude, cameraHeightM) { if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return []; const maxDistanceKm = cameraHeightM <= 5000 ? 2 : cameraHeightM <= 50000 ? 10 : cameraHeightM <= 250000 ? 35 : 0; if (maxDistanceKm <= 0) return []; const matches = []; for (const [cityId, city] of Object.entries(CITY_POIS)) { for (let poiIndex = 0; poiIndex < city.pois.length; poiIndex++) { const poi = city.pois[poiIndex]; const distanceKm = greatCircleKm({ lat: latitude, lon: longitude }, poi); if (distanceKm > maxDistanceKm) continue; matches.push({ name: poi.name, cityId, city: city.name, latitude: poi.lat, longitude: poi.lon, distanceKm: Number(distanceKm.toFixed(3)), }); } } return matches.sort((a, b) => a.distanceKm - b.distanceKm).slice(0, 5); } function coarseBasemapPlace( viewScale, latitude, longitude, inferredCountry = null, ) { if (viewScale === 'global') { return { formattedAddress: 'Global Earth view', locality: null, region: null, country: null, precision: 'global', note: 'Camera is too far out for a precise street or city label; do not infer a local place from the center point.', }; } return { formattedAddress: inferredCountry?.country ? `${viewScale[0].toUpperCase()}${viewScale.slice(1)} basemap view over ${inferredCountry.country}` : `${viewScale[0].toUpperCase()}${viewScale.slice(1)} basemap view centered near ${latitude.toFixed(2)}, ${longitude.toFixed(2)}`, locality: null, region: null, country: inferredCountry?.country || null, precision: viewScale, confidence: inferredCountry?.confidence || null, note: 'Camera altitude is high, so this is approximate basemap context rather than a precise address.', }; } function getViewTargetCartographic(viewer) { const signature = cameraViewSignature(viewer); const cached = viewTargetCache.get(viewer); if ( cached?.signature === signature && performance.now() - cached.cachedAt < 2500 ) { return cached.target; } const position = getViewTargetCartesian(viewer); // `fromCartesian` still returns undefined for a point too near the ellipsoid // center to project; normalize that to the same "no target" null the callers // already handle for a missed pick. const target = position ? Cesium.Cartographic.fromCartesian(position) || null : null; viewTargetCache.set(viewer, { signature, target, cachedAt: performance.now(), }); return target; } function cameraViewSignature(viewer) { const camera = viewer.camera; const cartographic = camera.positionCartographic; return [ Cesium.Math.toDegrees(cartographic.latitude).toFixed(5), Cesium.Math.toDegrees(cartographic.longitude).toFixed(5), Math.round(cartographic.height / 2), camera.heading.toFixed(3), camera.pitch.toFixed(3), ].join(':'); } /** * World position under the center of the viewport, or null when the view has no * target. Each stage of the cascade is validated before it is accepted: a depth * pick over empty sky can return a NaN or center-of-the-earth Cartesian, and * converting one of those throws deep inside Cesium. A degenerate pick is a * MISSED pick, so it falls through to the next stage rather than poisoning * every caller downstream. */ function getViewTargetCartesian(viewer) { const scene = viewer.scene; const canvas = scene.canvas; const width = canvas.clientWidth || canvas.width || 0; const height = canvas.clientHeight || canvas.height || 0; const center = new Cesium.Cartesian2(width / 2, height / 2); let position = null; if (scene.pickPositionSupported && typeof scene.pickPosition === 'function') { try { position = scene.pickPosition(center); } catch { position = null; } } if ( !isPickedWorldPosition(position) && viewer.camera && typeof viewer.camera.pickEllipsoid === 'function' ) { try { position = viewer.camera.pickEllipsoid(center, Cesium.Ellipsoid.WGS84); } catch { position = null; } } if ( !isPickedWorldPosition(position) && viewer.camera && typeof viewer.camera.getPickRay === 'function' ) { try { const ray = viewer.camera.getPickRay(center); position = scene.globe?.pick(ray, scene) || null; } catch { position = null; } } return isPickedWorldPosition(position) ? position : null; } function sampleViewportCartographics(viewer) { const scene = viewer.scene; const canvas = scene.canvas; const width = canvas.clientWidth || canvas.width || 0; const height = canvas.clientHeight || canvas.height || 0; if (!width || !height) return []; const points = [ [0.5, 0.5], [0.25, 0.35], [0.75, 0.35], [0.25, 0.65], [0.75, 0.65], [0.5, 0.25], [0.5, 0.75], ]; const samples = []; for (const [x, y] of points) { const cartesian = viewer.camera.pickEllipsoid( new Cesium.Cartesian2(width * x, height * y), Cesium.Ellipsoid.WGS84, ); if (!isPickedWorldPosition(cartesian)) continue; const carto = Cesium.Cartographic.fromCartesian(cartesian); if (!carto) continue; samples.push({ latitude: Number(Cesium.Math.toDegrees(carto.latitude).toFixed(4)), longitude: Number(Cesium.Math.toDegrees(carto.longitude).toFixed(4)), }); } return samples; } function inferCountryFromSamples(samples) { if (!samples.length) return null; const counts = new Map(); for (const sample of samples) { const country = inferCountry(sample.latitude, sample.longitude); if (!country) continue; counts.set(country, (counts.get(country) || 0) + 1); } if (!counts.size) return null; const [country, count] = [...counts.entries()].sort((a, b) => b[1] - a[1])[0]; return { country, confidence: Number((count / samples.length).toFixed(2)), sampleCount: count, totalSamples: samples.length, }; } function inferCountry(latitude, longitude) { const regions = [ { name: 'Iran', south: 24.0, north: 40.2, west: 44.0, east: 63.5 }, { name: 'Iraq', south: 29.0, north: 37.5, west: 38.5, east: 49.0 }, { name: 'Turkey', south: 35.5, north: 42.5, west: 25.5, east: 45.2 }, { name: 'Saudi Arabia', south: 16.0, north: 32.5, west: 34.0, east: 56.5 }, { name: 'Afghanistan', south: 29.0, north: 38.8, west: 60.0, east: 75.5 }, { name: 'Pakistan', south: 23.0, north: 37.2, west: 60.5, east: 77.5 }, { name: 'Turkmenistan', south: 35.0, north: 42.9, west: 52.0, east: 66.8 }, { name: 'Azerbaijan', south: 38.3, north: 41.9, west: 44.6, east: 50.8 }, { name: 'Armenia', south: 38.7, north: 41.4, west: 43.4, east: 46.7 }, { name: 'Japan', south: 24.0, north: 46.5, west: 122.0, east: 146.5 }, { name: 'South Korea', south: 33.0, north: 38.8, west: 124.0, east: 132.0 }, { name: 'North Korea', south: 37.5, north: 43.2, west: 124.0, east: 131.0 }, { name: 'China', south: 18.0, north: 53.8, west: 73.0, east: 135.2 }, { name: 'Russia', south: 41.0, north: 82.0, west: 19.0, east: 180.0 }, { name: 'United States', south: 24.0, north: 49.8, west: -125.0, east: -66.0, }, ]; const region = regions.find( (item) => latitude >= item.south && latitude <= item.north && longitude >= item.west && longitude <= item.east, ); return region?.name || null; } async function reverseGeocode(latitude, longitude, service) { const { reverseGeocodeCache, reverseGeocodeInFlight } = cachesFor(service); if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null; const key = reverseGeocodeKey(latitude, longitude, service); if (reverseGeocodeCache.has(key)) return reverseGeocodeCache.get(key); if (reverseGeocodeInFlight.has(key)) return reverseGeocodeInFlight.get(key); const request = (async () => { try { const place = (await service.reverseGeocode?.(latitude, longitude)) || null; if (place) { reverseGeocodeCache.set(key, place); if (reverseGeocodeCache.size > 256) reverseGeocodeCache.delete(reverseGeocodeCache.keys().next().value); } return place; } catch { return null; } finally { reverseGeocodeInFlight.delete(key); } })(); reverseGeocodeInFlight.set(key, request); return request; } async function reverseGeocodeViewportSamples(samples, cameraHeightM, service) { if (!shouldReverseGeocodeViewport(cameraHeightM) || !samples.length) return null; // At building scale, center geocoding plus Nearby Places is more precise and // avoids three redundant Google requests. if (cameraHeightM <= 10000) return null; const prioritySamples = [samples[0], samples[1], samples[2]].filter(Boolean); const places = ( await Promise.all( prioritySamples.map(async (sample) => { const place = await reverseGeocode( sample.latitude, sample.longitude, service, ); if (!place) return null; return { latitude: sample.latitude, longitude: sample.longitude, formattedAddress: place.formattedAddress, locality: place.locality, region: place.region, country: place.country, types: place.types, labels: place.labels, streetLabels: place.streetLabels, }; }), ) ).filter(Boolean); return summarizeViewportPlaces(places); } function viewportPlacesFromCache(samples, cameraHeightM, service) { const { reverseGeocodeCache } = cachesFor(service); if ( !shouldReverseGeocodeViewport(cameraHeightM) || cameraHeightM <= 10000 || !samples.length ) return null; const places = [samples[0], samples[1], samples[2]] .filter(Boolean) .flatMap((sample) => { const place = reverseGeocodeCache.get( reverseGeocodeKey(sample.latitude, sample.longitude, service), ); if (!place) return []; return [ { latitude: sample.latitude, longitude: sample.longitude, formattedAddress: place.formattedAddress, locality: place.locality, region: place.region, country: place.country, types: place.types, labels: place.labels, streetLabels: place.streetLabels, }, ]; }); return summarizeViewportPlaces(places); } function summarizeViewportPlaces(places) { if (!places.length) return null; return { samples: places, dominantCountry: dominantValue( places.map((place) => place.country).filter(Boolean), ), dominantRegion: dominantValue( places.map((place) => place.region).filter(Boolean), ), dominantLocality: dominantValue( places.map((place) => place.locality).filter(Boolean), ), visibleLabels: uniqueStrings( places.flatMap((place) => place.labels || []), ).slice(0, 24), streetLabels: uniqueStrings( places.flatMap((place) => place.streetLabels || []), ).slice(0, 20), }; } async function fetchNearbyPlaces(latitude, longitude, cameraHeightM, service) { const { nearbyPlacesCache, nearbyPlacesInFlight } = cachesFor(service); const radiusM = nearbyPlacesRadiusM(cameraHeightM); const cacheKey = nearbyPlacesCacheKey( latitude, longitude, cameraHeightM, service, ); if (nearbyPlacesCache.has(cacheKey)) return nearbyPlacesCache.get(cacheKey); if (nearbyPlacesInFlight.has(cacheKey)) return nearbyPlacesInFlight.get(cacheKey); const request = (async () => { try { const places = ( (await service.nearby?.({ latitude, longitude, radiusM })) || [] ) .filter((place) => place?.name) .slice(0, 12); nearbyPlacesCache.set(cacheKey, places); if (nearbyPlacesCache.size > 256) nearbyPlacesCache.delete(nearbyPlacesCache.keys().next().value); return places; } catch { return []; } finally { nearbyPlacesInFlight.delete(cacheKey); } })(); nearbyPlacesInFlight.set(cacheKey, request); return request; } function uniqueStrings(values) { return [ ...new Set(values.map((value) => sanitizeLabel(value)).filter(Boolean)), ]; } /** * Normalize a feed-sourced label (place/street/POI name from OSM, geocoding, * Google Places, etc.) before it enters the voice LLM's scene context. * Collapses newlines/control chars to single spaces and hard-caps length, so * crafted map data can't smuggle multi-line "instructions" into the prompt. * Defense-in-depth — these are reference labels, not commands. * @param {*} value - Raw label value. * @returns {string} Sanitized single-line label (max 120 chars). */ function sanitizeLabel(value) { const text = String(value || ''); let out = ''; for (const ch of text) { const code = ch.codePointAt(0); out += code < 0x20 || code === 0x7f ? ' ' : ch; // drop control chars incl. newlines } return out.replace(/\s+/g, ' ').trim().slice(0, 120); } function cachePrecision(service) { return Number.isInteger(service?.cachePrecision) ? Math.max(3, Math.min(6, service.cachePrecision)) : 4; } function reverseGeocodeKey(latitude, longitude, service) { return `${latitude.toFixed(cachePrecision(service))},${longitude.toFixed(cachePrecision(service))}`; } function nearbyPlacesCacheKey(latitude, longitude, cameraHeightM, service) { const radiusM = nearbyPlacesRadiusM(cameraHeightM); return `${latitude.toFixed(cachePrecision(service))},${longitude.toFixed(cachePrecision(service))},${radiusM}`; } function nearbyPlacesRadiusM(cameraHeightM) { if (cameraHeightM <= 1000) return 500; if (cameraHeightM <= 5000) return 2000; return 5000; } async function resolveWithin(promise, timeoutMs, fallback) { let timeout = null; try { return await Promise.race([ Promise.resolve(promise).catch(() => fallback), new Promise((resolve) => { timeout = window.setTimeout(() => resolve(fallback), timeoutMs); }), ]); } finally { if (timeout) window.clearTimeout(timeout); } } function approximateCoordinateDistanceSq(latA, lonA, latB, lonB) { const latDelta = latB - latA; const lonDelta = (lonB - lonA) * Math.cos(Cesium.Math.toRadians((latA + latB) / 2)); return latDelta * latDelta + lonDelta * lonDelta; } function logSlowContext(startedAt, scope) { const durationMs = Math.round(performance.now() - startedAt); if (durationMs >= 500) { console.info( `[GEV Voice] ${scope} scene context completed in ${durationMs}ms`, ); } } function dominantValue(values) { if (!values.length) return null; const counts = new Map(); for (const value of values) counts.set(value, (counts.get(value) || 0) + 1); const [value, count] = [...counts.entries()].sort((a, b) => b[1] - a[1])[0]; return { value, count, confidence: Number((count / values.length).toFixed(2)), }; } function summarizeContextRecord(record, { includeProperties = false } = {}) { return { id: String(record.id || ''), name: cleanText(record.label || record.properties?.name) || layerTitle(record.layerId), layerId: record.layerId || null, layerName: record.layerName || layerTitle(record.layerId), source: record.source || null, latitude: record.latitude ?? null, longitude: record.longitude ?? null, properties: includeProperties ? compactProperties(record.properties || {}) : undefined, active: isContextRecordActive(record), }; } function compactProperties(props) { const preferredKeys = [ 'name', 'operator', 'owner', 'brand', 'addr:city', 'addr:state', 'country', 'capacity', 'output', 'osm_id', 'source', ]; const flat = { ...props, ...(props.tags && typeof props.tags === 'object' ? props.tags : {}), }; const result = {}; for (const key of preferredKeys) { const value = cleanText(flat[key]); if (value) result[key] = value; } for (const [key, value] of Object.entries(flat)) { if (Object.keys(result).length >= 12) break; if (key === 'tags' || result[key] !== undefined) continue; const text = cleanText(value); if (text) result[key] = text; } return result; } function cleanText(value) { if (value == null || typeof value === 'object') return ''; const text = String(value).trim(); if (!text || text === 'undefined' || text === 'null') return ''; return text.length > 180 ? `${text.slice(0, 177)}...` : text; } function layerTitle(layerId) { if (layerId === 'local-datacenters') return 'Datacenter'; if (layerId === 'local-dams') return 'Dam'; if (layerId === 'telegeography-submarine-cables') return 'Submarine Cable'; if (layerId === 'local-firms') return 'Active Fire'; return layerId || 'Entity'; } function clampNumber(value, min, max, fallback) { const numeric = Number(value); if (!Number.isFinite(numeric)) return fallback; return Math.max(min, Math.min(max, numeric)); } /** * Analyst query — spoken questions over data already on the client * ("how many flights over Texas?", "biggest fire near LA?", "which ships * are headed to Oakland?"). The ENGINE (analystEngine.js) does the query * logic; this wiring supplies live providers and compacts the result for * the voice payload. One engine per runner keeps follow-up memory * ("which of those is closest?") scoped to the session. */ /** * Most records one layer hands a single query. Above every loaded feed seen * so far (flights ~8k, vessels ~12k, a 24 h global FIRMS day ~160k), so * answers count the whole loaded set; a feed past it reports * returned/total/truncated rather than a silently short count. */ const ANALYST_RECORD_CAP = 250_000; /** A voice-enabled layer is still filling its first poll for this long. */ const ANALYST_WARMUP_MS = 45_000; function layerIsWarming(enabledAt, layerKey) { const at = enabledAt.get(layerKey); return Boolean(at && Date.now() - at < ANALYST_WARMUP_MS); } function analystProviders( viewer, dataManager, { recordLimitByLayer = null, placeSearch = unavailablePlaceSearch, resolveRegionRing = (name) => defaultAnnotationResolver.resolveRegionRingForQuery( name, undefined, placeSearch, ), isWarming = () => false, } = {}, ) { // Per-layer truncation seen by the last getRecords call. const truncatedBy = new Map(); return { getRecords(layerKey) { truncatedBy.delete(layerKey); const layer = dataManager.layers.get(layerKey); if (!layer || !dataManager.isEnabled(layerKey)) return []; const mod = layer.module; if (typeof mod?.getAnalystRecords !== 'function') return []; const requestedLimit = recordLimitByLayer?.[layerKey]; const cap = Number.isFinite(requestedLimit) ? requestedLimit : ANALYST_RECORD_CAP; // One past the cap proves whether anything was left out. const rows = mod.getAnalystRecords(Math.min(cap + 1, 2 ** 31)) || []; if (rows.length > cap) { truncatedBy.set(layerKey, true); return rows.slice(0, cap); } return rows; }, getLayerSnapshot(layerKey) { const row = dataManager.getAll?.().find((layer) => layer.id === layerKey); if (row) return layerSnapshot(row); const module = dataManager.layers?.get(layerKey)?.module; return layerSnapshot({ id: layerKey, enabled: dataManager.isEnabled?.(layerKey), stats: module?.getStats?.() || {}, }); }, getRecordCoverage(layerKey, rows) { if (!truncatedBy.get(layerKey)) return { total: rows.length }; const loaded = dataManager.layers .get(layerKey) ?.module?.getStats?.().count; return { total: Number.isFinite(loaded) && loaded > rows.length ? loaded : null, truncated: true, }; }, isWarming, /** The layer's own status text for an empty result, e.g. "Fly below 400 km". */ getEmptyNote(layerKey) { const stats = dataManager.layers.get(layerKey)?.module?.getStats?.() || {}; const note = [stats.coverage, stats.statusMessage].find( (value) => typeof value === 'string' && value.trim(), ); return note ? note.trim() : null; }, resolveRegionRing, /** * The active Contacts subject, when there is one — the centre the operator * is reasoning about while Contacts is up. Null whenever Contacts is off, * so view-centred behaviour is unchanged outside it. * @returns {{lat: number, lon: number, label: string|null}|null} Subject centre. */ getContextSubject() { const snapshot = dataManager?.layers ?.get('military-awareness') ?.module?.getContextSnapshot?.(); const subject = snapshot?.subject; if (!subject?.position) return null; const carto = Cesium.Cartographic.fromCartesian(subject.position); if (!carto) return null; return { lat: Cesium.Math.toDegrees(carto.latitude), lon: Cesium.Math.toDegrees(carto.longitude), label: subject.label || subject.id || null, }; }, getViewContext() { const carto = viewer.camera.positionCartographic; const altKm = carto.height / 1000; // View radius scales with altitude: street-level asks stay local, // country-level asks sweep wide. Clamped so "in view" is never absurd. const viewRadiusKm = Math.max(25, Math.min(2500, altKm * 1.6)); return { lat: Cesium.Math.toDegrees(carto.latitude), lon: Cesium.Math.toDegrees(carto.longitude), viewRadiusKm, }; }, }; } /** Identity fields every compact analyst item keeps when present. */ const ANALYST_IDENTITY_FIELDS = Object.freeze([ 'icao24', 'mmsi', 'registration', 'label', 'callsign', 'name', 'noradId', ]); /** * Compact one analyst record for the voice payload: identity, position (so a * follow-up can fly to it), distance, and that layer's manifest fields with * times as ISO text. The full record set stays engine-side for follow-ups. * * `icao24`/`mmsi` ride along because the tool instructions tell the model to * hand this result straight to track_entity, and `id` is a DISPLAY label * (callsign, else registration, else hex). A callsign-less contact therefore * handed track_entity a tail number the lookup could not resolve, and the * model burned the turn on retries (owner field session 2026-08-21, 23:48). * @param {object} r Engine record. * @returns {object} Compact item. */ function compactAnalystItem(r) { const compact = { layerKey: r.layerKey, id: r.id }; const present = (value) => value !== null && value !== undefined; for (const key of ANALYST_IDENTITY_FIELDS) if (present(r[key])) compact[key] = r[key]; if (Number.isFinite(r.lat) && Number.isFinite(r.lon)) { compact.lat = Math.round(r.lat * 1e4) / 1e4; compact.lon = Math.round(r.lon * 1e4) / 1e4; } if (present(r.distanceKm)) compact.distanceKm = r.distanceKm; const fields = voiceLayer(r.layerKey)?.query?.fields || {}; for (const [key, spec] of Object.entries(fields)) { if (!present(r[key]) || key in compact) continue; if (spec.type === 'time') { const ms = Number(r[key]); // A finite value can still be past Date's range, where toISOString throws. const time = new Date(ms); if (Number.isFinite(time.getTime())) { if (key === 'lastContactAtMs') compact.lastContactAtMs = ms; else compact.time = time.toISOString(); } } else compact[key] = r[key]; } if (present(r.ageHours)) compact.ageHours = r.ageHours; return compact; } /** Refusal fields the analyst engine owns and the model must receive. */ const ANALYST_REFUSAL_FIELDS = Object.freeze([ 'allowed', 'field', 'expected', 'layers', 'layerStatus', 'cancelled', ]); const VIEWPORT_COVERAGE_NOTE = 'the flights layer loads by viewport'; /** Resolve omitted follow-up scope from the engine's retained coverage. */ function effectiveAnalystScopeKind(args, coverage) { const explicit = String(args.scope?.kind || '').toLowerCase(); if (explicit) return explicit; const retained = String(coverage?.scope || '') .split(':', 1)[0] .toLowerCase(); if (retained === 'window') return 'radius'; if (['anywhere', 'radius', 'region', 'view'].includes(retained)) return retained; return args.followUp ? 'anywhere' : 'view'; } /** Add the viewport qualification without replacing retained provenance. */ function withViewportCoverageNote(coverage) { if (!coverage) return coverage; const current = typeof coverage.note === 'string' ? coverage.note : ''; if (current.includes(VIEWPORT_COVERAGE_NOTE)) return coverage; const separator = current ? '; ' : ''; return { ...coverage, note: current ? `${current}${separator}${VIEWPORT_COVERAGE_NOTE}` : VIEWPORT_COVERAGE_NOTE, }; } /** Whether this query counts records from a viewport-loaded layer. */ function isViewportScopedAnalystResult(args, coverage) { const scopeKind = effectiveAnalystScopeKind(args, coverage); return ( (scopeKind === 'radius' || scopeKind === 'view') && (coverage?.layersQueried || []).some( (layer) => voiceLayer(layer.layerKey)?.query?.viewportLoaded, ) ); } async function runAnalystQuery( analystEngine, dataManager, args = {}, _layerEnabledAt, isCurrent = () => true, signal = undefined, ) { let entityWindow = null; const result = await analystEngine.query( { layers: Array.isArray(args.layers) ? args.layers : undefined, scope: args.scope, filters: Array.isArray(args.filters) ? args.filters : [], sortBy: args.sortBy || null, sortDir: args.sortDir, limit: args.limit, followUp: Boolean(args.followUp), }, { isCurrent, signal, beforeCommit: async (candidate) => { const projected = await aircraftProximityWindowForQuery( dataManager, args, candidate, isCurrent, ); if ( projected?.answer && projected?.memory && isViewportScopedAnalystResult(args, projected.answer.coverage) ) { const coverage = withViewportCoverageNote(projected.answer.coverage); projected.answer.coverage = coverage; projected.memory.coverage = coverage; } entityWindow = projected?.answer || null; return projected?.memory || null; }, }, ); if (!result.ok) { const refusal = { ok: false, action: 'analyst_query', code: result.code || null, error: result.error, }; // The engine's semantic fields pass through unchanged: a cancellation // stays a cancellation. for (const key of ANALYST_REFUSAL_FIELDS) if (result[key] !== undefined) refusal[key] = result[key]; if (result.coverage) refusal.coverage = result.coverage; return refusal; } const items = result.items.map(compactAnalystItem); // Warm-up honesty: a layer enabled seconds ago hasn't finished its first // poll (entity layers render one interval behind live BY DESIGN) — so a low // count is "still loading", not fact. const warming = (result.coverage?.layersQueried || []) .filter((l) => layerIsWarming(_layerEnabledAt, l.layerKey)) .map((l) => l.layerKey); if (warming.length) { const note = `${warming.join(', ')} just turned on — still loading, counts will rise`; result.display.caveat = result.display.caveat ? `${note}; ${result.display.caveat}` : note; } // A radius/view count over a viewport-loaded layer counts what is LOADED, and // the flights layer reloads as the camera moves — so this number can sit well // under the Contacts cohort without either being wrong. const scopeKind = effectiveAnalystScopeKind(args, result.coverage); const viewportScoped = isViewportScopedAnalystResult(args, result.coverage); if (viewportScoped && result.coverage) { // The engine's follow-up memory retains this same object, so update its // note in place after deriving the qualified copy. result.coverage.note = withViewportCoverageNote(result.coverage).note; } // ENTITY-CENTRED NEARBY: answered by the SAME engine that fills the Contacts // panel, so the spoken number and the panel readout for one centre cannot // differ. The generic record/scope engine still owns explicit regions and // arbitrary points — only "how many aircraft around " is // unified, because that is the question the panel is already answering. if (entityWindow) { return entityWindow; } const contactsWindow = activeContactsWindow(dataManager); const aircraftQueried = (result.coverage?.layersQueried || []).some( (l) => l.layerKey === 'flights' || l.layerKey === 'military', ); // Both numbers, and which one answers the question. The window counts have // ridden along in `contactsWindow` for a while, and the owner's trial showed // that is not enough on its own: with Contacts active and a DATACENTER in // the selection slot, the model centred a radius on the datacenter, answered // 15, and then explained away the 111 sitting in the same payload ("that // number is from the Contacts window, and I wasn't using Contacts as the // source"). So the payload now states the relationship instead of leaving it // to be inferred from two bare numbers. const windowAircraft = Number.isFinite(contactsWindow?.aircraft) ? contactsWindow.aircraft : null; const proximityScoped = scopeKind === 'radius' || scopeKind === 'view'; const countsReconciliation = contactsWindow && aircraftQueried && proximityScoped && windowAircraft !== null ? `Contacts is ACTIVE: its window holds ${windowAircraft} aircraft within ` + `${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}. ` + 'Give this one only if the operator asked about that specific area, and name both scopes if you give both.' : null; return { ok: true, action: 'analyst_query', count: result.count, // False when a layer was capped: the count is a floor ("at least"). complete: result.complete !== false, ...(result.partial ? { partial: true, unanswered: result.unanswered || [] } : {}), ...(result.centeredOn ? { centeredOn: result.centeredOn } : {}), // Every count names its scope in words; a bare number is what made two // honest answers look like a contradiction. scopeLabel: result.scopeLabel, // More matched than listed; the count above is still complete. listed: items.length, items, summary: result.summary, // On-screen detail (scope, feed window, caveats); not for recital. display: result.display, coverage: result.coverage, feedProvenance: result.coverage?.feedProvenance || null, feedState: result.coverage?.feedProvenance?.overall || null, // The panel's own numbers, carried so the answer can match what the // operator is looking at regardless of how the model reads the note. // Flattened alongside the object so the count and its subject cannot be // missed inside a nested shape. ...(contactsWindow && aircraftQueried ? { contactsWindow, contactsWindowCount: windowAircraft, contactsWindowSubject: contactsWindow.centeredOn || null, } : {}), ...(countsReconciliation ? { countsReconciliation } : {}), }; }