mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-09 12:54:01 +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,
|
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 fs from 'node:fs/promises';
|
||||||
import puppeteer from 'puppeteer';
|
import puppeteer from 'puppeteer';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
@@ -220,6 +221,27 @@ try {
|
|||||||
await sleep(2500);
|
await sleep(2500);
|
||||||
image = await capture('ab');
|
image = await capture('ab');
|
||||||
check('A/B dates on opposite sides', red(image.left) && green(image.right));
|
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());
|
await page.evaluate(() => window.__gevRecentImagery.layer.swapSides());
|
||||||
image = await capture('swap');
|
image = await capture('swap');
|
||||||
check('swap reverses dates', green(image.left) && red(image.right));
|
check('swap reverses dates', green(image.left) && red(image.right));
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ export function createApplicationCatalog({
|
|||||||
['cctv', () => createApplicationCctv({ surface, source: sources.cctv })],
|
['cctv', () => createApplicationCctv({ surface, source: sources.cctv })],
|
||||||
[
|
[
|
||||||
'radio',
|
'radio',
|
||||||
() => createApplicationRadio({ surface, source: sources.radio }),
|
() => sessionModel('radio', () => createApplicationRadio({ surface, source: sources.radio })),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'transit',
|
'transit',
|
||||||
@@ -266,9 +266,9 @@ export function createApplicationCatalog({
|
|||||||
[
|
[
|
||||||
'recent-imagery',
|
'recent-imagery',
|
||||||
() =>
|
() =>
|
||||||
createApplicationRecentImagery({
|
sessionModel('recent-imagery', () => createApplicationRecentImagery({
|
||||||
source: sources['recent-imagery'],
|
source: sources['recent-imagery'],
|
||||||
}),
|
})),
|
||||||
],
|
],
|
||||||
['ais-live-vessels', vessels],
|
['ais-live-vessels', vessels],
|
||||||
['military-installations', installations],
|
['military-installations', installations],
|
||||||
|
|||||||
@@ -17,6 +17,15 @@ export function createLifecycle({
|
|||||||
const methods = {
|
const methods = {
|
||||||
/** Initialize scene resources and the single audio element. */
|
/** Initialize scene resources and the single audio element. */
|
||||||
init(viewer, options = {}) {
|
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)
|
if (parts.renderer?.ready)
|
||||||
throw new Error('Radio layer is already initialized');
|
throw new Error('Radio layer is already initialized');
|
||||||
if (typeof rendererFactory !== 'function')
|
if (typeof rendererFactory !== 'function')
|
||||||
@@ -29,11 +38,22 @@ export function createLifecycle({
|
|||||||
engine: options.engine,
|
engine: options.engine,
|
||||||
});
|
});
|
||||||
parts.renderer.init(viewer, options);
|
parts.renderer.init(viewer, options);
|
||||||
layerState._sessionGeneration += 1;
|
if (layerState._enabled) {
|
||||||
parts.clustering.resetRadioClusterOverlayIdentities();
|
parts.rendering.drawStations(layerState._stations);
|
||||||
parts.playback.installAudio();
|
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);
|
clearOverlaySource(RADIO_OVERLAY_SOURCE_ID);
|
||||||
setOverlaySourceVisible(RADIO_OVERLAY_SOURCE_ID, false);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Show stations. Enabling or preset restoration never starts audio. */
|
/** Show stations. Enabling or preset restoration never starts audio. */
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function createRendering({
|
|||||||
|
|
||||||
function publishRadioOverlayEntries() {
|
function publishRadioOverlayEntries() {
|
||||||
layerState._overlayPublishTimer = null;
|
layerState._overlayPublishTimer = null;
|
||||||
if (!parts.interaction.radioPresentationAllowed()) {
|
if (!parts.renderer?.ready || !parts.interaction.radioPresentationAllowed()) {
|
||||||
parts.clustering.resetRadioClusterOverlayIdentities();
|
parts.clustering.resetRadioClusterOverlayIdentities();
|
||||||
layerState._overlayDiagnostics =
|
layerState._overlayDiagnostics =
|
||||||
parts.clustering.emptyRadioOverlayDiagnostics();
|
parts.clustering.emptyRadioOverlayDiagnostics();
|
||||||
@@ -257,7 +257,12 @@ export function createRendering({
|
|||||||
layerState._selectedId = null;
|
layerState._selectedId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.renderer?.clearStations();
|
drawStations(stations);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawStations(stations) {
|
||||||
|
if (!parts.renderer?.ready) return;
|
||||||
|
parts.renderer.clearStations();
|
||||||
layerState._renderById.clear();
|
layerState._renderById.clear();
|
||||||
for (const station of stations) {
|
for (const station of stations) {
|
||||||
const position = markerPosition(station);
|
const position = markerPosition(station);
|
||||||
@@ -278,5 +283,6 @@ export function createRendering({
|
|||||||
scheduleRadioOverlayPublish,
|
scheduleRadioOverlayPublish,
|
||||||
updateRenderVisibility,
|
updateRenderVisibility,
|
||||||
reconcileStations,
|
reconcileStations,
|
||||||
|
drawStations,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -808,15 +808,34 @@ export function createRecentImageryLayer({
|
|||||||
source: 'NASA GIBS · HLS + VIIRS',
|
source: 'NASA GIBS · HLS + VIIRS',
|
||||||
updateInterval: 0,
|
updateInterval: 0,
|
||||||
|
|
||||||
init(viewer, { engine } = {}) {
|
init(viewer, options) {
|
||||||
|
return this.attachRenderer(viewer, options);
|
||||||
|
},
|
||||||
|
|
||||||
|
attachRenderer(viewer, { engine } = {}) {
|
||||||
_viewer = viewer || null;
|
_viewer = viewer || null;
|
||||||
_engine = engine || null;
|
_engine = engine || null;
|
||||||
if (rendererFactory && !renderer)
|
if (rendererFactory && !renderer)
|
||||||
renderer = rendererFactory({ viewer, engine });
|
renderer = rendererFactory({ viewer, engine });
|
||||||
if (!renderer) throw new TypeError('Recent imagery requires a renderer');
|
if (!renderer) throw new TypeError('Recent imagery requires a renderer');
|
||||||
|
if (_enabled) syncRender();
|
||||||
return true;
|
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) {
|
enable(viewer) {
|
||||||
if (_destroyed) return false;
|
if (_destroyed) return false;
|
||||||
if (viewer) _viewer = viewer;
|
if (viewer) _viewer = viewer;
|
||||||
@@ -836,7 +855,7 @@ export function createRecentImageryLayer({
|
|||||||
applyPreview(_followKey);
|
applyPreview(_followKey);
|
||||||
// A pinned or previewed day that was still being probed drapes now.
|
// A pinned or previewed day that was still being probed drapes now.
|
||||||
const shown = plan();
|
const shown = plan();
|
||||||
const owned = renderer.getOwned?.() || {};
|
const owned = renderer?.getOwned?.() || {};
|
||||||
const drapesChanged = SLOT_IDS.some(
|
const drapesChanged = SLOT_IDS.some(
|
||||||
(slotId) => (owned[slotId]?.key || null) !== shown[slotId],
|
(slotId) => (owned[slotId]?.key || null) !== shown[slotId],
|
||||||
);
|
);
|
||||||
@@ -1225,7 +1244,7 @@ export function createRecentImageryLayer({
|
|||||||
const next = clampUnit(value, _alpha);
|
const next = clampUnit(value, _alpha);
|
||||||
if (next === _alpha) return;
|
if (next === _alpha) return;
|
||||||
_alpha = next;
|
_alpha = next;
|
||||||
for (const slotId of SLOT_IDS) renderer.setAlpha(slotId, next);
|
for (const slotId of SLOT_IDS) renderer?.setAlpha(slotId, next);
|
||||||
notify('alpha');
|
notify('alpha');
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1253,7 +1272,7 @@ export function createRecentImageryLayer({
|
|||||||
_swapped = false;
|
_swapped = false;
|
||||||
if (_alpha !== 1) {
|
if (_alpha !== 1) {
|
||||||
_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');
|
_notice = msg('Box and images cleared');
|
||||||
syncRender();
|
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 tools that still need the Cesium viewer or a subsystem built on it:
|
||||||
* the scene director, annotations and routes over them, the camera verbs
|
* annotations and routes over them, camera verbs, the context mode and CCTV. Without a viewer (the Navara engine) each answers a structured
|
||||||
* (orbit, pan, tilt), Contacts (the context mode), Radio and
|
|
||||||
* CCTV. Without a viewer (the Navara engine) each answers a structured
|
|
||||||
* refusal (UNAVAILABLE_ON_ENGINE) instead of running.
|
* refusal (UNAVAILABLE_ON_ENGINE) instead of running.
|
||||||
*/
|
*/
|
||||||
export const VIEWER_ONLY_ACTIONS = Object.freeze([
|
export const VIEWER_ONLY_ACTIONS = Object.freeze([
|
||||||
@@ -239,7 +237,6 @@ export const VIEWER_ONLY_ACTIONS = Object.freeze([
|
|||||||
'fly_route',
|
'fly_route',
|
||||||
'move_camera',
|
'move_camera',
|
||||||
'set_context_mode',
|
'set_context_mode',
|
||||||
'control_radio',
|
|
||||||
'control_cctv',
|
'control_cctv',
|
||||||
]);
|
]);
|
||||||
/** The refusal code of a viewer-only tool on an engine without a viewer. */
|
/** 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',
|
'fly_route',
|
||||||
'move_camera',
|
'move_camera',
|
||||||
'set_context_mode',
|
'set_context_mode',
|
||||||
'control_radio',
|
|
||||||
'control_cctv',
|
'control_cctv',
|
||||||
]);
|
]);
|
||||||
assert.ok(Object.isFrozen(VIEWER_ONLY_ACTIONS));
|
assert.ok(Object.isFrozen(VIEWER_ONLY_ACTIONS));
|
||||||
|
|||||||
Reference in new issue
Block a user