Street Level: a posted view without a photo point grounds its look-at too

show_in_gods_eye_view measured a view's altitude from the terrain before
taking the point the camera looks at; a view posted straight into the
embedded app used the ellipsoid altitude, so a camera over high ground
opened photos kilometres ahead. groundedLookAt moves to the view module,
and the embedded app uses it with the app's terrain heights for a posted
street_level that names no point.
This commit is contained in:
German Khokhlov committed 2026-11-11 13:46:10 +04:00
1 parent a8970c1f18
commit f612fc2180
5 files changed
+107 -26

No files matched your search

+21
View File
@@ -19,6 +19,7 @@ import {
annotationsFromParams,
createView,
followFromParams,
groundedLookAt,
streetLevelFromParams,
} from '../view/index.js';
@@ -329,6 +330,7 @@ export function installViews({
dataManager,
run,
signal,
terrain = null,
location = globalThis.location,
windowRef = globalThis.window,
}) {
@@ -477,8 +479,27 @@ export function installViews({
// A panel's host can hand back old results on a remount: only one issued
// just now may open a billed photo. A framing page's view is its own.
const autoOpen = !isEmbeddedInline() || event.data.fresh === true;
// Photos asked for without a point open where the camera looks, measured
// from the ground under it, as show_in_gods_eye_view places them.
const asked = event.data.view?.street_level;
const unplaced =
asked &&
typeof asked === 'object' &&
asked.lat == null &&
asked.lon == null;
queue = queue.then(async () => {
if (signal?.aborted) return;
if (unplaced && view.street_level) {
const point = await groundedLookAt(view.camera, terrain, signal).catch(
() => null,
);
if (signal?.aborted) return;
if (point)
view = createView({
...event.data.view,
street_level: { ...asked, ...point },
});
}
const steps = await applyView(view, {
viewer,
dataManager,
+59
View File
@@ -104,6 +104,65 @@ test('a followed entity is retried until its layer has it', async () => {
assert.deepEqual(steps.at(-1), { step: 'follow', ok: true });
});
test('a posted view without a photo point opens its photos where the camera looks over the ground', async () => {
const parent = { postMessage() {} };
const windowRef = new EventTarget();
windowRef.parent = parent;
windowRef.document = { body: { classList: new Set() } };
windowRef.document.body.classList.add = Set.prototype.add;
const shell = { initialRestorePromise: Promise.resolve(), setCleanView() {} };
const { calls, run } = recorder();
const asked = [];
// Denver: the ground is 1,590 m above the ellipsoid under a camera at 1,909 m.
const terrain = {
getHeights: async (points) => {
asked.push(points);
return points.map(() => ({ ellipsoid: 1590 }));
},
};
const remove = installViews({
shell,
viewer: fakeViewer(),
dataManager: fakeLayers(new Set()),
run,
terrain,
location: { search: '?embed=1', hash: '' },
windowRef,
});
await new Promise((resolve) => setTimeout(resolve, 0));
const event = new Event('message');
Object.assign(event, {
source: parent,
origin: 'https://host.example',
data: {
type: 'gev:view',
id: 1,
view: {
camera: {
lat: 39.74,
lon: -104.99,
altitude_m: 1909,
pitch_deg: -30,
heading_deg: 0,
},
street_level: { provider: 'mapillary' },
},
},
});
windowRef.dispatchEvent(event);
await new Promise((resolve) => setTimeout(resolve, 10));
remove();
const [, args] = calls.find(([name]) => name === 'show_street_level');
// 319 m above the ground at -30° looks ~550 m ahead, not the ~3.3 km the
// ellipsoid altitude would give.
assert.deepEqual(asked, [[{ lat: 39.74, lon: -104.99 }]]);
assert.ok(
args.latitude > 39.744 && args.latitude < 39.746,
String(args.latitude),
);
assert.equal(args.longitude, -104.99);
});
test('an embedded page takes views only from its parent and answers it', async () => {
const posted = [];
const targets = [];
+1
View File
@@ -249,6 +249,7 @@ export function createApplicationTools({
run: (name, args, options = {}) =>
voiceCommands.runner(name, args, { ...options, signal }),
signal,
terrain: operations.requests.terrain,
}),
);
// An application's own tool takes the pointer as Draw does, and its own
+2 -26
View File
@@ -3,7 +3,7 @@
import {
VIEW_PROPERTIES,
createView,
lookAtPoint,
groundedLookAt,
viewUrl,
} from '../../view/index.js';
import { defineTool, ToolError } from '../catalog.js';
@@ -41,30 +41,6 @@ async function followedAircraftPosition(follow, tools) {
}
}
/**
* Where a tilted camera looks, measured from the ground under it: a view's
* altitude is above the ellipsoid, and a city a mile up would otherwise put
* the point kilometres too far ahead. Null when the terrain cannot say.
*/
async function groundedLookAt(camera, services, signal) {
if (typeof services?.terrain?.getHeights !== 'function') return null;
if (!Number.isFinite(camera.altitude_m) || !(camera.pitch_deg > -89))
return null;
let heights;
try {
heights = await services.terrain.getHeights(
[{ lat: camera.lat, lon: camera.lon }],
{ signal },
);
} catch (error) {
if (signal?.aborted) throw error;
return null;
}
const ground = Number(heights?.[0]?.ellipsoid);
if (!Number.isFinite(ground)) return null;
return lookAtPoint({ ...camera, altitude_m: camera.altitude_m - ground });
}
/**
* The view described by tool arguments. An area frames the camera straight
* down over it; camera fields given alongside it override the framing. A
@@ -131,7 +107,7 @@ export async function resolveViewArguments(args, { services, signal, tools }) {
streetLevel.lat === undefined &&
streetLevel.lon === undefined
) {
const point = await groundedLookAt(camera, services, signal);
const point = await groundedLookAt(camera, services?.terrain, signal);
if (point) streetLevel = { ...streetLevel, ...point };
}
let view;
+24
View File
@@ -123,6 +123,30 @@ export function lookAtPoint(camera) {
};
}
/**
* Where a tilted camera looks, measured from the ground under it: a view's
* altitude is above the ellipsoid, and a city a mile up would otherwise put
* the point kilometres too far ahead. `terrain` is a heights service
* (`getHeights`); null when it is missing or cannot say.
*/
export async function groundedLookAt(camera, terrain, signal) {
if (typeof terrain?.getHeights !== 'function') return null;
if (!Number.isFinite(camera?.altitude_m) || !(camera.pitch_deg > -89))
return null;
let heights;
try {
heights = await terrain.getHeights([{ lat: camera.lat, lon: camera.lon }], {
signal,
});
} catch (error) {
if (signal?.aborted) throw error;
return null;
}
const ground = Number(heights?.[0]?.ellipsoid);
if (!Number.isFinite(ground)) return null;
return lookAtPoint({ ...camera, altitude_m: camera.altitude_m - ground });
}
/** Annotation kinds the app draws, as in its annotate_map action. */
export const VIEW_ANNOTATION_TYPES = Object.freeze([
'pin',