mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
Preserve custom Street Level provider switches in shared state
This commit is contained in:
1 parent
3b840ebdf3
commit
7b1b2df764
11 files changed
+238
-9
No files matched your search
@@ -1,5 +1,8 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
- Custom Street Level provider switches survive share links and stored state
|
||||||
|
using stable provider IDs; existing Mapillary links keep their meaning.
|
||||||
|
|
||||||
- Directions and Recent Imagery accept replaceable acquisition sources, including
|
- Directions and Recent Imagery accept replaceable acquisition sources, including
|
||||||
route transport, imagery catalog, thumbnails, tile templates and attribution.
|
route transport, imagery catalog, thumbnails, tile templates and attribution.
|
||||||
Explicitly removing either source leaves its layer unavailable; standalone
|
Explicitly removing either source leaves its layer unavailable; standalone
|
||||||
|
|||||||
@@ -24,8 +24,13 @@ all imagery providers. Without that option, Mapillary is registered only when
|
|||||||
with a truthful no-provider state, preserving layer controls and share tokens.
|
with a truthful no-provider state, preserving layer controls and share tokens.
|
||||||
Malformed selected sources and provider definitions still fail validation.
|
Malformed selected sources and provider definitions still fail validation.
|
||||||
An explicit provider list bypasses the unused Mapillary source, including its
|
An explicit provider list bypasses the unused Mapillary source, including its
|
||||||
validation. Layer share identities are preserved; custom provider switches require
|
validation. Layer share identities and the legacy Mapillary `m` option remain
|
||||||
a corresponding serialization contract.
|
stable. Custom provider switches use the `r` option, sorted by provider ID rather
|
||||||
|
than registration order, and round-trip through links and stored state. A receiver
|
||||||
|
applies only switches for its registered providers; absent custom switches leave
|
||||||
|
current defaults unchanged. The custom switch field is bounded to 256 characters;
|
||||||
|
provider registration rejects compositions that cannot fit rather than truncating
|
||||||
|
identities. Existing whole-link size limits still apply.
|
||||||
|
|
||||||
## God's Eye View in conversations — October 2, 2026
|
## God's Eye View in conversations — October 2, 2026
|
||||||
|
|
||||||
|
|||||||
@@ -552,6 +552,7 @@
|
|||||||
"src/contextModePolicy.js",
|
"src/contextModePolicy.js",
|
||||||
"src/data/detectionPolicy.js",
|
"src/data/detectionPolicy.js",
|
||||||
"src/data/installationFeedback.js",
|
"src/data/installationFeedback.js",
|
||||||
|
"src/layers/streetLevel/providerSwitches.js",
|
||||||
"src/data/layerState.js",
|
"src/data/layerState.js",
|
||||||
"src/data/layerStateCoordinator.js",
|
"src/data/layerStateCoordinator.js",
|
||||||
"src/data/layerStateTokenReservations.json",
|
"src/data/layerStateTokenReservations.json",
|
||||||
@@ -800,6 +801,7 @@
|
|||||||
"src/data/cockpitContactDot.js",
|
"src/data/cockpitContactDot.js",
|
||||||
"src/data/contactMatch.js",
|
"src/data/contactMatch.js",
|
||||||
"src/data/iconOrientation.js",
|
"src/data/iconOrientation.js",
|
||||||
|
"src/layers/streetLevel/providerSwitches.js",
|
||||||
"src/data/layerState.js",
|
"src/data/layerState.js",
|
||||||
"src/data/layerStateTokenReservations.json",
|
"src/data/layerStateTokenReservations.json",
|
||||||
"src/data/modelVisualAnchor.js",
|
"src/data/modelVisualAnchor.js",
|
||||||
@@ -842,6 +844,7 @@
|
|||||||
"src/data/cockpitContactDot.js",
|
"src/data/cockpitContactDot.js",
|
||||||
"src/data/contactMatch.js",
|
"src/data/contactMatch.js",
|
||||||
"src/data/iconOrientation.js",
|
"src/data/iconOrientation.js",
|
||||||
|
"src/layers/streetLevel/providerSwitches.js",
|
||||||
"src/data/layerState.js",
|
"src/data/layerState.js",
|
||||||
"src/data/layerStateTokenReservations.json",
|
"src/data/layerStateTokenReservations.json",
|
||||||
"src/data/modelVisualAnchor.js",
|
"src/data/modelVisualAnchor.js",
|
||||||
@@ -879,6 +882,7 @@
|
|||||||
"src/data/cockpitContactDot.js",
|
"src/data/cockpitContactDot.js",
|
||||||
"src/data/contactMatch.js",
|
"src/data/contactMatch.js",
|
||||||
"src/data/iconOrientation.js",
|
"src/data/iconOrientation.js",
|
||||||
|
"src/layers/streetLevel/providerSwitches.js",
|
||||||
"src/data/layerState.js",
|
"src/data/layerState.js",
|
||||||
"src/data/layerStateCoordinator.js",
|
"src/data/layerStateCoordinator.js",
|
||||||
"src/data/layerStateTokenReservations.json",
|
"src/data/layerStateTokenReservations.json",
|
||||||
@@ -1204,6 +1208,7 @@
|
|||||||
"src/layers/streetLevel/groundCast.js",
|
"src/layers/streetLevel/groundCast.js",
|
||||||
"src/layers/streetLevel/meshSampler.js",
|
"src/layers/streetLevel/meshSampler.js",
|
||||||
"src/layers/streetLevel/pickRouter.js",
|
"src/layers/streetLevel/pickRouter.js",
|
||||||
|
"src/layers/streetLevel/providerSwitches.js",
|
||||||
"src/layers/streetLevel/params.js",
|
"src/layers/streetLevel/params.js",
|
||||||
"src/layers/streetLevel/registry.js",
|
"src/layers/streetLevel/registry.js",
|
||||||
"src/layers/streetLevel/filter.js",
|
"src/layers/streetLevel/filter.js",
|
||||||
@@ -1249,6 +1254,7 @@
|
|||||||
"exports": ["./layers/radio"],
|
"exports": ["./layers/radio"],
|
||||||
"modules": [
|
"modules": [
|
||||||
"src/data/iconOrientation.js",
|
"src/data/iconOrientation.js",
|
||||||
|
"src/layers/streetLevel/providerSwitches.js",
|
||||||
"src/data/layerState.js",
|
"src/data/layerState.js",
|
||||||
"src/data/layerStateTokenReservations.json",
|
"src/data/layerStateTokenReservations.json",
|
||||||
"src/data/radioCountry.js",
|
"src/data/radioCountry.js",
|
||||||
@@ -1537,6 +1543,7 @@
|
|||||||
"src/data/installationFeedback.js",
|
"src/data/installationFeedback.js",
|
||||||
"src/data/issPass.js",
|
"src/data/issPass.js",
|
||||||
"src/data/labelArbiter.js",
|
"src/data/labelArbiter.js",
|
||||||
|
"src/layers/streetLevel/providerSwitches.js",
|
||||||
"src/data/layerState.js",
|
"src/data/layerState.js",
|
||||||
"src/data/layerStateCoordinator.js",
|
"src/data/layerStateCoordinator.js",
|
||||||
"src/data/layerStateTokenReservations.json",
|
"src/data/layerStateTokenReservations.json",
|
||||||
@@ -2286,6 +2293,7 @@
|
|||||||
"src/data/issPass.js",
|
"src/data/issPass.js",
|
||||||
"src/data/satellitePass.js",
|
"src/data/satellitePass.js",
|
||||||
"src/data/labelArbiter.js",
|
"src/data/labelArbiter.js",
|
||||||
|
"src/layers/streetLevel/providerSwitches.js",
|
||||||
"src/data/layerState.js",
|
"src/data/layerState.js",
|
||||||
"src/data/layerStateTokenReservations.json",
|
"src/data/layerStateTokenReservations.json",
|
||||||
"src/data/localGeojson.js",
|
"src/data/localGeojson.js",
|
||||||
@@ -2807,6 +2815,7 @@
|
|||||||
"exports": ["./ui/share-restoration"],
|
"exports": ["./ui/share-restoration"],
|
||||||
"modules": [
|
"modules": [
|
||||||
"src/data/installationFeedback.js",
|
"src/data/installationFeedback.js",
|
||||||
|
"src/layers/streetLevel/providerSwitches.js",
|
||||||
"src/data/layerState.js",
|
"src/data/layerState.js",
|
||||||
"src/data/layerStateCoordinator.js",
|
"src/data/layerStateCoordinator.js",
|
||||||
"src/data/layerStateTokenReservations.json",
|
"src/data/layerStateTokenReservations.json",
|
||||||
@@ -3327,6 +3336,7 @@
|
|||||||
"src/tools/queries/brief.js",
|
"src/tools/queries/brief.js",
|
||||||
"src/tools/queries/maritime.js",
|
"src/tools/queries/maritime.js",
|
||||||
"src/tools/queries/share.js",
|
"src/tools/queries/share.js",
|
||||||
|
"src/layers/streetLevel/providerSwitches.js",
|
||||||
"src/data/layerState.js",
|
"src/data/layerState.js",
|
||||||
"src/data/layerStateTokenReservations.json",
|
"src/data/layerStateTokenReservations.json",
|
||||||
"src/sources/gbfsStations.js",
|
"src/sources/gbfsStations.js",
|
||||||
@@ -3449,6 +3459,7 @@
|
|||||||
"exports": ["./view"],
|
"exports": ["./view"],
|
||||||
"modules": [
|
"modules": [
|
||||||
"src/view/index.js",
|
"src/view/index.js",
|
||||||
|
"src/layers/streetLevel/providerSwitches.js",
|
||||||
"src/data/layerState.js",
|
"src/data/layerState.js",
|
||||||
"src/data/layerStateTokenReservations.json"
|
"src/data/layerStateTokenReservations.json"
|
||||||
],
|
],
|
||||||
|
|||||||
+10
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import { normalizeProviderSwitches } from '../layers/streetLevel/providerSwitches.js';
|
||||||
import reservationRows from './layerStateTokenReservations.json' with { type: 'json' };
|
import reservationRows from './layerStateTokenReservations.json' with { type: 'json' };
|
||||||
|
|
||||||
const VALID_DISPOSITIONS = new Set([
|
const VALID_DISPOSITIONS = new Set([
|
||||||
@@ -411,8 +412,16 @@ const OPTION_GROUPS = Object.freeze({
|
|||||||
booleanOption('viirs', 'v', false),
|
booleanOption('viirs', 'v', false),
|
||||||
]),
|
]),
|
||||||
'street-level': Object.freeze([
|
'street-level': Object.freeze([
|
||||||
// One switch per registered imagery provider (src/app/layers/streetLevel.js).
|
// Preserve the published Mapillary token; custom switches use stable IDs.
|
||||||
booleanOption('mapillary', 'm', true),
|
booleanOption('mapillary', 'm', true),
|
||||||
|
Object.freeze({
|
||||||
|
key: 'providerSwitches',
|
||||||
|
token: 'r',
|
||||||
|
defaultValue: '',
|
||||||
|
normalize: normalizeProviderSwitches,
|
||||||
|
encode: normalizeProviderSwitches,
|
||||||
|
decode: normalizeProviderSwitches,
|
||||||
|
}),
|
||||||
enumOption('pano', 'p', 'all', ['all', 'pano', 'flat'], {
|
enumOption('pano', 'p', 'all', ['all', 'pano', 'flat'], {
|
||||||
all: 'a',
|
all: 'a',
|
||||||
pano: 'p',
|
pano: 'p',
|
||||||
|
|||||||
@@ -2660,12 +2660,14 @@ test('Street Level: every option round-trips through a share link and stored sta
|
|||||||
state.enabledLayerIds = ['street-level'];
|
state.enabledLayerIds = ['street-level'];
|
||||||
state.options['street-level'] = {
|
state.options['street-level'] = {
|
||||||
mapillary: false,
|
mapillary: false,
|
||||||
|
providerSwitches: '',
|
||||||
pano: 'flat',
|
pano: 'flat',
|
||||||
sinceDays: MAX_SINCE_DAYS,
|
sinceDays: MAX_SINCE_DAYS,
|
||||||
};
|
};
|
||||||
const decoded = decodeLayerStateParams(new URLSearchParams(encode(state)));
|
const decoded = decodeLayerStateParams(new URLSearchParams(encode(state)));
|
||||||
assert.deepEqual(decoded.options['street-level'], {
|
assert.deepEqual(decoded.options['street-level'], {
|
||||||
mapillary: false,
|
mapillary: false,
|
||||||
|
providerSwitches: '',
|
||||||
pano: 'flat',
|
pano: 'flat',
|
||||||
sinceDays: MAX_SINCE_DAYS,
|
sinceDays: MAX_SINCE_DAYS,
|
||||||
});
|
});
|
||||||
@@ -2693,6 +2695,7 @@ test('Street Level: the link codec accepts exactly the windows the filter keeps'
|
|||||||
test('Street Level: a link without the provider switch keeps Mapillary on', () => {
|
test('Street Level: a link without the provider switch keeps Mapillary on', () => {
|
||||||
assert.deepEqual(streetLevelOptions(''), {
|
assert.deepEqual(streetLevelOptions(''), {
|
||||||
mapillary: true,
|
mapillary: true,
|
||||||
|
providerSwitches: '',
|
||||||
pano: 'all',
|
pano: 'all',
|
||||||
sinceDays: 0,
|
sinceDays: 0,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -98,6 +98,17 @@ function withPublishedBase(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function writeCodecFixture(cwd, rows, entries) {
|
function writeCodecFixture(cwd, rows, entries) {
|
||||||
|
const providerCodec = path.join(
|
||||||
|
cwd,
|
||||||
|
'src/layers/streetLevel/providerSwitches.js',
|
||||||
|
);
|
||||||
|
mkdirSync(path.dirname(providerCodec), { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
providerCodec,
|
||||||
|
readFileSync(
|
||||||
|
new URL('../layers/streetLevel/providerSwitches.js', import.meta.url),
|
||||||
|
),
|
||||||
|
);
|
||||||
const source = readFileSync(CODEC_SOURCE_PATH, 'utf8');
|
const source = readFileSync(CODEC_SOURCE_PATH, 'utf8');
|
||||||
const registryEnd = ']);\n\nexport const REGISTERED_LAYER_IDS';
|
const registryEnd = ']);\n\nexport const REGISTERED_LAYER_IDS';
|
||||||
assert.equal(source.split(registryEnd).length, 2);
|
assert.equal(source.split(registryEnd).length, 2);
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import { normalizeFilter } from './filter.js';
|
import { normalizeFilter } from './filter.js';
|
||||||
|
import {
|
||||||
|
encodeProviderSwitches,
|
||||||
|
decodeProviderSwitches,
|
||||||
|
} from './providerSwitches.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Share-link params: one boolean per provider plus the filter. The codec in
|
* Provider params plus the filter. Custom switches share a stable ID-based
|
||||||
* src/data/layerState.js names the same keys, so a new provider needs one there.
|
* field; Mapillary retains its existing boolean and share token.
|
||||||
* @param {{providers: Iterable<[string, boolean]>, filter: {pano: string, sinceDays: number}}} input
|
* @param {{providers: Iterable<[string, boolean]>, filter: {pano: string, sinceDays: number}}} input
|
||||||
*/
|
*/
|
||||||
export function encodeParams({ providers, filter }) {
|
export function encodeParams({ providers, filter }) {
|
||||||
const params = {};
|
const params = {};
|
||||||
for (const [id, on] of providers) params[id] = on === true;
|
const entries = [...providers];
|
||||||
|
for (const [id, on] of entries) params[id] = on === true;
|
||||||
|
const switches = encodeProviderSwitches(entries);
|
||||||
|
if (switches) params.providerSwitches = switches;
|
||||||
params.pano = filter.pano;
|
params.pano = filter.pano;
|
||||||
params.sinceDays = filter.sinceDays;
|
params.sinceDays = filter.sinceDays;
|
||||||
return params;
|
return params;
|
||||||
@@ -23,8 +30,12 @@ export function encodeParams({ providers, filter }) {
|
|||||||
export function decodeParams(params, { providerIds, filter }) {
|
export function decodeParams(params, { providerIds, filter }) {
|
||||||
const providers = new Map();
|
const providers = new Map();
|
||||||
const source = params && typeof params === 'object' ? params : {};
|
const source = params && typeof params === 'object' ? params : {};
|
||||||
for (const id of providerIds)
|
const switches = decodeProviderSwitches(source.providerSwitches);
|
||||||
if (typeof source[id] === 'boolean') providers.set(id, source[id]);
|
for (const id of providerIds) {
|
||||||
|
if (switches.has(id)) providers.set(id, switches.get(id));
|
||||||
|
else if (Object.hasOwn(source, id) && typeof source[id] === 'boolean')
|
||||||
|
providers.set(id, source[id]);
|
||||||
|
}
|
||||||
const next = {};
|
const next = {};
|
||||||
if ('pano' in source) next.pano = source.pano;
|
if ('pano' in source) next.pano = source.pano;
|
||||||
if ('sinceDays' in source) next.sinceDays = source.sinceDays;
|
if ('sinceDays' in source) next.sinceDays = source.sinceDays;
|
||||||
|
|||||||
@@ -11,7 +11,13 @@ test('encodeParams writes one boolean per provider plus the filter', () => {
|
|||||||
],
|
],
|
||||||
filter: { pano: 'flat', sinceDays: 365 },
|
filter: { pano: 'flat', sinceDays: 365 },
|
||||||
}),
|
}),
|
||||||
{ mapillary: true, panoramax: false, pano: 'flat', sinceDays: 365 },
|
{
|
||||||
|
mapillary: true,
|
||||||
|
panoramax: false,
|
||||||
|
providerSwitches: 'panoramax=0',
|
||||||
|
pano: 'flat',
|
||||||
|
sinceDays: 365,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
const MAX_SWITCHES_LENGTH = 256;
|
||||||
|
const ENTRY = /^([a-z][a-z0-9-]*)=([01])$/;
|
||||||
|
|
||||||
|
/** Canonical, bounded provider switches; Mapillary retains its legacy token. */
|
||||||
|
export function normalizeProviderSwitches(value) {
|
||||||
|
if (typeof value !== 'string' || value.length > MAX_SWITCHES_LENGTH)
|
||||||
|
return null;
|
||||||
|
if (!value) return '';
|
||||||
|
const entries = new Map();
|
||||||
|
for (const field of value.split(',')) {
|
||||||
|
const match = ENTRY.exec(field);
|
||||||
|
if (!match || match[1] === 'mapillary' || entries.has(match[1]))
|
||||||
|
return null;
|
||||||
|
entries.set(match[1], match[2] === '1');
|
||||||
|
}
|
||||||
|
return [...entries]
|
||||||
|
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
||||||
|
.map(([id, on]) => `${id}=${on ? '1' : '0'}`)
|
||||||
|
.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Encode by stable IDs rather than registration order or generated tokens. */
|
||||||
|
export function encodeProviderSwitches(providers) {
|
||||||
|
const value = [...providers]
|
||||||
|
.filter(([id]) => id !== 'mapillary')
|
||||||
|
.map(([id, on]) => `${id}=${on === true ? '1' : '0'}`)
|
||||||
|
.join(',');
|
||||||
|
const normalized = normalizeProviderSwitches(value);
|
||||||
|
if (normalized === null)
|
||||||
|
throw new TypeError('Invalid or oversized Street Level provider switches');
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A Map keeps provider IDs separate from object prototype properties. */
|
||||||
|
export function decodeProviderSwitches(value) {
|
||||||
|
const normalized = normalizeProviderSwitches(value);
|
||||||
|
return new Map(
|
||||||
|
normalized
|
||||||
|
? normalized.split(',').map((entry) => {
|
||||||
|
const [id, on] = entry.split('=');
|
||||||
|
return [id, on === '1'];
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { encodeParams, decodeParams } from './params.js';
|
||||||
|
import {
|
||||||
|
normalizeProviderSwitches,
|
||||||
|
encodeProviderSwitches,
|
||||||
|
} from './providerSwitches.js';
|
||||||
|
import {
|
||||||
|
createDefaultLayerState,
|
||||||
|
encodeLayerStateParams,
|
||||||
|
decodeLayerStateParams,
|
||||||
|
serializeStoredLayerState,
|
||||||
|
parseStoredLayerState,
|
||||||
|
} from '../../data/layerState.js';
|
||||||
|
import { validateProviders } from './registry.js';
|
||||||
|
import { fakeStreetLevelProvider } from '../../testSupport/streetLevelFakes.mjs';
|
||||||
|
|
||||||
|
const filter = { pano: 'flat', sinceDays: 365 };
|
||||||
|
|
||||||
|
test('custom switches survive sharing and storage without depending on provider order', () => {
|
||||||
|
const providers = [
|
||||||
|
['panoramax', false],
|
||||||
|
['example', true],
|
||||||
|
['mapillary', false],
|
||||||
|
];
|
||||||
|
const options = encodeParams({ providers, filter });
|
||||||
|
assert.equal(options.providerSwitches, 'example=1,panoramax=0');
|
||||||
|
assert.equal(
|
||||||
|
encodeParams({ providers: [...providers].reverse(), filter })
|
||||||
|
.providerSwitches,
|
||||||
|
options.providerSwitches,
|
||||||
|
);
|
||||||
|
const state = createDefaultLayerState();
|
||||||
|
state.enabledLayerIds = ['street-level'];
|
||||||
|
state.options['street-level'] = options;
|
||||||
|
const params = new URLSearchParams({ v: '2' });
|
||||||
|
encodeLayerStateParams(params, state);
|
||||||
|
assert.match(params.get('lo'), /0\.m\.0/);
|
||||||
|
assert.match(params.get('lo'), /0\.r\.example=1,panoramax=0/);
|
||||||
|
for (const restored of [
|
||||||
|
decodeLayerStateParams(params),
|
||||||
|
parseStoredLayerState(serializeStoredLayerState(state)),
|
||||||
|
]) {
|
||||||
|
const decoded = decodeParams(restored.options['street-level'], {
|
||||||
|
providerIds: ['example', 'mapillary', 'panoramax'],
|
||||||
|
filter: { pano: 'all', sinceDays: 0 },
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
[...decoded.providers],
|
||||||
|
[
|
||||||
|
['example', true],
|
||||||
|
['mapillary', false],
|
||||||
|
['panoramax', false],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert.deepEqual(decoded.filter, filter);
|
||||||
|
assert.deepEqual(
|
||||||
|
[
|
||||||
|
...decodeParams(restored.options['street-level'], {
|
||||||
|
providerIds: ['panoramax'],
|
||||||
|
filter,
|
||||||
|
}).providers,
|
||||||
|
],
|
||||||
|
[['panoramax', false]],
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
decodeParams(restored.options['street-level'], {
|
||||||
|
providerIds: [],
|
||||||
|
filter,
|
||||||
|
}).providers.size,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('legacy links retain Mapillary semantics and leave custom defaults alone', () => {
|
||||||
|
for (const [lo, on] of [
|
||||||
|
['', true],
|
||||||
|
['0.m.0', false],
|
||||||
|
]) {
|
||||||
|
const state = decodeLayerStateParams(
|
||||||
|
new URLSearchParams({ v: '2', l: '0', lo }),
|
||||||
|
);
|
||||||
|
const restored = decodeParams(state.options['street-level'], {
|
||||||
|
providerIds: ['mapillary', 'example'],
|
||||||
|
filter,
|
||||||
|
});
|
||||||
|
assert.deepEqual([...restored.providers], [['mapillary', on]]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('switches reject ambiguous or oversized fields and cannot override the legacy token', () => {
|
||||||
|
for (const value of [
|
||||||
|
'example=0,example=1',
|
||||||
|
'mapillary=0',
|
||||||
|
'__proto__=1',
|
||||||
|
'a=2',
|
||||||
|
'a=1,',
|
||||||
|
'a.b=1',
|
||||||
|
'a_b=0',
|
||||||
|
`${'a'.repeat(255)}=1`,
|
||||||
|
null,
|
||||||
|
{},
|
||||||
|
]) {
|
||||||
|
assert.equal(normalizeProviderSwitches(value), null);
|
||||||
|
}
|
||||||
|
assert.equal(normalizeProviderSwitches('constructor=0'), 'constructor=0');
|
||||||
|
assert.throws(
|
||||||
|
() => encodeProviderSwitches([['a'.repeat(255), true]]),
|
||||||
|
/oversized/,
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => validateProviders([fakeStreetLevelProvider({ id: 'a'.repeat(255) })]),
|
||||||
|
/oversized/,
|
||||||
|
);
|
||||||
|
const decoded = decodeParams(
|
||||||
|
{ providerSwitches: 'mapillary=0', mapillary: true },
|
||||||
|
{ providerIds: ['mapillary'], filter },
|
||||||
|
);
|
||||||
|
assert.equal(decoded.providers.get('mapillary'), true);
|
||||||
|
});
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { encodeProviderSwitches } from './providerSwitches.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The contract an imagery provider registers with the Street Level layer.
|
* The contract an imagery provider registers with the Street Level layer.
|
||||||
*
|
*
|
||||||
@@ -125,6 +127,8 @@ export function validateProviders(providers) {
|
|||||||
ids.add(provider.id);
|
ids.add(provider.id);
|
||||||
prefixes.push(provider.pickPrefix);
|
prefixes.push(provider.pickPrefix);
|
||||||
}
|
}
|
||||||
|
// Validate the complete bounded share field once, before any instance exists.
|
||||||
|
encodeProviderSwitches(providers.map(({ id }) => [id, true]));
|
||||||
return Object.freeze([...providers]);
|
return Object.freeze([...providers]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user