mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
refactor(installations): use globe camera contract
This commit is contained in:
1 parent
c58bc9864b
commit
f4f0088814
6 files changed
+100
-17
No files matched your search
Vendored
+7
-8
@@ -1,4 +1,3 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { installationFeedback } from '../../data/installationFeedback.js';
|
||||
import {
|
||||
@@ -39,7 +38,7 @@ export function createControls({ state: layerState, services, parts, source }) {
|
||||
* only area that answers "what is near this contact". The window moves
|
||||
* only after the subject travels ANCHOR_REFRESH_M, and cached tiles make
|
||||
* that cheap. Passing null returns the layer to viewport loading.
|
||||
* @param {Cesium.Cartesian3|{latitude:number, longitude:number}|null} position
|
||||
* @param {object|{latitude:number, longitude:number}|null} position
|
||||
* Subject position.
|
||||
* @returns {boolean} Whether a new load was scheduled.
|
||||
*/
|
||||
@@ -145,19 +144,19 @@ export function createControls({ state: layerState, services, parts, source }) {
|
||||
*/
|
||||
focusById(id) {
|
||||
const record = layerState.recordById.get(String(id));
|
||||
if (!record || !layerState.viewer) return false;
|
||||
if (!record || !layerState.engine?.camera?.flyToSphere) return false;
|
||||
// No camera flight without a real selection: a flight plus a stale subject
|
||||
// reads as success to Context navigation and strands NEXT on this item.
|
||||
if (!parts.selection.selectRecord(record.id)) return false;
|
||||
layerState.viewer.camera.flyToBoundingSphere(
|
||||
new Cesium.BoundingSphere(
|
||||
geo.cartesian3.fromDegrees(
|
||||
layerState.engine.camera.flyToSphere(
|
||||
{
|
||||
center: geo.cartesian3.fromDegrees(
|
||||
record.longitude,
|
||||
record.latitude,
|
||||
parts.rendering.installationSurfaceHeightM(record),
|
||||
),
|
||||
18000,
|
||||
),
|
||||
radius: 18000,
|
||||
},
|
||||
{ duration: 1.4 },
|
||||
);
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { createControls } from './controls.js';
|
||||
|
||||
test('focus frames a selected installation through the globe engine camera', () => {
|
||||
const record = { id: 'osm:node:12', latitude: 70, longitude: 179.8 };
|
||||
let flight = null;
|
||||
const state = {
|
||||
engine: {
|
||||
camera: {
|
||||
flyToSphere: (sphere, options) => {
|
||||
flight = { sphere, options };
|
||||
},
|
||||
},
|
||||
},
|
||||
recordById: new Map([[record.id, record]]),
|
||||
};
|
||||
const controls = createControls({
|
||||
state,
|
||||
services: {},
|
||||
parts: {
|
||||
selection: { selectRecord: (id) => id === record.id },
|
||||
rendering: { installationSurfaceHeightM: () => 126 },
|
||||
},
|
||||
source: {},
|
||||
}).methods;
|
||||
|
||||
assert.equal(controls.focusById(record.id), true);
|
||||
assert.deepEqual(
|
||||
flight.sphere.center,
|
||||
geo.cartesian3.fromDegrees(179.8, 70, 126),
|
||||
);
|
||||
assert.equal(flight.sphere.radius, 18000);
|
||||
assert.deepEqual(flight.options, { duration: 1.4 });
|
||||
});
|
||||
@@ -15,19 +15,20 @@ export function createLifecycle({
|
||||
const { clearSelectedEntityContextForLayer } = services.context;
|
||||
|
||||
const methods = {
|
||||
init(viewer) {
|
||||
init(viewer, { engine = null } = {}) {
|
||||
layerState.viewer = viewer;
|
||||
layerState.engine = engine;
|
||||
layerState.dataSource = new Cesium.CustomDataSource(
|
||||
'military-installations',
|
||||
);
|
||||
viewer.dataSources.add(layerState.dataSource);
|
||||
const camera = engine?.camera ?? viewer.camera;
|
||||
const cameraChanged = () => {
|
||||
if (!layerState.contextAnchor) parts.viewport.scheduleLoad();
|
||||
};
|
||||
layerState.moveEndRemove =
|
||||
viewer.camera.moveEnd.addEventListener(cameraChanged);
|
||||
layerState.moveEndRemove = camera.moveEnd.addEventListener(cameraChanged);
|
||||
layerState.changedRemove =
|
||||
viewer.camera.changed?.addEventListener(cameraChanged);
|
||||
camera.changed?.addEventListener(cameraChanged);
|
||||
parts.selection.installInteraction(viewer);
|
||||
},
|
||||
|
||||
@@ -100,6 +101,7 @@ export function createLifecycle({
|
||||
if (layerState.dataSource && viewer)
|
||||
viewer.dataSources.remove(layerState.dataSource, true);
|
||||
layerState.dataSource = null;
|
||||
layerState.engine = null;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -189,3 +189,45 @@ test('wide views refresh screen decluttering when the globe rectangle stays unch
|
||||
t.mock.timers.tick(200);
|
||||
assert.equal(loads, 2, 'a different continent needs fresh projected labels');
|
||||
});
|
||||
|
||||
test('the engine camera drives installation viewport requests without a viewer', (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||
const rectangle = {
|
||||
west: Cesium.Math.toRadians(179),
|
||||
south: Cesium.Math.toRadians(-12),
|
||||
east: Cesium.Math.toRadians(-178),
|
||||
north: Cesium.Math.toRadians(16),
|
||||
};
|
||||
const state = {
|
||||
enabled: true,
|
||||
viewer: null,
|
||||
engine: {
|
||||
camera: {
|
||||
viewRectangle: () => rectangle,
|
||||
pose: () => ({
|
||||
position: { x: 1, y: 2, z: 3 },
|
||||
direction: { x: -1, y: 0, z: 0 },
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
const loads = [];
|
||||
const parts = {
|
||||
ingestion: {
|
||||
loadInstallations() {
|
||||
loads.push(parts.viewport.loadArea(null));
|
||||
},
|
||||
},
|
||||
};
|
||||
parts.viewport = createViewport({ state, parts, services: {}, source: {} });
|
||||
|
||||
parts.viewport.scheduleLoad();
|
||||
t.mock.timers.tick(200);
|
||||
|
||||
assert.equal(loads.length, 1);
|
||||
assert.ok(Math.abs(loads[0].box.south + 12) < 1e-10);
|
||||
assert.equal(loads[0].box.west, 179);
|
||||
assert.equal(loads[0].box.north, 16);
|
||||
assert.equal(loads[0].box.east, -178);
|
||||
assert.equal(loads[0].coverage.kind, 'viewport');
|
||||
});
|
||||
@@ -9,6 +9,7 @@ export function createState({ services }) {
|
||||
|
||||
Object.assign(state, {
|
||||
viewer: null,
|
||||
engine: null,
|
||||
dataSource: null,
|
||||
enabled: false,
|
||||
records: [],
|
||||
|
||||
@@ -23,9 +23,9 @@ export function createViewport({ state: layerState, services, parts, source }) {
|
||||
}
|
||||
|
||||
function viewportBox(viewer) {
|
||||
const rectangle = viewer?.camera?.computeViewRectangle(
|
||||
viewer.scene.globe.ellipsoid,
|
||||
);
|
||||
const rectangle =
|
||||
layerState.engine?.camera?.viewRectangle?.() ??
|
||||
viewer?.camera?.computeViewRectangle(viewer.scene.globe.ellipsoid);
|
||||
if (!rectangle) return null;
|
||||
const south = geo.math.toDegrees(rectangle.south);
|
||||
const north = geo.math.toDegrees(rectangle.north);
|
||||
@@ -89,14 +89,17 @@ export function createViewport({ state: layerState, services, parts, source }) {
|
||||
function scheduleLoad() {
|
||||
if (!layerState.enabled) return;
|
||||
const { box, coverage } = loadArea(layerState.viewer);
|
||||
const camera = layerState.viewer.camera;
|
||||
const pose = layerState.engine?.camera?.pose?.();
|
||||
const camera = layerState.viewer?.camera;
|
||||
const position = pose?.position ?? camera?.positionWC;
|
||||
const direction = pose?.direction ?? camera?.directionWC;
|
||||
const widePose =
|
||||
coverage.kind === 'viewport' &&
|
||||
box &&
|
||||
(box.east < box.west ||
|
||||
box.east - box.west > 10 ||
|
||||
box.north - box.south > 10)
|
||||
? [camera.positionWC, camera.directionWC]
|
||||
? [position, direction]
|
||||
.map((v) =>
|
||||
v ? [v.x, v.y, v.z].map((n) => n.toFixed(4)).join(',') : '',
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user