From b69d34bad2e60b0de78b2514b37f998495adf651 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Fri, 2 Oct 2026 18:19:33 -0700 Subject: [PATCH] Frame a view that only follows an aircraft where the aircraft is Asked for an aircraft's cockpit view, assistants passed only the follow, which failed for want of an area or camera, so every such request took a second call. show_in_gods_eye_view now looks the aircraft up and frames the view on it, and says so when the aircraft is not reported. --- src/tools/globePanel.test.mjs | 33 +++++++++++++++++++++++++ src/tools/queries/share.js | 46 ++++++++++++++++++++++++++++------- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/tools/globePanel.test.mjs b/src/tools/globePanel.test.mjs index ea67bb6e3..51270d884 100644 --- a/src/tools/globePanel.test.mjs +++ b/src/tools/globePanel.test.mjs @@ -75,3 +75,36 @@ test('show_in_gods_eye_view names the panel and shows a view another answer retu (error) => error.code === 'invalid_arguments', ); }); + +test('a view that only follows an aircraft is framed where the aircraft is', async () => { + const { resolveViewArguments } = await import('./queries/share.js'); + const tools = { + has: (name) => name === 'find_aircraft', + call: async (name, args) => ({ + data: { + rows: + args.icao24 === 'ae1234' + ? [{ id: 'ae1234', lat: 32.7, lon: -117.2 }] + : [], + }, + }), + }; + const { view, label } = await resolveViewArguments( + { follow: { kind: 'military_aircraft', id: 'ae1234', cockpit: true } }, + { services: {}, tools }, + ); + assert.deepEqual( + [view.camera.lat, view.camera.lon, view.follow.cockpit], + [32.7, -117.2, true], + ); + assert.equal(label, 'military aircraft ae1234'); + await assert.rejects( + resolveViewArguments( + { follow: { kind: 'aircraft', id: 'abc999' } }, + { services: {}, tools }, + ), + (error) => + error.code === 'invalid_arguments' && + /not reported now/.test(error.message), + ); +}); diff --git a/src/tools/queries/share.js b/src/tools/queries/share.js index fb51f6a01..0c59ca7f0 100644 --- a/src/tools/queries/share.js +++ b/src/tools/queries/share.js @@ -18,11 +18,30 @@ export const VIEW_ARGUMENTS = Object.freeze({ ...VIEW_PROPERTIES, }); +// Altitude a view framed on a followed aircraft starts from. +const FOLLOW_ALTITUDE_M = 20_000; + +/** Where a followed aircraft is now, or null when no feed reports it. */ +async function followedAircraftPosition(follow, tools) { + if (!['aircraft', 'military_aircraft'].includes(follow?.kind)) return null; + if (!tools?.has('find_aircraft')) return null; + try { + const { data } = await tools.call('find_aircraft', { icao24: follow.id }); + const row = data?.rows?.find( + (item) => Number.isFinite(item.lat) && Number.isFinite(item.lon), + ); + return row ? { lat: row.lat, lon: row.lon } : null; + } catch { + return null; + } +} + /** * The view described by tool arguments. An area frames the camera straight - * down over it; camera fields given alongside it override the framing. + * down over it; camera fields given alongside it override the framing. A + * view that only follows an aircraft is framed where the aircraft is now. */ -export async function resolveViewArguments(args, { services, signal }) { +export async function resolveViewArguments(args, { services, signal, tools }) { const base = args.view && typeof args.view === 'object' ? args.view : {}; let camera = args.area ? { ...args.camera } @@ -43,11 +62,19 @@ export async function resolveViewArguments(args, { services, signal }) { }) : { ...framed, ...camera }; } - if (!Number.isFinite(camera.lat) || !Number.isFinite(camera.lon)) - throw new ToolError( - 'invalid_arguments', - 'Give a view, an area, or a camera with lat and lon', - ); + const follow = args.follow ?? base.follow ?? null; + if (!Number.isFinite(camera.lat) || !Number.isFinite(camera.lon)) { + const position = await followedAircraftPosition(follow, tools); + if (!position) + throw new ToolError( + 'invalid_arguments', + follow + ? `${follow.id} is not reported now; give an area or a camera with lat and lon too` + : 'Give a view, an area, or a camera with lat and lon', + ); + camera = { altitude_m: FOLLOW_ALTITUDE_M, ...camera, ...position }; + label = `${follow.kind === 'military_aircraft' ? 'military aircraft' : 'aircraft'} ${follow.id}`; + } let view; try { view = createView({ @@ -55,7 +82,7 @@ export async function resolveViewArguments(args, { services, signal }) { layers: args.layers ?? base.layers, style: args.style ?? base.style ?? null, map: args.map ?? base.map ?? null, - follow: args.follow ?? base.follow ?? null, + follow, annotations: args.annotations ?? base.annotations ?? [], }); } catch (error) { @@ -94,11 +121,12 @@ export const showInGodsEyeView = defineTool({ }, requires: ['app'], ui: { resourceUri: GLOBE_PANEL_URI }, - async run(args, { services, signal }) { + async run(args, { services, signal, tools }) { const base = appBase(services); const { view, label } = await resolveViewArguments(args, { services, signal, + tools, }); const url = viewUrl(base, view); return {