mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Use a neutral compact switch codec and clarify provider size limits
This commit is contained in:
1 parent
fbe118aab9
commit
3ba8c6d61b
11 files changed
+127
-98
No files matched your search
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
- Custom Street Level provider switches survive share links and stored state
|
- Custom Street Level provider switches survive share links and stored state
|
||||||
using stable provider IDs; existing Mapillary links keep their meaning.
|
using URL-safe stable provider IDs and a shared boolean-switch codec; 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.
|
||||||
|
|||||||
@@ -27,12 +27,14 @@ 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 and the legacy Mapillary `m` option remain
|
validation. Layer share identities and the legacy Mapillary `m` option remain
|
||||||
stable. Custom provider switches use the `r` option, sorted by provider ID rather
|
stable. Custom provider switches use the `r` option (`example-1*panoramax-0`),
|
||||||
|
with URL-safe separators and a shared layer-neutral boolean-switch codec, sorted by provider ID rather
|
||||||
than registration order, and round-trip through links and stored state. A receiver
|
than registration order, and round-trip through links and stored state. A receiver
|
||||||
applies only switches for its registered providers; absent custom switches leave
|
applies only switches for its registered providers; absent custom switches leave
|
||||||
current defaults unchanged. The custom switch field is bounded to 256 characters;
|
current defaults unchanged. The custom switch field is bounded to 256 characters;
|
||||||
provider registration rejects compositions that cannot fit rather than truncating
|
provider registration rejects compositions that cannot fit rather than truncating
|
||||||
identities. Existing whole-link size limits still apply.
|
identities. A malformed or duplicate entry invalidates the complete custom-switch
|
||||||
|
field. 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,7 +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/idSwitches.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",
|
||||||
@@ -801,7 +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/idSwitches.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",
|
||||||
@@ -844,7 +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/idSwitches.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",
|
||||||
@@ -882,7 +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/idSwitches.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",
|
||||||
@@ -1208,7 +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/data/idSwitches.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",
|
||||||
@@ -1254,7 +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/idSwitches.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",
|
||||||
@@ -1543,7 +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/idSwitches.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",
|
||||||
@@ -2293,7 +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/idSwitches.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",
|
||||||
@@ -2815,7 +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/idSwitches.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",
|
||||||
@@ -3336,7 +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/idSwitches.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",
|
||||||
@@ -3459,7 +3459,7 @@
|
|||||||
"exports": ["./view"],
|
"exports": ["./view"],
|
||||||
"modules": [
|
"modules": [
|
||||||
"src/view/index.js",
|
"src/view/index.js",
|
||||||
"src/layers/streetLevel/providerSwitches.js",
|
"src/data/idSwitches.js",
|
||||||
"src/data/layerState.js",
|
"src/data/layerState.js",
|
||||||
"src/data/layerStateTokenReservations.json"
|
"src/data/layerStateTokenReservations.json"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
const MAX_SWITCHES_LENGTH = 256;
|
||||||
|
const ENTRY = /^([a-z][a-z0-9-]*)-([01])$/;
|
||||||
|
|
||||||
|
/** Canonical, bounded boolean switches keyed by stable slugs. */
|
||||||
|
export function normalizeIdSwitches(value, reservedIds = []) {
|
||||||
|
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 || reservedIds.includes(match[1]) || 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 encodeIdSwitches(switches) {
|
||||||
|
const value = [...switches]
|
||||||
|
.map(([id, on]) => `${id}-${on === true ? '1' : '0'}`)
|
||||||
|
.join('*');
|
||||||
|
if (value.length > MAX_SWITCHES_LENGTH)
|
||||||
|
throw new RangeError(
|
||||||
|
`Switch IDs exceed the ${MAX_SWITCHES_LENGTH}-character share-link limit`,
|
||||||
|
);
|
||||||
|
const normalized = normalizeIdSwitches(value);
|
||||||
|
if (normalized === null) throw new TypeError('Invalid boolean switch IDs');
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A Map keeps switch IDs separate from object prototype properties. */
|
||||||
|
export function decodeIdSwitches(value) {
|
||||||
|
const normalized = normalizeIdSwitches(value);
|
||||||
|
return new Map(
|
||||||
|
normalized
|
||||||
|
? normalized.split('*').map((entry) => {
|
||||||
|
const [, id, on] = ENTRY.exec(entry);
|
||||||
|
return [id, on === '1'];
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { normalizeProviderSwitches } from '../layers/streetLevel/providerSwitches.js';
|
import { normalizeIdSwitches } from './idSwitches.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([
|
||||||
@@ -418,9 +418,9 @@ const OPTION_GROUPS = Object.freeze({
|
|||||||
key: 'providerSwitches',
|
key: 'providerSwitches',
|
||||||
token: 'r',
|
token: 'r',
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
normalize: normalizeProviderSwitches,
|
normalize: (value) => normalizeIdSwitches(value, ['mapillary']),
|
||||||
encode: normalizeProviderSwitches,
|
encode: normalizeIdSwitches,
|
||||||
decode: normalizeProviderSwitches,
|
decode: (value) => normalizeIdSwitches(value, ['mapillary']),
|
||||||
}),
|
}),
|
||||||
enumOption('pano', 'p', 'all', ['all', 'pano', 'flat'], {
|
enumOption('pano', 'p', 'all', ['all', 'pano', 'flat'], {
|
||||||
all: 'a',
|
all: 'a',
|
||||||
|
|||||||
@@ -98,16 +98,11 @@ function withPublishedBase(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function writeCodecFixture(cwd, rows, entries) {
|
function writeCodecFixture(cwd, rows, entries) {
|
||||||
const providerCodec = path.join(
|
const providerCodec = path.join(cwd, 'src/data/idSwitches.js');
|
||||||
cwd,
|
|
||||||
'src/layers/streetLevel/providerSwitches.js',
|
|
||||||
);
|
|
||||||
mkdirSync(path.dirname(providerCodec), { recursive: true });
|
mkdirSync(path.dirname(providerCodec), { recursive: true });
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
providerCodec,
|
providerCodec,
|
||||||
readFileSync(
|
readFileSync(new URL('./idSwitches.js', import.meta.url)),
|
||||||
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';
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { normalizeFilter } from './filter.js';
|
import { normalizeFilter } from './filter.js';
|
||||||
import {
|
import { encodeIdSwitches, decodeIdSwitches } from '../../data/idSwitches.js';
|
||||||
encodeProviderSwitches,
|
|
||||||
decodeProviderSwitches,
|
|
||||||
} from './providerSwitches.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider params plus the filter. Custom switches share a stable ID-based
|
* Provider params plus the filter. Custom switches share a stable ID-based
|
||||||
@@ -13,7 +10,9 @@ export function encodeParams({ providers, filter }) {
|
|||||||
const params = {};
|
const params = {};
|
||||||
const entries = [...providers];
|
const entries = [...providers];
|
||||||
for (const [id, on] of entries) params[id] = on === true;
|
for (const [id, on] of entries) params[id] = on === true;
|
||||||
const switches = encodeProviderSwitches(entries);
|
const switches = encodeIdSwitches(
|
||||||
|
entries.filter(([id]) => id !== 'mapillary'),
|
||||||
|
);
|
||||||
if (switches) params.providerSwitches = switches;
|
if (switches) params.providerSwitches = switches;
|
||||||
params.pano = filter.pano;
|
params.pano = filter.pano;
|
||||||
params.sinceDays = filter.sinceDays;
|
params.sinceDays = filter.sinceDays;
|
||||||
@@ -30,7 +29,8 @@ 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 : {};
|
||||||
const switches = decodeProviderSwitches(source.providerSwitches);
|
const switches = decodeIdSwitches(source.providerSwitches);
|
||||||
|
switches.delete('mapillary');
|
||||||
for (const id of providerIds) {
|
for (const id of providerIds) {
|
||||||
if (switches.has(id)) providers.set(id, switches.get(id));
|
if (switches.has(id)) providers.set(id, switches.get(id));
|
||||||
else if (Object.hasOwn(source, id) && typeof source[id] === 'boolean')
|
else if (Object.hasOwn(source, id) && typeof source[id] === 'boolean')
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ test('encodeParams writes one boolean per provider plus the filter', () => {
|
|||||||
{
|
{
|
||||||
mapillary: true,
|
mapillary: true,
|
||||||
panoramax: false,
|
panoramax: false,
|
||||||
providerSwitches: 'panoramax=0',
|
providerSwitches: 'panoramax-0',
|
||||||
pano: 'flat',
|
pano: 'flat',
|
||||||
sinceDays: 365,
|
sinceDays: 365,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
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'];
|
|
||||||
})
|
|
||||||
: [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,10 @@ import test from 'node:test';
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { encodeParams, decodeParams } from './params.js';
|
import { encodeParams, decodeParams } from './params.js';
|
||||||
import {
|
import {
|
||||||
normalizeProviderSwitches,
|
normalizeIdSwitches,
|
||||||
encodeProviderSwitches,
|
decodeIdSwitches,
|
||||||
} from './providerSwitches.js';
|
encodeIdSwitches,
|
||||||
|
} from '../../data/idSwitches.js';
|
||||||
import {
|
import {
|
||||||
createDefaultLayerState,
|
createDefaultLayerState,
|
||||||
encodeLayerStateParams,
|
encodeLayerStateParams,
|
||||||
@@ -24,7 +25,7 @@ test('custom switches survive sharing and storage without depending on provider
|
|||||||
['mapillary', false],
|
['mapillary', false],
|
||||||
];
|
];
|
||||||
const options = encodeParams({ providers, filter });
|
const options = encodeParams({ providers, filter });
|
||||||
assert.equal(options.providerSwitches, 'example=1,panoramax=0');
|
assert.equal(options.providerSwitches, 'example-1*panoramax-0');
|
||||||
assert.equal(
|
assert.equal(
|
||||||
encodeParams({ providers: [...providers].reverse(), filter })
|
encodeParams({ providers: [...providers].reverse(), filter })
|
||||||
.providerSwitches,
|
.providerSwitches,
|
||||||
@@ -36,7 +37,11 @@ test('custom switches survive sharing and storage without depending on provider
|
|||||||
const params = new URLSearchParams({ v: '2' });
|
const params = new URLSearchParams({ v: '2' });
|
||||||
encodeLayerStateParams(params, state);
|
encodeLayerStateParams(params, state);
|
||||||
assert.match(params.get('lo'), /0\.m\.0/);
|
assert.match(params.get('lo'), /0\.m\.0/);
|
||||||
assert.match(params.get('lo'), /0\.r\.example=1,panoramax=0/);
|
assert.match(params.get('lo'), /0\.r\.example-1\*panoramax-0/);
|
||||||
|
assert.ok(
|
||||||
|
params.toString().includes('0.r.example-1*panoramax-0'),
|
||||||
|
'custom switches need no percent escaping',
|
||||||
|
);
|
||||||
for (const restored of [
|
for (const restored of [
|
||||||
decodeLayerStateParams(params),
|
decodeLayerStateParams(params),
|
||||||
parseStoredLayerState(serializeStoredLayerState(state)),
|
parseStoredLayerState(serializeStoredLayerState(state)),
|
||||||
@@ -91,31 +96,44 @@ test('legacy links retain Mapillary semantics and leave custom defaults alone',
|
|||||||
|
|
||||||
test('switches reject ambiguous or oversized fields and cannot override the legacy token', () => {
|
test('switches reject ambiguous or oversized fields and cannot override the legacy token', () => {
|
||||||
for (const value of [
|
for (const value of [
|
||||||
'example=0,example=1',
|
'example-0*example-1',
|
||||||
'mapillary=0',
|
'mapillary-0',
|
||||||
'__proto__=1',
|
'__proto__-1',
|
||||||
'a=2',
|
'a-2',
|
||||||
'a=1,',
|
'a-1*',
|
||||||
'a.b=1',
|
'a.b-1',
|
||||||
'a_b=0',
|
'a_b-0',
|
||||||
`${'a'.repeat(255)}=1`,
|
`${'a'.repeat(255)}-1`,
|
||||||
null,
|
null,
|
||||||
{},
|
{},
|
||||||
]) {
|
]) {
|
||||||
assert.equal(normalizeProviderSwitches(value), null);
|
assert.equal(normalizeIdSwitches(value, ['mapillary']), null);
|
||||||
}
|
}
|
||||||
assert.equal(normalizeProviderSwitches('constructor=0'), 'constructor=0');
|
assert.equal(normalizeIdSwitches('constructor-0'), 'constructor-0');
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => encodeProviderSwitches([['a'.repeat(255), true]]),
|
() => encodeIdSwitches([['a'.repeat(255), true]]),
|
||||||
/oversized/,
|
/share-link/,
|
||||||
);
|
);
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => validateProviders([fakeStreetLevelProvider({ id: 'a'.repeat(255) })]),
|
() => validateProviders([fakeStreetLevelProvider({ id: 'a'.repeat(255) })]),
|
||||||
/oversized/,
|
/share-link/,
|
||||||
);
|
);
|
||||||
const decoded = decodeParams(
|
const decoded = decodeParams(
|
||||||
{ providerSwitches: 'mapillary=0', mapillary: true },
|
{ providerSwitches: 'mapillary-0', mapillary: true },
|
||||||
{ providerIds: ['mapillary'], filter },
|
{ providerIds: ['mapillary'], filter },
|
||||||
);
|
);
|
||||||
assert.equal(decoded.providers.get('mapillary'), true);
|
assert.equal(decoded.providers.get('mapillary'), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('neutral switch codec preserves hyphens and numeric suffixes in IDs', () => {
|
||||||
|
const switches = [
|
||||||
|
['provider-0', false],
|
||||||
|
['provider-1', true],
|
||||||
|
['a-b-c', true],
|
||||||
|
];
|
||||||
|
const encoded = encodeIdSwitches(switches);
|
||||||
|
assert.equal(encoded, 'a-b-c-1*provider-0-0*provider-1-1');
|
||||||
|
const decoded = decodeIdSwitches(encoded);
|
||||||
|
for (const [id, on] of switches) assert.equal(decoded.get(id), on);
|
||||||
|
assert.equal(decodeIdSwitches('valid-1*broken').size, 0);
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { encodeProviderSwitches } from './providerSwitches.js';
|
import { encodeIdSwitches } from '../../data/idSwitches.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The contract an imagery provider registers with the Street Level layer.
|
* The contract an imagery provider registers with the Street Level layer.
|
||||||
@@ -128,7 +128,18 @@ export function validateProviders(providers) {
|
|||||||
prefixes.push(provider.pickPrefix);
|
prefixes.push(provider.pickPrefix);
|
||||||
}
|
}
|
||||||
// Validate the complete bounded share field once, before any instance exists.
|
// Validate the complete bounded share field once, before any instance exists.
|
||||||
encodeProviderSwitches(providers.map(({ id }) => [id, true]));
|
try {
|
||||||
|
encodeIdSwitches(
|
||||||
|
providers
|
||||||
|
.filter(({ id }) => id !== 'mapillary')
|
||||||
|
.map(({ id }) => [id, true]),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error instanceof RangeError)) throw error;
|
||||||
|
throw new RangeError(
|
||||||
|
'Street Level provider IDs cannot fit in the 256-character share-link field; use shorter IDs or fewer providers',
|
||||||
|
);
|
||||||
|
}
|
||||||
return Object.freeze([...providers]);
|
return Object.freeze([...providers]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user