mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
feat(street-level): OPEN STREET VIEW, and Street View only on Google 3D
- OPEN STREET VIEW in the PROVIDERS row opens Street View's nearest panorama at the ground under the middle of the screen, without moving the camera: Street View no longer needs a pointer. It is disabled (and says so) until the camera is at street zoom. - Off the Google 3D map Street View does not appear at all: no chip, legend swatch, globe credit, layer source or button, and Google's script is not loaded (the viewer has no prewarm; the script loads on first use). The share option keeps the user's choice for when Google 3D returns. - A ground click measures the camera height at the click rather than reading the cached flag. - A chip without a usable key explains it instead of lighting; it darkens beside a lit chip, and the other chips keep switching, so a refused Street View key cannot lock Mapillary out. - The gate checks that nothing of Street View shows off Google 3D even when a share link switches it on, presses OPEN STREET VIEW on Google 3D, and asserts that each chip it switches really switched. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
7bfcf177ac
commit
ef396655fc
18 files changed
+505
-110
No files matched your search
@@ -66,6 +66,7 @@ export class StreetLevelControls {
|
||||
status: byId('sl-status'),
|
||||
controls: byId('sl-controls'),
|
||||
providerChips: byId('sl-provider-chips'),
|
||||
nearestBtn: byId('sl-nearest-btn'),
|
||||
error: byId('sl-error'),
|
||||
errorText: byId('sl-error-text'),
|
||||
sinceRange: byId('sl-since'),
|
||||
@@ -103,9 +104,18 @@ export class StreetLevelControls {
|
||||
this.listen(el.providerChips, 'click', (event) => {
|
||||
const button = event.target?.closest?.('.data-toggle-chip');
|
||||
if (!button || button.disabled) return;
|
||||
// Without a key the chips stay focusable for their tooltip; a click
|
||||
// says the same thing rather than switching on a layer that cannot draw.
|
||||
if (this._view?.controlsDisabled) {
|
||||
// A chip without a usable key stays focusable for its tooltip; a click
|
||||
// that would light it, or switch the layer off through it, says the
|
||||
// same thing instead. Other chips still switch (a refused Street View
|
||||
// key cannot lock Mapillary out), and it darkens beside a lit one.
|
||||
const providers = this._view?.providers || [];
|
||||
const chip = providers.find(
|
||||
(entry) => entry.id === button.dataset.chipId,
|
||||
);
|
||||
const othersLit = providers.some(
|
||||
(entry) => entry.id !== chip?.id && entry.active,
|
||||
);
|
||||
if (chip?.state === 'error' && (!chip.active || !othersLit)) {
|
||||
if (button.title) this.actions.showToast?.(button.title);
|
||||
return;
|
||||
}
|
||||
@@ -129,6 +139,17 @@ export class StreetLevelControls {
|
||||
this.listen(el.sinceRange, 'change', () =>
|
||||
this._setParams({ sinceDays: sinceDays() }),
|
||||
);
|
||||
// The keyboard path to Street View: its nearest image at the view centre,
|
||||
// without moving the camera.
|
||||
this.listen(el.nearestBtn, 'click', () => {
|
||||
const nearest = this._view?.nearest;
|
||||
if (!nearest?.visible || nearest.disabled) return;
|
||||
this.layer.openNearest?.(undefined, {
|
||||
providerIds: nearest.providerIds,
|
||||
frame: false,
|
||||
aim: 'screen-centre',
|
||||
});
|
||||
});
|
||||
this.listen(el.followBtn, 'click', () => {
|
||||
this.layer.setFollow?.(!(this._state?.street?.follow === true));
|
||||
});
|
||||
@@ -466,6 +487,7 @@ export class StreetLevelControls {
|
||||
this._renderFilters(view);
|
||||
this._renderViewer(view);
|
||||
this._renderMeta(view);
|
||||
this._renderNearest(view);
|
||||
this._reactToTransitions(view, state);
|
||||
}
|
||||
|
||||
@@ -595,6 +617,17 @@ export class StreetLevelControls {
|
||||
setProp(this._elements.coverageMeta, 'textContent', view.meta);
|
||||
}
|
||||
|
||||
_renderNearest(view) {
|
||||
const button = this._elements.nearestBtn;
|
||||
if (!button) return;
|
||||
const { nearest } = view;
|
||||
setProp(button, 'hidden', !nearest.visible);
|
||||
if (!nearest.visible) return;
|
||||
setProp(button, 'textContent', nearest.label);
|
||||
setProp(button, 'title', nearest.title);
|
||||
setProp(button, 'disabled', nearest.disabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the panel on a switch-on or when an image opens. Never persisted:
|
||||
* the stored collapse state stays the user's own choice.
|
||||
|
||||
@@ -305,6 +305,7 @@ function panelDom() {
|
||||
const settings = add(main, 'div', { classes: ['sl-settings'] });
|
||||
for (const id of ['sl-provider-chips', 'sl-error', 'sl-error-text'])
|
||||
add(settings, 'div', { id });
|
||||
add(settings, 'button', { id: 'sl-nearest-btn' });
|
||||
// A missing key gates the filters only; the chips stay outside the gate.
|
||||
const controls = add(settings, 'fieldset', { id: 'sl-controls' });
|
||||
for (const pano of ['all', 'pano', 'flat'])
|
||||
@@ -1136,3 +1137,72 @@ test('a <body> child added while the viewer is expanded goes inert with the rest
|
||||
assert.equal(observer.watching(), false);
|
||||
}),
|
||||
));
|
||||
|
||||
test('OPEN STREET VIEW opens the nearest panorama at the view centre without moving the camera', () =>
|
||||
withDom(async (dom) => {
|
||||
const state = uiState();
|
||||
state.providers.push({
|
||||
...state.providers[0],
|
||||
id: 'google',
|
||||
name: 'Google Street View',
|
||||
label: 'STREET VIEW',
|
||||
groundClick: true,
|
||||
count: 0,
|
||||
});
|
||||
state.groundClickReady = true;
|
||||
const { layer } = stubPanel(dom, state);
|
||||
const asked = [];
|
||||
layer.openNearest = (point, options) => asked.push([point, options]);
|
||||
const button = dom.root.querySelector('#sl-nearest-btn');
|
||||
assert.equal(button.hidden, false);
|
||||
assert.equal(button.textContent, 'OPEN STREET VIEW');
|
||||
button.click();
|
||||
assert.deepEqual(asked, [
|
||||
[
|
||||
undefined,
|
||||
{ providerIds: ['google'], frame: false, aim: 'screen-centre' },
|
||||
],
|
||||
]);
|
||||
// Too high to mean a street: disabled, and a click does nothing.
|
||||
layer.publish({ ...state, groundClickReady: false });
|
||||
assert.equal(button.disabled, true);
|
||||
button.click();
|
||||
assert.equal(asked.length, 1);
|
||||
// Off Google 3D the provider is not listed: no button.
|
||||
layer.publish({ ...state, providers: [state.providers[0]] });
|
||||
assert.equal(button.hidden, true);
|
||||
}));
|
||||
|
||||
test('a refused key on one provider leaves the other chip switchable, and the refused chip darkens beside it', () =>
|
||||
withDom(async (dom) => {
|
||||
const state = uiState({ on: false, keyRequired: true });
|
||||
state.providers.push({
|
||||
...state.providers[0],
|
||||
id: 'google',
|
||||
name: 'Google Street View',
|
||||
label: 'STREET VIEW',
|
||||
on: true,
|
||||
keyRequired: true,
|
||||
keyRejected: true,
|
||||
error: 'Google refused GOOGLE_MAPS_API_KEY for Street View',
|
||||
});
|
||||
state.providers[0].keyRequired = false;
|
||||
const { calls, layer } = stubPanel(dom, state);
|
||||
const chips = () => dom.root.querySelector('#sl-provider-chips').children;
|
||||
chips()[0].click(); // Mapillary: configured, dark
|
||||
await settle();
|
||||
assert.deepEqual(calls.setParams, [
|
||||
[{ mapillary: true }, { origin: 'user' }],
|
||||
]);
|
||||
// With Mapillary lit, the refused Street View chip can be darkened.
|
||||
state.providers[0].on = true;
|
||||
state.keyRequired = false;
|
||||
layer.publish({ ...state });
|
||||
chips()[1].click();
|
||||
await settle();
|
||||
assert.deepEqual(calls.setParams.at(-1), [
|
||||
{ google: false },
|
||||
{ origin: 'user' },
|
||||
]);
|
||||
assert.equal(calls.toasts.length, 0);
|
||||
}));
|
||||
@@ -80,8 +80,6 @@ function presentProviders(state) {
|
||||
else if (keyRequired && provider.requiresKeyId)
|
||||
title = `${provider.name}: ${keySetupRequirement(provider.requiresKeyId)}`;
|
||||
else if (provider.error) title = `${provider.name}: ${provider.error}`;
|
||||
// Usable only on another map stack (Street View off Google 3D).
|
||||
else if (provider.unavailable) title = provider.unavailable;
|
||||
return {
|
||||
id: provider.id,
|
||||
label: provider.label,
|
||||
@@ -91,13 +89,11 @@ function presentProviders(state) {
|
||||
disabled: false,
|
||||
state: keyRequired
|
||||
? 'error'
|
||||
: on && provider.unavailable
|
||||
? 'warn'
|
||||
: on && provider.loading
|
||||
? 'loading'
|
||||
: on
|
||||
? 'active'
|
||||
: 'idle',
|
||||
: on && provider.loading
|
||||
? 'loading'
|
||||
: on
|
||||
? 'active'
|
||||
: 'idle',
|
||||
busy: on && provider.loading === true,
|
||||
};
|
||||
});
|
||||
@@ -134,6 +130,36 @@ function presentViewer(state) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A button for the providers with no coverage to click (Street View): opens
|
||||
* their nearest image at the view centre, so they need no pointer.
|
||||
*/
|
||||
function presentNearest(state) {
|
||||
const targets = (state.providers || []).filter(
|
||||
(provider) =>
|
||||
provider.on && provider.groundClick && provider.keyRequired !== true,
|
||||
);
|
||||
if (state.enabled !== true || !targets.length)
|
||||
return {
|
||||
visible: false,
|
||||
label: '',
|
||||
title: '',
|
||||
disabled: true,
|
||||
providerIds: [],
|
||||
};
|
||||
const names = targets.map((provider) => provider.name).join(' or ');
|
||||
const ready = state.groundClickReady === true;
|
||||
return {
|
||||
visible: true,
|
||||
label: `OPEN ${targets.map((provider) => provider.label).join(' / ')}`,
|
||||
title: ready
|
||||
? `Open ${names} at the centre of the view`
|
||||
: `Zoom in to a street to open ${names} here`,
|
||||
disabled: !ready || state.street.loading === true,
|
||||
providerIds: targets.map((provider) => provider.id),
|
||||
};
|
||||
}
|
||||
|
||||
function presentMeta(state) {
|
||||
if (!state.enabled) return 'Switch a provider on to draw its coverage.';
|
||||
if (state.sequence.selectedId)
|
||||
@@ -173,6 +199,7 @@ export function presentStreetLevelPanel(state, { now = Date.now() } = {}) {
|
||||
legend: state.legend || [],
|
||||
viewer: presentViewer(state),
|
||||
meta: presentMeta(state),
|
||||
nearest: presentNearest(state),
|
||||
wantsOpen: state.street.open === true,
|
||||
};
|
||||
}
|
||||
@@ -434,3 +434,52 @@ test('the meta line keeps the sequence count and adds the other providers’ hin
|
||||
`${(2514).toLocaleString()} sequences in view · click a line for its photos · click a street to open Street View`,
|
||||
);
|
||||
});
|
||||
|
||||
test('OPEN STREET VIEW shows while Street View is on, and waits for street zoom', () => {
|
||||
const streetView = provider({
|
||||
id: 'google',
|
||||
name: 'Google Street View',
|
||||
label: 'STREET VIEW',
|
||||
groundClick: true,
|
||||
requiresKeyId: 'google-maps',
|
||||
});
|
||||
const view = (overrides) =>
|
||||
presentStreetLevelPanel(
|
||||
snapshot({
|
||||
enabled: true,
|
||||
providers: [provider(), streetView],
|
||||
...overrides,
|
||||
}),
|
||||
).nearest;
|
||||
const ready = view({ groundClickReady: true });
|
||||
assert.deepEqual(ready, {
|
||||
visible: true,
|
||||
label: 'OPEN STREET VIEW',
|
||||
title: 'Open Google Street View at the centre of the view',
|
||||
disabled: false,
|
||||
providerIds: ['google'],
|
||||
});
|
||||
const high = view({ groundClickReady: false });
|
||||
assert.equal(high.disabled, true);
|
||||
assert.equal(
|
||||
high.title,
|
||||
'Zoom in to a street to open Google Street View here',
|
||||
);
|
||||
// Hidden with Street View off, without its key, or with the layer off
|
||||
// (off Google 3D the provider is not listed at all).
|
||||
assert.equal(
|
||||
view({ providers: [provider(), { ...streetView, on: false }] }).visible,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
view({ providers: [provider(), { ...streetView, keyRequired: true }] })
|
||||
.visible,
|
||||
false,
|
||||
);
|
||||
assert.equal(view({ providers: [provider()] }).visible, false);
|
||||
assert.equal(
|
||||
presentStreetLevelPanel(snapshot({ providers: [provider(), streetView] }))
|
||||
.nearest.visible,
|
||||
false,
|
||||
);
|
||||
});
|
||||
@@ -183,10 +183,12 @@ button.sl-status:focus-visible {
|
||||
border-color: rgba(255, 179, 0, 0.55);
|
||||
}
|
||||
|
||||
/* On, but not usable on this map stack (Street View off Google 3D). */
|
||||
.sl-chips .data-toggle-chip.chip-warn {
|
||||
opacity: 0.7;
|
||||
border-style: dashed;
|
||||
.sl-nearest-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.sl-nearest-btn[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── Errors ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
<div class="sl-providers" role="group" aria-label="Imagery providers">
|
||||
<span class="cctv-summary-label">PROVIDERS</span>
|
||||
<div id="sl-provider-chips" class="sl-chips"></div>
|
||||
<button id="sl-nearest-btn" class="scene-btn sl-nearest-btn" type="button" hidden>OPEN STREET VIEW</button>
|
||||
</div>
|
||||
<div id="sl-error" class="sl-error" role="alert" hidden><span aria-hidden="true">⚠</span> <span id="sl-error-text"></span></div>
|
||||
<!-- A missing key gates the filters only: the chips above stay
|
||||
|
||||
Reference in new issue
Block a user