mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Let a view show an aircraft's cockpit view
A view's follow may ask for cockpit view, which the app enters after it follows the aircraft; links cannot carry it and open the app following. Applying a view no longer flies the camera when the followed entity is already there, since the flight ended the follow; it flies only while waiting for the entity. The panel applies its first view again only when it asks for cockpit view.
This commit is contained in:
1 parent
89a448ee83
commit
d4311a9d54
6 files changed
+77
-25
No files matched your search
+6
-3
@@ -91,7 +91,8 @@ aircraft or satellite to follow. `createView` builds and bounds one,
|
|||||||
`viewToParams` and `viewFromParams` write and read it in the share-link
|
`viewToParams` and `viewFromParams` write and read it in the share-link
|
||||||
format the app restores, and `viewUrl` gives the address that opens it. The
|
format the app restores, and `viewUrl` gives the address that opens it. The
|
||||||
style names are the ones share links use. Ships cannot be followed from a
|
style names are the ones share links use. Ships cannot be followed from a
|
||||||
link yet.
|
link yet. An aircraft's `follow` may ask for its cockpit view
|
||||||
|
(`cockpit: true`); links cannot carry that and open the app following it.
|
||||||
|
|
||||||
A view can also carry `annotations`, the marks the app's `annotate_map`
|
A view can also carry `annotations`, the marks the app's `annotate_map`
|
||||||
action draws (pins, highlights, areas, arrows, routes and labels at a place
|
action draws (pins, highlights, areas, arrows, routes and labels at a place
|
||||||
@@ -156,8 +157,10 @@ an app-only tool with `ui: { visibility: ['app'] }`;
|
|||||||
and setup prompts hidden; provider attribution stays. A page that frames it
|
and setup prompts hidden; provider attribution stays. A page that frames it
|
||||||
changes the view by posting `{ type: 'gev:view', id, view }` to the frame.
|
changes the view by posting `{ type: 'gev:view', id, view }` to the frame.
|
||||||
The app applies it through its own actions (style, map, exactly the view's
|
The app applies it through its own actions (style, map, exactly the view's
|
||||||
layers, annotations, the camera, then the followed entity, retried until its
|
layers, annotations, then the followed entity, retried until its layer has
|
||||||
layer has it) and answers `{ type: 'gev:view-applied', id, ok, steps }`. It
|
it, or the camera when nothing is followed or the entity is not there yet,
|
||||||
|
since a camera flight would end the follow; then cockpit view when asked) and
|
||||||
|
answers `{ type: 'gev:view-applied', id, ok, steps }`. It
|
||||||
posts `{ type: 'gev:ready' }` once it can take views, and only its parent
|
posts `{ type: 'gev:ready' }` once it can take views, and only its parent
|
||||||
page can send them. See `src/app/embed.js`.
|
page can send them. See `src/app/embed.js`.
|
||||||
|
|
||||||
|
|||||||
+29
-18
@@ -185,25 +185,36 @@ export async function applyView(
|
|||||||
await act('clear_annotations', {});
|
await act('clear_annotations', {});
|
||||||
if (view.annotations.length)
|
if (view.annotations.length)
|
||||||
await act('annotate_map', { annotations: [...view.annotations] });
|
await act('annotate_map', { annotations: [...view.annotations] });
|
||||||
steps.push({ step: 'camera', ok: await flyTo(viewer, view.camera) });
|
if (!view.follow) {
|
||||||
if (view.follow) {
|
steps.push({ step: 'camera', ok: await flyTo(viewer, view.camera) });
|
||||||
// The followed entity appears once its layer has data; retry until then.
|
return steps;
|
||||||
const args = {
|
|
||||||
query: view.follow.id,
|
|
||||||
layerId: FOLLOW_LAYERS[view.follow.kind],
|
|
||||||
};
|
|
||||||
let followed = false;
|
|
||||||
for (let attempt = 0; attempt < FOLLOW_ATTEMPTS && !followed; attempt++) {
|
|
||||||
if (signal?.aborted) break;
|
|
||||||
try {
|
|
||||||
followed = (await run('track_entity', args))?.ok === true;
|
|
||||||
} catch {
|
|
||||||
followed = false;
|
|
||||||
}
|
|
||||||
if (!followed) await delay(retryMs, signal);
|
|
||||||
}
|
|
||||||
steps.push({ step: 'follow', ok: followed });
|
|
||||||
}
|
}
|
||||||
|
// A followed entity owns the camera, and a camera flight would end the
|
||||||
|
// follow, so fly to the view only while the entity is not there yet. It
|
||||||
|
// appears once its layer has data; retry until then.
|
||||||
|
const layerId = FOLLOW_LAYERS[view.follow.kind];
|
||||||
|
const follow = async () => {
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
(await run('track_entity', { query: view.follow.id, layerId }))?.ok ===
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let followed = await follow();
|
||||||
|
if (!followed) {
|
||||||
|
steps.push({ step: 'camera', ok: await flyTo(viewer, view.camera) });
|
||||||
|
for (let attempt = 1; attempt < FOLLOW_ATTEMPTS && !followed; attempt++) {
|
||||||
|
if (signal?.aborted) break;
|
||||||
|
await delay(retryMs, signal);
|
||||||
|
followed = await follow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps.push({ step: 'follow', ok: followed });
|
||||||
|
if (followed && view.follow.cockpit)
|
||||||
|
await act('control_cockpit', { action: 'enter', targetLayer: layerId });
|
||||||
return steps;
|
return steps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -283,3 +283,30 @@ test('a panel reports a render error in full and stops drawing', () => {
|
|||||||
console.error = original;
|
console.error = original;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a followed aircraft owns the camera, and cockpit view is entered after it', async () => {
|
||||||
|
const viewer = fakeViewer();
|
||||||
|
const { calls, run } = recorder();
|
||||||
|
const steps = await applyView(
|
||||||
|
createView({
|
||||||
|
camera: { lat: 32.7, lon: -117.2 },
|
||||||
|
follow: { kind: 'military_aircraft', id: 'AE1234', cockpit: true },
|
||||||
|
}),
|
||||||
|
{ viewer, dataManager: fakeLayers(new Set()), run, retryMs: 0 },
|
||||||
|
);
|
||||||
|
// Following worked at once, so no camera flight ended it.
|
||||||
|
assert.equal(viewer.flights.length, 0);
|
||||||
|
assert.deepEqual(calls.slice(-2), [
|
||||||
|
['track_entity', { query: 'ae1234', layerId: 'military' }],
|
||||||
|
['control_cockpit', { action: 'enter', targetLayer: 'military' }],
|
||||||
|
]);
|
||||||
|
assert.ok(steps.every((step) => step.ok));
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
createView({
|
||||||
|
camera: { lat: 0, lon: 0 },
|
||||||
|
follow: { kind: 'satellite', id: '25544', cockpit: true },
|
||||||
|
}),
|
||||||
|
/no cockpit view/,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -471,8 +471,8 @@ export function panelRuntime(config) {
|
|||||||
location.href.split('#')[0] + new URL(url).hash,
|
location.href.split('#')[0] + new URL(url).hash,
|
||||||
);
|
);
|
||||||
// The app restores this view from its link, following included, so
|
// The app restores this view from its link, following included, so
|
||||||
// it must not be applied again: flying there would end the follow.
|
// it needs applying again only for what links cannot carry: cockpit.
|
||||||
if (queued === view) queued = null;
|
if (queued === view && !view.follow?.cockpit) queued = null;
|
||||||
} catch {
|
} catch {
|
||||||
// The view still arrives once the app is ready.
|
// The view still arrives once the app is ready.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ export const showInGodsEyeView = defineTool({
|
|||||||
"God's Eye View globe in the conversation; everywhere, a link that opens " +
|
"God's Eye View globe in the conversation; everywhere, a link that opens " +
|
||||||
'it. Pass the view another answer returned, or describe one: an area ' +
|
'it. Pass the view another answer returned, or describe one: an area ' +
|
||||||
'framed from above or a camera position, with chosen data layers, visual ' +
|
'framed from above or a camera position, with chosen data layers, visual ' +
|
||||||
'style and map, optionally following an aircraft or satellite and with ' +
|
'style and map, optionally following an aircraft (or riding in its ' +
|
||||||
'marks drawn on the map.',
|
'cockpit view) or a satellite, and with marks drawn on the map.',
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: VIEW_ARGUMENTS,
|
properties: VIEW_ARGUMENTS,
|
||||||
|
|||||||
@@ -48,11 +48,13 @@ const FOLLOWABLE = Object.freeze({
|
|||||||
layer: 'flights',
|
layer: 'flights',
|
||||||
owner: 'flights',
|
owner: 'flights',
|
||||||
key: 'selectedFlightsTrackingId',
|
key: 'selectedFlightsTrackingId',
|
||||||
|
cockpit: true,
|
||||||
},
|
},
|
||||||
military_aircraft: {
|
military_aircraft: {
|
||||||
layer: 'military',
|
layer: 'military',
|
||||||
owner: 'flights',
|
owner: 'flights',
|
||||||
key: 'selectedMilitaryTrackingId',
|
key: 'selectedMilitaryTrackingId',
|
||||||
|
cockpit: true,
|
||||||
},
|
},
|
||||||
satellite: {
|
satellite: {
|
||||||
layer: 'satellites',
|
layer: 'satellites',
|
||||||
@@ -169,6 +171,12 @@ export const VIEW_PROPERTIES = Object.freeze({
|
|||||||
properties: {
|
properties: {
|
||||||
kind: { type: 'string', enum: [...VIEW_FOLLOW_KINDS] },
|
kind: { type: 'string', enum: [...VIEW_FOLLOW_KINDS] },
|
||||||
id: { type: 'string', pattern: '^[0-9A-Za-z~_-]{1,16}$' },
|
id: { type: 'string', pattern: '^[0-9A-Za-z~_-]{1,16}$' },
|
||||||
|
cockpit: {
|
||||||
|
type: 'boolean',
|
||||||
|
description:
|
||||||
|
"Show the aircraft's cockpit view instead of following it from " +
|
||||||
|
'outside. Aircraft only; links open the app following it.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: ['kind', 'id'],
|
required: ['kind', 'id'],
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
@@ -253,6 +261,8 @@ export function createView({
|
|||||||
const followed = follow ? FOLLOWABLE[follow.kind] : null;
|
const followed = follow ? FOLLOWABLE[follow.kind] : null;
|
||||||
if (follow && !followed)
|
if (follow && !followed)
|
||||||
throw new TypeError(`Cannot follow a ${follow.kind}`);
|
throw new TypeError(`Cannot follow a ${follow.kind}`);
|
||||||
|
if (follow?.cockpit === true && !followed.cockpit)
|
||||||
|
throw new TypeError(`A ${follow.kind} has no cockpit view`);
|
||||||
const enabled = new Set(
|
const enabled = new Set(
|
||||||
layers.filter((layer) => REGISTERED_LAYER_IDS.includes(layer)),
|
layers.filter((layer) => REGISTERED_LAYER_IDS.includes(layer)),
|
||||||
);
|
);
|
||||||
@@ -276,6 +286,7 @@ export function createView({
|
|||||||
? Object.freeze({
|
? Object.freeze({
|
||||||
kind: follow.kind,
|
kind: follow.kind,
|
||||||
id: String(follow.id).toLowerCase(),
|
id: String(follow.id).toLowerCase(),
|
||||||
|
...(follow.cockpit === true ? { cockpit: true } : {}),
|
||||||
})
|
})
|
||||||
: null,
|
: null,
|
||||||
annotations: Object.freeze(
|
annotations: Object.freeze(
|
||||||
|
|||||||
Reference in new issue
Block a user