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:
German Khokhlov committed 2026-11-11 13:41:30 +04:00
commit 1856477e0a
346 files changed
+41247 -1649

No files matched your search

+2 -2
View File
@@ -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,
+10
View File
@@ -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
View File
@@ -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": []
}
}
+6
View File
@@ -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',
+147
View File
@@ -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;
+1 -1
View File
@@ -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`,
);
}
+107
View File
@@ -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);
}
+1
View File
@@ -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],
+8
View File
@@ -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,
+13
View File
@@ -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');
+2 -2
View File
@@ -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
View File
@@ -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}`);