mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
test(qa): an engine-neutral layer probe
- src/app/qaProbe.js → window.__godsEyeView.layers: list() (with
`available`), unavailable(), available(id), enable/disable(id) (a user
toggle through the layer manager), objects(id) → [{ id, label, position,
screen }], screenOf(id, objectId), selected(id), following(), movers(),
framesDrawn() - all through the engine and the catalog's availability, the
same on Cesium and Navara. Installed after the tools.
- src/app/layers/qaSources.js: `?qaFeed=flights:synthetic:10000,
vessels:fixture` (dev server only, allowQaRegistration) replaces those
sources with seeded synthetic answers served through the live sources'
own parsers (same snapshot shape; loaded with import() only then). The
pure generators moved from src/tooling/w1Feeds.js to src/bench/w1Feeds.js
(the browser graph may not reach src/tooling); the W1 recordings
re-export them.
- scripts/qa-w1-layers.mjs: the parity checklist's frame, --layer
--engine=cesium|navara with the fixture feeds, JSON per run and
--compare (objects on screen per pose, 5 known objects within 2 px).
Filled: 1 view, 8 idle, 9 errors/no leftover mover sets after 10
on/off; 2-7 report TODO for the layer tasks.
- qa-perf, qa-labels, qa-overlay-baseline and qa-label-readability take
--engine= and use the engine (camera, frames, render requests, tiles) and
the probe (layers, following, movers) instead of the viewer; checks whose
layers an engine lacks are SKIPPED with the reason (Navara today), as are
qa-perf's animated-style checks there (Cesium post stages).
Verdicts, Cesium, before → after: qa-perf 24/24 → 24/24, qa-labels 14/14 →
14/14, qa-overlay-baseline (datacenters, firms, detection-50) 2 measured +
1 skipped → the same. Navara: qa-perf 14 passed, 3 skipped; qa-labels 3/3,
1 skipped; qa-overlay-baseline 3 skipped; qa-w1-layers flights: Cesium 5/5
(10 000 synthetic aircraft), Navara skipped (no flights renderer yet).
This commit is contained in:
1 parent
834d34296f
commit
ec7dcc944e
11 files changed
+754
-126
No files matched your search
@@ -0,0 +1,71 @@
|
||||
// QA fixture sources (only with allowQaRegistration, i.e. the dev server):
|
||||
// `?qaFeed=flights:synthetic:10000,vessels:fixture` replaces those layers'
|
||||
// live sources with seeded synthetic answers (src/bench/w1Feeds.js) served
|
||||
// through the live sources' own parsers, so the layers see the same snapshot
|
||||
// shape. Times are relative to now, so the objects stay fresh.
|
||||
//
|
||||
// flights:synthetic[:count] OpenSky states, `count` aircraft (8 000)
|
||||
// vessels:fixture 12 000 AIS rows, or those within an area
|
||||
|
||||
import {
|
||||
createFlightSource,
|
||||
createVesselSource,
|
||||
} from '../../sources/live/standalone.js';
|
||||
import { syntheticFlights, syntheticVessels } from '../../bench/w1Feeds.js';
|
||||
|
||||
const KINDS = Object.freeze({
|
||||
flights: new Set(['synthetic']),
|
||||
vessels: new Set(['fixture']),
|
||||
});
|
||||
|
||||
/** The qaFeed entries of a location search, or a TypeError. */
|
||||
export function parseQaFeed(search = '') {
|
||||
const value = new URLSearchParams(search).get('qaFeed');
|
||||
if (!value) return [];
|
||||
return value.split(',').map((item) => {
|
||||
const [layer, kind, rawCount] = item.split(':');
|
||||
const count = rawCount === undefined ? null : Number(rawCount);
|
||||
if (
|
||||
!KINDS[layer]?.has(kind) ||
|
||||
(count !== null && !(Number.isInteger(count) && count > 0))
|
||||
)
|
||||
throw new TypeError(
|
||||
`qaFeed: unknown entry "${item}" (flights:synthetic[:count], vessels:fixture)`,
|
||||
);
|
||||
return { layer, kind, count };
|
||||
});
|
||||
}
|
||||
|
||||
const json = (payload) =>
|
||||
new Response(JSON.stringify(payload), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
|
||||
/** Source overrides for createStandaloneLayerSources. */
|
||||
export function createQaSources(entries, { now = () => Date.now() } = {}) {
|
||||
const sources = {};
|
||||
for (const { layer, count } of entries) {
|
||||
if (layer === 'flights')
|
||||
sources.flights = createFlightSource({
|
||||
now,
|
||||
fetchImpl: async () =>
|
||||
json(syntheticFlights({ nowMs: now(), count: count ?? 8000 })),
|
||||
});
|
||||
if (layer === 'vessels')
|
||||
sources.vessels = createVesselSource({
|
||||
fetchImpl: async (url) => {
|
||||
const params = new URL(url).searchParams;
|
||||
const area = params.has('lat')
|
||||
? {
|
||||
lat: Number(params.get('lat')),
|
||||
lon: Number(params.get('lon')),
|
||||
radiusKm: Number(params.get('radius_km')),
|
||||
}
|
||||
: null;
|
||||
return json(syntheticVessels({ nowMs: now(), area }));
|
||||
},
|
||||
});
|
||||
}
|
||||
return sources;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createQaSources, parseQaFeed } from './qaSources.js';
|
||||
|
||||
test('qaFeed names fixture sources per layer', () => {
|
||||
assert.deepEqual(
|
||||
parseQaFeed('?qaFeed=flights:synthetic:10000,vessels:fixture'),
|
||||
[
|
||||
{ layer: 'flights', kind: 'synthetic', count: 10000 },
|
||||
{ layer: 'vessels', kind: 'fixture', count: null },
|
||||
],
|
||||
);
|
||||
assert.deepEqual(parseQaFeed('?engine=navara'), []);
|
||||
assert.throws(() => parseQaFeed('?qaFeed=flights:live'), /qaFeed/);
|
||||
assert.throws(() => parseQaFeed('?qaFeed=weather:fixture'), /qaFeed/);
|
||||
assert.throws(() => parseQaFeed('?qaFeed=flights:synthetic:-3'), /qaFeed/);
|
||||
});
|
||||
|
||||
test('fixture sources answer with the live sources’ snapshot shape', async () => {
|
||||
const now = Date.UTC(2026, 9, 16, 12);
|
||||
const sources = createQaSources(
|
||||
parseQaFeed('?qaFeed=flights:synthetic:500,vessels:fixture'),
|
||||
{ now: () => now },
|
||||
);
|
||||
const flights = await sources.flights.getSnapshot({});
|
||||
assert.equal(flights.records.length, 500);
|
||||
assert.ok(
|
||||
flights.records.every((record) => Number.isFinite(record.latitude)),
|
||||
);
|
||||
// Seeded: the same aircraft every time.
|
||||
const again = await sources.flights.getSnapshot({});
|
||||
assert.deepEqual(
|
||||
again.records.map(({ id }) => id),
|
||||
flights.records.map(({ id }) => id),
|
||||
);
|
||||
const vessels = await sources.vessels.getSnapshot({ maxRows: 12000 });
|
||||
assert.equal(vessels.records.length, 12000);
|
||||
const near = await sources.vessels.getSnapshot({
|
||||
maxRows: 12000,
|
||||
area: { lat: 40.65724, lon: -73.78, radiusKm: 10 },
|
||||
});
|
||||
assert.ok(near.records.length > 10 && near.records.length < 12000);
|
||||
assert.deepEqual(createQaSources([]), {});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
// The engine-neutral QA probe: window.__godsEyeView.layers. QA scripts ask
|
||||
// it what a layer shows and where, the same way on Cesium and Navara,
|
||||
// instead of reading Cesium objects. Read-only apart from enable/disable,
|
||||
// which go through the layer manager as a user toggle would.
|
||||
//
|
||||
// list() [{ id, name, enabled, available }] (the catalog)
|
||||
// unavailable() [{ id, name }] layers this engine leaves out
|
||||
// available(id) whether this engine can show layer `id`
|
||||
// enable(id) / disable(id) → Promise<boolean>
|
||||
// objects(id, max?) [{ id, label, position, screen }] (position a
|
||||
// plain { x, y, z }, screen a window point or null)
|
||||
// screenOf(id, objectId) the window point of one object, or null
|
||||
// selected(id) the layer's tracked/selected info, or null
|
||||
// following() { label, native } of engine.camera.following()
|
||||
// movers() engine.movers.list()
|
||||
// framesDrawn() frames drawn since the probe started
|
||||
|
||||
const plain = (position) =>
|
||||
position ? { x: position.x, y: position.y, z: position.z } : null;
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* engine: import('../globe/contract.js').GlobeEngine,
|
||||
* dataManager: object,
|
||||
* availability: import('./layers/renderers.js').LayerAvailability,
|
||||
* }} options
|
||||
*/
|
||||
export function createQaProbe({ engine, dataManager, availability }) {
|
||||
let frames = 0;
|
||||
const removeFrames = engine.frames.postRender.addEventListener(() => {
|
||||
frames++;
|
||||
});
|
||||
const moduleOf = (layerId) => dataManager.layers.get(layerId)?.module;
|
||||
const screenOf = (position) => {
|
||||
if (!position || !engine.projector.horizonVisible(position)) return null;
|
||||
const screen = engine.projector.worldToWindow(position);
|
||||
return screen ? { x: screen.x, y: screen.y } : null;
|
||||
};
|
||||
const objects = (layerId, max = 500) => {
|
||||
const rows = moduleOf(layerId)?.getAllPositions?.(max) ?? [];
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
label: row.label ?? null,
|
||||
position: plain(row.position),
|
||||
screen: screenOf(row.position),
|
||||
}));
|
||||
};
|
||||
return {
|
||||
list: () =>
|
||||
dataManager.getAll().map(({ id, name, enabled }) => ({
|
||||
id,
|
||||
name,
|
||||
enabled: Boolean(enabled),
|
||||
available: availability.isAvailable(id),
|
||||
})),
|
||||
unavailable: () => availability.unavailable(),
|
||||
available: (layerId) => availability.isAvailable(layerId),
|
||||
enable: (layerId) =>
|
||||
dataManager.setEnabled(layerId, true, { origin: 'user' }),
|
||||
disable: (layerId) =>
|
||||
dataManager.setEnabled(layerId, false, { origin: 'user' }),
|
||||
objects,
|
||||
screenOf(layerId, objectId) {
|
||||
const row = (moduleOf(layerId)?.getAllPositions?.(1_000_000) ?? []).find(
|
||||
({ id }) => String(id) === String(objectId),
|
||||
);
|
||||
return row ? screenOf(row.position) : null;
|
||||
},
|
||||
selected: (layerId) => moduleOf(layerId)?.getTrackedInfo?.() ?? null,
|
||||
following() {
|
||||
const handle = engine.camera.following();
|
||||
if (!handle) return null;
|
||||
return {
|
||||
label: handle.label?.id ?? null,
|
||||
native: handle.native?.id ?? null,
|
||||
};
|
||||
},
|
||||
movers: () => engine.movers.list(),
|
||||
framesDrawn: () => frames,
|
||||
destroy: () => removeFrames(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as geo from '../geo/index.js';
|
||||
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
|
||||
import { createQaProbe } from './qaProbe.js';
|
||||
import {
|
||||
createLayerAvailability,
|
||||
createRendererRegistry,
|
||||
} from './layers/renderers.js';
|
||||
|
||||
function fakeManager(modules) {
|
||||
const enabled = new Set();
|
||||
return {
|
||||
layers: new Map(modules.map((module) => [module.id, { module }])),
|
||||
getAll: () =>
|
||||
modules.map(({ id, name }) => ({ id, name, enabled: enabled.has(id) })),
|
||||
isEnabled: (id) => enabled.has(id),
|
||||
async setEnabled(id, value) {
|
||||
if (value) enabled.add(id);
|
||||
else enabled.delete(id);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('the probe lists, toggles and reads layers through the engine', async () => {
|
||||
const engine = createFakeEngine({ kind: 'navara' });
|
||||
const position = geo.cartesian3.fromDegrees(-73.78, 40.64, 1000);
|
||||
const flights = {
|
||||
id: 'flights',
|
||||
name: 'Live Flights',
|
||||
getAllPositions: () => [{ id: 'ae01ce', label: 'UAL1', position }],
|
||||
getTrackedInfo: () => ({ icao24: 'ae01ce' }),
|
||||
};
|
||||
const manager = fakeManager([flights]);
|
||||
const registry = createRendererRegistry();
|
||||
registry.register('navara', { flights: () => ({}) });
|
||||
const probe = createQaProbe({
|
||||
engine,
|
||||
dataManager: manager,
|
||||
availability: createLayerAvailability('navara', { registry }),
|
||||
});
|
||||
assert.deepEqual(probe.list(), [
|
||||
{ id: 'flights', name: 'Live Flights', enabled: false, available: true },
|
||||
]);
|
||||
assert.equal(probe.available('flights'), true);
|
||||
assert.equal(probe.available('satellites'), false);
|
||||
assert.deepEqual(
|
||||
probe
|
||||
.unavailable()
|
||||
.map(({ id }) => id)
|
||||
.includes('satellites'),
|
||||
true,
|
||||
);
|
||||
assert.equal(await probe.enable('flights'), true);
|
||||
assert.equal(manager.isEnabled('flights'), true);
|
||||
engine.camera.setView({
|
||||
destination: geo.cartesian3.fromDegrees(-73.78, 40.64, 50_000),
|
||||
});
|
||||
engine.fake.script('worldToWindow', () => ({ x: 640, y: 400 }));
|
||||
const [object] = probe.objects('flights');
|
||||
assert.equal(object.id, 'ae01ce');
|
||||
assert.deepEqual(
|
||||
[object.position.x, object.position.y, object.position.z],
|
||||
[position.x, position.y, position.z],
|
||||
);
|
||||
assert.deepEqual(object.screen, { x: 640, y: 400 });
|
||||
assert.deepEqual(probe.screenOf('flights', 'ae01ce'), { x: 640, y: 400 });
|
||||
assert.equal(probe.screenOf('flights', 'nope'), null);
|
||||
assert.deepEqual(probe.selected('flights'), { icao24: 'ae01ce' });
|
||||
assert.deepEqual(probe.objects('unknown'), []);
|
||||
// Follow, movers and frames come from the engine.
|
||||
assert.equal(probe.following(), null);
|
||||
engine.camera.follow({
|
||||
position: () => position,
|
||||
viewFrom: { x: 0, y: -1000, z: 300 },
|
||||
label: {
|
||||
id: 'flights:ae01ce',
|
||||
model: { title: 'UAL1' },
|
||||
position: () => position,
|
||||
},
|
||||
});
|
||||
assert.deepEqual(probe.following(), {
|
||||
label: 'flights:ae01ce',
|
||||
native: null,
|
||||
});
|
||||
engine.movers.createSet({
|
||||
id: 'fleet',
|
||||
kind: 'point',
|
||||
capacity: 4,
|
||||
point: { sizePx: 4 },
|
||||
});
|
||||
assert.deepEqual(probe.movers(), [
|
||||
{ id: 'fleet', kind: 'point', count: 0, capacity: 4 },
|
||||
]);
|
||||
const before = probe.framesDrawn();
|
||||
engine.fake.frame();
|
||||
engine.fake.frame();
|
||||
assert.equal(probe.framesDrawn() - before, 2);
|
||||
assert.equal(await probe.disable('flights'), true);
|
||||
assert.equal(manager.isEnabled('flights'), false);
|
||||
probe.destroy();
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
// Seeded synthetic feed answers: the global /api/flights snapshot (OpenSky
|
||||
// states), 12 000 AIS vessels on /api/vessels (with and without an area) and
|
||||
// the local receivers' 1090 MHz and 978 MHz UAT aircraft near JFK, timed
|
||||
// relative to one instant, so every run sees the same objects at the same
|
||||
// places. Pure: Node and browser, no I/O. Used by the W1 baseline recordings
|
||||
// (src/tooling/w1Feeds.js) and the app's QA fixture sources
|
||||
// (src/app/layers/qaSources.js).
|
||||
|
||||
import { createSyntheticFlights } from './syntheticFlights.js';
|
||||
|
||||
/** The replay clock of the synthetic recordings: 2026-10-16T12:00:00Z. */
|
||||
export const W1_FROZEN_MS = Date.UTC(2026, 9, 16, 12);
|
||||
const JFK = { lat: 40.64, lng: -73.78 };
|
||||
|
||||
/** Small deterministic PRNG (as syntheticFlights). */
|
||||
function mulberry32(seed) {
|
||||
let state = seed >>> 0;
|
||||
return () => {
|
||||
state = (state + 0x6d2b79f5) >>> 0;
|
||||
let t = state;
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
const hex = (value, digits) => value.toString(16).padStart(digits, '0');
|
||||
|
||||
/** An OpenSky states payload: `count` aircraft, 2 000 of them near JFK. */
|
||||
export function syntheticFlights({
|
||||
nowMs = W1_FROZEN_MS,
|
||||
count = 8_000,
|
||||
seed = 11,
|
||||
} = {}) {
|
||||
const fleet = createSyntheticFlights({
|
||||
count,
|
||||
seed,
|
||||
near: { ...JFK, radiusM: 60_000, count: Math.min(2_000, count) },
|
||||
});
|
||||
const random = mulberry32(seed + 1);
|
||||
const nowSec = Math.floor(nowMs / 1000);
|
||||
const states = [];
|
||||
for (let index = 0; index < count; index++) {
|
||||
const age = Math.floor(random() * 10);
|
||||
states.push([
|
||||
hex(0xa00000 + index, 6),
|
||||
`GEV${String(index).padStart(4, '0')}`,
|
||||
'United States',
|
||||
nowSec - age,
|
||||
nowSec - Math.min(age, 2),
|
||||
Number(fleet.lon[index].toFixed(5)),
|
||||
Number(fleet.lat[index].toFixed(5)),
|
||||
Math.round(fleet.alt[index]),
|
||||
false,
|
||||
Number(fleet.speed[index].toFixed(1)),
|
||||
Number(fleet.heading[index].toFixed(1)),
|
||||
0,
|
||||
null,
|
||||
Math.round(fleet.alt[index]) + 30,
|
||||
null,
|
||||
false,
|
||||
0,
|
||||
index % 7 === 0 ? 1 : 3,
|
||||
]);
|
||||
}
|
||||
return { time: nowSec, states };
|
||||
}
|
||||
|
||||
const PORTS = [
|
||||
{ lat: 40.6, lon: -74.02, spread: 0.25 }, // New York harbour
|
||||
{ lat: 51.95, lon: 4.1, spread: 0.4 }, // Rotterdam
|
||||
{ lat: 29.7, lon: -95.0, spread: 0.3 }, // Houston
|
||||
{ lat: 1.26, lon: 103.8, spread: 0.4 }, // Singapore
|
||||
];
|
||||
|
||||
function allVessels(nowMs, seed) {
|
||||
const random = mulberry32(seed);
|
||||
const nowSec = Math.floor(nowMs / 1000);
|
||||
const rows = [];
|
||||
for (let index = 0; index < 12_000; index++) {
|
||||
const port = index < 4_000 ? PORTS[index % PORTS.length] : null;
|
||||
const lat = port
|
||||
? port.lat + (random() - 0.5) * port.spread
|
||||
: random() * 120 - 60;
|
||||
const lon = port
|
||||
? port.lon + (random() - 0.5) * port.spread * 1.5
|
||||
: random() * 360 - 180;
|
||||
rows.push({
|
||||
mmsi: String(200_000_000 + index),
|
||||
lat: Number(lat.toFixed(5)),
|
||||
lon: Number(lon.toFixed(5)),
|
||||
name: `GEV VESSEL ${index}`,
|
||||
imo: '',
|
||||
type: ['Cargo', 'Tanker', 'Passenger', 'Fishing', 'Tug'][index % 5],
|
||||
destination: '',
|
||||
speed: Number((random() * 18).toFixed(1)),
|
||||
course: Math.round(random() * 360),
|
||||
heading: Math.round(random() * 360),
|
||||
last_position_epoch: nowSec - Math.floor(random() * 120),
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
const kmBetween = (a, b) => {
|
||||
const toRad = Math.PI / 180;
|
||||
const dLat = (b.lat - a.lat) * toRad;
|
||||
const dLon = (b.lon - a.lon) * toRad;
|
||||
const h =
|
||||
Math.sin(dLat / 2) ** 2 +
|
||||
Math.cos(a.lat * toRad) * Math.cos(b.lat * toRad) * Math.sin(dLon / 2) ** 2;
|
||||
return 2 * 6371 * Math.asin(Math.sqrt(h));
|
||||
};
|
||||
|
||||
/** The /api/vessels answer: 12 000 rows, or those within `area`. */
|
||||
export function syntheticVessels({
|
||||
nowMs = W1_FROZEN_MS,
|
||||
seed = 21,
|
||||
area = null,
|
||||
} = {}) {
|
||||
let rows = allVessels(nowMs, seed);
|
||||
if (area) rows = rows.filter((row) => kmBetween(area, row) <= area.radiusKm);
|
||||
return { rows, newestPositionAt: nowMs };
|
||||
}
|
||||
|
||||
/** The /api/local-receivers/aircraft answer: 1090 MHz and 978 MHz UAT aircraft. */
|
||||
export function syntheticLocalReceivers({
|
||||
nowMs = W1_FROZEN_MS,
|
||||
seed = 31,
|
||||
} = {}) {
|
||||
const random = mulberry32(seed);
|
||||
const records = [];
|
||||
for (let index = 0; index < 40; index++) {
|
||||
const uat = index % 4 === 0;
|
||||
records.push({
|
||||
icao: hex(0xad0000 + index, 6),
|
||||
callsign: `LCL${index}`,
|
||||
lat: Number((JFK.lat + (random() - 0.5) * 0.8).toFixed(5)),
|
||||
lon: Number((JFK.lng + (random() - 0.5) * 1.0).toFixed(5)),
|
||||
altitudeFt: Math.round(1_000 + random() * 20_000),
|
||||
groundSpeedKt: Math.round(80 + random() * 300),
|
||||
trackDeg: Math.round(random() * 360),
|
||||
verticalRateFpm: 0,
|
||||
onGround: false,
|
||||
category: uat ? 'A1' : 'A3',
|
||||
lastPositionAt: nowMs - Math.floor(random() * 2_000),
|
||||
lastMessageAt: nowMs - Math.floor(random() * 500),
|
||||
messageCount: 50,
|
||||
band: uat ? '978' : '1090',
|
||||
source: 'feed',
|
||||
});
|
||||
}
|
||||
return {
|
||||
configured: true,
|
||||
generatedAt: nowMs,
|
||||
feeds: [
|
||||
{ band: '1090', label: '1090 MHz', status: 'live', aircraft: 30 },
|
||||
{ band: '978', label: '978 MHz UAT', status: 'live', aircraft: 10 },
|
||||
],
|
||||
records,
|
||||
};
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { createStandaloneData } from './data.js';
|
||||
import { createStandaloneTools } from './tools.js';
|
||||
import { createGoogleTokenSource } from '../maps/googleTokens.js';
|
||||
import { resolveEngineKind } from '../app/engineChoice.js';
|
||||
import { createQaProbe } from '../app/qaProbe.js';
|
||||
|
||||
// The existing controls and layer catalog contain page-scoped state.
|
||||
let constructed = false;
|
||||
@@ -65,9 +66,18 @@ export function createStandaloneApplication({
|
||||
// The catalog of this engine: on Navara only the layers with a Navara
|
||||
// renderer (src/app/layers/renderers.js; none yet), the others' state
|
||||
// kept for links.
|
||||
// QA fixture sources (?qaFeed=…) on the dev server only.
|
||||
const search = globalThis.location?.search ?? '';
|
||||
const qaSources =
|
||||
allowQaRegistration && /[?&]qaFeed=/.test(search)
|
||||
? await import('../app/layers/qaSources.js').then(
|
||||
({ createQaSources, parseQaFeed }) =>
|
||||
createQaSources(parseQaFeed(search)),
|
||||
)
|
||||
: {};
|
||||
catalog = createStandaloneCatalog({
|
||||
engineKind,
|
||||
sources,
|
||||
sources: { ...sources, ...qaSources },
|
||||
streetLevelProviders,
|
||||
nepalBoundaryResolver: (signal) =>
|
||||
scene.operations.annotationResolver.resolveRegionRingForQuery(
|
||||
@@ -91,7 +101,28 @@ export function createStandaloneApplication({
|
||||
}),
|
||||
createData: (context) =>
|
||||
createStandaloneData({ ...context, allowQaRegistration, catalog }),
|
||||
createTools: (context) =>
|
||||
createStandaloneTools({ ...context, loadingScreen, placeSearch, voice }),
|
||||
createTools: async (context) => {
|
||||
const tools = await createStandaloneTools({
|
||||
...context,
|
||||
loadingScreen,
|
||||
placeSearch,
|
||||
voice,
|
||||
});
|
||||
// The engine-neutral QA probe on the debug handle (src/app/qaProbe.js).
|
||||
const debug = globalThis.window?.__godsEyeView;
|
||||
if (debug) {
|
||||
const probe = createQaProbe({
|
||||
engine: context.scene.engine,
|
||||
dataManager: context.data.dataManager,
|
||||
availability: catalog.availability,
|
||||
});
|
||||
debug.layers = probe;
|
||||
context.defer(() => {
|
||||
probe.destroy();
|
||||
if (debug.layers === probe) delete debug.layers;
|
||||
});
|
||||
}
|
||||
return tools;
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user