mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
fix(voice): Navara refusals name the feature and the way back to Cesium
Review of T8-T9: - The UNAVAILABLE_ON_ENGINE refusal now names the feature (UNAVAILABLE_FEATURES) and suggests the Cesium engine (?engine=cesium): "The cockpit is not available on the Navara engine yet. Switch to the Cesium engine (?engine=cesium) to use it." New fields: feature, suggestion. - set_visual_style and set_post_processing refuse without a viewer (CESIUM_LOOK_ACTIONS): Navara has no visual styles, so they used to answer ok, change nothing and record a style get_current_view_state then reported. - set_layer_visibility on a known layer the engine's catalog left out refuses with the same structured result instead of throwing "Unknown data layer". A name no engine knows still throws. - Removed the dead summarizeEntity and the two helpers only it used. Cesium (viewer present) is unchanged; tests cover each refusal and that the style still applies with a viewer.
This commit is contained in:
1 parent
907cb2092d
commit
bf46eb6d97
2 files changed
+141
-69
No files matched your search
+56
-68
@@ -286,14 +286,53 @@ export const VIEWER_ONLY_ACTIONS = Object.freeze([
|
||||
/** The refusal code of a viewer-only tool on an engine without a viewer. */
|
||||
export const UNAVAILABLE_ON_ENGINE = 'UNAVAILABLE_ON_ENGINE';
|
||||
|
||||
function unavailableOnEngine(name, engine) {
|
||||
/**
|
||||
* The tools that change the look of the Cesium scene (post stages, bloom,
|
||||
* sharpen). The Navara engine has no visual styles yet: without a viewer
|
||||
* these refuse too, rather than answer ok while changing nothing (and
|
||||
* recording a style the view does not show).
|
||||
*/
|
||||
export const CESIUM_LOOK_ACTIONS = Object.freeze([
|
||||
'set_visual_style',
|
||||
'set_post_processing',
|
||||
]);
|
||||
|
||||
/** What each refused tool is, for the refusal's message. */
|
||||
export const UNAVAILABLE_FEATURES = Object.freeze({
|
||||
control_scene: 'The scene director',
|
||||
annotate_map: 'Map annotation',
|
||||
clear_annotations: 'Map annotation',
|
||||
fly_route: 'Route flying',
|
||||
move_camera: 'Camera motion (orbit, pan, tilt)',
|
||||
control_cockpit: 'The cockpit',
|
||||
set_context_mode: 'The Contacts mode',
|
||||
control_radio: 'Radio',
|
||||
control_cctv: 'CCTV',
|
||||
set_visual_style: 'The visual style',
|
||||
set_post_processing: 'Post-processing (bloom, sharpen)',
|
||||
});
|
||||
|
||||
/** The engine to switch to for a refused feature. */
|
||||
export const SWITCH_ENGINE_HINT = '?engine=cesium';
|
||||
|
||||
const engineName = (engine) =>
|
||||
engine?.kind === 'navara' ? 'the Navara engine' : 'this engine';
|
||||
|
||||
function unavailableOnEngine(
|
||||
name,
|
||||
engine,
|
||||
feature = UNAVAILABLE_FEATURES[name],
|
||||
) {
|
||||
const what = feature || name;
|
||||
return {
|
||||
ok: false,
|
||||
action: name,
|
||||
unavailable: true,
|
||||
code: UNAVAILABLE_ON_ENGINE,
|
||||
engine: engine?.kind ?? null,
|
||||
error: 'Unavailable on this engine',
|
||||
feature: what,
|
||||
suggestion: SWITCH_ENGINE_HINT,
|
||||
error: `${what} is not available on ${engineName(engine)} yet. Switch to the Cesium engine (${SWITCH_ENGINE_HINT}) to use it.`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -389,7 +428,10 @@ export function createGevActionRunner({
|
||||
/* Progress is advisory. */
|
||||
}
|
||||
};
|
||||
if (!viewer && VIEWER_ONLY_ACTIONS.includes(name))
|
||||
if (
|
||||
!viewer &&
|
||||
(VIEWER_ONLY_ACTIONS.includes(name) || CESIUM_LOOK_ACTIONS.includes(name))
|
||||
)
|
||||
return unavailableOnEngine(name, engine);
|
||||
|
||||
// Navigation tools interrupt any continuous camera motion (spec §1.1) —
|
||||
@@ -427,6 +469,17 @@ export function createGevActionRunner({
|
||||
throw new Error(`Unknown data layer: ${args.layerId || 'missing'}`);
|
||||
}
|
||||
if (!dataManager.layers.has(layerId)) {
|
||||
// A layer this engine cannot show (the catalog left it out): a
|
||||
// refusal that names it, not an unknown-layer error.
|
||||
if (!viewer && voiceLayer(layerId))
|
||||
return {
|
||||
...unavailableOnEngine(
|
||||
'set_layer_visibility',
|
||||
engine,
|
||||
`The ${layerId} layer`,
|
||||
),
|
||||
layerId,
|
||||
};
|
||||
if (layerId === 'radio') {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -4512,46 +4565,6 @@ function dominantValue(values) {
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeEntity(viewer, entity, { includeProperties = false } = {}) {
|
||||
const now = Cesium.JulianDate.now();
|
||||
if (entity.__gevContextId) {
|
||||
const store = window.__gevContextStore;
|
||||
const record = store?.entities?.get(entity.__gevContextId);
|
||||
if (record) return summarizeContextRecord(record, { includeProperties });
|
||||
}
|
||||
const props = propertyObject(entity);
|
||||
const layerId = entity.__localLayerId || props.layerId || null;
|
||||
const tags = props.tags || {};
|
||||
const label = cleanText(
|
||||
props.name ||
|
||||
tags.name ||
|
||||
tags['name:en'] ||
|
||||
tags.official_name ||
|
||||
tags.operator ||
|
||||
props.operator ||
|
||||
entity.name ||
|
||||
layerTitle(layerId),
|
||||
);
|
||||
const position =
|
||||
entity.__localBaseCartesian ||
|
||||
entity.position?.getValue?.(now) ||
|
||||
polygonCenter(entity, now);
|
||||
const carto = position ? geo.cartographic.fromCartesian(position) : null;
|
||||
return {
|
||||
id: String(entity.id || ''),
|
||||
name: label || layerTitle(layerId),
|
||||
layerId,
|
||||
layerName: layerTitle(layerId),
|
||||
latitude: carto
|
||||
? Number(geo.math.toDegrees(carto.latitude).toFixed(6))
|
||||
: null,
|
||||
longitude: carto
|
||||
? Number(geo.math.toDegrees(carto.longitude).toFixed(6))
|
||||
: null,
|
||||
properties: includeProperties ? compactProperties(props) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeContextRecord(record, { includeProperties = false } = {}) {
|
||||
return {
|
||||
id: String(record.id || ''),
|
||||
@@ -4570,31 +4583,6 @@ function summarizeContextRecord(record, { includeProperties = false } = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function polygonCenter(entity, now) {
|
||||
const hierarchy = entity.polygon?.hierarchy?.getValue?.(now);
|
||||
const positions = hierarchy?.positions;
|
||||
if (!positions?.length) return null;
|
||||
return Cesium.BoundingSphere.fromPoints(positions).center;
|
||||
}
|
||||
|
||||
function propertyObject(entity) {
|
||||
const raw = entity?.properties?.getValue?.(Cesium.JulianDate.now()) || {};
|
||||
return unwrapProperties(raw);
|
||||
}
|
||||
|
||||
function unwrapProperties(value) {
|
||||
if (!value || typeof value !== 'object') return value;
|
||||
if (Array.isArray(value)) return value.map(unwrapProperties);
|
||||
const out = {};
|
||||
for (const [key, entry] of Object.entries(value)) {
|
||||
out[key] =
|
||||
entry && typeof entry.getValue === 'function'
|
||||
? unwrapProperties(entry.getValue(Cesium.JulianDate.now()))
|
||||
: unwrapProperties(entry);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function compactProperties(props) {
|
||||
const preferredKeys = [
|
||||
'name',
|
||||
|
||||
@@ -10,6 +10,8 @@ import * as geo from '../geo/index.js';
|
||||
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
|
||||
import { GEV_ACTION_SCHEMAS } from './actionSchemas.js';
|
||||
import {
|
||||
CESIUM_LOOK_ACTIONS,
|
||||
UNAVAILABLE_FEATURES,
|
||||
UNAVAILABLE_ON_ENGINE,
|
||||
VIEWER_ONLY_ACTIONS,
|
||||
createGevActionRunner,
|
||||
@@ -121,7 +123,13 @@ test('without a viewer the viewer-only actions refuse with a structured result',
|
||||
assert.equal(result.unavailable, true, name);
|
||||
assert.equal(result.code, UNAVAILABLE_ON_ENGINE, name);
|
||||
assert.equal(result.engine, 'navara', name);
|
||||
assert.equal(result.error, 'Unavailable on this engine', name);
|
||||
assert.equal(result.feature, UNAVAILABLE_FEATURES[name], name);
|
||||
assert.equal(result.suggestion, '?engine=cesium', name);
|
||||
assert.equal(
|
||||
result.error,
|
||||
`${UNAVAILABLE_FEATURES[name]} is not available on the Navara engine yet. Switch to the Cesium engine (?engine=cesium) to use it.`,
|
||||
name,
|
||||
);
|
||||
}
|
||||
assert.deepEqual(touched, [], 'a refusal reaches no controller');
|
||||
assert.deepEqual(engine.fake.calls, [], 'a refusal moves no camera');
|
||||
@@ -420,3 +428,79 @@ test('a fresh frame for capture comes from the engine render loop', async () =>
|
||||
globalThis.document = previous;
|
||||
}
|
||||
});
|
||||
|
||||
test('every refused tool names its feature', () => {
|
||||
for (const name of [...VIEWER_ONLY_ACTIONS, ...CESIUM_LOOK_ACTIONS])
|
||||
assert.ok(UNAVAILABLE_FEATURES[name], `${name} has a feature name`);
|
||||
assert.deepEqual(CESIUM_LOOK_ACTIONS, [
|
||||
'set_visual_style',
|
||||
'set_post_processing',
|
||||
]);
|
||||
});
|
||||
|
||||
test('without a viewer the visual style and post-processing refuse and record nothing', async () => {
|
||||
const engine = engineOverAustin({ kind: 'navara' });
|
||||
const calls = [];
|
||||
const styleManager = navigatingStyleManager({
|
||||
setStyle: (...args) => calls.push(['setStyle', ...args]),
|
||||
setBloom: (...args) => calls.push(['setBloom', ...args]) && {},
|
||||
setSharpen: (...args) => calls.push(['setSharpen', ...args]) && {},
|
||||
});
|
||||
const runner = runnerOn(engine, { styleManager });
|
||||
const style = await runner('set_visual_style', { style: 'thermal' });
|
||||
assert.equal(style.ok, false);
|
||||
assert.equal(style.code, UNAVAILABLE_ON_ENGINE);
|
||||
assert.match(
|
||||
style.error,
|
||||
/visual style is not available on the Navara engine/,
|
||||
);
|
||||
const post = await runner('set_post_processing', {
|
||||
bloom: { enabled: true },
|
||||
});
|
||||
assert.equal(post.ok, false);
|
||||
assert.equal(post.code, UNAVAILABLE_ON_ENGINE);
|
||||
assert.deepEqual(calls, [], 'no style is applied or recorded');
|
||||
});
|
||||
|
||||
test('with a viewer the visual style still applies', async () => {
|
||||
const calls = [];
|
||||
const runner = createGevActionRunner({
|
||||
viewer: {
|
||||
clock: { onTick: { addEventListener: () => () => {} } },
|
||||
scene: { canvas: { addEventListener() {}, removeEventListener() {} } },
|
||||
camera: { moveEnd: { addEventListener() {} } },
|
||||
},
|
||||
engine: engineOverAustin(),
|
||||
styleManager: navigatingStyleManager({
|
||||
setStyle: (style) => calls.push(style),
|
||||
}),
|
||||
dataManager: dataManagerWith({}),
|
||||
});
|
||||
const result = await runner('set_visual_style', { style: 'thermal' });
|
||||
assert.equal(result.ok, true);
|
||||
assert.deepEqual(calls, ['thermal']);
|
||||
});
|
||||
|
||||
test('without a viewer a layer the engine lacks refuses instead of throwing', async () => {
|
||||
const engine = engineOverAustin({ kind: 'navara' });
|
||||
const runner = runnerOn(engine, {
|
||||
dataManager: dataManagerWith({ flights: {} }),
|
||||
});
|
||||
const result = await runner('set_layer_visibility', {
|
||||
layerId: 'cctv',
|
||||
enabled: true,
|
||||
});
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.code, UNAVAILABLE_ON_ENGINE);
|
||||
assert.equal(result.layerId, 'cctv');
|
||||
assert.equal(result.suggestion, '?engine=cesium');
|
||||
assert.match(
|
||||
result.error,
|
||||
/The cctv layer is not available on the Navara engine yet/,
|
||||
);
|
||||
await assert.rejects(
|
||||
runner('set_layer_visibility', { layerId: 'no-such-layer', enabled: true }),
|
||||
/Unknown data layer/,
|
||||
'a name no engine knows is still an unknown layer',
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user