mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-07 03:44:00 +00:00
Preserve radio playback and imagery comparisons when switching planets
This commit is contained in:
1 parent
b98cbeebf2
commit
b01f1ee7dc
9 files changed
+246
-18
No files matched your search
@@ -36,3 +36,73 @@ export const stations = Array.from({ length: 750 }, (_, index) => {
|
||||
bitrate: 128,
|
||||
};
|
||||
});
|
||||
|
||||
export async function installRadioFixture(page) {
|
||||
await page.evaluateOnNewDocument((stations) => {
|
||||
localStorage.clear();
|
||||
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
|
||||
window.__qaRadio = {
|
||||
stations,
|
||||
generation: 1,
|
||||
plays: [],
|
||||
delay: 0,
|
||||
response: 'normal',
|
||||
pending: null,
|
||||
};
|
||||
HTMLMediaElement.prototype.play = function () {
|
||||
window.qaPlayer = this;
|
||||
window.__qaRadio.plays.push(this.src);
|
||||
if (window.__qaRadio.delayPlay) {
|
||||
window.__qaRadio.delayPlay = false;
|
||||
return new Promise((resolve) => {
|
||||
window.__qaRadio.releasePlay = () => {
|
||||
this.dispatchEvent(new Event('playing'));
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
queueMicrotask(() => this.dispatchEvent(new Event('playing')));
|
||||
return Promise.resolve();
|
||||
};
|
||||
const fetch = window.fetch.bind(window);
|
||||
window.fetch = async (input, init) => {
|
||||
const address = String(
|
||||
typeof input === 'string' || input instanceof URL ? input : input.url,
|
||||
);
|
||||
if (address.includes('/api/radio/stations')) {
|
||||
const qa = window.__qaRadio;
|
||||
qa.reads = (qa.reads || 0) + 1;
|
||||
const response = {
|
||||
stations: qa.response === 'empty' ? [] : qa.stations,
|
||||
updatedAt: new Date().toISOString(),
|
||||
stale: false,
|
||||
degraded: false,
|
||||
acceptedGeneration: qa.generation,
|
||||
catalogInstance: 'w2-radio-fixture',
|
||||
};
|
||||
const mode = qa.response;
|
||||
const delay = qa.delay;
|
||||
qa.delay = 0;
|
||||
if (delay)
|
||||
await new Promise((resolve) => {
|
||||
qa.pending = resolve;
|
||||
});
|
||||
init?.signal?.throwIfAborted();
|
||||
return new Response(
|
||||
mode === 'failure' ? '{}' : JSON.stringify(response),
|
||||
{
|
||||
status: mode === 'failure' ? 503 : 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
);
|
||||
}
|
||||
if (address.includes('/api/radio/click/'))
|
||||
return new Response(null, { status: 204 });
|
||||
if (address.includes('/api/flights')) return new Response('{"states":[]}');
|
||||
if (address.includes('/api/military')) return new Response('{"ac":[]}');
|
||||
if (address.includes('/api/vessels'))
|
||||
return new Response('{"connected":false,"vessels":[]}');
|
||||
return fetch(input, init);
|
||||
};
|
||||
}, stations);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import assert from 'node:assert/strict';
|
||||
import { stations, installRadioFixture } from './fixtures/w2/radio.mjs';
|
||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||
import { switchGlobe } from './qa-switch-globe.mjs';
|
||||
const url = new URL(process.argv[2]);
|
||||
if (
|
||||
!['localhost', '127.0.0.1'].includes(url.hostname) ||
|
||||
['', '4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
|
||||
url.port,
|
||||
)
|
||||
)
|
||||
throw Error('Local keyless preview required');
|
||||
url.search = '?engine=navara&welcome=0';
|
||||
url.hash = 'v=2&lat=30.27&lon=-97.74&alt=12000&pitch=-90&map=sentinel2&l=';
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--use-angle=metal', '--enable-gpu'],
|
||||
});
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await blockPaidQaMaps(page);
|
||||
const errors = [];
|
||||
page.on('pageerror', (e) => errors.push(e.message));
|
||||
await installRadioFixture(page);
|
||||
await page.goto(url.href);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.engine?.kind === 'navara' &&
|
||||
document.querySelector('#loading-screen.hidden'),
|
||||
);
|
||||
await page.evaluate(async (id) => {
|
||||
const { dataManager, styleManager, tileset } = window.__godsEyeView;
|
||||
await styleManager.initialRestorePromise;
|
||||
if (window.__GOOGLE_MAPS_API_KEY__ || tileset)
|
||||
throw Error('Expected keyless preview');
|
||||
window.qaLayer = dataManager.layers.get('radio').module;
|
||||
await dataManager.setEnabled('radio', true, { origin: 'user' });
|
||||
}, stations[0].id);
|
||||
await page.waitForFunction(() => window.qaLayer?.getStats().count === 750);
|
||||
await page.evaluate((id) => {
|
||||
window.qaLayer.selectStation(id, {
|
||||
autoplay: true,
|
||||
focus: false,
|
||||
origin: 'user',
|
||||
});
|
||||
window.qaLayer.setRadioVolume(0.36);
|
||||
}, stations[10].id);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__qaRadio.plays.length === 1 &&
|
||||
window.qaLayer.getUIState().audioState === 'playing',
|
||||
);
|
||||
const first = await page.evaluate(() => {
|
||||
window.qaAudio = window.qaPlayer;
|
||||
window.qaSnapshot = window.qaLayer.getAcceptedCatalogSnapshot();
|
||||
return window.__qaRadio.reads;
|
||||
});
|
||||
for (const kind of ['cesium', 'navara']) {
|
||||
await switchGlobe(page, kind);
|
||||
const state = await page.evaluate(() => ({
|
||||
same:
|
||||
window.qaLayer ===
|
||||
window.__godsEyeView.dataManager.layers.get('radio').module,
|
||||
player: window.qaAudio === window.qaPlayer,
|
||||
snapshot:
|
||||
window.qaSnapshot === window.qaLayer.getAcceptedCatalogSnapshot(),
|
||||
plays: window.__qaRadio.plays.length,
|
||||
reads: window.__qaRadio.reads,
|
||||
state: window.qaLayer.getUIState(),
|
||||
count: window.qaLayer.getStats().count,
|
||||
}));
|
||||
console.log(
|
||||
kind,
|
||||
state.count,
|
||||
state.state.selected?.id,
|
||||
state.state.audioState,
|
||||
state.plays,
|
||||
state.reads,
|
||||
);
|
||||
assert.ok(state.same && state.player && state.snapshot);
|
||||
assert.equal(state.plays, 1);
|
||||
assert.equal(state.reads, first);
|
||||
assert.equal(state.count, 750);
|
||||
assert.equal(state.state.audioState, 'playing');
|
||||
assert.equal(state.state.selected.id, stations[10].id);
|
||||
assert.equal(state.state.volume, 0.36);
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
console.log(
|
||||
'PASS one live radio player, one station directory and the same selection',
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import {switchGlobe} from './qa-switch-globe.mjs';
|
||||
import fs from 'node:fs/promises';
|
||||
import puppeteer from 'puppeteer';
|
||||
import sharp from 'sharp';
|
||||
@@ -220,6 +221,27 @@ try {
|
||||
await sleep(2500);
|
||||
image = await capture('ab');
|
||||
check('A/B dates on opposite sides', red(image.left) && green(image.right));
|
||||
if (args.includes('--switch')) {
|
||||
const first = await page.evaluate(() => {
|
||||
window.qaImageryOwner = window.__gevRecentImagery.layer;
|
||||
return window.qaImageryOwner.diagnostics().pins;
|
||||
});
|
||||
const reads = () => report.requests.filter(r => r.host === 'cmr.earthdata.nasa.gov').length;
|
||||
const expectedReads = reads();
|
||||
const initial = await page.evaluate(() => window.__godsEyeView.engine.kind);
|
||||
for (const kind of [initial === 'navara' ? 'cesium' : 'navara', initial]) {
|
||||
await switchGlobe(page, kind);
|
||||
const state = await page.evaluate(() => ({
|
||||
same: window.__gevRecentImagery.layer === window.qaImageryOwner,
|
||||
...window.qaImageryOwner.diagnostics(),
|
||||
}));
|
||||
check(kind+' holds both original pins',state.same && state.mode === 'ab' && state.pins.a === first.a && state.pins.b === first.b);
|
||||
await sleep(1800);
|
||||
const pixels=await capture('switch-'+kind);
|
||||
check(kind+' renders both pinned dates',red(pixels.left) && green(pixels.right));
|
||||
check(kind+' reuses the STAC search', reads() === expectedReads);
|
||||
}
|
||||
}
|
||||
await page.evaluate(() => window.__gevRecentImagery.layer.swapSides());
|
||||
image = await capture('swap');
|
||||
check('swap reverses dates', green(image.left) && red(image.right));
|
||||
|
||||
@@ -249,7 +249,7 @@ export function createApplicationCatalog({
|
||||
['cctv', () => createApplicationCctv({ surface, source: sources.cctv })],
|
||||
[
|
||||
'radio',
|
||||
() => createApplicationRadio({ surface, source: sources.radio }),
|
||||
() => sessionModel('radio', () => createApplicationRadio({ surface, source: sources.radio })),
|
||||
],
|
||||
[
|
||||
'transit',
|
||||
@@ -266,9 +266,9 @@ export function createApplicationCatalog({
|
||||
[
|
||||
'recent-imagery',
|
||||
() =>
|
||||
createApplicationRecentImagery({
|
||||
sessionModel('recent-imagery', () => createApplicationRecentImagery({
|
||||
source: sources['recent-imagery'],
|
||||
}),
|
||||
})),
|
||||
],
|
||||
['ais-live-vessels', vessels],
|
||||
['military-installations', installations],
|
||||
|
||||
@@ -17,6 +17,15 @@ export function createLifecycle({
|
||||
const methods = {
|
||||
/** Initialize scene resources and the single audio element. */
|
||||
init(viewer, options = {}) {
|
||||
this.attachRenderer(viewer, options);
|
||||
layerState._sessionGeneration += 1;
|
||||
parts.clustering.resetRadioClusterOverlayIdentities();
|
||||
parts.playback.installAudio();
|
||||
clearOverlaySource(RADIO_OVERLAY_SOURCE_ID);
|
||||
setOverlaySourceVisible(RADIO_OVERLAY_SOURCE_ID, false);
|
||||
},
|
||||
|
||||
attachRenderer(viewer, options = {}) {
|
||||
if (parts.renderer?.ready)
|
||||
throw new Error('Radio layer is already initialized');
|
||||
if (typeof rendererFactory !== 'function')
|
||||
@@ -29,11 +38,22 @@ export function createLifecycle({
|
||||
engine: options.engine,
|
||||
});
|
||||
parts.renderer.init(viewer, options);
|
||||
layerState._sessionGeneration += 1;
|
||||
parts.clustering.resetRadioClusterOverlayIdentities();
|
||||
parts.playback.installAudio();
|
||||
if (layerState._enabled) {
|
||||
parts.rendering.drawStations(layerState._stations);
|
||||
parts.interaction.syncRadioLifecyclePresentation();
|
||||
}
|
||||
},
|
||||
|
||||
detachRenderer() {
|
||||
parts.interaction.removeInteraction();
|
||||
clearTimeout(layerState._overlayPublishTimer);
|
||||
layerState._overlayPublishTimer = null;
|
||||
parts.renderer?.destroy();
|
||||
parts.renderer = null;
|
||||
layerState._renderById.clear();
|
||||
layerState._selectedPosition = null;
|
||||
layerState._lastHorizonCameraPosition = null;
|
||||
clearOverlaySource(RADIO_OVERLAY_SOURCE_ID);
|
||||
setOverlaySourceVisible(RADIO_OVERLAY_SOURCE_ID, false);
|
||||
},
|
||||
|
||||
/** Show stations. Enabling or preset restoration never starts audio. */
|
||||
|
||||
@@ -46,7 +46,7 @@ export function createRendering({
|
||||
|
||||
function publishRadioOverlayEntries() {
|
||||
layerState._overlayPublishTimer = null;
|
||||
if (!parts.interaction.radioPresentationAllowed()) {
|
||||
if (!parts.renderer?.ready || !parts.interaction.radioPresentationAllowed()) {
|
||||
parts.clustering.resetRadioClusterOverlayIdentities();
|
||||
layerState._overlayDiagnostics =
|
||||
parts.clustering.emptyRadioOverlayDiagnostics();
|
||||
@@ -257,7 +257,12 @@ export function createRendering({
|
||||
layerState._selectedId = null;
|
||||
}
|
||||
|
||||
parts.renderer?.clearStations();
|
||||
drawStations(stations);
|
||||
}
|
||||
|
||||
function drawStations(stations) {
|
||||
if (!parts.renderer?.ready) return;
|
||||
parts.renderer.clearStations();
|
||||
layerState._renderById.clear();
|
||||
for (const station of stations) {
|
||||
const position = markerPosition(station);
|
||||
@@ -278,5 +283,6 @@ export function createRendering({
|
||||
scheduleRadioOverlayPublish,
|
||||
updateRenderVisibility,
|
||||
reconcileStations,
|
||||
drawStations,
|
||||
};
|
||||
}
|
||||
@@ -808,15 +808,34 @@ export function createRecentImageryLayer({
|
||||
source: 'NASA GIBS · HLS + VIIRS',
|
||||
updateInterval: 0,
|
||||
|
||||
init(viewer, { engine } = {}) {
|
||||
init(viewer, options) {
|
||||
return this.attachRenderer(viewer, options);
|
||||
},
|
||||
|
||||
attachRenderer(viewer, { engine } = {}) {
|
||||
_viewer = viewer || null;
|
||||
_engine = engine || null;
|
||||
if (rendererFactory && !renderer)
|
||||
renderer = rendererFactory({ viewer, engine });
|
||||
if (!renderer) throw new TypeError('Recent imagery requires a renderer');
|
||||
if (_enabled) syncRender();
|
||||
return true;
|
||||
},
|
||||
|
||||
async detachRenderer() {
|
||||
_unsubscribeMap?.();
|
||||
_unsubscribeMap = null;
|
||||
_mapStackController = null;
|
||||
const lease = _lease;
|
||||
forgetLease();
|
||||
renderer?.destroy();
|
||||
renderer = null;
|
||||
_hostInfo = {collection: null, kind: 'none'};
|
||||
_viewer = _engine = _tileset = null;
|
||||
_keptGeneration = null;
|
||||
await Promise.all([lease?.release(), _releasing]);
|
||||
},
|
||||
|
||||
enable(viewer) {
|
||||
if (_destroyed) return false;
|
||||
if (viewer) _viewer = viewer;
|
||||
@@ -836,7 +855,7 @@ export function createRecentImageryLayer({
|
||||
applyPreview(_followKey);
|
||||
// A pinned or previewed day that was still being probed drapes now.
|
||||
const shown = plan();
|
||||
const owned = renderer.getOwned?.() || {};
|
||||
const owned = renderer?.getOwned?.() || {};
|
||||
const drapesChanged = SLOT_IDS.some(
|
||||
(slotId) => (owned[slotId]?.key || null) !== shown[slotId],
|
||||
);
|
||||
@@ -1225,7 +1244,7 @@ export function createRecentImageryLayer({
|
||||
const next = clampUnit(value, _alpha);
|
||||
if (next === _alpha) return;
|
||||
_alpha = next;
|
||||
for (const slotId of SLOT_IDS) renderer.setAlpha(slotId, next);
|
||||
for (const slotId of SLOT_IDS) renderer?.setAlpha(slotId, next);
|
||||
notify('alpha');
|
||||
},
|
||||
|
||||
@@ -1253,7 +1272,7 @@ export function createRecentImageryLayer({
|
||||
_swapped = false;
|
||||
if (_alpha !== 1) {
|
||||
_alpha = 1;
|
||||
for (const slotId of SLOT_IDS) renderer.setAlpha(slotId, 1);
|
||||
for (const slotId of SLOT_IDS) renderer?.setAlpha(slotId, 1);
|
||||
}
|
||||
_notice = msg('Box and images cleared');
|
||||
syncRender();
|
||||
|
||||
@@ -228,9 +228,7 @@ const FRAME_TARGETS = new Map([
|
||||
|
||||
/**
|
||||
* The tools that still need the Cesium viewer or a subsystem built on it:
|
||||
* the scene director, annotations and routes over them, the camera verbs
|
||||
* (orbit, pan, tilt), Contacts (the context mode), Radio and
|
||||
* CCTV. Without a viewer (the Navara engine) each answers a structured
|
||||
* annotations and routes over them, camera verbs, the context mode and CCTV. Without a viewer (the Navara engine) each answers a structured
|
||||
* refusal (UNAVAILABLE_ON_ENGINE) instead of running.
|
||||
*/
|
||||
export const VIEWER_ONLY_ACTIONS = Object.freeze([
|
||||
@@ -239,7 +237,6 @@ export const VIEWER_ONLY_ACTIONS = Object.freeze([
|
||||
'fly_route',
|
||||
'move_camera',
|
||||
'set_context_mode',
|
||||
'control_radio',
|
||||
'control_cctv',
|
||||
]);
|
||||
/** The refusal code of a viewer-only tool on an engine without a viewer. */
|
||||
|
||||
@@ -94,7 +94,6 @@ test('the viewer-only actions are one exported list of real tools', () => {
|
||||
'fly_route',
|
||||
'move_camera',
|
||||
'set_context_mode',
|
||||
'control_radio',
|
||||
'control_cctv',
|
||||
]);
|
||||
assert.ok(Object.isFrozen(VIEWER_ONLY_ACTIONS));
|
||||
|
||||
Reference in new issue
Block a user