mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +00:00
Fly layer focus flights through the camera port
flyToBounds takes an easing, as flyTo does: in the contract, the Cesium port and the reference recorder, pinned by a new conformance trace. The flights of radio, awareness, CCTV, submarine cables, cyclones, the launch preview, installations, ALPR, street-level framing, recent imagery, weather and world focus now go through the renderer's camera port. Street-level framing cancels its own flight handle rather than whatever flight is current. worldFocus.js no longer reaches Cesium and leaves the allow-list. Layer pose reads stay for each layer's own slice. The local GeoJSON flight stays on Cesium: gods-eye-view/infrastructure is used with plain Cesium viewers that have no renderer bound.
This commit is contained in:
1 parent
f95324acb1
commit
e3baeec223
31 files changed
+2349
-871
No files matched your search
@@ -1064,6 +1064,12 @@
|
|||||||
"src/ui/visualPresets.js",
|
"src/ui/visualPresets.js",
|
||||||
"src/ui/visualSettings.js",
|
"src/ui/visualSettings.js",
|
||||||
"src/worldFocus.js",
|
"src/worldFocus.js",
|
||||||
|
"src/geo/index.js",
|
||||||
|
"src/geo/vector.js",
|
||||||
|
"src/geo/ellipsoid.js",
|
||||||
|
"src/geo/frames.js",
|
||||||
|
"src/geo/celestial.js",
|
||||||
|
"src/geo/quaternion.js",
|
||||||
"src/data/feedState.js",
|
"src/data/feedState.js",
|
||||||
"src/data/layerSnapshot.js",
|
"src/data/layerSnapshot.js",
|
||||||
"src/ui/aircraftDisplay.js",
|
"src/ui/aircraftDisplay.js",
|
||||||
@@ -1574,6 +1580,7 @@
|
|||||||
"modules": [
|
"modules": [
|
||||||
"src/i18n/marker.js",
|
"src/i18n/marker.js",
|
||||||
"src/layers/recentImagery/index.js",
|
"src/layers/recentImagery/index.js",
|
||||||
|
"src/render/rendererOf.js",
|
||||||
"src/layers/recentImagery/source.js",
|
"src/layers/recentImagery/source.js",
|
||||||
"src/layers/recentImagery/model.js",
|
"src/layers/recentImagery/model.js",
|
||||||
"src/layers/recentImagery/catalog.js",
|
"src/layers/recentImagery/catalog.js",
|
||||||
@@ -1714,6 +1721,7 @@
|
|||||||
"src/layers/launches/paths.js",
|
"src/layers/launches/paths.js",
|
||||||
"src/layers/launches/policy.js",
|
"src/layers/launches/policy.js",
|
||||||
"src/layers/launches/policyHelpers.js",
|
"src/layers/launches/policyHelpers.js",
|
||||||
|
"src/render/rendererOf.js",
|
||||||
"src/layers/launches/rendering.js",
|
"src/layers/launches/rendering.js",
|
||||||
"src/layers/launches/replay.js",
|
"src/layers/launches/replay.js",
|
||||||
"src/layers/launches/selection.js",
|
"src/layers/launches/selection.js",
|
||||||
@@ -1753,6 +1761,7 @@
|
|||||||
"src/data/militaryInstallationData.js",
|
"src/data/militaryInstallationData.js",
|
||||||
"src/data/installationNames.js",
|
"src/data/installationNames.js",
|
||||||
"src/layers/installations/controls.js",
|
"src/layers/installations/controls.js",
|
||||||
|
"src/render/rendererOf.js",
|
||||||
"src/layers/installations/index.js",
|
"src/layers/installations/index.js",
|
||||||
"src/layers/installations/ingestion.js",
|
"src/layers/installations/ingestion.js",
|
||||||
"src/layers/installations/lifecycle.js",
|
"src/layers/installations/lifecycle.js",
|
||||||
@@ -1820,6 +1829,7 @@
|
|||||||
"src/layers/awareness/controls.js",
|
"src/layers/awareness/controls.js",
|
||||||
"src/layers/awareness/dependencies.js",
|
"src/layers/awareness/dependencies.js",
|
||||||
"src/layers/awareness/focus.js",
|
"src/layers/awareness/focus.js",
|
||||||
|
"src/render/rendererOf.js",
|
||||||
"src/layers/awareness/history.js",
|
"src/layers/awareness/history.js",
|
||||||
"src/layers/awareness/index.js",
|
"src/layers/awareness/index.js",
|
||||||
"src/layers/awareness/ingestion.js",
|
"src/layers/awareness/ingestion.js",
|
||||||
@@ -1986,6 +1996,7 @@
|
|||||||
"src/geoDistance.js",
|
"src/geoDistance.js",
|
||||||
"src/layers/cctv/modelSpecs.js",
|
"src/layers/cctv/modelSpecs.js",
|
||||||
"src/layers/cctv/navigation.js",
|
"src/layers/cctv/navigation.js",
|
||||||
|
"src/render/rendererOf.js",
|
||||||
"src/layers/cctv/policy.js",
|
"src/layers/cctv/policy.js",
|
||||||
"src/layers/cctv/presentation.js",
|
"src/layers/cctv/presentation.js",
|
||||||
"src/layers/cctv/projection.js",
|
"src/layers/cctv/projection.js",
|
||||||
@@ -2045,6 +2056,7 @@
|
|||||||
"src/layers/streetLevel/providers/mapillary/index.js",
|
"src/layers/streetLevel/providers/mapillary/index.js",
|
||||||
"src/layers/streetLevel/viewerHost.js",
|
"src/layers/streetLevel/viewerHost.js",
|
||||||
"src/layers/streetLevel/cameraFraming.js",
|
"src/layers/streetLevel/cameraFraming.js",
|
||||||
|
"src/render/rendererOf.js",
|
||||||
"src/layers/streetLevel/marker.js",
|
"src/layers/streetLevel/marker.js",
|
||||||
"src/layers/streetLevel/view.js",
|
"src/layers/streetLevel/view.js",
|
||||||
"src/layers/streetLevel/uiState.js",
|
"src/layers/streetLevel/uiState.js",
|
||||||
@@ -2120,6 +2132,7 @@
|
|||||||
"src/layers/alpr/model.js",
|
"src/layers/alpr/model.js",
|
||||||
"src/layers/alpr/policy.js",
|
"src/layers/alpr/policy.js",
|
||||||
"src/layers/alpr/presentation.js",
|
"src/layers/alpr/presentation.js",
|
||||||
|
"src/render/rendererOf.js",
|
||||||
"src/layers/alpr/visuals.js",
|
"src/layers/alpr/visuals.js",
|
||||||
"src/layers/alpr/overlay.js",
|
"src/layers/alpr/overlay.js",
|
||||||
"src/layers/alpr/markerState.js",
|
"src/layers/alpr/markerState.js",
|
||||||
@@ -2179,6 +2192,7 @@
|
|||||||
"src/layers/radio/lifecycle.js",
|
"src/layers/radio/lifecycle.js",
|
||||||
"src/layers/radio/model.js",
|
"src/layers/radio/model.js",
|
||||||
"src/layers/radio/navigation.js",
|
"src/layers/radio/navigation.js",
|
||||||
|
"src/render/rendererOf.js",
|
||||||
"src/layers/radio/playback.js",
|
"src/layers/radio/playback.js",
|
||||||
"src/layers/radio/policy.js",
|
"src/layers/radio/policy.js",
|
||||||
"src/layers/radio/presentation.js",
|
"src/layers/radio/presentation.js",
|
||||||
@@ -2224,6 +2238,7 @@
|
|||||||
"src/layers/submarineCables/index.js",
|
"src/layers/submarineCables/index.js",
|
||||||
"src/layers/submarineCables/ingestion.js",
|
"src/layers/submarineCables/ingestion.js",
|
||||||
"src/layers/submarineCables/interaction.js",
|
"src/layers/submarineCables/interaction.js",
|
||||||
|
"src/render/rendererOf.js",
|
||||||
"src/layers/submarineCables/pickState.js",
|
"src/layers/submarineCables/pickState.js",
|
||||||
"src/data/entityRecords.js",
|
"src/data/entityRecords.js",
|
||||||
"src/layers/submarineCables/lifecycle.js",
|
"src/layers/submarineCables/lifecycle.js",
|
||||||
@@ -3821,6 +3836,12 @@
|
|||||||
"src/ui/panelRailGeometry.js",
|
"src/ui/panelRailGeometry.js",
|
||||||
"src/ui/radioTunerModel.js",
|
"src/ui/radioTunerModel.js",
|
||||||
"src/worldFocus.js",
|
"src/worldFocus.js",
|
||||||
|
"src/geo/index.js",
|
||||||
|
"src/geo/vector.js",
|
||||||
|
"src/geo/ellipsoid.js",
|
||||||
|
"src/geo/frames.js",
|
||||||
|
"src/geo/celestial.js",
|
||||||
|
"src/geo/quaternion.js",
|
||||||
"src/layers/flights/records.js",
|
"src/layers/flights/records.js",
|
||||||
"src/layers/flights/recordPolicy.js",
|
"src/layers/flights/recordPolicy.js",
|
||||||
"src/layers/flights/snapshotRenderer.js",
|
"src/layers/flights/snapshotRenderer.js",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
|
import { bindFakeCesiumCamera } from '../testSupport/fakeCameraPort.mjs';
|
||||||
import alprCamerasLayer, {
|
import alprCamerasLayer, {
|
||||||
createAlprCamerasLayer,
|
createAlprCamerasLayer,
|
||||||
configureAlprSource,
|
configureAlprSource,
|
||||||
@@ -342,6 +343,9 @@ function cameraHarness(layer = alprCamerasLayer) {
|
|||||||
remove() {},
|
remove() {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
bindFakeCesiumCamera(viewer);
|
||||||
|
// The renderer's camera port relays moveEnd; the layer's own come on top.
|
||||||
|
const portMoveEndListeners = viewer.camera.moveEnd.numberOfListeners;
|
||||||
const setInput = Cesium.ScreenSpaceEventHandler.prototype.setInputAction;
|
const setInput = Cesium.ScreenSpaceEventHandler.prototype.setInputAction;
|
||||||
Cesium.ScreenSpaceEventHandler.prototype.setInputAction = function (
|
Cesium.ScreenSpaceEventHandler.prototype.setInputAction = function (
|
||||||
fn,
|
fn,
|
||||||
@@ -364,6 +368,7 @@ function cameraHarness(layer = alprCamerasLayer) {
|
|||||||
layer.enable();
|
layer.enable();
|
||||||
return {
|
return {
|
||||||
viewer,
|
viewer,
|
||||||
|
portMoveEndListeners,
|
||||||
requests,
|
requests,
|
||||||
credits,
|
credits,
|
||||||
get source() {
|
get source() {
|
||||||
@@ -932,7 +937,10 @@ test('two factories keep their requests, records and destruction independent', a
|
|||||||
b.source.entities.values[0],
|
b.source.entities.values[0],
|
||||||
)?.labelModel.details.includes('Source: Test directory'),
|
)?.labelModel.details.includes('Source: Test directory'),
|
||||||
);
|
);
|
||||||
assert.equal(a.viewer.camera.moveEnd.numberOfListeners, 0);
|
assert.equal(
|
||||||
|
a.viewer.camera.moveEnd.numberOfListeners,
|
||||||
|
a.portMoveEndListeners,
|
||||||
|
);
|
||||||
assert.equal(a.viewer.scene.postRender.numberOfListeners, 0);
|
assert.equal(a.viewer.scene.postRender.numberOfListeners, 0);
|
||||||
} finally {
|
} finally {
|
||||||
b.restore();
|
b.restore();
|
||||||
|
|||||||
+14
-13
@@ -13,6 +13,7 @@
|
|||||||
//
|
//
|
||||||
// computeFrustumGeometry is PURE (no viewer, no scene queries) so it runs under
|
// computeFrustumGeometry is PURE (no viewer, no scene queries) so it runs under
|
||||||
// plain node:test.
|
// plain node:test.
|
||||||
|
import { bindFakeCesiumCamera } from '../testSupport/fakeCameraPort.mjs';
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
// Preserve structural pins across component qualification and formatter wrapping.
|
// Preserve structural pins across component qualification and formatter wrapping.
|
||||||
function componentFunctionSource(fn) {
|
function componentFunctionSource(fn) {
|
||||||
@@ -868,6 +869,7 @@ test('CCTV focus distinguishes tracking ownership from a missing active camera',
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
bindFakeCesiumCamera(viewer);
|
||||||
const record = {
|
const record = {
|
||||||
camera: { rangeM: 210, headingDeg: 41 },
|
camera: { rangeM: 210, headingDeg: 41 },
|
||||||
position: { x: 1, y: 2, z: 3 },
|
position: { x: 1, y: 2, z: 3 },
|
||||||
@@ -899,6 +901,7 @@ test('CCTV focus reports when the camera flight starts', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
bindFakeCesiumCamera(viewer);
|
||||||
const record = {
|
const record = {
|
||||||
camera: { rangeM: 210, headingDeg: 41 },
|
camera: { rangeM: 210, headingDeg: 41 },
|
||||||
position: { x: 1, y: 2, z: 3 },
|
position: { x: 1, y: 2, z: 3 },
|
||||||
@@ -919,6 +922,7 @@ test('CCTV focus refuses camera flights while cockpit owns the view', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
bindFakeCesiumCamera(viewer);
|
||||||
const record = {
|
const record = {
|
||||||
camera: { rangeM: 210, headingDeg: 41 },
|
camera: { rangeM: 210, headingDeg: 41 },
|
||||||
position: { x: 1, y: 2, z: 3 },
|
position: { x: 1, y: 2, z: 3 },
|
||||||
@@ -1397,19 +1401,16 @@ test('CCTV drag-then-release over a camera is inert, while a clean tap activates
|
|||||||
target.addEventListener(CCTV_FOCUS_REQUEST_EVENT, (event) =>
|
target.addEventListener(CCTV_FOCUS_REQUEST_EVENT, (event) =>
|
||||||
requests.push(event.detail),
|
requests.push(event.detail),
|
||||||
);
|
);
|
||||||
bindCctvWorldClickGesture(
|
bindCctvWorldClickGesture(input, () => {
|
||||||
input,
|
activateCctvCameraFromWorldClick(
|
||||||
() => {
|
'atx-cam-3',
|
||||||
activateCctvCameraFromWorldClick(
|
() => {
|
||||||
'atx-cam-3',
|
activationCalls += 1;
|
||||||
() => {
|
return CCTV_ACTIVATION_RESULT.ACTIVATED;
|
||||||
activationCalls += 1;
|
},
|
||||||
return CCTV_ACTIVATION_RESULT.ACTIVATED;
|
target,
|
||||||
},
|
);
|
||||||
target,
|
});
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
handler.fire(GESTURE_TYPES.LEFT_DOWN, { position: { x: 10, y: 10 } });
|
handler.fire(GESTURE_TYPES.LEFT_DOWN, { position: { x: 10, y: 10 } });
|
||||||
timeMs = 20;
|
timeMs = 20;
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import {
|
|||||||
import { NAVIGATION_AUTHORITY_EVENT } from '../navigationPolicy.js';
|
import { NAVIGATION_AUTHORITY_EVENT } from '../navigationPolicy.js';
|
||||||
import { withEntityRecord } from './entityRecords.js';
|
import { withEntityRecord } from './entityRecords.js';
|
||||||
import { framePortFor } from '../testSupport/fakeFramePort.mjs';
|
import { framePortFor } from '../testSupport/fakeFramePort.mjs';
|
||||||
|
import { bindFakeCesiumCamera } from '../testSupport/fakeCameraPort.mjs';
|
||||||
|
|
||||||
const militaryAwarenessSource = readLayerSource(
|
const militaryAwarenessSource = readLayerSource(
|
||||||
new URL('./militaryAwareness.js', import.meta.url),
|
new URL('./militaryAwareness.js', import.meta.url),
|
||||||
@@ -183,6 +184,7 @@ function installAwarenessRuntime({
|
|||||||
},
|
},
|
||||||
container: new FakeElement(),
|
container: new FakeElement(),
|
||||||
};
|
};
|
||||||
|
bindFakeCesiumCamera(viewer);
|
||||||
const layers = new Map(
|
const layers = new Map(
|
||||||
AWARENESS_DEPENDENCIES.map((layerId) => [
|
AWARENESS_DEPENDENCIES.map((layerId) => [
|
||||||
layerId,
|
layerId,
|
||||||
@@ -825,7 +827,10 @@ for (const fixture of [
|
|||||||
let retrackCalls = 0;
|
let retrackCalls = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const trackedEntity = withEntityRecord({}, { trackedId: `${subject.layerId}:${subject.id}` });
|
const trackedEntity = withEntityRecord(
|
||||||
|
{},
|
||||||
|
{ trackedId: `${subject.layerId}:${subject.id}` },
|
||||||
|
);
|
||||||
runtime.viewer.trackedEntity = trackedEntity;
|
runtime.viewer.trackedEntity = trackedEntity;
|
||||||
replaceMethod(
|
replaceMethod(
|
||||||
fixture.layer,
|
fixture.layer,
|
||||||
@@ -927,7 +932,10 @@ test('Contacts activation adopts the production military tracked-subject descrip
|
|||||||
),
|
),
|
||||||
'mutating one accessor result must not alter the tracked source position',
|
'mutating one accessor result must not alter the tracked source position',
|
||||||
);
|
);
|
||||||
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: `military:${id}` });
|
runtime.viewer.trackedEntity = withEntityRecord(
|
||||||
|
{},
|
||||||
|
{ trackedId: `military:${id}` },
|
||||||
|
);
|
||||||
replaceMethod(flightsLayer, 'getTrackedSubject', () => null, restores);
|
replaceMethod(flightsLayer, 'getTrackedSubject', () => null, restores);
|
||||||
replaceMethod(
|
replaceMethod(
|
||||||
flightsLayer,
|
flightsLayer,
|
||||||
@@ -1032,7 +1040,10 @@ test('production military tracked-subject label falls back to registration befor
|
|||||||
'getAllPositions id stays the identity hex',
|
'getAllPositions id stays the identity hex',
|
||||||
);
|
);
|
||||||
|
|
||||||
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: `military:${id}` });
|
runtime.viewer.trackedEntity = withEntityRecord(
|
||||||
|
{},
|
||||||
|
{ trackedId: `military:${id}` },
|
||||||
|
);
|
||||||
replaceMethod(flightsLayer, 'getTrackedSubject', () => null, restores);
|
replaceMethod(flightsLayer, 'getTrackedSubject', () => null, restores);
|
||||||
replaceMethod(flightsLayer, 'trackById', () => true, restores);
|
replaceMethod(flightsLayer, 'trackById', () => true, restores);
|
||||||
replaceMethod(militaryFlightsLayer, 'trackById', () => true, restores);
|
replaceMethod(militaryFlightsLayer, 'trackById', () => true, restores);
|
||||||
@@ -1104,7 +1115,10 @@ test('a cached Context subject re-reads its label when enrichment lands after se
|
|||||||
meta,
|
meta,
|
||||||
});
|
});
|
||||||
|
|
||||||
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: `flights:${id}` });
|
runtime.viewer.trackedEntity = withEntityRecord(
|
||||||
|
{},
|
||||||
|
{ trackedId: `flights:${id}` },
|
||||||
|
);
|
||||||
replaceMethod(
|
replaceMethod(
|
||||||
militaryFlightsLayer,
|
militaryFlightsLayer,
|
||||||
'getTrackedSubject',
|
'getTrackedSubject',
|
||||||
@@ -1247,7 +1261,10 @@ test('Contacts activation adopts the production civilian tracked-subject descrip
|
|||||||
),
|
),
|
||||||
'mutating one accessor result must not alter the tracked source position',
|
'mutating one accessor result must not alter the tracked source position',
|
||||||
);
|
);
|
||||||
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: `flights:${id}` });
|
runtime.viewer.trackedEntity = withEntityRecord(
|
||||||
|
{},
|
||||||
|
{ trackedId: `flights:${id}` },
|
||||||
|
);
|
||||||
replaceMethod(
|
replaceMethod(
|
||||||
militaryFlightsLayer,
|
militaryFlightsLayer,
|
||||||
'getTrackedSubject',
|
'getTrackedSubject',
|
||||||
@@ -1334,7 +1351,10 @@ test('Contacts activation reconciles to a newer cross-layer tracked flight after
|
|||||||
const restores = [];
|
const restores = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: 'flights:first' });
|
runtime.viewer.trackedEntity = withEntityRecord(
|
||||||
|
{},
|
||||||
|
{ trackedId: 'flights:first' },
|
||||||
|
);
|
||||||
replaceMethod(
|
replaceMethod(
|
||||||
flightsLayer,
|
flightsLayer,
|
||||||
'getTrackedSubject',
|
'getTrackedSubject',
|
||||||
@@ -1357,7 +1377,10 @@ test('Contacts activation reconciles to a newer cross-layer tracked flight after
|
|||||||
|
|
||||||
civilianTracked = null;
|
civilianTracked = null;
|
||||||
militaryTracked = awarenessSubject('military', 'latest', positions.latest);
|
militaryTracked = awarenessSubject('military', 'latest', positions.latest);
|
||||||
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: 'military:latest' });
|
runtime.viewer.trackedEntity = withEntityRecord(
|
||||||
|
{},
|
||||||
|
{ trackedId: 'military:latest' },
|
||||||
|
);
|
||||||
releaseDependencies();
|
releaseDependencies();
|
||||||
await nextTurn();
|
await nextTurn();
|
||||||
await nextTurn();
|
await nextTurn();
|
||||||
@@ -1401,7 +1424,10 @@ test('tracked flight cleared during Contacts activation suppresses fallback auto
|
|||||||
const focused = [];
|
const focused = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: 'flights:selected' });
|
runtime.viewer.trackedEntity = withEntityRecord(
|
||||||
|
{},
|
||||||
|
{ trackedId: 'flights:selected' },
|
||||||
|
);
|
||||||
replaceMethod(flightsLayer, 'getTrackedSubject', () => tracked, restores);
|
replaceMethod(flightsLayer, 'getTrackedSubject', () => tracked, restores);
|
||||||
replaceMethod(
|
replaceMethod(
|
||||||
militaryFlightsLayer,
|
militaryFlightsLayer,
|
||||||
@@ -1999,7 +2025,7 @@ test('vessel entry and selection framing both use the 3 km focus radius', () =>
|
|||||||
);
|
);
|
||||||
const focusRadiusUses =
|
const focusRadiusUses =
|
||||||
militaryAwarenessSource.match(
|
militaryAwarenessSource.match(
|
||||||
/new\s*Cesium\.BoundingSphere\(\s*vessel\.position,\s*VESSEL_FOCUS_RADIUS_M,?\s*\)/g,
|
/\{\s*center:\s*vessel\.position,\s*radius:\s*VESSEL_FOCUS_RADIUS_M\s*\}/g,
|
||||||
) || [];
|
) || [];
|
||||||
assert.equal(focusRadiusUses.length, 2);
|
assert.equal(focusRadiusUses.length, 2);
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
+1179
-476
File diff suppressed because it is too large.
Load diff
@@ -26,6 +26,7 @@ import {
|
|||||||
import { entityRecord, readEntityRecord } from '../../data/entityRecords.js';
|
import { entityRecord, readEntityRecord } from '../../data/entityRecords.js';
|
||||||
import { alprMarkers } from './markerState.js';
|
import { alprMarkers } from './markerState.js';
|
||||||
import { heightsFor } from '../../queries/heights.js';
|
import { heightsFor } from '../../queries/heights.js';
|
||||||
|
import { cameraOf } from '../../render/rendererOf.js';
|
||||||
|
|
||||||
export function createAlprPresentation({ state, services, source }) {
|
export function createAlprPresentation({ state, services, source }) {
|
||||||
const { governorRequestRender } = services.render;
|
const { governorRequestRender } = services.render;
|
||||||
@@ -534,14 +535,18 @@ export function createAlprPresentation({ state, services, source }) {
|
|||||||
validAlprGroundHeight(height) ? height : 0,
|
validAlprGroundHeight(height) ? height : 0,
|
||||||
);
|
);
|
||||||
if (!selectRecord(nearest.id)) return false;
|
if (!selectRecord(nearest.id)) return false;
|
||||||
camera.flyToBoundingSphere(new Cesium.BoundingSphere(center, 30), {
|
const port = cameraOf(state.viewer);
|
||||||
duration: 1.2,
|
port.flyToBounds(
|
||||||
offset: new Cesium.HeadingPitchRange(
|
{ center, radius: 30 },
|
||||||
camera.heading || 0,
|
{
|
||||||
-Math.PI / 4,
|
durationS: 1.2,
|
||||||
800,
|
offset: {
|
||||||
),
|
heading: port.pose().heading || 0,
|
||||||
});
|
pitch: -Math.PI / 4,
|
||||||
|
range: 800,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
governorRequestRender('alpr-focus');
|
governorRequestRender('alpr-focus');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { VESSEL_FOCUS_RADIUS_M } from './policy.js';
|
import { VESSEL_FOCUS_RADIUS_M } from './policy.js';
|
||||||
|
import { cameraOf } from '../../render/rendererOf.js';
|
||||||
|
|
||||||
export function createFocus({ state: layerState, services, parts, source }) {
|
export function createFocus({ state: layerState, services, parts, source }) {
|
||||||
const flightsLayer = services.flights;
|
const flightsLayer = services.flights;
|
||||||
@@ -45,9 +46,9 @@ export function createFocus({ state: layerState, services, parts, source }) {
|
|||||||
)
|
)
|
||||||
return true;
|
return true;
|
||||||
announceNavigationAuthority('context-vessel-focus');
|
announceNavigationAuthority('context-vessel-focus');
|
||||||
layerState.viewer.camera.flyToBoundingSphere(
|
cameraOf(layerState.viewer).flyToBounds(
|
||||||
new Cesium.BoundingSphere(vessel.position, VESSEL_FOCUS_RADIUS_M),
|
{ center: vessel.position, radius: VESSEL_FOCUS_RADIUS_M },
|
||||||
{ duration: 1.4 },
|
{ durationS: 1.4 },
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -180,9 +181,9 @@ export function createFocus({ state: layerState, services, parts, source }) {
|
|||||||
announceNavigationAuthority('context-vessel-autofocus', {
|
announceNavigationAuthority('context-vessel-autofocus', {
|
||||||
cancelPendingSelection: false,
|
cancelPendingSelection: false,
|
||||||
});
|
});
|
||||||
layerState.viewer.camera.flyToBoundingSphere(
|
cameraOf(layerState.viewer).flyToBounds(
|
||||||
new Cesium.BoundingSphere(vessel.position, VESSEL_FOCUS_RADIUS_M),
|
{ center: vessel.position, radius: VESSEL_FOCUS_RADIUS_M },
|
||||||
{ duration: 1.6 },
|
{ durationS: 1.6 },
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { CCTV_FOCUS_RESULT } from './policy.js';
|
import { CCTV_FOCUS_RESULT } from './policy.js';
|
||||||
|
import { cameraOf } from '../../render/rendererOf.js';
|
||||||
|
|
||||||
export function createNavigation({
|
export function createNavigation({
|
||||||
state: layerState,
|
state: layerState,
|
||||||
@@ -59,19 +60,16 @@ export function createNavigation({
|
|||||||
}
|
}
|
||||||
const { camera } = record;
|
const { camera } = record;
|
||||||
const range = Math.max(280, camera.rangeM * 1.18);
|
const range = Math.max(280, camera.rangeM * 1.18);
|
||||||
viewer.camera.flyToBoundingSphere(
|
cameraOf(viewer).flyToBounds(
|
||||||
new Cesium.BoundingSphere(
|
{ center: record.position, radius: Math.max(40, camera.rangeM * 0.36) },
|
||||||
record.position,
|
|
||||||
Math.max(40, camera.rangeM * 0.36),
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
offset: new Cesium.HeadingPitchRange(
|
offset: {
|
||||||
parts.model.toRad(camera.headingDeg),
|
heading: parts.model.toRad(camera.headingDeg),
|
||||||
parts.model.toRad(-22),
|
pitch: parts.model.toRad(-22),
|
||||||
range,
|
range,
|
||||||
),
|
},
|
||||||
duration: Math.max(0.2, duration || 0),
|
durationS: Math.max(0.2, duration || 0),
|
||||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
easing: 'cubicInOut',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return CCTV_FOCUS_RESULT.FOCUSED;
|
return CCTV_FOCUS_RESULT.FOCUSED;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
translateKnown,
|
translateKnown,
|
||||||
} from '../../i18n/index.js';
|
} from '../../i18n/index.js';
|
||||||
import { formatQuantity } from '../../ui/unitLabels.js';
|
import { formatQuantity } from '../../ui/unitLabels.js';
|
||||||
|
import { cameraOf } from '../../render/rendererOf.js';
|
||||||
|
|
||||||
// "10-12 15:00 UTC" never breaks across lines: no-break spaces, and a word
|
// "10-12 15:00 UTC" never breaks across lines: no-break spaces, and a word
|
||||||
// joiner after the hyphen (JetBrains Mono has no U+2011 non-breaking hyphen).
|
// joiner after the hyphen (JetBrains Mono has no U+2011 non-breaking hyphen).
|
||||||
@@ -319,12 +320,15 @@ export function createCyclonesLayer({
|
|||||||
selectedId !== storm.id
|
selectedId !== storm.id
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
return viewer.camera.flyToBoundingSphere(sphere, {
|
cameraOf(viewer).flyToBounds(
|
||||||
duration: matchMedia?.('(prefers-reduced-motion: reduce)')
|
{ center: sphere.center, radius: sphere.radius },
|
||||||
?.matches
|
{
|
||||||
? 0
|
durationS: matchMedia?.('(prefers-reduced-motion: reduce)')
|
||||||
: 1.4,
|
?.matches
|
||||||
});
|
? 0
|
||||||
|
: 1.4,
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from '../../data/inputOwnership.js';
|
} from '../../data/inputOwnership.js';
|
||||||
import { isOwnedByOtherLayer } from '../../data/pickRegistry.js';
|
import { isOwnedByOtherLayer } from '../../data/pickRegistry.js';
|
||||||
import { fakeInput } from '../../input/testDoubles.mjs';
|
import { fakeInput } from '../../input/testDoubles.mjs';
|
||||||
|
import { bindFakeCesiumCamera } from '../../testSupport/fakeCameraPort.mjs';
|
||||||
|
|
||||||
/** Record each click subscription the layer makes, as the old handler fakes did. */
|
/** Record each click subscription the layer makes, as the old handler fakes did. */
|
||||||
function recordHandlers(viewer, handlers) {
|
function recordHandlers(viewer, handlers) {
|
||||||
@@ -120,6 +121,7 @@ test('map selection yields to pointer owners and owns only its enabled handler',
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
bindFakeCesiumCamera(viewer);
|
||||||
recordHandlers(viewer, handlers);
|
recordHandlers(viewer, handlers);
|
||||||
layer.init(viewer);
|
layer.init(viewer);
|
||||||
layer.setRowControlsListener(() => notices++);
|
layer.setRowControlsListener(() => notices++);
|
||||||
@@ -360,7 +362,10 @@ function harness(
|
|||||||
setSelection(id) {
|
setSelection(id) {
|
||||||
selection = id;
|
selection = id;
|
||||||
},
|
},
|
||||||
getFocusSphere: () => ({ radius: 500000 }),
|
getFocusSphere: () => ({
|
||||||
|
center: { x: 6378137, y: 0, z: 0 },
|
||||||
|
radius: 500000,
|
||||||
|
}),
|
||||||
clear() {
|
clear() {
|
||||||
cleared++;
|
cleared++;
|
||||||
},
|
},
|
||||||
@@ -384,6 +389,7 @@ function harness(
|
|||||||
navigation.push({ sphere, options }),
|
navigation.push({ sphere, options }),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
bindFakeCesiumCamera(viewer);
|
||||||
layer.init(viewer);
|
layer.init(viewer);
|
||||||
layer.attachShellServices({
|
layer.attachShellServices({
|
||||||
runNavigation: (fn) => {
|
runNavigation: (fn) => {
|
||||||
|
|||||||
Vendored
+11
-10
@@ -6,6 +6,7 @@ import {
|
|||||||
ANCHOR_REFRESH_M,
|
ANCHOR_REFRESH_M,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
import { formatNumber, t, tn } from '../../i18n/index.js';
|
import { formatNumber, t, tn } from '../../i18n/index.js';
|
||||||
|
import { cameraOf } from '../../render/rendererOf.js';
|
||||||
|
|
||||||
// installationFeedback.js is shared with the HUD (src/loadingFeedback.js) and
|
// installationFeedback.js is shared with the HUD (src/loadingFeedback.js) and
|
||||||
// hands back msg()-marked English plus values; getStats translates them here.
|
// hands back msg()-marked English plus values; getStats translates them here.
|
||||||
@@ -175,16 +176,16 @@ export function createControls({ state: layerState, services, parts, source }) {
|
|||||||
// No camera flight without a real selection: a flight plus a stale subject
|
// 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.
|
// reads as success to Context navigation and strands NEXT on this item.
|
||||||
if (!parts.selection.selectRecord(record.id)) return false;
|
if (!parts.selection.selectRecord(record.id)) return false;
|
||||||
layerState.viewer.camera.flyToBoundingSphere(
|
cameraOf(layerState.viewer).flyToBounds(
|
||||||
new Cesium.BoundingSphere(
|
{
|
||||||
Cesium.Cartesian3.fromDegrees(
|
center: {
|
||||||
record.longitude,
|
lon: record.longitude,
|
||||||
record.latitude,
|
lat: record.latitude,
|
||||||
parts.rendering.installationSurfaceHeightM(record),
|
height: parts.rendering.installationSurfaceHeightM(record),
|
||||||
),
|
},
|
||||||
18000,
|
radius: 18000,
|
||||||
),
|
},
|
||||||
{ duration: 1.4 },
|
{ durationS: 1.4 },
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { t } from '../../i18n/index.js';
|
import { t } from '../../i18n/index.js';
|
||||||
|
import { cameraOf } from '../../render/rendererOf.js';
|
||||||
import {
|
import {
|
||||||
SATELLITE_STANDALONE_DEFAULTS,
|
SATELLITE_STANDALONE_DEFAULTS,
|
||||||
LAUNCH_PAD_ZONE_MAX_CAMERA_HEIGHT_M,
|
LAUNCH_PAD_ZONE_MAX_CAMERA_HEIGHT_M,
|
||||||
@@ -228,17 +229,12 @@ export function createPolicyHelpers({
|
|||||||
const range = parts.panel.missionHoverPreviewRange(
|
const range = parts.panel.missionHoverPreviewRange(
|
||||||
layerState._viewer.camera.positionCartographic?.height,
|
layerState._viewer.camera.positionCartographic?.height,
|
||||||
);
|
);
|
||||||
const position = Cesium.Cartesian3.fromDegrees(launch.lon, launch.lat);
|
cameraOf(layerState._viewer).flyToBounds(
|
||||||
layerState._viewer.camera.flyToBoundingSphere(
|
{ center: { lon: launch.lon, lat: launch.lat }, radius: 0 },
|
||||||
new Cesium.BoundingSphere(position, 0),
|
|
||||||
{
|
{
|
||||||
offset: new Cesium.HeadingPitchRange(
|
offset: { heading: 0, pitch: -Cesium.Math.PI_OVER_TWO, range },
|
||||||
0,
|
durationS: 0.8,
|
||||||
-Cesium.Math.PI_OVER_TWO,
|
easing: 'cubicInOut',
|
||||||
range,
|
|
||||||
),
|
|
||||||
duration: 0.8,
|
|
||||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
GLOBAL_RADIO_ALTITUDE_M,
|
GLOBAL_RADIO_ALTITUDE_M,
|
||||||
RADIO_GLOBE_RECENTER_MAX_HEIGHT_M,
|
RADIO_GLOBE_RECENTER_MAX_HEIGHT_M,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
|
import { cameraOf } from '../../render/rendererOf.js';
|
||||||
|
|
||||||
export function createNavigation({
|
export function createNavigation({
|
||||||
state: layerState,
|
state: layerState,
|
||||||
@@ -229,7 +230,7 @@ export function createNavigation({
|
|||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
layerState._activeRadioCameraFlight = null;
|
layerState._activeRadioCameraFlight = null;
|
||||||
layerState._viewer.camera.cancelFlight();
|
cameraOf(layerState._viewer).cancelFlight();
|
||||||
}
|
}
|
||||||
|
|
||||||
function invalidateRadioCameraNavigation() {
|
function invalidateRadioCameraNavigation() {
|
||||||
@@ -247,7 +248,7 @@ export function createNavigation({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startRadioCameraFlight(navigation, options, onComplete) {
|
function startRadioCameraFlight(navigation, flight, onComplete) {
|
||||||
if (!radioCameraNavigationIsCurrent(navigation)) return false;
|
if (!radioCameraNavigationIsCurrent(navigation)) return false;
|
||||||
const token = ++layerState._radioCameraFlightSequence;
|
const token = ++layerState._radioCameraFlightSequence;
|
||||||
layerState._activeRadioCameraFlight = {
|
layerState._activeRadioCameraFlight = {
|
||||||
@@ -260,10 +261,10 @@ export function createNavigation({
|
|||||||
if (completed && radioCameraNavigationIsCurrent(navigation))
|
if (completed && radioCameraNavigationIsCurrent(navigation))
|
||||||
onComplete?.();
|
onComplete?.();
|
||||||
};
|
};
|
||||||
layerState._viewer.camera.flyTo({
|
cameraOf(layerState._viewer).flyTo(flight.destination, {
|
||||||
...options,
|
...flight.options,
|
||||||
complete: () => finish(true),
|
onComplete: () => finish(true),
|
||||||
cancel: () => finish(false),
|
onCancel: () => finish(false),
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -280,17 +281,15 @@ export function createNavigation({
|
|||||||
return startRadioCameraFlight(
|
return startRadioCameraFlight(
|
||||||
navigation,
|
navigation,
|
||||||
{
|
{
|
||||||
destination: Cesium.Cartesian3.fromDegrees(
|
destination: { lon: plan.lon, lat: plan.lat, height: plan.height },
|
||||||
plan.lon,
|
options: {
|
||||||
plan.lat,
|
orientation: {
|
||||||
plan.height,
|
heading: plan.heading,
|
||||||
),
|
pitch: plan.pitch,
|
||||||
orientation: {
|
roll: plan.roll,
|
||||||
heading: plan.heading,
|
},
|
||||||
pitch: plan.pitch,
|
durationS: navigation.duration,
|
||||||
roll: plan.roll,
|
|
||||||
},
|
},
|
||||||
duration: navigation.duration,
|
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
navigation.phase = 'settled';
|
navigation.phase = 'settled';
|
||||||
@@ -340,13 +339,15 @@ export function createNavigation({
|
|||||||
activeNavigation,
|
activeNavigation,
|
||||||
{
|
{
|
||||||
destination: activeNavigation.recenterPlan.destination,
|
destination: activeNavigation.recenterPlan.destination,
|
||||||
orientation: {
|
options: {
|
||||||
heading: activeNavigation.cameraState.heading,
|
orientation: {
|
||||||
pitch: activeNavigation.cameraState.pitch,
|
heading: activeNavigation.cameraState.heading,
|
||||||
roll: activeNavigation.cameraState.roll,
|
pitch: activeNavigation.cameraState.pitch,
|
||||||
|
roll: activeNavigation.cameraState.roll,
|
||||||
|
},
|
||||||
|
durationS: recenterDuration,
|
||||||
|
easing: 'cubicInOut',
|
||||||
},
|
},
|
||||||
duration: recenterDuration,
|
|
||||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
activeNavigation.recentered = true;
|
activeNavigation.recentered = true;
|
||||||
@@ -363,14 +364,10 @@ export function createNavigation({
|
|||||||
if (!station || !radioCameraNavigationAllowed(layerState._viewer))
|
if (!station || !radioCameraNavigationAllowed(layerState._viewer))
|
||||||
return false;
|
return false;
|
||||||
cancelActiveRadioCameraFlight();
|
cancelActiveRadioCameraFlight();
|
||||||
layerState._viewer.camera.flyTo({
|
cameraOf(layerState._viewer).flyTo(
|
||||||
destination: Cesium.Cartesian3.fromDegrees(
|
{ lon: station.lon, lat: station.lat, height: 85_000 },
|
||||||
station.lon,
|
{ durationS: 1.1 },
|
||||||
station.lat,
|
);
|
||||||
85_000,
|
|
||||||
),
|
|
||||||
duration: 1.1,
|
|
||||||
});
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
} from './model.js';
|
} from './model.js';
|
||||||
import { msg, t } from '../../i18n/index.js';
|
import { msg, t } from '../../i18n/index.js';
|
||||||
import { READOUT_WORDS, displayDay, displayMetres } from './display.js';
|
import { READOUT_WORDS, displayDay, displayMetres } from './display.js';
|
||||||
|
import { cameraOf } from '../../render/rendererOf.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Quiet time after the last arrow key before the focused day previews, so a
|
* Quiet time after the last arrow key before the focused day previews, so a
|
||||||
@@ -987,15 +988,17 @@ export function createRecentImageryLayer({
|
|||||||
height: canvas?.clientHeight,
|
height: canvas?.clientHeight,
|
||||||
fovy: camera.frustum?.fovy,
|
fovy: camera.frustum?.fovy,
|
||||||
});
|
});
|
||||||
camera.flyTo({
|
cameraOf(_viewer).flyTo(
|
||||||
destination: ellipsoid.cartographicToCartesian({
|
ellipsoid.cartographicToCartesian({
|
||||||
longitude: toRadians(target.lon),
|
longitude: toRadians(target.lon),
|
||||||
latitude: toRadians(target.lat),
|
latitude: toRadians(target.lat),
|
||||||
height,
|
height,
|
||||||
}),
|
}),
|
||||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
{
|
||||||
duration: ZOOM_FIT_SECONDS,
|
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||||
});
|
durationS: ZOOM_FIT_SECONDS,
|
||||||
|
},
|
||||||
|
);
|
||||||
return height;
|
return height;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
import * as Cesium from 'cesium';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { NO_IMAGERY_HOST } from '../../maps/imageryHost.js';
|
import { NO_IMAGERY_HOST } from '../../maps/imageryHost.js';
|
||||||
import { acquireImageryComparison } from '../../maps/imageryComparison.js';
|
import { acquireImageryComparison } from '../../maps/imageryComparison.js';
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
manualTimers,
|
manualTimers,
|
||||||
settle,
|
settle,
|
||||||
} from './testDoubles.mjs';
|
} from './testDoubles.mjs';
|
||||||
|
import { bindFakeCesiumCamera } from '../../testSupport/fakeCameraPort.mjs';
|
||||||
|
|
||||||
const NOW = new Date('2026-09-21T15:00:00Z');
|
const NOW = new Date('2026-09-21T15:00:00Z');
|
||||||
const LINK_BOX = {
|
const LINK_BOX = {
|
||||||
@@ -133,6 +135,7 @@ function fixture({
|
|||||||
});
|
});
|
||||||
if (viirs) layer.setSources({ viirs: true });
|
if (viirs) layer.setSources({ viirs: true });
|
||||||
const viewer = { camera: {} };
|
const viewer = { camera: {} };
|
||||||
|
bindFakeCesiumCamera(viewer);
|
||||||
const reasons = [];
|
const reasons = [];
|
||||||
layer.subscribe((snapshot, reason) => reasons.push(reason));
|
layer.subscribe((snapshot, reason) => reasons.push(reason));
|
||||||
layer.init(viewer);
|
layer.init(viewer);
|
||||||
@@ -966,6 +969,8 @@ test('USE VIEW takes the camera rectangle and refuses a view over the cap instea
|
|||||||
test('ZOOM IN: an oversized box or view flies top-down to its centre at the height whose view is 400 km wide', () => {
|
test('ZOOM IN: an oversized box or view flies top-down to its centre at the height whose view is 400 km wide', () => {
|
||||||
const f = fixture();
|
const f = fixture();
|
||||||
const flights = [];
|
const flights = [];
|
||||||
|
const landing = (i) =>
|
||||||
|
Cesium.Ellipsoid.WGS84.cartesianToCartographic(flights[i].destination);
|
||||||
const deg = (rad) => (rad * 180) / Math.PI;
|
const deg = (rad) => (rad * 180) / Math.PI;
|
||||||
const rad = (value) => (value * Math.PI) / 180;
|
const rad = (value) => (value * Math.PI) / 180;
|
||||||
// A 1000 × 500 canvas with a 60° horizontal FOV.
|
// A 1000 × 500 canvas with a 60° horizontal FOV.
|
||||||
@@ -976,7 +981,8 @@ test('ZOOM IN: an oversized box or view flies top-down to its centre at the heig
|
|||||||
scene: {
|
scene: {
|
||||||
canvas: { clientWidth: 1000, clientHeight: 500 },
|
canvas: { clientWidth: 1000, clientHeight: 500 },
|
||||||
ellipsoid: {
|
ellipsoid: {
|
||||||
cartographicToCartesian: (c) => ({ ...c, cartesian: true }),
|
cartographicToCartesian: (c) =>
|
||||||
|
Cesium.Ellipsoid.WGS84.cartographicToCartesian(c),
|
||||||
cartesianToCartographic: (c) => c && { ...c },
|
cartesianToCartographic: (c) => c && { ...c },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -999,9 +1005,9 @@ test('ZOOM IN: an oversized box or view flies top-down to its centre at the heig
|
|||||||
assert.ok(Math.abs(height - expected) < 1e-6, String(height));
|
assert.ok(Math.abs(height - expected) < 1e-6, String(height));
|
||||||
assert.equal(flights.length, 1);
|
assert.equal(flights.length, 1);
|
||||||
const [flight] = flights;
|
const [flight] = flights;
|
||||||
assert.ok(Math.abs(deg(flight.destination.longitude) - 10) < 1e-9);
|
assert.ok(Math.abs(deg(landing(0).longitude) - 10) < 1e-9);
|
||||||
assert.ok(Math.abs(deg(flight.destination.latitude) - 10) < 1e-9);
|
assert.ok(Math.abs(deg(landing(0).latitude) - 10) < 1e-9);
|
||||||
assert.equal(flight.destination.height, height);
|
assert.ok(Math.abs(landing(0).height - height) < 1e-6);
|
||||||
assert.deepEqual(flight.orientation, {
|
assert.deepEqual(flight.orientation, {
|
||||||
heading: 0,
|
heading: 0,
|
||||||
pitch: -Math.PI / 2,
|
pitch: -Math.PI / 2,
|
||||||
@@ -1019,14 +1025,14 @@ test('ZOOM IN: an oversized box or view flies top-down to its centre at the heig
|
|||||||
assert.equal(f.layer.useCurrentView(), false);
|
assert.equal(f.layer.useCurrentView(), false);
|
||||||
assert.equal(f.snap().zoomToFit, true);
|
assert.equal(f.snap().zoomToFit, true);
|
||||||
f.layer.zoomToFit();
|
f.layer.zoomToFit();
|
||||||
assert.ok(Math.abs(deg(flights[1].destination.longitude) + 97) < 1e-9);
|
assert.ok(Math.abs(deg(landing(1).longitude) + 97) < 1e-9);
|
||||||
assert.ok(Math.abs(deg(flights[1].destination.latitude) - 31) < 1e-9);
|
assert.ok(Math.abs(deg(landing(1).latitude) - 31) < 1e-9);
|
||||||
// Sky at the centre: the view rectangle's centre instead.
|
// Sky at the centre: the view rectangle's centre instead.
|
||||||
centreHit = null;
|
centreHit = null;
|
||||||
f.layer.useCurrentView();
|
f.layer.useCurrentView();
|
||||||
f.layer.zoomToFit();
|
f.layer.zoomToFit();
|
||||||
assert.ok(Math.abs(deg(flights[2].destination.longitude) + 98) < 1e-9);
|
assert.ok(Math.abs(deg(landing(2).longitude) + 98) < 1e-9);
|
||||||
assert.ok(Math.abs(deg(flights[2].destination.latitude) - 30) < 1e-9);
|
assert.ok(Math.abs(deg(landing(2).latitude) - 30) < 1e-9);
|
||||||
// The box tool's own oversized drag carries its box.
|
// The box tool's own oversized drag carries its box.
|
||||||
f.layer.reportBoxRefusal('Box is 2,226 km wide · limit 1,000 km', {
|
f.layer.reportBoxRefusal('Box is 2,226 km wide · limit 1,000 km', {
|
||||||
west: 0,
|
west: 0,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { plausibleSurfaceHeight, sampledSurfaceHeight } from './view.js';
|
import { plausibleSurfaceHeight, sampledSurfaceHeight } from './view.js';
|
||||||
import { heightsFor } from '../../queries/heights.js';
|
import { heightsFor } from '../../queries/heights.js';
|
||||||
|
import { cameraOf } from '../../render/rendererOf.js';
|
||||||
|
|
||||||
/** Fly the globe camera once to frame a photo after it opens. */
|
/** Fly the globe camera once to frame a photo after it opens. */
|
||||||
export function createCameraFraming({ state }) {
|
export function createCameraFraming({ state }) {
|
||||||
@@ -49,36 +50,37 @@ export function createCameraFraming({ state }) {
|
|||||||
return;
|
return;
|
||||||
const ground = groundHeightAt(position.lon, position.lat, altitude);
|
const ground = groundHeightAt(position.lon, position.lat, altitude);
|
||||||
// Owned before the call: starting it cancels the previous flight (ours
|
// Owned before the call: starting it cancels the previous flight (ours
|
||||||
// included) synchronously, and a zero-length one completes at once.
|
// included), and a zero-length one completes inside the call.
|
||||||
const current = {};
|
const current = {};
|
||||||
flight = current;
|
flight = current;
|
||||||
const release = () => {
|
const release = () => {
|
||||||
if (flight === current) flight = null;
|
if (flight === current) flight = null;
|
||||||
};
|
};
|
||||||
state.viewer.camera.flyToBoundingSphere(
|
const started = cameraOf(state.viewer).flyToBounds(
|
||||||
new Cesium.BoundingSphere(
|
|
||||||
Cesium.Cartesian3.fromDegrees(position.lon, position.lat, ground + 2),
|
|
||||||
4,
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
offset: new Cesium.HeadingPitchRange(
|
center: { lon: position.lon, lat: position.lat, height: ground + 2 },
|
||||||
Cesium.Math.toRadians(bearing || 0),
|
radius: 4,
|
||||||
Cesium.Math.toRadians(-32),
|
},
|
||||||
140,
|
{
|
||||||
),
|
offset: {
|
||||||
duration: 1.6,
|
heading: Cesium.Math.toRadians(bearing || 0),
|
||||||
complete: release,
|
pitch: Cesium.Math.toRadians(-32),
|
||||||
cancel: release,
|
range: 140,
|
||||||
|
},
|
||||||
|
durationS: 1.6,
|
||||||
|
onComplete: release,
|
||||||
|
onCancel: release,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
if (flight === current) current.handle = started;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Stop our framing flight if it is still in the air; another feature's flight is left alone. */
|
/** Stop our framing flight if it is still in the air; another feature's flight is left alone. */
|
||||||
function cancel() {
|
function cancel() {
|
||||||
if (!flight) return;
|
if (!flight) return;
|
||||||
// Give up ownership first: cancelFlight delivers `cancel` synchronously.
|
const { handle } = flight;
|
||||||
flight = null;
|
flight = null;
|
||||||
state.viewer?.camera?.cancelFlight?.();
|
handle?.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
return { attachNavigation, begin, frame, cancel };
|
return { attachNavigation, begin, frame, cancel };
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
|
import { bindFakeCesiumCamera } from '../../testSupport/fakeCameraPort.mjs';
|
||||||
import { createStreetLevelLayer } from './index.js';
|
import { createStreetLevelLayer } from './index.js';
|
||||||
import { MAPILLARY_CREDIT_HTML } from './providers/mapillary/policy.js';
|
import { MAPILLARY_CREDIT_HTML } from './providers/mapillary/policy.js';
|
||||||
import { createMapillaryProvider } from './providers/mapillary/index.js';
|
import { createMapillaryProvider } from './providers/mapillary/index.js';
|
||||||
@@ -326,7 +327,7 @@ function fakeViewer() {
|
|||||||
disableRootEvents: true,
|
disableRootEvents: true,
|
||||||
onwheel: null,
|
onwheel: null,
|
||||||
});
|
});
|
||||||
return {
|
return bindFakeCesiumCamera({
|
||||||
credits,
|
credits,
|
||||||
flights,
|
flights,
|
||||||
scene: {
|
scene: {
|
||||||
@@ -358,7 +359,7 @@ function fakeViewer() {
|
|||||||
if (at !== -1) credits.splice(at, 1);
|
if (at !== -1) credits.splice(at, 1);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A layer over `providers`, initialised and enabled on a stand-in viewer. */
|
/** A layer over `providers`, initialised and enabled on a stand-in viewer. */
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { cablePicks } from './pickState.js';
|
import { cablePicks } from './pickState.js';
|
||||||
|
import { cameraOf } from '../../render/rendererOf.js';
|
||||||
|
|
||||||
export function createInteraction({ state, inputFor }) {
|
export function createInteraction({ state, inputFor }) {
|
||||||
function registerPickEntity(entity, info) {
|
function registerPickEntity(entity, info) {
|
||||||
@@ -45,22 +46,20 @@ export function createInteraction({ state, inputFor }) {
|
|||||||
|
|
||||||
function flyToReference(viewer, reference) {
|
function flyToReference(viewer, reference) {
|
||||||
if (!viewer || !reference) return;
|
if (!viewer || !reference) return;
|
||||||
const destination = Cesium.Cartesian3.fromDegrees(
|
const camera = cameraOf(viewer);
|
||||||
reference.lon,
|
camera.cancelFlight();
|
||||||
reference.lat,
|
camera.flyTo(
|
||||||
6500,
|
{ lon: reference.lon, lat: reference.lat, height: 6500 },
|
||||||
);
|
{
|
||||||
viewer.camera.cancelFlight();
|
orientation: {
|
||||||
viewer.camera.flyTo({
|
heading: camera.pose().heading || 0,
|
||||||
destination,
|
pitch: Cesium.Math.toRadians(-52),
|
||||||
orientation: {
|
roll: 0,
|
||||||
heading: viewer.camera.heading || 0,
|
},
|
||||||
pitch: Cesium.Math.toRadians(-52),
|
durationS: 1.35,
|
||||||
roll: 0,
|
easing: 'cubicInOut',
|
||||||
},
|
},
|
||||||
duration: 1.35,
|
);
|
||||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
registerPickEntity,
|
registerPickEntity,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { createWeatherRendering } from './rendering.js';
|
|||||||
import { weatherDisplay, insideExtent, escapeCreditText } from './display.js';
|
import { weatherDisplay, insideExtent, escapeCreditText } from './display.js';
|
||||||
import { createShellCesium, createShellScene } from './shellFixture.mjs';
|
import { createShellCesium, createShellScene } from './shellFixture.mjs';
|
||||||
import { formatScaleLabel } from '../../ui/unitLabels.js';
|
import { formatScaleLabel } from '../../ui/unitLabels.js';
|
||||||
|
import { bindFakeCesiumCamera } from '../../testSupport/fakeCameraPort.mjs';
|
||||||
|
|
||||||
const times = ['2026-09-15T20:00:00.000Z', '2026-09-15T20:05:00.000Z'];
|
const times = ['2026-09-15T20:00:00.000Z', '2026-09-15T20:05:00.000Z'];
|
||||||
const base = {
|
const base = {
|
||||||
@@ -58,18 +59,22 @@ async function shownLayer(id, manifest, params, view = {}) {
|
|||||||
getDiagnostics: () => ({ time, loading: false, error: null }),
|
getDiagnostics: () => ({ time, loading: false, error: null }),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
layer.init({
|
layer.init(
|
||||||
camera: {
|
bindFakeCesiumCamera({
|
||||||
moveEnd: { addEventListener: () => () => {} },
|
camera: {
|
||||||
flyTo: ({ destination }) => flights.push(destination),
|
moveEnd: { addEventListener: () => () => {} },
|
||||||
pickEllipsoid: () =>
|
flyTo: ({ destination }) => flights.push(destination),
|
||||||
view.center ? Cesium.Cartesian3.fromDegrees(...view.center) : undefined,
|
pickEllipsoid: () =>
|
||||||
},
|
view.center
|
||||||
scene: {
|
? Cesium.Cartesian3.fromDegrees(...view.center)
|
||||||
canvas: { clientWidth: 800, clientHeight: 600 },
|
: undefined,
|
||||||
globe: { ellipsoid: Cesium.Ellipsoid.WGS84 },
|
},
|
||||||
},
|
scene: {
|
||||||
});
|
canvas: { clientWidth: 800, clientHeight: 600 },
|
||||||
|
globe: { ellipsoid: Cesium.Ellipsoid.WGS84 },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
if (params) layer.setParams(params);
|
if (params) layer.setParams(params);
|
||||||
layer.enable();
|
layer.enable();
|
||||||
await layer.update();
|
await layer.update();
|
||||||
@@ -638,6 +643,7 @@ function globeViewer() {
|
|||||||
requestRender() {},
|
requestRender() {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
bindFakeCesiumCamera(viewer);
|
||||||
class Provider {
|
class Provider {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
|||||||
+14
-11
@@ -10,6 +10,7 @@ import {
|
|||||||
} from './clock.js';
|
} from './clock.js';
|
||||||
import { formatUtcDayMonth, t, msg } from '../../i18n/index.js';
|
import { formatUtcDayMonth, t, msg } from '../../i18n/index.js';
|
||||||
import { formatScaleLabel } from '../../ui/unitLabels.js';
|
import { formatScaleLabel } from '../../ui/unitLabels.js';
|
||||||
|
import { cameraOf } from '../../render/rendererOf.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A stored (English) failure in the language. HTTP failures from source.js
|
* A stored (English) failure in the language. HTTP failures from source.js
|
||||||
@@ -475,17 +476,19 @@ export function createWeatherLayer({
|
|||||||
(lightning
|
(lightning
|
||||||
? { west: 110, south: -25, east: 0, north: 80 }
|
? { west: 110, south: -25, east: 0, north: 80 }
|
||||||
: manifest.bounds);
|
: manifest.bounds);
|
||||||
runNavigation(() =>
|
runNavigation(() => {
|
||||||
viewer.camera.flyTo({
|
cameraOf(viewer).flyTo(
|
||||||
destination: cesium.Rectangle.fromDegrees(
|
{
|
||||||
b.west,
|
rect: {
|
||||||
b.south,
|
west: b.west,
|
||||||
b.east,
|
south: b.south,
|
||||||
b.north,
|
east: b.east,
|
||||||
),
|
north: b.north,
|
||||||
duration: motion?.matches ? 0 : 1.4,
|
},
|
||||||
}),
|
},
|
||||||
);
|
{ durationS: motion?.matches ? 0 : 1.4 },
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (clock && enabled) {
|
if (clock && enabled) {
|
||||||
if (params.play === true) void clock.togglePlay();
|
if (params.play === true) void clock.togglePlay();
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ export function createCesiumCamera({ scene, camera, report }) {
|
|||||||
o.range ?? 0,
|
o.range ?? 0,
|
||||||
),
|
),
|
||||||
duration: options.durationS,
|
duration: options.durationS,
|
||||||
|
easingFunction: options.easing && EASINGS[options.easing],
|
||||||
...callbacks,
|
...callbacks,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ function cesiumDriver(clock, threshold) {
|
|||||||
)
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
duration: options.durationS,
|
duration: options.durationS,
|
||||||
|
easingFunction: EASINGS[options.easing],
|
||||||
complete,
|
complete,
|
||||||
cancel,
|
cancel,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -230,6 +230,5 @@
|
|||||||
"src/ui/shareFollow.js",
|
"src/ui/shareFollow.js",
|
||||||
"src/ui/visualEffects.js",
|
"src/ui/visualEffects.js",
|
||||||
"src/voice/gevActions.js",
|
"src/voice/gevActions.js",
|
||||||
"src/voice/pointerPick.js",
|
"src/voice/pointerPick.js"
|
||||||
"src/worldFocus.js"
|
|
||||||
]
|
]
|
||||||
@@ -371,6 +371,27 @@ export const CAMERA_SCRIPTS = Object.freeze([
|
|||||||
{ record: 'pose' },
|
{ record: 'pose' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'flyToBounds easing',
|
||||||
|
steps: [
|
||||||
|
START,
|
||||||
|
{
|
||||||
|
do: 'flyToBounds',
|
||||||
|
args: [
|
||||||
|
{ center: { ...AUSTIN, height: 0 }, radius: 500 },
|
||||||
|
{
|
||||||
|
offset: { heading: 0.5, pitch: -0.4, range: 2500 },
|
||||||
|
durationS: 1,
|
||||||
|
easing: 'cubicInOut',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ do: 'frames', count: 20 },
|
||||||
|
{ record: 'pose' },
|
||||||
|
{ do: 'frames', count: 70 },
|
||||||
|
{ record: 'pose' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'a cancelled flight stops where it was',
|
name: 'a cancelled flight stops where it was',
|
||||||
steps: [
|
steps: [
|
||||||
|
|||||||
@@ -2071,6 +2071,79 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"flyToBounds easing": {
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"step": 3,
|
||||||
|
"kind": "pose",
|
||||||
|
"value": {
|
||||||
|
"position": {
|
||||||
|
"x": -2267093.6902229865,
|
||||||
|
"y": -16670141.730590489,
|
||||||
|
"z": 9795550.536889307
|
||||||
|
},
|
||||||
|
"direction": {
|
||||||
|
"x": 0.13436019637409768,
|
||||||
|
"y": 0.918216318410333,
|
||||||
|
"z": -0.37259378716678554
|
||||||
|
},
|
||||||
|
"up": {
|
||||||
|
"x": 0.11222044188344235,
|
||||||
|
"y": 0.3594849211517312,
|
||||||
|
"z": 0.9263785208477249
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"x": 0.9845577230684848,
|
||||||
|
"y": -0.1662810394167284,
|
||||||
|
"z": -0.05474217639712155
|
||||||
|
},
|
||||||
|
"heading": 0.06349647744048603,
|
||||||
|
"pitch": -1.4221294499846318,
|
||||||
|
"roll": 6.283185231543937,
|
||||||
|
"height": 13094845.933521682,
|
||||||
|
"frameIsIdentity": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 5,
|
||||||
|
"kind": "pose",
|
||||||
|
"value": {
|
||||||
|
"position": {
|
||||||
|
"x": -744171.8196286416,
|
||||||
|
"y": -5464810.027402491,
|
||||||
|
"z": 3194736.500278188
|
||||||
|
},
|
||||||
|
"direction": {
|
||||||
|
"x": 0.5377602708229207,
|
||||||
|
"y": 0.6774719395626166,
|
||||||
|
"z": 0.5018422682773198
|
||||||
|
},
|
||||||
|
"up": {
|
||||||
|
"x": 0.1010227917440539,
|
||||||
|
"y": -0.6427253771880552,
|
||||||
|
"z": 0.7594066664618568
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"x": 0.8370234684121447,
|
||||||
|
"y": -0.35768122766471616,
|
||||||
|
"z": -0.41407227956428677
|
||||||
|
},
|
||||||
|
"heading": 0.4998993704309962,
|
||||||
|
"pitch": -0.4003616646350623,
|
||||||
|
"roll": 6.283184711710125,
|
||||||
|
"height": 973.9627016434621,
|
||||||
|
"frameIsIdentity": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"flights": [
|
||||||
|
{
|
||||||
|
"step": 1,
|
||||||
|
"outcome": "complete",
|
||||||
|
"frames": 62
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"a cancelled flight stops where it was": {
|
"a cancelled flight stops where it was": {
|
||||||
"records": [
|
"records": [
|
||||||
{
|
{
|
||||||
|
|||||||
Vendored
+2
-2
@@ -210,9 +210,9 @@ export interface CameraPort {
|
|||||||
*/
|
*/
|
||||||
flyToBounds(
|
flyToBounds(
|
||||||
sphere: Sphere,
|
sphere: Sphere,
|
||||||
options?: { offset?: HeadingPitchRange; durationS?: number } & Pick<
|
options?: { offset?: HeadingPitchRange } & Pick<
|
||||||
FlightOptions,
|
FlightOptions,
|
||||||
'onComplete' | 'onCancel'
|
'durationS' | 'easing' | 'onComplete' | 'onCancel'
|
||||||
>,
|
>,
|
||||||
): Flight;
|
): Flight;
|
||||||
cancelFlight(): void;
|
cancelFlight(): void;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/** Shared Street Level test stand-ins: provider snapshot and provider, Mapillary source, photo viewer, Cesium viewer, ray-casting camera. */
|
/** Shared Street Level test stand-ins: provider snapshot and provider, Mapillary source, photo viewer, Cesium viewer, ray-casting camera. */
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
|
import { bindFakeCesiumCamera } from './fakeCameraPort.mjs';
|
||||||
import { MAPILLARY_CREDIT_HTML } from '../layers/streetLevel/providers/mapillary/policy.js';
|
import { MAPILLARY_CREDIT_HTML } from '../layers/streetLevel/providers/mapillary/policy.js';
|
||||||
import {
|
import {
|
||||||
latToTileY,
|
latToTileY,
|
||||||
@@ -190,7 +191,7 @@ export function fakeCesiumViewer({ view = false } = {}) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return {
|
return bindFakeCesiumCamera({
|
||||||
credits,
|
credits,
|
||||||
flights,
|
flights,
|
||||||
scene: {
|
scene: {
|
||||||
@@ -211,7 +212,7 @@ export function fakeCesiumViewer({ view = false } = {}) {
|
|||||||
if (at !== -1) credits.splice(at, 1);
|
if (at !== -1) credits.splice(at, 1);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as Cesium from 'cesium';
|
||||||
import '../testSupport/registerDom.mjs';
|
import '../testSupport/registerDom.mjs';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
response,
|
response,
|
||||||
settle,
|
settle,
|
||||||
} from '../layers/recentImagery/testDoubles.mjs';
|
} from '../layers/recentImagery/testDoubles.mjs';
|
||||||
|
import { bindFakeCesiumCamera } from '../testSupport/fakeCameraPort.mjs';
|
||||||
|
|
||||||
const S18 = 'S30:2026-09-18';
|
const S18 = 'S30:2026-09-18';
|
||||||
const L16 = 'L30:2026-09-16';
|
const L16 = 'L30:2026-09-16';
|
||||||
@@ -128,6 +130,7 @@ function fixture({ fetchImpl, controller = fakeController() } = {}) {
|
|||||||
camera: {},
|
camera: {},
|
||||||
scene: { canvas: { clientWidth: 1000 }, requestRender() {} },
|
scene: { canvas: { clientWidth: 1000 }, requestRender() {} },
|
||||||
};
|
};
|
||||||
|
bindFakeCesiumCamera(viewer);
|
||||||
layer.init(viewer);
|
layer.init(viewer);
|
||||||
layer.attachMapStackController(controller);
|
layer.attachMapStackController(controller);
|
||||||
let active = false;
|
let active = false;
|
||||||
@@ -801,9 +804,7 @@ test('an oversized box offers ZOOM IN in a fixed slot at the end of the notice l
|
|||||||
const f = fixture();
|
const f = fixture();
|
||||||
const flights = [];
|
const flights = [];
|
||||||
f.viewer.scene.canvas.clientHeight = 500;
|
f.viewer.scene.canvas.clientHeight = 500;
|
||||||
f.viewer.scene.ellipsoid = {
|
f.viewer.scene.ellipsoid = Cesium.Ellipsoid.WGS84;
|
||||||
cartographicToCartesian: (c) => ({ ...c }),
|
|
||||||
};
|
|
||||||
Object.assign(f.viewer.camera, {
|
Object.assign(f.viewer.camera, {
|
||||||
frustum: { fovy: 2 * Math.atan(Math.tan(Math.PI / 6) / 2) },
|
frustum: { fovy: 2 * Math.atan(Math.tan(Math.PI / 6) / 2) },
|
||||||
flyTo: (options) => flights.push(options),
|
flyTo: (options) => flights.push(options),
|
||||||
@@ -835,7 +836,9 @@ test('an oversized box offers ZOOM IN in a fixed slot at the end of the notice l
|
|||||||
assert.equal(flights.length, 1);
|
assert.equal(flights.length, 1);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
Math.abs(
|
Math.abs(
|
||||||
flights[0].destination.height - 400_000 / (2 * Math.tan(Math.PI / 6)),
|
Cesium.Ellipsoid.WGS84.cartesianToCartographic(flights[0].destination)
|
||||||
|
.height -
|
||||||
|
400_000 / (2 * Math.tan(Math.PI / 6)),
|
||||||
) < 1e-6,
|
) < 1e-6,
|
||||||
);
|
);
|
||||||
// Another refusal has nothing to fit; a box that succeeds hides it again.
|
// Another refusal has nothing to fit; a box that succeeds hides it again.
|
||||||
|
|||||||
+16
-17
@@ -1,7 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* One-click camera transfer for layer-owned world targets.
|
* One-click camera transfer for layer-owned world targets.
|
||||||
*/
|
*/
|
||||||
import * as Cesium from 'cesium';
|
import { WGS84, toRadians } from './geo/index.js';
|
||||||
|
import { rendererOf } from './render/rendererOf.js';
|
||||||
|
|
||||||
export const WORLD_FOCUS_REQUEST_EVENT = 'gev:world-request-focus';
|
export const WORLD_FOCUS_REQUEST_EVENT = 'gev:world-request-focus';
|
||||||
export const WORLD_CLICK_FOCUS_DURATION_SEC = 1.9;
|
export const WORLD_CLICK_FOCUS_DURATION_SEC = 1.9;
|
||||||
@@ -26,11 +27,8 @@ export function isValidWorldFocusTarget(detail) {
|
|||||||
// Vessel and fire targets are surface-anchored Earth positions. Merely
|
// Vessel and fire targets are surface-anchored Earth positions. Merely
|
||||||
// finite coordinates near the ECEF origin cannot be flown to, and must be
|
// finite coordinates near the ECEF origin cannot be flown to, and must be
|
||||||
// rejected before the camera policy releases a current follow owner.
|
// rejected before the camera policy releases a current follow owner.
|
||||||
const magnitude = Cesium.Cartesian3.magnitude(position);
|
const magnitude = Math.hypot(position.x, position.y, position.z);
|
||||||
return (
|
return Number.isFinite(magnitude) && magnitude >= WGS84.radii.z * 0.95;
|
||||||
Number.isFinite(magnitude) &&
|
|
||||||
magnitude >= Cesium.Ellipsoid.WGS84.minimumRadius * 0.95
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Announce a valid user-click focus request. */
|
/** Announce a valid user-click focus request. */
|
||||||
@@ -71,25 +69,26 @@ export function routeWorldFocusRequest(event, runExplicitFocus, fly) {
|
|||||||
|
|
||||||
/** Fly to a world target after ownership has been released. */
|
/** Fly to a world target after ownership has been released. */
|
||||||
export function flyToWorldTarget(viewer, target = {}) {
|
export function flyToWorldTarget(viewer, target = {}) {
|
||||||
const camera = viewer?.camera;
|
const camera = rendererOf(viewer)?.camera;
|
||||||
const framing = WORLD_FOCUS_FRAMING[target.kind];
|
const framing = WORLD_FOCUS_FRAMING[target.kind];
|
||||||
if (!camera || !framing || !isValidWorldFocusTarget(target)) return false;
|
if (!camera || !framing || !isValidWorldFocusTarget(target)) return false;
|
||||||
const heading = Number.isFinite(camera.heading) ? camera.heading : 0;
|
const poseHeading = camera.pose().heading;
|
||||||
|
const heading = Number.isFinite(poseHeading) ? poseHeading : 0;
|
||||||
const duration =
|
const duration =
|
||||||
target.durationSec > 0
|
target.durationSec > 0
|
||||||
? target.durationSec
|
? target.durationSec
|
||||||
: WORLD_CLICK_FOCUS_DURATION_SEC;
|
: WORLD_CLICK_FOCUS_DURATION_SEC;
|
||||||
camera.cancelFlight?.();
|
camera.cancelFlight();
|
||||||
camera.flyToBoundingSphere(
|
camera.flyToBounds(
|
||||||
new Cesium.BoundingSphere(target.position, framing.radiusM),
|
{ center: target.position, radius: framing.radiusM },
|
||||||
{
|
{
|
||||||
offset: new Cesium.HeadingPitchRange(
|
offset: {
|
||||||
heading,
|
heading,
|
||||||
Cesium.Math.toRadians(framing.pitchDeg),
|
pitch: toRadians(framing.pitchDeg),
|
||||||
framing.rangeM,
|
range: framing.rangeM,
|
||||||
),
|
},
|
||||||
duration,
|
durationS: duration,
|
||||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
easing: 'cubicInOut',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
+124
-30
@@ -3,6 +3,7 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
|
import { bindFakeCesiumCamera } from './testSupport/fakeCameraPort.mjs';
|
||||||
import {
|
import {
|
||||||
WORLD_CLICK_FOCUS_DURATION_SEC,
|
WORLD_CLICK_FOCUS_DURATION_SEC,
|
||||||
WORLD_FOCUS_FRAMING,
|
WORLD_FOCUS_FRAMING,
|
||||||
@@ -21,16 +22,28 @@ function stubCamera() {
|
|||||||
return {
|
return {
|
||||||
calls,
|
calls,
|
||||||
heading: Cesium.Math.toRadians(45),
|
heading: Cesium.Math.toRadians(45),
|
||||||
cancelFlight() { calls.cancelFlight += 1; },
|
cancelFlight() {
|
||||||
flyToBoundingSphere(sphere, options) { calls.flights.push({ sphere, options }); },
|
calls.cancelFlight += 1;
|
||||||
|
},
|
||||||
|
flyToBoundingSphere(sphere, options) {
|
||||||
|
calls.flights.push({ sphere, options });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test('a focus request carries the clicked target to any registered listener', () => {
|
test('a focus request carries the clicked target to any registered listener', () => {
|
||||||
const target = new EventTarget();
|
const target = new EventTarget();
|
||||||
const seen = [];
|
const seen = [];
|
||||||
const dispose = registerWorldFocusRequestListener(target, (event) => seen.push(event.detail));
|
const dispose = registerWorldFocusRequestListener(target, (event) =>
|
||||||
assert.equal(requestWorldFocus({ kind: 'vessel', position: POSITION, id: '123' }, target), true);
|
seen.push(event.detail),
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
requestWorldFocus(
|
||||||
|
{ kind: 'vessel', position: POSITION, id: '123' },
|
||||||
|
target,
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
assert.equal(seen.length, 1);
|
assert.equal(seen.length, 1);
|
||||||
assert.equal(seen[0].kind, 'vessel');
|
assert.equal(seen[0].kind, 'vessel');
|
||||||
assert.equal(seen[0].id, '123');
|
assert.equal(seen[0].id, '123');
|
||||||
@@ -44,7 +57,9 @@ test('a focus request carries the clicked target to any registered listener', ()
|
|||||||
test('requests without a kind or a position are dropped, never dispatched', () => {
|
test('requests without a kind or a position are dropped, never dispatched', () => {
|
||||||
const target = new EventTarget();
|
const target = new EventTarget();
|
||||||
let count = 0;
|
let count = 0;
|
||||||
target.addEventListener(WORLD_FOCUS_REQUEST_EVENT, () => { count += 1; });
|
target.addEventListener(WORLD_FOCUS_REQUEST_EVENT, () => {
|
||||||
|
count += 1;
|
||||||
|
});
|
||||||
assert.equal(requestWorldFocus({ position: POSITION }, target), false);
|
assert.equal(requestWorldFocus({ position: POSITION }, target), false);
|
||||||
assert.equal(requestWorldFocus({ kind: 'vessel' }, target), false);
|
assert.equal(requestWorldFocus({ kind: 'vessel' }, target), false);
|
||||||
assert.equal(requestWorldFocus(null, target), false);
|
assert.equal(requestWorldFocus(null, target), false);
|
||||||
@@ -74,51 +89,95 @@ test('a refusing policy (cockpit owns the camera) never reaches the flight', ()
|
|||||||
const result = routeWorldFocusRequest(
|
const result = routeWorldFocusRequest(
|
||||||
{ detail: { kind: 'vessel', id: '123', position: POSITION } },
|
{ detail: { kind: 'vessel', id: '123', position: POSITION } },
|
||||||
() => false,
|
() => false,
|
||||||
() => { flights += 1; },
|
() => {
|
||||||
|
flights += 1;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
assert.equal(result, false);
|
assert.equal(result, false);
|
||||||
assert.equal(flights, 0);
|
assert.equal(flights, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('malformed requests are inert', () => {
|
test('malformed requests are inert', () => {
|
||||||
assert.equal(routeWorldFocusRequest(null, () => 'x', () => 'y'), false);
|
assert.equal(
|
||||||
assert.equal(routeWorldFocusRequest({ detail: { kind: 'vessel' } }, () => 'x', () => 'y'), false);
|
routeWorldFocusRequest(
|
||||||
|
null,
|
||||||
|
() => 'x',
|
||||||
|
() => 'y',
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
routeWorldFocusRequest(
|
||||||
|
{ detail: { kind: 'vessel' } },
|
||||||
|
() => 'x',
|
||||||
|
() => 'y',
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// A request that cannot fly must never cost the user their tracking: the UI
|
// A request that cannot fly must never cost the user their tracking: the UI
|
||||||
// policy releases the follow camera, so validation happens BEFORE it runs.
|
// policy releases the follow camera, so validation happens BEFORE it runs.
|
||||||
test('an unflyable request never reaches the release policy', () => {
|
test('an unflyable request never reaches the release policy', () => {
|
||||||
const unflyable = [
|
const unflyable = [
|
||||||
{ kind: 'plane', position: POSITION }, // no framing for this kind
|
{ kind: 'plane', position: POSITION }, // no framing for this kind
|
||||||
{ kind: 'vessel', position: { x: NaN, y: 0, z: 0 } }, // malformed position
|
{ kind: 'vessel', position: { x: NaN, y: 0, z: 0 } }, // malformed position
|
||||||
{ kind: 'vessel', position: { x: 0, y: 0 } }, // truthy but incomplete
|
{ kind: 'vessel', position: { x: 0, y: 0 } }, // truthy but incomplete
|
||||||
{ kind: 'vessel', id: '123', position: { x: 0, y: 0, z: 0 } }, // ECEF origin
|
{ kind: 'vessel', id: '123', position: { x: 0, y: 0, z: 0 } }, // ECEF origin
|
||||||
{ kind: 'fire', id: 'fire-1', position: { x: 1, y: 1, z: 1 } }, // finite but inside Earth
|
{ kind: 'fire', id: 'fire-1', position: { x: 1, y: 1, z: 1 } }, // finite but inside Earth
|
||||||
{ kind: 'vessel', position: 'somewhere' },
|
{ kind: 'vessel', position: 'somewhere' },
|
||||||
{ kind: 'vessel', position: POSITION }, // missing owner id
|
{ kind: 'vessel', position: POSITION }, // missing owner id
|
||||||
{ position: POSITION },
|
{ position: POSITION },
|
||||||
];
|
];
|
||||||
for (const detail of unflyable) {
|
for (const detail of unflyable) {
|
||||||
assert.equal(isValidWorldFocusTarget(detail), false, JSON.stringify(detail));
|
assert.equal(
|
||||||
|
isValidWorldFocusTarget(detail),
|
||||||
|
false,
|
||||||
|
JSON.stringify(detail),
|
||||||
|
);
|
||||||
let policyRuns = 0;
|
let policyRuns = 0;
|
||||||
const result = routeWorldFocusRequest(
|
const result = routeWorldFocusRequest(
|
||||||
{ detail },
|
{ detail },
|
||||||
() => { policyRuns += 1; return 'released'; },
|
() => {
|
||||||
|
policyRuns += 1;
|
||||||
|
return 'released';
|
||||||
|
},
|
||||||
() => 'flew',
|
() => 'flew',
|
||||||
);
|
);
|
||||||
assert.equal(result, false, JSON.stringify(detail));
|
assert.equal(result, false, JSON.stringify(detail));
|
||||||
assert.equal(policyRuns, 0, `the policy must not run for ${JSON.stringify(detail)}`);
|
assert.equal(
|
||||||
|
policyRuns,
|
||||||
|
0,
|
||||||
|
`the policy must not run for ${JSON.stringify(detail)}`,
|
||||||
|
);
|
||||||
// Nor may such a request be dispatched in the first place.
|
// Nor may such a request be dispatched in the first place.
|
||||||
assert.equal(requestWorldFocus(detail, new EventTarget()), false);
|
assert.equal(requestWorldFocus(detail, new EventTarget()), false);
|
||||||
}
|
}
|
||||||
assert.equal(isValidWorldFocusTarget({ kind: 'vessel', id: '123', position: POSITION }), true);
|
assert.equal(
|
||||||
assert.equal(isValidWorldFocusTarget({ kind: 'fire', id: 'fire-1', position: POSITION }), true);
|
isValidWorldFocusTarget({ kind: 'vessel', id: '123', position: POSITION }),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
isValidWorldFocusTarget({ kind: 'fire', id: 'fire-1', position: POSITION }),
|
||||||
|
true,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the transfer flight supersedes any flight in progress and keeps the operator heading', () => {
|
test('the transfer flight supersedes any flight in progress and keeps the operator heading', () => {
|
||||||
const camera = stubCamera();
|
const camera = stubCamera();
|
||||||
assert.equal(flyToWorldTarget({ camera }, { kind: 'vessel', id: '123', position: POSITION }), true);
|
assert.equal(
|
||||||
assert.equal(camera.calls.cancelFlight, 1, 'a prior flight must be cancelled, not queued');
|
flyToWorldTarget(bindFakeCesiumCamera({ camera }), {
|
||||||
|
kind: 'vessel',
|
||||||
|
id: '123',
|
||||||
|
position: POSITION,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
camera.calls.cancelFlight,
|
||||||
|
1,
|
||||||
|
'a prior flight must be cancelled, not queued',
|
||||||
|
);
|
||||||
assert.equal(camera.calls.flights.length, 1);
|
assert.equal(camera.calls.flights.length, 1);
|
||||||
const { sphere, options } = camera.calls.flights[0];
|
const { sphere, options } = camera.calls.flights[0];
|
||||||
assert.equal(sphere.radius, WORLD_FOCUS_FRAMING.vessel.radiusM);
|
assert.equal(sphere.radius, WORLD_FOCUS_FRAMING.vessel.radiusM);
|
||||||
@@ -131,12 +190,20 @@ test('the transfer flight supersedes any flight in progress and keeps the operat
|
|||||||
|
|
||||||
test('fires frame wider than vessels — a fire is read by its surroundings', () => {
|
test('fires frame wider than vessels — a fire is read by its surroundings', () => {
|
||||||
const camera = stubCamera();
|
const camera = stubCamera();
|
||||||
flyToWorldTarget({ camera }, { kind: 'fire', id: 'fire-1', position: POSITION });
|
flyToWorldTarget(bindFakeCesiumCamera({ camera }), {
|
||||||
|
kind: 'fire',
|
||||||
|
id: 'fire-1',
|
||||||
|
position: POSITION,
|
||||||
|
});
|
||||||
const { sphere, options } = camera.calls.flights[0];
|
const { sphere, options } = camera.calls.flights[0];
|
||||||
assert.equal(sphere.radius, WORLD_FOCUS_FRAMING.fire.radiusM);
|
assert.equal(sphere.radius, WORLD_FOCUS_FRAMING.fire.radiusM);
|
||||||
assert.equal(options.offset.range, WORLD_FOCUS_FRAMING.fire.rangeM);
|
assert.equal(options.offset.range, WORLD_FOCUS_FRAMING.fire.rangeM);
|
||||||
assert.ok(WORLD_FOCUS_FRAMING.fire.rangeM > WORLD_FOCUS_FRAMING.vessel.rangeM);
|
assert.ok(
|
||||||
assert.ok(WORLD_FOCUS_FRAMING.fire.radiusM > WORLD_FOCUS_FRAMING.vessel.radiusM);
|
WORLD_FOCUS_FRAMING.fire.rangeM > WORLD_FOCUS_FRAMING.vessel.rangeM,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
WORLD_FOCUS_FRAMING.fire.radiusM > WORLD_FOCUS_FRAMING.vessel.radiusM,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('every framing is a real oblique standoff, not a nadir or an inside-out sphere', () => {
|
test('every framing is a real oblique standoff, not a nadir or an inside-out sphere', () => {
|
||||||
@@ -145,25 +212,52 @@ test('every framing is a real oblique standoff, not a nadir or an inside-out sph
|
|||||||
for (const kind of kinds) {
|
for (const kind of kinds) {
|
||||||
const framing = WORLD_FOCUS_FRAMING[kind];
|
const framing = WORLD_FOCUS_FRAMING[kind];
|
||||||
// Looking DOWN at the target, but obliquely — a nadir drop reads as a map.
|
// Looking DOWN at the target, but obliquely — a nadir drop reads as a map.
|
||||||
assert.ok(framing.pitchDeg < 0, `${kind}: pitch must look down, got ${framing.pitchDeg}`);
|
assert.ok(
|
||||||
assert.ok(framing.pitchDeg > -80, `${kind}: pitch must stay oblique, got ${framing.pitchDeg}`);
|
framing.pitchDeg < 0,
|
||||||
|
`${kind}: pitch must look down, got ${framing.pitchDeg}`,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
framing.pitchDeg > -80,
|
||||||
|
`${kind}: pitch must stay oblique, got ${framing.pitchDeg}`,
|
||||||
|
);
|
||||||
// Standing off outside the framed sphere, at a human-readable distance.
|
// Standing off outside the framed sphere, at a human-readable distance.
|
||||||
assert.ok(framing.radiusM > 0, `${kind}: radius must be positive`);
|
assert.ok(framing.radiusM > 0, `${kind}: radius must be positive`);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
framing.rangeM > framing.radiusM,
|
framing.rangeM > framing.radiusM,
|
||||||
`${kind}: range ${framing.rangeM} must stand off outside radius ${framing.radiusM}`,
|
`${kind}: range ${framing.rangeM} must stand off outside radius ${framing.radiusM}`,
|
||||||
);
|
);
|
||||||
assert.ok(framing.rangeM <= 10000, `${kind}: range must stay close-in, got ${framing.rangeM}`);
|
assert.ok(
|
||||||
|
framing.rangeM <= 10000,
|
||||||
|
`${kind}: range must stay close-in, got ${framing.rangeM}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Exact shipped values — a silent retune must show up as a failing test.
|
// Exact shipped values — a silent retune must show up as a failing test.
|
||||||
assert.deepEqual({ ...WORLD_FOCUS_FRAMING.vessel }, { radiusM: 150, rangeM: 1200, pitchDeg: -30 });
|
assert.deepEqual(
|
||||||
assert.deepEqual({ ...WORLD_FOCUS_FRAMING.fire }, { radiusM: 400, rangeM: 3000, pitchDeg: -35 });
|
{ ...WORLD_FOCUS_FRAMING.vessel },
|
||||||
|
{ radiusM: 150, rangeM: 1200, pitchDeg: -30 },
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
{ ...WORLD_FOCUS_FRAMING.fire },
|
||||||
|
{ radiusM: 400, rangeM: 3000, pitchDeg: -35 },
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('unknown kinds and missing viewers issue no flight', () => {
|
test('unknown kinds and missing viewers issue no flight', () => {
|
||||||
const camera = stubCamera();
|
const camera = stubCamera();
|
||||||
assert.equal(flyToWorldTarget({ camera }, { kind: 'plane', position: POSITION }), false);
|
assert.equal(
|
||||||
assert.equal(flyToWorldTarget({ camera }, { kind: 'vessel' }), false);
|
flyToWorldTarget(bindFakeCesiumCamera({ camera }), {
|
||||||
assert.equal(flyToWorldTarget(null, { kind: 'vessel', position: POSITION }), false);
|
kind: 'plane',
|
||||||
|
position: POSITION,
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
flyToWorldTarget(bindFakeCesiumCamera({ camera }), { kind: 'vessel' }),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
flyToWorldTarget(null, { kind: 'vessel', position: POSITION }),
|
||||||
|
false,
|
||||||
|
);
|
||||||
assert.equal(camera.calls.flights.length, 0);
|
assert.equal(camera.calls.flights.length, 0);
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user