mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Street Level: polar look-at, a bounded terrain wait, takeovers during the flight
- The look-at longitude wraps fully, so a tilted camera at a pole gives a point on the globe. The strict off-globe check on street_level is undone: points are clamped again, as before. - A posted view that fails is answered, and the views behind it still apply. - A posted view waits at most 2 s for the terrain under its camera, then keeps its own estimate. - A camera taken during that wait stays where it is: the rest of the view applies, and only its photos stand down. - show_street_level keeps a search or a press on the globe made during its own flight: it adopts only its own navigation stamps on landing, and a gesture is never forgiven.
This commit is contained in:
1 parent
ae66d07575
commit
0ebc09079d
8 files changed
+193
-79
No files matched your search
@@ -5323,7 +5323,9 @@ another map; `street_level` does not combine with `follow`.
|
|||||||
`show_in_gods_eye_view`, and the embedded app for a posted view with no
|
`show_in_gods_eye_view`, and the embedded app for a posted view with no
|
||||||
point, measure the view's altitude from the terrain under the camera
|
point, measure the view's altitude from the terrain under the camera
|
||||||
(`groundedLookAt` with the terrain heights service) before taking the look-at
|
(`groundedLookAt` with the terrain heights service) before taking the look-at
|
||||||
point, and keep the view's own estimate without a terrain answer. Links carry
|
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
|
||||||
`sl=<source>,<lat>,<lon>`; the globe panel applies views through the action,
|
`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
|
and a plain link runs it once restored (one shot: the rewritten hash drops
|
||||||
`sl`). A link that follows an entity ignores `sl`.
|
`sl`). A link that follows an entity ignores `sl`.
|
||||||
|
|||||||
+64
-42
@@ -39,6 +39,8 @@ const FOLLOW_LAYERS = {
|
|||||||
const FOLLOW_ATTEMPTS = 20;
|
const FOLLOW_ATTEMPTS = 20;
|
||||||
const FOLLOW_RETRY_MS = 1000;
|
const FOLLOW_RETRY_MS = 1000;
|
||||||
const FLIGHT_SECONDS = 2;
|
const FLIGHT_SECONDS = 2;
|
||||||
|
/** How long a posted view waits for the terrain under its camera. */
|
||||||
|
const TERRAIN_WAIT_MS = 2000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this page shows the app embedded: `?embed=1` when another page
|
* Whether this page shows the app embedded: `?embed=1` when another page
|
||||||
@@ -495,51 +497,71 @@ export function installViews({
|
|||||||
typeof asked === 'object' &&
|
typeof asked === 'object' &&
|
||||||
asked.lat == null &&
|
asked.lat == null &&
|
||||||
asked.lon == null;
|
asked.lon == null;
|
||||||
queue = queue.then(async () => {
|
queue = queue
|
||||||
if (signal?.aborted) return;
|
.then(async () => {
|
||||||
if (unplaced && view.street_level) {
|
|
||||||
// Whoever takes the camera while the terrain answers (a drag, a
|
|
||||||
// search, the assistant) keeps it: the view stands down.
|
|
||||||
const owner = watchCameraOwner(viewer, shell);
|
|
||||||
const point = await groundedLookAt(view.camera, terrain, signal)
|
|
||||||
.catch(() => null)
|
|
||||||
.finally(() => owner.stop());
|
|
||||||
if (signal?.aborted) return;
|
if (signal?.aborted) return;
|
||||||
if (owner.taken()) {
|
let apply = run;
|
||||||
post(
|
if (unplaced && view.street_level) {
|
||||||
{
|
// A terrain service that does not answer soon leaves the view's
|
||||||
type: EMBED_APPLIED_MESSAGE,
|
// own estimate.
|
||||||
id,
|
const wait = AbortSignal.timeout(TERRAIN_WAIT_MS);
|
||||||
ok: false,
|
const owner = watchCameraOwner(viewer, shell);
|
||||||
steps: [{ step: 'street_level', ok: false, superseded: true }],
|
const point = await groundedLookAt(
|
||||||
},
|
view.camera,
|
||||||
replyOrigin,
|
terrain,
|
||||||
);
|
signal ? AbortSignal.any([signal, wait]) : wait,
|
||||||
return;
|
)
|
||||||
|
.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)
|
||||||
|
view = createView({
|
||||||
|
...event.data.view,
|
||||||
|
street_level: { ...asked, ...point },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (point)
|
const steps = await applyView(view, {
|
||||||
view = createView({
|
viewer,
|
||||||
...event.data.view,
|
dataManager,
|
||||||
street_level: { ...asked, ...point },
|
run: apply,
|
||||||
});
|
signal,
|
||||||
}
|
autoOpen,
|
||||||
const steps = await applyView(view, {
|
});
|
||||||
viewer,
|
post(
|
||||||
dataManager,
|
{
|
||||||
run,
|
type: EMBED_APPLIED_MESSAGE,
|
||||||
signal,
|
id,
|
||||||
autoOpen,
|
ok: steps.every((step) => step.ok),
|
||||||
});
|
steps,
|
||||||
post(
|
},
|
||||||
{
|
replyOrigin,
|
||||||
type: EMBED_APPLIED_MESSAGE,
|
);
|
||||||
id,
|
})
|
||||||
ok: steps.every((step) => step.ok),
|
// A view that fails is answered, and the views behind it still apply.
|
||||||
steps,
|
.catch((error) =>
|
||||||
},
|
post(
|
||||||
replyOrigin,
|
{
|
||||||
|
type: EMBED_APPLIED_MESSAGE,
|
||||||
|
id,
|
||||||
|
ok: false,
|
||||||
|
error: error?.message || String(error),
|
||||||
|
},
|
||||||
|
replyOrigin,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
|
||||||
};
|
};
|
||||||
windowRef.addEventListener('message', onMessage);
|
windowRef.addEventListener('message', onMessage);
|
||||||
void ready.then(() => {
|
void ready.then(() => {
|
||||||
|
|||||||
@@ -188,8 +188,9 @@ const CAMERA_GESTURES = ['pointerdown', 'wheel'];
|
|||||||
/**
|
/**
|
||||||
* Whether the camera is still a request's: a navigation the shell stamps (a
|
* Whether the camera is still a request's: a navigation the shell stamps (a
|
||||||
* search, the assistant, a link) or a press or wheel on the globe hands it to
|
* search, the assistant, a link) or a press or wheel on the globe hands it to
|
||||||
* someone else. `claim()` makes the camera as it is now the request's (after
|
* someone else. `claim()` takes the navigation stamped so far as the
|
||||||
* its own flight); `stop()` removes the gesture listeners.
|
* request's (its own flight); a gesture is never forgiven. `stop()` removes
|
||||||
|
* the gesture listeners.
|
||||||
*/
|
*/
|
||||||
export function watchCameraOwner(viewer, navigation) {
|
export function watchCameraOwner(viewer, navigation) {
|
||||||
let generation = navigation?._navigationGeneration;
|
let generation = navigation?._navigationGeneration;
|
||||||
@@ -203,7 +204,6 @@ export function watchCameraOwner(viewer, navigation) {
|
|||||||
return {
|
return {
|
||||||
claim: () => {
|
claim: () => {
|
||||||
generation = navigation?._navigationGeneration;
|
generation = navigation?._navigationGeneration;
|
||||||
taken = false;
|
|
||||||
},
|
},
|
||||||
taken: () =>
|
taken: () =>
|
||||||
taken ||
|
taken ||
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import { createStreetLevelLayer } from './layers/streetLevel/index.js';
|
|||||||
import { fakeStreetLevelProvider, fakeCesiumViewer } from './testSupport/streetLevelFakes.mjs';
|
import { fakeStreetLevelProvider, fakeCesiumViewer } from './testSupport/streetLevelFakes.mjs';
|
||||||
import { showStreetLevel } from './voice/gevActions.js';
|
import { showStreetLevel } from './voice/gevActions.js';
|
||||||
import { installViews } from './app/embed.js';
|
import { installViews } from './app/embed.js';
|
||||||
|
import { groundedLookAt } from './view/index.js';
|
||||||
|
import { composeCatalog, coreTools } from './tools/index.js';
|
||||||
|
|
||||||
// Replacing a street-level photo, across sources and against the user's own
|
// Replacing a street-level photo, across sources and against the user's own
|
||||||
// moves; posted views waiting on terrain. Offline: providers, lookups, terrain
|
// moves; posted views waiting on terrain, and their look-at near a pole.
|
||||||
// and flights are fixtures.
|
// Offline: providers, lookups, terrain and flights are fixtures.
|
||||||
globalThis.fetch = async () => { throw new Error('Unexpected network use in offline test'); };
|
globalThis.fetch = async () => { throw new Error('Unexpected network use in offline test'); };
|
||||||
const settle = () => new Promise(resolve => setTimeout(resolve, 0));
|
const settle = () => new Promise(resolve => setTimeout(resolve, 0));
|
||||||
function deferred() {
|
function deferred() {
|
||||||
@@ -142,6 +144,11 @@ 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(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(a.calls.open.length, takeover === 'none' ? 1 : 0);
|
||||||
assert.equal(reply.ok, takeover === 'none');
|
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));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,3 +206,81 @@ test('a failed replacement from another source does not keep the old photo\'s de
|
|||||||
assert.deepEqual({ imageId: street.imageId, position: street.position, creator: street.creator, externalUrl: street.externalUrl },
|
assert.deepEqual({ imageId: street.imageId, position: street.position, creator: street.creator, externalUrl: street.externalUrl },
|
||||||
{ imageId: null, position: null, creator: null, externalUrl: null }, 'destroyed provider photo must not be attributed to replacement');
|
{ imageId: null, position: null, creator: null, externalUrl: null }, 'destroyed provider photo must not be attributed to replacement');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const rejections = [];
|
||||||
|
process.on('unhandledRejection', reason => { rejections.push(String(reason?.message || reason)); });
|
||||||
|
// 111 m from the North Pole, looking east and down: 1 km ahead is more than a
|
||||||
|
// full turn of that parallel.
|
||||||
|
const north = { camera: { lat: 89.999, lon: 170, altitude_m: 1000, pitch_deg: -45, heading_deg: 90 }, street_level: { provider: 'mapillary' } };
|
||||||
|
const placed = { camera: { lat: 48, lon: 2, altitude_m: 1000, pitch_deg: -45 }, street_level: { provider: 'mapillary', lat: 48, lon: 2 } };
|
||||||
|
|
||||||
|
test('a grounded look-at near a pole stays on the globe', async () => {
|
||||||
|
const point = await groundedLookAt(north.camera, { getHeights: async () => [{ ellipsoid: 0 }] });
|
||||||
|
assert.ok(Math.abs(point.lat) <= 90 && Math.abs(point.lon) <= 180, JSON.stringify(point));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('show_in_gods_eye_view frames the South Pole with a tilted camera', async () => {
|
||||||
|
const catalog = composeCatalog({
|
||||||
|
tools: coreTools,
|
||||||
|
services: { app: { baseUrl: 'http://localhost:4173/' }, terrain: { getHeights: async () => [{ ellipsoid: 2835 }] } },
|
||||||
|
});
|
||||||
|
const result = await catalog.call('show_in_gods_eye_view', {
|
||||||
|
camera: { lat: -90, lon: 0, altitude_m: 3500, pitch_deg: -30, heading_deg: 45 },
|
||||||
|
street_level: { provider: 'mapillary' },
|
||||||
|
});
|
||||||
|
const street = result.data?.view?.street_level;
|
||||||
|
assert.ok(street && Math.abs(street.lat) <= 90 && Math.abs(street.lon) <= 180, JSON.stringify(result));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a posted polar view is answered and the next view still applies', async t => {
|
||||||
|
rejections.length = 0;
|
||||||
|
const h = await harness(t, [provider('mapillary')]);
|
||||||
|
const e = await embed(t, h, { getHeights: async () => [{ ellipsoid: 0 }] });
|
||||||
|
e.post(1, north);
|
||||||
|
e.post(2, placed);
|
||||||
|
for (let n = 0; n < 20 && !e.replies.some(r => r.id === 2); n++) await settle();
|
||||||
|
t.diagnostic(JSON.stringify({ replies: e.replies.filter(r => r.id), rejections }));
|
||||||
|
assert.ok(e.replies.some(r => r.id === 1), 'the polar view gets an answer');
|
||||||
|
assert.ok(e.replies.some(r => r.id === 2 && r.ok === true), 'the next view still applies');
|
||||||
|
assert.deepEqual(rejections, [], 'no rejection escapes the views queue');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a stalled terrain answer does not hold posted views', { timeout: 15000 }, async t => {
|
||||||
|
const h = await harness(t, [provider('mapillary')]);
|
||||||
|
let asked = 0;
|
||||||
|
// Hangs like a stalled upstream; gives up only when its signal aborts.
|
||||||
|
const terrain = {
|
||||||
|
getHeights: (_points, { signal } = {}) => {
|
||||||
|
asked++;
|
||||||
|
return new Promise((_, reject) => signal?.addEventListener('abort', () => reject(signal.reason), { once: true }));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const e = await embed(t, h, terrain);
|
||||||
|
e.post(1, view(-45));
|
||||||
|
e.post(2, placed);
|
||||||
|
const started = Date.now();
|
||||||
|
while (Date.now() - started < 5000 && !e.replies.some(r => r.id === 2)) await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
t.diagnostic(JSON.stringify({ asked, waitedMs: Date.now() - started, replies: e.replies.filter(r => r.id).map(r => r.id) }));
|
||||||
|
assert.equal(asked, 1);
|
||||||
|
assert.ok(e.replies.some(r => r.id === 1), 'the view falls back to its own estimate within 5 s');
|
||||||
|
assert.ok(e.replies.some(r => r.id === 2), 'the next view is not held behind it');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a posted view that throws is answered and does not hold the views behind it', async t => {
|
||||||
|
rejections.length = 0;
|
||||||
|
const h = await harness(t, [provider('mapillary')]);
|
||||||
|
const getAll = h.dataManager.getAll;
|
||||||
|
let calls = 0;
|
||||||
|
h.dataManager.getAll = () => {
|
||||||
|
if (calls++ === 0) throw new Error('Fixture layer list failure');
|
||||||
|
return getAll();
|
||||||
|
};
|
||||||
|
const e = await embed(t, h, null);
|
||||||
|
e.post(1, placed);
|
||||||
|
e.post(2, placed);
|
||||||
|
for (let n = 0; n < 20 && !e.replies.some(r => r.id === 2); n++) await settle();
|
||||||
|
t.diagnostic(JSON.stringify({ replies: e.replies.filter(r => r.id), rejections }));
|
||||||
|
assert.deepEqual(e.replies.find(r => r.id === 1), { type: 'gev:view-applied', id: 1, ok: false, error: 'Fixture layer list failure' });
|
||||||
|
assert.ok(e.replies.some(r => r.id === 2 && r.ok === true), 'the next view still applies');
|
||||||
|
assert.deepEqual(rejections, []);
|
||||||
|
});
|
||||||
+2
-7
@@ -119,7 +119,8 @@ export function lookAtPoint(camera) {
|
|||||||
const lon = camera.lon + dLon;
|
const lon = camera.lon + dLon;
|
||||||
return {
|
return {
|
||||||
lat: clamp(camera.lat + dLat, -90, 90),
|
lat: clamp(camera.lat + dLat, -90, 90),
|
||||||
lon: lon > 180 ? lon - 360 : lon < -180 ? lon + 360 : lon,
|
// Near a pole one step ahead can be several turns of the parallel.
|
||||||
|
lon: ((((lon + 180) % 360) + 360) % 360) - 180,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,12 +388,6 @@ export function createView({
|
|||||||
// photos at X, also when the camera sits behind it. It is a pair: half
|
// photos at X, also when the camera sits behind it. It is a pair: half
|
||||||
// of one is not mixed with the camera's other half.
|
// of one is not mixed with the camera's other half.
|
||||||
const given = [latitude(streetLevel.lat), longitude(streetLevel.lon)];
|
const given = [latitude(streetLevel.lat), longitude(streetLevel.lon)];
|
||||||
// A coordinate given off the globe is an error, not a reason to guess.
|
|
||||||
for (const [index, key] of ['lat', 'lon'].entries())
|
|
||||||
if (streetLevel[key] != null && given[index] === undefined)
|
|
||||||
throw new TypeError(
|
|
||||||
`Street-level ${key} is not a valid coordinate: ${streetLevel[key]}`,
|
|
||||||
);
|
|
||||||
const seen = lookAtPoint(camera);
|
const seen = lookAtPoint(camera);
|
||||||
const [lat, lon] = given.includes(undefined) ? [seen.lat, seen.lon] : given;
|
const [lat, lon] = given.includes(undefined) ? [seen.lat, seen.lon] : given;
|
||||||
street = Object.freeze({
|
street = Object.freeze({
|
||||||
|
|||||||
@@ -208,21 +208,6 @@ test('street-level photos travel in the link, light their source and bring their
|
|||||||
assert.equal(streetLevelFromParams(new URLSearchParams('sl=bing,1,2')), null);
|
assert.equal(streetLevelFromParams(new URLSearchParams('sl=bing,1,2')), null);
|
||||||
assert.equal(streetLevelFromParams(new URLSearchParams('lat=1')), null);
|
assert.equal(streetLevelFromParams(new URLSearchParams('lat=1')), null);
|
||||||
assert.equal(createView({ camera: { lat: 1, lon: 2 } }).street_level, null);
|
assert.equal(createView({ camera: { lat: 1, lon: 2 } }).street_level, null);
|
||||||
// A point off the globe is refused, like an unknown source.
|
|
||||||
for (const bad of [
|
|
||||||
{ lat: 95, lon: 2 },
|
|
||||||
{ lat: 48, lon: 200 },
|
|
||||||
{ lat: Number.NaN, lon: 2 },
|
|
||||||
])
|
|
||||||
assert.throws(
|
|
||||||
() =>
|
|
||||||
createView({
|
|
||||||
camera: { lat: 1, lon: 2 },
|
|
||||||
street_level: { provider: 'mapillary', ...bad },
|
|
||||||
}),
|
|
||||||
TypeError,
|
|
||||||
JSON.stringify(bad),
|
|
||||||
);
|
|
||||||
// Half a point is not mixed with the camera's other half.
|
// Half a point is not mixed with the camera's other half.
|
||||||
const half = createView({
|
const half = createView({
|
||||||
camera: { lat: 1, lon: 2 },
|
camera: { lat: 1, lon: 2 },
|
||||||
|
|||||||
@@ -812,6 +812,7 @@ export function createGevActionRunner({
|
|||||||
runOptions,
|
runOptions,
|
||||||
current,
|
current,
|
||||||
reportProgress,
|
reportProgress,
|
||||||
|
otherHandoffs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3813,9 +3814,12 @@ async function requestStreetLevel(
|
|||||||
runOptions = {},
|
runOptions = {},
|
||||||
current = () => true,
|
current = () => true,
|
||||||
reportProgress = () => {},
|
reportProgress = () => {},
|
||||||
|
otherHandoffs = () => 0,
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const action = 'show_street_level';
|
const action = 'show_street_level';
|
||||||
|
// Navigations other than voice fly-tos (this request's own flight) so far.
|
||||||
|
const handoffs = otherHandoffs();
|
||||||
let provider = null;
|
let provider = null;
|
||||||
let providerName = null;
|
let providerName = null;
|
||||||
let label = null;
|
let label = null;
|
||||||
@@ -3972,8 +3976,9 @@ async function requestStreetLevel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A mode that hides the panel may have started during the flight. The
|
// A mode that hides the panel may have started during the flight. The
|
||||||
// camera is the request's from where its flight landed.
|
// camera is the request's from where its flight landed, unless another
|
||||||
if (location) owner.claim();
|
// navigation took it during the flight.
|
||||||
|
if (location && otherHandoffs() === handoffs) owner.claim();
|
||||||
const arrived = interrupted('view');
|
const arrived = interrupted('view');
|
||||||
if (arrived) return arrived;
|
if (arrived) return arrived;
|
||||||
|
|
||||||
|
|||||||
@@ -6573,11 +6573,10 @@ function createStreetLevelHarness({
|
|||||||
const lookups = [];
|
const lookups = [];
|
||||||
const origins = [];
|
const origins = [];
|
||||||
const revealed = [];
|
const revealed = [];
|
||||||
const { viewer, styleManager, currentGeneration } = createVoiceNavigationHarness();
|
const { viewer, styleManager, currentGeneration, panelSearch } = createVoiceNavigationHarness();
|
||||||
// Who owns the camera: the shell's navigation stamps and globe gestures.
|
// Who owns the camera: the shell's navigation stamps and globe gestures.
|
||||||
let userNavigations = 0;
|
|
||||||
Object.defineProperty(styleManager, '_navigationGeneration', {
|
Object.defineProperty(styleManager, '_navigationGeneration', {
|
||||||
get: () => currentGeneration() + userNavigations,
|
get: () => currentGeneration(),
|
||||||
});
|
});
|
||||||
viewer.scene.canvas = Object.assign(new EventTarget(), { clientWidth: 1200, clientHeight: 800 });
|
viewer.scene.canvas = Object.assign(new EventTarget(), { clientWidth: 1200, clientHeight: 800 });
|
||||||
let completeFlight = null;
|
let completeFlight = null;
|
||||||
@@ -6719,10 +6718,9 @@ function createStreetLevelHarness({
|
|||||||
origins,
|
origins,
|
||||||
revealed,
|
revealed,
|
||||||
street,
|
street,
|
||||||
// The user searches or flies elsewhere, or drags the globe.
|
// The user searches or flies elsewhere (a stamp, as the shell's), or
|
||||||
navigate: () => {
|
// drags the globe.
|
||||||
userNavigations += 1;
|
navigate: panelSearch,
|
||||||
},
|
|
||||||
gesture: () => viewer.scene.canvas.dispatchEvent(new Event('pointerdown')),
|
gesture: () => viewer.scene.canvas.dispatchEvent(new Event('pointerdown')),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -7428,6 +7426,28 @@ test('show_street_level owns the camera from where its own flight lands', async
|
|||||||
assert.ok(h.order.some((step) => step.startsWith('open:mapillary')));
|
assert.ok(h.order.some((step) => step.startsWith('open:mapillary')));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// A camera taken during the action's own flight. 'navigate' is a
|
||||||
|
// location-panel search: a stamp that leaves the flight flying. 'gesture' is a
|
||||||
|
// press on the globe, which does not cancel a Cesium flight either.
|
||||||
|
for (const take of ['navigate', 'gesture']) {
|
||||||
|
test(`show_street_level stands down when the camera is taken during its own flight (${take})`, async () => {
|
||||||
|
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
|
||||||
|
const h = createStreetLevelHarness();
|
||||||
|
const pending = h.runner('show_street_level', { latitude: 38.58, longitude: -121.49, provider: 'google' });
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
assert.deepEqual(h.order, ['fly'], 'the flight is under way');
|
||||||
|
h[take]();
|
||||||
|
h.finishFlight();
|
||||||
|
const result = await pending;
|
||||||
|
assert.deepEqual([result.ok, result.superseded], [false, true], result.error);
|
||||||
|
assert.deepEqual(
|
||||||
|
h.order.filter((step) => /^(map|params|open|arm|enable)/.test(step)),
|
||||||
|
[],
|
||||||
|
'no map switch, no source, no billed photo for the place the user left',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
test('show_street_level closes the previous photo, so one that finds nothing leaves no other place on screen', async () => {
|
test('show_street_level closes the previous photo, so one that finds nothing leaves no other place on screen', async () => {
|
||||||
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
|
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
|
||||||
const empty = createStreetLevelHarness({ enabled: true, photoOpen: true, nearest: null });
|
const empty = createStreetLevelHarness({ enabled: true, photoOpen: true, nearest: null });
|
||||||
|
|||||||
Reference in new issue
Block a user