mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
feat: carry annotations in views and aim tilted views at the area
A view can carry the marks the app's annotate_map action draws, and links carry them in the an parameter, bounded in count and size. A tilted view of an area now places the camera behind its center so the area stays in the middle of the frame.
This commit is contained in:
1 parent
af8856cedf
commit
f68aa988ec
5 files changed
+265
-4
No files matched your search
@@ -3,7 +3,7 @@
|
||||
import { VIEW_PROPERTIES, createView, viewUrl } from '../../view/index.js';
|
||||
import { defineTool, ToolError } from '../catalog.js';
|
||||
import { AREA_SCHEMA, resolveArea } from '../area.js';
|
||||
import { cameraForArea } from '../views.js';
|
||||
import { cameraForArea, cameraLookingAt } from '../views.js';
|
||||
|
||||
/** Tool arguments that describe a view: an area to frame, or a camera. */
|
||||
export const VIEW_ARGUMENTS = Object.freeze({
|
||||
@@ -21,7 +21,17 @@ export async function resolveViewArguments(args, { services, signal }) {
|
||||
if (args.area) {
|
||||
const area = await resolveArea(args.area, { services, signal });
|
||||
label = area.label;
|
||||
camera = { ...cameraForArea(area), ...camera };
|
||||
const framed = cameraForArea(area);
|
||||
// A tilted view of an area looks at its center from behind, unless the
|
||||
// caller placed the camera itself.
|
||||
camera =
|
||||
camera.lat === undefined && camera.lon === undefined
|
||||
? cameraLookingAt(framed, {
|
||||
altitudeM: camera.altitude_m ?? framed.altitude_m,
|
||||
headingDeg: camera.heading_deg ?? 0,
|
||||
pitchDeg: camera.pitch_deg ?? -90,
|
||||
})
|
||||
: { ...framed, ...camera };
|
||||
}
|
||||
if (!Number.isFinite(camera.lat) || !Number.isFinite(camera.lon))
|
||||
throw new ToolError(
|
||||
@@ -34,6 +44,7 @@ export async function resolveViewArguments(args, { services, signal }) {
|
||||
style: args.style ?? null,
|
||||
map: args.map ?? null,
|
||||
follow: args.follow ?? null,
|
||||
annotations: args.annotations ?? [],
|
||||
});
|
||||
return {
|
||||
view,
|
||||
@@ -56,7 +67,8 @@ export const openInGodsEyeView = defineTool({
|
||||
description:
|
||||
"A link that opens God's Eye View at a view: an area framed from above " +
|
||||
'or a camera position, with chosen data layers, visual style and map, ' +
|
||||
'optionally following an aircraft or satellite.',
|
||||
'optionally following an aircraft or satellite and with marks drawn on ' +
|
||||
'the globe.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: VIEW_ARGUMENTS,
|
||||
|
||||
@@ -28,6 +28,43 @@ export function cameraForArea(area) {
|
||||
};
|
||||
}
|
||||
|
||||
const EARTH_RADIUS_M = 6_371_008.8;
|
||||
|
||||
/**
|
||||
* The camera position for looking at a point from `altitudeM` with a heading
|
||||
* and a tilt: straight above it when looking down, otherwise pulled back
|
||||
* along the heading so the point stays in the middle of the view.
|
||||
*/
|
||||
export function cameraLookingAt(
|
||||
target,
|
||||
{ altitudeM, headingDeg = 0, pitchDeg = -90 },
|
||||
) {
|
||||
const camera = {
|
||||
lat: target.lat,
|
||||
lon: target.lon,
|
||||
altitude_m: altitudeM,
|
||||
heading_deg: headingDeg,
|
||||
pitch_deg: pitchDeg,
|
||||
};
|
||||
if (pitchDeg <= -89.5) return camera;
|
||||
// Ground distance from the camera to the point it looks at, on flat ground.
|
||||
const back = altitudeM / Math.tan((-Math.min(pitchDeg, -5) * Math.PI) / 180);
|
||||
const heading = (headingDeg * Math.PI) / 180;
|
||||
const lat =
|
||||
target.lat -
|
||||
((back * Math.cos(heading)) / EARTH_RADIUS_M) * (180 / Math.PI);
|
||||
const lon =
|
||||
target.lon -
|
||||
((back * Math.sin(heading)) /
|
||||
(EARTH_RADIUS_M * Math.cos((target.lat * Math.PI) / 180))) *
|
||||
(180 / Math.PI);
|
||||
return {
|
||||
...camera,
|
||||
lat: Math.max(-90, Math.min(90, lat)),
|
||||
lon: ((((lon + 180) % 360) + 360) % 360) - 180,
|
||||
};
|
||||
}
|
||||
|
||||
/** The app's address, or null when it is not configured. */
|
||||
function appUrl(services) {
|
||||
try {
|
||||
|
||||
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { composeCatalog, coreTools } from './index.js';
|
||||
import { viewFromParams } from '../view/index.js';
|
||||
import { cameraForArea, suggestView } from './views.js';
|
||||
import { cameraForArea, cameraLookingAt, suggestView } from './views.js';
|
||||
import { resolveArea } from './area.js';
|
||||
|
||||
const app = { baseUrl: 'https://maps.example/' };
|
||||
@@ -100,3 +100,29 @@ test('the flood and the brief frame what they describe', async () => {
|
||||
});
|
||||
assert.deepEqual(brief.data.view.layers, ['earthquakes', 'flights']);
|
||||
});
|
||||
|
||||
test('a tilted camera sits behind the point it looks at', () => {
|
||||
const target = { lat: 25, lon: 121 };
|
||||
assert.deepEqual(cameraLookingAt(target, { altitudeM: 1000 }), {
|
||||
lat: 25,
|
||||
lon: 121,
|
||||
altitude_m: 1000,
|
||||
heading_deg: 0,
|
||||
pitch_deg: -90,
|
||||
});
|
||||
// Looking north at 45 degrees from 10 km up: about 10 km south.
|
||||
const north = cameraLookingAt(target, {
|
||||
altitudeM: 10000,
|
||||
headingDeg: 0,
|
||||
pitchDeg: -45,
|
||||
});
|
||||
assert.ok(Math.abs(north.lat - (25 - 10000 / 111195)) < 1e-3);
|
||||
assert.ok(Math.abs(north.lon - 121) < 1e-9);
|
||||
// Looking east: the camera is to the west.
|
||||
const east = cameraLookingAt(target, {
|
||||
altitudeM: 10000,
|
||||
headingDeg: 90,
|
||||
pitchDeg: -45,
|
||||
});
|
||||
assert.ok(east.lon < 121 && Math.abs(east.lat - 25) < 1e-9);
|
||||
});
|
||||
Reference in new issue
Block a user