mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +00:00
fix(satellites): class choices survive a reload and travel in share links
Only catalog and the tracked satellite were saved for the layer, so a class
chip lasted until the next reload and no share link carried it. The choice
is now the satellites option `classes`: one letter per class shown
(`sitpd` by default, `0` for none), with Starlink left to `catalog`, and
the layer takes the letters back through setParams. A partial request
(`{debris: false}`) hides only what it names.
This commit is contained in:
1 parent
8ae56587c3
commit
c84cfcadeb
9 files changed
+148
-12
No files matched your search
@@ -2,6 +2,7 @@
|
||||
// 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 { normalizeSatelliteClassMask } from './satelliteClassMask.js';
|
||||
import reservationRows from './layerStateTokenReservations.json' with { type: 'json' };
|
||||
|
||||
const VALID_DISPOSITIONS = new Set([
|
||||
@@ -416,6 +417,16 @@ const OPTION_GROUPS = Object.freeze({
|
||||
dense: 'd',
|
||||
}),
|
||||
integerOption('selectedSatTrackingId', 't', null),
|
||||
// Every class but Starlink shows by default (satellites/classes.js).
|
||||
Object.freeze({
|
||||
key: 'classes',
|
||||
token: 'k',
|
||||
defaultValue: 'sitpd',
|
||||
normalize: normalizeSatelliteClassMask,
|
||||
encode: (value) => value,
|
||||
decode: (value) =>
|
||||
typeof value === 'string' ? normalizeSatelliteClassMask(value) : null,
|
||||
}),
|
||||
]),
|
||||
cctv: Object.freeze([
|
||||
enumOption('coverageMode', 'c', 'on', ['off', 'on', 'viewshed'], {
|
||||
|
||||
@@ -970,6 +970,41 @@ test('a direct Context selection promotes its owned tracker layer before persist
|
||||
coordinator.destroy();
|
||||
});
|
||||
|
||||
test('a satellite class choice is saved, shared and restored', async () => {
|
||||
const storage = memoryStorage();
|
||||
const manager = productionManager();
|
||||
const coordinator = new LayerStateCoordinator(manager, shareSink(), {
|
||||
storage,
|
||||
});
|
||||
await coordinator.start();
|
||||
await manager.setEnabled('satellites', true, { origin: 'user' });
|
||||
assert.equal(
|
||||
coordinator.getDurableState().options.satellites.classes,
|
||||
'sitpd',
|
||||
'every class but Starlink by default',
|
||||
);
|
||||
manager.setLayerParams(
|
||||
'satellites',
|
||||
{ classes: { debris: false } },
|
||||
{ origin: 'user' },
|
||||
);
|
||||
const durable = coordinator.getDurableState();
|
||||
assert.equal(durable.options.satellites.classes, 'sitp');
|
||||
assert.equal(
|
||||
new URLSearchParams(encode(durable)).get('lo')?.includes('s.k.sitp'),
|
||||
true,
|
||||
);
|
||||
coordinator.destroy();
|
||||
|
||||
// A reload hands the layer its letters back (the layer decodes them:
|
||||
// src/data/satelliteClass.test.mjs).
|
||||
const reloaded = productionManager();
|
||||
const next = new LayerStateCoordinator(reloaded, shareSink(), { storage });
|
||||
await next.start();
|
||||
assert.equal(reloaded.getLayerParams('satellites')?.classes, 'sitp');
|
||||
next.destroy();
|
||||
});
|
||||
|
||||
test('explicit Flight to Satellite replacement keeps the new Satellite ID durable', async () => {
|
||||
const manager = productionManager();
|
||||
const coordinator = new LayerStateCoordinator(manager, shareSink(), {
|
||||
|
||||
@@ -102,12 +102,13 @@ const preWildlifeRows = reservationRows.filter(
|
||||
);
|
||||
|
||||
function writeCodecFixture(cwd, rows, entries) {
|
||||
const providerCodec = path.join(cwd, 'src/data/idSwitches.js');
|
||||
mkdirSync(path.dirname(providerCodec), { recursive: true });
|
||||
writeFileSync(
|
||||
providerCodec,
|
||||
readFileSync(new URL('./idSwitches.js', import.meta.url)),
|
||||
);
|
||||
mkdirSync(path.join(cwd, 'src/data'), { recursive: true });
|
||||
// The codec's own imports, beside it.
|
||||
for (const name of ['idSwitches.js', 'satelliteClassMask.js'])
|
||||
writeFileSync(
|
||||
path.join(cwd, 'src/data', name),
|
||||
readFileSync(new URL(`./${name}`, import.meta.url)),
|
||||
);
|
||||
let source = readFileSync(CODEC_SOURCE_PATH, 'utf8');
|
||||
if (!rows.some(([id]) => id === 'wildlife-sightings'))
|
||||
source = source.replace(
|
||||
|
||||
@@ -469,6 +469,18 @@ test('every class but Starlink shows by default; each class chip turns its class
|
||||
satellitesLayer.setParams(chip('imaging').params);
|
||||
assert.deepEqual(shown(), [25544, 40001], 'telecom back on, imaging off');
|
||||
assert.equal(satellitesLayer.setParams({ classes: 'all' }), false, 'malformed request refused');
|
||||
|
||||
// A saved or shared choice comes back as class letters.
|
||||
assert.equal(satellitesLayer.setParams({ classes: 'sip' }), true);
|
||||
assert.deepEqual(satellitesLayer.getParams().classes, {
|
||||
stations: true,
|
||||
imaging: true,
|
||||
telecom: false,
|
||||
starlink: false,
|
||||
probes: true,
|
||||
debris: false,
|
||||
});
|
||||
assert.deepEqual(shown(), [25544, 40002], 'restored from letters');
|
||||
} finally {
|
||||
console.log = log;
|
||||
console.warn = warn;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @file The satellites layer's class choice as share-link and saved-state
|
||||
* letters. Shared by the layer-state codec and the layer, which takes a saved
|
||||
* choice back in this form.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Satellite classes shown, as one letter each in legend order: stations,
|
||||
* imaging, telecom, probes, debris (`sitpd`, every one; `0`, none).
|
||||
* Starlink is the `catalog` option's, so it has no letter here.
|
||||
*/
|
||||
const SATELLITE_CLASS_LETTERS = Object.freeze([
|
||||
['stations', 's'],
|
||||
['imaging', 'i'],
|
||||
['telecom', 't'],
|
||||
['probes', 'p'],
|
||||
['debris', 'd'],
|
||||
]);
|
||||
const SATELLITE_CLASS_MASK = /^(0|s?i?t?p?d?)$/;
|
||||
|
||||
/**
|
||||
* The canonical class letters for a class choice (an object of booleans, as
|
||||
* the layer's `classes` param, or a partial one) or for letters already,
|
||||
* else null.
|
||||
* @param {unknown} value
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function normalizeSatelliteClassMask(value) {
|
||||
if (typeof value === 'string')
|
||||
return value && SATELLITE_CLASS_MASK.test(value) ? value : null;
|
||||
if (!value || typeof value !== 'object') return null;
|
||||
// A class the choice leaves out keeps its default, which is shown: a
|
||||
// partial request (`{debris: false}`) hides only what it names.
|
||||
const letters = SATELLITE_CLASS_LETTERS.filter(([id]) => value[id] !== false)
|
||||
.map(([, letter]) => letter)
|
||||
.join('');
|
||||
return letters || '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* The class choice the letters stand for, without Starlink, or null.
|
||||
* @param {string} mask
|
||||
* @returns {Record<string, boolean>|null}
|
||||
*/
|
||||
export function decodeSatelliteClassMask(mask) {
|
||||
const normalized = normalizeSatelliteClassMask(mask);
|
||||
if (normalized === null || typeof mask !== 'string') return null;
|
||||
return Object.fromEntries(
|
||||
SATELLITE_CLASS_LETTERS.map(([id, letter]) => [
|
||||
id,
|
||||
normalized.includes(letter),
|
||||
]),
|
||||
);
|
||||
}
|
||||
Vendored
+11
-4
@@ -1,4 +1,5 @@
|
||||
import { satelliteClassText } from '../../data/satelliteClass.js';
|
||||
import { decodeSatelliteClassMask } from '../../data/satelliteClassMask.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import {
|
||||
@@ -493,16 +494,22 @@ export function createControls({ state: layerState, services, parts, source }) {
|
||||
let catalog = params.catalog;
|
||||
if (catalog !== undefined && catalog !== 'core' && catalog !== 'dense')
|
||||
return false;
|
||||
// A saved or shared choice arrives as class letters (layerState.js).
|
||||
const requestedClasses =
|
||||
typeof params.classes === 'string'
|
||||
? decodeSatelliteClassMask(params.classes)
|
||||
: params.classes;
|
||||
if (params.classes !== undefined) {
|
||||
if (!params.classes || typeof params.classes !== 'object') return false;
|
||||
if (!requestedClasses || typeof requestedClasses !== 'object')
|
||||
return false;
|
||||
// Starlink's toggle is the dense catalog: its points load on demand.
|
||||
if (
|
||||
typeof params.classes.starlink === 'boolean' &&
|
||||
typeof requestedClasses.starlink === 'boolean' &&
|
||||
catalog === undefined
|
||||
)
|
||||
catalog = params.classes.starlink ? 'dense' : 'core';
|
||||
catalog = requestedClasses.starlink ? 'dense' : 'core';
|
||||
layerState._params.classes = mergeClassVisibility(
|
||||
params.classes,
|
||||
requestedClasses,
|
||||
_classes(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -273,7 +273,11 @@ export const SHARE_VIEW_LAYER_OPTION_ABSENT = Object.freeze({
|
||||
selectedFlightsTrackingId: null,
|
||||
selectedMilitaryTrackingId: null,
|
||||
}),
|
||||
satellites: Object.freeze({ catalog: 'core', selectedSatTrackingId: null }),
|
||||
satellites: Object.freeze({
|
||||
catalog: 'core',
|
||||
selectedSatTrackingId: null,
|
||||
classes: 'sitpd',
|
||||
}),
|
||||
cctv: Object.freeze({
|
||||
coverageMode: 'on',
|
||||
showProjection: true,
|
||||
|
||||
Reference in new issue
Block a user