mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 22:02:21 +00:00
Street Level: a posted view whose camera was taken stands down whole again
Applying the rest of a posted view after the camera was taken during its terrain wait still let it change the camera: stop_tracking dropped a follow the user had just started, and annotate_map framed the view's marks. A camera taken during the wait now ends the view again, as before. The 2 s terrain timeout and the per-view catch on the queue stay.
This commit is contained in:
1 parent
0ebc09079d
commit
71797427d6
4 files changed
+135
-23
No files matched your search
@@ -5324,8 +5324,7 @@ another map; `street_level` does not combine with `follow`.
|
||||
point, measure the view's altitude from the terrain under the camera
|
||||
(`groundedLookAt` with the terrain heights service) before taking the look-at
|
||||
point, and keep the view's own estimate without a terrain answer (the embedded
|
||||
app waits 2 s for one). A camera taken during that wait stays where it is:
|
||||
the rest of the view applies and its photos do not open. Links carry
|
||||
app waits 2 s for one). Links carry
|
||||
`sl=<source>,<lat>,<lon>`; the globe panel applies views through the action,
|
||||
and a plain link runs it once restored (one shot: the rewritten hash drops
|
||||
`sl`). A link that follows an entity ignores `sl`.
|
||||
|
||||
+16
-16
@@ -500,7 +500,6 @@ export function installViews({
|
||||
queue = queue
|
||||
.then(async () => {
|
||||
if (signal?.aborted) return;
|
||||
let apply = run;
|
||||
if (unplaced && view.street_level) {
|
||||
// A terrain service that does not answer soon leaves the view's
|
||||
// own estimate.
|
||||
@@ -514,20 +513,21 @@ export function installViews({
|
||||
.catch(() => null)
|
||||
.finally(() => owner.stop());
|
||||
if (signal?.aborted) return;
|
||||
// Whoever takes the camera meanwhile (a drag, a search, the
|
||||
// assistant) keeps it: the photos stand down, as when the camera
|
||||
// is taken during the action, and the rest of the view applies.
|
||||
if (owner.taken())
|
||||
apply = (name, ...rest) =>
|
||||
name === 'show_street_level'
|
||||
? {
|
||||
ok: false,
|
||||
cancelled: true,
|
||||
superseded: true,
|
||||
error: 'The camera moved on before the photo opened',
|
||||
}
|
||||
: run(name, ...rest);
|
||||
else if (point)
|
||||
// Whoever takes the camera while the terrain answers (a drag, a
|
||||
// search, the assistant) keeps it: the view stands down.
|
||||
if (owner.taken()) {
|
||||
post(
|
||||
{
|
||||
type: EMBED_APPLIED_MESSAGE,
|
||||
id,
|
||||
ok: false,
|
||||
steps: [{ step: 'street_level', ok: false, superseded: true }],
|
||||
},
|
||||
replyOrigin,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (point)
|
||||
view = createView({
|
||||
...event.data.view,
|
||||
street_level: { ...asked, ...point },
|
||||
@@ -536,7 +536,7 @@ export function installViews({
|
||||
const steps = await applyView(view, {
|
||||
viewer,
|
||||
dataManager,
|
||||
run: apply,
|
||||
run,
|
||||
signal,
|
||||
autoOpen,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import '../testSupport/registerDom.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import { installViews } from './embed.js';
|
||||
import { createGevActionRunner } from '../voice/gevActions.js';
|
||||
import { createAnnotationEngine } from '../annotations/annotationEngine.js';
|
||||
import { fakeCesiumViewer } from '../testSupport/streetLevelFakes.mjs';
|
||||
|
||||
// A camera taken while a posted view waits on terrain stays with whoever took
|
||||
// it: the stale view stands down whole, so it neither stops a newer follow nor
|
||||
// frames its own marks. Offline: real installViews, action runner and
|
||||
// annotation engine; the viewer, renderer, flights data and terrain are fixtures.
|
||||
globalThis.fetch = async () => { throw new Error('Unexpected network request in offline reproduction'); };
|
||||
globalThis.requestAnimationFrame = () => 1;
|
||||
globalThis.cancelAnimationFrame = () => {};
|
||||
const tick = () => new Promise(resolve => setTimeout(resolve, 0));
|
||||
function deferred() {
|
||||
let resolve;
|
||||
const promise = new Promise(done => { resolve = done; });
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
async function scenario(t, effect) {
|
||||
document.body.className = '';
|
||||
const entered = deferred();
|
||||
const terrain = deferred();
|
||||
const viewer = fakeCesiumViewer();
|
||||
viewer.clock = { onTick: new Cesium.Event() };
|
||||
viewer.canvas = viewer.scene.canvas;
|
||||
// A real perspective frustum looking down at 0,0; the old view's mark
|
||||
// at 48N,2E is outside it. No override of the engine's visibility logic.
|
||||
Object.assign(viewer.camera, {
|
||||
position: Cesium.Cartesian3.fromDegrees(0, 0, 1000),
|
||||
direction: new Cesium.Cartesian3(-1, 0, 0),
|
||||
up: new Cesium.Cartesian3(0, 0, 1),
|
||||
heading: 0,
|
||||
frustum: new Cesium.PerspectiveFrustum({ fov: Math.PI / 3, aspectRatio: 1, near: 1, far: 20000000 }),
|
||||
});
|
||||
const entity = { id: 'fixture-aircraft' };
|
||||
const aircraft = { icao24: 'fixture', callsign: 'Fixture', latitude: 0, longitude: 0 };
|
||||
let stopCalls = 0;
|
||||
const flights = {
|
||||
findByQuery: () => aircraft,
|
||||
trackById: () => { viewer.trackedEntity = entity; return true; },
|
||||
getTrackedInfo: () => viewer.trackedEntity ? aircraft : null,
|
||||
stopTracking: () => { stopCalls++; viewer.trackedEntity = undefined; return true; },
|
||||
};
|
||||
const dataManager = {
|
||||
layers: new Map([['flights', { module: flights }]]),
|
||||
getAll: () => [{ id: 'flights', enabled: true }],
|
||||
isEnabled: id => id === 'flights',
|
||||
setLayerParams: () => true,
|
||||
};
|
||||
const shell = {
|
||||
_navigationGeneration: 1,
|
||||
initialRestorePromise: Promise.resolve(),
|
||||
setCleanView() {},
|
||||
runImmediateNavigation(_noun, navigate) { this._navigationGeneration++; return navigate(); },
|
||||
};
|
||||
const annotations = createAnnotationEngine({ viewer,
|
||||
renderer: { add() {}, update() {}, remove() {}, sync() {}, destroy() {} },
|
||||
});
|
||||
const runner = createGevActionRunner({ viewer, dataManager, styleManager: shell, annotations });
|
||||
const calls = [];
|
||||
const run = async (name, args, options) => {
|
||||
calls.push(name);
|
||||
return runner(name, args, options);
|
||||
};
|
||||
const replies = [];
|
||||
const peer = { postMessage: message => replies.push(message) };
|
||||
const windowRef = new EventTarget();
|
||||
windowRef.parent = peer;
|
||||
windowRef.document = document;
|
||||
const remove = installViews({ shell, viewer, dataManager, run, windowRef,
|
||||
location: { search: '?embed=1', hash: '' },
|
||||
terrain: { getHeights: () => { entered.resolve(); return terrain.promise; } },
|
||||
});
|
||||
t.after(() => { remove(); runner.dispose(); annotations.destroy(); document.body.className = ''; });
|
||||
await tick();
|
||||
const event = new Event('message');
|
||||
Object.assign(event, { source: peer, origin: 'https://host.example', data: {
|
||||
type: 'gev:view', id: 1, view: {
|
||||
camera: { lat: 48, lon: 2, altitude_m: 1000, pitch_deg: -45 },
|
||||
layers: ['flights'],
|
||||
street_level: { provider: 'mapillary' },
|
||||
annotations: effect === 'annotations'
|
||||
? [{ type: 'pin', latitude: 48, longitude: 2, label: 'Old view' }] : [],
|
||||
},
|
||||
} });
|
||||
windowRef.dispatchEvent(event);
|
||||
await entered.promise;
|
||||
if (effect === 'follow') {
|
||||
const follow = await runner('track_entity', { query: 'Fixture', layerId: 'flights' });
|
||||
assert.equal(follow.ok, true, 'new follow starts through the real runner');
|
||||
assert.equal(viewer.trackedEntity, entity);
|
||||
} else {
|
||||
viewer.canvas.dispatchEvent(new Event('pointerdown'));
|
||||
}
|
||||
terrain.resolve([{ ellipsoid: 200 }]);
|
||||
for (let n = 0; n < 50 && !replies.some(reply => reply.id === 1); n++) await tick();
|
||||
const reply = replies.find(reply => reply.id === 1);
|
||||
t.diagnostic(JSON.stringify({ effect, calls, stopCalls, followed: viewer.trackedEntity === entity,
|
||||
annotationCount: annotations.count(), annotationFlights: viewer.flights.started, reply }));
|
||||
assert.ok(reply, 'host receives completion');
|
||||
assert.equal(reply.ok, false, 'superseded view is acknowledged as not applied');
|
||||
assert.ok(!calls.includes('show_street_level'), 'superseded request never reaches photo handler');
|
||||
if (effect === 'follow') {
|
||||
assert.equal(stopCalls, 0, 'stale view must not stop the newer follow');
|
||||
assert.equal(viewer.trackedEntity, entity, 'the new tracked entity stays selected');
|
||||
} else {
|
||||
assert.equal(viewer.flights.started, 0, 'stale annotations must not fly the camera');
|
||||
}
|
||||
}
|
||||
|
||||
for (const effect of ['follow', 'annotations']) {
|
||||
test(`a camera taken during the terrain wait keeps the user's ${effect}`, { timeout: 5000 }, t => scenario(t, effect));
|
||||
}
|
||||
@@ -144,11 +144,6 @@ for (const takeover of ['navigation', 'gesture', 'none']) {
|
||||
assert.equal(h.flights.length, takeover === 'none' ? 1 : 0, 'superseded view must not reclaim the camera');
|
||||
assert.equal(a.calls.open.length, takeover === 'none' ? 1 : 0);
|
||||
assert.equal(reply.ok, takeover === 'none');
|
||||
// Only the photos stand down: the rest of the view still applies, and
|
||||
// the camera stays with whoever took it.
|
||||
const steps = reply.steps.map(step => step.step);
|
||||
assert.ok(steps.includes('clear_annotations'), JSON.stringify(steps));
|
||||
assert.ok(!steps.includes('camera'), JSON.stringify(steps));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user