fix(street-level): a photo that fails to open opens the panel too

With the panel collapsed (the layer switched on from DATA LAYERS no longer
opens it), a line or ground click whose lookup failed — KartaView timing out
or answering 503, no Street View within 50 m — left its error inside the
collapsed panel: the click seemed to do nothing. A new open error now opens
the panel, unstored, as a photo opening does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-19 23:17:24 +04:00
1 parent 44d9455738
commit fce328b7cd
4 files changed
+33 -9

No files matched your search

+3 -2
View File
@@ -1,4 +1,5 @@
- Street Level panel: SINCE is a dropdown of its nine windows (the tooltip - Street Level panel: SINCE is a dropdown of its nine windows (the tooltip
names the cut-off date), IMAGERY and FIT / FILL are plain toggle buttons, names the cut-off date), IMAGERY and FIT / FILL are plain toggle buttons,
and the panel opens by itself only when a photo opens, no longer when the and the panel opens by itself only when a photo opens or fails to, no
layer is switched on. FIT / FILL starts at FIT each session (gekh, #1241). longer when the layer is switched on. FIT / FILL starts at FIT each
session (gekh, #1241).
+2 -2
View File
@@ -3539,8 +3539,8 @@ image; FIT/FILL starts at FIT each session. IMAGERY and FIT/FILL are rows of
toggle buttons (`aria-pressed`), each its own tab stop. SINCE is a select of toggle buttons (`aria-pressed`), each its own tab stop. SINCE is a select of
the nine windows; a link's day count between them gets its own option, and the nine windows; a link's day count between them gets its own option, and
the tooltip names the cut-off date. The panel opens by itself only when a the tooltip names the cut-off date. The panel opens by itself only when a
photo opens (a click on the map or a line would otherwise load it out of photo opens or fails to (a click on the map or a line would otherwise load
sight), never when the layer is switched on. it, or its error, out of sight), never when the layer is switched on.
MapillaryJS loads on the first photo. Opening one frames it with a single MapillaryJS loads on the first photo. Opening one frames it with a single
camera flight through the application's deferred navigation (`begin` / camera flight through the application's deferred navigation (`begin` /
+11 -4
View File
@@ -29,6 +29,8 @@ export class StreetLevelControls {
this._state = null; this._state = null;
this._view = null; this._view = null;
this._wasOpen = false; this._wasOpen = false;
/** The open error last shown, so the same one never reopens the panel. */
this._streetError = null;
/** The SINCE option for a day count between the stops, while one is set. */ /** The SINCE option for a day count between the stops, while one is set. */
this._customSince = null; this._customSince = null;
this._resizeQueued = false; this._resizeQueued = false;
@@ -430,16 +432,21 @@ export class StreetLevelControls {
} }
/** /**
* Open the panel when an image opens (a click on the map or a line would * Open the panel when an image opens, or fails to (a click on the map or a
* otherwise load it out of sight). Never persisted: the stored collapse * line would otherwise load it, or its error, out of sight). Never
* state stays the user's own choice. * persisted: the stored collapse state stays the user's own choice.
*/ */
_reactToTransitions(view) { _reactToTransitions(view) {
// Other size changes reach the viewer through the ResizeObserver; opening // Other size changes reach the viewer through the ResizeObserver; opening
// asks for one resize because the element may not have a size yet. // asks for one resize because the element may not have a size yet.
const open = view.viewer.open; const open = view.viewer.open;
if (open && !this._wasOpen) this.setCollapsed(false, { persist: false }); // A photo that failed to open (a line or ground click, OPEN) says why in
// the panel: it opens for that too, or the click would seem to do nothing.
const error = this._state?.street?.error || null;
if ((open && !this._wasOpen) || (error && error !== this._streetError))
this.setCollapsed(false, { persist: false });
this._wasOpen = open; this._wasOpen = open;
this._streetError = error;
} }
destroy() { destroy() {
+17 -1
View File
@@ -425,6 +425,7 @@ function uiState({
sinceDays = 0, sinceDays = 0,
renderMode = 'letterbox', renderMode = 'letterbox',
keyRequired = false, keyRequired = false,
streetError = null,
} = {}) { } = {}) {
return { return {
enabled, enabled,
@@ -439,7 +440,7 @@ function uiState({
loading: false, loading: false,
renderMode, renderMode,
imageId: open ? 'img-1' : null, imageId: open ? 'img-1' : null,
error: null, error: streetError,
}, },
}; };
} }
@@ -787,6 +788,21 @@ test('a photo opening opens the panel, unstored; switching the layer on does not
controls.destroy(); controls.destroy();
})); }));
test('a photo that fails to open opens the panel too, so its error shows', () =>
withDom(async (dom) => {
const { layer, calls, controls } = stubPanel(dom, uiState());
// A line click whose lookup KartaView never answered.
layer.publish(uiState({ streetError: 'KartaView did not answer in time' }));
assert.deepEqual(calls.collapsed, [[false, { persist: false }]]);
// The same error again opens nothing more; a new one does.
layer.publish(uiState({ streetError: 'KartaView did not answer in time' }));
assert.equal(calls.collapsed.length, 1);
layer.publish(uiState());
layer.publish(uiState({ streetError: 'No street-level imagery here' }));
assert.equal(calls.collapsed.length, 2);
controls.destroy();
}));
test('FIT / FILL show the selected render mode (P3)', () => test('FIT / FILL show the selected render mode (P3)', () =>
withDom(async (dom) => { withDom(async (dom) => {
const { layer, controls } = stubPanel(dom, uiState({ open: true })); const { layer, controls } = stubPanel(dom, uiState({ open: true }));