mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 22:02:21 +00:00
Street Level: leave an interrupted link and an overtaken view alone (gekh review)
- A pasted or start-up link opens its photos only when its restore's camera landed: a drag or search during the restore flight keeps the camera. - A posted view whose photo step was cancelled or superseded no longer closes the viewer or flies; one that found a photo it could not load keeps the error. - openNearest gains `replace` (keep the open photo while it looks, close it only when nothing opens, so EXPAND stays) and `proceed()` (asked before a found photo loads); the action uses both, so a navigation, a gesture or a hiding mode during the lookup loads no billed panorama. - Mapillary's timeout message is a msg() string with all eleven translations.
This commit is contained in:
1 parent
5370b8a268
commit
9c73939425
22 files changed
+329
-17
No files matched your search
@@ -31,6 +31,10 @@
|
||||
recording and scene playback hide the photo. The action checks again after
|
||||
every wait: a navigation or a drag of the globe, or a mode that hides the
|
||||
panel, stops it before a photo loads for a place the user left or in a
|
||||
panel nobody sees, and a request that finds nothing closes the previous
|
||||
photo. The panel's later results open a billed photo only when they are
|
||||
panel nobody sees, also between finding a photo and loading it. A request
|
||||
that finds nothing closes the previous photo, and one that finds a photo
|
||||
replaces it in place, so EXPAND stays. A link whose restore flight the
|
||||
visitor interrupted, or a posted view the user overtook, leaves their
|
||||
camera and photo alone, and a photo found but not loaded keeps its error.
|
||||
Mapillary's timeout message is translated. The panel's later results open a billed photo only when they are
|
||||
fresh too (Said Turksever, @asturksever, #1113).
|
||||
+12
-3
@@ -5119,13 +5119,22 @@ brings `map: photoreal` and refuses another map; not with `follow`), links as
|
||||
applies it through the action, and a plain link runs the action once restored
|
||||
(one shot: the rewritten hash drops `sl`). A link pasted into the open tab
|
||||
runs it after its restore settles (`gev:share-navigation-restore-settled`),
|
||||
and a start link whose restore a newer one superseded never runs. In embed mode the rail holding the
|
||||
and only when the restore's camera landed (`share.camera === 'applied'`): a
|
||||
link whose restore flight the visitor stopped with a drag or a search, or
|
||||
whose restore a newer link superseded, never runs and never pulls the camera
|
||||
back. In embed mode the rail holding the
|
||||
panel is hidden, so an open photo moves to an overlay on the globe
|
||||
(`sl-viewer-wrap-embed`) and back when it closes; Esc closes it (in full
|
||||
screen, Esc leaves full screen first). EXPAND stays open under embed mode's
|
||||
own clean view; cockpit, recording and scene playback hide the overlay. A
|
||||
posted view whose photo step fails closes the earlier photo before its
|
||||
camera flies. Image-loading failures render inside
|
||||
posted view whose photo step fails before the imagery stage closes the
|
||||
earlier photo before its camera flies; one the user overtook (a click on the
|
||||
map, a newer request) leaves their photo and camera alone, and one that
|
||||
found a photo it could not load keeps that error in view. The action's
|
||||
lookup replaces an open photo in place (`openNearest` `replace: true`), so
|
||||
the viewer and EXPAND stay up, and closes it only when it opens nothing; once
|
||||
an image is found, `proceed()` asks whether the camera and the panel are
|
||||
still the request's before a billed photo loads. Image-loading failures render inside
|
||||
the embedded overlay, since the panel's error region is hidden with the rail.
|
||||
A caller's abort is
|
||||
honoured through the viewer's mount and load, not just the lookup, and only
|
||||
|
||||
@@ -2087,6 +2087,7 @@
|
||||
"scripts/qa-street-level.mjs",
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/i18n/locales/ar.json",
|
||||
"src/i18n/locales/de.json",
|
||||
"src/i18n/locales/es.json",
|
||||
|
||||
+15
-3
@@ -268,8 +268,14 @@ export async function applyView(
|
||||
streetLevelArgs(view.street_level, { autoOpen }),
|
||||
);
|
||||
if (result?.ok !== true) {
|
||||
// Overtaken (a click on the map, a newer request): the panel and the
|
||||
// camera are someone else's now.
|
||||
if (result?.cancelled || result?.superseded) return steps;
|
||||
// An earlier view's photo must not stay over a place this view left.
|
||||
dataManager.layers?.get?.(STREET_LEVEL_LAYER)?.module?.closeViewer?.();
|
||||
// From the imagery stage on the action has settled the panel itself:
|
||||
// a photo it found but could not load keeps its error in view.
|
||||
if (result?.stage !== 'imagery')
|
||||
dataManager.layers?.get?.(STREET_LEVEL_LAYER)?.module?.closeViewer?.();
|
||||
if (!result?.location)
|
||||
steps.push({ step: 'camera', ok: await flyTo(viewer, view.camera) });
|
||||
}
|
||||
@@ -332,11 +338,16 @@ export function installViews({
|
||||
// link must not fly over them.
|
||||
// A start-up restore that a newer link superseded hands the camera to that
|
||||
// link: its own steps never run.
|
||||
// A restore whose camera never landed (the recipient dragged, searched or
|
||||
// opened another place during its flight) leaves the camera with them.
|
||||
const cameraLanded = (outcome) => outcome?.share?.camera === 'applied';
|
||||
let generationAtRestore = null;
|
||||
let startSuperseded = false;
|
||||
let queue = ready.then((outcome) => {
|
||||
generationAtRestore = shell._navigationGeneration;
|
||||
startSuperseded = outcome?.status === 'superseded';
|
||||
startSuperseded =
|
||||
outcome?.status === 'superseded' ||
|
||||
(Boolean(outcome?.share) && !cameraLanded(outcome));
|
||||
});
|
||||
const navigatedSince = (generation) =>
|
||||
Number.isFinite(generation) && shell._navigationGeneration !== generation;
|
||||
@@ -397,7 +408,8 @@ export function installViews({
|
||||
// its photos follow once that restore settles, under the same rule: a plain
|
||||
// link never loads a billed photo by itself.
|
||||
const onNavigatedRestore = (event) => {
|
||||
if (event?.detail?.status !== 'settled') return;
|
||||
if (event?.detail?.status !== 'settled' || !cameraLanded(event.detail))
|
||||
return;
|
||||
const streetLevel = linkedStreetLevel(
|
||||
new URLSearchParams(String(location?.hash || '').replace(/^#/, '')),
|
||||
);
|
||||
|
||||
+110
-2
@@ -972,6 +972,36 @@ test('a start link whose restore a newer link superseded opens nothing', async (
|
||||
assert.deepEqual(calls, [], 'start link A does not pull the camera from B');
|
||||
});
|
||||
|
||||
test('a start link whose restore flight the recipient interrupted leaves the camera with them', async () => {
|
||||
for (const [camera, expected] of [
|
||||
['cancelled', 0],
|
||||
['skipped', 0],
|
||||
['applied', 1],
|
||||
]) {
|
||||
const { calls, run } = recorder();
|
||||
installViews({
|
||||
shell: {
|
||||
initialRestorePromise: Promise.resolve({
|
||||
status: 'settled',
|
||||
share: { camera },
|
||||
}),
|
||||
_navigationGeneration: 4,
|
||||
},
|
||||
viewer: fakeViewer(),
|
||||
dataManager: fakeLayers(new Set()),
|
||||
run,
|
||||
location: {
|
||||
search: '',
|
||||
hash: '#v=2&lat=1&lon=2&sl=mapillary,38.58000,-121.49000',
|
||||
},
|
||||
windowRef: new EventTarget(),
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(calls.length, expected, camera);
|
||||
}
|
||||
});
|
||||
|
||||
test('a link pasted into the open tab opens or arms its photos once its restore settles', async () => {
|
||||
const { calls, run, options } = recorder();
|
||||
const windowRef = new EventTarget();
|
||||
@@ -990,10 +1020,10 @@ test('a link pasted into the open tab opens or arms its photos once its restore
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.deepEqual(calls, [], 'the start link has no photos');
|
||||
const settled = (status) =>
|
||||
const settled = (status, camera = 'applied') =>
|
||||
windowRef.dispatchEvent(
|
||||
Object.assign(new Event('gev:share-navigation-restore-settled'), {
|
||||
detail: { status },
|
||||
detail: { status, share: { camera } },
|
||||
}),
|
||||
);
|
||||
location.hash = '#v=2&lat=3&lon=4&sl=mapillary,48.88740,2.34950';
|
||||
@@ -1001,6 +1031,19 @@ test('a link pasted into the open tab opens or arms its photos once its restore
|
||||
settled('failed');
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.deepEqual(calls, [], 'a failed restore opens nothing');
|
||||
// The recipient dragged or searched during the restore flight: the camera
|
||||
// is theirs, and the link does not pull it back.
|
||||
shell._navigationGeneration = 3;
|
||||
for (const camera of ['cancelled', 'superseded']) {
|
||||
settled('settled', camera);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.deepEqual(
|
||||
calls,
|
||||
[],
|
||||
`an interrupted restore (${camera}) opens nothing`,
|
||||
);
|
||||
}
|
||||
settled('settled');
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
@@ -1027,6 +1070,71 @@ test('a link pasted into the open tab opens or arms its photos once its restore
|
||||
assert.equal(calls.length, 1, 'removed with the install');
|
||||
});
|
||||
|
||||
test('a view whose photo step the user overtook leaves their photo and camera alone', async () => {
|
||||
// Photo B, opened by a click while the posted view looked up its photo.
|
||||
const street = { open: false, imageId: null };
|
||||
const dataManager = {
|
||||
...fakeLayers(new Set()),
|
||||
layers: new Map([
|
||||
[
|
||||
'street-level',
|
||||
{
|
||||
module: {
|
||||
closeViewer: () =>
|
||||
Object.assign(street, { open: false, imageId: null }),
|
||||
},
|
||||
},
|
||||
],
|
||||
]),
|
||||
};
|
||||
const { run } = recorder({
|
||||
show_street_level: () => {
|
||||
Object.assign(street, { open: true, imageId: 'b' });
|
||||
return {
|
||||
ok: false,
|
||||
stage: 'imagery',
|
||||
cancelled: true,
|
||||
superseded: true,
|
||||
error: 'The camera moved on before the photo opened',
|
||||
};
|
||||
},
|
||||
});
|
||||
const view = createView({
|
||||
camera: { lat: 1, lon: 2, altitude_m: 500 },
|
||||
street_level: { provider: 'mapillary' },
|
||||
});
|
||||
const viewer = fakeViewer();
|
||||
const steps = await applyView(view, { viewer, dataManager, run });
|
||||
assert.deepEqual(street, { open: true, imageId: 'b' }, 'photo B stays');
|
||||
assert.equal(viewer.flights.length, 0, 'the camera stays with the user');
|
||||
assert.equal(steps.at(-1).ok, false, 'reported as not applied');
|
||||
});
|
||||
|
||||
test('a view whose photo was found but could not load keeps its error in view', async () => {
|
||||
const closed = [];
|
||||
const dataManager = {
|
||||
...fakeLayers(new Set()),
|
||||
layers: new Map([
|
||||
['street-level', { module: { closeViewer: () => closed.push(true) } }],
|
||||
]),
|
||||
};
|
||||
const { run } = recorder({
|
||||
show_street_level: () => ({
|
||||
ok: false,
|
||||
stage: 'imagery',
|
||||
imageryError: true,
|
||||
location: '1.00000, 2.00000',
|
||||
error: 'Image could not be opened',
|
||||
}),
|
||||
});
|
||||
const view = createView({
|
||||
camera: { lat: 1, lon: 2, altitude_m: 500 },
|
||||
street_level: { provider: 'mapillary' },
|
||||
});
|
||||
await applyView(view, { viewer: fakeViewer(), dataManager, run });
|
||||
assert.equal(closed.length, 0, 'the viewer keeps the new photo’s error');
|
||||
});
|
||||
|
||||
test('a view whose photos fail closes the photo an earlier view opened', async () => {
|
||||
const closed = [];
|
||||
const dataManager = {
|
||||
|
||||
@@ -1787,6 +1787,7 @@
|
||||
"KartaView did not filter this area in time — move the map to ask again": "لم يُصفِّ KartaView هذه المنطقة في الوقت المحدد — حرّك الخريطة لإعادة الطلب",
|
||||
"None of KartaView’s photos here pass the imagery filter": "لا تجتاز أي من صور KartaView هنا مرشح الصور",
|
||||
"KartaView did not answer in time — try again": "لم يستجب KartaView في الوقت المحدد — أعد المحاولة",
|
||||
"Mapillary did not answer in time — try again": "لم يستجب Mapillary في الوقت المحدد — أعد المحاولة",
|
||||
"KartaView could not be reached — check the network": "تعذّر الوصول إلى KartaView — تحقّق من الشبكة",
|
||||
"KartaView sent an answer that could not be read": "أرسل KartaView استجابة تعذّرت قراءتها",
|
||||
"KartaView is rate-limiting requests": "يقيّد KartaView معدل الطلبات",
|
||||
|
||||
@@ -1701,6 +1701,7 @@
|
||||
"KartaView did not filter this area in time — move the map to ask again": "KartaView hat dieses Gebiet nicht rechtzeitig gefiltert – Karte verschieben, um erneut anzufragen",
|
||||
"None of KartaView’s photos here pass the imagery filter": "Keines der KartaView-Fotos hier passt zum Bildfilter",
|
||||
"KartaView did not answer in time — try again": "KartaView hat nicht rechtzeitig geantwortet – erneut versuchen",
|
||||
"Mapillary did not answer in time — try again": "Mapillary hat nicht rechtzeitig geantwortet – erneut versuchen",
|
||||
"KartaView could not be reached — check the network": "KartaView ist nicht erreichbar – Netzwerk prüfen",
|
||||
"KartaView sent an answer that could not be read": "KartaView hat eine unlesbare Antwort gesendet",
|
||||
"KartaView is rate-limiting requests": "KartaView begrenzt die Anfragen",
|
||||
|
||||
@@ -2320,6 +2320,7 @@
|
||||
"KartaView did not filter this area in time — move the map to ask again": "KartaView no filtró esta zona a tiempo — mueve el mapa para pedirlo de nuevo",
|
||||
"None of KartaView’s photos here pass the imagery filter": "Ninguna foto de KartaView de aquí pasa el filtro de imágenes",
|
||||
"KartaView did not answer in time — try again": "KartaView no respondió a tiempo — inténtalo de nuevo",
|
||||
"Mapillary did not answer in time — try again": "Mapillary no respondió a tiempo — inténtalo de nuevo",
|
||||
"KartaView could not be reached — check the network": "No se pudo conectar con KartaView — comprueba la red",
|
||||
"KartaView sent an answer that could not be read": "KartaView envió una respuesta que no se pudo leer",
|
||||
"KartaView is rate-limiting requests": "KartaView está limitando las peticiones",
|
||||
|
||||
@@ -1093,6 +1093,7 @@
|
||||
"KartaView did not filter this area in time — move the map to ask again": "KartaView n'a pas filtré cette zone à temps — déplacez la carte pour relancer la demande",
|
||||
"None of KartaView’s photos here pass the imagery filter": "Aucune photo KartaView de cet endroit ne passe le filtre d'images",
|
||||
"KartaView did not answer in time — try again": "KartaView n'a pas répondu à temps — réessayez",
|
||||
"Mapillary did not answer in time — try again": "Mapillary n'a pas répondu à temps — réessayez",
|
||||
"KartaView could not be reached — check the network": "KartaView est injoignable — vérifiez le réseau",
|
||||
"KartaView sent an answer that could not be read": "KartaView a envoyé une réponse illisible",
|
||||
"KartaView is rate-limiting requests": "KartaView limite les requêtes",
|
||||
|
||||
@@ -1093,6 +1093,7 @@
|
||||
"KartaView did not filter this area in time — move the map to ask again": "KartaView non ha filtrato quest'area in tempo: sposta la mappa per riprovare",
|
||||
"None of KartaView’s photos here pass the imagery filter": "Nessuna delle foto KartaView qui supera il filtro delle immagini",
|
||||
"KartaView did not answer in time — try again": "KartaView non ha risposto in tempo: riprova",
|
||||
"Mapillary did not answer in time — try again": "Mapillary non ha risposto in tempo: riprova",
|
||||
"KartaView could not be reached — check the network": "KartaView non è raggiungibile: controlla la rete",
|
||||
"KartaView sent an answer that could not be read": "KartaView ha inviato una risposta illeggibile",
|
||||
"KartaView is rate-limiting requests": "KartaView sta limitando le richieste",
|
||||
|
||||
@@ -1061,6 +1061,7 @@
|
||||
"KartaView did not filter this area in time — move the map to ask again": "KartaViewがこの範囲を時間内に絞り込めませんでした。地図を動かして再度リクエストしてください",
|
||||
"None of KartaView’s photos here pass the imagery filter": "この場所のKartaViewの写真はいずれも画像フィルターを通過しません",
|
||||
"KartaView did not answer in time — try again": "KartaViewから時間内に応答がありませんでした。もう一度お試しください",
|
||||
"Mapillary did not answer in time — try again": "Mapillaryから時間内に応答がありませんでした。もう一度お試しください",
|
||||
"KartaView could not be reached — check the network": "KartaViewに接続できません。ネットワークを確認してください",
|
||||
"KartaView sent an answer that could not be read": "KartaViewから読み取れない応答が返されました",
|
||||
"KartaView is rate-limiting requests": "KartaViewがリクエストをレート制限中です",
|
||||
|
||||
@@ -1091,6 +1091,7 @@
|
||||
"KartaView did not filter this area in time — move the map to ask again": "O KartaView não filtrou esta área a tempo — mova o mapa para pedir de novo",
|
||||
"None of KartaView’s photos here pass the imagery filter": "Nenhuma foto do KartaView aqui passa no filtro de imagens",
|
||||
"KartaView did not answer in time — try again": "O KartaView não respondeu a tempo — tente novamente",
|
||||
"Mapillary did not answer in time — try again": "O Mapillary não respondeu a tempo — tente novamente",
|
||||
"KartaView could not be reached — check the network": "Não foi possível acessar o KartaView — verifique a rede",
|
||||
"KartaView sent an answer that could not be read": "O KartaView enviou uma resposta que não pôde ser lida",
|
||||
"KartaView is rate-limiting requests": "O KartaView está limitando as requisições",
|
||||
|
||||
@@ -2358,6 +2358,7 @@
|
||||
"KartaView did not filter this area in time — move the map to ask again": "KartaView не успел отфильтровать эту область — сдвиньте карту, чтобы запросить снова",
|
||||
"None of KartaView’s photos here pass the imagery filter": "Ни одна фотография KartaView здесь не проходит фильтр снимков",
|
||||
"KartaView did not answer in time — try again": "KartaView не ответил вовремя — попробуйте снова",
|
||||
"Mapillary did not answer in time — try again": "Mapillary не ответил вовремя — попробуйте снова",
|
||||
"KartaView could not be reached — check the network": "Не удалось связаться с KartaView — проверьте сеть",
|
||||
"KartaView sent an answer that could not be read": "KartaView прислал ответ, который не удалось прочитать",
|
||||
"KartaView is rate-limiting requests": "KartaView ограничивает частоту запросов",
|
||||
|
||||
@@ -1108,6 +1108,7 @@
|
||||
"KartaView did not filter this area in time — move the map to ask again": "KartaView не встиг відфільтрувати цю область — зсуньте карту, щоб запитати знову",
|
||||
"None of KartaView’s photos here pass the imagery filter": "Жодне фото KartaView тут не проходить фільтр знімків",
|
||||
"KartaView did not answer in time — try again": "KartaView не відповів вчасно — спробуйте знову",
|
||||
"Mapillary did not answer in time — try again": "Mapillary не відповів вчасно — спробуйте знову",
|
||||
"KartaView could not be reached — check the network": "Не вдалося звʼязатися з KartaView — перевірте мережу",
|
||||
"KartaView sent an answer that could not be read": "KartaView надіслав відповідь, яку не вдалося прочитати",
|
||||
"KartaView is rate-limiting requests": "KartaView обмежує частоту запитів",
|
||||
|
||||
@@ -1038,6 +1038,7 @@
|
||||
"KartaView did not filter this area in time — move the map to ask again": "KartaView 未能及时筛选此区域——移动地图以重新请求",
|
||||
"None of KartaView’s photos here pass the imagery filter": "此处没有 KartaView 照片通过影像筛选",
|
||||
"KartaView did not answer in time — try again": "KartaView 未及时响应——请重试",
|
||||
"Mapillary did not answer in time — try again": "Mapillary 未及时响应——请重试",
|
||||
"KartaView could not be reached — check the network": "无法连接 KartaView——请检查网络",
|
||||
"KartaView sent an answer that could not be read": "KartaView 返回的响应无法读取",
|
||||
"KartaView is rate-limiting requests": "KartaView 已被限流",
|
||||
|
||||
@@ -1033,6 +1033,7 @@
|
||||
"KartaView did not filter this area in time — move the map to ask again": "KartaView 未能及時篩選此區域——移動地圖以重新請求",
|
||||
"None of KartaView’s photos here pass the imagery filter": "此處沒有 KartaView 照片通過影像篩選",
|
||||
"KartaView did not answer in time — try again": "KartaView 未及時響應——請重試",
|
||||
"Mapillary did not answer in time — try again": "Mapillary 未及時響應——請重試",
|
||||
"KartaView could not be reached — check the network": "無法連接 KartaView——請檢查網絡",
|
||||
"KartaView sent an answer that could not be read": "KartaView 返回的響應無法讀取",
|
||||
"KartaView is rate-limiting requests": "KartaView 已被限流",
|
||||
|
||||
@@ -695,6 +695,12 @@ export function createStreetLevelLayer({
|
||||
* as the assistant's) neither uses nor drops a link's armed point. Why
|
||||
* nothing opened is left in `street.nearestOutcome`: 'empty' (no imagery
|
||||
* in reach), 'sky' (the screen centre is not ground), or null (an error).
|
||||
* `replace: true` (a request that takes over the panel) keeps an open
|
||||
* photo up while it looks, so the viewer and EXPAND stay, and closes it
|
||||
* when the lookup opens nothing: the panel shows this request's outcome.
|
||||
* `proceed()` is asked once the lookup has found an image and before it
|
||||
* loads; false retires the request (a photo that bills every load must
|
||||
* not load for a request its caller has given up).
|
||||
*/
|
||||
async openNearest(
|
||||
point,
|
||||
@@ -705,6 +711,8 @@ export function createStreetLevelLayer({
|
||||
radiusM = null,
|
||||
signal: callerSignal = null,
|
||||
armed: useArmed = true,
|
||||
replace = false,
|
||||
proceed = null,
|
||||
} = {},
|
||||
) {
|
||||
if (callerSignal?.aborted) return false;
|
||||
@@ -713,6 +721,11 @@ export function createStreetLevelLayer({
|
||||
// any other lookup is a new request and drops the armed one.
|
||||
const armed = useArmed ? state.street.armed : null;
|
||||
if (useArmed) state.street.armed = null;
|
||||
const closeReplaced = () => {
|
||||
if (!replace || !state.street.open) return;
|
||||
parts.viewerHost.close();
|
||||
parts.clearSequences();
|
||||
};
|
||||
if (!point && aim === 'screen-centre' && armed) {
|
||||
// The button promises the centre of the view: the link's spot only
|
||||
// while the view still shows it, or when the view cannot be picked.
|
||||
@@ -735,6 +748,7 @@ export function createStreetLevelLayer({
|
||||
view = screenCentreGround();
|
||||
if (view === null) {
|
||||
abortNearest();
|
||||
closeReplaced();
|
||||
state.street.loading = false;
|
||||
state.street.error = AIM_AT_GROUND_MESSAGE;
|
||||
state.street.nearestOutcome = 'sky';
|
||||
@@ -795,6 +809,7 @@ export function createStreetLevelLayer({
|
||||
if (!state.enabled) break;
|
||||
// Switched off, or its map left, while it looked: try the next.
|
||||
if (!imageId || !entry.on || !providerAvailable(entry)) continue;
|
||||
if (proceed && proceed() !== true) return retired();
|
||||
nearestLookup = null;
|
||||
callerSignal?.removeEventListener('abort', onCallerAbort);
|
||||
// The caller's signal still counts while the viewer mounts and loads.
|
||||
@@ -807,6 +822,7 @@ export function createStreetLevelLayer({
|
||||
// Replaced while it looked: the panel is the newer lookup's.
|
||||
if (nearestLookup !== lookup) return false;
|
||||
nearestLookup = null;
|
||||
closeReplaced();
|
||||
// Switched off while it looked: nothing to open, nothing to report.
|
||||
const stillOn = entries.some(
|
||||
(entry) => entry.on && providerAvailable(entry),
|
||||
|
||||
@@ -2110,3 +2110,83 @@ test('filter-only updates keep the lit source, even a chosen keyless Mapillary',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('a replacing lookup keeps the viewer open while it looks, so EXPAND stays with the new photo', async (t) => {
|
||||
const answers = ['img-1', 'img-2'];
|
||||
const provider = posingProvider({
|
||||
nearestImage: async () => answers.shift(),
|
||||
});
|
||||
const { layer } = await enabledLayer(t, [provider]);
|
||||
layer.attachViewerHost({});
|
||||
assert.equal(await layer.openNearest({ lat: 1, lon: 2 }), true);
|
||||
const seen = [];
|
||||
const unsubscribe = layer.subscribe(() =>
|
||||
seen.push(layer.getUIState().street.open),
|
||||
);
|
||||
t.after(unsubscribe);
|
||||
assert.equal(
|
||||
await layer.openNearest({ lat: 3, lon: 4 }, { replace: true }),
|
||||
true,
|
||||
);
|
||||
assert.ok(seen.length > 0);
|
||||
assert.ok(!seen.includes(false), 'never published closed on the way');
|
||||
assert.deepEqual(provider.calls.open, ['img-1', 'img-2']);
|
||||
});
|
||||
|
||||
test('a replacing lookup that finds nothing closes the old photo and keeps its own message', async (t) => {
|
||||
const answers = ['img-1', null];
|
||||
const provider = posingProvider({
|
||||
nearestImage: async () => answers.shift(),
|
||||
});
|
||||
const { layer } = await enabledLayer(t, [provider]);
|
||||
layer.attachViewerHost({});
|
||||
await layer.openNearest({ lat: 1, lon: 2 });
|
||||
assert.equal(
|
||||
await layer.openNearest({ lat: 3, lon: 4 }, { replace: true }),
|
||||
false,
|
||||
);
|
||||
const street = layer.getUIState().street;
|
||||
assert.equal(street.open, false, 'no photo of another place');
|
||||
assert.equal(street.nearestOutcome, 'empty');
|
||||
assert.match(street.error, /No street-level imagery within 50 m/);
|
||||
});
|
||||
|
||||
test('a found photo the viewer cannot load keeps its error on the open viewer', async (t) => {
|
||||
const provider = posingProvider({ nearestImage: async () => 'img-1' });
|
||||
const create = provider.create;
|
||||
provider.create = (context) => {
|
||||
const instance = create(context);
|
||||
instance.viewer.open = async () => {
|
||||
throw new Error('Image could not be opened');
|
||||
};
|
||||
return instance;
|
||||
};
|
||||
const { layer } = await enabledLayer(t, [provider]);
|
||||
layer.attachViewerHost({});
|
||||
assert.equal(
|
||||
await layer.openNearest({ lat: 1, lon: 2 }, { replace: true }),
|
||||
false,
|
||||
);
|
||||
const street = layer.getUIState().street;
|
||||
assert.equal(street.open, true);
|
||||
assert.equal(street.error, 'Image could not be opened');
|
||||
});
|
||||
|
||||
test('proceed() is asked after the lookup: false loads nothing and clears the spinner', async (t) => {
|
||||
const provider = posingProvider({ nearestImage: async () => 'img-1' });
|
||||
const { layer } = await enabledLayer(t, [provider]);
|
||||
layer.attachViewerHost({});
|
||||
let asked = 0;
|
||||
const opened = await layer.openNearest(
|
||||
{ lat: 1, lon: 2 },
|
||||
{ proceed: () => (asked++, false) },
|
||||
);
|
||||
assert.equal(opened, false);
|
||||
assert.equal(asked, 1);
|
||||
assert.deepEqual(provider.calls.open, [], 'the viewer never loads it');
|
||||
assert.equal(layer.getUIState().street.loading, false);
|
||||
assert.equal(
|
||||
await layer.openNearest({ lat: 1, lon: 2 }, { proceed: () => true }),
|
||||
true,
|
||||
);
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { msg } from '../../../../i18n/marker.js';
|
||||
import { NEAREST_RADIUS_M } from '../../policy.js';
|
||||
import {
|
||||
MAPILLARY_GRAPH_HOST,
|
||||
@@ -8,6 +9,9 @@ import {
|
||||
SEQUENCE_IMAGES_LIMIT,
|
||||
} from './policy.js';
|
||||
|
||||
/** English data: the Street Level panel translates it. */
|
||||
const NO_ANSWER = msg('Mapillary did not answer in time — try again');
|
||||
|
||||
/** HTTP error flagged `keyRequired`, `keyRejected` or `retryAfterSec` (429). */
|
||||
export class MapillarySourceError extends Error {
|
||||
constructor(message, { status = 0, payload = null, retryAfterSec } = {}) {
|
||||
@@ -81,7 +85,7 @@ export function createMapillarySource({
|
||||
const timer = setTimeout(
|
||||
() =>
|
||||
controller.abort(
|
||||
new MapillarySourceError('Mapillary did not answer in time', {
|
||||
new MapillarySourceError(NO_ANSWER, {
|
||||
status: 504,
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -201,6 +201,25 @@ test('a nearest-image search that never answers is given up, body included', asy
|
||||
assert.ok(error instanceof MapillarySourceError, String(error));
|
||||
assert.equal(error.status, 504);
|
||||
assert.equal(error.keyRequired, false, 'not a key problem');
|
||||
// English data, shown in the interface language like the other sources'.
|
||||
const { setLocale } = await import('../../../../i18n/index.js');
|
||||
const { translateStreetLevelError } =
|
||||
await import('../../../../ui/streetLevelPresentation.js');
|
||||
assert.equal(
|
||||
translateStreetLevelError(error.message),
|
||||
'Mapillary did not answer in time — try again',
|
||||
);
|
||||
for (const [locale, expected] of [
|
||||
['de', /nicht rechtzeitig/],
|
||||
['ja', /時間内に応答/],
|
||||
]) {
|
||||
setLocale(locale);
|
||||
try {
|
||||
assert.match(translateStreetLevelError(error.message), expected, locale);
|
||||
} finally {
|
||||
setLocale('en');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('the caller’s abort still ends a nearest-image search before its deadline', async () => {
|
||||
|
||||
+10
-6
@@ -4084,14 +4084,12 @@ async function requestStreetLevel(
|
||||
location,
|
||||
});
|
||||
|
||||
// A photo of another place must not stay up beside this request's answer,
|
||||
// armed or opened, found or not: the panel shows this request's outcome.
|
||||
if (layerModule.getUIState?.()?.street?.open === true)
|
||||
layerModule.closeViewer?.();
|
||||
|
||||
// A billed source never loads from a link without a click: everything is
|
||||
// ready and OPEN is one press away at the view centre.
|
||||
if (args.autoOpen === false && status?.billed === true) {
|
||||
// A photo of another place must not stay up beside the armed request.
|
||||
if (layerModule.getUIState?.()?.street?.open === true)
|
||||
layerModule.closeViewer?.();
|
||||
// The next OPEN press loads exactly what the link asked for.
|
||||
layerModule.armNearest?.(point, {
|
||||
providerIds: [provider],
|
||||
@@ -4113,11 +4111,17 @@ async function requestStreetLevel(
|
||||
|
||||
// A source with no coverage to click (Street View) is searched wider: a
|
||||
// geocoded landmark is rarely within 50 m of a street. A newer turn
|
||||
// retires the lookup: nothing opens after the user moved on.
|
||||
// retires the lookup: nothing opens after the user moved on. A photo of
|
||||
// another place stays up while it looks (the viewer, EXPAND included, is
|
||||
// replaced, not closed) and closes if this request opens nothing: the
|
||||
// panel shows this request's outcome. Once an image is found it loads
|
||||
// only if the camera and the panel are still the request's.
|
||||
reportProgress('imagery', label);
|
||||
const opened = await layerModule.openNearest(point, {
|
||||
providerIds: [provider],
|
||||
frame: false,
|
||||
replace: true,
|
||||
proceed: () => current() && !owner.taken() && !streetLevelPanelHidden(),
|
||||
// Under the screen centre, not the middle of a tilted view's footprint;
|
||||
// this request is the user's own, not a link's armed one.
|
||||
...(point ? {} : { aim: 'screen-centre', armed: false }),
|
||||
|
||||
@@ -6660,16 +6660,26 @@ function createStreetLevelHarness({
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Like the layer: a replacing lookup that opens nothing closes the photo.
|
||||
const closeReplaced = () => {
|
||||
if (!options.replace || !street.open) return;
|
||||
order.push('close');
|
||||
Object.assign(street, { open: false, imageId: null, error: null, nearestOutcome: null });
|
||||
};
|
||||
if (imageryError) {
|
||||
closeReplaced();
|
||||
street.error = imageryError;
|
||||
street.nearestOutcome = null;
|
||||
return false;
|
||||
}
|
||||
if (!nearest) {
|
||||
closeReplaced();
|
||||
street.nearestOutcome = options.aim === 'screen-centre' && sky ? 'sky' : 'empty';
|
||||
street.error = `No street-level imagery within ${options.radiusM ?? 50} m of ${point ? 'that point' : 'the view centre'}`;
|
||||
return false;
|
||||
}
|
||||
// Found: it loads only if the request still wants it.
|
||||
if (options.proceed && options.proceed() !== true) return false;
|
||||
street.open = true;
|
||||
street.imageId = nearest;
|
||||
street.title = '10th St, Sacramento';
|
||||
@@ -7426,6 +7436,40 @@ test('show_street_level stops once the camera is someone else’s: a navigation
|
||||
assert.ok(!h.order.some((step) => step.startsWith('open:')));
|
||||
});
|
||||
|
||||
test('show_street_level loads no photo it found once the camera or the panel moved on during the lookup', async () => {
|
||||
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
|
||||
for (const take of ['navigate', 'gesture']) {
|
||||
let h;
|
||||
h = createStreetLevelHarness({ enabled: true, activeStack: 'photoreal', duringLookup: () => h[take]() });
|
||||
const result = await h.runner('show_street_level', { latitude: 38.58, longitude: -121.49, provider: 'google', fly: false });
|
||||
assert.deepEqual([result.ok, result.stage, result.superseded], [false, 'imagery', true], take);
|
||||
assert.equal(h.lookups.length, 1);
|
||||
assert.equal(h.street.open, false, `${take}: the billed panorama never loads`);
|
||||
assert.ok(!h.order.includes('close'), `${take}: nothing opened, nothing to close`);
|
||||
}
|
||||
const saved = globalThis.document;
|
||||
const classes = new Set();
|
||||
globalThis.document = { body: { classList: { contains: (name) => classes.has(name) } } };
|
||||
try {
|
||||
const h = createStreetLevelHarness({ enabled: true, activeStack: 'photoreal', duringLookup: () => classes.add('ui-clean-view') });
|
||||
const result = await h.runner('show_street_level', { latitude: 38.58, longitude: -121.49, provider: 'google', fly: false });
|
||||
assert.deepEqual([result.ok, result.stage], [false, 'view']);
|
||||
assert.equal(h.street.open, false, 'nothing loads into the hidden panel');
|
||||
} finally {
|
||||
globalThis.document = saved;
|
||||
}
|
||||
});
|
||||
|
||||
test('show_street_level replaces an open photo in place: the viewer (and EXPAND) stays up while it looks', async () => {
|
||||
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
|
||||
const h = createStreetLevelHarness({ enabled: true, photoOpen: true, nearest: 'pano-b' });
|
||||
const result = await h.runner('show_street_level', { latitude: 38.58, longitude: -121.49, provider: 'mapillary', fly: false });
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(h.lookups[0].replace, true, 'the lookup takes over the open photo');
|
||||
assert.ok(!h.order.includes('close'), 'never closed on the way');
|
||||
assert.deepEqual([h.street.open, h.street.imageId], [true, 'pano-b']);
|
||||
});
|
||||
|
||||
test('show_street_level owns the camera from where its own flight lands', async () => {
|
||||
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
|
||||
const h = createStreetLevelHarness();
|
||||
|
||||
Reference in new issue
Block a user