mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
fix(street-level): KartaView marker and camera direction; remaining review P4s
Marker and direction (reported on the PR): - KartaView has its own marker, a purple view cone (Street View's is blue), not GEV's default pin that Mapillary uses. - The camera direction (marker, cones, FOLLOW, a panorama's centre) is the direction of travel to the next photo a few metres on. KartaView's heading is the device compass: whole drives read 0 (no compass) and others sit a steady ~110° off. Until the drive loads the compass stands in (0 = unknown), and the pose is sent again once the course is known. Remaining P4s from the review of #1121: - Segment chaining is linear (a per-point cursor) and capped at 20,000 segments a tile and 5,000 points a line: a hostile tile of 60k segments sharing one point took 1.5 s, now tens of ms. - Decoded tiles are dropped when KartaView is switched off. - No network / a refused request says so and is not retried as a timeout; an unreadable answer says so. - A docked flat photo skips the 2,560 px download until the viewer grows. - The panel counts the drive's photos (countActivePhotos), not the thinned page of cones (2,250, not 217). - After a GPU reset the viewer redraws the photo itself, so a lookup the user started is not cancelled; a failure is still reported. - The gate's keyless page lights KartaView and checks it draws with no key; the fixture answers the sequence count; the line-click step checks the camera looks along the drive when the compass says 200°. - README/CHANGELOG show the share-link form the app writes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
c99eb93174
commit
e9a4dbb062
15 files changed
+588
-63
No files matched your search
@@ -16,17 +16,27 @@ import {
|
||||
TILE_TIMEOUT_MS,
|
||||
} from './policy.js';
|
||||
|
||||
/** An HTTP or API failure; `retryAfterSec` is set while KartaView rate-limits. */
|
||||
/**
|
||||
* An HTTP or API failure; `retryAfterSec` is set while KartaView rate-limits,
|
||||
* `timedOut` when it did not answer in time (the one failure worth asking
|
||||
* again at once).
|
||||
*/
|
||||
export class KartaViewSourceError extends Error {
|
||||
constructor(message, { status = 0, retryAfterSec = null } = {}) {
|
||||
constructor(
|
||||
message,
|
||||
{ status = 0, retryAfterSec = null, timedOut = false } = {},
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'KartaViewSourceError';
|
||||
this.status = status;
|
||||
this.retryAfterSec = retryAfterSec;
|
||||
this.timedOut = timedOut;
|
||||
}
|
||||
}
|
||||
|
||||
const NO_ANSWER = 'KartaView did not answer in time — try again';
|
||||
const UNREACHABLE = 'KartaView could not be reached — check the network';
|
||||
const UNREADABLE = 'KartaView sent an answer that could not be read';
|
||||
/** Statuses that mean a photo file is gone, not that storage hiccupped. */
|
||||
const GONE_STATUSES = Object.freeze([403, 404, 409, 410]);
|
||||
|
||||
@@ -129,7 +139,10 @@ export function createKartaViewSource({
|
||||
if (signal?.aborted) throw signal.reason;
|
||||
signal?.addEventListener('abort', onAbort, { once: true });
|
||||
const timer = setTimeout(
|
||||
() => controller.abort(new KartaViewSourceError(NO_ANSWER)),
|
||||
() =>
|
||||
controller.abort(
|
||||
new KartaViewSourceError(NO_ANSWER, { timedOut: true }),
|
||||
),
|
||||
timeoutMs,
|
||||
);
|
||||
try {
|
||||
@@ -152,7 +165,16 @@ export function createKartaViewSource({
|
||||
if (length > MAX_RESPONSE_BYTES) throw tooMuch();
|
||||
// KartaView answers chunked, without a length: count while reading.
|
||||
const reader = response.body?.getReader?.();
|
||||
if (!reader) return await response.json();
|
||||
if (!reader) {
|
||||
try {
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) throw error;
|
||||
throw new KartaViewSourceError(UNREADABLE, {
|
||||
status: response.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
const chunks = [];
|
||||
let bytes = 0;
|
||||
for (;;) {
|
||||
@@ -171,12 +193,19 @@ export function createKartaViewSource({
|
||||
body.set(chunk, offset);
|
||||
offset += chunk.byteLength;
|
||||
}
|
||||
return JSON.parse(new TextDecoder().decode(body));
|
||||
try {
|
||||
return JSON.parse(new TextDecoder().decode(body));
|
||||
} catch {
|
||||
throw new KartaViewSourceError(UNREADABLE, {
|
||||
status: response.status,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (signal?.aborted) throw signal.reason;
|
||||
if (controller.signal.aborted) throw controller.signal.reason;
|
||||
if (error instanceof KartaViewSourceError) throw error;
|
||||
throw new KartaViewSourceError(NO_ANSWER);
|
||||
// fetch rejects on no network, a blocked request or a refused CORS check.
|
||||
throw new KartaViewSourceError(UNREACHABLE);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
signal?.removeEventListener('abort', onAbort);
|
||||
@@ -221,7 +250,7 @@ export function createKartaViewSource({
|
||||
return payload?.result ?? {};
|
||||
} catch (error) {
|
||||
if (signal?.aborted) throw error;
|
||||
const timedOut = error?.message === NO_ANSWER && error.status === 0;
|
||||
const timedOut = error?.timedOut === true;
|
||||
if (timedOut && timeouts++ < 1) continue;
|
||||
if (error?.status !== 429 || attempt >= PHOTO_API_RETRIES) {
|
||||
if (error?.status === 429)
|
||||
@@ -259,7 +288,7 @@ export function createKartaViewSource({
|
||||
} catch (error) {
|
||||
// A cold dense tile can outlast the timeout; KartaView has it ready
|
||||
// by the second ask.
|
||||
if (signal?.aborted || error?.message !== NO_ANSWER) throw error;
|
||||
if (signal?.aborted || error?.timedOut !== true) throw error;
|
||||
return once();
|
||||
}
|
||||
},
|
||||
@@ -322,6 +351,17 @@ export function createKartaViewSource({
|
||||
return result.data;
|
||||
},
|
||||
|
||||
/** How many photos a sequence has, or null if KartaView does not say. */
|
||||
async getSequenceCount(sequenceId, { signal } = {}) {
|
||||
const result = await photoApi(
|
||||
`/2.0/sequence/${encodeURIComponent(sequenceId)}`,
|
||||
null,
|
||||
{ signal },
|
||||
);
|
||||
const count = Number(result.data?.countActivePhotos);
|
||||
return Number.isFinite(count) && count > 0 ? count : null;
|
||||
},
|
||||
|
||||
/** A page of a sequence's photos in capture order (1-based `page`). */
|
||||
async getSequencePhotos(
|
||||
sequenceId,
|
||||
|
||||
Reference in new issue
Block a user