mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
feat(wind): render the inspection marker from a store
createWindInspectionMarker() moves to src/layers/wind/inspectionMarker.js, so the portable ./tools entry that reads wind/inspection.js stays free of Vue. It keeps the frame listener and projection and presents where the inspected point falls to the gev-wind-inspection store after each rendered frame, publishing nothing for an unchanged frame; the scene chrome teleports the marker into the wind layer's container with the same class, inline style, hidden state and position. The application guide lists everything leaving out the scene chrome now leaves out.
This commit is contained in:
1 parent
0926cbc105
commit
e0bfa471ac
13 files changed
+292
-131
No files matched your search
@@ -6,11 +6,8 @@ import {
|
||||
windReadingResult,
|
||||
windUnitChips,
|
||||
} from './presentation.js';
|
||||
import {
|
||||
inspectWindAtCenter,
|
||||
createWindInspectionMarker,
|
||||
WIND_UNITS,
|
||||
} from './inspection.js';
|
||||
import { inspectWindAtCenter, WIND_UNITS } from './inspection.js';
|
||||
import { createWindInspectionMarker } from './inspectionMarker.js';
|
||||
import { t, msg, translateKnown } from '../../i18n/index.js';
|
||||
import { unitLabel } from '../../ui/unitLabels.js';
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createWindLayer, formatWindValidTime, windStats } from './index.js';
|
||||
import { useWindInspectionStore } from '../../ui/stores/windInspection.js';
|
||||
import { applicationPinia } from '../../ui/vueRuntime.js';
|
||||
|
||||
const snapshot = (model) => ({
|
||||
model,
|
||||
@@ -302,12 +304,12 @@ test('weather summary describes the selected forecast and count remains numeric'
|
||||
});
|
||||
|
||||
test('sample stays fixed across model, field and unit changes; dismissal and disable clear marker', async () => {
|
||||
const nodes = [];
|
||||
// The marker the scene chrome renders, as the wind layer presents it.
|
||||
const marker = () => useWindInspectionStore(applicationPinia()).marker;
|
||||
let listener;
|
||||
let samples = 0;
|
||||
const container = {
|
||||
ownerDocument: { createElement: () => ({ style: {}, setAttribute() {}, remove() { nodes.splice(nodes.indexOf(this), 1); } }) },
|
||||
appendChild(node) { nodes.push(node); },
|
||||
ownerDocument: {},
|
||||
getBoundingClientRect: () => ({ left: 0, top: 0 }),
|
||||
};
|
||||
const viewer = {
|
||||
@@ -338,9 +340,9 @@ test('sample stays fixed across model, field and unit changes; dismissal and dis
|
||||
layer.setParams({ inspect: true });
|
||||
const captured = layer.getRowControls().summary.reading;
|
||||
assert.equal(samples, 1);
|
||||
const retained = nodes[0];
|
||||
const retained = marker().owner;
|
||||
layer.setParams({ units: 'mph' });
|
||||
assert.equal(nodes[0], retained);
|
||||
assert.ok(marker().owner === retained);
|
||||
const changed = layer.getRowControls().summary.reading;
|
||||
assert.equal(changed.speed, captured.speed);
|
||||
assert.equal(changed.coordinates, captured.coordinates);
|
||||
@@ -348,7 +350,7 @@ test('sample stays fixed across model, field and unit changes; dismissal and dis
|
||||
assert.equal(samples, 1, 'units do not resample');
|
||||
assert.equal(layer.getRowControls().summary.result.id, 'reading');
|
||||
assert.deepEqual(layer.getRowControls().summary.settings.map(({ label }) => label), ['MODEL', 'FIELD', 'UNITS', 'MOTION']);
|
||||
assert.equal(nodes.length, 1);
|
||||
assert.ok(marker());
|
||||
assert.equal(typeof listener, 'function');
|
||||
viewer.camera.pickEllipsoid = () => { samples++; return { longitude: 70, latitude: 20 }; };
|
||||
layer.setParams({ overlay: 'speed' });
|
||||
@@ -368,9 +370,9 @@ test('sample stays fixed across model, field and unit changes; dismissal and dis
|
||||
assert.equal(samples, 2, 'the next explicit read samples the new center');
|
||||
for (const change of [() => layer.setParams({ inspect: false }), () => layer.disable()]) {
|
||||
layer.setParams({ inspect: true });
|
||||
assert.equal(nodes.length, 1);
|
||||
assert.ok(marker());
|
||||
change();
|
||||
assert.equal(nodes.length, 0);
|
||||
assert.equal(marker(), null);
|
||||
assert.equal(layer.getRowControls().summary.reading, null);
|
||||
assert.equal(layer.getRowControls().summary.result, null);
|
||||
assert.equal(listener, null);
|
||||
|
||||
@@ -112,62 +112,3 @@ export function inspectWindAtCenter(
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/** One passive marker for the captured location; it never samples on camera movement. */
|
||||
export function createWindInspectionMarker({ container, viewer, cesium }) {
|
||||
let removeRender = null;
|
||||
let marker = null;
|
||||
const clear = () => {
|
||||
removeRender?.();
|
||||
removeRender = null;
|
||||
marker?.remove();
|
||||
marker = null;
|
||||
viewer?.scene?.requestRender?.();
|
||||
};
|
||||
return {
|
||||
show(position) {
|
||||
clear();
|
||||
const scene = viewer?.scene;
|
||||
if (
|
||||
!position ||
|
||||
!container?.ownerDocument?.createElement ||
|
||||
!scene?.postRender?.addEventListener ||
|
||||
!scene.cartesianToCanvasCoordinates
|
||||
)
|
||||
return;
|
||||
marker = container.ownerDocument.createElement('div');
|
||||
marker.className = 'gev-wind-inspection-marker';
|
||||
marker.setAttribute('aria-hidden', 'true');
|
||||
marker.style.cssText =
|
||||
'position:absolute;width:18px;height:18px;border:2px solid #fff;border-radius:50%;box-shadow:0 0 0 2px #153440,0 0 0 5px rgba(102,225,242,.35);transform:translate(-50%,-50%);pointer-events:none;z-index:150;';
|
||||
container.appendChild(marker);
|
||||
const occluder = new cesium.EllipsoidalOccluder(
|
||||
scene.globe?.ellipsoid || cesium.Ellipsoid.WGS84,
|
||||
viewer.camera.positionWC,
|
||||
);
|
||||
const render = () => {
|
||||
occluder.cameraPosition = viewer.camera.positionWC;
|
||||
const point = scene.cartesianToCanvasCoordinates(position);
|
||||
const visible =
|
||||
point &&
|
||||
point.x >= 0 &&
|
||||
point.y >= 0 &&
|
||||
point.x <= scene.canvas.clientWidth &&
|
||||
point.y <= scene.canvas.clientHeight &&
|
||||
(scene.mode !== cesium.SceneMode.SCENE3D ||
|
||||
occluder.isPointVisible(position));
|
||||
marker.hidden = !visible;
|
||||
if (!visible) return;
|
||||
const canvasRect = scene.canvas.getBoundingClientRect();
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
marker.style.left = `${point.x + canvasRect.left - containerRect.left}px`;
|
||||
marker.style.top = `${point.y + canvasRect.top - containerRect.top}px`;
|
||||
};
|
||||
removeRender = scene.postRender.addEventListener(render);
|
||||
render();
|
||||
scene.requestRender?.();
|
||||
},
|
||||
clear,
|
||||
destroy: clear,
|
||||
};
|
||||
}
|
||||
@@ -58,55 +58,3 @@ test('center inspection uses a fixed forecast reading without double-converting
|
||||
'No surface reading',
|
||||
);
|
||||
});
|
||||
|
||||
test('inspection marker follows only its captured point, hides beyond limb, and releases its listener', async () => {
|
||||
const { createWindInspectionMarker } = await import('./inspection.js');
|
||||
let listener = null;
|
||||
let visible = true;
|
||||
const projected = [];
|
||||
const nodes = [];
|
||||
const container = {
|
||||
ownerDocument: { createElement: () => ({ style: {}, setAttribute() {}, remove() { nodes.splice(nodes.indexOf(this), 1); } }) },
|
||||
appendChild(node) { nodes.push(node); },
|
||||
getBoundingClientRect: () => ({ left: 5, top: 10 }),
|
||||
};
|
||||
const viewer = {
|
||||
camera: { positionWC: {} },
|
||||
scene: {
|
||||
mode: 3,
|
||||
canvas: { clientWidth: 800, clientHeight: 600, getBoundingClientRect: () => ({ left: 15, top: 30 }) },
|
||||
postRender: { addEventListener(fn) { assert.equal(listener, null); listener = fn; return () => { listener = null; }; } },
|
||||
cartesianToCanvasCoordinates(point) { projected.push(point); return { x: 100, y: 200 }; },
|
||||
requestRender() {},
|
||||
},
|
||||
};
|
||||
const cesium = {
|
||||
SceneMode: { SCENE3D: 3 }, Ellipsoid: { WGS84: {} },
|
||||
EllipsoidalOccluder: class { isPointVisible() { return visible; } },
|
||||
};
|
||||
const owner = createWindInspectionMarker({ container, viewer, cesium });
|
||||
const point = { x: 1, y: 2, z: 3 };
|
||||
owner.show(point);
|
||||
assert.equal(nodes.length, 1);
|
||||
assert.match(nodes[0].style.cssText, /pointer-events:none/);
|
||||
assert.equal(nodes[0].style.left, '110px');
|
||||
assert.equal(nodes[0].style.top, '220px');
|
||||
viewer.camera.positionWC = { moved: true };
|
||||
visible = false;
|
||||
listener();
|
||||
assert.equal(nodes[0].hidden, true);
|
||||
assert.ok(projected.every((value) => value === point));
|
||||
visible = true;
|
||||
listener();
|
||||
assert.equal(nodes[0].hidden, false);
|
||||
owner.show({ x: 4 });
|
||||
assert.equal(nodes.length, 1);
|
||||
owner.clear();
|
||||
assert.equal(nodes.length, 0);
|
||||
assert.equal(listener, null);
|
||||
owner.show(point);
|
||||
owner.destroy();
|
||||
owner.destroy();
|
||||
assert.equal(nodes.length, 0);
|
||||
assert.equal(listener, null);
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useWindInspectionStore } from '../../ui/stores/windInspection.js';
|
||||
import { applicationPinia } from '../../ui/vueRuntime.js';
|
||||
|
||||
/**
|
||||
* One passive marker for the captured location; it never samples on camera
|
||||
* movement. Where the point falls is presented to the wind inspection store
|
||||
* after every rendered frame, and the scene chrome renders the marker in
|
||||
* `container`.
|
||||
*/
|
||||
export function createWindInspectionMarker({
|
||||
container,
|
||||
viewer,
|
||||
cesium,
|
||||
store = useWindInspectionStore(applicationPinia()),
|
||||
}) {
|
||||
const owner = {};
|
||||
let removeRender = null;
|
||||
let marker = null;
|
||||
// Only the owner whose marker the store shows may move or remove it.
|
||||
const shown = () => marker && store.marker?.owner === owner;
|
||||
const present = (patch) => {
|
||||
marker = { ...marker, ...patch };
|
||||
store.publish(marker);
|
||||
};
|
||||
const clear = () => {
|
||||
removeRender?.();
|
||||
removeRender = null;
|
||||
if (shown()) store.publish(null);
|
||||
marker = null;
|
||||
viewer?.scene?.requestRender?.();
|
||||
};
|
||||
return {
|
||||
show(position) {
|
||||
clear();
|
||||
const scene = viewer?.scene;
|
||||
if (
|
||||
!position ||
|
||||
!container?.ownerDocument ||
|
||||
!scene?.postRender?.addEventListener ||
|
||||
!scene.cartesianToCanvasCoordinates
|
||||
)
|
||||
return;
|
||||
present({ owner, container, visible: false, left: 0, top: 0 });
|
||||
const occluder = new cesium.EllipsoidalOccluder(
|
||||
scene.globe?.ellipsoid || cesium.Ellipsoid.WGS84,
|
||||
viewer.camera.positionWC,
|
||||
);
|
||||
const render = () => {
|
||||
if (!shown()) return;
|
||||
occluder.cameraPosition = viewer.camera.positionWC;
|
||||
const point = scene.cartesianToCanvasCoordinates(position);
|
||||
const visible =
|
||||
point &&
|
||||
point.x >= 0 &&
|
||||
point.y >= 0 &&
|
||||
point.x <= scene.canvas.clientWidth &&
|
||||
point.y <= scene.canvas.clientHeight &&
|
||||
(scene.mode !== cesium.SceneMode.SCENE3D ||
|
||||
occluder.isPointVisible(position));
|
||||
if (!visible) {
|
||||
// A hidden marker keeps its last place, as the element did.
|
||||
if (marker.visible) present({ visible: false });
|
||||
return;
|
||||
}
|
||||
const canvasRect = scene.canvas.getBoundingClientRect();
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const left = point.x + canvasRect.left - containerRect.left;
|
||||
const top = point.y + canvasRect.top - containerRect.top;
|
||||
if (marker.visible && marker.left === left && marker.top === top)
|
||||
return;
|
||||
present({ visible: true, left, top });
|
||||
};
|
||||
removeRender = scene.postRender.addEventListener(render);
|
||||
render();
|
||||
scene.requestRender?.();
|
||||
},
|
||||
clear,
|
||||
destroy: clear,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// src/layers/wind/inspectionMarker.test.mjs — the wind inspection marker
|
||||
// follows only its captured point, renders in its container, and releases
|
||||
// its frame listener.
|
||||
import '../../testSupport/registerDom.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createApp, nextTick } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import { useWindInspectionStore } from '../../ui/stores/windInspection.js';
|
||||
import WindInspectionMarker from '../../ui/views/panels/WindInspectionMarker.vue';
|
||||
import { createWindInspectionMarker } from './inspectionMarker.js';
|
||||
|
||||
const cesium = {
|
||||
SceneMode: { SCENE3D: 3 },
|
||||
Ellipsoid: { WGS84: {} },
|
||||
EllipsoidalOccluder: class {
|
||||
isPointVisible() {
|
||||
return cesium.visible;
|
||||
}
|
||||
},
|
||||
visible: true,
|
||||
};
|
||||
|
||||
function fixture(t) {
|
||||
document.body.replaceChildren();
|
||||
const container = document.body.appendChild(document.createElement('div'));
|
||||
container.getBoundingClientRect = () => ({ left: 5, top: 10 });
|
||||
const pinia = createPinia();
|
||||
const app = createApp(WindInspectionMarker).use(pinia);
|
||||
app.mount(document.body.appendChild(document.createElement('div')));
|
||||
t.after(() => app.unmount());
|
||||
const frames = { listener: null };
|
||||
const projected = [];
|
||||
const viewer = {
|
||||
camera: { positionWC: {} },
|
||||
scene: {
|
||||
mode: 3,
|
||||
canvas: {
|
||||
clientWidth: 800,
|
||||
clientHeight: 600,
|
||||
getBoundingClientRect: () => ({ left: 15, top: 30 }),
|
||||
},
|
||||
postRender: {
|
||||
addEventListener(fn) {
|
||||
assert.equal(frames.listener, null);
|
||||
frames.listener = fn;
|
||||
return () => {
|
||||
frames.listener = null;
|
||||
};
|
||||
},
|
||||
},
|
||||
cartesianToCanvasCoordinates(point) {
|
||||
projected.push(point);
|
||||
return { x: 100, y: 200 };
|
||||
},
|
||||
requestRender() {},
|
||||
},
|
||||
};
|
||||
const store = useWindInspectionStore(pinia);
|
||||
cesium.visible = true;
|
||||
const owner = createWindInspectionMarker({
|
||||
container,
|
||||
viewer,
|
||||
cesium,
|
||||
store,
|
||||
});
|
||||
const nodes = () => [
|
||||
...container.querySelectorAll('.gev-wind-inspection-marker'),
|
||||
];
|
||||
return { owner, viewer, store, frames, projected, nodes, container, cesium };
|
||||
}
|
||||
|
||||
test('the marker follows only its captured point, hides beyond the limb, and releases its listener', async (t) => {
|
||||
const { owner, viewer, frames, projected, nodes } = fixture(t);
|
||||
const point = { x: 1, y: 2, z: 3 };
|
||||
owner.show(point);
|
||||
await nextTick();
|
||||
assert.equal(nodes().length, 1);
|
||||
const [marker] = nodes();
|
||||
assert.equal(marker.getAttribute('aria-hidden'), 'true');
|
||||
assert.equal(marker.style.pointerEvents, 'none');
|
||||
assert.equal(marker.style.position, 'absolute');
|
||||
assert.equal(marker.style.zIndex, '150');
|
||||
assert.equal(marker.style.left, '110px');
|
||||
assert.equal(marker.style.top, '220px');
|
||||
assert.equal(marker.hidden, false);
|
||||
viewer.camera.positionWC = { moved: true };
|
||||
cesium.visible = false;
|
||||
frames.listener();
|
||||
await nextTick();
|
||||
assert.equal(marker.hidden, true);
|
||||
assert.equal(marker.style.left, '110px', 'a hidden marker keeps its place');
|
||||
assert.ok(projected.every((value) => value === point));
|
||||
cesium.visible = true;
|
||||
frames.listener();
|
||||
await nextTick();
|
||||
assert.equal(marker.hidden, false);
|
||||
owner.show({ x: 4 });
|
||||
await nextTick();
|
||||
assert.equal(nodes().length, 1);
|
||||
owner.clear();
|
||||
await nextTick();
|
||||
assert.equal(nodes().length, 0);
|
||||
assert.equal(frames.listener, null);
|
||||
owner.show(point);
|
||||
owner.destroy();
|
||||
owner.destroy();
|
||||
await nextTick();
|
||||
assert.equal(nodes().length, 0);
|
||||
assert.equal(frames.listener, null);
|
||||
});
|
||||
|
||||
test('an unchanged frame publishes nothing, and an older owner leaves a newer marker', (t) => {
|
||||
const { owner, viewer, store, frames, container } = fixture(t);
|
||||
owner.show({ x: 1 });
|
||||
let publishes = 0;
|
||||
const publish = store.publish;
|
||||
store.publish = (next) => {
|
||||
publishes += 1;
|
||||
publish(next);
|
||||
};
|
||||
frames.listener();
|
||||
assert.equal(publishes, 0);
|
||||
frames.listener = null;
|
||||
const newer = createWindInspectionMarker({
|
||||
container,
|
||||
viewer,
|
||||
cesium,
|
||||
store,
|
||||
});
|
||||
newer.show({ x: 2 });
|
||||
const shown = store.marker;
|
||||
owner.destroy();
|
||||
assert.ok(store.marker === shown);
|
||||
newer.destroy();
|
||||
assert.equal(store.marker, null);
|
||||
});
|
||||
Reference in new issue
Block a user