mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-07 03:44:00 +00:00
Keep street imagery and picked coverage across Cesium and Navara
This commit is contained in:
1 parent
75df3389f3
commit
b98cbeebf2
20 files changed
+561
-62
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
[
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user