refactor: compose configurable geocoding and route services

This commit is contained in:
Sameh Khamis committed 2026-09-13 21:23:13 -07:00
1 parent 3b91feffdc
commit 8b36c28b06
25 files changed
+764 -211

No files matched your search

+7 -21
View File
@@ -1,3 +1,4 @@
import { defaultGeospatial } from '../search/defaults.js';
import * as Cesium from 'cesium';
import { holdContinuousRender, releaseContinuousRender } from '../renderGovernor.js';
import { isRateLimitedOutcome, resolveAnnotationTarget } from './annotationResolver.js';
@@ -94,7 +95,7 @@ export function normalizeTargetKey(target) {
}
export function createAnnotationEngine({
placeSearch,
placeSearch = defaultGeospatial,
viewer,
renderer,
outlineRetryDelaysMs = OUTLINE_RETRY_DELAYS_MS,
@@ -418,7 +419,7 @@ export function createAnnotationEngine({
}
// Real street-following route (OSM/OSRM), mode-aware.
const mode = normalizeMode(spec.mode);
const routed = await fetchRoute(resolvedPts.map((p) => [p.lon, p.lat]), mode, signal);
const routed = await fetchRoute(resolvedPts.map((p) => [p.lon, p.lat]), mode, signal, placeSearch);
if (routed) {
return {
path: routed.geometry.map(([lon, lat]) => ({ lon, lat, height: 0 })),
@@ -1101,25 +1102,10 @@ function normalizeMode(m) {
return 'foot';
}
/** Fetch a real street-following route from the /api/route proxy (OSM/OSRM). */
async function fetchRoute(coordPairs, mode, externalSignal) {
const controller = new AbortController();
const onAbort = () => controller.abort();
if (externalSignal) {
if (externalSignal.aborted) controller.abort();
else externalSignal.addEventListener('abort', onAbort, { once: true });
}
const timer = setTimeout(() => controller.abort(), 13000);
try {
const coords = coordPairs.map(([lon, lat]) => `${lon.toFixed(6)},${lat.toFixed(6)}`).join(';');
const res = await fetch(`/api/route?profile=${mode}&coords=${encodeURIComponent(coords)}`, { signal: controller.signal });
const data = await res.json();
if (data?.ok && Array.isArray(data.geometry) && data.geometry.length >= 2) return data;
} catch { /* routing unavailable / aborted → caller falls back to straight segments */ } finally {
clearTimeout(timer);
if (externalSignal) externalSignal.removeEventListener('abort', onAbort);
}
return null;
/** Route failure remains an explicitly labelled direct line in the caller. */
async function fetchRoute(coordPairs, mode, signal, service) {
try { return await service.route?.(coordPairs, mode, { signal }) || null; }
catch { return null; }
}
function greatCircleM(a, b) {
+12 -15
View File
@@ -1,3 +1,4 @@
import { defaultGeospatial } from '../search/defaults.js';
import * as Cesium from 'cesium';
import { lookupNeighborhoodRing } from '../data/neighborhoodPolygons.js';
import { lookupNaturalRegionOutline, findNaturalRegion } from '../data/naturalEarthRegions.js';
@@ -124,7 +125,7 @@ export async function resolveAnnotationTarget({
// miss we fall through to geocode + fetchLocalMonument below. The model's entityKind counts too:
// a point_feature by fact ("Heroes of the Alamo" — no monument word) deserves the same path.
if (center && (isMonumentLikeQuery(query) || isGroundsLikeQuery(query) || entityKind === 'point_feature')) {
const placeHit = await placesTextSearch(query, center.lat, center.lon, 6000, signal);
const placeHit = await placesTextSearch(query, center.lat, center.lon, 6000, signal, placeSearch);
if (placeHit) {
trace.places = `${placeHit.lat.toFixed(5)},${placeHit.lon.toFixed(5)}`;
if (placeHit.distanceM <= PLACES_MAX_DISTANCE_M) {
@@ -163,7 +164,7 @@ export async function resolveAnnotationTarget({
geocodeFar = true;
}
if (geocodeFar) {
const placeHit = await placesTextSearch(query, center.lat, center.lon, 6000, signal);
const placeHit = await placesTextSearch(query, center.lat, center.lon, 6000, signal, placeSearch);
if (placeHit && placeHit.distanceM <= PLACES_MAX_DISTANCE_M) {
lat = placeHit.lat;
lon = placeHit.lon;
@@ -627,7 +628,7 @@ function normalizeGeocodeViewport(vp) {
};
}
const placesCache = new Map(); // Text Search hits, keyed by query + rounded view centre
const placesCaches = new WeakMap(); // Cache isolated by provider configuration
/**
* View-biased Google Places TEXT SEARCH for a named landmark/POI. Geocoding
@@ -640,7 +641,9 @@ const placesCache = new Map(); // Text Search hits, keyed by query + rounded vie
* @returns {Promise<null | { lat:number, lon:number, label:string|null, distanceM:number,
* viewport:{low:{latitude:number,longitude:number},high:{latitude:number,longitude:number}}|null }>}
*/
async function placesTextSearch(query, centerLat, centerLon, radiusM, signal) {
async function placesTextSearch(query, centerLat, centerLon, radiusM, signal, service = defaultGeospatial) {
const placesCache = placesCaches.get(service) || new Map();
placesCaches.set(service, placesCache);
const q = String(query || '').trim();
if (!q || !Number.isFinite(centerLat) || !Number.isFinite(centerLon)) return null;
@@ -648,16 +651,10 @@ async function placesTextSearch(query, centerLat, centerLon, radiusM, signal) {
const cached = cacheRead(placesCache, cacheKey);
if (cached !== undefined) return cached;
const params = new URLSearchParams({
q,
lat: String(centerLat),
lon: String(centerLon),
radiusM: String(radiusM),
});
try {
const response = await fetch(`/api/google/text-search?${params}`, { signal });
if (!response.ok) { negCache(placesCache, cacheKey, signal, false); return null; } // transient
const data = await response.json();
const data = { places: await service.textSearch?.(q, {
latitude: centerLat, longitude: centerLon, radiusM,
}, { signal }) };
const hit = Array.isArray(data?.places)
? data.places.find((p) => Number.isFinite(p?.latitude) && Number.isFinite(p?.longitude))
: null;
@@ -1788,13 +1785,13 @@ export function viewportBias(viewer) {
* plain geocode path. Returns the Places hit
* ({ lat, lon, label, types, viewport, distanceM, … }) or null.
*/
export async function placesNearViewRecovery(viewer, query, geocoded = null, signal = undefined) {
export async function placesNearViewRecovery(viewer, query, geocoded = null, signal = undefined, placeSearch = defaultGeospatial) {
const center = pickWorldFromScreen(viewer, 0.5, 0.5) || viewportProximity(viewer);
if (!center) return null;
const geocodeFar = !geocoded
|| approximateDistanceM(center.lat, center.lon, geocoded.lat, geocoded.lon) / 1000 > MIN_DRIFT_FLOOR_KM;
if (!geocodeFar) return null;
const hit = await placesTextSearch(query, center.lat, center.lon, 6000, signal);
const hit = await placesTextSearch(query, center.lat, center.lon, 6000, signal, placeSearch);
return (hit && hit.distanceM <= PLACES_MAX_DISTANCE_M) ? hit : null;
}