mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
refactor(app): share application components and request services
This commit is contained in:
1 parent
a0a0128a59
commit
a67bc60a29
84 files changed
+2798
-1731
No files matched your search
@@ -246,7 +246,7 @@ export function createAnnotationEngine({
|
||||
// allSettled gives per-item error isolation (one failed item never aborts the batch); the
|
||||
// mutation pass below then runs in ORDER, so de-dup, the synchronous live-cap check, and output
|
||||
// order are all preserved exactly as the old serial loop had them.
|
||||
const settled = await Promise.allSettled(list.map((spec) => resolveSpec(spec, controller.signal)));
|
||||
const settled = await Promise.allSettled(list.map((spec) => resolveSpec(spec, controller.signal, opts.flyTo === true)));
|
||||
|
||||
try {
|
||||
for (let i = 0; i < list.length; i += 1) {
|
||||
@@ -385,7 +385,7 @@ export function createAnnotationEngine({
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveSpec(spec, signal) {
|
||||
async function resolveSpec(spec, signal, allowDistant = false) {
|
||||
const type = normalizeType(spec?.type);
|
||||
if (type === 'route') {
|
||||
const points = Array.isArray(spec.points) ? spec.points : [];
|
||||
@@ -395,13 +395,15 @@ export function createAnnotationEngine({
|
||||
for (const pt of points) {
|
||||
const name = pt.target ?? pt.name ?? null;
|
||||
const r = await resolveTarget({
|
||||
placeSearch, viewer,
|
||||
placeSearch,
|
||||
viewer,
|
||||
target: name,
|
||||
latitude: pt.latitude,
|
||||
longitude: pt.longitude,
|
||||
screenX: pt.screenX,
|
||||
screenY: pt.screenY,
|
||||
footprint: false,
|
||||
allowDistant,
|
||||
signal,
|
||||
});
|
||||
if (r) resolvedPts.push(r);
|
||||
@@ -438,23 +440,27 @@ export function createAnnotationEngine({
|
||||
}
|
||||
if (type === 'arrow') {
|
||||
const from = await resolveTarget({
|
||||
placeSearch, viewer,
|
||||
placeSearch,
|
||||
viewer,
|
||||
target: spec.target,
|
||||
latitude: spec.latitude,
|
||||
longitude: spec.longitude,
|
||||
screenX: spec.screenX,
|
||||
screenY: spec.screenY,
|
||||
footprint: false,
|
||||
allowDistant,
|
||||
signal,
|
||||
});
|
||||
const to = await resolveTarget({
|
||||
placeSearch, viewer,
|
||||
placeSearch,
|
||||
viewer,
|
||||
target: spec.toTarget,
|
||||
latitude: spec.toLatitude,
|
||||
longitude: spec.toLongitude,
|
||||
screenX: spec.toScreenX,
|
||||
screenY: spec.toScreenY,
|
||||
footprint: false,
|
||||
allowDistant,
|
||||
signal,
|
||||
});
|
||||
if (!from || !to) {
|
||||
@@ -473,7 +479,8 @@ export function createAnnotationEngine({
|
||||
}
|
||||
const wantFootprint = type === 'area' ? spec.footprint !== false : Boolean(spec.footprint);
|
||||
return resolveTarget({
|
||||
placeSearch, viewer,
|
||||
placeSearch,
|
||||
viewer,
|
||||
target: spec.target,
|
||||
latitude: spec.latitude,
|
||||
longitude: spec.longitude,
|
||||
@@ -490,6 +497,7 @@ export function createAnnotationEngine({
|
||||
// timeout — field test 7 logs); deferring them lets the mark appear and the tool
|
||||
// result return while the outline resolves, then upgrades the mark in place.
|
||||
deferFootprint: wantFootprint,
|
||||
allowDistant,
|
||||
signal,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -710,3 +710,19 @@ test('a late annotation resolver cannot redraw after destruction', async (t) =>
|
||||
assert.equal(calls.add, 0);
|
||||
assert.equal(engine.count(), 0);
|
||||
});
|
||||
|
||||
test('only an explicit navigation request permits resolving distant annotation targets', async (t) => {
|
||||
installAnimationFrameStubs(t);
|
||||
_resetRenderGovernorForTest();
|
||||
t.after(() => _resetRenderGovernorForTest());
|
||||
const { renderer } = throwingRendererHarness();
|
||||
renderer.destroy = () => {};
|
||||
const received = [];
|
||||
const engine = createAnnotationEngine({ viewer: {}, renderer,
|
||||
resolveTarget: async (options) => { received.push(options); return null; },
|
||||
});
|
||||
await engine.annotate([{ type: 'point', target: 'Remote landmark' }]);
|
||||
await engine.annotate([{ type: 'point', target: 'Remote landmark' }], { flyTo: true });
|
||||
assert.deepEqual(received.map(options => options.allowDistant), [false, true]);
|
||||
engine.destroy();
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { applicationServices } from '../services/application.js';
|
||||
import { defaultGeospatial } from '../search/defaults.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { lookupNeighborhoodRing } from '../data/neighborhoodPolygons.js';
|
||||
@@ -93,7 +94,7 @@ function linkAbort(controller, externalSignal) {
|
||||
export async function resolveAnnotationTarget({
|
||||
placeSearch = unavailablePlaceSearch,
|
||||
viewer, target, latitude, longitude, footprint = false, intent = 'the_thing',
|
||||
entityKind = null, labelHint = null, deferFootprint = false, screenX, screenY, signal,
|
||||
entityKind = null, labelHint = null, deferFootprint = false, allowDistant = false, screenX, screenY, signal,
|
||||
}) {
|
||||
let lon = Number(longitude);
|
||||
let lat = Number(latitude);
|
||||
@@ -158,7 +159,7 @@ export async function resolveAnnotationTarget({
|
||||
// LOOKING AT. If the geocode missed or landed far from the view centre, try a view-biased
|
||||
// Places Text Search; a hit within the trust bound overrides + skips the gate. Local geocodes
|
||||
// (neighborhoods, nearby buildings) are NOT far, so they keep the geocode + scope/polygon path.
|
||||
if (center && trace.places === 'skipped' && !bypassNearViewGuards) {
|
||||
if (center && trace.places === 'skipped' && !bypassNearViewGuards && !allowDistant) {
|
||||
let geocodeFar = source !== 'geocode';
|
||||
if (source === 'geocode' && approximateDistanceM(center.lat, center.lon, lat, lon) / 1000 > MIN_DRIFT_FLOOR_KM) {
|
||||
geocodeFar = true;
|
||||
@@ -243,7 +244,7 @@ export async function resolveAnnotationTarget({
|
||||
const limitKm = Math.max(VIEWPORT_DRIFT_FACTOR * vpGate.radiusKm, MIN_DRIFT_FLOOR_KM);
|
||||
return driftKm > limitKm ? { driftKm, limitKm } : null;
|
||||
};
|
||||
if (fromGeocode && !bypassNearViewGuards) {
|
||||
if (fromGeocode && !bypassNearViewGuards && !allowDistant) {
|
||||
const drift = gateDrift(lat, lon);
|
||||
if (drift) {
|
||||
if (trace.query) {
|
||||
@@ -739,48 +740,18 @@ export function refineScope(scope, entityKind) {
|
||||
return scope;
|
||||
}
|
||||
|
||||
/** True when an HTTP-200 Overpass body actually signals a runtime FAILURE (server-side
|
||||
* timeout / out-of-memory) via its `remark` — a transient error, not an authoritative
|
||||
* empty result, so callers must not cache it as a definitive not-found. */
|
||||
function overpassHasError(data) {
|
||||
const remark = String(data?.remark || '').toLowerCase();
|
||||
return remark.includes('runtime error') || remark.includes('timed out') || remark.includes('out of memory');
|
||||
}
|
||||
|
||||
/** A distinct Overpass throttle result that must not enter the ordinary transient ladder. */
|
||||
export function isRateLimitedOutcome(value) {
|
||||
return value?.rateLimited === true;
|
||||
}
|
||||
|
||||
/** Parse Retry-After seconds or an HTTP date into a non-negative millisecond delay. */
|
||||
function parseRetryAfterMs(value) {
|
||||
if (value == null || String(value).trim() === '') return null;
|
||||
const seconds = Number(value);
|
||||
if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds * 1000);
|
||||
const at = Date.parse(String(value));
|
||||
return Number.isFinite(at) ? Math.max(0, at - Date.now()) : null;
|
||||
}
|
||||
|
||||
/** POST an Overpass QL query and return elements, a transient null, or a throttle object. */
|
||||
async function overpassJson(query, timeoutMs = 14000, signal) {
|
||||
const controller = new AbortController();
|
||||
const detach = linkAbort(controller, signal);
|
||||
const timer = window.setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const res = await fetch('/api/overpass', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: `data=${encodeURIComponent(query)}`,
|
||||
signal: controller.signal,
|
||||
});
|
||||
const retryAfter = res.headers?.get?.('Retry-After');
|
||||
if (res.status === 429 || (res.status === 503 && retryAfter != null)) {
|
||||
return { rateLimited: true, retryAfterMs: parseRetryAfterMs(retryAfter) };
|
||||
}
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
if (overpassHasError(data)) return null; // 200 with a body-level timeout/error → transient
|
||||
return Array.isArray(data?.elements) ? data.elements : null;
|
||||
return await applicationServices.boundaries.query(query, { signal: controller.signal });
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
|
||||
Reference in new issue
Block a user