mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Split Open Map Overlays into Sea Marks and Ski Pistes layers
Each overlay is now its own Infrastructure row instead of a chip on one shared row: sea-marks (token 6) and ski-pistes (token 7), both enabled-only, built by createOsmOverlayLayers() from one descriptor list. Drops the option group and its share-codec defaults, gives each layer its own name, voice entry and CURRENT-STATE row, and shifts the token-ledger fixtures to the next free digit 8.
This commit is contained in:
1 parent
c489e65796
commit
fc7ff1055c
29 files changed
+194
-315
No files matched your search
@@ -29,7 +29,7 @@ import { createApplicationFirePerimeters } from './layers/perimeters.js';
|
||||
import { createApplicationGnssIntegrity } from './layers/gnss.js';
|
||||
import { createApplicationGhostMaps } from './layers/ghostMaps.js';
|
||||
import { createApplicationCables } from './layers/submarineCables.js';
|
||||
import { createOsmOverlaysLayer } from '../data/osmOverlays.js';
|
||||
import { createOsmOverlayLayers } from '../data/osmOverlays.js';
|
||||
import { createApplicationStreetLevel } from './layers/streetLevel.js';
|
||||
import { createInfrastructureLayers } from '../data/infrastructure.js';
|
||||
import { localGeoJsonServices } from './localGeojsonServices.js';
|
||||
@@ -170,7 +170,7 @@ export function createApplicationCatalog({
|
||||
clock: weatherClock,
|
||||
}),
|
||||
createCyclonesLayer({ feed: sources.cyclones }),
|
||||
createOsmOverlaysLayer(),
|
||||
...createOsmOverlayLayers(),
|
||||
...createInfrastructureLayers(localGeoJsonServices),
|
||||
createApplicationCables({ source: sources.cables }),
|
||||
createApplicationFirms({
|
||||
|
||||
@@ -40,7 +40,7 @@ test('catalogs construct distinct layers and classification from their supplied
|
||||
signal: b.signal,
|
||||
surface: fixtureSurface(b.signal),
|
||||
});
|
||||
assert.equal(first.layers.length, 34);
|
||||
assert.equal(first.layers.length, 35);
|
||||
assert.ok(first.get('local-adsb'), 'Local ADS-B is registered');
|
||||
assert.deepEqual(
|
||||
first.metadata.find(({ id }) => id === 'local-adsb'),
|
||||
@@ -155,7 +155,7 @@ test('Mapillary can be omitted or replaced without changing catalog membership',
|
||||
surface: fixtureSurface(lifetime.signal),
|
||||
};
|
||||
const catalog = createApplicationCatalog(options);
|
||||
assert.equal(catalog.layers.length, 34);
|
||||
assert.equal(catalog.layers.length, 35);
|
||||
assert.deepEqual(catalog.get('street-level').providerIds, []);
|
||||
assert.equal(
|
||||
catalogControlServices(catalog).streetLevelLayer,
|
||||
|
||||
@@ -37,7 +37,7 @@ test('every acquisition source can be omitted without losing catalog membership'
|
||||
|
||||
test('all acquisition sources may be absent, and enable fails before initializing or fetching', async (t) => {
|
||||
const actual = catalog({}, t);
|
||||
assert.equal(actual.layers.length, 34);
|
||||
assert.equal(actual.layers.length, 35);
|
||||
const manager = new LayerLifecycle(
|
||||
{},
|
||||
{ getSourceAvailability: actual.getSourceAvailability },
|
||||
|
||||
+2
-14
@@ -430,14 +430,6 @@ const OPTION_GROUPS = Object.freeze({
|
||||
// MAX_SINCE_DAYS in streetLevel/policy.js (pinned by layerState.test).
|
||||
boundedIntegerOption('sinceDays', 's', 0, { min: 0, max: 36500 }),
|
||||
]),
|
||||
// Two independently toggleable open-data raster overlays (#10) sharing one
|
||||
// owner, the same "one master row, several booleans" shape as `cctv`/`radio`.
|
||||
// Both default on: switching the row on with nothing chosen would show
|
||||
// nothing and read as broken.
|
||||
'osm-overlays': Object.freeze([
|
||||
booleanOption('seamap', 's', true),
|
||||
booleanOption('snowmap', 'n', true),
|
||||
]),
|
||||
radio: Object.freeze([
|
||||
Object.freeze({
|
||||
key: 'filter',
|
||||
@@ -646,12 +638,6 @@ export const LAYER_STATE_REGISTRY = Object.freeze([
|
||||
token: 'i',
|
||||
disposition: 'enabled-only',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'osm-overlays',
|
||||
token: '6',
|
||||
disposition: 'enabled+options',
|
||||
optionOwner: 'osm-overlays',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'radio',
|
||||
token: 'r',
|
||||
@@ -680,6 +666,8 @@ export const LAYER_STATE_REGISTRY = Object.freeze([
|
||||
disposition: 'enabled+options',
|
||||
optionOwner: 'satellites',
|
||||
}),
|
||||
Object.freeze({ id: 'sea-marks', token: '6', disposition: 'enabled-only' }),
|
||||
Object.freeze({ id: 'ski-pistes', token: '7', disposition: 'enabled-only' }),
|
||||
Object.freeze({
|
||||
id: 'street-level',
|
||||
token: '0',
|
||||
|
||||
@@ -202,8 +202,8 @@ function encode(state) {
|
||||
|
||||
test('production registry is exact, canonical, and rejects incomplete contracts', async () => {
|
||||
assert.equal(validateLayerStateRegistry(), true);
|
||||
assert.equal(REGISTERED_LAYER_IDS.length, 33);
|
||||
assert.equal(new Set(REGISTERED_LAYER_IDS).size, 33);
|
||||
assert.equal(REGISTERED_LAYER_IDS.length, 34);
|
||||
assert.equal(new Set(REGISTERED_LAYER_IDS).size, 34);
|
||||
assert.ok(REGISTERED_LAYER_IDS.includes('transit'));
|
||||
assert.deepEqual(REGISTERED_LAYER_IDS, [...REGISTERED_LAYER_IDS].sort());
|
||||
assert.deepEqual(LEGACY_LAYER_STATE_TOKENS, {
|
||||
@@ -242,18 +242,18 @@ test('production registry is exact, canonical, and rejects incomplete contracts'
|
||||
for (const [id, token] of Object.entries(LEGACY_LAYER_STATE_TOKENS)) {
|
||||
assert.equal(LAYER_STATE_TOKEN_RESERVATIONS[id], token);
|
||||
}
|
||||
assert.equal(nextLayerStateToken(), '7');
|
||||
assert.equal(nextLayerStateToken(), '8');
|
||||
assert.equal(
|
||||
nextLayerStateToken({
|
||||
...LAYER_STATE_TOKEN_RESERVATIONS,
|
||||
alpha: '7',
|
||||
bravo: '8',
|
||||
alpha: '8',
|
||||
bravo: '9',
|
||||
}),
|
||||
'9',
|
||||
'00',
|
||||
);
|
||||
const digitsExhausted = {
|
||||
...LAYER_STATE_TOKEN_RESERVATIONS,
|
||||
...Object.fromEntries([...'789'].map((digit) => [`prior-${digit}`, digit])),
|
||||
...Object.fromEntries([...'89'].map((digit) => [`prior-${digit}`, digit])),
|
||||
};
|
||||
assert.equal(nextLayerStateToken(digitsExhausted), '00');
|
||||
assert.equal(
|
||||
@@ -305,8 +305,8 @@ test('production registry is exact, canonical, and rejects incomplete contracts'
|
||||
assert.equal(
|
||||
validateLayerStateAllocations(LAYER_STATE_TOKEN_RESERVATIONS, {
|
||||
...LAYER_STATE_TOKEN_RESERVATIONS,
|
||||
future: '7',
|
||||
next: '8',
|
||||
future: '8',
|
||||
next: '9',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
@@ -316,7 +316,7 @@ test('production registry is exact, canonical, and rejects incomplete contracts'
|
||||
...LAYER_STATE_TOKEN_RESERVATIONS,
|
||||
future: '00',
|
||||
}),
|
||||
/next free token 7/,
|
||||
/next free token 8/,
|
||||
);
|
||||
const beforeLastDigit = { ...digitsExhausted };
|
||||
delete beforeLastDigit['prior-9'];
|
||||
@@ -339,10 +339,10 @@ test('production registry is exact, canonical, and rejects incomplete contracts'
|
||||
assert.throws(
|
||||
() =>
|
||||
validateLayerStateAllocations(
|
||||
{ ...LAYER_STATE_TOKEN_RESERVATIONS, merged: '6' },
|
||||
{ ...LAYER_STATE_TOKEN_RESERVATIONS, merged: '6', competing: '6' },
|
||||
{ ...LAYER_STATE_TOKEN_RESERVATIONS, merged: '8' },
|
||||
{ ...LAYER_STATE_TOKEN_RESERVATIONS, merged: '8', competing: '8' },
|
||||
),
|
||||
/next free token 7/,
|
||||
/next free token 9/,
|
||||
);
|
||||
assert.throws(
|
||||
() => validateLayerStateAllocations({ future: '00' }, { future: '01' }),
|
||||
|
||||
@@ -128,7 +128,7 @@ test('reservation ledger is complete, pinned, and rejects duplicate or malformed
|
||||
{ ...parseLayerStateTokenReservations(reservationRows) },
|
||||
{ ...LAYER_STATE_TOKEN_RESERVATIONS },
|
||||
);
|
||||
assert.equal(Object.keys(LAYER_STATE_TOKEN_RESERVATIONS).length, 33);
|
||||
assert.equal(Object.keys(LAYER_STATE_TOKEN_RESERVATIONS).length, 34);
|
||||
assert.deepEqual(
|
||||
{ ...LAYER_STATE_TOKEN_RESERVATIONS },
|
||||
{
|
||||
@@ -137,7 +137,8 @@ test('reservation ledger is complete, pinned, and rejects duplicate or malformed
|
||||
'gnss-integrity': '3',
|
||||
'air-quality': '4',
|
||||
's2-ghostmaps': '5',
|
||||
'osm-overlays': '6',
|
||||
'sea-marks': '6',
|
||||
'ski-pistes': '7',
|
||||
},
|
||||
);
|
||||
assert.throws(
|
||||
@@ -163,9 +164,9 @@ test('reservation ledger is complete, pinned, and rejects duplicate or malformed
|
||||
/Duplicate layer-state token reservation/,
|
||||
);
|
||||
assert.equal(
|
||||
parseLayerStateTokenReservations([...reservationRows, ['future', '7']])
|
||||
parseLayerStateTokenReservations([...reservationRows, ['future', '8']])
|
||||
.future,
|
||||
'7',
|
||||
'8',
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
@@ -183,14 +184,14 @@ test('reservation ledger is complete, pinned, and rejects duplicate or malformed
|
||||
|
||||
test('published retired digits and competing PRs advance the merge-time allocation', () => {
|
||||
withPublishedBase(
|
||||
{ rows: [...reservationRows, ['retired-layer', '7']] },
|
||||
{ rows: [...reservationRows, ['retired-layer', '8']] },
|
||||
(cwd) => {
|
||||
const published = readPublishedLayerStateReservations('HEAD', cwd);
|
||||
assert.equal(nextLayerStateToken(published), '8');
|
||||
assert.equal(nextLayerStateToken(published), '9');
|
||||
assert.equal(
|
||||
validateLayerStateAllocations(published, {
|
||||
...published,
|
||||
newcomer: '8',
|
||||
newcomer: '9',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
@@ -198,15 +199,15 @@ test('published retired digits and competing PRs advance the merge-time allocati
|
||||
() =>
|
||||
validateLayerStateAllocations(published, {
|
||||
...published,
|
||||
competing: '7',
|
||||
competing: '8',
|
||||
}),
|
||||
/next free token 8/,
|
||||
/next free token 9/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
validateLayerStateAllocations(published, {
|
||||
...LAYER_STATE_TOKEN_RESERVATIONS,
|
||||
newcomer: '7',
|
||||
newcomer: '8',
|
||||
}),
|
||||
/changed or removed: retired-layer/,
|
||||
);
|
||||
@@ -214,8 +215,8 @@ test('published retired digits and competing PRs advance the merge-time allocati
|
||||
);
|
||||
});
|
||||
|
||||
test('PR B manually replaces provisional 7 with 8 after PR A publishes 7', () => {
|
||||
withPublishedBase({ rows: [...reservationRows, ['pr-a', '7']] }, (cwd) => {
|
||||
test('PR B manually replaces provisional 8 with 9 after PR A publishes 8', () => {
|
||||
withPublishedBase({ rows: [...reservationRows, ['pr-a', '8']] }, (cwd) => {
|
||||
mkdirSync(path.join(cwd, 'scripts'));
|
||||
const fixtureChecker = path.join(
|
||||
cwd,
|
||||
@@ -236,8 +237,8 @@ test('PR B manually replaces provisional 7 with 8 after PR A publishes 7', () =>
|
||||
|
||||
writeCodecFixture(
|
||||
cwd,
|
||||
[...reservationRows, ['pr-b', '7']],
|
||||
[{ id: 'pr-b', token: '7' }],
|
||||
[...reservationRows, ['pr-b', '8']],
|
||||
[{ id: 'pr-b', token: '8' }],
|
||||
);
|
||||
const beforeRebase = runCheck();
|
||||
assert.equal(beforeRebase.status, 1);
|
||||
@@ -245,10 +246,10 @@ test('PR B manually replaces provisional 7 with 8 after PR A publishes 7', () =>
|
||||
|
||||
writeCodecFixture(
|
||||
cwd,
|
||||
[...reservationRows, ['pr-a', '7'], ['pr-b', '7']],
|
||||
[...reservationRows, ['pr-a', '8'], ['pr-b', '8']],
|
||||
[
|
||||
{ id: 'pr-a', token: '7' },
|
||||
{ id: 'pr-b', token: '7' },
|
||||
{ id: 'pr-a', token: '8' },
|
||||
{ id: 'pr-b', token: '8' },
|
||||
],
|
||||
);
|
||||
const staleRebase = runCheck();
|
||||
@@ -256,16 +257,16 @@ test('PR B manually replaces provisional 7 with 8 after PR A publishes 7', () =>
|
||||
assert.match(staleRebase.stderr, /Duplicate layer-state token reservation/);
|
||||
|
||||
const published = readPublishedLayerStateReservations('HEAD', cwd);
|
||||
assert.equal(nextLayerStateToken(published), '8');
|
||||
const validBaselineRows = [...reservationRows, ['pr-a', '7']];
|
||||
writeCodecFixture(cwd, validBaselineRows, [{ id: 'pr-a', token: '7' }]);
|
||||
assert.equal(nextLayerStateToken(published), '9');
|
||||
const validBaselineRows = [...reservationRows, ['pr-a', '8']];
|
||||
writeCodecFixture(cwd, validBaselineRows, [{ id: 'pr-a', token: '8' }]);
|
||||
const helper = spawnSync(
|
||||
process.execPath,
|
||||
[realpathSync(fixtureNext), 'pr-b'],
|
||||
{ cwd, encoding: 'utf8' },
|
||||
);
|
||||
assert.equal(helper.status, 0, helper.stderr);
|
||||
assert.match(helper.stdout, /pr-b: 8/);
|
||||
assert.match(helper.stdout, /pr-b: 9/);
|
||||
assert.deepEqual(
|
||||
JSON.parse(readFileSync(path.join(cwd, LEDGER_PATH), 'utf8')),
|
||||
validBaselineRows,
|
||||
@@ -273,20 +274,20 @@ test('PR B manually replaces provisional 7 with 8 after PR A publishes 7', () =>
|
||||
);
|
||||
writeCodecFixture(
|
||||
cwd,
|
||||
[...reservationRows, ['pr-a', '7'], ['pr-b', '8']],
|
||||
[...reservationRows, ['pr-a', '8'], ['pr-b', '9']],
|
||||
[
|
||||
{ id: 'pr-a', token: '7' },
|
||||
{ id: 'pr-b', token: '8' },
|
||||
{ id: 'pr-a', token: '8' },
|
||||
{ id: 'pr-b', token: '9' },
|
||||
],
|
||||
);
|
||||
const corrected = runCheck();
|
||||
assert.equal(corrected.status, 0, corrected.stderr);
|
||||
assert.match(corrected.stdout, /34 published, 1 new/);
|
||||
assert.match(corrected.stdout, /35 published, 1 new/);
|
||||
});
|
||||
});
|
||||
|
||||
test('allocation batches cross the last digit and base-36 pair boundaries in order', () => {
|
||||
const digitRows = [...'789'].map((digit) => [`retired-${digit}`, digit]);
|
||||
const digitRows = [...'89'].map((digit) => [`retired-${digit}`, digit]);
|
||||
const allDigits = parseLayerStateTokenReservations([
|
||||
...reservationRows,
|
||||
...digitRows,
|
||||
@@ -338,7 +339,7 @@ test('allocation batches cross the last digit and base-36 pair boundaries in ord
|
||||
test('an isolated valid two-character fixture round-trips an l field beyond the old 64-character cap', async () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), 'gev-layer-token-width-'));
|
||||
try {
|
||||
const priorDigits = [...'789'].map((digit) => [
|
||||
const priorDigits = [...'89'].map((digit) => [
|
||||
`qa-prior-digit-${digit}`,
|
||||
digit,
|
||||
]);
|
||||
@@ -375,7 +376,7 @@ test('an isolated valid two-character fixture round-trips an l field beyond the
|
||||
const params = new URLSearchParams([['v', '2']]);
|
||||
codec.encodeLayerStateParams(params, state);
|
||||
assert.ok(params.get('l').length > 64, 'fixture must cross the old cap');
|
||||
assert.equal(params.get('l').length, 77);
|
||||
assert.equal(params.get('l').length, 79);
|
||||
const restored = codec.decodeLayerStateParams(params);
|
||||
assert.deepEqual(restored?.enabledLayerIds, expectedLayerIds);
|
||||
assert.deepEqual(restored?.options, state.options);
|
||||
@@ -393,7 +394,7 @@ test('checker reads a complete future base ledger and rejects retired-token reus
|
||||
},
|
||||
(cwd) => {
|
||||
const published = readPublishedLayerStateReservations('HEAD', cwd);
|
||||
assert.equal(Object.keys(published).length, 34);
|
||||
assert.equal(Object.keys(published).length, 35);
|
||||
assert.equal(published['retired-layer'], '00');
|
||||
assert.throws(
|
||||
() =>
|
||||
|
||||
@@ -17,12 +17,13 @@
|
||||
["military", "m"],
|
||||
["military-awareness", "g"],
|
||||
["military-installations", "i"],
|
||||
["osm-overlays", "6"],
|
||||
["radio", "r"],
|
||||
["recent-imagery", "1"],
|
||||
["rocket-launches", "x"],
|
||||
["s2-ghostmaps", "5"],
|
||||
["satellites", "s"],
|
||||
["sea-marks", "6"],
|
||||
["ski-pistes", "7"],
|
||||
["street-level", "0"],
|
||||
["telegeography-submarine-cables", "u"],
|
||||
["traffic", "t"],
|
||||
|
||||
+65
-124
@@ -1,12 +1,10 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { msg, t } from '../i18n/index.js';
|
||||
import { NO_IMAGERY_HOST, resolveImageryHost } from '../maps/imageryHost.js';
|
||||
|
||||
/**
|
||||
* @file Open-data raster overlays: OpenSeaMap sea marks and OpenSnowMap ski
|
||||
* pistes (#10). Two independently toggleable overlays sharing one
|
||||
* config-driven layer, the same "small array of descriptors" shape as the
|
||||
* map stacks.
|
||||
* pistes (#10). Each overlay is its own layer with its own row under
|
||||
* Infrastructure; both are built from the descriptors below.
|
||||
*
|
||||
* OpenRailwayMap and OpenInfraMap, also named in #10, are deliberately not
|
||||
* included:
|
||||
@@ -25,14 +23,13 @@ import { NO_IMAGERY_HOST, resolveImageryHost } from '../maps/imageryHost.js';
|
||||
* @module data/osmOverlays
|
||||
*/
|
||||
|
||||
export const OSM_OVERLAYS_LAYER_ID = 'osm-overlays';
|
||||
|
||||
/** @type {ReadonlyArray<{key:string,label:string,icon:string,urlTemplate:string,credit:string,maximumLevel:number}>} */
|
||||
/** @type {ReadonlyArray<{id:string,name:string,icon:string,source:string,urlTemplate:string,credit:string,maximumLevel:number}>} */
|
||||
export const OSM_OVERLAYS = Object.freeze([
|
||||
Object.freeze({
|
||||
key: 'seamap',
|
||||
label: msg('Sea Marks'),
|
||||
id: 'sea-marks',
|
||||
name: 'Sea Marks',
|
||||
icon: '⚓',
|
||||
source: 'OpenSeaMap',
|
||||
urlTemplate: 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png',
|
||||
// Chart tiles CC BY-SA 2.0; the underlying data is OpenStreetMap (ODbL).
|
||||
credit:
|
||||
@@ -40,9 +37,10 @@ export const OSM_OVERLAYS = Object.freeze([
|
||||
maximumLevel: 18,
|
||||
}),
|
||||
Object.freeze({
|
||||
key: 'snowmap',
|
||||
label: msg('Ski Pistes'),
|
||||
id: 'ski-pistes',
|
||||
name: 'Ski Pistes',
|
||||
icon: '⛷',
|
||||
source: 'OpenSnowMap',
|
||||
urlTemplate: 'https://www.opensnowmap.org/pistes/{z}/{x}/{y}.png',
|
||||
// OpenSnowMap asks for "(c)www.opensnowmap.org" wherever its tiles appear.
|
||||
credit:
|
||||
@@ -51,71 +49,49 @@ export const OSM_OVERLAYS = Object.freeze([
|
||||
}),
|
||||
]);
|
||||
|
||||
const DEFAULT_PARAMS = Object.freeze(
|
||||
Object.fromEntries(OSM_OVERLAYS.map((overlay) => [overlay.key, true])),
|
||||
);
|
||||
|
||||
/**
|
||||
* Sanitize a candidate params object against the known overlay keys. Unknown
|
||||
* keys are ignored; a non-boolean value for a known key leaves that key
|
||||
* untouched rather than coercing it.
|
||||
* @param {object} current the live `{seamap, snowmap}` state
|
||||
* @param {object} candidate
|
||||
* @returns {{next: object, changed: boolean}}
|
||||
*/
|
||||
export function applyOsmOverlayParams(current, candidate = {}) {
|
||||
const next = { ...current };
|
||||
let changed = false;
|
||||
for (const overlay of OSM_OVERLAYS) {
|
||||
if (!Object.hasOwn(candidate ?? {}, overlay.key)) continue;
|
||||
const value = candidate[overlay.key];
|
||||
if (typeof value !== 'boolean') continue;
|
||||
if (next[overlay.key] !== value) changed = true;
|
||||
next[overlay.key] = value;
|
||||
}
|
||||
return { next, changed };
|
||||
}
|
||||
|
||||
/** Why an enabled overlay with something chosen is showing nothing, or null. */
|
||||
export function osmOverlaysHiddenReason(enabled, params, hostKind) {
|
||||
if (!enabled || hostKind !== 'none') return null;
|
||||
const anyChosen = OSM_OVERLAYS.some((overlay) =>
|
||||
Boolean(params?.[overlay.key]),
|
||||
);
|
||||
return anyChosen ? NO_IMAGERY_HOST : null;
|
||||
/** Why an enabled overlay is showing nothing, or null. */
|
||||
export function osmOverlayHiddenReason(enabled, hostKind) {
|
||||
return enabled && hostKind === 'none' ? NO_IMAGERY_HOST : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create one Open Map Overlays layer. Each application catalog gets its own
|
||||
* instance; the Cesium factories are injectable so tests run without WebGL.
|
||||
* Create the layer for one overlay descriptor. Each application catalog gets
|
||||
* its own instances; the Cesium factories are injectable so tests run without
|
||||
* WebGL.
|
||||
*/
|
||||
export function createOsmOverlaysLayer({
|
||||
host = resolveImageryHost,
|
||||
createProvider = (overlay) =>
|
||||
new Cesium.UrlTemplateImageryProvider({
|
||||
url: overlay.urlTemplate,
|
||||
credit: overlay.credit,
|
||||
maximumLevel: overlay.maximumLevel,
|
||||
}),
|
||||
createImageryLayer = (provider) => new Cesium.ImageryLayer(provider),
|
||||
} = {}) {
|
||||
export function createOsmOverlayLayer(
|
||||
overlay,
|
||||
{
|
||||
host = resolveImageryHost,
|
||||
createProvider = (descriptor) =>
|
||||
new Cesium.UrlTemplateImageryProvider({
|
||||
url: descriptor.urlTemplate,
|
||||
credit: descriptor.credit,
|
||||
maximumLevel: descriptor.maximumLevel,
|
||||
}),
|
||||
createImageryLayer = (provider) => new Cesium.ImageryLayer(provider),
|
||||
} = {},
|
||||
) {
|
||||
if (!OSM_OVERLAYS.includes(overlay))
|
||||
throw new TypeError(`Unknown open map overlay: ${overlay?.id}`);
|
||||
let viewer = null;
|
||||
let mapStack = null;
|
||||
let unsubscribeMap = null;
|
||||
let enabled = false;
|
||||
let params = { ...DEFAULT_PARAMS };
|
||||
let hostInfo = { collection: null, kind: 'none' };
|
||||
/** Imagery layers this instance owns, by overlay key: `{ layer, collection }`. */
|
||||
const owned = new Map();
|
||||
/** The imagery layer this instance owns while enabled, and where it sits. */
|
||||
let owned = null;
|
||||
|
||||
const usable = (collection) => collection && !collection.isDestroyed?.();
|
||||
|
||||
function detach(entry, destroy) {
|
||||
const { collection, layer } = entry;
|
||||
function detach(destroy) {
|
||||
if (!owned) return;
|
||||
const { collection, layer } = owned;
|
||||
if (usable(collection) && collection.contains(layer))
|
||||
collection.remove(layer, destroy);
|
||||
else if (destroy && !layer.isDestroyed?.()) layer.destroy?.();
|
||||
entry.collection = null;
|
||||
owned.collection = null;
|
||||
if (destroy) owned = null;
|
||||
}
|
||||
|
||||
function resolveHost() {
|
||||
@@ -127,48 +103,39 @@ export function createOsmOverlaysLayer({
|
||||
);
|
||||
}
|
||||
|
||||
/** Create, move or drop owned imagery layers so they match the wanted set. */
|
||||
/** Create, move or drop the owned imagery layer to match the state. */
|
||||
function sync() {
|
||||
if (!viewer) return;
|
||||
hostInfo = enabled ? resolveHost() : { collection: null, kind: 'none' };
|
||||
for (const overlay of OSM_OVERLAYS) {
|
||||
const wanted = enabled && Boolean(params[overlay.key]);
|
||||
let entry = owned.get(overlay.key);
|
||||
if (!wanted) {
|
||||
if (entry) {
|
||||
detach(entry, true);
|
||||
owned.delete(overlay.key);
|
||||
if (!enabled) {
|
||||
detach(true);
|
||||
} else {
|
||||
owned ||= {
|
||||
layer: createImageryLayer(createProvider(overlay)),
|
||||
collection: null,
|
||||
};
|
||||
if (owned.collection !== hostInfo.collection) {
|
||||
detach(false);
|
||||
if (usable(hostInfo.collection)) {
|
||||
// Appended, so it draws above the map stack's base imagery.
|
||||
hostInfo.collection.add(owned.layer);
|
||||
owned.collection = hostInfo.collection;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!entry) {
|
||||
entry = {
|
||||
layer: createImageryLayer(createProvider(overlay)),
|
||||
collection: null,
|
||||
};
|
||||
owned.set(overlay.key, entry);
|
||||
}
|
||||
if (entry.collection === hostInfo.collection) continue;
|
||||
detach(entry, false);
|
||||
if (usable(hostInfo.collection)) {
|
||||
// Appended, so it draws above the map stack's base imagery.
|
||||
hostInfo.collection.add(entry.layer);
|
||||
entry.collection = hostInfo.collection;
|
||||
}
|
||||
}
|
||||
viewer.scene?.requestRender?.();
|
||||
}
|
||||
|
||||
return {
|
||||
id: OSM_OVERLAYS_LAYER_ID,
|
||||
name: 'Open Map Overlays',
|
||||
icon: '🗺️',
|
||||
source: 'OpenSeaMap / OpenSnowMap',
|
||||
id: overlay.id,
|
||||
name: overlay.name,
|
||||
icon: overlay.icon,
|
||||
source: overlay.source,
|
||||
updateInterval: 0,
|
||||
init(nextViewer) {
|
||||
viewer = nextViewer;
|
||||
},
|
||||
/** Follow map-stack switches so the drapes move with the surface. */
|
||||
/** Follow map-stack switches so the drape moves with the surface. */
|
||||
attachMapStackController(controller) {
|
||||
unsubscribeMap?.();
|
||||
unsubscribeMap = null;
|
||||
@@ -189,33 +156,6 @@ export function createOsmOverlaysLayer({
|
||||
update() {
|
||||
return true;
|
||||
},
|
||||
setParams(next = {}) {
|
||||
const result = applyOsmOverlayParams(params, next);
|
||||
if (result.changed) {
|
||||
params = result.next;
|
||||
sync();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
getParams() {
|
||||
return { ...params };
|
||||
},
|
||||
getRowControls() {
|
||||
if (!enabled) return { chips: [], legend: [] };
|
||||
return {
|
||||
chips: OSM_OVERLAYS.map((overlay) => {
|
||||
const active = Boolean(params[overlay.key]);
|
||||
return {
|
||||
id: overlay.key,
|
||||
label: `${overlay.icon} ${t(overlay.label)}`,
|
||||
active,
|
||||
title: overlay.credit,
|
||||
params: { [overlay.key]: !active },
|
||||
};
|
||||
}),
|
||||
legend: [],
|
||||
};
|
||||
},
|
||||
getStats() {
|
||||
// Without a controller subscription the stats poll doubles as the
|
||||
// map-stack watch.
|
||||
@@ -224,26 +164,27 @@ export function createOsmOverlaysLayer({
|
||||
if (next.collection !== hostInfo.collection) sync();
|
||||
}
|
||||
return {
|
||||
count: enabled
|
||||
? OSM_OVERLAYS.filter((overlay) => params[overlay.key]).length
|
||||
: 0,
|
||||
error: osmOverlaysHiddenReason(enabled, params, hostInfo.kind),
|
||||
count: enabled ? 1 : 0,
|
||||
error: osmOverlayHiddenReason(enabled, hostInfo.kind),
|
||||
};
|
||||
},
|
||||
/** Owned layer count and host kind, for tests and live gates. */
|
||||
diagnostics() {
|
||||
return { host: hostInfo.kind, ownedCount: owned.size };
|
||||
return { host: hostInfo.kind, ownedCount: owned ? 1 : 0 };
|
||||
},
|
||||
destroy() {
|
||||
enabled = false;
|
||||
for (const entry of owned.values()) detach(entry, true);
|
||||
owned.clear();
|
||||
detach(true);
|
||||
unsubscribeMap?.();
|
||||
unsubscribeMap = null;
|
||||
mapStack = null;
|
||||
viewer = null;
|
||||
hostInfo = { collection: null, kind: 'none' };
|
||||
params = { ...DEFAULT_PARAMS };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** One layer per overlay, in panel order. */
|
||||
export function createOsmOverlayLayers(options) {
|
||||
return OSM_OVERLAYS.map((overlay) => createOsmOverlayLayer(overlay, options));
|
||||
}
|
||||
@@ -2,9 +2,9 @@ import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
OSM_OVERLAYS,
|
||||
applyOsmOverlayParams,
|
||||
createOsmOverlaysLayer,
|
||||
osmOverlaysHiddenReason,
|
||||
createOsmOverlayLayer,
|
||||
createOsmOverlayLayers,
|
||||
osmOverlayHiddenReason,
|
||||
} from './osmOverlays.js';
|
||||
import { NO_IMAGERY_HOST } from '../maps/imageryHost.js';
|
||||
|
||||
@@ -49,15 +49,15 @@ function fakeMapStack() {
|
||||
};
|
||||
}
|
||||
|
||||
function setup() {
|
||||
function setup(overlay = OSM_OVERLAYS[0]) {
|
||||
const globe = fakeCollection();
|
||||
const viewer = {
|
||||
imageryLayers: globe,
|
||||
scene: { globe: { show: true }, requestRender() {} },
|
||||
};
|
||||
const created = [];
|
||||
const layer = createOsmOverlaysLayer({
|
||||
createProvider: (overlay) => ({ overlay }),
|
||||
const layer = createOsmOverlayLayer(overlay, {
|
||||
createProvider: (descriptor) => ({ descriptor }),
|
||||
createImageryLayer: (provider) => {
|
||||
const imagery = { provider, destroyed: false };
|
||||
created.push(imagery);
|
||||
@@ -70,10 +70,10 @@ function setup() {
|
||||
return { layer, viewer, globe, created, mapStack };
|
||||
}
|
||||
|
||||
test('OSM_OVERLAYS: exactly the two shipped overlays, each with a tile template and credit', () => {
|
||||
test('OSM_OVERLAYS: the two shipped layers, each with a tile template and credit', () => {
|
||||
assert.deepEqual(
|
||||
OSM_OVERLAYS.map((o) => o.key),
|
||||
['seamap', 'snowmap'],
|
||||
OSM_OVERLAYS.map((o) => o.id),
|
||||
['sea-marks', 'ski-pistes'],
|
||||
);
|
||||
for (const overlay of OSM_OVERLAYS) {
|
||||
assert.match(overlay.urlTemplate, /^https:\/\/.*\{z\}.*\{x\}.*\{y\}/);
|
||||
@@ -83,77 +83,64 @@ test('OSM_OVERLAYS: exactly the two shipped overlays, each with a tile template
|
||||
assert.match(OSM_OVERLAYS[1].credit, /www\.opensnowmap\.org/);
|
||||
});
|
||||
|
||||
test('osmOverlaysHiddenReason: only when enabled, something chosen and no imagery host', () => {
|
||||
const both = { seamap: true, snowmap: true };
|
||||
assert.equal(osmOverlaysHiddenReason(false, both, 'none'), null);
|
||||
assert.equal(
|
||||
osmOverlaysHiddenReason(true, { seamap: false, snowmap: false }, 'none'),
|
||||
null,
|
||||
test('createOsmOverlayLayers builds one independent layer per overlay', () => {
|
||||
const layers = createOsmOverlayLayers();
|
||||
assert.deepEqual(
|
||||
layers.map((layer) => [layer.id, layer.name]),
|
||||
[
|
||||
['sea-marks', 'Sea Marks'],
|
||||
['ski-pistes', 'Ski Pistes'],
|
||||
],
|
||||
);
|
||||
assert.equal(osmOverlaysHiddenReason(true, both, 'globe'), null);
|
||||
assert.equal(osmOverlaysHiddenReason(true, both, 'tileset'), null);
|
||||
assert.equal(osmOverlaysHiddenReason(true, both, 'none'), NO_IMAGERY_HOST);
|
||||
assert.notEqual(createOsmOverlayLayers()[0], layers[0]);
|
||||
assert.throws(() => createOsmOverlayLayer({ id: 'other' }), /Unknown/);
|
||||
});
|
||||
|
||||
test('applyOsmOverlayParams: flips only known keys with boolean values, reports whether anything changed', () => {
|
||||
const current = { seamap: true, snowmap: true };
|
||||
const a = applyOsmOverlayParams(current, { seamap: false });
|
||||
assert.deepEqual(a.next, { seamap: false, snowmap: true });
|
||||
assert.equal(a.changed, true);
|
||||
|
||||
const b = applyOsmOverlayParams(current, { seamap: true });
|
||||
assert.deepEqual(b.next, current);
|
||||
assert.equal(b.changed, false);
|
||||
|
||||
const c = applyOsmOverlayParams(current, {
|
||||
unknownKey: false,
|
||||
snowmap: 'nope',
|
||||
});
|
||||
assert.deepEqual(c.next, current);
|
||||
assert.equal(c.changed, false);
|
||||
assert.equal(applyOsmOverlayParams(current, null).changed, false);
|
||||
test('osmOverlayHiddenReason: only when enabled with no imagery host', () => {
|
||||
assert.equal(osmOverlayHiddenReason(false, 'none'), null);
|
||||
assert.equal(osmOverlayHiddenReason(true, 'globe'), null);
|
||||
assert.equal(osmOverlayHiddenReason(true, 'tileset'), null);
|
||||
assert.equal(osmOverlayHiddenReason(true, 'none'), NO_IMAGERY_HOST);
|
||||
});
|
||||
|
||||
test('layer owns no imagery until enabled and destroys what it created on disable', () => {
|
||||
const { layer, globe, created } = setup();
|
||||
test('a layer owns no imagery until enabled and destroys it on disable', () => {
|
||||
const { layer, globe, created } = setup(OSM_OVERLAYS[1]);
|
||||
assert.equal(created.length, 0);
|
||||
assert.equal(globe.layers.length, 0);
|
||||
|
||||
layer.enable();
|
||||
assert.equal(globe.layers.length, 2);
|
||||
assert.deepEqual(layer.getStats(), { count: 2, error: null });
|
||||
|
||||
layer.setParams({ snowmap: false });
|
||||
assert.equal(globe.layers.length, 1);
|
||||
assert.equal(globe.layers[0].provider.overlay.key, 'seamap');
|
||||
assert.equal(created[1].destroyed, true);
|
||||
assert.equal(globe.layers[0].provider.descriptor.id, 'ski-pistes');
|
||||
assert.deepEqual(layer.getStats(), { count: 1, error: null });
|
||||
|
||||
layer.disable();
|
||||
assert.equal(globe.layers.length, 0);
|
||||
assert.ok(created.every((imagery) => imagery.destroyed));
|
||||
assert.equal(created[0].destroyed, true);
|
||||
assert.deepEqual(layer.diagnostics(), { host: 'none', ownedCount: 0 });
|
||||
assert.deepEqual(layer.getStats(), { count: 0, error: null });
|
||||
|
||||
layer.enable();
|
||||
assert.equal(created.length, 2, 're-enabling creates a fresh imagery layer');
|
||||
});
|
||||
|
||||
test('overlays follow a map-stack switch from the globe onto the Google 3D tileset and back', () => {
|
||||
test('the overlay follows a map-stack switch onto the Google 3D tileset and back', () => {
|
||||
const { layer, viewer, globe, mapStack } = setup();
|
||||
layer.enable();
|
||||
const before = [...globe.layers];
|
||||
const [imagery] = globe.layers;
|
||||
|
||||
const tileset = { imageryLayers: fakeCollection() };
|
||||
viewer.scene.globe.show = false;
|
||||
mapStack.tileset = tileset;
|
||||
mapStack.emit();
|
||||
assert.equal(globe.layers.length, 0);
|
||||
assert.deepEqual(tileset.imageryLayers.layers, before);
|
||||
assert.ok(globe.removed.every(({ destroy }) => destroy === false));
|
||||
assert.equal(layer.getStats().error, null);
|
||||
assert.deepEqual(tileset.imageryLayers.layers, [imagery]);
|
||||
assert.equal(globe.removed[0].destroy, false);
|
||||
assert.equal(layer.diagnostics().host, 'tileset');
|
||||
assert.equal(layer.getStats().error, null);
|
||||
|
||||
viewer.scene.globe.show = true;
|
||||
mapStack.tileset = null;
|
||||
mapStack.emit();
|
||||
assert.deepEqual(globe.layers, before);
|
||||
assert.deepEqual(globe.layers, [imagery]);
|
||||
assert.equal(tileset.imageryLayers.layers.length, 0);
|
||||
});
|
||||
|
||||
@@ -166,39 +153,15 @@ test('with no surface to drape on the row explains itself, and recovers when one
|
||||
|
||||
viewer.scene.globe.show = true;
|
||||
mapStack.emit();
|
||||
assert.equal(globe.layers.length, 2);
|
||||
assert.equal(globe.layers.length, 1);
|
||||
assert.equal(layer.getStats().error, null);
|
||||
});
|
||||
|
||||
test('destroy removes owned imagery, unsubscribes from the map stack and resets params', () => {
|
||||
test('destroy removes owned imagery and unsubscribes from the map stack', () => {
|
||||
const { layer, globe, created, mapStack } = setup();
|
||||
layer.enable();
|
||||
layer.setParams({ seamap: false });
|
||||
layer.destroy();
|
||||
assert.equal(globe.layers.length, 0);
|
||||
assert.ok(created.every((imagery) => imagery.destroyed));
|
||||
assert.equal(created[0].destroyed, true);
|
||||
assert.equal(mapStack.subscribers.size, 0);
|
||||
assert.deepEqual(layer.getParams(), { seamap: true, snowmap: true });
|
||||
});
|
||||
|
||||
test('each catalog gets its own instance', () => {
|
||||
const a = createOsmOverlaysLayer();
|
||||
const b = createOsmOverlaysLayer();
|
||||
a.setParams({ seamap: false });
|
||||
assert.deepEqual(b.getParams(), { seamap: true, snowmap: true });
|
||||
});
|
||||
|
||||
test('chips translate their labels and toggle one overlay each', () => {
|
||||
const { layer } = setup();
|
||||
assert.deepEqual(layer.getRowControls(), { chips: [], legend: [] });
|
||||
layer.enable();
|
||||
const { chips } = layer.getRowControls();
|
||||
assert.deepEqual(
|
||||
chips.map((chip) => [chip.id, chip.active, chip.params]),
|
||||
[
|
||||
['seamap', true, { seamap: false }],
|
||||
['snowmap', true, { snowmap: false }],
|
||||
],
|
||||
);
|
||||
assert.match(chips[0].label, /Sea Marks/);
|
||||
});
|
||||
@@ -1030,11 +1030,11 @@ test('the voice TOOL SCHEMA matches the pinned contract — the mission mapping
|
||||
// preserves the mission mapping and globe-rotation arguments.
|
||||
// S2 GhostMaps extends the generated layer enums; both feature contracts remain.
|
||||
// The layer alias hint leaves out aliases the layer id already spells.
|
||||
// Open Map Overlays extends the generated layer enums and alias hint.
|
||||
assert.equal(block.length, 30975, 'serialized tool schema length drifted');
|
||||
// Sea Marks and Ski Pistes extend the generated layer enums and alias hint.
|
||||
assert.equal(block.length, 31008, 'serialized tool schema length drifted');
|
||||
assert.equal(
|
||||
crypto.createHash('sha256').update(block).digest('hex'),
|
||||
'deeb6c52e706bfa6dcd330fee0df3228581d2bd0836c2e433e371da2c6b5f60c',
|
||||
'da1674545ab9380fb86d387f6e8513aa271e0afe539b69b6a6d09add6c710b0c',
|
||||
'the first-run missions must ride EXISTING tools: no schema edit, no cache bust',
|
||||
);
|
||||
const instructions = fs.readFileSync(
|
||||
|
||||
@@ -3220,6 +3220,5 @@
|
||||
"Edit anchors, data packs and shot details": "تعديل نقاط الارتكاز وحزم البيانات وتفاصيل اللقطة",
|
||||
"Download this scene as JSON or an asset bundle": "تنزيل هذا المشهد بصيغة JSON أو حزمة موارد",
|
||||
"Sea Marks": "علامات بحرية",
|
||||
"Ski Pistes": "مسارات التزلج",
|
||||
"Open Map Overlays": "طبقات خرائط مفتوحة"
|
||||
"Ski Pistes": "مسارات التزلج"
|
||||
}
|
||||
@@ -2940,6 +2940,5 @@
|
||||
"CONFIRMED": "BESTÄTIGT",
|
||||
"PAD / RTLS": "STARTPLATZ / RTLS",
|
||||
"Sea Marks": "Seezeichen",
|
||||
"Ski Pistes": "Skipisten",
|
||||
"Open Map Overlays": "Offene Kartenebenen"
|
||||
"Ski Pistes": "Skipisten"
|
||||
}
|
||||
@@ -3010,6 +3010,5 @@
|
||||
"Left panels": "Paneles izquierdos",
|
||||
"Right panels": "Paneles derechos",
|
||||
"Sea Marks": "Señales marítimas",
|
||||
"Ski Pistes": "Pistas de esquí",
|
||||
"Open Map Overlays": "Capas de mapas abiertos"
|
||||
"Ski Pistes": "Pistas de esquí"
|
||||
}
|
||||
@@ -3010,6 +3010,5 @@
|
||||
"CONFIRMED": "CONFIRMÉE",
|
||||
"PAD / RTLS": "PAS DE TIR / RTLS",
|
||||
"Sea Marks": "Balisage maritime",
|
||||
"Ski Pistes": "Pistes de ski",
|
||||
"Open Map Overlays": "Calques de cartes ouvertes"
|
||||
"Ski Pistes": "Pistes de ski"
|
||||
}
|
||||
@@ -3010,6 +3010,5 @@
|
||||
"CONFIRMED": "CONFERMATA",
|
||||
"PAD / RTLS": "SITO DI LANCIO / RTLS",
|
||||
"Sea Marks": "Segnalamenti marittimi",
|
||||
"Ski Pistes": "Piste da sci",
|
||||
"Open Map Overlays": "Livelli di mappe aperte"
|
||||
"Ski Pistes": "Piste da sci"
|
||||
}
|
||||
@@ -2870,6 +2870,5 @@
|
||||
"CONFIRMED": "確認済み",
|
||||
"PAD / RTLS": "射場 / RTLS",
|
||||
"Sea Marks": "航路標識",
|
||||
"Ski Pistes": "スキーゲレンデ",
|
||||
"Open Map Overlays": "オープンマップのオーバーレイ"
|
||||
"Ski Pistes": "スキーゲレンデ"
|
||||
}
|
||||
@@ -3010,6 +3010,5 @@
|
||||
"CONFIRMED": "CONFIRMADA",
|
||||
"PAD / RTLS": "PLATAFORMA / RTLS",
|
||||
"Sea Marks": "Sinais marítimos",
|
||||
"Ski Pistes": "Pistas de esqui",
|
||||
"Open Map Overlays": "Camadas de mapas abertos"
|
||||
"Ski Pistes": "Pistas de esqui"
|
||||
}
|
||||
@@ -3080,6 +3080,5 @@
|
||||
"Left panels": "Левые панели",
|
||||
"Right panels": "Правые панели",
|
||||
"Sea Marks": "Морские знаки",
|
||||
"Ski Pistes": "Горнолыжные трассы",
|
||||
"Open Map Overlays": "Открытые картографические слои"
|
||||
"Ski Pistes": "Горнолыжные трассы"
|
||||
}
|
||||
@@ -3080,6 +3080,5 @@
|
||||
"CONFIRMED": "ПІДТВЕРДЖЕНО",
|
||||
"PAD / RTLS": "МІСЦЕ ЗАПУСКУ / RTLS",
|
||||
"Sea Marks": "Морські знаки",
|
||||
"Ski Pistes": "Гірськолижні траси",
|
||||
"Open Map Overlays": "Відкриті картографічні шари"
|
||||
"Ski Pistes": "Гірськолижні траси"
|
||||
}
|
||||
@@ -2870,6 +2870,5 @@
|
||||
"Left panels": "左侧面板",
|
||||
"Right panels": "右侧面板",
|
||||
"Sea Marks": "航标",
|
||||
"Ski Pistes": "滑雪道",
|
||||
"Open Map Overlays": "开放地图叠加层"
|
||||
"Ski Pistes": "滑雪道"
|
||||
}
|
||||
@@ -2870,6 +2870,5 @@
|
||||
"Left panels": "左側面板",
|
||||
"Right panels": "右側面板",
|
||||
"Sea Marks": "航標",
|
||||
"Ski Pistes": "滑雪道",
|
||||
"Open Map Overlays": "開放地圖疊加層"
|
||||
"Ski Pistes": "滑雪道"
|
||||
}
|
||||
@@ -296,7 +296,6 @@ export const SHARE_VIEW_LAYER_OPTION_ABSENT = Object.freeze({
|
||||
sinceDays: 0,
|
||||
}),
|
||||
radio: Object.freeze({ filter: 'all', volume: 0.8 }),
|
||||
'osm-overlays': Object.freeze({ seamap: true, snowmap: true }),
|
||||
});
|
||||
|
||||
/** The frozen meaning of an omitted option; unlisted options are always written. */
|
||||
|
||||
@@ -43,7 +43,8 @@ export const LAYER_NAME_KEYS = Object.freeze({
|
||||
'air-quality': msg('Air quality'),
|
||||
directions: msg('Directions'),
|
||||
'alpr-cameras': msg('ALPR Cameras'),
|
||||
'osm-overlays': msg('Open Map Overlays'),
|
||||
'sea-marks': msg('Sea Marks'),
|
||||
'ski-pistes': msg('Ski Pistes'),
|
||||
});
|
||||
|
||||
/** The layer panel's own English names, where they differ from the module's. */
|
||||
|
||||
@@ -64,7 +64,8 @@ const PANEL_GROUPS = [
|
||||
'local-datacenters',
|
||||
'telegeography-submarine-cables',
|
||||
'local-dams',
|
||||
'osm-overlays',
|
||||
'sea-marks',
|
||||
'ski-pistes',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -48,7 +48,7 @@ test('the complete Realtime tool payload pins the manifest-generated layer relea
|
||||
// Snow leaves the set_visual_style enum.
|
||||
// The Operator HUD layout leaves set_hud's layout enum.
|
||||
// The layer alias hint leaves out aliases the layer id already spells.
|
||||
'b59bf989c98cb5935b628dd9c586f303f3e1e7febb8aac83f479fd60ec023bbc',
|
||||
'c13f59886cce3780e7b27462df19659242fdac98928289dce74cf61a1f74054a',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -72,21 +72,16 @@ const layers = [
|
||||
noQuery: 'published third-party intel placemarks; not analyst records',
|
||||
},
|
||||
{
|
||||
id: 'osm-overlays',
|
||||
aliases: [
|
||||
'open map overlays',
|
||||
'map overlays',
|
||||
'sea marks',
|
||||
'seamarks',
|
||||
'nautical charts',
|
||||
'openseamap',
|
||||
'ski pistes',
|
||||
'ski slopes',
|
||||
'snow map',
|
||||
'opensnowmap',
|
||||
],
|
||||
id: 'sea-marks',
|
||||
aliases: ['seamarks', 'nautical charts', 'buoys', 'openseamap'],
|
||||
context: false,
|
||||
noQuery: 'raster tile overlays; no countable records',
|
||||
noQuery: 'raster tile overlay; no countable records',
|
||||
},
|
||||
{
|
||||
id: 'ski-pistes',
|
||||
aliases: ['ski slopes', 'ski runs', 'snow map', 'opensnowmap'],
|
||||
context: false,
|
||||
noQuery: 'raster tile overlay; no countable records',
|
||||
},
|
||||
{
|
||||
id: 'flights',
|
||||
|
||||
Reference in new issue
Block a user