mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-07 03:44:00 +00:00
Merge pull request #1601 from bilawalsidhu/fix/street-level-keyless-default
fix(street-level): without a Mapillary key, start on a keyless source Thanks @asturksever. Maintainer review fixes (separate commits): filter-only updates keep an explicitly chosen source, keyless Street Level QA gate updated, tests and docs.
This commit is contained in:
8 files changed
+127
-9
No files matched your search
@@ -485,9 +485,9 @@ _The Space Missions layer replaying a Falcon 9 ascent — labeled `RECONSTRUCTED
|
||||
|
||||
> Street-level photos from any provider in one panel. This build ships **Mapillary** (a free client token), **Google Street View** (your Google Maps key), **KartaView** and **Panoramax** (no key).
|
||||
|
||||
**Setup.** At [mapillary.com/dashboard/developers](https://www.mapillary.com/dashboard/developers), register an application with **Read** access and copy its **Client Token** (`MLY|…`). Paste it into **POWER UP → Mapillary**, or set `MAPILLARY_CLIENT_TOKEN` in `.env` and restart. Without it the panel reads **KEY REQUIRED**. **Google Street View** uses `GOOGLE_MAPS_API_KEY` with the **Maps JavaScript API** enabled for it in Google Cloud; its chip starts off, since every panorama is billed to the key. **KartaView** and **Panoramax** need nothing: their public APIs are keyless.
|
||||
**Setup.** At [mapillary.com/dashboard/developers](https://www.mapillary.com/dashboard/developers), register an application with **Read** access and copy its **Client Token** (`MLY|…`). Paste it into **POWER UP → Mapillary**, or set `MAPILLARY_CLIENT_TOKEN` in `.env` and restart. Without it Street Level starts on **KartaView** instead, and the Mapillary chip says which key to add. **Google Street View** uses `GOOGLE_MAPS_API_KEY` with the **Maps JavaScript API** enabled for it in Google Cloud; its chip starts off, since every panorama is billed to the key. **KartaView** and **Panoramax** need nothing: their public APIs are keyless.
|
||||
|
||||
**Use.** Turn on **Street Level** under DATA LAYERS → Cameras, or with the pill in the STREET LEVEL panel header. Each provider has a chip in its own colour (Mapillary green), and one is lit at a time: lighting one switches the others off. Mapillary, which is free, is lit by default. Zoom in to street level (below 60 km) for the coverage lines; from orbit the panel says to zoom in. The **IMAGERY** (ALL / 360° / FLAT) and **SINCE** filters apply to Mapillary, KartaView and Panoramax; Street View panoramas are all 360°, so with Street View lit the filters are hidden. Click a coverage line for its image cones and a cone to open the photo. Light **KARTAVIEW** (purple) and zoom in below about 6 km: its lines are the streets KartaView has photos of, and a click on one opens the photo nearest the click, flat or 360° (drag to look around), with cones along its drive; **‹ ›** or ← → step through the drive (Shift+← →, ↑ ↓ and + − look around a 360° photo; pinch zooms on a phone). Photos KartaView has archived are skipped, and where all are archived the panel says so. For KartaView, **SINCE** goes by upload date, as KartaView's own map does (it can filter by nothing else), and the panel notes it beside the SINCE value (ANY DATE (by upload date)); KartaView filters its lines on its own server, and a filtered area KartaView has not drawn before can take several seconds. Light **PANORAMAX** (orange) for the open, federated commons (strongest in France and Belgium): below about 60 km its lines are sequences. A click on a line opens that sequence's picture nearest the click without moving the camera, flat or 360°, with cones along the sequence around it; **‹ ›** or ← → step through it, and both filters apply. Its caption names the picture's authors (or the instance holding it) and licence (mostly CC BY-SA 4.0; pictures on the IGN instance are Licence Ouverte 2.0). On the **Google 3D** map (Google's terms keep Street View off other maps, where it does not appear), light **GOOGLE STREET VIEW** and click a street from below 3 km, or press **OPEN STREET VIEW** for the view centre, to open Google's panorama there; step along its arrows. Above the photo: **EXPAND** (full screen; Esc leaves) and **FIT / FILL** (Mapillary, and flat KartaView and Panoramax photos). The caption links to the image on the provider's site, and the globe credits each provider that is on ("© Mapillary contributors, CC BY-SA 4.0", "Street View imagery © Google", "© KartaView contributors, CC BY-SA 4.0", "© Panoramax contributors, licence per picture"). KartaView's own photo pages fail for signed-out visitors, so its link opens the full-size photo.
|
||||
**Use.** Turn on **Street Level** under DATA LAYERS → Cameras, or with the pill in the STREET LEVEL panel header. Each provider has a chip in its own colour (Mapillary green), and one is lit at a time: lighting one switches the others off. Mapillary, which is free, is lit by default when its token is set; otherwise KartaView is. Zoom in to street level (below 60 km) for the coverage lines; from orbit the panel says to zoom in. The **IMAGERY** (ALL / 360° / FLAT) and **SINCE** filters apply to Mapillary, KartaView and Panoramax; Street View panoramas are all 360°, so with Street View lit the filters are hidden. Click a coverage line for its image cones and a cone to open the photo. Light **KARTAVIEW** (purple) and zoom in below about 6 km: its lines are the streets KartaView has photos of, and a click on one opens the photo nearest the click, flat or 360° (drag to look around), with cones along its drive; **‹ ›** or ← → step through the drive (Shift+← →, ↑ ↓ and + − look around a 360° photo; pinch zooms on a phone). Photos KartaView has archived are skipped, and where all are archived the panel says so. For KartaView, **SINCE** goes by upload date, as KartaView's own map does (it can filter by nothing else), and the panel notes it beside the SINCE value (ANY DATE (by upload date)); KartaView filters its lines on its own server, and a filtered area KartaView has not drawn before can take several seconds. Light **PANORAMAX** (orange) for the open, federated commons (strongest in France and Belgium): below about 60 km its lines are sequences. A click on a line opens that sequence's picture nearest the click without moving the camera, flat or 360°, with cones along the sequence around it; **‹ ›** or ← → step through it, and both filters apply. Its caption names the picture's authors (or the instance holding it) and licence (mostly CC BY-SA 4.0; pictures on the IGN instance are Licence Ouverte 2.0). On the **Google 3D** map (Google's terms keep Street View off other maps, where it does not appear), light **GOOGLE STREET VIEW** and click a street from below 3 km, or press **OPEN STREET VIEW** for the view centre, to open Google's panorama there; step along its arrows. Above the photo: **EXPAND** (full screen; Esc leaves) and **FIT / FILL** (Mapillary, and flat KartaView and Panoramax photos). The caption links to the image on the provider's site, and the globe credits each provider that is on ("© Mapillary contributors, CC BY-SA 4.0", "Street View imagery © Google", "© KartaView contributors, CC BY-SA 4.0", "© Panoramax contributors, licence per picture"). KartaView's own photo pages fail for signed-out visitors, so its link opens the full-size photo.
|
||||
|
||||
Share links carry the provider switches and filters (`0.m.0` Mapillary off, `0.m.0_0.r.google-1` Street View on, `0.m.0_0.r.google-0*kartaview-1*panoramax-0` KartaView on and `0.m.0_0.r.google-0*kartaview-0*panoramax-1` Panoramax on, as the app writes them (`0.m.0_0.r.kartaview-1` and `0.m.0_0.r.panoramax-1` work too; a link that leaves out `m` keeps Mapillary, the default, lit), `0.p.p` panoramas only, `0.s.365` the last year). To add a provider, implement the contract in `src/layers/streetLevel/registry.js` and register it in `src/app/layers/streetLevel.js`.
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
- Without a Mapillary token, Street Level starts on the first keyless source
|
||||
(KartaView) instead of opening on Mapillary's `KEY REQUIRED` with nothing on
|
||||
the globe. A share link that turns a source off is respected
|
||||
(asturksever, #1601).
|
||||
@@ -4429,8 +4429,10 @@ provider draws in one colour (`PROVIDER_COLORS` in
|
||||
sequence is GEV cyan. Share options: `m` (Mapillary), `r` (the other providers' switches by id,
|
||||
e.g. `google-1` for Street View), `p` (`a`/`p`/`f` panoramas), `s` (since, days). Mapillary, Google
|
||||
Street View, KartaView and Panoramax are registered. Without
|
||||
`MAPILLARY_CLIENT_TOKEN` the panel reads KEY REQUIRED and the controls are
|
||||
disabled. With it, coverage draws as z11–14 sequence lines below 60 km; the proxy strips the unused `image` layer from z14
|
||||
`MAPILLARY_CLIENT_TOKEN` (and no Graph proxy) Street Level starts on the first
|
||||
keyless provider (KartaView), including a restored "Mapillary on" unless the link
|
||||
switches the keyless providers off; filter-only updates never change the source.
|
||||
With it, coverage draws as z11–14 sequence lines below 60 km; the proxy strips the unused `image` layer from z14
|
||||
tiles (12 MB → ~80 KB) and shares one upstream fetch between concurrent
|
||||
requests for a tile.
|
||||
|
||||
@@ -4444,7 +4446,7 @@ the camera, nearest first, at most 9 (the halves of a view across
|
||||
the date line count a shared tile once). Lines are draped
|
||||
`GroundPolylinePrimitive`s (`ClassificationType.BOTH`), so they follow terrain
|
||||
and Google 3D tiles alike; cones and the photo marker are ground-clamped
|
||||
billboards with a horizon cull. Without `MAPILLARY_CLIENT_TOKEN` the panel
|
||||
billboards with a horizon cull. When Mapillary is lit without `MAPILLARY_CLIENT_TOKEN` the panel
|
||||
reads KEY REQUIRED, says which key to add and disables the filters; a rejected
|
||||
key reads KEY REJECTED and stops tile requests until the layer goes off; a 429
|
||||
pauses them for its Retry-After.
|
||||
|
||||
@@ -296,6 +296,22 @@ async function expandPanel(page) {
|
||||
);
|
||||
}
|
||||
|
||||
/** A keyless bundle opens on the first keyless source with live controls. */
|
||||
async function assertKeylessDefault(page) {
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.dataManager.setEnabled('street-level', true, {
|
||||
origin: 'user',
|
||||
}),
|
||||
);
|
||||
await uiUntil(page, litAlone, 'kartaview');
|
||||
const shown = await page.evaluate(() => ({
|
||||
status: document.getElementById('sl-status').textContent,
|
||||
disabled: document.getElementById('sl-controls').disabled,
|
||||
}));
|
||||
assert.notEqual(shown.status, 'KEY REQUIRED');
|
||||
assert.equal(shown.disabled, false, 'controls stay live');
|
||||
}
|
||||
|
||||
async function assertKeyRequired(page) {
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.dataManager.setEnabled('street-level', true, {
|
||||
@@ -528,11 +544,25 @@ async function main() {
|
||||
);
|
||||
|
||||
await expandPanel(page);
|
||||
if (!configured) {
|
||||
if (!configured && litAlone(await getUI(page), 'mapillary')) {
|
||||
// A keyed bundle behind a server without the token keeps Mapillary.
|
||||
await step('keyless server: KEY REQUIRED gates the controls', () =>
|
||||
assertKeyRequired(page),
|
||||
);
|
||||
skip('no Mapillary key', 'the keyed steps');
|
||||
} else if (!configured) {
|
||||
await step('keyless server: opens on KartaView, not KEY REQUIRED', () =>
|
||||
assertKeylessDefault(page),
|
||||
);
|
||||
await step(
|
||||
'keyless server: the MAPILLARY chip explains its key instead of switching',
|
||||
async () => {
|
||||
await press(page, '#sl-provider-chips [data-chip-id="mapillary"]');
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
assert.ok(litAlone(await getUI(page), 'kartaview'));
|
||||
},
|
||||
);
|
||||
skip('no Mapillary key', 'the keyed steps');
|
||||
} else {
|
||||
await park(page);
|
||||
await step(
|
||||
@@ -751,6 +781,8 @@ async function main() {
|
||||
await park(page);
|
||||
|
||||
await fixtureStep(
|
||||
// The fixture bundle carries a token, so this covers a keyless
|
||||
// server behind a keyed bundle; the keyless default is checked above.
|
||||
'keyless server (second page): KEY REQUIRED gates the controls',
|
||||
async () => {
|
||||
const keyless = await open(
|
||||
|
||||
@@ -126,8 +126,17 @@ export function createStreetLevelLayer({
|
||||
});
|
||||
}
|
||||
|
||||
// One source at a time: the first provider that defaults on.
|
||||
const startsOn = defs.find((def) => def.defaultOn !== false);
|
||||
/** `def` if its key is ready, else the first keyless provider not refused. */
|
||||
function keylessInstead(def, refused = () => false) {
|
||||
if (def.keyReady?.() !== false) return def;
|
||||
return defs.find((d) => !d.requiresKeyId && !refused(d.id)) || def;
|
||||
}
|
||||
|
||||
// One source at a time: the first provider that defaults on. Without its
|
||||
// key, the first keyless source instead, so a fresh keyless install opens
|
||||
// on coverage rather than KEY REQUIRED.
|
||||
const preferred = defs.find((def) => def.defaultOn !== false);
|
||||
const startsOn = preferred && keylessInstead(preferred);
|
||||
for (const def of defs) {
|
||||
const entry = {
|
||||
def,
|
||||
@@ -585,7 +594,17 @@ export function createStreetLevelLayer({
|
||||
providerIds: state.providers.keys(),
|
||||
filter: state.filter,
|
||||
});
|
||||
const lit = litByParams(decoded.providers);
|
||||
// A restored "Mapillary on" (the codec default) would light a source a
|
||||
// keyless install cannot draw. Only redirect a provider the params switch
|
||||
// on: a filter-only update keeps whatever the user chose.
|
||||
const asked = litByParams(decoded.providers);
|
||||
const lit =
|
||||
asked && decoded.providers.get(asked) === true
|
||||
? keylessInstead(
|
||||
state.providers.get(asked).def,
|
||||
(id) => decoded.providers.get(id) === false,
|
||||
).id
|
||||
: asked;
|
||||
const before = litIds();
|
||||
for (const [id, entry] of state.providers)
|
||||
if (id !== lit) switchProvider(entry, false);
|
||||
|
||||
@@ -1127,3 +1127,59 @@ test('the panel says why a lit Street View draws nothing off Google 3D', async (
|
||||
const view = presentStreetLevelPanel(layer.getUIState());
|
||||
assert.match(view.meta, /Google Street View needs the Google 3D map/);
|
||||
});
|
||||
|
||||
/** Mapillary (keyless unless `mapillary` says otherwise) plus KartaView. */
|
||||
function keylessLayer(t, mapillary = { hasToken: () => false }) {
|
||||
const layer = createStreetLevelLayer({
|
||||
providers: [
|
||||
createMapillaryProvider({
|
||||
source: fakeMapillarySource(mapillary),
|
||||
photoViewer: fakePhotoViewer(),
|
||||
}),
|
||||
createKartaViewProvider(),
|
||||
],
|
||||
});
|
||||
t.after(() => layer.destroy());
|
||||
return layer;
|
||||
}
|
||||
const litProviders = (layer) =>
|
||||
layer
|
||||
.getUIState()
|
||||
.providers.filter((provider) => provider.on)
|
||||
.map((provider) => provider.id);
|
||||
|
||||
test('without a Mapillary key the first keyless source is lit instead', (t) => {
|
||||
assert.deepEqual(litProviders(keylessLayer(t)), ['kartaview']);
|
||||
assert.deepEqual(litProviders(keylessLayer(t, {})), ['mapillary']);
|
||||
// A token-adding Graph proxy counts as a key.
|
||||
const proxied = keylessLayer(t, {
|
||||
hasToken: () => false,
|
||||
graphEndpoint: '/api/mapillary/graph',
|
||||
});
|
||||
assert.deepEqual(litProviders(proxied), ['mapillary']);
|
||||
});
|
||||
|
||||
test('restoring the default "Mapillary on" without its key keeps a keyless source', (t) => {
|
||||
const layer = keylessLayer(t);
|
||||
layer.setParams({ mapillary: true, providerSwitches: '' });
|
||||
assert.deepEqual(litProviders(layer), ['kartaview']);
|
||||
// A link that switches KartaView off keeps its word.
|
||||
layer.setParams({ mapillary: true, providerSwitches: 'kartaview-0' });
|
||||
assert.deepEqual(litProviders(layer), ['mapillary']);
|
||||
});
|
||||
|
||||
test('filter-only updates keep the lit source, even a chosen keyless Mapillary', (t) => {
|
||||
const layer = keylessLayer(t);
|
||||
layer.setParams({ pano: 'pano' });
|
||||
assert.deepEqual(litProviders(layer), ['kartaview']);
|
||||
// What the MAPILLARY chip sends, and what stored state restores.
|
||||
layer.setParams({ mapillary: true, providerSwitches: 'kartaview-0' });
|
||||
for (const params of [{ pano: 'pano' }, { sinceDays: 365 }, {}]) {
|
||||
layer.setParams(params);
|
||||
assert.deepEqual(
|
||||
litProviders(layer),
|
||||
['mapillary'],
|
||||
JSON.stringify(params),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -74,6 +74,9 @@ export function createMapillaryProvider({ source, photoViewer = null }) {
|
||||
name: MAPILLARY_NAME,
|
||||
label: MAPILLARY_LABEL,
|
||||
requiresKeyId: MAPILLARY_KEY_ID,
|
||||
/** A browser token, or a Graph proxy that adds one server-side. */
|
||||
keyReady: () =>
|
||||
source.hasToken?.() !== false || Boolean(source.graphEndpoint),
|
||||
pickPrefix: PICK_PREFIX.root,
|
||||
colors: COLORS,
|
||||
credit: Object.freeze({ html: MAPILLARY_CREDIT_HTML }),
|
||||
|
||||
@@ -20,6 +20,8 @@ export function filtersOf(provider) {
|
||||
* @property {boolean} [groundClick] No coverage: a ground click at street zoom opens its nearest image.
|
||||
* @property {string} [requiresMapStack] Usable only on this map stack.
|
||||
* @property {boolean} [defaultOn] Default true; the first that defaults on is lit.
|
||||
* @property {() => boolean} [keyReady] False when its key is missing now; the
|
||||
* first keyless provider is lit instead of a default that cannot draw.
|
||||
* @property {{kind: 'view-cone', color: string}} [marker] Default: GEV's pin.
|
||||
* @property {Array<'pano'|'since'>} [filters] Filters it honours (default both).
|
||||
* @property {{noun: string, action: string}} [coverageLabel] How the panel counts its lines and says what a click does (default: sequences, "click a line for its photos").
|
||||
|
||||
Reference in new issue
Block a user