mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Merge origin/main into german/navara
Brings in 190 commits from main. Integration on the engine contract: - News and Shipwrecks draw, pick and keep their selection through the contract and shared map markers, so they follow engine switches. - Aircraft tracking uses main's segmented trails with contract frame numbers. - Decoded weather tiles move into the Cesium adapter and are injected through the renderer registry; Navara hides those sources on globe surfaces (docs/KNOWN-ISSUES.md). - Street-vehicle GPU depth occlusion stays Cesium-only; Navara keeps the geometric visibility checks (docs/KNOWN-ISSUES.md). - Transit includes the heading-neutral atlas icons.
This commit is contained in:
commit
1856477e0a
346 files changed
+41247
-1649
No files matched your search
@@ -7,10 +7,10 @@
|
||||
"cesiumLayerRendererLines": 0,
|
||||
"navaraLayerRendererLines": 0,
|
||||
"cesiumImportFiles": 2,
|
||||
"viewerLines": 613,
|
||||
"viewerLines": 632,
|
||||
"navaraKindChecks": 1,
|
||||
"engineSniffCalls": 0,
|
||||
"engineExtensionReads": 2,
|
||||
"engineExtensionReads": 4,
|
||||
"groundEffectPasses": 0,
|
||||
"cesiumSdkLoads": 3,
|
||||
"screenSpaceHandlerFiles": 2,
|
||||
|
||||
@@ -673,6 +673,7 @@
|
||||
"src/voice/control.js",
|
||||
"src/voice/realtimeBackend.js",
|
||||
"src/voice/realtimeBackend.test.mjs",
|
||||
"src/voice/realtimeViewportPolicy.test.mjs",
|
||||
"src/agent/worldPacket.test.mjs",
|
||||
"src/voice/layerSummary.js",
|
||||
"src/app/sources.js",
|
||||
@@ -1407,6 +1408,7 @@
|
||||
"src/data/firmsIntensity.js",
|
||||
"src/data/firmsIntensity.test.mjs",
|
||||
"src/ui/layerPanel/RowSwitch.vue",
|
||||
"src/ui/layerPanel/RowRange.vue",
|
||||
"src/ui/layerPanel/RowSlider.vue",
|
||||
"src/sources/phaseTiming.test.mjs",
|
||||
"scripts/build-census-places.mjs",
|
||||
@@ -1461,6 +1463,13 @@
|
||||
"src/voice/panelLayout.js",
|
||||
"src/voice/textChat.js",
|
||||
"src/voice/textProtocol.js",
|
||||
"src/layers/shipwrecks/records.test.mjs",
|
||||
"src/layers/shipwrecks/cards.test.mjs",
|
||||
"src/layers/shipwrecks/eras.test.mjs",
|
||||
"src/layers/shipwrecks/period.test.mjs",
|
||||
"src/layers/shipwrecks/index.test.mjs",
|
||||
"src/layers/shipwrecks/source.test.mjs",
|
||||
"src/data/shipwrecksProxy.test.mjs",
|
||||
"src/app/sourceComposition.js",
|
||||
"src/app/sourceComposition.test.mjs",
|
||||
"src/data/dataCredits.js",
|
||||
@@ -1471,6 +1480,7 @@
|
||||
"src/i18n/locales/de.json",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/fr.json",
|
||||
"src/i18n/locales/he.json",
|
||||
"src/i18n/locales/it.json",
|
||||
"src/i18n/locales/ja.json",
|
||||
"src/i18n/locales/pt.json",
|
||||
|
||||
+144
-18
@@ -172,7 +172,8 @@
|
||||
"src/ui/kit/UiIcon.vue",
|
||||
"src/ui/stores/worldTime.js",
|
||||
"src/ui/views/panels/WorldTimeline.vue",
|
||||
"src/time/clock.js"
|
||||
"src/time/clock.js",
|
||||
"src/data/regionalNewsWindow.js"
|
||||
],
|
||||
"external": ["vue", "pinia", "vue-i18n"]
|
||||
},
|
||||
@@ -521,7 +522,8 @@
|
||||
"src/localRequestGate.mjs",
|
||||
"src/data/adminBoundaries.js",
|
||||
"src/data/placeNames.js",
|
||||
"src/sources/featureGeometry.js"
|
||||
"src/sources/featureGeometry.js",
|
||||
"src/data/regionalNewsWindow.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -608,7 +610,8 @@
|
||||
"src/data/retryableLoad.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/",
|
||||
"src/sources/nominatim.js"
|
||||
"src/sources/nominatim.js",
|
||||
"src/data/regionalNewsWindow.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -719,7 +722,13 @@
|
||||
"src/ui/kit/UiBadge.vue",
|
||||
"src/ui/rail/fieldValue.js",
|
||||
"src/ui/kit/UiSlider.js",
|
||||
"src/ui/kit/UiSegmented.vue"
|
||||
"src/ui/kit/UiSegmented.vue",
|
||||
"src/ui/kit/UiEmpty.vue",
|
||||
"src/ui/kit/UiSelect.js",
|
||||
"src/ui/layerPanel/RowRange.vue",
|
||||
"src/ui/news/NewsCard.vue",
|
||||
"src/ui/news/NewsFeed.vue",
|
||||
"src/ui/newsPanel.js"
|
||||
],
|
||||
"external": ["vue"]
|
||||
},
|
||||
@@ -851,7 +860,8 @@
|
||||
"src/globe/cesium/orbital.js",
|
||||
"src/globe/surfaceDetail.js",
|
||||
"src/globe/surfacePlacement.js",
|
||||
"src/globe/cesium/orbitalImagery.js"
|
||||
"src/globe/cesium/orbitalImagery.js",
|
||||
"src/data/regionalNewsWindow.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -1113,7 +1123,13 @@
|
||||
"src/globe/cesium/orbital.js",
|
||||
"src/time/clock.js",
|
||||
"src/globe/cesium/celestialHost.js",
|
||||
"src/globe/cesium/orbitalImagery.js"
|
||||
"src/globe/cesium/orbitalImagery.js",
|
||||
"src/data/regionalNewsWindow.js",
|
||||
"src/ui/kit/UiEmpty.vue",
|
||||
"src/ui/kit/UiSelect.js",
|
||||
"src/ui/news/NewsCard.vue",
|
||||
"src/ui/news/NewsFeed.vue",
|
||||
"src/ui/newsPanel.js"
|
||||
],
|
||||
"external": ["cesium", "vue", "pinia", "vue-i18n"]
|
||||
},
|
||||
@@ -1279,7 +1295,11 @@
|
||||
"src/overlays/bindOverlayHost.js",
|
||||
"src/time/worldClock.js",
|
||||
"src/data/planespottersPhoto.js",
|
||||
"src/time/clock.js"
|
||||
"src/time/clock.js",
|
||||
"src/data/trailContinuity.js",
|
||||
"src/layers/flights/clockFollow.js",
|
||||
"src/sources/live/contract.js",
|
||||
"src/time/snapshotTimeline.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -1329,7 +1349,12 @@
|
||||
"src/layers/aircraft/qaDrawn.js",
|
||||
"src/overlays/bindOverlayHost.js",
|
||||
"src/time/worldClock.js",
|
||||
"src/time/clock.js"
|
||||
"src/time/clock.js",
|
||||
"src/data/trailContinuity.js",
|
||||
"src/layers/military/clockFollow.js",
|
||||
"src/sources/live/contract.js",
|
||||
"src/time/snapshotTimeline.js",
|
||||
"src/ui/unitLabels.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -1397,7 +1422,12 @@
|
||||
"src/layers/shared/color.js",
|
||||
"src/globe/cesium/sceneProjection.js",
|
||||
"src/geo/",
|
||||
"src/overlays/bindOverlayHost.js"
|
||||
"src/overlays/bindOverlayHost.js",
|
||||
"src/layers/vessels/motion.js",
|
||||
"src/sources/live/contract.js",
|
||||
"src/time/clock.js",
|
||||
"src/time/snapshotTimeline.js",
|
||||
"src/time/worldClock.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -1799,7 +1829,8 @@
|
||||
"src/i18n/",
|
||||
"src/sources/vectorTiles.js",
|
||||
"src/sources/phaseTiming.js",
|
||||
"src/geo/"
|
||||
"src/geo/",
|
||||
"src/data/displayText.js"
|
||||
],
|
||||
"external": ["@mapbox/vector-tile", "pbf"]
|
||||
},
|
||||
@@ -2822,7 +2853,34 @@
|
||||
"src/globe/cesium/orbitalImagery.js",
|
||||
"src/globe/cesium/windGpuRendering.js",
|
||||
"src/globe/cesium/windRelief.js",
|
||||
"src/globe/cesium/windRendering.js"
|
||||
"src/globe/cesium/windRendering.js",
|
||||
"src/app/layers/news.js",
|
||||
"src/app/layers/shipwrecks.js",
|
||||
"src/data/detectionOcclusion.js",
|
||||
"src/data/displayText.js",
|
||||
"src/data/regionalNewsWindow.js",
|
||||
"src/data/sceneOcclusionProbe.js",
|
||||
"src/data/trailContinuity.js",
|
||||
"src/data/trailRenderer.js",
|
||||
"src/globe/cesium/weatherFrames.js",
|
||||
"src/layers/news/cards.js",
|
||||
"src/layers/news/index.js",
|
||||
"src/layers/news/model.js",
|
||||
"src/layers/news/records.js",
|
||||
"src/layers/news/source.js",
|
||||
"src/layers/news/sprites.js",
|
||||
"src/layers/shipwrecks/cards.js",
|
||||
"src/layers/shipwrecks/eras.js",
|
||||
"src/layers/shipwrecks/index.js",
|
||||
"src/layers/shipwrecks/period.js",
|
||||
"src/layers/shipwrecks/records.js",
|
||||
"src/layers/shipwrecks/source.js",
|
||||
"src/layers/vessels/motion.js",
|
||||
"src/time/snapshotTimeline.js",
|
||||
"src/ui/kit/UiSelect.js",
|
||||
"src/ui/news/NewsCard.vue",
|
||||
"src/ui/news/NewsFeed.vue",
|
||||
"src/ui/newsPanel.js"
|
||||
],
|
||||
"external": [
|
||||
"hls.js",
|
||||
@@ -3263,7 +3321,31 @@
|
||||
"src/globe/cesium/celestialHost.js",
|
||||
"src/globe/cesium/windGpuRendering.js",
|
||||
"src/globe/cesium/windRelief.js",
|
||||
"src/globe/cesium/windRendering.js"
|
||||
"src/globe/cesium/windRendering.js",
|
||||
"src/app/layers/news.js",
|
||||
"src/app/layers/shipwrecks.js",
|
||||
"src/data/detectionOcclusion.js",
|
||||
"src/data/displayText.js",
|
||||
"src/data/regionalNewsWindow.js",
|
||||
"src/data/sceneOcclusionProbe.js",
|
||||
"src/data/trailContinuity.js",
|
||||
"src/data/trailRenderer.js",
|
||||
"src/globe/cesium/weatherFrames.js",
|
||||
"src/layers/news/cards.js",
|
||||
"src/layers/news/index.js",
|
||||
"src/layers/news/model.js",
|
||||
"src/layers/news/records.js",
|
||||
"src/layers/news/source.js",
|
||||
"src/layers/news/sprites.js",
|
||||
"src/layers/shipwrecks/cards.js",
|
||||
"src/layers/shipwrecks/eras.js",
|
||||
"src/layers/shipwrecks/index.js",
|
||||
"src/layers/shipwrecks/period.js",
|
||||
"src/layers/shipwrecks/records.js",
|
||||
"src/layers/shipwrecks/source.js",
|
||||
"src/layers/vessels/motion.js",
|
||||
"src/sources/live/contract.js",
|
||||
"src/time/snapshotTimeline.js"
|
||||
],
|
||||
"external": [
|
||||
"hls.js",
|
||||
@@ -3351,7 +3433,8 @@
|
||||
"src/data/placeBoundaries.js",
|
||||
"src/data/placeNames.js",
|
||||
"src/data/local_data/us_census_places/files.js",
|
||||
"src/data/local_data/wof_neighborhoods/files.js"
|
||||
"src/data/local_data/wof_neighborhoods/files.js",
|
||||
"src/data/regionalNewsWindow.js"
|
||||
],
|
||||
"external": [
|
||||
"cesium",
|
||||
@@ -3573,7 +3656,11 @@
|
||||
},
|
||||
"regional-source": {
|
||||
"exports": ["./sources/regional"],
|
||||
"modules": ["src/data/regionalModel.js", "src/geoDistance.js"],
|
||||
"modules": [
|
||||
"src/data/regionalModel.js",
|
||||
"src/geoDistance.js",
|
||||
"src/data/regionalNewsWindow.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
"voice-action-schemas": {
|
||||
@@ -3636,7 +3723,8 @@
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/",
|
||||
"src/sources/vectorTiles.js",
|
||||
"src/sources/phaseTiming.js"
|
||||
"src/sources/phaseTiming.js",
|
||||
"src/data/displayText.js"
|
||||
],
|
||||
"external": ["@mapbox/vector-tile", "pbf"]
|
||||
},
|
||||
@@ -3764,7 +3852,8 @@
|
||||
"src/geoDistance.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/",
|
||||
"src/sources/nominatim.js"
|
||||
"src/sources/nominatim.js",
|
||||
"src/data/regionalNewsWindow.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -3787,7 +3876,8 @@
|
||||
"src/data/regionalModel.js",
|
||||
"src/geoDistance.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/"
|
||||
"src/i18n/",
|
||||
"src/data/regionalNewsWindow.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -4273,7 +4363,8 @@
|
||||
"src/layers/satellites/sensors.js",
|
||||
"src/data/ucsSatellites.js",
|
||||
"src/data/imagingSensors.js",
|
||||
"src/data/planespottersPhoto.js"
|
||||
"src/data/planespottersPhoto.js",
|
||||
"src/data/displayText.js"
|
||||
],
|
||||
"external": ["@mapbox/vector-tile", "pbf"]
|
||||
},
|
||||
@@ -4385,5 +4476,40 @@
|
||||
"src/layers/wildlifeSightings/performance.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
"news-provider": {
|
||||
"runtime": "node",
|
||||
"exports": ["./server/providers/news"],
|
||||
"modules": [
|
||||
"server/providers/news.js",
|
||||
"server/providers/news/keyed.js",
|
||||
"server/providers/news/categorize.js",
|
||||
"server/providers/news/areas.js",
|
||||
"server/providers/news/gdeltEvents.js",
|
||||
"server/providers/news/upstream.js",
|
||||
"server/providers/common/http.js",
|
||||
"server/providers/common/same-site.js",
|
||||
"src/data/adminBoundaries.js",
|
||||
"src/data/bundledJson.js",
|
||||
"src/data/retryableLoad.js",
|
||||
"src/layers/news/source.js",
|
||||
"src/layers/news/records.js",
|
||||
"src/localRequestGate.mjs",
|
||||
"src/sources/httpBody.js",
|
||||
"src/sources/capability.js",
|
||||
"src/i18n/marker.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
"news-source": {
|
||||
"exports": ["./layers/news/source"],
|
||||
"modules": [
|
||||
"src/layers/news/source.js",
|
||||
"src/layers/news/records.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/sources/capability.js",
|
||||
"src/i18n/marker.js"
|
||||
],
|
||||
"external": []
|
||||
}
|
||||
}
|
||||
@@ -286,6 +286,12 @@ const soakRuns = quick
|
||||
const longRun = quick ? '--swaps=100' : '--swaps=500';
|
||||
// label, script, layers, extra arguments, timeout in seconds.
|
||||
const runs = [
|
||||
[
|
||||
'switch-equals-cold-start--markers',
|
||||
'switch-equals-cold-start',
|
||||
['news', 'shipwrecks'],
|
||||
['--group=markers'],
|
||||
],
|
||||
[
|
||||
'switch-equals-cold-start--aircraft',
|
||||
'switch-equals-cold-start',
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env node
|
||||
/** Check every rendered frame across a trail chunk boundary on either engine.
|
||||
* Requires a dev server: node scripts/qa-trail-freeze.mjs --url http://localhost:4312
|
||||
*/
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import { waitForEngine } from './qa-strict-harness.mjs';
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||
|
||||
const urlFlag = process.argv.indexOf('--url');
|
||||
const url = urlFlag > 0 ? process.argv[urlFlag + 1] : 'http://localhost:4312';
|
||||
const browser = await launchBrowser({ protocolTimeout: 300_000 });
|
||||
let failed = false;
|
||||
const report = (name, ok, detail = '') => {
|
||||
if (!ok) failed = true;
|
||||
console.log(`[${ok ? 'PASS' : 'FAIL'}] ${name} ${detail}`);
|
||||
};
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 640 });
|
||||
await blockPaidQaMaps(page);
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||
await waitForEngine(page);
|
||||
const result = await evaluateWithGeo(page, async (geo) => {
|
||||
const { createTrail, CHUNK_POINTS } =
|
||||
await import('/src/data/trailRenderer.js');
|
||||
const { engine } = window.__godsEyeView;
|
||||
engine.camera.cancelFlight();
|
||||
const camera = engine.camera.pose();
|
||||
const ahead = geo.cartesian3.add(
|
||||
camera.position,
|
||||
geo.cartesian3.multiplyByScalar(camera.direction, 3000),
|
||||
);
|
||||
const points = Array.from({ length: CHUNK_POINTS + 1 }, (_, index) =>
|
||||
geo.cartesian3.add(
|
||||
ahead,
|
||||
geo.cartesian3.multiplyByScalar(
|
||||
camera.right,
|
||||
-1200 + (2400 * index) / CHUNK_POINTS,
|
||||
),
|
||||
),
|
||||
);
|
||||
const gl =
|
||||
engine.host.canvas.getContext('webgl2') ||
|
||||
engine.host.canvas.getContext('webgl');
|
||||
const pixels = new Uint8Array(
|
||||
gl.drawingBufferWidth * gl.drawingBufferHeight * 4,
|
||||
);
|
||||
const nextFrame = () =>
|
||||
new Promise((resolve) => {
|
||||
const remove = engine.frames.postRender.addEventListener(() => {
|
||||
remove();
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
gl.readPixels(
|
||||
0,
|
||||
0,
|
||||
gl.drawingBufferWidth,
|
||||
gl.drawingBufferHeight,
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
pixels,
|
||||
);
|
||||
let count = 0;
|
||||
for (let p = 0; p < pixels.length; p += 4)
|
||||
if (pixels[p] > 150 && pixels[p + 2] > 150 && pixels[p + 1] < 110)
|
||||
count++;
|
||||
resolve(count);
|
||||
});
|
||||
engine.render.request('qa-trail-freeze');
|
||||
});
|
||||
const beforeIds = new Set(engine.lines.list().map(({ id }) => id));
|
||||
const ids = new Map();
|
||||
const createLine = engine.lines.createLine;
|
||||
engine.lines.createLine = (options) => {
|
||||
const line = createLine(options);
|
||||
ids.set(line, options.id);
|
||||
return line;
|
||||
};
|
||||
const trail = createTrail(engine, { color: '#ff00ff', width: 8 });
|
||||
try {
|
||||
trail.setPositions(points.slice(0, CHUNK_POINTS));
|
||||
for (let i = 0; i < 20; i++) await nextFrame();
|
||||
const frozenBefore = trail._chunksForTest().frozen.length;
|
||||
let baseline = 0;
|
||||
for (let i = 0; i < 5; i++)
|
||||
baseline = Math.max(baseline, await nextFrame());
|
||||
trail.setPositions(points);
|
||||
const frames = [];
|
||||
for (let i = 0; i < 60; i++) {
|
||||
const pixels = await nextFrame();
|
||||
const lines = engine.lines.list();
|
||||
const covered = trail
|
||||
._chunksForTest()
|
||||
.frozen.every(({ line }) =>
|
||||
lines.some(
|
||||
(drawn) =>
|
||||
drawn.id === ids.get(line) &&
|
||||
drawn.shown &&
|
||||
drawn.points >= CHUNK_POINTS,
|
||||
),
|
||||
);
|
||||
frames.push({ covered, pixels });
|
||||
}
|
||||
const frozen = trail._chunksForTest().frozen.length;
|
||||
trail.clear();
|
||||
await nextFrame();
|
||||
const frozenAfterClear = trail._chunksForTest().frozen.length;
|
||||
trail.destroy();
|
||||
return {
|
||||
baseline,
|
||||
frozenBefore,
|
||||
frozen,
|
||||
frames,
|
||||
frozenAfterClear,
|
||||
leaked: engine.lines.list().filter(({ id }) => !beforeIds.has(id))
|
||||
.length,
|
||||
};
|
||||
} finally {
|
||||
trail.destroy();
|
||||
engine.lines.createLine = createLine;
|
||||
}
|
||||
});
|
||||
report(
|
||||
'the trail draws before the freeze',
|
||||
result.baseline > 200 && result.frozenBefore === 0,
|
||||
`pixels=${result.baseline}`,
|
||||
);
|
||||
report('the chunk freezes into a retained line', result.frozen > 0);
|
||||
report(
|
||||
'every frame retains the frozen chunk',
|
||||
result.frames.every(({ covered }) => covered),
|
||||
);
|
||||
report(
|
||||
'no blank frame across the chunk boundary',
|
||||
result.frames.every(({ pixels }) => pixels >= result.baseline * 0.9),
|
||||
);
|
||||
report(
|
||||
'clearing and destroying release all chunks',
|
||||
result.frozenAfterClear === 0 && result.leaked === 0,
|
||||
);
|
||||
} catch (error) {
|
||||
report('gate ran', false, error.stack);
|
||||
} finally {
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(failed ? 'RESULT: FAIL' : 'RESULT: PASS');
|
||||
process.exitCode = failed ? 1 : 0;
|
||||
@@ -816,7 +816,7 @@ try {
|
||||
);
|
||||
check(
|
||||
`${preset.name}: the icon raster cache stays bounded`,
|
||||
styling.iconCacheSize <= 6 * 2 * 3,
|
||||
styling.iconCacheSize <= 6 * 2 * 3 * 2,
|
||||
`${styling.iconCacheSize} rasters`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env node
|
||||
/** Supplied decoded weather tiles on Cesium, and inert controls on Navara.
|
||||
* Requires a keyless dev server: node scripts/qa-weather-decoded-tiles.mjs http://localhost:4312
|
||||
*/
|
||||
import assert from 'node:assert/strict';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import { waitForEngine } from './qa-strict-harness.mjs';
|
||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
|
||||
const base = new URL(process.argv[2] || 'http://localhost:4312');
|
||||
const browser = await launchBrowser();
|
||||
try {
|
||||
for (const kind of ['cesium', 'navara']) {
|
||||
const page = await browser.newPage();
|
||||
await blockPaidQaMaps(page);
|
||||
const url = new URL(base);
|
||||
url.search = `?engine=${kind}&welcome=0`;
|
||||
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
|
||||
await waitForEngine(page);
|
||||
await evaluateWithGeo(page, (geo) => {
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: geo.cartesian3.fromDegrees(-79.38, 43.65, 500000),
|
||||
});
|
||||
});
|
||||
await page.evaluate(async () => {
|
||||
const { createWeatherLayer } =
|
||||
await import('/src/layers/weather/index.js');
|
||||
const { rendererFor } = await import('/src/app/layers/renderers.js');
|
||||
const { engine } = window.__godsEyeView;
|
||||
const time = '2026-11-01T12:00:00.000Z';
|
||||
const probe = { requests: 0, time, before: engine.imagery.list().length };
|
||||
probe.layer = createWeatherLayer({
|
||||
id: 'weather-radar',
|
||||
feed: {
|
||||
getSnapshot: async () => ({
|
||||
schemaVersion: 1,
|
||||
product: 'radar',
|
||||
times: [time],
|
||||
latest: time,
|
||||
bounds: { west: -130, south: 20, east: -60, north: 55 },
|
||||
tileSize: 256,
|
||||
maxLevel: 6,
|
||||
tilingScheme: 'geographic',
|
||||
}),
|
||||
getFrameTile: async () => {
|
||||
probe.requests++;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = canvas.height = 256;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = '#ff00ff';
|
||||
ctx.fillRect(0, 0, 256, 256);
|
||||
return canvas;
|
||||
},
|
||||
},
|
||||
createNativeRendering: (engine, options) =>
|
||||
rendererFor('weather-radar', engine, options),
|
||||
});
|
||||
probe.layer.attachRenderer({ engine });
|
||||
probe.layer.enable();
|
||||
window.__decodedWeatherProbe = probe;
|
||||
await probe.layer.update();
|
||||
});
|
||||
if (kind === 'cesium')
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const probe = window.__decodedWeatherProbe;
|
||||
return (
|
||||
probe.requests > 0 &&
|
||||
probe.layer.getDiagnostics().time === probe.time
|
||||
);
|
||||
},
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
const result = await page.evaluate(() => {
|
||||
const probe = window.__decodedWeatherProbe;
|
||||
const diagnostics = probe.layer.getDiagnostics();
|
||||
const controls = probe.layer.getRowControls();
|
||||
probe.layer.destroy();
|
||||
delete window.__decodedWeatherProbe;
|
||||
return {
|
||||
requests: probe.requests,
|
||||
time: diagnostics.time,
|
||||
expectedTime: probe.time,
|
||||
controls: controls !== null,
|
||||
remaining: window.__godsEyeView.engine.imagery.list().length,
|
||||
before: probe.before,
|
||||
};
|
||||
});
|
||||
if (kind === 'cesium') {
|
||||
assert.ok(result.requests > 0);
|
||||
assert.equal(result.time, result.expectedTime);
|
||||
assert.equal(result.controls, true);
|
||||
} else {
|
||||
assert.equal(result.requests, 0);
|
||||
assert.equal(result.time, null);
|
||||
assert.equal(result.controls, false);
|
||||
}
|
||||
assert.equal(result.remaining, result.before);
|
||||
console.log(
|
||||
`[PASS] decoded weather tiles on ${kind}: ${JSON.stringify(result)}`,
|
||||
);
|
||||
await page.close();
|
||||
}
|
||||
} finally {
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { compareLedgers, drawLedger } from './ledger.mjs';
|
||||
|
||||
// Each group needs four page starts and two picker switches within 300 seconds.
|
||||
export const COLD_START_GROUPS = {
|
||||
markers: { layers: ['news', 'shipwrecks'], view: [49, 36, 1000000] },
|
||||
aircraft: {
|
||||
layers: ['flights', 'military'],
|
||||
view: [40.2539, -125.5779, 21130],
|
||||
|
||||
@@ -337,6 +337,14 @@ function installFeeds({ group, epoch, groups, ghost, launches, firms }) {
|
||||
})
|
||||
: response,
|
||||
);
|
||||
if (path === '/api/shipwrecks')
|
||||
return json({ records: [{ id: 'fixture-wreck', name: 'Fixture wreck', source: 'wikidata', lat: 49.2, lon: 36.2, year: 1912, casualties: 12 }], version: 'fixture', fetchedAt: epoch, sources: { emodnet: true, wikidata: true } });
|
||||
if (path === '/api/news/hotspots')
|
||||
return json({ hotspots: [{ id: 'fixture-area', name: 'Fixture area', bbox: [35, 48, 37, 50], focus: { lat: 49, lon: 36 }, counts: { last24h: 1 } }] });
|
||||
if (path === '/api/news/feed')
|
||||
return json({ reports: [{ id: 'fixture-report', title: 'Fixture report', url: 'https://example.com/report', publishedAt: new RealDate(epoch - 600_000).toISOString(), publisher: 'Example', category: 'strike', severity: 3, relevance: 'major', hotspotId: 'fixture-area', geo: { lat: 49, lon: 36, place: 'Fixture place', mappable: true } }], nextBefore: null });
|
||||
if (path === '/api/news/outline')
|
||||
return json({ polygons: [{ outer: [[35, 48], [37, 48], [37, 50], [35, 50]], holes: [] }], borders: [], disputed: [] });
|
||||
if (path === '/api/flights')
|
||||
return json({
|
||||
time: epoch / 1000,
|
||||
|
||||
@@ -10,6 +10,8 @@ import { createTrafficDisplay } from '../../src/layers/traffic/render/display.js
|
||||
import { createAnimation } from '../../src/layers/traffic/animation.js';
|
||||
import { createRecentImagerySource } from '../../src/layers/recentImagery/source.js';
|
||||
import { createThumbnailLoader } from '../../src/layers/recentImagery/thumbnails.js';
|
||||
import { createShipwrecksSource } from '../../src/layers/shipwrecks/source.js';
|
||||
import { createNewsSource } from '../../src/layers/news/source.js';
|
||||
|
||||
const origin = 'http://127.0.0.1:4309';
|
||||
test('every group has valid strict fixture routes', async () => {
|
||||
@@ -161,6 +163,17 @@ test('the page fetch guard delegates NASA snapshot APIs to their image fixture',
|
||||
await assert.rejects(page.fetch('/api/v1/snapshot'), /No cold-start fixture/);
|
||||
});
|
||||
|
||||
test('marker fixtures pass the real news and shipwreck source decoders', async () => {
|
||||
const page = await installedPage('markers');
|
||||
const news = createNewsSource({ fetchImpl: page.fetch });
|
||||
const wrecks = createShipwrecksSource({ fetchImpl: page.fetch });
|
||||
assert.ok((await wrecks.getSnapshot()).records.length);
|
||||
const { hotspots } = await news.getHotspots();
|
||||
assert.ok(hotspots.length);
|
||||
assert.ok((await news.getFeed()).reports.length);
|
||||
assert.ok((await news.getOutline(hotspots[0].id)).polygons.length);
|
||||
});
|
||||
|
||||
test('every TLE group has matching deterministic SATCAT records on both pages', async () => {
|
||||
const a = await installedPage('space'),
|
||||
b = await installedPage('space');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { optionSpecs, REGISTRY_BY_ID } from '../../src/data/layerState.js';
|
||||
import { createFakeEngine } from '../../src/globe/testing/fakeEngine.js';
|
||||
import { createOsmPlacesLayer } from '../../src/layers/osmPlaces/index.js';
|
||||
@@ -35,7 +35,7 @@ test('every declaration is covered by a real handoff exercise, without extra fie
|
||||
.trim()
|
||||
.split('\n');
|
||||
const found = {};
|
||||
for (const file of files.filter((file) => file.endsWith('.js'))) {
|
||||
for (const file of files.filter((file) => file.endsWith('.js') && existsSync(file))) {
|
||||
const text = readFileSync(file, 'utf8');
|
||||
for (const match of text.matchAll(/sessionState\s*:\s*(\[[^\]]*\])/g)) {
|
||||
assert.ok(!found[file], `Duplicate declaration in ${file}`);
|
||||
|
||||
+153
-21
@@ -42,8 +42,9 @@
|
||||
* and a real mouse click on the tracked plane is a
|
||||
* NO-OP (no deselect, no camera spike).
|
||||
* M3. AGE-OUT RELEASE — when the tracked plane's fixes stop arriving
|
||||
* (3 missed polls via the shim), tracking clears and
|
||||
* the camera is RELEASED IN PLACE: viewer.trackedEntity
|
||||
* it stays tracked (STALE, then SIGNAL LOST) until
|
||||
* 10 min after its newest fix; then tracking clears
|
||||
* and the camera is RELEASED IN PLACE: viewer.trackedEntity
|
||||
* undefined, NO jump (owner decision 2026-07-02 —
|
||||
* the old ~80 km overview flyTo is gone).
|
||||
*
|
||||
@@ -362,7 +363,12 @@ async function main() {
|
||||
'Synthetica', // 2 origin_country
|
||||
Math.floor(Date.now() / 1000), // 3 time_position
|
||||
Math.floor(Date.now() / 1000), // 4 last_contact
|
||||
f.lon, // 5 longitude
|
||||
// A plane given `moveFromMs` flies east at its reported speed
|
||||
// (the trail-head check needs a moving icon); others hold still.
|
||||
Number.isFinite(f.moveFromMs)
|
||||
? f.lon + (f.vel * (Date.now() - f.moveFromMs)) / 1000
|
||||
/ (111320 * Math.cos((f.lat * Math.PI) / 180))
|
||||
: f.lon, // 5 longitude
|
||||
f.lat, // 6 latitude
|
||||
f.alt, // 7 baro_altitude (m)
|
||||
f.onGround === true, // 8 on_ground (ground-traffic phase flips this live)
|
||||
@@ -1498,6 +1504,110 @@ async function main() {
|
||||
`max=${maxDelta.toFixed(3)} m over ${samples.length} frames (tol ${JITTER_TOL_M} m, mode=${modelUp ? 'model-matrix' : 'display-contract'})`);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// TRAIL HEAD ENDS AT THE DRAWN ICON, every rendered frame.
|
||||
// The head's last vertex is where the tracked plane is drawn (the
|
||||
// display position, or the model's tail anchor while the model is the
|
||||
// visual). getTrackedInfo reports the dead-reckoned position without
|
||||
// the brief post-report easing; its gap is logged, not asserted.
|
||||
// ============================================================
|
||||
console.log('\nTrail head — ends at the drawn icon on every frame');
|
||||
// The fixture planes repeat one position, so the icon holds and draws no
|
||||
// head. Fly the tracked one east for this check; the display runs 30 s
|
||||
// behind, so give it time to reach the moving reports.
|
||||
await evalPage((icao) => {
|
||||
const f = window.__SYNTH.flights.find((p) => p.icao === icao);
|
||||
f.moveFromMs = Date.now();
|
||||
}, trackedIcao);
|
||||
await sleep(55_000);
|
||||
const headSamples = await sampleFrames(page, 40, (icao) => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const fl = window.__godsEyeView.dataManager.layers.get('flights').module;
|
||||
const ent = viewer.trackedEntity;
|
||||
const time = viewer.clock.currentTime;
|
||||
const head = viewer.entities.values.find((e) => String(e.id).startsWith('gev-trail:fl-head-'));
|
||||
const line = head?.polyline?.positions?.getValue(time);
|
||||
const drawn = ent?.position?.getValue(time);
|
||||
if (!line?.length || !drawn) return null;
|
||||
const end = line[line.length - 1];
|
||||
const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z);
|
||||
const ti = fl.getTrackedInfo();
|
||||
let infoGap = null;
|
||||
if (ti && ti.icao24 === icao) {
|
||||
// WGS84 lat/lon → ECEF on the ellipsoid; the angle at the centre to
|
||||
// the drawn position, times the mean radius, is the ground gap.
|
||||
const a = 6378137, e2 = 6.69437999014e-3;
|
||||
const lat = (ti.latitude * Math.PI) / 180, lon = (ti.longitude * Math.PI) / 180;
|
||||
const n = a / Math.sqrt(1 - e2 * Math.sin(lat) ** 2);
|
||||
const p = {
|
||||
x: n * Math.cos(lat) * Math.cos(lon),
|
||||
y: n * Math.cos(lat) * Math.sin(lon),
|
||||
z: n * (1 - e2) * Math.sin(lat),
|
||||
};
|
||||
const dot = p.x * drawn.x + p.y * drawn.y + p.z * drawn.z;
|
||||
const cos = dot / (Math.hypot(p.x, p.y, p.z) * Math.hypot(drawn.x, drawn.y, drawn.z));
|
||||
infoGap = 6371008.8 * Math.acos(Math.min(1, cos));
|
||||
}
|
||||
// While the 3D model is the visual the head ends at the model's tail
|
||||
// anchor, which sits on the drawn (pixel-size-enlarged) model.
|
||||
const model = window.__findTrackedModel ? window.__findTrackedModel() : null;
|
||||
const sphere = model && model.ready && model.show ? model.boundingSphere : null;
|
||||
// boundingSphere leaves out the minimum-pixel-size enlargement that
|
||||
// computedScale carries; the drawn model is that much larger.
|
||||
const enlarge = sphere && Number.isFinite(model.computedScale) && model.scale > 0
|
||||
? Math.max(1, model.computedScale / model.scale)
|
||||
: 1;
|
||||
const radius = sphere ? sphere.radius * enlarge : null;
|
||||
return {
|
||||
end: dist(end, drawn),
|
||||
onModel: sphere ? dist(end, sphere.center) <= radius + 50 : false,
|
||||
modelRadius: radius,
|
||||
infoGap,
|
||||
vertices: line.length,
|
||||
};
|
||||
}, trackedIcao);
|
||||
// The same reading between frames, the way a page script polls it: the
|
||||
// head holds the last rendered frame while getTrackedInfo dead-reckons
|
||||
// the current instant. Logged only, to tell the two apart.
|
||||
const betweenFrames = [];
|
||||
for (let i = 0; i < 8; i++) {
|
||||
await sleep(600);
|
||||
betweenFrames.push(await evalPage((icao) => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const head = viewer.entities.values.find((e) => String(e.id).startsWith('gev-trail:fl-head-'));
|
||||
const line = head?.polyline?.positions?.getValue(viewer.clock.currentTime);
|
||||
const drawn = viewer.trackedEntity?.position?.getValue(viewer.clock.currentTime);
|
||||
if (!line?.length || !drawn) return null;
|
||||
const end = line[line.length - 1];
|
||||
return Math.hypot(end.x - drawn.x, end.y - drawn.y, end.z - drawn.z);
|
||||
}, trackedIcao));
|
||||
}
|
||||
console.log(` (between frames: head end vs entity position, m: ${
|
||||
betweenFrames.map((v) => (v == null ? '-' : v.toFixed(1))).join(' ')})`);
|
||||
// Freeze the plane where it got to, so later checks see a still contact.
|
||||
await evalPage((icao) => {
|
||||
const f = window.__SYNTH.flights.find((p) => p.icao === icao);
|
||||
f.lon += (f.vel * (Date.now() - f.moveFromMs)) / 1000
|
||||
/ (111320 * Math.cos((f.lat * Math.PI) / 180));
|
||||
delete f.moveFromMs;
|
||||
}, trackedIcao);
|
||||
const headUsable = headSamples.values.filter((v) => v != null);
|
||||
if (headUsable.length < 10) {
|
||||
skip('trail head: ends at the drawn icon on every frame',
|
||||
`only ${headUsable.length} frames with a drawn head (timedOut=${headSamples.timedOut})`);
|
||||
} else {
|
||||
const worst = Math.max(...headUsable.map((v) => v.end));
|
||||
const off = headUsable.filter((v) => !(v.end <= 50 || v.onModel));
|
||||
const onModel = headUsable.filter((v) => v.end > 50 && v.onModel).length;
|
||||
const radii = headUsable.map((v) => v.modelRadius).filter((v) => v != null);
|
||||
const gaps = headUsable.map((v) => v.infoGap).filter((v) => v != null);
|
||||
record('trail head: last vertex on the drawn icon (within 50 m, or on the drawn 3D model) on every frame',
|
||||
off.length === 0,
|
||||
`${off.length} of ${headUsable.length} frames off; max end-to-centre=${worst.toFixed(1)} m, ${onModel} frames ending on the model's tail (model radius ${
|
||||
radii.length ? Math.max(...radii).toFixed(0) : 'n/a'} m); getTrackedInfo vs drawn icon max=${
|
||||
gaps.length ? Math.max(...gaps).toFixed(1) : 'n/a'} m (logged only)`);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// INVARIANT 2: NO PULL-OUT on a track switch
|
||||
// tracking B while tracking A must not fly to ~80 km overview.
|
||||
@@ -1744,7 +1854,8 @@ async function main() {
|
||||
|
||||
// ============================================================
|
||||
// REGRESSION M3 (rewritten 2026-07-02 — deliberate behavior change):
|
||||
// when the TRACKED plane ages out (3 missed polls), tracking must clear
|
||||
// when the TRACKED plane ages out (10 min after its newest fix, past the
|
||||
// 3 missed polls that remove an untracked plane), tracking must clear
|
||||
// and the camera must be RELEASED IN PLACE — viewer.trackedEntity
|
||||
// undefined and NO jump (the old contract flew an ~80 km overview; the
|
||||
// owner field-ruled that wrong: "it randomly zooms way up and loses my
|
||||
@@ -1765,27 +1876,48 @@ async function main() {
|
||||
const beforeTracking = !!fl.getTrackedInfo();
|
||||
// Simulate the age-out via the shim: the plane stops arriving.
|
||||
window.__SYNTH.flights = window.__SYNTH.flights.filter((f) => f.icao !== icao);
|
||||
// MISSING_POLL_LIMIT = 3 consecutive absent polls → removal + clear.
|
||||
// Capture the camera pose right BEFORE the removing poll — the follow
|
||||
// camera drifts with the coasting plane until the instant of release,
|
||||
// so this is the "where the user was looking" reference.
|
||||
// The tracked plane outlives MISSING_POLL_LIMIT (3 absent polls): it is
|
||||
// kept until 10 min after its newest fix, so the camera keeps its target.
|
||||
await fl.update(v);
|
||||
await fl.update(v);
|
||||
const c0 = v.camera.positionCartographic;
|
||||
const atAgeOut = {
|
||||
lonDeg: (c0.longitude * 180) / Math.PI,
|
||||
latDeg: (c0.latitude * 180) / Math.PI,
|
||||
height: c0.height,
|
||||
};
|
||||
await fl.update(v); // third miss → removal + clear + in-place release
|
||||
return {
|
||||
beforeTracking,
|
||||
atAgeOut,
|
||||
stillTrackedAfter: !!fl.getTrackedInfo(),
|
||||
viewerStillTracking: !!v.trackedEntity,
|
||||
};
|
||||
await fl.update(v);
|
||||
const trackedThroughMisses = !!fl.getTrackedInfo();
|
||||
// Jump the page clock past the 10 min retention instead of waiting.
|
||||
const realNow = Date.now;
|
||||
Date.now = () => realNow() + 11 * 60 * 1000;
|
||||
try {
|
||||
// Let the follow camera settle on the stopped plane, then capture the
|
||||
// pose right BEFORE the removing poll: the "where the user was
|
||||
// looking" reference.
|
||||
await new Promise((res) => {
|
||||
const stop = v.scene.postRender.addEventListener(() => {
|
||||
stop();
|
||||
res();
|
||||
});
|
||||
v.scene.requestRender();
|
||||
});
|
||||
const c0 = v.camera.positionCartographic;
|
||||
const atAgeOut = {
|
||||
lonDeg: (c0.longitude * 180) / Math.PI,
|
||||
latDeg: (c0.latitude * 180) / Math.PI,
|
||||
height: c0.height,
|
||||
};
|
||||
await fl.update(v); // past retention → removal + clear + in-place release
|
||||
return {
|
||||
beforeTracking,
|
||||
trackedThroughMisses,
|
||||
atAgeOut,
|
||||
stillTrackedAfter: !!fl.getTrackedInfo(),
|
||||
viewerStillTracking: !!v.trackedEntity,
|
||||
};
|
||||
} finally {
|
||||
Date.now = realNow;
|
||||
}
|
||||
}, m3Icao);
|
||||
|
||||
record('M3: the tracked plane stays tracked through 3 missed polls (kept 10 min after its newest fix)',
|
||||
m3.trackedThroughMisses,
|
||||
`trackedThroughMisses=${m3.trackedThroughMisses}`);
|
||||
record('M3: tracking cleared after the tracked plane ages out (getTrackedInfo null)',
|
||||
m3.beforeTracking && !m3.stillTrackedAfter,
|
||||
`before=${m3.beforeTracking} trackedAfter=${m3.stillTrackedAfter}`);
|
||||
|
||||
Reference in new issue
Block a user