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:
Said TurkseverandClaude Opus 5.5 committed 2026-10-14 12:17:33 +01:00
1 parent c99eb93174
commit e9a4dbb062
15 files changed
+588 -63

No files matched your search

@@ -1,3 +1,8 @@
/** More than any real tile holds (a dense z13 tile has ~4,700 segments). */
export const MAX_TILE_SEGMENTS = 20_000;
/** A chained line stops growing here and the next one starts. */
export const MAX_LINE_POINTS = 5_000;
/**
* Decode a KartaView coverage tile (`{lines: [{track: [{lat, lng}]}]}`) into
* line records for the shared coverage. The tile's lines are OSM way
@@ -14,6 +19,7 @@ export function decodeCoverageTile(payload, { x, y, z }) {
throw new Error('KartaView sent an unreadable coverage tile');
const segments = [];
for (const line of payload.lines) {
if (segments.length >= MAX_TILE_SEGMENTS) break;
const points = [];
for (const point of Array.isArray(line?.track) ? line.track : []) {
const lon = Number(point?.lng);
@@ -34,16 +40,28 @@ export function decodeCoverageTile(payload, { x, y, z }) {
else startingAt.set(at, [index]);
});
const used = new Uint8Array(segments.length);
// Per start point, how far its list has been used: each segment is looked
// at once, so a tile of segments sharing one point stays linear.
const cursor = new Map();
const nextFrom = (point) => {
const at = key(point);
const list = startingAt.get(at);
if (!list) return undefined;
let i = cursor.get(at) ?? 0;
while (i < list.length && used[list[i]]) i++;
cursor.set(at, i);
return list[i];
};
const sequences = [];
segments.forEach((segment, index) => {
if (used[index]) return;
used[index] = 1;
const chain = [...segment];
for (;;) {
const next = startingAt
.get(key(chain[chain.length - 1]))
?.find((candidate) => !used[candidate]);
const next = nextFrom(chain[chain.length - 1]);
if (next === undefined) break;
// Full: the segment starts the next line instead.
if (chain.length + segments[next].length - 1 > MAX_LINE_POINTS) break;
used[next] = 1;
chain.push(...segments[next].slice(1));
}