Files
gods-eye-view/src/data/layerState.js
T
Sameh Khamis f60b9ef7c8 chore: remove unused imports and local helpers
Drop module-local helpers that nothing calls (linkAbort, summarize,
stringOption, summarizeEntity and the polygonCenter, propertyObject and
unwrapProperties helpers only it used, CALLOUT_DOT_SIZE_PX) and unused
import specifiers in runtime modules, tests, QA scripts and tools.

None of the removed bindings is exported. Re-exports such as the ALPR
policy constants and every package export are unchanged.
2026-10-22 18:09:37 -07:00

1086 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Restore labels are marked with msg(); ui/shareRestoration.js translates them.
// Local, not i18n/marker.js: the token-ledger tests run this file in isolation.
const msg = (text) => text;
import { normalizeIdSwitches, patchRequestedIdSwitches } from './idSwitches.js';
import reservationRows from './layerStateTokenReservations.json' with { type: 'json' };
const VALID_DISPOSITIONS = new Set([
'enabled-only',
'enabled+options',
'enabled+mirrored-options',
]);
export const LAYER_STATE_VERSION = 2;
export const LAYER_STATE_TOKEN_ALPHABET =
'0123456789abcdefghijklmnopqrstuvwxyz';
const LAYER_STATE_TOKEN_PATTERN = /^[a-z0-9]{1,2}$/;
const NEW_SINGLE_CHARACTER_TOKEN_PATTERN = /^[0-9]$/;
/**
* A tracking ID is a transponder address, not free text: 6 hex digits for an
* ICAO24, with slack for TIS-B (`~abc123`) and similar prefixed forms. Bounding
* it at the codec keeps an arbitrarily long string out of durable state, the
* generated URL, and local storage. Identity is never TRUNCATED to fit — an
* out-of-grammar ID is rejected outright, because half an address is a
* DIFFERENT aircraft, not a shorter name for the same one.
*/
const TRACKING_ID_GRAMMAR = /^[0-9a-z~_-]{1,16}$/;
/**
* Ceilings for the untrusted v2 layer fields. The layer ceiling covers the
* complete reserved one-character space plus every two-character base-36
* allocation, while the option ceiling covers a dozen short assignments.
* Reject the WHOLE payload, matching the unknown-token rule — never salvage a
* prefix.
*/
const MAX_ENABLED_LAYERS_CHARS = 4_096;
const MAX_LAYER_OPTIONS_CHARS = 512;
export const LAYER_STATE_STORAGE_KEY = 'gev:layer-state:v2';
export const LAYER_RESTORE_ORIGINS = Object.freeze({
share: 'share-restore',
local: 'local-restore',
});
const RADIO_FILTER_CODES = Object.freeze({
all: 'a',
news: 'n',
talk: 't',
weather: 'w',
'public-safety': 'p',
'aviation-marine': 'v',
'traffic-transit': 'x',
music: 'm',
other: 'o',
});
const RADIO_CODE_FILTERS = Object.freeze(
Object.fromEntries(
Object.entries(RADIO_FILTER_CODES).map(([key, value]) => [value, key]),
),
);
function normalizeBoolean(value) {
return typeof value === 'boolean' ? value : null;
}
function normalizeEnum(values, value) {
return values.includes(value) ? value : null;
}
export function normalizeRadioFilter(value) {
const normalized = String(value || '')
.trim()
.toLowerCase();
if (Object.hasOwn(RADIO_FILTER_CODES, normalized)) return normalized;
if (/^genre:[a-z0-9][a-z0-9 &-]{0,31}$/.test(normalized)) return normalized;
return null;
}
function encodeRadioFilter(value) {
return RADIO_FILTER_CODES[value] || `g-${value.slice('genre:'.length)}`;
}
function decodeRadioFilter(value) {
if (Object.hasOwn(RADIO_CODE_FILTERS, value))
return RADIO_CODE_FILTERS[value];
if (/^g-[a-z0-9][a-z0-9 &-]{0,31}$/.test(value))
return `genre:${value.slice(2)}`;
return null;
}
function normalizeVolume(value) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return null;
return Math.round(Math.max(0, Math.min(1, numeric)) * 100) / 100;
}
function booleanOption(
key,
token,
defaultValue,
{ absentValue = defaultValue } = {},
) {
return Object.freeze({
key,
token,
defaultValue,
absentValue,
normalize: normalizeBoolean,
encode: (value) => (value ? '1' : '0'),
decode: (value) => (value === '1' ? true : value === '0' ? false : null),
});
}
/**
* What an ABSENT token means for this option inside the CURRENT schema version.
*
* Usually that is simply the default: the encoder omits default-valued fields to
* keep the URL short and the decoder fills the default back in. But a DEFAULT CAN
* MOVE while the schema version does not, and every link already in the wild was
* authored under the old one. `absentValue` is that frozen historical meaning —
* what an omitted token meant when links like it were being written — so a
* default flip cannot silently rewrite what an existing link SAYS. A share link
* is authored state; the only honest reading of `v=2&l=f` is the one its author
* saw.
*
* The consequence is not cosmetic: once `absentValue` and `defaultValue` differ,
* the NEW default has to be emitted EXPLICITLY, or one omission would mean two
* different things inside a single schema version. Both sides of the codec read
* this function so they cannot disagree about which it is.
*
* (This is the same rule `scf` follows in sharelink.js by hand. `models3d` is the
* first option in THIS codec to need it — flipped to default-ON on 2026-08-22.)
*/
function absentTokenValue(spec) {
return Object.hasOwn(spec, 'absentValue')
? spec.absentValue
: spec.defaultValue;
}
function trackingIdOption(key, token, defaultValue = null) {
const bounded = (candidate) => {
if (candidate === null || candidate === undefined) return null;
const raw =
typeof candidate === 'number' && Number.isFinite(candidate)
? String(candidate)
: typeof candidate === 'string'
? candidate
: null;
if (raw === null) return null;
const normalized = raw.trim().toLowerCase();
return TRACKING_ID_GRAMMAR.test(normalized) ? normalized : null;
};
return Object.freeze({
key,
token,
defaultValue,
normalize: bounded,
encode: (value) => String(value),
decode: bounded,
});
}
function enumOption(
key,
token,
defaultValue,
values,
codes,
{ absentValue = defaultValue } = {},
) {
const reverse = Object.fromEntries(
Object.entries(codes).map(([name, code]) => [code, name]),
);
return Object.freeze({
key,
token,
defaultValue,
absentValue,
normalize: (value) => normalizeEnum(values, value),
encode: (value) => codes[value],
decode: (value) => reverse[value] || null,
});
}
function integerOption(key, token, defaultValue) {
return Object.freeze({
key,
token,
defaultValue,
normalize: (value) => {
if (typeof value === 'number' && Number.isInteger(value) && value > 0)
return value;
const candidate = typeof value === 'string' ? value.trim() : '';
const parsed = Number(candidate);
if (!candidate || !Number.isInteger(parsed) || parsed <= 0) return null;
return parsed;
},
encode: (value) => String(Math.trunc(value)),
decode: (value) => {
const candidate = Number(value);
if (!Number.isInteger(candidate) || candidate <= 0) return null;
return candidate;
},
});
}
/**
* A share-link-only option: the whole-state codec carries it, but the stored
* local blob always holds its default.
*/
function shareOnlyOption(spec) {
return Object.freeze({ ...spec, shareOnly: true });
}
/**
* A signed integer in `[min, max]`, rejected (never clamped) outside it: a
* clamped box edge would be a different box, not a shorter spelling.
*/
function boundedIntegerOption(key, token, defaultValue, { min, max }) {
const parse = (value) => {
const number =
typeof value === 'number'
? value
: /^-?\d{1,9}$/.test(String(value).trim())
? Number(String(value).trim())
: NaN;
return Number.isInteger(number) && number >= min && number <= max
? number
: null;
};
return Object.freeze({
key,
token,
defaultValue,
normalize: parse,
encode: (value) => String(value),
decode: parse,
});
}
/**
* Recent Imagery day: runtime key `S30:2026-09-18`, URL form `S20260918`.
* The calendar is checked both ways, and "today" is never consulted, so a
* link decodes the same whenever it is opened.
*/
const IMAGERY_LETTERS = Object.freeze({ S30: 'S', L30: 'L', VIIRS: 'V' });
const IMAGERY_PRODUCTS = Object.freeze({ S: 'S30', L: 'L30', V: 'VIIRS' });
function imageryKey(product, year, month, day) {
const time = Date.UTC(Number(year), Number(month) - 1, Number(day));
const iso = Number.isFinite(time)
? new Date(time).toISOString().slice(0, 10)
: '';
return product && iso === `${year}-${month}-${day}`
? `${product}:${iso}`
: null;
}
function imageryPinOption(key, token) {
return Object.freeze({
key,
token,
defaultValue: null,
normalize: (value) => {
const match = /^(S30|L30|VIIRS):(\d{4})-(\d{2})-(\d{2})$/.exec(
typeof value === 'string' ? value.trim() : '',
);
return match ? imageryKey(match[1], match[2], match[3], match[4]) : null;
},
encode: (value) => {
const [product, day] = value.split(':');
return `${IMAGERY_LETTERS[product]}${day.replaceAll('-', '')}`;
},
decode: (value) => {
const match = /^([SLV])(\d{4})(\d{2})(\d{2})$/.exec(
typeof value === 'string' ? value : '',
);
return match
? imageryKey(IMAGERY_PRODUCTS[match[1]], match[2], match[3], match[4])
: null;
},
});
}
function satelliteFilterOption() {
const percentage = boundedIntegerOption('infrared', 'i', 100, {
min: 0,
max: 100,
});
return Object.freeze({
...percentage,
normalize: (value) =>
value === 'filtered'
? 100
: value === 'full'
? 0
: percentage.normalize(value),
decode: (value) =>
value === 'f' ? 100 : value === 'a' ? 0 : percentage.decode(value),
});
}
const OPTION_GROUPS = Object.freeze({
traffic: Object.freeze([
enumOption('roadMode', 'r', null, ['tomtom', 'osm', 'hybrid'], {
tomtom: 't',
osm: 'o',
hybrid: 'h',
}),
]),
'weather-lightning': Object.freeze([
enumOption('opacity', 'o', 'strong', ['light', 'strong'], {
light: 'l',
strong: 's',
}),
]),
'weather-radar': Object.freeze([
enumOption('opacity', 'o', 'strong', ['light', 'strong'], {
light: 'l',
strong: 's',
}),
]),
'weather-satellite': Object.freeze([
satelliteFilterOption(),
enumOption('opacity', 'o', 'strong', ['light', 'strong'], {
light: 'l',
strong: 's',
}),
enumOption(
'product',
'p',
'clouds-regional',
['clouds', 'clouds-regional'],
{ clouds: 'g', 'clouds-regional': 'r' },
),
]),
wind: Object.freeze([
enumOption('model', 'm', 'gfs', ['gfs', 'ifs'], { gfs: 'g', ifs: 'i' }),
enumOption(
'overlay',
'o',
'none',
['none', 'speed', 'temperature', 'pressure'],
{ none: 'n', speed: 's', temperature: 't', pressure: 'p' },
{ absentValue: 'speed' },
),
enumOption('units', 'u', 'km/h', ['km/h', 'm/s', 'mph'], {
'km/h': 'k',
'm/s': 'm',
mph: 'i',
}),
booleanOption('paused', 'p', false),
]),
flights: Object.freeze([
// Owner directive 2026-08-22: the fleet's 3D models are DEFAULT-ON in
// PROXIMITY mode. Proximity is itself the altitude/count gate — models only
// materialize once the camera is close enough and only for the nearest
// contacts in view — so "on" costs nothing at globe scale, and an operator
// who wants every in-view plane still opts into `all` deliberately.
// This default must stay in lockstep with `_models3dEnabled` in BOTH flight
// layers, `this._models3dEnabled` in ui.js, and the `active` / `visible`
// classes in index.html: the fresh-boot path skips restoration entirely (see
// `start()` below), so nothing ever pushes this value into the layers — those
// four initializers ARE the agreement, and they are pinned together in
// layerState.test.mjs.
//
// `absentValue: false` is what keeps the flip out of links already in the
// wild. Schema v2 shipped with OFF as the omitted default, so `v=2&l=f`
// MEANS off — and it has to keep meaning that. Moving the default without
// this would have silently turned 3D on for every existing v2 link, and
// `v=2&l=f&lo=f.m.a` (an OFF link that remembered mode All) would have come
// back as ON+All. The price is that ON is now written explicitly (`f.e.1`)
// instead of ridden in on the omission; see `absentTokenValue`.
booleanOption('models3d', 'e', true, { absentValue: false }),
enumOption('models3dMode', 'm', 'proximity', ['proximity', 'all'], {
proximity: 'p',
all: 'a',
}),
trackingIdOption('selectedFlightsTrackingId', 't', null),
trackingIdOption('selectedMilitaryTrackingId', 'u', null),
]),
satellites: Object.freeze([
enumOption('catalog', 'c', 'core', ['core', 'dense'], {
core: 'c',
dense: 'd',
}),
integerOption('selectedSatTrackingId', 't', null),
]),
cctv: Object.freeze([
enumOption('coverageMode', 'c', 'on', ['off', 'on', 'viewshed'], {
off: '0',
on: '1',
viewshed: 'v',
}),
booleanOption('showProjection', 'p', true),
booleanOption('autoHop', 'a', false),
]),
'recent-imagery': Object.freeze([
// Box edges in degrees × 100000; latitudes stop at the Web-Mercator limit.
boundedIntegerOption('west', 'w', null, { min: -18000000, max: 18000000 }),
boundedIntegerOption('south', 's', null, { min: -8505110, max: 8505110 }),
boundedIntegerOption('east', 'e', null, { min: -18000000, max: 18000000 }),
boundedIntegerOption('north', 'n', null, { min: -8505110, max: 8505110 }),
imageryPinOption('a', 'a'),
imageryPinOption('b', 'b'),
// 0 one image, 1 image against the basemap, 2 two images A / B.
boundedIntegerOption('mode', 'm', 0, { min: 0, max: 2 }),
// The swipe position is one comparison's framing, not a preference: a
// stored split resurfaced in the next session's first comparison.
shareOnlyOption(
boundedIntegerOption('split', 'p', 50, { min: 0, max: 100 }),
),
booleanOption('viirs', 'v', false),
]),
'street-level': Object.freeze([
// Preserve the published Mapillary token; custom switches use stable IDs.
booleanOption('mapillary', 'm', true),
Object.freeze({
key: 'providerSwitches',
patchRequested: patchRequestedIdSwitches,
token: 'r',
defaultValue: '',
normalize: (value) => normalizeIdSwitches(value, ['mapillary']),
encode: normalizeIdSwitches,
decode: (value) => normalizeIdSwitches(value, ['mapillary']),
}),
enumOption('pano', 'p', 'all', ['all', 'pano', 'flat'], {
all: 'a',
pano: 'p',
flat: 'f',
}),
// Relative days, so a link means the same next year. The max matches
// MAX_SINCE_DAYS in streetLevel/policy.js (pinned by layerState.test).
boundedIntegerOption('sinceDays', 's', 0, { min: 0, max: 36500 }),
]),
radio: Object.freeze([
Object.freeze({
key: 'filter',
token: 'f',
defaultValue: 'all',
normalize: normalizeRadioFilter,
encode: encodeRadioFilter,
decode: decodeRadioFilter,
}),
Object.freeze({
key: 'volume',
token: 'v',
defaultValue: 0.8,
normalize: normalizeVolume,
encode: (value) => String(Math.round(value * 100)),
decode: (value) =>
/^\d{1,3}$/.test(value) ? normalizeVolume(Number(value) / 100) : null,
}),
]),
});
export const SHARE_TRACKING_RESTORE_POLICIES = Object.freeze({
flights: Object.freeze({
optionOwner: 'flights',
optionKey: 'selectedFlightsTrackingId',
expiryWindowMs: 90_000,
label: msg('flight'),
}),
military: Object.freeze({
optionOwner: 'flights',
optionKey: 'selectedMilitaryTrackingId',
expiryWindowMs: 45_000,
label: msg('military flight'),
}),
satellites: Object.freeze({
optionOwner: 'satellites',
optionKey: 'selectedSatTrackingId',
expiryWindowMs: 300_000,
label: msg('satellite'),
}),
});
/**
* The original single-character assignments are a closed compatibility set.
* Keep their exact mapping pinned by the independent snapshot in the tests.
*/
export const LEGACY_LAYER_STATE_TOKENS = Object.freeze({
'ais-live-vessels': 'a',
'alpr-cameras': 'p',
'bhote-koshi-2026': 'h',
'bhote-koshi-locator': 'z',
bikeshare: 'b',
cctv: 'c',
directions: 'n',
earthquakes: 'e',
'fire-perimeters': '2',
flights: 'f',
'local-dams': 'q',
'local-datacenters': 'd',
'local-firms': 'w',
military: 'm',
'military-awareness': 'g',
'military-installations': 'i',
radio: 'r',
'recent-imagery': '1',
'rocket-launches': 'x',
satellites: 's',
'telegeography-submarine-cables': 'u',
traffic: 't',
transit: 'j',
'weather-cyclones': 'y',
'weather-lightning': 'l',
'weather-radar': 'v',
'weather-satellite': 'o',
wind: 'k',
});
/**
* Permanent token ownership. Existing share links are public authored state,
* so an allocation stays here even if its layer is later removed. The JSON
* ledger is also read directly from the published Git base by the checker.
*/
export function parseLayerStateTokenReservations(rows) {
if (!Array.isArray(rows)) {
throw new Error('Layer-state token ledger must be an array');
}
const reservations = Object.create(null);
const reservedIdsByToken = new Map();
for (const row of rows) {
if (
!Array.isArray(row) ||
row.length !== 2 ||
typeof row[0] !== 'string' ||
typeof row[1] !== 'string'
) {
throw new Error('Invalid layer-state token ledger row');
}
const [id, token] = row;
if (!/^[a-z0-9-]+$/.test(id) || !LAYER_STATE_TOKEN_PATTERN.test(token)) {
throw new Error(`Invalid layer-state token reservation: ${id}`);
}
if (Object.hasOwn(reservations, id)) {
throw new Error(`Duplicate layer-state token reservation id: ${id}`);
}
if (reservedIdsByToken.has(token)) {
throw new Error(`Duplicate layer-state token reservation: ${token}`);
}
if (
(Object.hasOwn(LEGACY_LAYER_STATE_TOKENS, id) &&
token !== LEGACY_LAYER_STATE_TOKENS[id]) ||
(token.length === 1 &&
!NEW_SINGLE_CHARACTER_TOKEN_PATTERN.test(token) &&
LEGACY_LAYER_STATE_TOKENS[id] !== token)
) {
throw new Error(`Legacy layer-state token is immutable: ${id}`);
}
reservations[id] = token;
reservedIdsByToken.set(token, id);
}
for (const [id, token] of Object.entries(LEGACY_LAYER_STATE_TOKENS)) {
if (reservations[id] !== token) {
throw new Error(`Missing or changed legacy layer-state token: ${id}`);
}
}
return Object.freeze(reservations);
}
export const LAYER_STATE_TOKEN_RESERVATIONS =
parseLayerStateTokenReservations(reservationRows);
/**
* Canonical serialization registry. Its order, not runtime registration order,
* owns stable URL ordering.
*/
export const LAYER_STATE_REGISTRY = Object.freeze([
Object.freeze({
id: 'air-quality',
token: '4',
disposition: 'enabled-only',
}),
Object.freeze({
id: 'ais-live-vessels',
token: 'a',
disposition: 'enabled-only',
}),
Object.freeze({
id: 'alpr-cameras',
token: 'p',
disposition: 'enabled-only',
}),
Object.freeze({
id: 'bhote-koshi-2026',
token: 'h',
disposition: 'enabled-only',
}),
Object.freeze({
id: 'bhote-koshi-locator',
token: 'z',
disposition: 'enabled-only',
}),
Object.freeze({ id: 'bikeshare', token: 'b', disposition: 'enabled-only' }),
Object.freeze({
id: 'cctv',
token: 'c',
disposition: 'enabled+options',
optionOwner: 'cctv',
}),
Object.freeze({ id: 'directions', token: 'n', disposition: 'enabled-only' }),
Object.freeze({ id: 'earthquakes', token: 'e', disposition: 'enabled-only' }),
Object.freeze({
id: 'fire-perimeters',
token: '2',
disposition: 'enabled-only',
}),
Object.freeze({
id: 'flights',
token: 'f',
disposition: 'enabled+options',
optionOwner: 'flights',
}),
Object.freeze({
id: 'gnss-integrity',
token: '3',
disposition: 'enabled-only',
}),
Object.freeze({ id: 'local-dams', token: 'q', disposition: 'enabled-only' }),
Object.freeze({
id: 'local-datacenters',
token: 'd',
disposition: 'enabled-only',
}),
Object.freeze({ id: 'local-firms', token: 'w', disposition: 'enabled-only' }),
Object.freeze({
id: 'military',
token: 'm',
disposition: 'enabled+mirrored-options',
optionOwner: 'flights',
}),
Object.freeze({
id: 'military-awareness',
token: 'g',
disposition: 'enabled-only',
}),
Object.freeze({
id: 'military-installations',
token: 'i',
disposition: 'enabled-only',
}),
Object.freeze({
id: 'radio',
token: 'r',
disposition: 'enabled+options',
optionOwner: 'radio',
}),
Object.freeze({
id: 'recent-imagery',
token: '1',
disposition: 'enabled+options',
optionOwner: 'recent-imagery',
}),
Object.freeze({
id: 'rocket-launches',
token: 'x',
disposition: 'enabled-only',
}),
Object.freeze({
id: 'satellites',
token: 's',
disposition: 'enabled+options',
optionOwner: 'satellites',
}),
Object.freeze({
id: 'street-level',
token: '0',
disposition: 'enabled+options',
optionOwner: 'street-level',
}),
Object.freeze({
id: 'telegeography-submarine-cables',
token: 'u',
disposition: 'enabled-only',
}),
Object.freeze({
id: 'traffic',
token: 't',
disposition: 'enabled+options',
optionOwner: 'traffic',
}),
Object.freeze({ id: 'transit', token: 'j', disposition: 'enabled-only' }),
Object.freeze({
id: 'weather-cyclones',
token: 'y',
disposition: 'enabled-only',
}),
Object.freeze({
id: 'weather-lightning',
token: 'l',
disposition: 'enabled+options',
optionOwner: 'weather-lightning',
}),
Object.freeze({
id: 'weather-radar',
token: 'v',
disposition: 'enabled+options',
optionOwner: 'weather-radar',
}),
Object.freeze({
id: 'weather-satellite',
token: 'o',
disposition: 'enabled+options',
optionOwner: 'weather-satellite',
}),
Object.freeze({
id: 'wind',
token: 'k',
disposition: 'enabled+options',
optionOwner: 'wind',
}),
]);
export const REGISTERED_LAYER_IDS = Object.freeze(
LAYER_STATE_REGISTRY.map((entry) => entry.id),
);
/** Consume unreserved digits before the two-character base-36 namespace. */
export function nextLayerStateToken(
reservations = LAYER_STATE_TOKEN_RESERVATIONS,
) {
const occupied = new Set(Object.values(reservations || {}));
for (const digit of '0123456789') {
if (!occupied.has(digit)) return digit;
}
for (const first of LAYER_STATE_TOKEN_ALPHABET) {
for (const second of LAYER_STATE_TOKEN_ALPHABET) {
const candidate = `${first}${second}`;
if (!occupied.has(candidate)) return candidate;
}
}
throw new Error('Layer-state token namespace exhausted');
}
function allocationRank(token) {
if (NEW_SINGLE_CHARACTER_TOKEN_PATTERN.test(token)) return Number(token);
if (typeof token !== 'string' || token.length !== 2) return Infinity;
const first = LAYER_STATE_TOKEN_ALPHABET.indexOf(token[0]);
const second = LAYER_STATE_TOKEN_ALPHABET.indexOf(token[1]);
return first < 0 || second < 0 ? Infinity : 10 + first * 36 + second;
}
/** Check a proposed ledger against the published merge-time base. */
export function validateLayerStateAllocations(
baseReservations,
reservations = LAYER_STATE_TOKEN_RESERVATIONS,
) {
if (!baseReservations || typeof baseReservations !== 'object') {
throw new Error('Base layer-state token reservations are required');
}
for (const [id, token] of Object.entries(baseReservations)) {
if (reservations[id] !== token) {
throw new Error(`Published layer-state token changed or removed: ${id}`);
}
}
const newlyReserved = Object.entries(reservations)
.filter(([id]) => !Object.hasOwn(baseReservations, id))
.sort((left, right) => allocationRank(left[1]) - allocationRank(right[1]));
const occupied = { ...baseReservations };
for (const [id, token] of newlyReserved) {
const expected = nextLayerStateToken(occupied);
if (token !== expected) {
throw new Error(
`Layer-state token for ${id} must be the next free token ${expected}`,
);
}
occupied[id] = token;
}
return true;
}
export const REGISTRY_BY_ID = new Map(
LAYER_STATE_REGISTRY.map((entry) => [entry.id, entry]),
);
const REGISTRY_BY_TOKEN = new Map(
LAYER_STATE_REGISTRY.map((entry) => [entry.token, entry]),
);
const OPTION_OWNER_IDS = Object.freeze([
...new Set(
LAYER_STATE_REGISTRY.map((entry) => entry.optionOwner).filter(Boolean),
),
]);
export function optionSpecs(ownerId) {
return OPTION_GROUPS[ownerId] || [];
}
function defaultsForOwner(ownerId) {
return Object.fromEntries(
optionSpecs(ownerId).map((spec) => [spec.key, spec.defaultValue]),
);
}
function normalizeOwnerOptions(ownerId, candidate = {}) {
const input = candidate && typeof candidate === 'object' ? candidate : {};
const normalized = {};
for (const spec of optionSpecs(ownerId)) {
const value = Object.hasOwn(input, spec.key)
? spec.normalize(input[spec.key])
: null;
normalized[spec.key] = value === null ? spec.defaultValue : value;
}
return normalized;
}
/** Return whether an event origin represents durable direct intent. */
export function isExplicitLayerStateOrigin(origin) {
return origin === 'user' || origin === 'voice' || origin === 'tool';
}
/** Validate the static registry itself before it is used to seal a manager. */
export function validateLayerStateRegistry(
registry = LAYER_STATE_REGISTRY,
reservations = LAYER_STATE_TOKEN_RESERVATIONS,
) {
if (!Array.isArray(registry) || registry.length === 0) {
throw new Error('Layer-state registry must be a non-empty array');
}
if (
!reservations ||
typeof reservations !== 'object' ||
Array.isArray(reservations)
) {
throw new Error('Layer-state token reservations must be an object');
}
const reservedIdsByToken = new Map();
for (const [id, token] of Object.entries(reservations)) {
if (!/^[a-z0-9-]+$/.test(id) || !LAYER_STATE_TOKEN_PATTERN.test(token)) {
throw new Error(`Invalid layer-state token reservation: ${id}`);
}
if (
(Object.hasOwn(LEGACY_LAYER_STATE_TOKENS, id) &&
token !== LEGACY_LAYER_STATE_TOKENS[id]) ||
(token.length === 1 &&
!NEW_SINGLE_CHARACTER_TOKEN_PATTERN.test(token) &&
LEGACY_LAYER_STATE_TOKENS[id] !== token)
) {
throw new Error(`Legacy layer-state token is immutable: ${id}`);
}
if (reservedIdsByToken.has(token)) {
throw new Error(`Duplicate layer-state token reservation: ${token}`);
}
reservedIdsByToken.set(token, id);
}
const ids = new Set();
const tokens = new Set();
for (const entry of registry) {
if (!entry || typeof entry.id !== 'string' || !entry.id)
throw new Error('Layer-state entry missing id');
if (!/^[a-z0-9-]+$/.test(entry.id))
throw new Error(`Invalid layer-state id: ${entry.id}`);
if (ids.has(entry.id))
throw new Error(`Duplicate layer-state id: ${entry.id}`);
ids.add(entry.id);
if (!LAYER_STATE_TOKEN_PATTERN.test(entry.token || ''))
throw new Error(`Invalid layer-state token: ${entry.id}`);
if (reservations[entry.id] !== entry.token) {
throw new Error(`Unreserved layer-state token: ${entry.id}`);
}
if (reservedIdsByToken.get(entry.token) !== entry.id) {
throw new Error(`Layer-state token reservation mismatch: ${entry.token}`);
}
if (tokens.has(entry.token))
throw new Error(`Duplicate layer-state token: ${entry.token}`);
tokens.add(entry.token);
if (!VALID_DISPOSITIONS.has(entry.disposition)) {
throw new Error(`Invalid layer-state disposition: ${entry.id}`);
}
if (entry.disposition !== 'enabled-only') {
if (!entry.optionOwner || optionSpecs(entry.optionOwner).length === 0) {
throw new Error(`Layer-state option owner missing: ${entry.id}`);
}
} else if (entry.optionOwner) {
throw new Error(`Enabled-only layer cannot own options: ${entry.id}`);
}
}
return true;
}
validateLayerStateRegistry();
/** Produce the complete durable default state. */
export function createDefaultLayerState() {
return {
version: LAYER_STATE_VERSION,
enabledLayerIds: [],
options: Object.fromEntries(
OPTION_OWNER_IDS.map((ownerId) => [ownerId, defaultsForOwner(ownerId)]),
),
};
}
/** Sanitize and canonicalize an externally supplied layer-state object. */
export function normalizeLayerState(candidate) {
const input = candidate && typeof candidate === 'object' ? candidate : {};
const requestedEnabled = new Set(
Array.isArray(input.enabledLayerIds)
? input.enabledLayerIds.map(String)
: [],
);
const enabledLayerIds = REGISTERED_LAYER_IDS.filter((id) =>
requestedEnabled.has(id),
);
const enabled = new Set(enabledLayerIds);
const options = Object.fromEntries(
OPTION_OWNER_IDS.map((ownerId) => [
ownerId,
normalizeOwnerOptions(ownerId, input.options?.[ownerId]),
]),
);
// A selected entity cannot outlive an explicitly disabled owner layer.
// Keeping these IDs would resurrect tracking when that layer is enabled
// later, even though OFF was newer explicit intent.
if (!enabled.has('flights')) options.flights.selectedFlightsTrackingId = null;
if (!enabled.has('military'))
options.flights.selectedMilitaryTrackingId = null;
if (!enabled.has('satellites'))
options.satellites.selectedSatTrackingId = null;
// The codec has no cross-family recency field, so multiple tracking IDs are
// ambiguous rather than an ordered handoff. Fail closed instead of letting
// asynchronous feed arrival decide which tracker and camera owner wins.
const trackingSelectionCount = [
options.flights.selectedFlightsTrackingId,
options.flights.selectedMilitaryTrackingId,
options.satellites.selectedSatTrackingId,
].filter((value) => value !== null).length;
if (trackingSelectionCount > 1) {
options.flights.selectedFlightsTrackingId = null;
options.flights.selectedMilitaryTrackingId = null;
options.satellites.selectedSatTrackingId = null;
}
return {
version: LAYER_STATE_VERSION,
enabledLayerIds,
options,
};
}
export function cloneLayerState(state) {
const normalized = normalizeLayerState(state);
return {
...normalized,
enabledLayerIds: [...normalized.enabledLayerIds],
options: Object.fromEntries(
Object.entries(normalized.options).map(([id, options]) => [
id,
{ ...options },
]),
),
};
}
/** Append the compact v2 layer fields to an existing URLSearchParams object. */
export function encodeLayerStateParams(params, state) {
const normalized = normalizeLayerState(state);
const enabled = new Set(normalized.enabledLayerIds);
params.set(
'l',
LAYER_STATE_REGISTRY.filter((entry) => enabled.has(entry.id))
.map((entry) => entry.token)
.join('.'),
);
const encodedOptions = [];
for (const ownerId of OPTION_OWNER_IDS) {
const ownerEntry = REGISTRY_BY_ID.get(ownerId);
const ownerOptions = normalized.options[ownerId];
for (const spec of optionSpecs(ownerId)) {
// Omit against what an ABSENT token means to a DECODER, not against the
// current default — those are the same thing for every option whose
// default never moved, and deliberately different for one whose did.
if (ownerOptions[spec.key] === absentTokenValue(spec)) continue;
encodedOptions.push(
`${ownerEntry.token}.${spec.token}.${spec.encode(ownerOptions[spec.key])}`,
);
}
}
if (encodedOptions.length) params.set('lo', encodedOptions.join('_'));
else params.delete('lo');
return params;
}
/** Decode v2 fields. Null means that the layer payload is absent. */
export function decodeLayerStateParams(params) {
const layerFields = params.getAll('l');
if (
params.get('v') !== String(LAYER_STATE_VERSION) ||
layerFields.length !== 1
)
return null;
const rawLayers = layerFields[0];
const rawOptionsField = String(params.get('lo') || '');
// Fail closed on an oversized payload rather than decoding a truncated one.
if (rawLayers.length > MAX_ENABLED_LAYERS_CHARS) return null;
if (rawOptionsField.length > MAX_LAYER_OPTIONS_CHARS) return null;
const layerTokens = rawLayers ? rawLayers.split('.') : [];
// `l=` is the one valid explicit-empty representation. Reject repeated
// fields, empty members, duplicate members, or unknown members; silently
// removing one would turn a malformed share into a different state.
if (
layerTokens.some((token) => !token || !REGISTRY_BY_TOKEN.has(token)) ||
new Set(layerTokens).size !== layerTokens.length
)
return null;
const enabledLayerIds = layerTokens.map(
(token) => REGISTRY_BY_TOKEN.get(token).id,
);
const rawOptions = {};
for (const assignment of rawOptionsField.split('_')) {
if (!assignment) continue;
const [layerToken, optionToken, encodedValue, ...extra] =
assignment.split('.');
if (extra.length) continue;
const entry = REGISTRY_BY_TOKEN.get(layerToken);
const ownerId = entry?.optionOwner || null;
if (!ownerId) continue;
const spec = optionSpecs(ownerId).find(
(candidate) => candidate.token === optionToken,
);
if (!spec) continue;
const decoded = spec.decode(encodedValue);
if (decoded === null) continue;
if (!rawOptions[ownerId]) rawOptions[ownerId] = {};
rawOptions[ownerId][spec.key] = decoded;
}
// Fill every token the link did NOT carry with its absent-meaning before
// normalization, which would otherwise substitute the CURRENT default. For all
// but one option those are identical and this is a no-op; for `models3d` it is
// the whole point — an omitted `e` is a v2 author saying OFF, not a v2 author
// saying "whatever the default happens to be today". See `absentTokenValue`.
for (const ownerId of OPTION_OWNER_IDS) {
for (const spec of optionSpecs(ownerId)) {
if (rawOptions[ownerId] && Object.hasOwn(rawOptions[ownerId], spec.key))
continue;
if (!rawOptions[ownerId]) rawOptions[ownerId] = {};
rawOptions[ownerId][spec.key] = absentTokenValue(spec);
}
}
return normalizeLayerState({ enabledLayerIds, options: rawOptions });
}
/** Options with share-link-only values reset to their defaults. */
function withoutShareOnlyOptions(options) {
const out = {};
for (const ownerId of OPTION_OWNER_IDS) {
out[ownerId] = { ...(options?.[ownerId] || {}) };
for (const spec of optionSpecs(ownerId)) {
if (spec.shareOnly) out[ownerId][spec.key] = spec.defaultValue;
}
}
return out;
}
/**
* Stable local-storage representation (full IDs for debuggability), with
* share-link-only options at their defaults.
*/
export function serializeStoredLayerState(state) {
const normalized = normalizeLayerState(state);
return JSON.stringify({
v: LAYER_STATE_VERSION,
l: normalized.enabledLayerIds,
o: withoutShareOnlyOptions(normalized.options),
});
}
export function parseStoredLayerState(raw) {
if (typeof raw !== 'string' || !raw) return null;
try {
const parsed = JSON.parse(raw);
if (parsed?.v !== LAYER_STATE_VERSION || !Array.isArray(parsed.l))
return null;
return normalizeLayerState({
enabledLayerIds: parsed.l,
options: withoutShareOnlyOptions(parsed.o),
});
} catch {
return null;
}
}
/** Return sanitized options to apply to one registered module. */
export function layerOptionsForRestore(state, layerId) {
const entry = REGISTRY_BY_ID.get(layerId);
if (!entry?.optionOwner) return null;
return { ...normalizeLayerState(state).options[entry.optionOwner] };
}