mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
fix(street-level): KartaView QA round — archived photos, connection budget, paging, ‹ ›
From two agentic QA passes (UX and failure modes): - A line click opens the nearest photo that can be shown: archived photos (409; a HEAD says 200) and records without files are skipped, a few at most, else 'KartaView's photos here are archived'. ‹ › skip them too. - api.kartaview.org is HTTP/1.1 (six connections): tiles take three, lookups two, sequence pages one, so a lookup never times out behind tiles; a timed-out lookup is asked once more; waits are capped at 10 s. - Sequence pages follow photo ids, not the index: a photo's page is found by search, a neighbour page joins near an edge, so ‹ › neither skip at page edges nor loop past gaps. Pages are 500 photos (~1.2 MB). - ‹ › open through the core without framing: loading, errors and cones behave as for any open; presses step on from the photo on its way; the ends disable up front. - Cones ignore the hidden IMAGERY / SINCE filters; no 'Selected' swatch for a source whose lines never highlight; 'no KartaView photos in view'; an unreadable tile is not cached as empty; a 0,0 fix is no photo; a failed lookup's error goes when another source is lit; the link goes through the image proxy; a stalled panorama shows its front view after 15 s instead of 60. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
310984ff0b
commit
454e261256
16 files changed
+683
-179
No files matched your search
@@ -49,7 +49,7 @@ export function loadImage(
|
||||
* @param {object} options
|
||||
* @param {(photoId: string) => Promise<object>} options.getPhoto A normalized photo (policy.js normalizePhoto).
|
||||
* @param {(photo: object, delta: -1|1) => Promise<string|null>} options.step The photo `delta` along the sequence, or null at its end.
|
||||
* @param {(imageId: string) => void} [options.onStepped] A photo opened from ‹ ›, after its pose.
|
||||
* @param {(imageId: string) => void} options.onStep Open the photo ‹ › chose (through the core, so loading and errors show).
|
||||
* @param {object} [options.render] Render governor.
|
||||
* @param {(url: string, options?: object) => Promise<HTMLImageElement>} [options.load]
|
||||
* @param {(canvas: HTMLCanvasElement) => object} [options.createRenderer]
|
||||
@@ -58,7 +58,7 @@ export function loadImage(
|
||||
export function createKartaViewViewer({
|
||||
getPhoto,
|
||||
step,
|
||||
onStepped = () => {},
|
||||
onStep,
|
||||
render,
|
||||
load = loadImage,
|
||||
createRenderer = createPanoramaRenderer,
|
||||
@@ -80,6 +80,12 @@ export function createKartaViewViewer({
|
||||
let drag = null;
|
||||
let poseFrame = 0;
|
||||
let removeListeners = [];
|
||||
/** The photo an open is fetching: ‹ › step on from it, not the old one. */
|
||||
let openingId = null;
|
||||
/** ‹ › chose this photo: keep the panorama's look direction for it. */
|
||||
let keepViewFor = null;
|
||||
/** ‹ › presses run one after another, each from where the last one went. */
|
||||
let steps = Promise.resolve();
|
||||
|
||||
function requestRender() {
|
||||
render?.governorRequestRender?.('kartaview-viewer');
|
||||
@@ -133,6 +139,21 @@ export function createKartaViewViewer({
|
||||
const enabled = Boolean(photo?.sequenceId) && showing !== null;
|
||||
if (prevButton) prevButton.disabled = !enabled;
|
||||
if (nextButton) nextButton.disabled = !enabled;
|
||||
if (enabled) checkEnds(photo);
|
||||
}
|
||||
|
||||
/** Disable ‹ or › at once where the sequence ends (it also warms the neighbours). */
|
||||
function checkEnds(target) {
|
||||
for (const [delta, button] of [
|
||||
[-1, prevButton],
|
||||
[1, nextButton],
|
||||
])
|
||||
step(target, delta).then(
|
||||
(id) => {
|
||||
if (photo === target && button && !id) button.disabled = true;
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
|
||||
function show(kind) {
|
||||
@@ -159,29 +180,33 @@ export function createKartaViewViewer({
|
||||
return renderer;
|
||||
}
|
||||
|
||||
/** Step along the sequence from the open photo. */
|
||||
async function stepBy(delta) {
|
||||
const from = photo;
|
||||
if (!from?.sequenceId || !showing) return;
|
||||
const seq = openSeq;
|
||||
let nextId = null;
|
||||
try {
|
||||
nextId = await step(from, delta);
|
||||
} catch {
|
||||
nextId = null;
|
||||
}
|
||||
if (seq !== openSeq || photo !== from) return;
|
||||
if (!nextId) {
|
||||
const button = delta < 0 ? prevButton : nextButton;
|
||||
if (button) button.disabled = true;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await adapter.open(nextId, { keepView: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (photo?.id === nextId) onStepped(nextId);
|
||||
/**
|
||||
* Step along the sequence from the photo showing, or from the one on its
|
||||
* way after an earlier press, and open it through the core.
|
||||
*/
|
||||
function stepBy(delta) {
|
||||
steps = steps.then(async () => {
|
||||
const fromId = openingId ?? photo?.id;
|
||||
if (!fromId || !root) return;
|
||||
let from;
|
||||
let nextId = null;
|
||||
try {
|
||||
from = await getPhoto(fromId);
|
||||
if (from.sequenceId) nextId = await step(from, delta);
|
||||
} catch {
|
||||
nextId = null;
|
||||
}
|
||||
// Closed, or another photo was opened meanwhile.
|
||||
if ((openingId ?? photo?.id) !== fromId) return;
|
||||
if (!nextId) {
|
||||
const button = delta < 0 ? prevButton : nextButton;
|
||||
if (button && photo?.id === fromId) button.disabled = true;
|
||||
return;
|
||||
}
|
||||
openingId = nextId;
|
||||
keepViewFor = nextId;
|
||||
onStep(nextId);
|
||||
});
|
||||
}
|
||||
|
||||
function onPointerDown(event) {
|
||||
@@ -324,15 +349,12 @@ export function createKartaViewViewer({
|
||||
}
|
||||
|
||||
async function showPano(target, seq, keepView) {
|
||||
const preview = resizedImageUrl(target.urls.pano, PANO_PREVIEW_SIZE);
|
||||
let image;
|
||||
try {
|
||||
image = await load(preview, { crossOrigin: true });
|
||||
} catch {
|
||||
image = await load(resizedImageUrl(target.urls.pano, PANO_FULL_SIZE), {
|
||||
crossOrigin: true,
|
||||
});
|
||||
}
|
||||
// Only the proxy can shrink a 13,000 px panorama: if it fails, the
|
||||
// caller shows the front view rather than waiting on it twice.
|
||||
const image = await load(
|
||||
resizedImageUrl(target.urls.pano, PANO_PREVIEW_SIZE),
|
||||
{ crossOrigin: true },
|
||||
);
|
||||
if (seq !== openSeq) return false;
|
||||
if (!keepView || showing !== 'pano')
|
||||
renderer.setView({ yaw: 0, pitch: 0, fov: PANO_FOV_DEFAULT_DEG });
|
||||
@@ -359,6 +381,24 @@ export function createKartaViewViewer({
|
||||
return true;
|
||||
}
|
||||
|
||||
async function openPhoto(id, seq, keepView) {
|
||||
const target = await getPhoto(id);
|
||||
if (seq !== openSeq) return;
|
||||
let shown = false;
|
||||
if (target.isPano && target.urls.pano && ensureRenderer()) {
|
||||
try {
|
||||
shown = await showPano(target, seq, keepView);
|
||||
} catch {
|
||||
// The panorama failed: its front view is better than nothing.
|
||||
if (seq !== openSeq) return;
|
||||
shown = await showFlat(target, seq);
|
||||
}
|
||||
} else shown = await showFlat(target, seq);
|
||||
if (!shown) return;
|
||||
emit();
|
||||
upgrade(target, seq, showing);
|
||||
}
|
||||
|
||||
const adapter = {
|
||||
async mount(host) {
|
||||
if (!host) throw new Error('Street Level viewer has no host element');
|
||||
@@ -368,28 +408,24 @@ export function createKartaViewViewer({
|
||||
},
|
||||
|
||||
/** Resolves once the photo is on screen and its pose was emitted. */
|
||||
async open(imageId, { keepView = false } = {}) {
|
||||
async open(imageId) {
|
||||
if (!root) throw new Error('KartaView viewer is not mounted');
|
||||
const id = String(imageId);
|
||||
const seq = ++openSeq;
|
||||
const target = await getPhoto(String(imageId));
|
||||
if (seq !== openSeq) return;
|
||||
let shown = false;
|
||||
if (target.isPano && target.urls.pano && ensureRenderer()) {
|
||||
try {
|
||||
shown = await showPano(target, seq, keepView);
|
||||
} catch {
|
||||
// The panorama failed: its front view is better than nothing.
|
||||
if (seq !== openSeq) return;
|
||||
shown = await showFlat(target, seq);
|
||||
}
|
||||
} else shown = await showFlat(target, seq);
|
||||
if (!shown) return;
|
||||
emit();
|
||||
upgrade(target, seq, showing);
|
||||
const keepView = keepViewFor === id;
|
||||
keepViewFor = null;
|
||||
openingId = id;
|
||||
try {
|
||||
await openPhoto(id, seq, keepView);
|
||||
} finally {
|
||||
if (openingId === id && seq === openSeq) openingId = null;
|
||||
}
|
||||
},
|
||||
|
||||
close() {
|
||||
openSeq++;
|
||||
openingId = null;
|
||||
keepViewFor = null;
|
||||
photo = null;
|
||||
show(null);
|
||||
if (picture) picture.removeAttribute('src');
|
||||
@@ -398,6 +434,8 @@ export function createKartaViewViewer({
|
||||
|
||||
unmount() {
|
||||
openSeq++;
|
||||
openingId = null;
|
||||
keepViewFor = null;
|
||||
teardown();
|
||||
},
|
||||
|
||||
|
||||
Reference in new issue
Block a user