mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
Keep your target in view: smooth camera controls and organized data layers (#583)
## Explore the map without losing your target Tilt and north-up used to stop tracking an aircraft or satellite. Both controls now keep the selected target centered while easing into the new angle over 650 ms, preserving camera range. Tilt switches between top-down and oblique views; north-up takes the shortest rotation. Rapid presses reverse a pending tilt, and pressing north-up during a tilt completes both changes. New navigation, target selection, map gestures, and teardown cancel the transition. Cockpit keeps its existing camera guard. ## A data panel that follows how you explore The panel now has five clearly labeled sections: | Section | Layer order | | --- | --- | | Movement | Satellites → Live Flights → Military Flights → Live Vessels → Street Traffic → Bike Share | | Cameras | Cameras → Mapped ALPR Cameras | | Infrastructure | Mapped Installations → Data Centers → Submarine Cables → Dams | | Events | Space Missions → Earthquakes → Active Fires | | Utilities | Directions → Radio | Readable panel labels replace CCTV, Bikeshare, Datacenters, and provider-heavy names. Source attribution remains in the subtitles, and toggle accessibility labels match the visible names. Presentation order is independent of layer registration; hidden layers stay hidden and unlisted layers remain available under Other layers. ## Validation - Camera changes: full Node 24 suite, including allocation gates — 3,699 passing and one skipped. - Final panel changes: 42 focused tests passed; browser inspection confirmed all sections, ordering, and labels. - Real Cesium Camera/EntityView tests cover moving aircraft, low/high satellites, cancellation, navigation ownership, and rapid button presses. - Live browser: UITMSAT-2 remained centered and selected through tilt and north-up, including combined commands. - Final formatting, package-boundary checks, and production build pass. - GitHub CI verifies the final head on Node 24, Node 26, and Windows onboarding before merge.
This commit is contained in:
1 parent
c9f9896b48
commit
1ad565c8af
6 files changed
+402
-8
No files matched your search
@@ -171,6 +171,7 @@ export class StyleManager {
|
||||
cancelShareSelection: () => this._shareRestoration.cancelSelection(),
|
||||
getDataManager: () => this._dataManager,
|
||||
stopOrbit: () => this._stopOrbit(),
|
||||
cancelOrientation: () => this._cameraOrientationControls?.cancel(),
|
||||
showToast: (text) => this._showToast(text),
|
||||
});
|
||||
this._shareRestoration = new ShareRestoration({
|
||||
@@ -3281,7 +3282,7 @@ export class StyleManager {
|
||||
northButton: this._northUpBtn,
|
||||
},
|
||||
runNavigation: (noun, navigate) =>
|
||||
this._runExplicitNavigation(noun, navigate),
|
||||
this._navigation.runOrientation(noun, navigate),
|
||||
showToast: (message) => this._showToast(message),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { isPickedWorldPosition } from '../data/scenePick.js';
|
||||
|
||||
function trackedTarget(viewer) {
|
||||
const entity = viewer.trackedEntity;
|
||||
// Read the visual's sample first; sampling a flight CallbackProperty twice
|
||||
// can advance dead reckoning ahead of the displayed aircraft.
|
||||
return (
|
||||
entity?.gevDisplayPosition?.() ||
|
||||
entity?.position?.getValue(viewer.clock?.currentTime)
|
||||
);
|
||||
}
|
||||
|
||||
export const OBLIQUE_PITCH = Cesium.Math.toRadians(-35);
|
||||
export const STRAIGHT_DOWN_PITCH = Cesium.Math.toRadians(-89);
|
||||
const OBLIQUE_THRESHOLD = Cesium.Math.toRadians(-60);
|
||||
@@ -63,7 +73,9 @@ export function pickViewTarget(viewer) {
|
||||
/** Describe the camera as an orbit around the current viewport center. */
|
||||
export function readCameraTargetFrame(viewer) {
|
||||
const camera = viewer?.camera;
|
||||
const target = pickViewTarget(viewer);
|
||||
const target = viewer?.trackedEntity
|
||||
? trackedTarget(viewer)
|
||||
: pickViewTarget(viewer);
|
||||
if (!camera || !target || !isPickedWorldPosition(camera.positionWC))
|
||||
return null;
|
||||
|
||||
@@ -98,6 +110,9 @@ export function setCameraTargetFrame(viewer, frame) {
|
||||
const camera = viewer?.camera;
|
||||
if (!camera || !frame?.target || !Number.isFinite(frame.range)) return false;
|
||||
try {
|
||||
const trackedTransform = viewer.trackedEntity
|
||||
? Cesium.Matrix4.clone(camera.transform)
|
||||
: null;
|
||||
camera.lookAt(
|
||||
frame.target,
|
||||
new Cesium.HeadingPitchRange(
|
||||
@@ -106,6 +121,13 @@ export function setCameraTargetFrame(viewer, frame) {
|
||||
frame.range,
|
||||
),
|
||||
);
|
||||
if (trackedTransform) {
|
||||
// Preserve EntityView's reference frame (including satellite frames).
|
||||
// Its next update carries this new local offset along with the target.
|
||||
camera.lookAtTransform(trackedTransform);
|
||||
viewer.scene?.requestRender?.();
|
||||
return true;
|
||||
}
|
||||
const destination = Cesium.Cartesian3.clone(camera.positionWC);
|
||||
const direction = Cesium.Cartesian3.clone(camera.directionWC);
|
||||
const up = Cesium.Cartesian3.clone(camera.upWC);
|
||||
@@ -160,6 +182,62 @@ function headingDegrees(camera) {
|
||||
return Cesium.Math.toDegrees(normalizedHeading(camera?.heading));
|
||||
}
|
||||
|
||||
/** Ease an orbit without handing off the entity that owns the follow camera. */
|
||||
export function createCameraOrientationAnimator(
|
||||
viewer,
|
||||
{ now = () => performance.now(), duration = 650 } = {},
|
||||
) {
|
||||
let remove = null;
|
||||
let pending = null;
|
||||
const cancel = () => {
|
||||
remove?.();
|
||||
remove = null;
|
||||
pending = null;
|
||||
};
|
||||
function animate(frame, destination) {
|
||||
cancel();
|
||||
if (!viewer.scene?.preUpdate?.addEventListener || duration <= 0)
|
||||
return setCameraTargetFrame(viewer, { ...frame, ...destination });
|
||||
const entity = viewer.trackedEntity;
|
||||
pending = destination;
|
||||
const start = now();
|
||||
const headingDelta = Cesium.Math.negativePiToPi(
|
||||
destination.heading - frame.heading,
|
||||
);
|
||||
remove = viewer.scene.preUpdate.addEventListener(() => {
|
||||
if (viewer.isDestroyed?.() || viewer.trackedEntity !== entity) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
const progress = Cesium.Math.clamp((now() - start) / duration, 0, 1);
|
||||
const eased = Cesium.EasingFunction.CUBIC_IN_OUT(progress);
|
||||
const target = entity ? trackedTarget(viewer) : frame.target;
|
||||
if (
|
||||
!isPickedWorldPosition(target) ||
|
||||
!setCameraTargetFrame(viewer, {
|
||||
...frame,
|
||||
target,
|
||||
heading: frame.heading + headingDelta * eased,
|
||||
pitch: Cesium.Math.lerp(frame.pitch, destination.pitch, eased),
|
||||
})
|
||||
) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
if (progress === 1) cancel();
|
||||
});
|
||||
viewer.scene.requestRender?.();
|
||||
return true;
|
||||
}
|
||||
return {
|
||||
animate,
|
||||
cancel,
|
||||
get destination() {
|
||||
return pending;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Bind the two map-orientation actions and keep their accessible state current. */
|
||||
export function bindCameraOrientationControls({
|
||||
viewer,
|
||||
@@ -176,6 +254,7 @@ export function bindCameraOrientationControls({
|
||||
// of the screen: 4 ms median and up to 12 ms, measured on this machine. It is
|
||||
// therefore held between refreshes rather than recomputed per frame.
|
||||
let tilted = false;
|
||||
const animator = createCameraOrientationAnimator(viewer);
|
||||
|
||||
/** Recompute the tilt state exactly, paying for one pick. */
|
||||
const refreshTilt = () => {
|
||||
@@ -229,7 +308,19 @@ export function bindCameraOrientationControls({
|
||||
removers.push(() => element.removeEventListener('click', handler));
|
||||
};
|
||||
listen(tiltButton, () => {
|
||||
const result = runNavigation('camera', () => toggleCameraTilt(viewer));
|
||||
const pending = animator.destination;
|
||||
const result = runNavigation('camera', () => {
|
||||
const frame = readCameraTargetFrame(viewer);
|
||||
if (!frame) return false;
|
||||
const nextTilted = !frameIsTilted(pending || frame);
|
||||
const pitch = nextTilted ? OBLIQUE_PITCH : STRAIGHT_DOWN_PITCH;
|
||||
return animator.animate(frame, {
|
||||
heading: pending?.heading ?? frame.heading,
|
||||
pitch,
|
||||
})
|
||||
? { tilted: nextTilted, pitch }
|
||||
: false;
|
||||
});
|
||||
if (result) {
|
||||
showToast?.(result.tilted ? 'Tilted view' : 'Straight-down view');
|
||||
// The action reports the state it just commanded, so the button can
|
||||
@@ -239,7 +330,17 @@ export function bindCameraOrientationControls({
|
||||
sync();
|
||||
});
|
||||
listen(northButton, () => {
|
||||
const result = runNavigation('camera', () => resetCameraNorth(viewer));
|
||||
const pending = animator.destination;
|
||||
const result = runNavigation('camera', () => {
|
||||
const frame = readCameraTargetFrame(viewer);
|
||||
return (
|
||||
frame &&
|
||||
animator.animate(frame, {
|
||||
heading: 0,
|
||||
pitch: pending?.pitch ?? frame.pitch,
|
||||
})
|
||||
);
|
||||
});
|
||||
if (result) showToast?.('North up');
|
||||
sync();
|
||||
});
|
||||
@@ -255,14 +356,22 @@ export function bindCameraOrientationControls({
|
||||
// The exact tilt state is refreshed once per gesture, not once per frame —
|
||||
// moveEnd fires when the camera stops, which is where the pick is affordable.
|
||||
subscribe(viewer?.camera?.moveEnd, refreshTilt);
|
||||
subscribe(viewer?.trackedEntityChanged, animator.cancel);
|
||||
for (const type of ['pointerdown', 'wheel']) {
|
||||
const canvas = viewer?.scene?.canvas;
|
||||
canvas?.addEventListener?.(type, animator.cancel, { passive: true });
|
||||
removers.push(() => canvas?.removeEventListener?.(type, animator.cancel));
|
||||
}
|
||||
refreshTilt();
|
||||
|
||||
return {
|
||||
sync,
|
||||
refreshTilt,
|
||||
cancel: animator.cancel,
|
||||
destroy() {
|
||||
if (destroyed) return;
|
||||
destroyed = true;
|
||||
animator.cancel();
|
||||
for (const remove of removers.splice(0)) remove();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,11 +5,13 @@ import {
|
||||
OBLIQUE_PITCH,
|
||||
STRAIGHT_DOWN_PITCH,
|
||||
bindCameraOrientationControls,
|
||||
createCameraOrientationAnimator,
|
||||
pickViewTarget,
|
||||
readCameraTargetFrame,
|
||||
resetCameraNorth,
|
||||
toggleCameraTilt,
|
||||
} from './cameraOrientationControls.js';
|
||||
import { NavigationController } from './navigationController.js';
|
||||
|
||||
function createViewer({ cameraPosition, target, pickPosition = null } = {}) {
|
||||
const calls = [];
|
||||
@@ -489,3 +491,173 @@ test('bindings route both controls and release every listener on destroy', () =>
|
||||
assert.deepEqual(navigations, ['camera', 'camera']);
|
||||
assert.equal(STRAIGHT_DOWN_PITCH, Cesium.Math.toRadians(-89));
|
||||
});
|
||||
|
||||
for (const [label, altitude, range] of [
|
||||
['aircraft', 10000, 1500],
|
||||
['satellite', 400000, 1500000],
|
||||
['high satellite', 35786000, 3000000],
|
||||
]) {
|
||||
test(`${label}: animated tilt and north retain a moving EntityView`, (t) => {
|
||||
// Use Cesium's built-in TEME fallback without fetching Earth-orientation data.
|
||||
t.mock.method(
|
||||
Cesium.Transforms,
|
||||
'computeFixedToIcrfMatrix',
|
||||
() => undefined,
|
||||
);
|
||||
let target = Cesium.Cartesian3.fromDegrees(-97, 30, altitude);
|
||||
const { viewer } = createRealViewer(target);
|
||||
viewer.scene.preUpdate = new Cesium.Event();
|
||||
viewer.clock = { currentTime: Cesium.JulianDate.now() };
|
||||
const entity = new Cesium.Entity({
|
||||
position: new Cesium.CallbackPositionProperty(() => target, false),
|
||||
trackingReferenceFrame: Cesium.TrackingReferenceFrame.ENU,
|
||||
});
|
||||
entity.gevDisplayPosition = () => target;
|
||||
viewer.trackedEntity = entity;
|
||||
const follow = new Cesium.EntityView(entity, viewer.scene);
|
||||
follow.update(viewer.clock.currentTime);
|
||||
viewer.camera.lookAt(
|
||||
target,
|
||||
new Cesium.HeadingPitchRange(
|
||||
Cesium.Math.toRadians(350),
|
||||
OBLIQUE_PITCH,
|
||||
range,
|
||||
),
|
||||
);
|
||||
let time = 0;
|
||||
const animator = createCameraOrientationAnimator(viewer, {
|
||||
now: () => time,
|
||||
duration: 1000,
|
||||
});
|
||||
for (const pitch of [STRAIGHT_DOWN_PITCH, OBLIQUE_PITCH]) {
|
||||
const frame = readCameraTargetFrame(viewer);
|
||||
animator.animate(frame, { heading: 0, pitch });
|
||||
for (const step of [0, 100, 500, 900, 1000]) {
|
||||
time = (pitch === STRAIGHT_DOWN_PITCH ? 0 : 1000) + step;
|
||||
target = Cesium.Cartesian3.fromDegrees(
|
||||
-97 + time / 100000,
|
||||
30,
|
||||
altitude,
|
||||
);
|
||||
follow.update(viewer.clock.currentTime);
|
||||
const transform = Cesium.Matrix4.clone(viewer.camera.transform);
|
||||
viewer.scene.preUpdate.raiseEvent();
|
||||
assert.equal(viewer.trackedEntity, entity);
|
||||
assert.ok(
|
||||
Cesium.Matrix4.equalsEpsilon(
|
||||
viewer.camera.transform,
|
||||
transform,
|
||||
1e-8,
|
||||
),
|
||||
);
|
||||
const actual = readCameraTargetFrame(viewer);
|
||||
const eased = Cesium.EasingFunction.CUBIC_IN_OUT(step / 1000);
|
||||
assert.ok(
|
||||
Math.abs(actual.pitch - Cesium.Math.lerp(frame.pitch, pitch, eased)) <
|
||||
1e-6,
|
||||
);
|
||||
assert.ok(Math.abs(actual.range - range) < 1e-5);
|
||||
}
|
||||
assert.equal(viewer.scene.preUpdate.numberOfListeners, 0);
|
||||
}
|
||||
target = Cesium.Cartesian3.fromDegrees(-96, 31, altitude);
|
||||
follow.update(viewer.clock.currentTime);
|
||||
assert.equal(viewer.trackedEntity, entity);
|
||||
assert.ok(Math.abs(readCameraTargetFrame(viewer).range - range) < 1e-5);
|
||||
});
|
||||
}
|
||||
|
||||
test('orientation animation cancels on replacement and explicit cancellation', () => {
|
||||
const { viewer } = createRealViewer(Cesium.Cartesian3.fromDegrees(0, 0));
|
||||
orbit(viewer, Cesium.Cartesian3.fromDegrees(0, 0), 90, OBLIQUE_PITCH, 1000);
|
||||
viewer.scene.preUpdate = new Cesium.Event();
|
||||
const animator = createCameraOrientationAnimator(viewer);
|
||||
const frame = readCameraTargetFrame(viewer);
|
||||
animator.animate(frame, { heading: 0, pitch: STRAIGHT_DOWN_PITCH });
|
||||
animator.animate(frame, { heading: 0, pitch: OBLIQUE_PITCH });
|
||||
assert.equal(viewer.scene.preUpdate.numberOfListeners, 1);
|
||||
viewer.trackedEntity = {};
|
||||
viewer.scene.preUpdate.raiseEvent();
|
||||
assert.equal(viewer.scene.preUpdate.numberOfListeners, 0);
|
||||
viewer.trackedEntity = undefined;
|
||||
animator.animate(frame, { heading: 0, pitch: STRAIGHT_DOWN_PITCH });
|
||||
animator.cancel();
|
||||
assert.equal(viewer.scene.preUpdate.numberOfListeners, 0);
|
||||
});
|
||||
|
||||
test('orientation navigation preserves follow and selection; new navigation cancels animation', () => {
|
||||
const calls = [];
|
||||
const entity = {};
|
||||
const viewer = {
|
||||
trackedEntity: entity,
|
||||
camera: { cancelFlight: () => calls.push('cancel-flight') },
|
||||
};
|
||||
let cockpit = false;
|
||||
const navigation = new NavigationController({
|
||||
viewer,
|
||||
tracking: {},
|
||||
isCockpitActive: () => cockpit,
|
||||
cancelOrientation: () => calls.push('cancel-orientation'),
|
||||
clearLocation: () => calls.push('clear-location'),
|
||||
cancelShareSelection: () => calls.push('clear-selection'),
|
||||
interruptCameraMotion: () => calls.push('interrupt-motion'),
|
||||
stopOrbit: () => calls.push('stop-orbit'),
|
||||
showToast: () => calls.push('toast'),
|
||||
});
|
||||
assert.equal(
|
||||
navigation.runOrientation('camera', () => 'animated'),
|
||||
'animated',
|
||||
);
|
||||
assert.equal(viewer.trackedEntity, entity);
|
||||
assert.deepEqual(calls, [
|
||||
'cancel-orientation',
|
||||
'interrupt-motion',
|
||||
'stop-orbit',
|
||||
'cancel-flight',
|
||||
]);
|
||||
calls.length = 0;
|
||||
navigation._stampNavigation({ cancelPendingSelection: false });
|
||||
assert.deepEqual(calls, ['cancel-orientation', 'clear-location']);
|
||||
calls.length = 0;
|
||||
cockpit = true;
|
||||
assert.equal(
|
||||
navigation.runOrientation('camera', () =>
|
||||
assert.fail('cockpit should refuse'),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.deepEqual(calls, ['toast']);
|
||||
});
|
||||
|
||||
test('rapid tilt presses reverse direction and north-up keeps the requested tilt', (t) => {
|
||||
let time = 0;
|
||||
t.mock.method(performance, 'now', () => time);
|
||||
const target = Cesium.Cartesian3.fromDegrees(0, 0);
|
||||
const { viewer } = createRealViewer(target);
|
||||
orbit(viewer, target, 90, OBLIQUE_PITCH, 1000);
|
||||
viewer.scene.preUpdate = new Cesium.Event();
|
||||
const tiltButton = new FakeButton();
|
||||
const northButton = new FakeButton();
|
||||
let controls;
|
||||
controls = bindCameraOrientationControls({
|
||||
viewer,
|
||||
elements: { tiltButton, northButton },
|
||||
runNavigation: (_noun, navigate) => {
|
||||
controls.cancel();
|
||||
return navigate();
|
||||
},
|
||||
});
|
||||
tiltButton.click();
|
||||
tiltButton.click();
|
||||
assert.equal(tiltButton.getAttribute('aria-pressed'), 'true');
|
||||
tiltButton.click();
|
||||
northButton.click();
|
||||
assert.equal(viewer.scene.preUpdate.numberOfListeners, 1);
|
||||
time = 650;
|
||||
viewer.scene.preUpdate.raiseEvent();
|
||||
const frame = readCameraTargetFrame(viewer);
|
||||
assert.ok(Math.abs(frame.pitch - STRAIGHT_DOWN_PITCH) < 1e-6);
|
||||
assert.ok(Math.abs(frame.heading) < 1e-6);
|
||||
controls.destroy();
|
||||
assert.equal(viewer.scene.preUpdate.numberOfListeners, 0);
|
||||
});
|
||||
+75
-4
@@ -11,6 +11,60 @@ const FEED_STATE_LABELS = Object.freeze({
|
||||
unavailable: 'UNAVAILABLE',
|
||||
});
|
||||
|
||||
// Presentation order is independent of catalog registration and startup order.
|
||||
const PANEL_GROUPS = [
|
||||
{
|
||||
label: 'Movement',
|
||||
ids: [
|
||||
'satellites',
|
||||
'flights',
|
||||
'military',
|
||||
'ais-live-vessels',
|
||||
'traffic',
|
||||
'bikeshare',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Cameras',
|
||||
ids: ['cctv', 'alpr-cameras'],
|
||||
},
|
||||
{
|
||||
label: 'Infrastructure',
|
||||
ids: [
|
||||
'military-installations',
|
||||
'local-datacenters',
|
||||
'telegeography-submarine-cables',
|
||||
'local-dams',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Events',
|
||||
ids: ['rocket-launches', 'earthquakes', 'local-firms'],
|
||||
},
|
||||
{
|
||||
label: 'Utilities',
|
||||
ids: ['directions', 'radio'],
|
||||
},
|
||||
];
|
||||
const PANEL_ORDER = PANEL_GROUPS.flatMap(({ label, ids }) =>
|
||||
ids.map((id) => ({ id, label })),
|
||||
);
|
||||
const PANEL_POSITIONS = new Map(
|
||||
PANEL_ORDER.map(({ id }, index) => [id, index]),
|
||||
);
|
||||
const PANEL_LABELS = {
|
||||
'ais-live-vessels': 'Live Vessels',
|
||||
bikeshare: 'Bike Share',
|
||||
cctv: 'Cameras',
|
||||
'alpr-cameras': 'Mapped ALPR Cameras',
|
||||
'local-datacenters': 'Data Centers',
|
||||
'local-firms': 'Active Fires',
|
||||
};
|
||||
|
||||
function panelLabel(layer) {
|
||||
return PANEL_LABELS[layer.id] || layer.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guidance for a control a missing provider key is holding back.
|
||||
*
|
||||
@@ -81,8 +135,25 @@ export class LayerPanel {
|
||||
this._toggleContainer.innerHTML = '';
|
||||
|
||||
const generation = this._generation;
|
||||
for (const layer of this.getAll()) {
|
||||
const layers = this.getAll()
|
||||
.slice()
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(PANEL_POSITIONS.get(a.id) ?? PANEL_ORDER.length) -
|
||||
(PANEL_POSITIONS.get(b.id) ?? PANEL_ORDER.length),
|
||||
);
|
||||
let previousGroup = '';
|
||||
for (const layer of layers) {
|
||||
if (!layer.showInTogglePanel) continue;
|
||||
const group =
|
||||
PANEL_ORDER[PANEL_POSITIONS.get(layer.id)]?.label ?? 'Other layers';
|
||||
if (group && group !== previousGroup) {
|
||||
const heading = document.createElement('h3');
|
||||
heading.className = 'data-layer-group-heading';
|
||||
heading.textContent = group;
|
||||
this._toggleContainer.appendChild(heading);
|
||||
}
|
||||
previousGroup = group;
|
||||
const row = document.createElement('div');
|
||||
row.className = 'data-toggle-row';
|
||||
row.dataset.layerId = layer.id;
|
||||
@@ -97,7 +168,7 @@ export class LayerPanel {
|
||||
icon.textContent = layer.icon;
|
||||
const name = document.createElement('span');
|
||||
name.className = 'data-name';
|
||||
name.textContent = layer.name;
|
||||
name.textContent = panelLabel(layer);
|
||||
left.appendChild(icon);
|
||||
left.appendChild(name);
|
||||
|
||||
@@ -489,8 +560,8 @@ export class LayerPanel {
|
||||
button.setAttribute(
|
||||
'aria-label',
|
||||
keyGuidance
|
||||
? `${layer.name}: ${button.textContent}. ${keyGuidance}`
|
||||
: `${layer.name}: ${button.textContent}`,
|
||||
? `${panelLabel(layer)}: ${button.textContent}. ${keyGuidance}`
|
||||
: `${panelLabel(layer)}: ${button.textContent}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export class NavigationController {
|
||||
getDataManager,
|
||||
stopOrbit,
|
||||
showToast,
|
||||
cancelOrientation = () => {},
|
||||
}) {
|
||||
Object.assign(this, {
|
||||
viewer,
|
||||
@@ -30,6 +31,7 @@ export class NavigationController {
|
||||
getDataManager,
|
||||
stopOrbit,
|
||||
showToast,
|
||||
cancelOrientation,
|
||||
});
|
||||
this._navigationGeneration = 0;
|
||||
this._activeLocationSearchGeneration = null;
|
||||
@@ -39,6 +41,7 @@ export class NavigationController {
|
||||
cancelPendingSelection = true,
|
||||
clearSearchedLocation = true,
|
||||
} = {}) {
|
||||
this.cancelOrientation();
|
||||
const { flightsLayer, militaryFlightsLayer, satellitesLayer } =
|
||||
this.tracking;
|
||||
this._navigationGeneration += 1;
|
||||
@@ -188,6 +191,27 @@ export class NavigationController {
|
||||
});
|
||||
}
|
||||
|
||||
/** Change the viewing angle without clearing selection or follow ownership. */
|
||||
runOrientation(noun, navigate) {
|
||||
return runExplicitNavigation({
|
||||
disposed: this._disposed,
|
||||
cockpitActive: this.isCockpitActive(),
|
||||
noun,
|
||||
showToast: (text) => this.showToast(text),
|
||||
stamp: () =>
|
||||
this._stampNavigation({
|
||||
cancelPendingSelection: false,
|
||||
clearSearchedLocation: false,
|
||||
}),
|
||||
release: () => {
|
||||
this.interruptCameraMotion('camera-orientation');
|
||||
this.stopOrbit();
|
||||
this.viewer.camera.cancelFlight();
|
||||
},
|
||||
navigate,
|
||||
});
|
||||
}
|
||||
|
||||
_beginDeferredNavigation(
|
||||
noun = 'location',
|
||||
{ cancelPendingSelection = true } = {},
|
||||
|
||||
@@ -107,6 +107,23 @@
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.data-layer-group-heading {
|
||||
margin: 8px 10px 2px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: var(--text-secondary);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1.4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.data-layer-group-heading:first-child {
|
||||
margin-top: 0;
|
||||
padding-top: 4px;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.data-toggle-row {
|
||||
padding: 8px 10px 6px;
|
||||
border-radius: 8px;
|
||||
|
||||
Reference in new issue
Block a user