Keep street imagery and picked coverage across Cesium and Navara

This commit is contained in:
German Khokhlov committed 2026-11-03 04:13:26 +04:00
1 parent 75df3389f3
commit b98cbeebf2
20 files changed
+561 -62

No files matched your search

+1
View File
@@ -3680,6 +3680,7 @@
"src/layers/submarineCables/render/navara.js",
"src/layers/directions/render/navara.js",
"src/layers/traffic/render/navara.js",
"src/layers/streetLevel/render/navara.js",
"src/layers/shared/render/navaraStory.js",
"src/layers/bhoteKoshi/render/navara.js",
"src/layers/weather/render/navara.js",
+171
View File
@@ -0,0 +1,171 @@
import assert from 'node:assert/strict';
import puppeteer from 'puppeteer';
import { blockPaidQaMaps } from './qa-map-policy.mjs';
import { serveFixtures, newFixture } from './qa-street-level.mjs';
import { KV_LINE, KV_SEQUENCE_ID } from './fixtures/street-level/kartaView.mjs';
import {
PNX_PARK,
PNX_SEQUENCES,
panoramaxPictures,
} from './fixtures/street-level/panoramax.mjs';
const panorama = process.argv.includes('--panoramax');
const provider = panorama ? 'panoramax' : 'kartaview';
const base = new URL(process.argv[2]);
if (
!['localhost', '127.0.0.1'].includes(base.hostname) ||
['', '4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
base.port,
)
)
throw Error('Keyless local preview required');
base.search = '?engine=navara&welcome=0';
const at = panorama
? PNX_PARK
: { lat: 38.5831, lon: KV_LINE.lon, height: 850 };
base.hash = `v=2&lat=${at.lat}&lon=${at.lon}&alt=${at.height}&heading=0&pitch=-90&roll=0&map=sentinel2&l=`;
const browser = await puppeteer.launch({
headless: true,
args: ['--use-angle=metal', '--enable-gpu'],
});
try {
const p = await browser.newPage();
await blockPaidQaMaps(p);
await p.setViewport({ width: 1280, height: 900 });
const errors = [],
fixture = newFixture({ configured: true });
p.on('pageerror', (e) => {
errors.push(e.message);
console.error(e.message.split('?')[0]);
});
p.on('console', (m) => {
if (/shader|registerMeshPicker/i.test(m.text()) && m.type() === 'error')
errors.push(m.text());
});
await serveFixtures(p, fixture);
let photoRequests = 0;
p.on('request', (req) => {
if (
/\.jpg(?:$|\?)/i.test(new URL(req.url()).pathname) &&
/openstreetcam|kartaview|panoramax/.test(new URL(req.url()).hostname)
)
photoRequests++;
});
await p.goto(base.href);
const ready = async (k) => {
await p.waitForFunction(
(kind) =>
window.__godsEyeView?.engine?.kind === kind &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
k,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await p.evaluate(async (provider) => {
const { dataManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
throw Error('Expected keyless preview');
window.qaStreet = dataManager.layers.get('street-level').module;
await dataManager.setEnabled('street-level', true, { origin: 'user' });
window.qaStreet.setProviderEnabled(provider, true);
}, provider);
const until = async (fn) => {
try {
await p.waitForFunction(fn, { timeout: 20000 });
} catch (e) {
console.log(await p.evaluate(() => window.qaStreet.getUIState()));
throw e;
}
};
await until(() => window.qaStreet.getUIState().coverage.count > 0);
// The fixture drive passes under the middle of the canvas. Click its actual picked line.
const xy = await p.evaluate(
(prefix) => {
const { engine } = window.__godsEyeView,
canvas = engine.host.canvas,
rect = canvas.getBoundingClientRect();
for (let dy = -30; dy <= 30; dy += 3)
for (let dx = -40; dx <= 40; dx++) {
const at = { x: rect.width / 2 + dx, y: rect.height / 2 + dy },
pick = engine.picker.pick(at);
if (typeof pick?.id === 'string' && pick.id.startsWith(prefix))
return { x: at.x + rect.x, y: at.y + rect.y, id: pick.id };
}
return null;
},
panorama ? 'pnx:seq:' : 'kv:line:',
);
assert.ok(xy, 'native ground line was pickable');
console.log('pick', xy);
await p.mouse.click(xy.x, xy.y);
await until(
() =>
window.qaStreet.getUIState().street.open &&
window.qaStreet.getUIState().sequence.images > 0 &&
(document.querySelector('#sl-viewer img')?.naturalWidth > 0 ||
document.querySelector('#sl-viewer canvas:not([hidden])')?.width > 0),
);
if (panorama) {
const expected = panoramaxPictures().filter(
(p) => p.sequenceId === xy.id.slice('pnx:seq:'.length),
).length;
await p.waitForFunction(
(n) => window.qaStreet.getUIState().sequence.images === n,
{ timeout: 20000 },
expected,
);
}
const first = await p.evaluate(() => {
window.streetPhoto =
document.querySelector('#sl-viewer canvas:not([hidden])') ??
document.querySelector('#sl-viewer img');
return window.qaStreet.getUIState();
});
assert.ok(
(panorama ? PNX_SEQUENCES : [KV_SEQUENCE_ID]).includes(
first.street.sequenceId,
),
);
const reads = photoRequests;
for (const kind of ['cesium', 'navara']) {
await p.select('#globe-engine', kind);
await ready(kind);
const state = await p.evaluate(() => ({
model:
window.__godsEyeView.dataManager.layers.get('street-level').module ===
window.qaStreet,
samePhoto:
document.querySelector('#sl-viewer')?.contains(window.streetPhoto) &&
window.streetPhoto.isConnected,
ui: window.qaStreet.getUIState(),
}));
console.log(
kind,
state.model,
state.samePhoto,
state.ui.street.imageId,
state.ui.sequence.images,
state.ui.coverage.count,
);
assert.equal(state.model, true);
assert.equal(state.samePhoto, true);
assert.ok(state.ui.street.open);
assert.equal(state.ui.street.imageId, first.street.imageId);
assert.equal(state.ui.sequence.images, first.sequence.images);
assert.deepEqual(state.ui.filter, first.filter);
assert.ok(state.ui.coverage.count > 0);
}
assert.equal(photoRequests, reads);
assert.deepEqual(errors, []);
assert.deepEqual(fixture.unknown, []);
console.log(
'PASS native line pick, same mounted photo and sequence across two switches',
);
} finally {
await browser.close();
}
+2 -2
View File
@@ -125,7 +125,7 @@ const KEYLESS_HOSTS = [
* `fixture`, which steps may change mid-run. `configured: undefined` lets the
* server's real status route answer.
*/
async function serveFixtures(page, fixture) {
export async function serveFixtures(page, fixture) {
// CDP Fetch on just these URLs: page-wide interception stalls Cesium workers.
const cdp = await page.createCDPSession();
await cdp.send('Fetch.enable', {
@@ -206,7 +206,7 @@ async function serveFixtures(page, fixture) {
});
}
function newFixture(overrides = {}) {
export function newFixture(overrides = {}) {
return {
configured: undefined,
rejected: false,
+3 -2
View File
@@ -224,11 +224,12 @@ export function createApplicationCatalog({
[
'street-level',
() =>
createApplicationStreetLevel({
sessionModel('street-level', () => createApplicationStreetLevel({
surface,
nativeRenderer:engine=>rendererFor('street-level',engine),
sources: { mapillary: sources.mapillary },
providers: streetLevelProviders,
}),
})),
],
['satellites', satellites],
[
+2 -1
View File
@@ -33,10 +33,11 @@ function defaultProviders(sources) {
*/
export function createApplicationStreetLevel({
sources = {},
nativeRenderer,
providers = defaultProviders(sources),
}) {
return createStreetLevelLayer({
providers,
services: { sprites, picking, input, scenePick, render },
services: { sprites, picking, input, scenePick, render, nativeRenderer },
});
}
+1
View File
@@ -54,6 +54,7 @@ test('the Navara mode registers migrated renderers once', async () => {
'gnss-integrity',
's2-ghostmaps',
'alpr-cameras',
'street-level',
'satellites',
'rocket-launches',
'traffic',
+4
View File
@@ -126,6 +126,10 @@ export async function registerNavaraLayerRenderers() {
const { drawStorySource } = await import('../layers/shared/render/navaraStory.js');
return (engine, { data }) => drawStorySource(engine, navaraViews.get(engine), data);
},
'street-level': async () => {
const { createStreetMap } = await import('../layers/streetLevel/render/navara.js');
return engine => createStreetMap(engine, navaraViews.get(engine));
},
traffic: async () => {
const { createTrafficNavaraRenderer } = await import('../layers/traffic/render/navara.js');
return engine => createTrafficNavaraRenderer(engine, navaraViews.get(engine));
+9 -1
View File
@@ -29,7 +29,8 @@ export function createCameraFraming({ state }) {
function groundHeightAt(lon, lat, fallback) {
const scene = state.viewer?.scene;
const carto = Cesium.Cartographic.fromDegrees(lon, lat);
let height = sampledSurfaceHeight(scene, carto);
const engine = state.viewer?.engine;
let height = engine ? engine.surface.sampleHeight(carto) ?? engine.surface.terrainHeight(carto) : sampledSurfaceHeight(scene, carto);
if (height === null) height = scene?.globe?.getHeight?.(carto);
if (!plausibleSurfaceHeight(height)) height = fallback;
return plausibleSurfaceHeight(height) ? height : 0;
@@ -53,6 +54,13 @@ export function createCameraFraming({ state }) {
const release = () => {
if (flight === current) flight = null;
};
if (state.viewer.engine) {
state.viewer.engine.camera.flyToSphere({
center:Cesium.Cartesian3.fromDegrees(position.lon,position.lat,ground+2),radius:4
}, {heading:Cesium.Math.toRadians(bearing || 0),pitch:Cesium.Math.toRadians(-32),range:140,duration:1.6,
complete:release,cancel:release});
return;
}
state.viewer.camera.flyToBoundingSphere(
new Cesium.BoundingSphere(
Cesium.Cartesian3.fromDegrees(position.lon, position.lat, ground + 2),
+8 -2
View File
@@ -9,7 +9,12 @@ export function createCredits() {
const shown = new Map();
function show(viewer, def) {
if (shown.has(def.id) || !viewer?.creditDisplay) return;
if (shown.has(def.id)) return;
if (viewer?.engine) {
shown.set(def.id, viewer.engine.credits.addStatic(def.credit.html, { onScreen: true }));
return;
}
if (!viewer?.creditDisplay) return;
try {
const credit = new Cesium.Credit(def.credit.html, true);
viewer.creditDisplay.addStaticCredit(credit);
@@ -23,7 +28,8 @@ export function createCredits() {
const credit = shown.get(def.id);
if (!credit) return;
try {
viewer?.creditDisplay?.removeStaticCredit?.(credit);
if (typeof credit === 'function') credit();
else viewer?.creditDisplay?.removeStaticCredit?.(credit);
} catch {
/* credit display already torn down */
}
+3 -3
View File
@@ -1,10 +1,10 @@
import * as geo from '../../geo/index.js';
import { GROUND_CLICK_MAX_HEIGHT_M } from './policy.js';
import { cameraHeightAboveGround } from './view.js';
import { cameraHeightAboveGround, cameraLocation } from './view.js';
/** Whether the camera is low enough for a click on the ground to mean a street. */
export function groundClickHeightOk(viewer) {
if (!viewer?.camera?.positionCartographic) return false;
if (!cameraLocation(viewer)) return false;
return cameraHeightAboveGround(viewer) <= GROUND_CLICK_MAX_HEIGHT_M;
}
@@ -32,7 +32,7 @@ export function createGroundClick({ state, openNearest, isAvailable }) {
)
.map((entry) => entry.def.id);
if (!providerIds.length) return false;
const position = scenePick.pickGroundPosition(state.viewer, screenPosition);
const position = state.viewer.engine ? state.viewer.engine.picker.pickGlobe(screenPosition) : scenePick.pickGroundPosition(state.viewer, screenPosition);
const carto = position && geo.cartographic.fromCartesian(position);
if (!carto) return false;
return openNearest(
+61 -18
View File
@@ -71,6 +71,7 @@ export function createStreetLevelLayer({
isAvailable: (entry) => providerAvailable(entry),
});
let waitingForView = false;
let notifyQueued = false;
function notify() {
if (notifyQueued) return;
@@ -96,6 +97,7 @@ export function createStreetLevelLayer({
getFilter: () => resolveFilter(state.filter),
/** The layer and this provider's switch are both on. */
isActive: () => state.enabled && entry.on,
waitingForView: () => waitingForView,
notify,
actions: {
/** `{frame: false}` opens it without moving the camera. */
@@ -186,7 +188,7 @@ export function createStreetLevelLayer({
cancelGroundRetry();
const ready = state.enabled && groundClickHeightOk(state.viewer);
if (
state.enabled &&
state.enabled && state.viewer &&
!ready &&
groundRetries < GROUND_RETRY_LIMIT &&
groundUnderCamera(state.viewer) === null
@@ -257,14 +259,12 @@ export function createStreetLevelLayer({
/** Ground under the screen centre; null for sky, undefined when it cannot aim. */
function screenCentreGround() {
const canvas = state.viewer?.scene?.canvas;
const canvas = state.viewer?.engine?.host.canvas ?? state.viewer?.scene?.canvas;
const pick = state.services.scenePick?.pickGroundPosition;
if (!pick || !canvas?.clientWidth || !canvas?.clientHeight)
return undefined;
const position = pick(state.viewer, {
x: canvas.clientWidth / 2,
y: canvas.clientHeight / 2,
});
const center = { x:canvas.clientWidth/2, y:canvas.clientHeight/2 };
const position = state.viewer.engine ? state.viewer.engine.picker.pickGlobe(center) : pick(state.viewer,center);
const carto = position && geo.cartographic.fromCartesian(position);
return carto
? {
@@ -477,24 +477,66 @@ export function createStreetLevelLayer({
};
},
init(viewer) {
state.viewer = viewer;
parts.marker.ensure(viewer);
parts.marker.setVisible(false);
init(viewer, options) {
this.attachRenderer(viewer,options);
for (const entry of state.providers.values()) checkStatus(entry);
},
attachRenderer(viewer, {engine = null, waitForCameraRestore = false} = {}) {
const map = viewer ?? services.nativeRenderer?.(engine);
state.viewer = map;
waitingForView = waitForCameraRestore;
if (!map) return;
parts.marker.ensure(map);
parts.marker.setVisible(state.enabled);
for (const entry of state.providers.values()) {
entry.instance.init(viewer);
checkStatus(entry);
entry.instance.init(map);
if(state.enabled && entry.on){
entry.instance.activate(map);
entry.instance.repaintMap?.();
if(providerAvailable(entry)) parts.credits.show(map,entry.def);
}
}
console.log('[Data:StreetLevel] Initialized');
if (state.enabled) {
parts.selection.install(map);
watchCamera(map);
parts.marker.set(state.street.position,state.street.bearing,state.providers.get(state.street.providerId)?.def.marker);
remeasureGround();
}
},
resumeViewport() {
if (!waitingForView) return;
waitingForView=false;
if(state.enabled && state.viewer) {
for(const entry of activeEntries())entry.instance.refreshCoverage?.();
remeasureGround();
}
},
detachRenderer() {
const map=state.viewer;
if(!map)return;
waitingForView=true;
removeCameraListener?.();removeCameraListener=null;
cancelGroundRetry();
parts.framing.cancel();
parts.selection.uninstall();
for(const entry of state.providers.values()){
if(entry.instance.detachMap)entry.instance.detachMap();
else if(entry.on)entry.instance.deactivate();
}
parts.marker.destroy(map);
parts.credits.hideAll(map);
unsubscribeMapStack?.();
unsubscribeMapStack=null;
mapStack=null;
state.viewer=null;
},
enable(viewer) {
state.enabled = true;
state.viewer = viewer;
if (viewer) state.viewer = viewer;
parts.marker.setVisible(true);
parts.selection.install(viewer);
parts.selection.install(state.viewer);
for (const entry of activeEntries()) activate(entry);
watchCamera(viewer);
watchCamera(state.viewer);
remeasureGround();
notify();
},
@@ -518,7 +560,8 @@ export function createStreetLevelLayer({
return state.enabled;
},
destroy(viewer = state.viewer) {
destroy() {
const viewer = state.viewer;
layer.disable();
for (const entry of state.providers.values())
entry.instance.destroy(viewer);
+24 -6
View File
@@ -166,6 +166,7 @@ export function createLineCoverage({
}
function ensureTerrainReady() {
if (state.viewer?.engine) return;
return (state.coverage.terrainReady ||=
Cesium.GroundPolylinePrimitive.initializeTerrainHeights());
}
@@ -232,10 +233,11 @@ export function createLineCoverage({
function highlight(sequenceId = state.sequence.selectedId) {
const scene = state.viewer?.scene;
if (state.coverage.highlight) {
scene?.groundPrimitives?.remove(state.coverage.highlight);
if (state.viewer?.engine) state.coverage.highlight.destroy();
else scene?.groundPrimitives?.remove(state.coverage.highlight);
state.coverage.highlight = null;
}
if (!sequenceId || !scene) return;
if (!sequenceId || (!state.viewer?.engine && !scene)) return;
// From every loaded line, drawn or not: a sequence selected after its
// tiles were drawn may have been left out by their caps.
const current = filter();
@@ -244,6 +246,11 @@ export function createLineCoverage({
const sequence = entry.sequenceList.find(({ id }) => id === sequenceId);
if (sequence && passes(sequence, current)) parts.push(...sequence.parts);
}
if(state.viewer?.engine){
state.coverage.highlight=state.viewer.lines([{id:sequenceId,parts}],colors.selected,tuning.lineWidthPx+1,pickPrefix);
requestRender();
return;
}
const instances = lineInstances(
[{ id: sequenceId, parts }],
Cesium.Color.fromCssColorString(colors.selected),
@@ -259,10 +266,15 @@ export function createLineCoverage({
function attachPrimitive(entry) {
const scene = state.viewer?.scene;
if (!scene) return;
if (!scene && !state.viewer?.engine) return;
// Lookup, picking and counts follow what is drawn, not the whole tile.
const sequences = drawnSequences(entry);
entry.sequences = new Map(sequences.map((s) => [s.id, s]));
if(state.viewer?.engine){
const group = state.viewer.lines(sequences,colors.coverage,tuning.lineWidthPx,pickPrefix);
entry.primitives = group ? [group] : [];
return;
}
entry.primitives = buildSequencePrimitives(sequences);
for (const primitive of entry.primitives)
scene.groundPrimitives.add(primitive);
@@ -272,7 +284,8 @@ export function createLineCoverage({
const scene = state.viewer?.scene;
for (const primitive of entry.primitives || []) {
try {
scene?.groundPrimitives?.remove(primitive);
if (state.viewer?.engine) primitive.destroy();
else scene?.groundPrimitives?.remove(primitive);
} catch {
/* already gone */
}
@@ -396,7 +409,7 @@ export function createLineCoverage({
*/
function wantedTiles(viewer) {
const ground = groundUnderCamera(viewer);
const cameraHeight = viewer.camera?.positionCartographic?.height;
const cameraHeight = viewer.engine?.camera.pose().cartographic.height ?? viewer.camera?.positionCartographic?.height;
const height = Number.isFinite(cameraHeight)
? cameraHeight - (ground ?? 0)
: null;
@@ -425,7 +438,7 @@ export function createLineCoverage({
/** Recompute the tile set for the current camera and reconcile primitives. */
function refresh() {
const viewer = state.viewer;
if (!viewer || !state.context.isActive() || state.coverage.blocked) return;
if (!viewer || state.context.waitingForView?.() || !state.context.isActive() || state.coverage.blocked) return;
const { zoom, tiles, hint } = wantedTiles(viewer);
if (hint) {
state.coverage.hint = hint;
@@ -534,6 +547,11 @@ export function createLineCoverage({
return {
attach,
detach,
releaseGraphics() {
detach();
highlight(null);
for(const entry of state.coverage.tiles.values()) detachPrimitive(entry);
},
refresh,
clear,
unblock,
+8 -2
View File
@@ -38,6 +38,10 @@ export function createMarker({ state }) {
function ensure(viewer) {
if (state.marker.collection) return;
if (viewer?.engine) {
state.marker.collection = viewer.billboards(SPRITE_ID);
return;
}
state.marker.collection = new Cesium.BillboardCollection({
scene: viewer.scene,
});
@@ -80,7 +84,8 @@ export function createMarker({ state }) {
state.marker.billboard.position = cartesian;
}
state.marker.billboard.rotation = -Cesium.Math.toRadians(bearing || 0);
horizon.update([state.marker.billboard]);
if (state.viewer?.engine) collection.changed();
else horizon.update([state.marker.billboard]);
requestRender();
}
@@ -94,7 +99,8 @@ export function createMarker({ state }) {
const collection = state.marker.collection;
if (!collection) return;
sprites?.unregisterSpriteCollection?.(SPRITE_ID, collection);
viewer?.scene?.primitives?.remove(collection);
if (viewer?.engine) collection.destroy();
else viewer?.scene?.primitives?.remove(collection);
state.marker.collection = null;
state.marker.billboard = null;
}
@@ -457,6 +457,16 @@ export function createKartaViewProvider({
coverage.attach(cesiumViewer);
},
refreshCoverage: () => coverage.refresh(),
detachMap() {
coverage.releaseGraphics();
cones.releaseCollection(state.viewer);
state.viewer = null;
},
repaintMap() {
coverage.rebuild();
cones.rerender();
},
deactivate() {
coverage.detach();
coverage.clear();
@@ -147,6 +147,16 @@ export function createMapillaryProvider({ source, photoViewer = null }) {
},
/** Clear lines and cones, forget refusals. */
refreshCoverage: () => parts.coverage.refresh(),
detachMap() {
parts.coverage.releaseGraphics();
parts.sequences.releaseCollection(state.viewer);
state.viewer = null;
},
repaintMap() {
parts.coverage.rebuild();
parts.sequences.rerender();
},
deactivate() {
parts.coverage.detach();
parts.coverage.clear();
@@ -256,7 +256,7 @@ export function createPanoramaxProvider({
* from high up the click may land tens of metres off the line.
*/
function lineClickRadius() {
const height = state.viewer?.camera?.positionCartographic?.height;
const height = state.viewer?.engine?.camera.pose().cartographic.height ?? state.viewer?.camera?.positionCartographic?.height;
if (!Number.isFinite(height)) return NEAREST_RADIUS_M;
return Math.min(
LINE_CLICK_RADIUS_MAX_M,
@@ -453,6 +453,16 @@ export function createPanoramaxProvider({
coverage.attach(cesiumViewer);
},
refreshCoverage: () => coverage.refresh(),
detachMap() {
coverage.releaseGraphics();
cones.releaseCollection(state.viewer);
state.viewer = null;
},
repaintMap() {
coverage.rebuild();
cones.rerender();
},
deactivate() {
coverage.detach();
coverage.clear();
+183
View File
@@ -0,0 +1,183 @@
import * as geo from '../../../geo/index.js';
import { screenProjectedRotation } from '../../../data/iconOrientation.js';
import { bindGroundShapePicks } from '../../shared/render/navaraGroundPicks.js';
export function createStreetMap(engine, { view }) {
const { camera } = engine;
function billboards(id) {
const rows = new Set(),
sets = new Map();
let visible = true,
dirty = true,
lastCamera = '',
settled = false;
const wake = () => {
dirty = true;
engine.render.request(id);
};
function paint() {
const pose = camera.pose();
const signature = [
pose.position.x,
pose.position.y,
pose.position.z,
pose.heading,
pose.pitch,
pose.roll,
].join('/');
const loaded = engine.surface.tilesLoaded();
if (!dirty && signature === lastCamera && loaded === settled) return;
settled = loaded;
dirty = false;
lastCamera = signature;
const scenes = { camera: { rightWC: pose.right, upWC: pose.up } };
const groups = new Map();
if (visible)
for (const row of rows) {
const key = row.imageId ?? row.image;
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(row);
}
for (const [key, record] of sets)
if (!groups.has(key)) {
record.bank.destroy();
sets.delete(key);
}
for (const [key, group] of groups) {
const first = group[0],
scale = first.scaleByDistance;
let rec = sets.get(key);
if (!rec || rec.capacity < group.length) {
rec?.bank.destroy();
const capacity = Math.max(1, group.length);
const bank = engine.movers.createSet({
id: id + '-' + key,
kind: 'sprite',
capacity,
depthTest: 'always',
pickable: true,
sprite: {
sizePx: first.width,
atlas: { url: first.image, cells: { glyph: [0, 0, 1, 1] } },
scaleByDistance: scale
? [scale.near, scale.nearValue, scale.far, scale.farValue]
: null,
},
});
rec = { capacity, bank };
sets.set(key, rec);
}
let i = 0;
for (const row of group) {
const pos = row.position,
carto = geo.cartographic.fromCartesian(pos);
const h = engine.surface.terrainHeight(carto) ?? carto.height;
const ground = geo.cartesian3.fromRadians(
carto.longitude,
carto.latitude,
h,
);
rec.bank.set(i++, {
position: ground,
rotation: screenProjectedRotation(
scenes,
ground,
-geo.math.toDegrees(row.rotation || 0),
0,
),
color: [1, 1, 1, 1],
cell: 'glyph',
pickId: row.id,
show: row.show !== false && engine.projector.horizonVisible(ground),
});
}
rec.bank.setCount(i);
rec.bank.commit();
}
}
const off = engine.frames.preRender.addEventListener(paint);
return {
add(spec) {
const row = { ...spec };
rows.add(row);
wake();
return row;
},
remove(row) {
rows.delete(row);
wake();
},
removeAll() {
rows.clear();
wake();
},
set show(value) {
visible = value;
wake();
},
get show() {
return visible;
},
changed: wake,
destroy() {
off();
for (const rec of sets.values()) rec.bank.destroy();
sets.clear();
rows.clear();
},
};
}
function lines(sequences, color, width, prefix) {
const features = [],
shapes = [];
for (const seq of sequences)
for (const path of seq.parts) {
if (path.length < 2) continue;
const id = prefix + seq.id;
features.push({
type: 'Feature',
geometry: { type: 'LineString', coordinates: path },
properties: { id },
});
shapes.push({
id,
geometry: { type: 'line', positions: path },
style: { lineWidth: width },
});
}
if (!features.length) return null;
const source = view.addSource({
type: 'geojson',
data: { type: 'FeatureCollection', features },
});
let layer, unbind;
try {
layer = view.addLayer({
type: 'vector',
source,
polyline: {
color: parseInt(color.slice(1), 16),
width,
opacity: 0.92,
clampToGround: true,
lit: false,
transparent: true,
},
});
unbind = bindGroundShapePicks(engine, 'street-level', shapes);
} catch (e) {
layer?.delete();
source.delete();
throw e;
}
return {
destroy() {
unbind();
layer.delete();
source.delete();
engine.render.request('street-level-line');
},
};
}
return { engine, camera, billboards, lines };
}
+7 -2
View File
@@ -10,7 +10,7 @@ export function createSelection({ state, parts }) {
const viewer = state.viewer;
if (!viewer || !state.enabled) return;
if (input?.isPointerFree && !input.isPointerFree()) return;
const picked = viewer.scene.pick(
const picked = viewer.engine ? click.picked : viewer.scene.pick(
click.position,
CLICK_PICK_WINDOW_PX,
CLICK_PICK_WINDOW_PX,
@@ -33,7 +33,7 @@ export function createSelection({ state, parts }) {
/** Lon/lat of the ground under a screen point, or null (sky, no picker). */
function groundPointAt(viewer, screenPosition) {
const position = state.services.scenePick?.pickGroundPosition?.(
const position = viewer.engine ? viewer.engine.picker.pickGlobe(screenPosition) : state.services.scenePick?.pickGroundPosition?.(
viewer,
screenPosition,
);
@@ -68,6 +68,10 @@ export function createSelection({ state, parts }) {
function install(viewer) {
if (state.clickHandler) return;
if (viewer.engine) {
const remove = viewer.engine.input.on('click',onClick,{pick:true});
state.clickHandler = { isDestroyed:()=>false,destroy:remove };
} else {
state.clickHandler = new Cesium.ScreenSpaceEventHandler(
viewer.scene.canvas,
);
@@ -75,6 +79,7 @@ export function createSelection({ state, parts }) {
onClick,
Cesium.ScreenSpaceEventType.LEFT_CLICK,
);
}
document.addEventListener('keydown', onKeyDown);
picking?.registerPickOwner?.(STREET_LEVEL_LAYER_ID, parts.router.ownsPick);
}
+15 -5
View File
@@ -82,6 +82,10 @@ export function createSequenceCones({
function ensureCollections(viewer) {
if (state.sequence.collection) return;
if (viewer?.engine) {
state.sequence.collection = viewer.billboards(spriteId);
return;
}
state.sequence.collection = new Cesium.BillboardCollection({
scene: viewer.scene,
});
@@ -128,7 +132,8 @@ export function createSequenceCones({
});
cones.push(billboard);
}
horizon.update();
if (state.viewer?.engine) collection.changed();
else horizon.update();
requestRender();
}
@@ -137,7 +142,7 @@ export function createSequenceCones({
* reloads a selected one (a provider that loads part of a sequence moved on).
*/
async function select(sequenceId, { force = false } = {}) {
if (!sequenceId || !state.viewer) return;
if (!sequenceId || (!state.viewer && !state.context.waitingForView?.())) return;
// Already on screen, or already on its way.
if (
!force &&
@@ -206,22 +211,27 @@ export function createSequenceCones({
requestRender();
}
function destroy(viewer) {
state.sequence.abort?.abort();
function releaseCollection(viewer) {
horizon.stop();
cones = [];
const collection = state.sequence.collection;
if (collection) {
sprites?.unregisterSpriteCollection?.(spriteId, collection);
viewer?.scene?.primitives?.remove(collection);
if (viewer?.engine) collection.destroy();
else viewer?.scene?.primitives?.remove(collection);
state.sequence.collection = null;
}
}
function destroy(viewer) {
state.sequence.abort?.abort();
releaseCollection(viewer);
state.sequence.images = [];
state.sequence.selectedId = null;
}
return {
ensureCollections,
releaseCollection,
select,
clearSelection,
rerender,
+28 -17
View File
@@ -1,6 +1,9 @@
import * as Cesium from 'cesium';
import { wrapLon } from './tileMath.js';
export const cameraLocation = (view) =>
view?.engine?.camera.pose().cartographic ?? view?.camera?.positionCartographic;
const canvasOf = (view) => view?.engine?.host.canvas ?? view?.scene?.canvas;
/** Equirectangular metres between {lon, lat} points, the short way round ±180°; street scale. */
export function metresBetween(a, b) {
const lat = (((a.lat + b.lat) / 2) * Math.PI) / 180;
@@ -36,8 +39,13 @@ export function sampledSurfaceHeight(scene, carto) {
* the camera.
*/
export function groundUnderCamera(viewer) {
const carto = viewer?.camera?.positionCartographic;
const carto = cameraLocation(viewer);
const scene = viewer?.scene;
if (carto && viewer?.engine) {
const surface = viewer.engine.surface;
const h = surface.sampleHeight(carto) ?? surface.terrainHeight(carto);
return plausibleSurfaceHeight(h) && h < carto.height ? h : null;
}
if (!carto || !scene) return null;
const globe = scene.globe;
if (globe && globe.show !== false) {
@@ -59,26 +67,26 @@ function raisedEllipsoid(ellipsoid, height) {
* Where a screen point's ray meets the ground, as a Cartographic, or null.
* `maxRange` (metres from the camera) drops hits near the horizon.
*/
function groundHit(camera, point, ellipsoid, maxRange) {
function groundHit(camera, point, ellipsoid, maxRange, engine) {
let cartesian = null;
try {
cartesian = camera.pickEllipsoid(point, ellipsoid);
cartesian = engine ? engine.picker.pickGlobe(point) : camera.pickEllipsoid(point, ellipsoid);
} catch {
cartesian = null;
}
if (!cartesian) return null;
if (
Number.isFinite(maxRange) &&
camera.positionWC &&
Cesium.Cartesian3.distance(cartesian, camera.positionWC) > maxRange
(engine?.camera.livePosition || camera.positionWC) &&
Cesium.Cartesian3.distance(cartesian, engine?.camera.livePosition ?? camera.positionWC) > maxRange
)
return null;
return Cesium.Cartographic.fromCartesian(cartesian, ellipsoid) || null;
return Cesium.Cartographic.fromCartesian(cartesian, engine ? Cesium.Ellipsoid.WGS84 : ellipsoid) || null;
}
/** Camera height (m) above the ground under it. */
export function cameraHeightAboveGround(viewer) {
const carto = viewer?.camera?.positionCartographic;
const carto = cameraLocation(viewer);
if (!carto) return null;
return carto.height - (groundUnderCamera(viewer) ?? 0);
}
@@ -97,13 +105,14 @@ export function visibleBbox(
) {
const scene = viewer?.scene;
const camera = viewer?.camera;
if (!scene || !camera) return null;
const width = scene.canvas?.clientWidth || scene.canvas?.width || 0;
const height = scene.canvas?.clientHeight || scene.canvas?.height || 0;
if ((!scene && !viewer?.engine) || !camera) return null;
const canvas = canvasOf(viewer);
const width = canvas?.clientWidth || canvas?.width || 0;
const height = canvas?.clientHeight || canvas?.height || 0;
const hits = [];
if (width > 0 && height > 0) {
const ellipsoid = raisedEllipsoid(
scene.globe?.ellipsoid || Cesium.Ellipsoid.WGS84,
scene?.globe?.ellipsoid || Cesium.Ellipsoid.WGS84,
groundHeight,
);
const point = new Cesium.Cartesian2();
@@ -111,7 +120,7 @@ export function visibleBbox(
for (let j = 0; j <= BBOX_GRID; j++) {
point.x = (width * i) / BBOX_GRID;
point.y = (height * j) / BBOX_GRID;
const carto = groundHit(camera, point, ellipsoid, maxRange);
const carto = groundHit(camera, point, ellipsoid, maxRange, viewer.engine);
if (carto) hits.push(carto);
}
}
@@ -153,7 +162,7 @@ export function visibleBbox(
/** The point on the ground under the camera, as {lon, lat} degrees. */
export function cameraNadir(viewer) {
const carto = viewer?.camera?.positionCartographic;
const carto = cameraLocation(viewer);
if (!carto) return null;
return {
lon: Cesium.Math.toDegrees(carto.longitude),
@@ -171,17 +180,19 @@ export function viewFocus(
) {
const nadir = cameraNadir(viewer);
const scene = viewer?.scene;
const width = scene?.canvas?.clientWidth || scene?.canvas?.width || 0;
const height = scene?.canvas?.clientHeight || scene?.canvas?.height || 0;
const canvas = canvasOf(viewer);
const width = canvas?.clientWidth || canvas?.width || 0;
const height = canvas?.clientHeight || canvas?.height || 0;
if (!nadir || !width || !height) return nadir;
const hit = groundHit(
viewer.camera,
new Cesium.Cartesian2(width / 2, height / 2),
raisedEllipsoid(
scene.globe?.ellipsoid || Cesium.Ellipsoid.WGS84,
scene?.globe?.ellipsoid || Cesium.Ellipsoid.WGS84,
groundHeight,
),
maxRange,
viewer.engine,
);
if (!hit) return nadir;
const ahead = {
@@ -275,7 +286,7 @@ export function viewCentre(viewer) {
if (lon > 180) lon -= 360;
return { lat: (bbox[1] + bbox[3]) / 2, lon };
}
const carto = viewer.camera?.positionCartographic;
const carto = cameraLocation(viewer);
if (!carto) return null;
return {
lat: Cesium.Math.toDegrees(carto.latitude),