diff --git a/src/voice/gevActions.js b/src/voice/gevActions.js index 20a502a82..f7860749f 100644 --- a/src/voice/gevActions.js +++ b/src/voice/gevActions.js @@ -332,7 +332,9 @@ function unavailableOnEngine( engine: engine?.kind ?? null, 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.`, + error: /\blayers$/.test(what) + ? `${what} are not available on ${engineName(engine)} yet. Switch to the Cesium engine (${SWITCH_ENGINE_HINT}) to use them.` + : `${what} is not available on ${engineName(engine)} yet. Switch to the Cesium engine (${SWITCH_ENGINE_HINT}) to use it.`, }; } @@ -1110,6 +1112,29 @@ export function createGevActionRunner({ } if (name === 'analyst_query') { + // Layers this engine's catalog left out cannot be asked: say so rather + // than answer zero. + const missing = !viewer + ? (Array.isArray(args.layers) ? args.layers : []) + .map(normalizeLayerId) + .filter( + (layerId) => + layerId && + voiceLayer(layerId) && + !dataManager.layers.has(layerId), + ) + : []; + if (missing.length) + return { + ...unavailableOnEngine( + 'analyst_query', + engine, + missing.length === 1 + ? `The ${missing[0]} layer` + : `The ${missing.join(', ')} layers`, + ), + layerIds: missing, + }; return runAnalystQuery( ensureAnalystEngine(), dataManager, @@ -2484,6 +2509,8 @@ function stopAllTracking(engine, dataManager) { ['military', 'selectedMilitaryTrackingId'], ['satellites', 'selectedSatTrackingId'], ]) { + // A layer this engine does not have tracks nothing to clear. + if (dataManager.layers && !dataManager.layers.has(layerId)) continue; try { if ( dataManager.setLayerParams( diff --git a/src/voice/gevActionsEngine.test.mjs b/src/voice/gevActionsEngine.test.mjs index 7506fee0c..68e2965ad 100644 --- a/src/voice/gevActionsEngine.test.mjs +++ b/src/voice/gevActionsEngine.test.mjs @@ -504,3 +504,54 @@ test('without a viewer a layer the engine lacks refuses instead of throwing', as 'a name no engine knows is still an unknown layer', ); }); + +test('without a viewer analyst_query on a layer the engine lacks refuses', async () => { + const engine = engineOverAustin({ kind: 'navara' }); + const runner = runnerOn(engine, { + dataManager: dataManagerWith({ flights: {} }), + }); + const one = await runner('analyst_query', { layers: ['cctv'] }); + assert.equal(one.ok, false); + assert.equal(one.code, UNAVAILABLE_ON_ENGINE); + assert.deepEqual(one.layerIds, ['cctv']); + assert.match( + one.error, + /^The cctv layer is not available on the Navara engine yet/, + ); + const two = await runner('analyst_query', { layers: ['cctv', 'vessels'] }); + assert.match( + two.error, + /^The cctv, ais-live-vessels layers are not available on the Navara engine yet\. Switch to the Cesium engine \(\?engine=cesium\) to use them\.$/, + ); +}); + +test('stop_tracking with nothing tracked is a no-op success, also without the layers', async () => { + const engine = engineOverAustin({ kind: 'navara' }); + const dataManager = dataManagerWith({ + flights: { getTrackedInfo: () => null }, + }); + // As the manager answers: a layer it does not hold takes no params. + dataManager.setLayerParams = (id) => dataManager.layers.has(id); + const runner = runnerOn(engine, { dataManager }); + const result = await runner('stop_tracking', {}); + assert.equal(result.ok, true); + assert.deepEqual(result.released, []); +}); + +test('the spoken refusal of an unavailable layer carries its reason', async () => { + const { SPEECH_BUILDERS } = await import('./speech.js'); + const engine = engineOverAustin({ kind: 'navara' }); + const runner = runnerOn(engine, { + dataManager: dataManagerWith({ flights: {} }), + }); + const result = await runner('set_layer_visibility', { + layerId: 'cctv', + enabled: true, + }); + const speech = SPEECH_BUILDERS.set_layer_visibility(result, { + layerId: 'cctv', + enabled: true, + }); + assert.equal(speech.say, result.error); + assert.match(speech.say, /Switch to the Cesium engine/); +}); diff --git a/src/voice/speech.js b/src/voice/speech.js index 31716e03e..8d5f15bf3 100644 --- a/src/voice/speech.js +++ b/src/voice/speech.js @@ -103,6 +103,13 @@ function setLayerVisibility(result, args = {}) { if (result?.cancelled) return null; if (!result?.ok) { const wanted = args.enabled === false ? 'off' : 'on'; + // An engine without the layer says why and what to do (the refusal's + // own sentence), not only that it failed. + if (result?.code === 'UNAVAILABLE_ON_ENGINE' && result.error) + return { + say: result.error, + display: { title: label, lines: [result.error] }, + }; return { say: `Couldn't turn ${wanted} ${label}.`, display: { title: label, lines: [result?.error].filter(Boolean) },