mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
fix(street-level): one keydown listener on the photo root; format scope listed a test twice
Esc moved to the viewer's box (`.sl-viewer-wrap`) in the last commit, but without one it was a second keydown listener on the root; Esc is now taken by the root's own handler there. format-scope.json listed panorama.test.mjs twice, which made the formatter refuse the scope; the files it skipped are formatted. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
cf502429a3
commit
1a546bec72
14 files changed
+96
-30
No files matched your search
@@ -937,7 +937,6 @@
|
|||||||
"src/testSupport/panoramaxFixtures.mjs",
|
"src/testSupport/panoramaxFixtures.mjs",
|
||||||
"src/layers/streetLevel/photoViewer.test.mjs",
|
"src/layers/streetLevel/photoViewer.test.mjs",
|
||||||
"src/layers/streetLevel/limiter.test.mjs",
|
"src/layers/streetLevel/limiter.test.mjs",
|
||||||
"src/layers/streetLevel/panorama.test.mjs",
|
|
||||||
"src/layers/streetLevel/lineCoverage.test.mjs",
|
"src/layers/streetLevel/lineCoverage.test.mjs",
|
||||||
"src/testSupport/kartaViewFakes.mjs",
|
"src/testSupport/kartaViewFakes.mjs",
|
||||||
"src/testSupport/googleStreetViewFakes.mjs",
|
"src/testSupport/googleStreetViewFakes.mjs",
|
||||||
|
|||||||
@@ -1276,7 +1276,10 @@ async function main() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
assert.ok(aimed, 'the line is pickable over the other’s pictures');
|
assert.ok(
|
||||||
|
aimed,
|
||||||
|
'the line is pickable over the other’s pictures',
|
||||||
|
);
|
||||||
at = await aimed.jsonValue();
|
at = await aimed.jsonValue();
|
||||||
}
|
}
|
||||||
const parked = await camera();
|
const parked = await camera();
|
||||||
|
|||||||
@@ -70,7 +70,11 @@ test('a task already aborted when its turn comes is skipped', async () => {
|
|||||||
const gate = createLimiter(1);
|
const gate = createLimiter(1);
|
||||||
const first = held(log, 'a');
|
const first = held(log, 'a');
|
||||||
const running = gate.run(first.task);
|
const running = gate.run(first.task);
|
||||||
const signal = { aborted: false, reason: new Error('gone'), addEventListener() {} };
|
const signal = {
|
||||||
|
aborted: false,
|
||||||
|
reason: new Error('gone'),
|
||||||
|
addEventListener() {},
|
||||||
|
};
|
||||||
const queued = gate.run(held(log, 'b').task, { signal });
|
const queued = gate.run(held(log, 'b').task, { signal });
|
||||||
signal.aborted = true;
|
signal.aborted = true;
|
||||||
first.finish();
|
first.finish();
|
||||||
|
|||||||
@@ -217,17 +217,29 @@ test('overview points take the provider’s size, each its own', async (t) => {
|
|||||||
|
|
||||||
test('a point budget skips lines past it, and a shorter one later may still fit', async (t) => {
|
test('a point budget skips lines past it, and a shorter one later may still fit', async (t) => {
|
||||||
const { state, coverage } = setup(t, {
|
const { state, coverage } = setup(t, {
|
||||||
tuning: { ...TUNING, maxTiles: 1, maxSequences: 10, maxPoints: 5, keepOrder: true },
|
tuning: {
|
||||||
|
...TUNING,
|
||||||
|
maxTiles: 1,
|
||||||
|
maxSequences: 10,
|
||||||
|
maxPoints: 5,
|
||||||
|
keepOrder: true,
|
||||||
|
},
|
||||||
decoded: (centre) => {
|
decoded: (centre) => {
|
||||||
const line = (id, n) => ({
|
const line = (id, n) => ({
|
||||||
id,
|
id,
|
||||||
parts: [
|
parts: [
|
||||||
Array.from({ length: n }, (_, i) => [centre.lon - 0.001 + i * 0.0001, centre.lat]),
|
Array.from({ length: n }, (_, i) => [
|
||||||
|
centre.lon - 0.001 + i * 0.0001,
|
||||||
|
centre.lat,
|
||||||
|
]),
|
||||||
],
|
],
|
||||||
capturedAt: 0,
|
capturedAt: 0,
|
||||||
isPano: false,
|
isPano: false,
|
||||||
});
|
});
|
||||||
return { sequences: [line('three', 3), line('four', 4), line('two', 2)], overview: [] };
|
return {
|
||||||
|
sequences: [line('three', 3), line('four', 4), line('two', 2)],
|
||||||
|
overview: [],
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
coverage.refresh();
|
coverage.refresh();
|
||||||
|
|||||||
@@ -178,6 +178,8 @@ export function createPhotoViewer({
|
|||||||
let documentOf = null;
|
let documentOf = null;
|
||||||
/** A context loss's look direction, for the reopen of that photo. */
|
/** A context loss's look direction, for the reopen of that photo. */
|
||||||
let restoreView = null;
|
let restoreView = null;
|
||||||
|
/** The panel's viewer box, where Esc is listened for (null: the root). */
|
||||||
|
let escapeTarget = null;
|
||||||
/** Width of the panorama texture on screen (0: the preview). */
|
/** Width of the panorama texture on screen (0: the preview). */
|
||||||
let panoWidth = 0;
|
let panoWidth = 0;
|
||||||
/** The sharper panorama on its way, for which open (resizes come in bursts). */
|
/** The sharper panorama on its way, for which open (resizes come in bursts). */
|
||||||
@@ -450,6 +452,7 @@ export function createPhotoViewer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onKeyDown(event) {
|
function onKeyDown(event) {
|
||||||
|
if (!escapeTarget) onEscapeKey(event);
|
||||||
if (
|
if (
|
||||||
event.defaultPrevented ||
|
event.defaultPrevented ||
|
||||||
event.altKey ||
|
event.altKey ||
|
||||||
@@ -528,7 +531,10 @@ export function createPhotoViewer({
|
|||||||
root.append(picture, canvas, prevButton, nextButton);
|
root.append(picture, canvas, prevButton, nextButton);
|
||||||
host.append(root);
|
host.append(root);
|
||||||
listen(root, 'keydown', onKeyDown);
|
listen(root, 'keydown', onKeyDown);
|
||||||
listen(host.closest?.('.sl-viewer-wrap') ?? root, 'keydown', onEscapeKey);
|
// The viewer's box in the panel; without one, the root's own handler
|
||||||
|
// takes Esc (one listener per element and type).
|
||||||
|
escapeTarget = host.closest?.('.sl-viewer-wrap') ?? null;
|
||||||
|
if (escapeTarget) listen(escapeTarget, 'keydown', onEscapeKey);
|
||||||
applyRenderMode();
|
applyRenderMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,9 +570,7 @@ export function createPhotoViewer({
|
|||||||
if (size[0] <= panoWidth) return;
|
if (size[0] <= panoWidth) return;
|
||||||
if (panoComing?.seq === seq && size[0] <= panoComing.width) return;
|
if (panoComing?.seq === seq && size[0] <= panoComing.width) return;
|
||||||
const sharp = sources.panoSharp(target, size, {
|
const sharp = sources.panoSharp(target, size, {
|
||||||
expanded: Boolean(
|
expanded: Boolean(root?.closest?.('.sl-viewer-wrap-expanded')),
|
||||||
root?.closest?.('.sl-viewer-wrap-expanded'),
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
if (!sharp?.url) return;
|
if (!sharp?.url) return;
|
||||||
const coming = { seq, width: size[0], controller: new AbortController() };
|
const coming = { seq, width: size[0], controller: new AbortController() };
|
||||||
|
|||||||
@@ -178,7 +178,9 @@ test('a sharp panorama still downloading when another photo opens is aborted', a
|
|||||||
url.endsWith('-sharp')
|
url.endsWith('-sharp')
|
||||||
? new Promise((_, reject) => {
|
? new Promise((_, reject) => {
|
||||||
signals.push(options.signal);
|
signals.push(options.signal);
|
||||||
options.signal?.addEventListener('abort', () => reject(new Error('aborted')));
|
options.signal?.addEventListener('abort', () =>
|
||||||
|
reject(new Error('aborted')),
|
||||||
|
);
|
||||||
})
|
})
|
||||||
: Promise.resolve({ src: url }),
|
: Promise.resolve({ src: url }),
|
||||||
createRenderer: () => ({
|
createRenderer: () => ({
|
||||||
@@ -278,9 +280,8 @@ test('docked, Esc anywhere in the viewer’s box (toolbar, caption) is offered t
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('a picture file or bitmap past any real photo’s size is refused', async (t) => {
|
test('a picture file or bitmap past any real photo’s size is refused', async (t) => {
|
||||||
const { loadImage, MAX_IMAGE_BYTES, MAX_IMAGE_SIDE } = await import(
|
const { loadImage, MAX_IMAGE_BYTES, MAX_IMAGE_SIDE } =
|
||||||
'./photoViewer.js'
|
await import('./photoViewer.js');
|
||||||
);
|
|
||||||
const saved = {
|
const saved = {
|
||||||
fetch: globalThis.fetch,
|
fetch: globalThis.fetch,
|
||||||
createImageBitmap: globalThis.createImageBitmap,
|
createImageBitmap: globalThis.createImageBitmap,
|
||||||
|
|||||||
@@ -78,8 +78,7 @@ export function decodeCoverageTile(bytes, address) {
|
|||||||
const props = feature.properties || {};
|
const props = feature.properties || {};
|
||||||
const parts = [];
|
const parts = [];
|
||||||
let length = 0;
|
let length = 0;
|
||||||
const tolerance =
|
const tolerance = (SIMPLIFY_TOLERANCE_UNITS * sequences.extent) / 4096;
|
||||||
(SIMPLIFY_TOLERANCE_UNITS * sequences.extent) / 4096;
|
|
||||||
for (const line of feature.loadGeometry()) {
|
for (const line of feature.loadGeometry()) {
|
||||||
if (line.length < 2) continue;
|
if (line.length < 2) continue;
|
||||||
for (let k = 1; k < line.length; k++)
|
for (let k = 1; k < line.length; k++)
|
||||||
|
|||||||
@@ -191,8 +191,15 @@ test('a z15 tile’s pictures of one sequence become cones; other sequences’ a
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('lines are simplified within a few tile units, ends and corners kept', () => {
|
test('lines are simplified within a few tile units, ends and corners kept', () => {
|
||||||
const zigzag = Array.from({ length: 1000 }, (_, i) => ({ x: i, y: i % 2 ? 1 : 0 }));
|
const zigzag = Array.from({ length: 1000 }, (_, i) => ({
|
||||||
assert.deepEqual(simplifyLine(zigzag, 4), [zigzag[0], zigzag.at(-1)], 'a 1-unit wobble goes');
|
x: i,
|
||||||
|
y: i % 2 ? 1 : 0,
|
||||||
|
}));
|
||||||
|
assert.deepEqual(
|
||||||
|
simplifyLine(zigzag, 4),
|
||||||
|
[zigzag[0], zigzag.at(-1)],
|
||||||
|
'a 1-unit wobble goes',
|
||||||
|
);
|
||||||
const corner = [
|
const corner = [
|
||||||
{ x: 0, y: 0 },
|
{ x: 0, y: 0 },
|
||||||
{ x: 50, y: 1 },
|
{ x: 50, y: 1 },
|
||||||
@@ -221,6 +228,9 @@ test('a decoded GPS track keeps a fraction of its points', () => {
|
|||||||
encodePanoramaxTile(tile, { sequences: [{ id: 'gps', parts: [track] }] }),
|
encodePanoramaxTile(tile, { sequences: [{ id: 'gps', parts: [track] }] }),
|
||||||
tile,
|
tile,
|
||||||
).sequences;
|
).sequences;
|
||||||
assert.ok(sequence.parts[0].length < 100, `${sequence.parts[0].length} points`);
|
assert.ok(
|
||||||
|
sequence.parts[0].length < 100,
|
||||||
|
`${sequence.parts[0].length} points`,
|
||||||
|
);
|
||||||
assert.ok(sequence.parts[0].length > 5);
|
assert.ok(sequence.parts[0].length > 5);
|
||||||
});
|
});
|
||||||
@@ -227,7 +227,12 @@ test('hostile item data: a malformed link escape, null coordinates, a huge produ
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('the instance holding a picture is named (its via host), never as its author', () => {
|
test('the instance holding a picture is named (its via host), never as its author', () => {
|
||||||
const item = stacItem({ id: 'a', lon: 1, lat: 2, producer: '[email protected]' });
|
const item = stacItem({
|
||||||
|
id: 'a',
|
||||||
|
lon: 1,
|
||||||
|
lat: 2,
|
||||||
|
producer: '[email protected]',
|
||||||
|
});
|
||||||
item.links.push({ rel: 'via', href: 'https://panoramax.ign.fr' });
|
item.links.push({ rel: 'via', href: 'https://panoramax.ign.fr' });
|
||||||
assert.equal(normalizeItem(item).creator, null);
|
assert.equal(normalizeItem(item).creator, null);
|
||||||
assert.equal(normalizeItem(item).via, 'panoramax.ign.fr');
|
assert.equal(normalizeItem(item).via, 'panoramax.ign.fr');
|
||||||
|
|||||||
@@ -543,7 +543,12 @@ test('a broken neighbour z15 tile only thins the cones; an unreadable home tile
|
|||||||
const box = tileBounds(home.tile.x, home.tile.y, 15);
|
const box = tileBounds(home.tile.x, home.tile.y, 15);
|
||||||
const at = { lon: box.west + 0.0003, lat: box.north - 0.0002 };
|
const at = { lon: box.west + 0.0003, lat: box.north - 0.0002 };
|
||||||
const items = [0, 1, 2].map((i) =>
|
const items = [0, 1, 2].map((i) =>
|
||||||
stacItem({ id: `c-${i}`, sequenceId: 'c', lon: at.lon + i * 0.00005, lat: at.lat }),
|
stacItem({
|
||||||
|
id: `c-${i}`,
|
||||||
|
sequenceId: 'c',
|
||||||
|
lon: at.lon + i * 0.00005,
|
||||||
|
lat: at.lat,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
const tiles = pictureTiles(items);
|
const tiles = pictureTiles(items);
|
||||||
const broken = new TextEncoder().encode('<html>oops</html>');
|
const broken = new TextEncoder().encode('<html>oops</html>');
|
||||||
@@ -835,7 +840,10 @@ test('a search hit outside the radius (a bounding box’s corner) is not the nea
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('a line clicked from low down still searches its own sequence within 50 m', async () => {
|
test('a line clicked from low down still searches its own sequence within 50 m', async () => {
|
||||||
const items = [...sequence('other', 1, { offset: 2 }), ...sequence('clicked', 1, { offset: 30 })];
|
const items = [
|
||||||
|
...sequence('other', 1, { offset: 2 }),
|
||||||
|
...sequence('clicked', 1, { offset: 30 }),
|
||||||
|
];
|
||||||
const { instance, source } = setup({ items });
|
const { instance, source } = setup({ items });
|
||||||
instance.init({
|
instance.init({
|
||||||
camera: { positionCartographic: { height: 300 } },
|
camera: { positionCartographic: { height: 300 } },
|
||||||
@@ -885,7 +893,9 @@ test('a filter that hides every density dot says so', async (t) => {
|
|||||||
grid: [{ lon: ORIGIN.lon, lat: ORIGIN.lat, pano: 0, flat: 5 }],
|
grid: [{ lon: ORIGIN.lon, lat: ORIGIN.lat, pano: 0, flat: 5 }],
|
||||||
});
|
});
|
||||||
env.instance.activate(env.viewer);
|
env.instance.activate(env.viewer);
|
||||||
assert.ok(await until(() => env.instance.coverageStats().hint === FILTERED_HINT));
|
assert.ok(
|
||||||
|
await until(() => env.instance.coverageStats().hint === FILTERED_HINT),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('dots are drawn at their own sizes, and the busiest is the biggest', async (t) => {
|
test('dots are drawn at their own sizes, and the busiest is the biggest', async (t) => {
|
||||||
|
|||||||
@@ -338,7 +338,9 @@ test('a Retry-After is honoured up to five minutes', async () => {
|
|||||||
headers: { 'retry-after': header },
|
headers: { 'retry-after': header },
|
||||||
})).fetchImpl,
|
})).fetchImpl,
|
||||||
});
|
});
|
||||||
return source.getTile({ x: 0, y: 0, z: 0 }).catch((error) => error.retryAfterSec);
|
return source
|
||||||
|
.getTile({ x: 0, y: 0, z: 0 })
|
||||||
|
.catch((error) => error.retryAfterSec);
|
||||||
};
|
};
|
||||||
assert.equal(await wait('3600'), 300);
|
assert.equal(await wait('3600'), 300);
|
||||||
assert.equal(await wait('99999999999'), 300);
|
assert.equal(await wait('99999999999'), 300);
|
||||||
|
|||||||
@@ -179,10 +179,19 @@ test('a 360° picture shows its SD equirectangular image, then HD shrunk to 4,09
|
|||||||
test('HD panoramas too big to decode, or no bigger than SD, are never fetched; nor while docked', () => {
|
test('HD panoramas too big to decode, or no bigger than SD, are never fetched; nor while docked', () => {
|
||||||
const size = [4096, 2048];
|
const size = [4096, 2048];
|
||||||
const expanded = { expanded: true };
|
const expanded = { expanded: true };
|
||||||
assert.equal(PANORAMAX_SOURCES.panoSharp(PHOTOS.pano, size, { expanded: false }), null);
|
assert.equal(
|
||||||
|
PANORAMAX_SOURCES.panoSharp(PHOTOS.pano, size, { expanded: false }),
|
||||||
|
null,
|
||||||
|
);
|
||||||
assert.equal(PANORAMAX_SOURCES.panoSharp(PHOTOS.pano, size), null);
|
assert.equal(PANORAMAX_SOURCES.panoSharp(PHOTOS.pano, size), null);
|
||||||
assert.equal(PANORAMAX_SOURCES.panoSharp(PHOTOS.bigPano, size, expanded), null);
|
assert.equal(
|
||||||
assert.equal(PANORAMAX_SOURCES.panoSharp(PHOTOS.smallPano, size, expanded), null);
|
PANORAMAX_SOURCES.panoSharp(PHOTOS.bigPano, size, expanded),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
PANORAMAX_SOURCES.panoSharp(PHOTOS.smallPano, size, expanded),
|
||||||
|
null,
|
||||||
|
);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
PANORAMAX_SOURCES.panoSharp({ ...PHOTOS.pano, width: 3000 }, size, expanded)
|
PANORAMAX_SOURCES.panoSharp({ ...PHOTOS.pano, width: 3000 }, size, expanded)
|
||||||
.resize,
|
.resize,
|
||||||
|
|||||||
@@ -816,12 +816,17 @@ test('Panoramax fixtures: two sequences on one street, searches honour sequence
|
|||||||
const boxed = ask(
|
const boxed = ask(
|
||||||
`bbox=${at.lon - d},${at.lat - d},${at.lon + d},${at.lat + d}&limit=100`,
|
`bbox=${at.lon - d},${at.lat - d},${at.lon + d},${at.lat + d}&limit=100`,
|
||||||
);
|
);
|
||||||
assert.ok(boxed.some((p) => p.lon < at.lon) && boxed.some((p) => p.lon > at.lon));
|
assert.ok(
|
||||||
|
boxed.some((p) => p.lon < at.lon) && boxed.some((p) => p.lon > at.lon),
|
||||||
|
);
|
||||||
const placed = ask(
|
const placed = ask(
|
||||||
`place_position=${at.lon},${at.lat}&place_distance=0-30&limit=50`,
|
`place_position=${at.lon},${at.lat}&place_distance=0-30&limit=50`,
|
||||||
);
|
);
|
||||||
assert.ok(placed.length > 0);
|
assert.ok(placed.length > 0);
|
||||||
assert.ok(placed.every((p) => p.lon <= at.lon + 1e-7), 'none it is behind');
|
assert.ok(
|
||||||
|
placed.every((p) => p.lon <= at.lon + 1e-7),
|
||||||
|
'none it is behind',
|
||||||
|
);
|
||||||
const one = ask(`ids=${at.id}&limit=1`);
|
const one = ask(`ids=${at.id}&limit=1`);
|
||||||
assert.equal(one[0].id, at.id);
|
assert.equal(one[0].id, at.id);
|
||||||
// Its pictures, and only those, are served; HD downloads are counted.
|
// Its pictures, and only those, are served; HD downloads are counted.
|
||||||
|
|||||||
@@ -555,6 +555,9 @@ test('the caption names an image’s own licence after its author, and leaves it
|
|||||||
caption({ creator: 'Fixture Mapper', license: 'CC BY-SA 4.0' }),
|
caption({ creator: 'Fixture Mapper', license: 'CC BY-SA 4.0' }),
|
||||||
'Image by Fixture Mapper · CC BY-SA 4.0',
|
'Image by Fixture Mapper · CC BY-SA 4.0',
|
||||||
);
|
);
|
||||||
assert.equal(caption({ license: 'Licence Ouverte 2.0' }), 'Licence Ouverte 2.0');
|
assert.equal(
|
||||||
|
caption({ license: 'Licence Ouverte 2.0' }),
|
||||||
|
'Licence Ouverte 2.0',
|
||||||
|
);
|
||||||
assert.equal(caption({ creator: 'Someone' }), 'Image by Someone');
|
assert.equal(caption({ creator: 'Someone' }), 'Image by Someone');
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user