mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
fix(street-level): /revmain cleanup after the slim rebuild
- Google 3D: the OPEN button re-measures a ground whose tiles are still loading (a few times, a second apart) and on a map change, not only after the next camera move. - Mapillary: two mounts on one host during the first download share one build instead of leaking a second WebGL viewer. - Retired floating-panel keys (v8 panelPos, panelFloatHintShown) are dropped from localStorage; dead inline-position resets, the drag JSDoc and the dead qa-ui-disposal observer branch are gone. - Docs: z ladder, panel chrome, position storage and the fixture gate's actual coverage; changelog fragment for #974 (old .gev-cache/mapillary can be deleted). - ru/es: the layer is named as everywhere else in the UI. - Fixtures and test fakes: unused exports removed; a fake credit removal of an absent credit no longer drops another one; the tile test reset frees upstream slots. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
00e8e3b997
commit
28e0ba7104
22 files changed
+220
-74
No files matched your search
@@ -14,7 +14,7 @@ import {
|
||||
keyAlternatives,
|
||||
summarizeCoverage,
|
||||
} from './uiState.js';
|
||||
import { viewCentre } from './view.js';
|
||||
import { groundUnderCamera, viewCentre } from './view.js';
|
||||
import { createGroundClick, groundClickHeightOk } from './groundClick.js';
|
||||
import { msg, t, translateKnown } from '../../i18n/index.js';
|
||||
import {
|
||||
@@ -29,6 +29,9 @@ export { STREET_LEVEL_LAYER_ID } from './policy.js';
|
||||
|
||||
/** Longest wait for a provider's key status; past it, the provider decides. */
|
||||
const KEY_STATUS_TIMEOUT_MS = 10_000;
|
||||
/** Re-measures of a ground not loaded yet (Google 3D), a second apart. */
|
||||
const GROUND_RETRY_LIMIT = 5;
|
||||
const GROUND_RETRY_MS = 1000;
|
||||
|
||||
/**
|
||||
* The Street Level layer over imagery providers (contract in registry.js).
|
||||
@@ -163,22 +166,50 @@ export function createStreetLevelLayer({
|
||||
parts.viewerHost.unmount(entry.def.id);
|
||||
}
|
||||
}
|
||||
remeasureGround();
|
||||
notify();
|
||||
}
|
||||
|
||||
/** Whether a ground click can open an image: the camera is at street zoom. */
|
||||
let groundRetry = null;
|
||||
let groundRetries = 0;
|
||||
function cancelGroundRetry() {
|
||||
clearTimeout(groundRetry);
|
||||
groundRetry = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a ground click can open an image: the camera is at street zoom.
|
||||
* Under Google 3D the ground is unknown until its tiles load, so an unknown
|
||||
* ground is measured again a few times instead of waiting for the next move.
|
||||
*/
|
||||
function syncGroundClickReady() {
|
||||
cancelGroundRetry();
|
||||
const ready = state.enabled && groundClickHeightOk(state.viewer);
|
||||
if (
|
||||
state.enabled &&
|
||||
!ready &&
|
||||
groundRetries < GROUND_RETRY_LIMIT &&
|
||||
groundUnderCamera(state.viewer) === null
|
||||
) {
|
||||
groundRetries++;
|
||||
groundRetry = setTimeout(syncGroundClickReady, GROUND_RETRY_MS);
|
||||
}
|
||||
if (ready === state.groundClickReady) return;
|
||||
state.groundClickReady = ready;
|
||||
notify();
|
||||
}
|
||||
|
||||
/** A camera move or a map change measures afresh. */
|
||||
function remeasureGround() {
|
||||
groundRetries = 0;
|
||||
syncGroundClickReady();
|
||||
}
|
||||
|
||||
let removeCameraListener = null;
|
||||
function watchCamera(viewer) {
|
||||
removeCameraListener?.();
|
||||
removeCameraListener =
|
||||
viewer?.camera?.moveEnd?.addEventListener(syncGroundClickReady) || null;
|
||||
viewer?.camera?.moveEnd?.addEventListener(remeasureGround) || null;
|
||||
}
|
||||
|
||||
const activeEntries = () =>
|
||||
@@ -463,7 +494,7 @@ export function createStreetLevelLayer({
|
||||
parts.selection.install(viewer);
|
||||
for (const entry of activeEntries()) activate(entry);
|
||||
watchCamera(viewer);
|
||||
syncGroundClickReady();
|
||||
remeasureGround();
|
||||
notify();
|
||||
},
|
||||
|
||||
@@ -472,6 +503,7 @@ export function createStreetLevelLayer({
|
||||
abortNearest();
|
||||
removeCameraListener?.();
|
||||
removeCameraListener = null;
|
||||
cancelGroundRetry();
|
||||
parts.viewerHost.unmount();
|
||||
for (const entry of state.providers.values()) entry.instance.deactivate();
|
||||
syncGroundClickReady();
|
||||
|
||||
@@ -352,8 +352,11 @@ function fakeViewer() {
|
||||
},
|
||||
creditDisplay: {
|
||||
addStaticCredit: (credit) => credits.push(credit),
|
||||
removeStaticCredit: (credit) =>
|
||||
credits.splice(credits.indexOf(credit), 1),
|
||||
removeStaticCredit: (credit) => {
|
||||
// Cesium ignores a credit it does not hold.
|
||||
const at = credits.indexOf(credit);
|
||||
if (at !== -1) credits.splice(at, 1);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -786,6 +789,62 @@ test('Street View appears only on the Google 3D map: chip, credit, source and im
|
||||
assert.deepEqual(listed(), ['mapillary']);
|
||||
});
|
||||
|
||||
test('under Google 3D the ground is measured again once its tiles load, without a move', async (t) => {
|
||||
const saved = globalThis.document;
|
||||
globalThis.document = new EventTarget();
|
||||
t.after(() => {
|
||||
globalThis.document = saved;
|
||||
});
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||
const viewer = fakeViewer();
|
||||
// 4 km up the ellipsoid, 2 km over a city whose tiles are still loading.
|
||||
let ground;
|
||||
viewer.camera.positionCartographic = { height: 4000 };
|
||||
viewer.scene.globe = { show: false };
|
||||
viewer.scene.sampleHeightSupported = true;
|
||||
viewer.scene.sampleHeight = () => ground;
|
||||
const layer = createStreetLevelLayer({ providers: [fakeProvider()] });
|
||||
layer.init(viewer);
|
||||
layer.enable(viewer);
|
||||
t.after(() => layer.destroy());
|
||||
assert.equal(layer.getUIState().groundClickReady, false);
|
||||
|
||||
ground = 2000;
|
||||
t.mock.timers.tick(1000);
|
||||
assert.equal(layer.getUIState().groundClickReady, true);
|
||||
});
|
||||
|
||||
test('an unknown ground is measured only a few times, and not after the layer is off', (t) => {
|
||||
const saved = globalThis.document;
|
||||
globalThis.document = new EventTarget();
|
||||
t.after(() => {
|
||||
globalThis.document = saved;
|
||||
});
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||
const viewer = fakeViewer();
|
||||
let samples = 0;
|
||||
viewer.camera.positionCartographic = { height: 4000 };
|
||||
viewer.scene.globe = { show: false };
|
||||
viewer.scene.sampleHeightSupported = true;
|
||||
viewer.scene.sampleHeight = () => {
|
||||
samples++;
|
||||
return undefined;
|
||||
};
|
||||
const layer = createStreetLevelLayer({ providers: [fakeProvider()] });
|
||||
layer.init(viewer);
|
||||
layer.enable(viewer);
|
||||
t.after(() => layer.destroy());
|
||||
t.mock.timers.tick(60_000);
|
||||
const measured = samples;
|
||||
assert.ok(measured > 1 && measured <= 12, `measured ${measured} times`);
|
||||
|
||||
viewer.camera.moveEnd.raiseEvent();
|
||||
layer.disable();
|
||||
const atDisable = samples;
|
||||
t.mock.timers.tick(60_000);
|
||||
assert.equal(samples, atDisable, 'no measuring once the layer is off');
|
||||
});
|
||||
|
||||
test('the UI state says when a click (or the button) can open Street View here', async (t) => {
|
||||
const { layer } = await withStreetView(t);
|
||||
assert.equal(typeof layer.getUIState().groundClickReady, 'boolean');
|
||||
|
||||
@@ -21,6 +21,8 @@ export function createMapillaryViewer({ source, render } = {}) {
|
||||
let viewer = null;
|
||||
let Library = null;
|
||||
let container = null;
|
||||
/** The viewer being built for a host: a second mount joins it, not a twin. */
|
||||
let building = null;
|
||||
let pendingOpen = null;
|
||||
/** Bumped by `unmount`, so a construction it overtook builds nothing. */
|
||||
let generation = 0;
|
||||
@@ -100,8 +102,17 @@ export function createMapillaryViewer({ source, render } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureViewer(host) {
|
||||
if (viewer && container === host) return viewer;
|
||||
function ensureViewer(host) {
|
||||
if (viewer && container === host) return Promise.resolve(viewer);
|
||||
if (building?.host === host) return building.promise;
|
||||
const promise = buildViewer(host).finally(() => {
|
||||
if (building?.promise === promise) building = null;
|
||||
});
|
||||
building = { host, promise };
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function buildViewer(host) {
|
||||
const built = generation;
|
||||
destroyViewer();
|
||||
const { Viewer } = await ensureLibrary();
|
||||
@@ -177,6 +188,7 @@ export function createMapillaryViewer({ source, render } = {}) {
|
||||
pendingOpen = null;
|
||||
// A build still waiting for the library gives up when it arrives.
|
||||
generation++;
|
||||
building = null;
|
||||
destroyViewer();
|
||||
},
|
||||
|
||||
|
||||
@@ -180,6 +180,19 @@ test('Esc in a text field or rich-text editor keeps the selection (M61)', () =>
|
||||
}
|
||||
});
|
||||
|
||||
test('a click on the position marker neither selects nor opens at the ground', () => {
|
||||
const h = harness();
|
||||
try {
|
||||
h.scene.under = 'sl:pos';
|
||||
h.click();
|
||||
assert.deepEqual(h.groundClicks, []);
|
||||
assert.deepEqual(h.opened, []);
|
||||
assert.deepEqual(h.sequences.selects, []);
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('a click on nothing of ours goes to the ground handler with the pick, which decides', () => {
|
||||
const h = harness();
|
||||
try {
|
||||
|
||||
@@ -385,6 +385,19 @@ test('a Mapillary viewer unmounted mid-download is never built; the next mount b
|
||||
assert.equal(viewers.live, 0, 'nothing left holding a WebGL context');
|
||||
});
|
||||
|
||||
test('two mounts on one host during the first download build one Mapillary viewer', async () => {
|
||||
const { gate, viewers } = fakeLibrary();
|
||||
const adapter = createMapillaryViewer({ source: { token: 't' } });
|
||||
const element = {};
|
||||
const first = adapter.mount(element);
|
||||
const second = adapter.mount(element);
|
||||
gate.resolve();
|
||||
await Promise.all([first, second]);
|
||||
assert.equal(viewers.created, 1);
|
||||
adapter.unmount();
|
||||
assert.equal(viewers.live, 0, 'nothing left holding a WebGL context');
|
||||
});
|
||||
|
||||
test('closing the photo stops sequence playback in the hidden viewer', async () => {
|
||||
const { gate, viewers } = fakeLibrary();
|
||||
gate.resolve();
|
||||
|
||||
Reference in new issue
Block a user