Merge branch 'main' into feat/radio-country-coverage

This commit is contained in:
Said Turksever authored and GitHub committed 2026-11-03 15:46:24 +00:00
commit bc6af1ccd1
51 files changed
+1452 -284

No files matched your search

+14
View File
@@ -150,6 +150,20 @@ export const DATA_CREDITS = [
key: 'usgs',
html: 'Earthquakes: Data courtesy of the U.S. Geological Survey',
},
{
key: 'openseamap',
html:
'Sea marks overlay: ' +
'<a href="https://www.openseamap.org" target="_blank" rel="noopener">© OpenSeaMap contributors</a> (CC BY-SA 2.0) · ' +
'Data © OpenStreetMap contributors (ODbL)',
},
{
key: 'opensnowmap',
html:
'Ski pistes overlay: ' +
'<a href="https://www.opensnowmap.org" target="_blank" rel="noopener">© www.opensnowmap.org</a> (CC BY-SA) · ' +
'Data © OpenStreetMap contributors (ODbL)',
},
{
key: 'nasa-gibs',
html:
+2
View File
@@ -666,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',
+13 -15
View File
@@ -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, 32);
assert.equal(new Set(REGISTERED_LAYER_IDS).size, 32);
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,20 +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(), '6');
assert.equal(nextLayerStateToken(), '8');
assert.equal(
nextLayerStateToken({
...LAYER_STATE_TOKEN_RESERVATIONS,
alpha: '6',
bravo: '7',
alpha: '8',
bravo: '9',
}),
'8',
'00',
);
const digitsExhausted = {
...LAYER_STATE_TOKEN_RESERVATIONS,
...Object.fromEntries(
[...'6789'].map((digit) => [`prior-${digit}`, digit]),
),
...Object.fromEntries([...'89'].map((digit) => [`prior-${digit}`, digit])),
};
assert.equal(nextLayerStateToken(digitsExhausted), '00');
assert.equal(
@@ -307,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: '6',
next: '7',
future: '8',
next: '9',
}),
true,
);
@@ -318,7 +316,7 @@ test('production registry is exact, canonical, and rejects incomplete contracts'
...LAYER_STATE_TOKEN_RESERVATIONS,
future: '00',
}),
/next free token 6/,
/next free token 8/,
);
const beforeLastDigit = { ...digitsExhausted };
delete beforeLastDigit['prior-9'];
@@ -341,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' }),
+30 -28
View File
@@ -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, 32);
assert.equal(Object.keys(LAYER_STATE_TOKEN_RESERVATIONS).length, 34);
assert.deepEqual(
{ ...LAYER_STATE_TOKEN_RESERVATIONS },
{
@@ -137,6 +137,8 @@ test('reservation ledger is complete, pinned, and rejects duplicate or malformed
'gnss-integrity': '3',
'air-quality': '4',
's2-ghostmaps': '5',
'sea-marks': '6',
'ski-pistes': '7',
},
);
assert.throws(
@@ -162,9 +164,9 @@ test('reservation ledger is complete, pinned, and rejects duplicate or malformed
/Duplicate layer-state token reservation/,
);
assert.equal(
parseLayerStateTokenReservations([...reservationRows, ['future', '6']])
parseLayerStateTokenReservations([...reservationRows, ['future', '8']])
.future,
'6',
'8',
);
assert.throws(
() =>
@@ -182,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', '6']] },
{ rows: [...reservationRows, ['retired-layer', '8']] },
(cwd) => {
const published = readPublishedLayerStateReservations('HEAD', cwd);
assert.equal(nextLayerStateToken(published), '7');
assert.equal(nextLayerStateToken(published), '9');
assert.equal(
validateLayerStateAllocations(published, {
...published,
newcomer: '7',
newcomer: '9',
}),
true,
);
@@ -197,15 +199,15 @@ test('published retired digits and competing PRs advance the merge-time allocati
() =>
validateLayerStateAllocations(published, {
...published,
competing: '6',
competing: '8',
}),
/next free token 7/,
/next free token 9/,
);
assert.throws(
() =>
validateLayerStateAllocations(published, {
...LAYER_STATE_TOKEN_RESERVATIONS,
newcomer: '6',
newcomer: '8',
}),
/changed or removed: retired-layer/,
);
@@ -213,8 +215,8 @@ test('published retired digits and competing PRs advance the merge-time allocati
);
});
test('PR B manually replaces provisional 6 with 7 after PR A publishes 6', () => {
withPublishedBase({ rows: [...reservationRows, ['pr-a', '6']] }, (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,
@@ -235,8 +237,8 @@ test('PR B manually replaces provisional 6 with 7 after PR A publishes 6', () =>
writeCodecFixture(
cwd,
[...reservationRows, ['pr-b', '6']],
[{ id: 'pr-b', token: '6' }],
[...reservationRows, ['pr-b', '8']],
[{ id: 'pr-b', token: '8' }],
);
const beforeRebase = runCheck();
assert.equal(beforeRebase.status, 1);
@@ -244,10 +246,10 @@ test('PR B manually replaces provisional 6 with 7 after PR A publishes 6', () =>
writeCodecFixture(
cwd,
[...reservationRows, ['pr-a', '6'], ['pr-b', '6']],
[...reservationRows, ['pr-a', '8'], ['pr-b', '8']],
[
{ id: 'pr-a', token: '6' },
{ id: 'pr-b', token: '6' },
{ id: 'pr-a', token: '8' },
{ id: 'pr-b', token: '8' },
],
);
const staleRebase = runCheck();
@@ -255,16 +257,16 @@ test('PR B manually replaces provisional 6 with 7 after PR A publishes 6', () =>
assert.match(staleRebase.stderr, /Duplicate layer-state token reservation/);
const published = readPublishedLayerStateReservations('HEAD', cwd);
assert.equal(nextLayerStateToken(published), '7');
const validBaselineRows = [...reservationRows, ['pr-a', '6']];
writeCodecFixture(cwd, validBaselineRows, [{ id: 'pr-a', token: '6' }]);
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: 7/);
assert.match(helper.stdout, /pr-b: 9/);
assert.deepEqual(
JSON.parse(readFileSync(path.join(cwd, LEDGER_PATH), 'utf8')),
validBaselineRows,
@@ -272,20 +274,20 @@ test('PR B manually replaces provisional 6 with 7 after PR A publishes 6', () =>
);
writeCodecFixture(
cwd,
[...reservationRows, ['pr-a', '6'], ['pr-b', '7']],
[...reservationRows, ['pr-a', '8'], ['pr-b', '9']],
[
{ id: 'pr-a', token: '6' },
{ id: 'pr-b', token: '7' },
{ id: 'pr-a', token: '8' },
{ id: 'pr-b', token: '9' },
],
);
const corrected = runCheck();
assert.equal(corrected.status, 0, corrected.stderr);
assert.match(corrected.stdout, /33 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 = [...'6789'].map((digit) => [`retired-${digit}`, digit]);
const digitRows = [...'89'].map((digit) => [`retired-${digit}`, digit]);
const allDigits = parseLayerStateTokenReservations([
...reservationRows,
...digitRows,
@@ -337,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 = [...'6789'].map((digit) => [
const priorDigits = [...'89'].map((digit) => [
`qa-prior-digit-${digit}`,
digit,
]);
@@ -374,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, 75);
assert.equal(params.get('l').length, 79);
const restored = codec.decodeLayerStateParams(params);
assert.deepEqual(restored?.enabledLayerIds, expectedLayerIds);
assert.deepEqual(restored?.options, state.options);
@@ -392,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, 33);
assert.equal(Object.keys(published).length, 35);
assert.equal(published['retired-layer'], '00');
assert.throws(
() =>
@@ -22,6 +22,8 @@
["rocket-launches", "x"],
["s2-ghostmaps", "5"],
["satellites", "s"],
["sea-marks", "6"],
["ski-pistes", "7"],
["street-level", "0"],
["telegeography-submarine-cables", "u"],
["traffic", "t"],
+190
View File
@@ -0,0 +1,190 @@
import * as Cesium from 'cesium';
import { NO_IMAGERY_HOST, resolveImageryHost } from '../maps/imageryHost.js';
/**
* @file Open-data raster overlays: OpenSeaMap sea marks and OpenSnowMap ski
* 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:
* - OpenRailwayMap's public tile server (tiles.openrailwaymap.org) only
* serves requests whose Referer is openrailwaymap.org; every other Referer,
* including none, gets 403. A tab on this app sends its own origin, so the
* tiles would fail, and embedding them would run against their tile-usage
* policy either way.
* - OpenInfraMap serves MapLibre vector tiles, not raster XYZ tiles. That
* needs a vector-tile rendering pipeline, not an imagery-layer drop-in.
*
* The overlays drape on whatever surface the active map stack offers (see
* `maps/imageryHost.js`): the globe on the OSM and Bing stacks, the Google
* Photorealistic tileset's own `imageryLayers` on Google 3D. Only a stack with
* neither leaves them hidden, and the row says so.
* @module data/osmOverlays
*/
/** @type {ReadonlyArray<{id:string,name:string,icon:string,source:string,urlTemplate:string,credit:string,maximumLevel:number}>} */
export const OSM_OVERLAYS = Object.freeze([
Object.freeze({
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:
'Sea marks © OpenSeaMap contributors (CC BY-SA 2.0) · Data © OpenStreetMap contributors (ODbL)',
maximumLevel: 18,
}),
Object.freeze({
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:
'Ski pistes © www.opensnowmap.org (CC BY-SA) · Data © OpenStreetMap contributors (ODbL)',
maximumLevel: 18,
}),
]);
/** Why an enabled overlay is showing nothing, or null. */
export function osmOverlayHiddenReason(enabled, hostKind) {
return enabled && hostKind === 'none' ? NO_IMAGERY_HOST : null;
}
/**
* 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 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 hostInfo = { collection: null, kind: 'none' };
/** The imagery layer this instance owns while enabled, and where it sits. */
let owned = null;
const usable = (collection) => collection && !collection.isDestroyed?.();
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?.();
owned.collection = null;
if (destroy) owned = null;
}
function resolveHost() {
return (
host({ viewer, tileset: mapStack?.getImageryHostTileset?.() }) || {
collection: null,
kind: 'none',
}
);
}
/** Create, move or drop the owned imagery layer to match the state. */
function sync() {
if (!viewer) return;
hostInfo = enabled ? resolveHost() : { collection: null, kind: 'none' };
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;
}
}
}
viewer.scene?.requestRender?.();
}
return {
id: overlay.id,
name: overlay.name,
icon: overlay.icon,
source: overlay.source,
updateInterval: 0,
init(nextViewer) {
viewer = nextViewer;
},
/** Follow map-stack switches so the drape moves with the surface. */
attachMapStackController(controller) {
unsubscribeMap?.();
unsubscribeMap = null;
mapStack = controller || null;
const off = controller?.subscribe?.(() => {
if (enabled) sync();
});
unsubscribeMap = typeof off === 'function' ? off : null;
},
enable() {
enabled = true;
sync();
},
disable() {
enabled = false;
sync();
},
update() {
return true;
},
getStats() {
// Without a controller subscription the stats poll doubles as the
// map-stack watch.
if (enabled && !unsubscribeMap) {
const next = resolveHost();
if (next.collection !== hostInfo.collection) sync();
}
return {
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 ? 1 : 0 };
},
destroy() {
enabled = false;
detach(true);
unsubscribeMap?.();
unsubscribeMap = null;
mapStack = null;
viewer = null;
hostInfo = { collection: null, kind: 'none' };
},
};
}
/** One layer per overlay, in panel order. */
export function createOsmOverlayLayers(options) {
return OSM_OVERLAYS.map((overlay) => createOsmOverlayLayer(overlay, options));
}
+167
View File
@@ -0,0 +1,167 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
OSM_OVERLAYS,
createOsmOverlayLayer,
createOsmOverlayLayers,
osmOverlayHiddenReason,
} from './osmOverlays.js';
import { NO_IMAGERY_HOST } from '../maps/imageryHost.js';
function fakeCollection() {
const layers = [];
const removed = [];
return {
layers,
removed,
add(layer) {
layers.push(layer);
},
contains(layer) {
return layers.includes(layer);
},
remove(layer, destroy) {
const index = layers.indexOf(layer);
if (index < 0) return false;
layers.splice(index, 1);
removed.push({ layer, destroy });
if (destroy) layer.destroyed = true;
return true;
},
};
}
function fakeMapStack() {
const subscribers = new Set();
return {
tileset: null,
getImageryHostTileset() {
return this.tileset;
},
subscribe(fn) {
subscribers.add(fn);
return () => subscribers.delete(fn);
},
emit() {
for (const fn of subscribers) fn();
},
subscribers,
};
}
function setup(overlay = OSM_OVERLAYS[0]) {
const globe = fakeCollection();
const viewer = {
imageryLayers: globe,
scene: { globe: { show: true }, requestRender() {} },
};
const created = [];
const layer = createOsmOverlayLayer(overlay, {
createProvider: (descriptor) => ({ descriptor }),
createImageryLayer: (provider) => {
const imagery = { provider, destroyed: false };
created.push(imagery);
return imagery;
},
});
const mapStack = fakeMapStack();
layer.init(viewer);
layer.attachMapStackController(mapStack);
return { layer, viewer, globe, created, mapStack };
}
test('OSM_OVERLAYS: the two shipped layers, each with a tile template and credit', () => {
assert.deepEqual(
OSM_OVERLAYS.map((o) => o.id),
['sea-marks', 'ski-pistes'],
);
for (const overlay of OSM_OVERLAYS) {
assert.match(overlay.urlTemplate, /^https:\/\/.*\{z\}.*\{x\}.*\{y\}/);
assert.match(overlay.credit, /OpenStreetMap contributors \(ODbL\)/);
}
assert.match(OSM_OVERLAYS[0].credit, /CC BY-SA 2\.0/);
assert.match(OSM_OVERLAYS[1].credit, /www\.opensnowmap\.org/);
});
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.notEqual(createOsmOverlayLayers()[0], layers[0]);
assert.throws(() => createOsmOverlayLayer({ id: 'other' }), /Unknown/);
});
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('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);
layer.enable();
assert.equal(globe.layers.length, 1);
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.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('the overlay follows a map-stack switch onto the Google 3D tileset and back', () => {
const { layer, viewer, globe, mapStack } = setup();
layer.enable();
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, [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, [imagery]);
assert.equal(tileset.imageryLayers.layers.length, 0);
});
test('with no surface to drape on the row explains itself, and recovers when one appears', () => {
const { layer, viewer, globe, mapStack } = setup();
viewer.scene.globe.show = false;
layer.enable();
assert.equal(globe.layers.length, 0);
assert.equal(layer.getStats().error, NO_IMAGERY_HOST);
viewer.scene.globe.show = true;
mapStack.emit();
assert.equal(globe.layers.length, 1);
assert.equal(layer.getStats().error, null);
});
test('destroy removes owned imagery and unsubscribes from the map stack', () => {
const { layer, globe, created, mapStack } = setup();
layer.enable();
layer.destroy();
assert.equal(globe.layers.length, 0);
assert.equal(created[0].destroyed, true);
assert.equal(mapStack.subscribers.size, 0);
});