mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 06:12:18 +00:00
feat(navara): render clipped recent imagery comparisons
This commit is contained in:
1 parent
5a8bc33fbd
commit
f59d134928
15 files changed
+1526
-21
No files matched your search
@@ -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"
|
||||
]
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user