mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
refactor: compose configurable geocoding and route services
This commit is contained in:
1 parent
3b91feffdc
commit
8b36c28b06
25 files changed
+764
-211
No files matched your search
+47
-74
@@ -1,3 +1,4 @@
|
||||
import { defaultGeospatial } from '../search/defaults.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { CITY_POIS, findPoiByName, flyToGlobeView, flyToLandmark, flyToPOI, flyToPresetLocation, GLOBE_VIEW, searchAndFlyTo } from '../locations.js';
|
||||
import {
|
||||
@@ -246,10 +247,20 @@ const FRAME_TARGETS = new Map([
|
||||
['ships', 'ais-live-vessels'],
|
||||
]);
|
||||
|
||||
const reverseGeocodeCache = new Map();
|
||||
const reverseGeocodeInFlight = new Map();
|
||||
const nearbyPlacesCache = new Map();
|
||||
const nearbyPlacesInFlight = new Map();
|
||||
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();
|
||||
@@ -842,7 +853,7 @@ export function createGevActionRunner({ viewer, styleManager, dataManager, scene
|
||||
}
|
||||
|
||||
if (name === 'get_entity_context') {
|
||||
return getEntityContext(viewer, dataManager, styleManager, args);
|
||||
return getEntityContext(viewer, dataManager, styleManager, args, placeSearch);
|
||||
}
|
||||
|
||||
if (name === 'get_current_view_state') {
|
||||
@@ -1885,7 +1896,7 @@ function collectTrackedEntities(dataManager) {
|
||||
return tracked;
|
||||
}
|
||||
|
||||
export async function getBasemapLabelContext(viewer) {
|
||||
export async function getBasemapLabelContext(viewer, service = defaultGeospatial) {
|
||||
const samples = sampleViewportCartographics(viewer);
|
||||
const cameraHeightM = viewer.camera.positionCartographic.height;
|
||||
const target = getViewTargetCartographic(viewer);
|
||||
@@ -1899,15 +1910,15 @@ export async function getBasemapLabelContext(viewer) {
|
||||
|
||||
const latitude = Number(Cesium.Math.toDegrees(target.latitude).toFixed(6));
|
||||
const longitude = Number(Cesium.Math.toDegrees(target.longitude).toFixed(6));
|
||||
const cachedViewportPlaces = viewportPlacesFromCache(samples, cameraHeightM);
|
||||
const cachedViewportPlaces = viewportPlacesFromCache(samples, cameraHeightM, service);
|
||||
const viewportPromise = cachedViewportPlaces
|
||||
? Promise.resolve(cachedViewportPlaces)
|
||||
: reverseGeocodeViewportSamples(samples, cameraHeightM);
|
||||
: reverseGeocodeViewportSamples(samples, cameraHeightM, service);
|
||||
const placePromise = shouldReverseGeocode(cameraHeightM)
|
||||
? reverseGeocode(latitude, longitude)
|
||||
? reverseGeocode(latitude, longitude, service)
|
||||
: Promise.resolve(null);
|
||||
const nearbyPromise = shouldFetchNearbyPlaces(cameraHeightM)
|
||||
? fetchNearbyPlaces(latitude, longitude, cameraHeightM)
|
||||
? fetchNearbyPlaces(latitude, longitude, cameraHeightM, service)
|
||||
: Promise.resolve([]);
|
||||
const [viewportPlaces, place, nearbyPlaces] = await Promise.all([
|
||||
resolveWithin(viewportPromise, BASEMAP_CONTEXT_WAIT_MS, cachedViewportPlaces),
|
||||
@@ -2381,7 +2392,7 @@ function getCurrentViewState(viewer, styleManager, dataManager, sceneDirector =
|
||||
};
|
||||
}
|
||||
|
||||
async function getEntityContext(viewer, dataManager, styleManager, args = {}) {
|
||||
async function getEntityContext(viewer, dataManager, styleManager, args = {}, service = defaultGeospatial) {
|
||||
const startedAt = performance.now();
|
||||
const scope = String(args.scope || 'auto').toLowerCase();
|
||||
const layerId = normalizeLayerId(args.layerId || args.layer);
|
||||
@@ -2389,7 +2400,7 @@ async function getEntityContext(viewer, dataManager, styleManager, args = {}) {
|
||||
const selected = selectedEntityContext(dataManager);
|
||||
const cameraHeightM = viewer.camera.positionCartographic.height;
|
||||
const viewTarget = getViewTargetCartographic(viewer);
|
||||
const scenePromise = getSceneContext(viewer, styleManager, dataManager, viewTarget);
|
||||
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 })
|
||||
@@ -2594,9 +2605,9 @@ function insertNearestRecord(records, candidate, limit) {
|
||||
records.pop();
|
||||
}
|
||||
|
||||
async function getSceneContext(viewer, styleManager, dataManager, viewTarget = null) {
|
||||
async function getSceneContext(viewer, styleManager, dataManager, viewTarget = null, service = defaultGeospatial) {
|
||||
const cartographic = Cesium.Cartographic.fromCartesian(viewer.camera.positionWC);
|
||||
const basemap = await getBasemapContext(viewer, viewTarget);
|
||||
const basemap = await getBasemapContext(viewer, viewTarget, service);
|
||||
const enabledLayers = dataManager.getAll()
|
||||
.filter((layer) => layer.enabled)
|
||||
.map((layer) => ({
|
||||
@@ -2617,16 +2628,17 @@ async function getSceneContext(viewer, styleManager, dataManager, viewTarget = n
|
||||
};
|
||||
}
|
||||
|
||||
async function getBasemapContext(viewer, viewTarget = null) {
|
||||
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);
|
||||
const cachedViewportPlaces = viewportPlacesFromCache(samples, cameraHeightM, service);
|
||||
const viewportPlacesPromise = cachedViewportPlaces
|
||||
? Promise.resolve(cachedViewportPlaces)
|
||||
: reverseGeocodeViewportSamples(samples, cameraHeightM);
|
||||
: reverseGeocodeViewportSamples(samples, cameraHeightM, service);
|
||||
if (!target) {
|
||||
const viewportPlaces = await resolveWithin(
|
||||
viewportPlacesPromise,
|
||||
@@ -2657,10 +2669,10 @@ async function getBasemapContext(viewer, viewTarget = null) {
|
||||
? nearbyPlacesCache.get(nearbyCacheKey)
|
||||
: null;
|
||||
const placePromise = shouldReverseGeocode(cameraHeightM) && !cachedPlace
|
||||
? reverseGeocode(latitude, longitude)
|
||||
? reverseGeocode(latitude, longitude, service)
|
||||
: Promise.resolve(cachedPlace);
|
||||
const nearbyPlacesPromise = shouldFetchNearbyPlaces(cameraHeightM) && !cachedNearbyPlaces
|
||||
? fetchNearbyPlaces(latitude, longitude, cameraHeightM)
|
||||
? fetchNearbyPlaces(latitude, longitude, cameraHeightM, service)
|
||||
: Promise.resolve(cachedNearbyPlaces);
|
||||
const [viewportPlaces, resolvedPlace, resolvedNearbyPlaces] = await Promise.all([
|
||||
resolveWithin(viewportPlacesPromise, BASEMAP_CONTEXT_WAIT_MS, cachedViewportPlaces),
|
||||
@@ -2929,44 +2941,20 @@ function inferCountry(latitude, longitude) {
|
||||
return region?.name || null;
|
||||
}
|
||||
|
||||
async function reverseGeocode(latitude, longitude) {
|
||||
const apiKey = window.__GOOGLE_MAPS_API_KEY__;
|
||||
if (!apiKey || !Number.isFinite(latitude) || !Number.isFinite(longitude)) return 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);
|
||||
if (reverseGeocodeCache.has(key)) return reverseGeocodeCache.get(key);
|
||||
if (reverseGeocodeInFlight.has(key)) return reverseGeocodeInFlight.get(key);
|
||||
|
||||
const request = (async () => {
|
||||
try {
|
||||
const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${encodeURIComponent(`${latitude},${longitude}`)}&key=${apiKey}`;
|
||||
const response = await fetchWithTimeout(url, {}, 5000);
|
||||
const data = await response.json();
|
||||
if (data.status !== 'OK' || !data.results?.length) {
|
||||
reverseGeocodeCache.set(key, null);
|
||||
return null;
|
||||
const place = await service.reverseGeocode?.(latitude, longitude) || null;
|
||||
if (place) {
|
||||
reverseGeocodeCache.set(key, place);
|
||||
if (reverseGeocodeCache.size > 256) reverseGeocodeCache.delete(reverseGeocodeCache.keys().next().value);
|
||||
}
|
||||
|
||||
const result = data.results[0];
|
||||
const relevantResults = data.results.slice(0, 12);
|
||||
const components = Array.isArray(result.address_components) ? result.address_components : [];
|
||||
const component = (type) => components.find((item) => item.types?.includes(type))?.long_name || null;
|
||||
const labels = uniqueStrings(relevantResults.map((item) => item.formatted_address)).slice(0, 12);
|
||||
const streetLabels = uniqueStrings(relevantResults.flatMap((item) => {
|
||||
const itemComponents = Array.isArray(item.address_components) ? item.address_components : [];
|
||||
return itemComponents
|
||||
.filter((entry) => entry.types?.includes('route'))
|
||||
.map((entry) => entry.long_name);
|
||||
})).slice(0, 12);
|
||||
const place = {
|
||||
formattedAddress: result.formatted_address || null,
|
||||
locality: component('locality') || component('postal_town') || component('administrative_area_level_2'),
|
||||
region: component('administrative_area_level_1'),
|
||||
country: component('country'),
|
||||
types: result.types || [],
|
||||
labels,
|
||||
streetLabels,
|
||||
};
|
||||
reverseGeocodeCache.set(key, place);
|
||||
return place;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -2978,14 +2966,14 @@ async function reverseGeocode(latitude, longitude) {
|
||||
return request;
|
||||
}
|
||||
|
||||
async function reverseGeocodeViewportSamples(samples, cameraHeightM) {
|
||||
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);
|
||||
const place = await reverseGeocode(sample.latitude, sample.longitude, service);
|
||||
if (!place) return null;
|
||||
return {
|
||||
latitude: sample.latitude,
|
||||
@@ -3002,7 +2990,8 @@ async function reverseGeocodeViewportSamples(samples, cameraHeightM) {
|
||||
return summarizeViewportPlaces(places);
|
||||
}
|
||||
|
||||
function viewportPlacesFromCache(samples, cameraHeightM) {
|
||||
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));
|
||||
@@ -3034,7 +3023,8 @@ function summarizeViewportPlaces(places) {
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchNearbyPlaces(latitude, longitude, cameraHeightM) {
|
||||
async function fetchNearbyPlaces(latitude, longitude, cameraHeightM, service) {
|
||||
const { nearbyPlacesCache, nearbyPlacesInFlight } = cachesFor(service);
|
||||
const radiusM = nearbyPlacesRadiusM(cameraHeightM);
|
||||
const cacheKey = nearbyPlacesCacheKey(latitude, longitude, cameraHeightM);
|
||||
if (nearbyPlacesCache.has(cacheKey)) return nearbyPlacesCache.get(cacheKey);
|
||||
@@ -3042,17 +3032,10 @@ async function fetchNearbyPlaces(latitude, longitude, cameraHeightM) {
|
||||
|
||||
const request = (async () => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
lat: String(latitude),
|
||||
lon: String(longitude),
|
||||
radiusM: String(radiusM),
|
||||
});
|
||||
const response = await fetchWithTimeout(`/api/google/nearby-places?${params}`, {}, 5000);
|
||||
const data = await response.json().catch(() => null);
|
||||
const places = response.ok && Array.isArray(data?.places)
|
||||
? data.places.filter((place) => place?.name).slice(0, 12)
|
||||
: [];
|
||||
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 [];
|
||||
@@ -3089,16 +3072,6 @@ function sanitizeLabel(value) {
|
||||
return out.replace(/\s+/g, ' ').trim().slice(0, 120);
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
|
||||
const controller = new AbortController();
|
||||
const timeout = window.setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
return await fetch(url, { ...options, signal: controller.signal });
|
||||
} finally {
|
||||
window.clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
function reverseGeocodeKey(latitude, longitude) {
|
||||
return `${latitude.toFixed(4)},${longitude.toFixed(4)}`;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user