feat(navara): render clipped recent imagery comparisons

This commit is contained in:
German Khokhlov committed 2026-10-29 18:30:49 +04:00
1 parent 5a8bc33fbd
commit f59d134928
15 files changed
+1526 -21

No files matched your search

+5 -1
View File
@@ -1266,5 +1266,9 @@
"scripts/fixtures/w2/radio.mjs",
"scripts/qa-map-policy.mjs",
"scripts/qa-map-policy.test.mjs",
"scripts/qa-map-startup.mjs"
"scripts/qa-map-startup.mjs",
"src/layers/recentImagery/render/tiles.test.mjs",
"src/layers/recentImagery/render/navara.test.mjs",
"src/ui/imageryBoxNavara.test.mjs",
"scripts/qa-w2-imagery.mjs"
]
+4
View File
@@ -2571,6 +2571,7 @@
"src/maps/imageryHost.js",
"src/ui/recentImagery.js",
"src/ui/imageryBoxTool.js",
"src/ui/imageryBoxNavara.js",
"src/voice/pointerContext.js",
"src/voice/pointerReticle.js",
"src/voice/deixis.js",
@@ -2710,6 +2711,9 @@
"src/layers/transit/render/navaraSprites.js",
"src/layers/transit/render/navaraView.js",
"src/layers/transit/render/navaraTrail.js",
"src/layers/recentImagery/render/navara.js",
"src/layers/recentImagery/render/navaraGround.js",
"src/layers/recentImagery/render/tiles.js",
"src/layers/cctv/render/navara.js",
"src/layers/cctv/render/navaraMeshes.js",
"src/layers/cctv/render/navaraGizmo.js",
+428
View File
@@ -0,0 +1,428 @@
import fs from 'node:fs/promises';
import puppeteer from 'puppeteer';
import sharp from 'sharp';
import { blockPaidQaMaps } from './qa-map-policy.mjs';
import { isExpectedPreviewSetupError, redact } from './qa-browserEvidence.mjs';
const args = process.argv.slice(2);
const option = (name, fallback) =>
args.includes(name) ? args[args.indexOf(name) + 1] : fallback;
const url = option('--url', 'http://127.0.0.1:4193');
const output = option('--report', '/private/tmp/gev-imagery.json');
if (
['4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
new URL(url).port,
)
)
throw new Error('Reserved QA port');
const report = {
url,
checks: [],
errors: [],
expectedErrors: [],
requests: [],
};
let failLandsat = false,
failedTiles = 0;
const check = (name, value) => {
report.checks.push({ name, pass: Boolean(value) });
console.log(`${value ? 'PASS' : 'FAIL'} ${name}`);
};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const colors = await Promise.all(
['#ed3838', '#35c969'].map((background) =>
sharp({ create: { width: 256, height: 256, channels: 4, background } })
.png()
.toBuffer(),
),
);
const browser = await puppeteer.launch({
headless: true,
args: [
'--no-sandbox',
'--use-angle=metal',
'--enable-gpu',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
],
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 });
await blockPaidQaMaps(page);
page.on('pageerror', (e) => report.errors.push(redact(e.message)));
page.on('console', (message) => {
if (message.type() !== 'error') return;
const text = redact(message.text());
const expected =
isExpectedPreviewSetupError(message, {
preview: args.includes('--preview'),
baseUrl: url,
}) ||
(failedTiles > 0 &&
((/\b503\b/.test(text) &&
String(message.location()?.url).includes('HLS_L30')) ||
(failLandsat && /Failed to obtain image tile/.test(text))));
(expected ? report.expectedErrors : report.errors).push(text);
});
const session = await page.createCDPSession();
await session.send('Fetch.enable', {
patterns: [{ urlPattern: '*://*.earthdata.nasa.gov/*' }],
});
session.on('Fetch.requestPaused', async (event) => {
try {
const request = new URL(event.request.url);
const catalog = request.hostname === 'cmr.earthdata.nasa.gov';
const green = request.href.includes('2026-10-27');
const tileFailure =
failLandsat &&
request.pathname.includes('HLS_L30') &&
request.hostname.startsWith('gibs');
if (tileFailure) failedTiles++;
report.requests.push({ host: request.hostname, path: request.pathname });
const body = catalog
? Buffer.from(
JSON.stringify({
hits: 2,
items: ['2026-10-28', '2026-10-27'].map((day) => ({
umm: {
TemporalExtent: {
RangeDateTime: { BeginningDateTime: `${day}T12:00:00Z` },
},
AdditionalAttributes: [
{ Name: 'CLOUD_COVERAGE', Values: ['0'] },
],
SpatialExtent: {
HorizontalSpatialDomain: {
Geometry: {
BoundingRectangles: [
{
WestBoundingCoordinate: -98,
EastBoundingCoordinate: -97,
SouthBoundingCoordinate: 30,
NorthBoundingCoordinate: 31,
},
],
},
},
},
},
})),
}),
)
: colors[green ? 1 : 0];
await session.send('Fetch.fulfillRequest', {
requestId: event.requestId,
responseCode: tileFailure ? 503 : 200,
responseHeaders: [
{
name: 'Content-Type',
value: catalog ? 'application/json' : 'image/png',
},
{ name: 'Access-Control-Allow-Origin', value: '*' },
{ name: 'Access-Control-Expose-Headers', value: 'Data-Present' },
{ name: 'Data-Present', value: 'true' },
{ name: 'Cache-Control', value: 'no-store' },
],
body: body.toString('base64'),
});
} catch (e) {
if (
String(e.message).includes(
'Fetch.fulfillRequest): Invalid InterceptionId',
)
)
report.expectedErrors.push(
'Fixture response cancelled before fulfillment',
);
else if (!page.isClosed()) report.errors.push(redact(e.message));
}
});
const target = new URL(url);
target.searchParams.set('welcome', '0');
target.hash = 'lat=30.27&lon=-97.74&alt=12000&heading=0&pitch=-90&map=osm';
await page.goto(target.href, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(
() => window.__gevRecentImagery && window.__godsEyeView,
{ timeout: 90000 },
);
await page.evaluate(async () => {
const app = window.__godsEyeView;
await app.styleManager.initialRestorePromise;
app.styleManager.orbitController.stop();
for (const [id, entry] of app.dataManager.layers)
if (entry.enabled) await app.dataManager.setEnabled(id, false);
app.styleManager._setDetectionMode('OFF');
await app.dataManager.setEnabled('recent-imagery', true);
window.__gevRecentImagery.layer.setBox({
west: -97.77,
east: -97.71,
south: 30.25,
north: 30.29,
});
});
await page.waitForFunction(
() =>
window.__gevRecentImagery.layer
.getSnapshot()
.candidates.filter(
(c) => c.product === 'S30' && c.thumbnail?.status === 'present',
).length >= 2,
{ timeout: 30000 },
);
await page.evaluate(() => {
const l = window.__gevRecentImagery.layer;
const c = l
.getSnapshot()
.candidates.filter((c) => c.product === 'S30')
.sort((a, b) => b.day.localeCompare(a.day));
l.setAssignment('a', c[0].key);
l.setAssignment('b', c[1].key);
l.setMode('image');
});
await page.waitForFunction(
() => window.__godsEyeView.engine.surface.tilesLoaded(),
{ timeout: 90000 },
);
await sleep(4000);
const capture = async (name) => {
await page.evaluate(() => window.__godsEyeView.requestRender('imagery-qa'));
await sleep(350);
const buffer = await page.screenshot();
await fs.writeFile(output.replace(/\.json$/, `-${name}.png`), buffer);
const pixels = await sharp(buffer)
.raw()
.toBuffer({ resolveWithObject: true });
const sample = (x, y) =>
Array.from(
pixels.data.subarray(
(y * pixels.info.width + x) * pixels.info.channels,
(y * pixels.info.width + x) * pixels.info.channels + 3,
),
);
const result = {
left: sample(565, 400),
right: sample(715, 400),
outside: sample(340, 400),
};
report[name] = result;
return result;
};
const red = (p) => p[0] > p[1] * 1.6 && p[0] > 100;
const green = (p) => p[1] > p[0] * 1.6 && p[1] > 100;
let image = await capture('image');
check(
'image pixels inside selected box',
red(image.left) && red(image.right),
);
check('geographic clip outside box', !red(image.outside));
await page.evaluate(() => window.__gevRecentImagery.layer.setMode('ab'));
await sleep(2500);
image = await capture('ab');
check('A/B dates on opposite sides', red(image.left) && green(image.right));
await page.evaluate(() => window.__gevRecentImagery.layer.swapSides());
image = await capture('swap');
check('swap reverses dates', green(image.left) && red(image.right));
await page.evaluate(() => window.__gevRecentImagery.layer.swapSides());
await page.mouse.move(640, 400);
await page.mouse.down();
await page.mouse.move(780, 400, { steps: 10 });
await page.mouse.up();
image = await capture('divider');
check(
'physical divider drag changes rendered split',
red(image.left) && red(image.right),
);
await page.focus('.recent-imagery-split-handle');
await page.keyboard.press('Home');
image = await capture('home');
check('divider Home exposes B', green(image.left) && green(image.right));
await page.keyboard.press('End');
image = await capture('end');
check('divider End exposes A', red(image.left) && red(image.right));
await page.evaluate(() => {
const l = window.__gevRecentImagery.layer;
l.setMode('image');
l.setAlpha(0);
});
const base = await capture('transparent');
await page.evaluate(() => window.__gevRecentImagery.layer.setAlpha(0.5));
const half = await capture('half');
check(
'half opacity blends source with basemap',
half.left.every(
(c, i) => Math.abs(c - (base.left[i] + report.image.left[i]) / 2) < 5,
),
);
await page.evaluate(() => {
const l = window.__gevRecentImagery.layer;
l.setAlpha(1);
l.setMode('basemap');
});
image = await capture('basemap');
check(
'basemap comparison clips only A',
red(image.left) && !red(image.right),
);
report.beforeToggle = await page.evaluate(() =>
window.__gevRecentImagery.layer.diagnostics(),
);
for (let i = 0; i < 10; i++)
await page.evaluate(async () => {
const app = window.__godsEyeView;
await app.dataManager.setEnabled('recent-imagery', false);
await app.dataManager.setEnabled('recent-imagery', true);
});
await page.waitForFunction(
() => window.__gevRecentImagery.layer.diagnostics().ownedCount === 1,
{ timeout: 30000 },
);
await sleep(2000);
image = await capture('toggles');
check('ten toggles restore visible image', red(image.left));
report.afterToggle = await page.evaluate(() =>
window.__gevRecentImagery.layer.diagnostics(),
);
if (report.afterToggle.renderer)
check(
'Navara tile admission stays bounded',
report.afterToggle.renderer.peak <= 6 &&
Object.values(report.afterToggle.renderer.slots).every(
(s) => !s || s.tiles <= 64,
),
);
failLandsat = true;
await page.evaluate(() => {
const l = window.__gevRecentImagery.layer;
l.setMode('image');
l.setAssignment(
'a',
l
.getSnapshot()
.candidates.find((c) => c.product === 'L30' && c.day === '2026-10-28')
.key,
);
});
await sleep(2000);
image = await capture('source-failure');
check(
'failed replacement does not leave stale image pixels',
failedTiles > 0 && !red(image.left),
);
report.failedTiles = failedTiles;
failLandsat = false;
await page.evaluate(() => {
const l = window.__gevRecentImagery.layer;
const keys = l
.getSnapshot()
.candidates.filter((c) => c.product === 'S30')
.sort((a, b) => b.day.localeCompare(a.day))
.map((c) => c.key);
for (let i = 0; i < 10; i++) l.setAssignment('a', keys[i % 2]);
});
await sleep(2000);
image = await capture('replaced');
check(
'rapid replacement leaves final date only',
green(image.left) && green(image.right),
);
await page.evaluate(() => window.__gevRecentImagery.layer.clear());
await sleep(1000);
check(
'clear removes both slots',
await page.evaluate(
() => window.__gevRecentImagery.layer.diagnostics().ownedCount === 0,
),
);
const dragStart = await page.evaluate(() => {
const app = window.__godsEyeView;
return {
pose: app.engine.camera.pose().position,
started: window.__gevRecentImagery.tool.start(),
};
});
await page.mouse.move(480, 300);
await page.mouse.down();
await page.mouse.move(780, 500, { steps: 12 });
await capture('box-drag');
await page.mouse.up();
const dragged = await page.evaluate(() => ({
box: window.__gevRecentImagery.layer.getSnapshot().box,
active: window.__gevRecentImagery.tool.isActive(),
pose: window.__godsEyeView.engine.camera.pose().position,
}));
check(
'physical box drag selects geography without moving camera',
dragStart.started &&
dragged.box &&
!dragged.active &&
['x', 'y', 'z'].every(
(k) => Math.abs(dragged.pose[k] - dragStart.pose[k]) < 0.1,
),
);
const usedView = await page.evaluate(() =>
window.__gevRecentImagery.layer.useCurrentView(),
);
check('USE VIEW works on real engine camera', usedView);
await page.evaluate(() => {
const e = window.__godsEyeView.engine,
p = e.camera.pose();
e.camera.setView({
destination: {
x: p.position.x * 1.2,
y: p.position.y * 1.2,
z: p.position.z * 1.2,
},
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
});
await page.waitForFunction(
() =>
window.__godsEyeView.engine.camera.pose().cartographic.height > 1000000,
{ timeout: 10000 },
);
await sleep(350);
const fit = (report.fit = await page.evaluate(() => {
const l = window.__gevRecentImagery.layer;
const used = l.useCurrentView();
return { used, offered: l.getSnapshot().zoomToFit, height: l.zoomToFit() };
}));
check(
'oversized view refuses and offers bounded zoom',
!fit.used && fit.offered && fit.height >= 5000 && fit.height <= 400000,
);
await sleep(2200);
check(
'ZOOM IN lands at requested height',
await page.evaluate(
(h) =>
Math.abs(
window.__godsEyeView.engine.camera.pose().cartographic.height - h,
) < 5,
fit.height,
),
);
await page.evaluate(async () => {
window.__gevRecentImagery.layer.clear();
await window.__godsEyeView.dataManager.setEnabled('recent-imagery', false);
});
await sleep(1000);
report.disabled = await page.evaluate(() =>
window.__gevRecentImagery.layer.diagnostics(),
);
check(
'disable leaves no imagery owners or queued requests',
report.disabled.ownedCount === 0 &&
(!report.disabled.renderer ||
(report.disabled.renderer.active === 0 &&
report.disabled.renderer.queued === 0)),
);
check('no browser exceptions', report.errors.length === 0);
} catch (e) {
report.errors.push(e.stack);
check('driver completes', false);
} finally {
await browser.close();
await fs.writeFile(output, JSON.stringify(report, null, 2));
}
if (report.checks.some((c) => !c.pass)) process.exitCode = 1;