feat(search): compose Google and Photon through an explicit provider service

This commit is contained in:
Sameh Khamis committed 2026-09-12 15:38:43 -07:00
commit 57d7f9fc23
30 files changed
+1621 -151

No files matched your search

+18 -32
View File
@@ -17,6 +17,7 @@ import militaryAwarenessLayer, {
import { initCameraVerbs, moveCamera, flyRoute, interruptCameraMotion, adjustOrbitRange } from '../cameraVerbs.js';
import { cachedGroundFloor, warmGroundFloor } from '../data/groundFloor.js';
import { isPickedWorldPosition } from '../data/scenePick.js';
import { unavailablePlaceSearch } from '../search/placeSearch.js';
import { resolveRegionRingForQuery } from '../annotations/annotationResolver.js';
import { normalizeRadioCountryInput } from '../data/radioCountry.js';
import { TR3B_CLASS } from '../data/tr3bRegistry.js';
@@ -287,7 +288,7 @@ export function readLayerLifecycleSummary(dataManager, layerId, { fallbackEnable
};
}
export function createGevActionRunner({ viewer, styleManager, dataManager, sceneDirector = null, annotations = null }) {
export function createGevActionRunner({ viewer, styleManager, dataManager, sceneDirector = null, annotations = null, placeSearch = unavailablePlaceSearch }) {
installViewTargetPrewarm(viewer);
initCameraVerbs(viewer, getViewTargetCartesian);
return async function runGevAction(name, rawArgs = {}, runOptions = {}) {
@@ -510,7 +511,7 @@ export function createGevActionRunner({ viewer, styleManager, dataManager, scene
};
const nearest = await createAnalystEngine(analystProviders(viewer, dataManager, {
recordLimitByLayer: { [layerId]: Number.MAX_SAFE_INTEGER },
recordLimitByLayer: { [layerId]: Number.MAX_SAFE_INTEGER }, placeSearch,
})).query({
layers: [layerId],
scope: { kind: 'view' },
@@ -769,6 +770,7 @@ export function createGevActionRunner({ viewer, styleManager, dataManager, scene
if (name === 'fly_to_location') {
return flyToRequestedLocation(viewer, args, {
placeSearch, signal: runOptions.signal,
runImmediate: typeof styleManager?.runImmediateLocationNavigation === 'function'
? (navigate) => styleManager.runImmediateLocationNavigation(navigate)
: null,
@@ -814,7 +816,7 @@ export function createGevActionRunner({ viewer, styleManager, dataManager, scene
}
if (name === 'analyst_query') {
return runAnalystQuery(viewer, dataManager, args);
return runAnalystQuery(viewer, dataManager, args, placeSearch);
}
if (name === 'move_camera') {
@@ -899,7 +901,7 @@ export function createGevActionRunner({ viewer, styleManager, dataManager, scene
}
if (name === 'control_radio') {
return controlRadio(viewer, dataManager, args, runOptions);
return controlRadio(viewer, dataManager, args, { ...runOptions, placeSearch });
}
if (name === 'track_entity') {
@@ -1264,30 +1266,12 @@ async function resolveRadioLocation(args = {}, coordinates = radioCoordinatePair
const known = knownRadioLocation(query, args.locationId);
if (known) return known;
if (!query) return null;
const apiKey = window.__GOOGLE_MAPS_API_KEY__ || import.meta.env.GOOGLE_MAPS_API_KEY;
if (!apiKey) throw new Error('No Google Maps API key available for Radio location search');
const controller = new AbortController();
const cancelFromTurn = () => controller.abort();
if (options.signal?.aborted) throw radioAbortError();
options.signal?.addEventListener('abort', cancelFromTurn, { once: true });
const timer = setTimeout(() => controller.abort(), 6000);
try {
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(query)}&key=${apiKey}`;
const response = await fetch(url, { signal: controller.signal });
const body = await response.json();
if (!radioActionIsCurrent(options)) throw radioAbortError();
const result = body.status === 'OK' ? body.results?.[0] : null;
if (!result?.geometry?.location) return null;
return {
lat: result.geometry.location.lat,
lon: result.geometry.location.lng,
label: result.formatted_address || query,
country: '',
};
} finally {
clearTimeout(timer);
options.signal?.removeEventListener('abort', cancelFromTurn);
}
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. */
@@ -2206,6 +2190,7 @@ function normalizeStyle(value) {
}
async function flyToRequestedLocation(viewer, args, {
placeSearch = unavailablePlaceSearch, signal,
onStart = null,
runImmediate = null,
beginDeferred = null,
@@ -2321,6 +2306,7 @@ async function flyToRequestedLocation(viewer, args, {
if (generation === false) return cancelled(query);
const managedDeferred = typeof reassertDeferred === 'function';
const destination = await searchAndFlyTo(viewer, query, {
placeSearch, signal,
...(rangeM ? { range: rangeM } : {}),
forceClose: args.viewMode === 'close',
// 'overview' frames the geocode viewport even for precise-place results —
@@ -3310,7 +3296,7 @@ function activeContactsWindow() {
}
}
function analystProviders(viewer, dataManager, { recordLimitByLayer = null } = {}) {
function analystProviders(viewer, dataManager, { recordLimitByLayer = null, placeSearch = unavailablePlaceSearch } = {}) {
return {
getRecords(layerKey) {
const layer = dataManager.layers.get(layerKey);
@@ -3322,7 +3308,7 @@ function analystProviders(viewer, dataManager, { recordLimitByLayer = null } = {
? (mod.getAnalystRecords(requestedLimit) || [])
: (mod.getAnalystRecords() || []);
},
resolveRegionRing: (name) => resolveRegionRingForQuery(name),
resolveRegionRing: (name) => resolveRegionRingForQuery(name, undefined, placeSearch),
/**
* The active Contacts subject, when there is one — the centre the operator
* is reasoning about while Contacts is up. Null whenever Contacts is off,
@@ -3356,8 +3342,8 @@ function analystProviders(viewer, dataManager, { recordLimitByLayer = null } = {
};
}
async function runAnalystQuery(viewer, dataManager, args = {}) {
if (!_analystEngine) _analystEngine = createAnalystEngine(analystProviders(viewer, dataManager));
async function runAnalystQuery(viewer, dataManager, args = {}, placeSearch = unavailablePlaceSearch) {
if (!_analystEngine) _analystEngine = createAnalystEngine(analystProviders(viewer, dataManager, { placeSearch }));
const result = await _analystEngine.query({
layers: Array.isArray(args.layers) ? args.layers : undefined,
scope: args.scope,