mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
test(street-level): coverage race, camera framing and panel-control tests
From the test-engineering review of #768: the rendering and panel code had thin unit coverage (coverage.js 29% of lines, sequences/viewer/selection ~30%, cameraFollow 50%, streetLevelControls only source-text checks), and two shipped bugs were caught only by the manual browser gate. - coverage.test.mjs drives the real createCoverage against a fake scene and a source whose tile requests the test resolves. It reproduces gekh's P0 #2: a request superseded by retire() finishes after a newer one for the same tile. Without the ownership checks it drops the newer request's key and a refresh starts a duplicate whose lines nothing can remove (fails on the old loadTile, passes now). It also covers the draped-to-cast swap keeping both line sets on the globe until the cast one is built and clear() removing both mid-swap, and a tile with nothing castable not being rebuilt. - coverageFixture.mjs encodes Mapillary-shaped coverage tiles (sequence lines, overview points) from lon/lat; the tests and the hermetic gate use it. - cameraFollow.test.mjs pins the underground-camera fix: a -14,886 m sample is ignored for bare earth, a plausible sample wins, and following stands at eye height. Three of the four fail on the pre-fix module (the first reports "centre at -14884 m"). - streetLevelControls.test.mjs is a contract test through the real DataLayerManager and LayerStateCoordinator: a 360 deg click and a SINCE release reach the durable layer state and the encoded share link under Street Level's token. Against a stand-in layer it also covers the chip rule (darkening the last lit chip turns the layer off, provider kept on; lighting a dark chip is a user params request, then enable), the legend rebuilding on a same-count swap (review #9) and the viewer resizing only on open (review #8). With the old direct-to-layer routing, three of these fail, including both share-link tests. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
5a7ce5785e
commit
7ae5f52c89
6 files changed
+974
No files matched your search
@@ -0,0 +1,132 @@
|
||||
import { PbfWriter } from 'pbf';
|
||||
|
||||
/**
|
||||
* Encode a Mapillary-shaped coverage tile (mly1_public: `sequence` lines and
|
||||
* `overview` points) from lon/lat features, for tests and the hermetic QA
|
||||
* gate. The decoder in ./decode.js reads it like a real tile.
|
||||
*/
|
||||
|
||||
const zigzag = (value) => (value << 1) ^ (value >> 31);
|
||||
|
||||
/** Tile-local [x, y] (0..extent) of a lon/lat inside tile (x, y, z). */
|
||||
export function lonLatToTileLocal([lon, lat], { x, y, z }, extent = 4096) {
|
||||
const n = 2 ** z;
|
||||
const rad = (lat * Math.PI) / 180;
|
||||
const tx = ((lon + 180) / 360) * n;
|
||||
const ty =
|
||||
((1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) / 2) * n;
|
||||
return [Math.round((tx - x) * extent), Math.round((ty - y) * extent)];
|
||||
}
|
||||
|
||||
/** One MVT layer writer with deduplicated keys and appended values. */
|
||||
function layerWriter(name, extent) {
|
||||
const keys = new Map();
|
||||
const values = [];
|
||||
const features = [];
|
||||
function tags(properties) {
|
||||
const out = [];
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
if (!keys.has(key)) keys.set(key, keys.size);
|
||||
out.push(keys.get(key), values.length);
|
||||
values.push(value);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return {
|
||||
add(type, geometry, properties, id) {
|
||||
features.push({ type, geometry, tags: tags(properties), id });
|
||||
},
|
||||
write(pbf) {
|
||||
pbf.writeVarintField(15, 2);
|
||||
pbf.writeStringField(1, name);
|
||||
pbf.writeVarintField(5, extent);
|
||||
for (const feature of features)
|
||||
pbf.writeMessage(
|
||||
2,
|
||||
(f, out) => {
|
||||
if (Number.isInteger(f.id)) out.writeVarintField(1, f.id);
|
||||
out.writePackedVarint(2, f.tags);
|
||||
out.writeVarintField(3, f.type);
|
||||
out.writePackedVarint(4, f.geometry);
|
||||
},
|
||||
feature,
|
||||
);
|
||||
for (const key of keys.keys()) pbf.writeStringField(3, key);
|
||||
for (const value of values)
|
||||
pbf.writeMessage(
|
||||
4,
|
||||
(v, out) => {
|
||||
if (typeof v === 'boolean') out.writeBooleanField(7, v);
|
||||
else if (typeof v === 'number') out.writeVarintField(5, v);
|
||||
else out.writeStringField(1, String(v));
|
||||
},
|
||||
value,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** MoveTo/LineTo commands for line parts, with the cursor carried across parts. */
|
||||
function lineGeometry(parts) {
|
||||
const out = [];
|
||||
let cx = 0;
|
||||
let cy = 0;
|
||||
for (const part of parts) {
|
||||
if (part.length < 2) continue;
|
||||
const [x0, y0] = part[0];
|
||||
out.push(9, zigzag(x0 - cx), zigzag(y0 - cy));
|
||||
cx = x0;
|
||||
cy = y0;
|
||||
out.push(((part.length - 1) << 3) | 2);
|
||||
for (const [x, y] of part.slice(1)) {
|
||||
out.push(zigzag(x - cx), zigzag(y - cy));
|
||||
cx = x;
|
||||
cy = y;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{x: number, y: number, z: number}} tile
|
||||
* @param {{sequences?: Array<{id: string, capturedAt?: number, isPano?: boolean, parts: Array<Array<[number, number]>>}>, overview?: Array<{id: string, lon: number, lat: number, capturedAt?: number, isPano?: boolean}>, extent?: number}} [content]
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function encodeCoverageTile(
|
||||
tile,
|
||||
{ sequences = [], overview = [], extent = 4096 } = {},
|
||||
) {
|
||||
const pbf = new PbfWriter();
|
||||
if (sequences.length) {
|
||||
const layer = layerWriter('sequence', extent);
|
||||
for (const sequence of sequences)
|
||||
layer.add(
|
||||
2,
|
||||
lineGeometry(
|
||||
sequence.parts.map((part) =>
|
||||
part.map((point) => lonLatToTileLocal(point, tile, extent)),
|
||||
),
|
||||
),
|
||||
{
|
||||
id: sequence.id,
|
||||
captured_at: sequence.capturedAt ?? Date.now(),
|
||||
is_pano: sequence.isPano === true,
|
||||
},
|
||||
);
|
||||
pbf.writeMessage(3, (_, out) => layer.write(out), null);
|
||||
}
|
||||
if (overview.length) {
|
||||
const layer = layerWriter('overview', extent);
|
||||
for (const point of overview) {
|
||||
const [px, py] = lonLatToTileLocal([point.lon, point.lat], tile, extent);
|
||||
layer.add(1, [9, zigzag(px), zigzag(py)], {
|
||||
id: point.id,
|
||||
captured_at: point.capturedAt ?? Date.now(),
|
||||
is_pano: point.isPano === true,
|
||||
});
|
||||
}
|
||||
pbf.writeMessage(3, (_, out) => layer.write(out), null);
|
||||
}
|
||||
return new Uint8Array(pbf.finish());
|
||||
}
|
||||
Reference in new issue
Block a user