mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
fix(vessels): world-clock motion moves only drawn vessels, once per world time
Vessels out of view move on every 250 ms so one coming into view is current; vessels at the end of their fixes ask for no frames; nothing moves while the layer is off; 2D views judge the half-pixel step across the view's width; a re-floor redraws vessels where they are on the next pass.
This commit is contained in:
1 parent
7bea88b395
commit
52695daf5c
5 files changed
+178
-40
No files matched your search
@@ -2001,7 +2001,7 @@ snapshot still loading for more than 8 seconds. Those snapshots apply as polls
|
|||||||
do, so contacts move on smoothly: aircraft are drawn at the clock's time
|
do, so contacts move on smoothly: aircraft are drawn at the clock's time
|
||||||
rather than behind it, and vessels between two fixes move along the great
|
rather than behind it, and vessels between two fixes move along the great
|
||||||
circle, never past the newer one, redrawn every frame with frames asked for
|
circle, never past the newer one, redrawn every frame with frames asked for
|
||||||
as for placed live vessels. Records, absences and the Layers panel's age
|
as for placed live vessels (vessels out of view move on every 250 ms). Records, absences and the Layers panel's age
|
||||||
are judged at the clock's time. Outside the coverage the layer shows no
|
are judged at the clock's time. Outside the coverage the layer shows no
|
||||||
contacts and loads nothing; while the coverage is unknown it loads nothing and
|
contacts and loads nothing; while the coverage is unknown it loads nothing and
|
||||||
clears nothing. A snapshot that fails clears the time and is asked again after
|
clears nothing. A snapshot that fails clears the time and is asked again after
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { VISIBILITY_UPDATE_MS } from './policy.js';
|
|||||||
import { layerPlacement } from '../../data/placementProvider.js';
|
import { layerPlacement } from '../../data/placementProvider.js';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
|
|
||||||
// Vessels move only when a poll lands, so the layer must let the scene idle:
|
// The layer must let the scene idle: it asks for frames for forced passes,
|
||||||
// it asks for frames for forced passes and one trailing frame after the camera
|
// for vessels that move, and one trailing frame after the camera stops, and
|
||||||
// stops, and never schedules a frame from a pass the camera did not cause.
|
// never schedules a frame from a pass nothing caused.
|
||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
@@ -252,4 +252,106 @@ test('a vessel moving visibly asks for its next frame as soon as it moves half a
|
|||||||
assert.ok(waitAfterStep(0.0001) <= 16);
|
assert.ok(waitAfterStep(0.0001) <= 16);
|
||||||
// About 1 cm in a frame: half a pixel takes seconds, so once a second.
|
// About 1 cm in a frame: half a pixel takes seconds, so once a second.
|
||||||
assert.equal(waitAfterStep(0.0000001), 1000);
|
assert.equal(waitAfterStep(0.0000001), 1000);
|
||||||
|
// A 2D view 40 km across in 1,000 px: 11 m is over a quarter pixel, so the
|
||||||
|
// next frame comes within two frames.
|
||||||
|
camera.frustum = { left: -20_000, right: 20_000 };
|
||||||
|
layer.testing._setVesselStateForTest({
|
||||||
|
viewer: { camera, scene: {}, canvas: { clientWidth: 1000 } },
|
||||||
|
records: [record],
|
||||||
|
});
|
||||||
|
layer.testing._prepareVesselVisualForTest(record);
|
||||||
|
assert.ok(waitAfterStep(0.0001) <= 32);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('following the clock moves drawn vessels in place each world time, prepares missing ones and leaves hidden ones be', (t) => {
|
||||||
|
let mono = 0;
|
||||||
|
t.mock.method(performance, 'now', () => mono);
|
||||||
|
const originalDocument = globalThis.document;
|
||||||
|
globalThis.document = { getElementById: () => null };
|
||||||
|
t.after(() => {
|
||||||
|
if (originalDocument === undefined) delete globalThis.document;
|
||||||
|
else globalThis.document = originalDocument;
|
||||||
|
});
|
||||||
|
const { layer, camera, timers, advance } = setup();
|
||||||
|
camera.positionWC = Cesium.Cartesian3.fromDegrees(4.05, 51.93, 20_000);
|
||||||
|
camera.frustum = { fovy: 1 };
|
||||||
|
const motion = {
|
||||||
|
fromLat: 51.93,
|
||||||
|
fromLon: 4.05,
|
||||||
|
fromMs: 0,
|
||||||
|
toLat: 51.94,
|
||||||
|
toLon: 4.05,
|
||||||
|
toMs: 60_000,
|
||||||
|
};
|
||||||
|
const vessel = (mmsi, show) => ({
|
||||||
|
mmsi,
|
||||||
|
lat: 51.94,
|
||||||
|
lon: 4.05,
|
||||||
|
motion: { ...motion },
|
||||||
|
billboard: { show, position: null },
|
||||||
|
});
|
||||||
|
const drawn = vessel('111', true);
|
||||||
|
const hidden = vessel('222', false);
|
||||||
|
const owned = vessel('333', false);
|
||||||
|
const missing = vessel('444', true);
|
||||||
|
layer.testing._setVesselStateForTest({
|
||||||
|
viewer: { camera, scene: {}, canvas: { clientHeight: 1000 } },
|
||||||
|
records: [drawn, hidden, owned, missing],
|
||||||
|
});
|
||||||
|
const visuals = [drawn, hidden, owned].map((record) =>
|
||||||
|
layer.testing._prepareVesselVisualForTest(record),
|
||||||
|
);
|
||||||
|
visuals[2].modelOwnsVisual = true;
|
||||||
|
const before = visuals.map((visual) => ({ ...visual.position }));
|
||||||
|
const positions = visuals.map((visual) => visual.position);
|
||||||
|
|
||||||
|
// The first pass also moves hidden vessels, so the next stays within
|
||||||
|
// HIDDEN_FOLLOW_MS of it.
|
||||||
|
layer.testing._runClockPassForTest(10_000);
|
||||||
|
mono += 16;
|
||||||
|
layer.testing._runClockPassForTest(20_000);
|
||||||
|
assert.equal(visuals[0].position, positions[0], 'written in place');
|
||||||
|
assert.notDeepEqual({ ...visuals[0].position }, before[0]);
|
||||||
|
assert.notDeepEqual({ ...visuals[2].position }, before[2], 'model moves');
|
||||||
|
const hiddenAt = { ...visuals[1].position };
|
||||||
|
assert.ok(missing.billboard.position, 'a missing visual is prepared');
|
||||||
|
assert.equal(timers.size, 1, 'moving vessels ask for a frame');
|
||||||
|
|
||||||
|
// The hidden vessel waits for HIDDEN_FOLLOW_MS.
|
||||||
|
mono += 16;
|
||||||
|
layer.testing._runClockPassForTest(30_000);
|
||||||
|
assert.deepEqual({ ...visuals[1].position }, hiddenAt);
|
||||||
|
mono += 250;
|
||||||
|
layer.testing._runClockPassForTest(40_000);
|
||||||
|
assert.notDeepEqual({ ...visuals[1].position }, hiddenAt);
|
||||||
|
|
||||||
|
// The same world time again moves nothing and asks for no frame.
|
||||||
|
advance(1_000);
|
||||||
|
assert.equal(timers.size, 0);
|
||||||
|
const still = { ...visuals[0].position };
|
||||||
|
mono += 16;
|
||||||
|
layer.testing._runClockPassForTest(40_000);
|
||||||
|
assert.deepEqual({ ...visuals[0].position }, still);
|
||||||
|
assert.equal(timers.size, 0);
|
||||||
|
|
||||||
|
// Scrubbed back, a vessel moves back.
|
||||||
|
mono += 16;
|
||||||
|
layer.testing._runClockPassForTest(20_000);
|
||||||
|
assert.notDeepEqual({ ...visuals[0].position }, still);
|
||||||
|
|
||||||
|
// Each vessel moves once per world time: new fixes at the same world time
|
||||||
|
// are drawn when the snapshot that brings them prepares the visual.
|
||||||
|
const backAt = { ...visuals[0].position };
|
||||||
|
drawn.motion = { ...motion, toLat: 52 };
|
||||||
|
mono += 16;
|
||||||
|
layer.testing._runClockPassForTest(20_000);
|
||||||
|
assert.deepEqual({ ...visuals[0].position }, backAt);
|
||||||
|
|
||||||
|
// Past the newest fix, vessels hold still and ask for nothing.
|
||||||
|
mono += 16;
|
||||||
|
layer.testing._runClockPassForTest(70_000);
|
||||||
|
advance(1_000);
|
||||||
|
mono += 16;
|
||||||
|
layer.testing._runClockPassForTest(80_000);
|
||||||
|
assert.equal(timers.size, 0);
|
||||||
});
|
});
|
||||||
@@ -37,6 +37,9 @@ import { isAidToNavigation } from '../../data/vesselTypes.js';
|
|||||||
const VESSEL_STEP_PX = 0.5;
|
const VESSEL_STEP_PX = 0.5;
|
||||||
/** A frame is asked for at least this often while any drawn vessel moves. */
|
/** A frame is asked for at least this often while any drawn vessel moves. */
|
||||||
const PLACEMENT_FRAME_MS = 1000;
|
const PLACEMENT_FRAME_MS = 1000;
|
||||||
|
/** How often vessels not drawn move on as the world clock plays (wall ms),
|
||||||
|
* so one that comes into view is judged where it is. */
|
||||||
|
const HIDDEN_FOLLOW_MS = 250;
|
||||||
|
|
||||||
/** How often the vessels near enough for a model are gathered again (wall ms). */
|
/** How often the vessels near enough for a model are gathered again (wall ms). */
|
||||||
const MODEL_CANDIDATES_MS = 500;
|
const MODEL_CANDIDATES_MS = 500;
|
||||||
@@ -87,10 +90,8 @@ export function createRendering({
|
|||||||
visual.position,
|
visual.position,
|
||||||
new Cesium.Cartesian3(),
|
new Cesium.Cartesian3(),
|
||||||
);
|
);
|
||||||
if (services.placement) {
|
|
||||||
visual.drawnLat = lat;
|
visual.drawnLat = lat;
|
||||||
visual.drawnLon = lon;
|
visual.drawnLon = lon;
|
||||||
}
|
|
||||||
return visual;
|
return visual;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,39 +430,51 @@ export function createRendering({
|
|||||||
modelCandidatesAtMs = -Infinity;
|
modelCandidatesAtMs = -Infinity;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vessels move only when a poll lands, so the layer does not hold the
|
// The layer does not hold the continuous render loop. Its passes run in
|
||||||
// continuous render loop. Its passes run in preRender, which idle mode fires
|
// preRender, which idle mode fires only for rendered frames: moving vessels
|
||||||
// only for rendered frames: a forced pass (new data, selection) requests its
|
// ask for the frame that next moves them visibly (askMotionFrame), a forced
|
||||||
// frame, a frame that moved the camera but fell inside the 800 ms cadence
|
// pass (new data, selection) requests its frame, a frame that moved the
|
||||||
// leaves a trailing frame so the view the camera stopped on is always
|
// camera but fell inside the 800 ms cadence leaves a trailing frame so the view the camera stopped on is always
|
||||||
// evaluated, and the continuous hold is kept only while focus emphasis is
|
// evaluated, and the continuous hold is kept only while focus emphasis is
|
||||||
// animating. The trailing frame is keyed to camera motion alone: the pass's
|
// animating. The trailing frame is keyed to camera motion alone: the pass's
|
||||||
// own label publication requests a frame, and scheduling on that would loop.
|
// own label publication requests a frame, and scheduling on that would loop.
|
||||||
function installRuntime(viewer) {
|
function installRuntime(viewer) {
|
||||||
if (state.preRenderRemover || !viewer) return;
|
if (state.preRenderRemover || !viewer) return;
|
||||||
state.preRenderRemover = viewer.scene.preRender.addEventListener(() => {
|
state.preRenderRemover = viewer.scene.preRender.addEventListener(() => {
|
||||||
|
if (state.feed.enabled) {
|
||||||
if (followsClock(vesselState)) followClockPositions();
|
if (followsClock(vesselState)) followClockPositions();
|
||||||
else if (services.placement && state.feed.enabled) followPlacements();
|
else if (services.placement) followPlacements();
|
||||||
|
}
|
||||||
updateVisibility();
|
updateVisibility();
|
||||||
if (state.feed.enabled) updateModels();
|
if (state.feed.enabled) updateModels();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// As the world clock plays, vessels between two fixes move on along them,
|
// As the world clock plays, vessels between two fixes move on along them:
|
||||||
// every frame.
|
// drawn ones every frame, each once per world time, hidden ones (which may
|
||||||
let followedAtMs = null;
|
// come into view) every HIDDEN_FOLLOW_MS.
|
||||||
|
let hiddenFollowedAtWallMs = -Infinity;
|
||||||
function followClockPositions() {
|
function followClockPositions() {
|
||||||
const atMs = vesselState.worldNow();
|
const atMs = vesselState.worldNow();
|
||||||
if (atMs === followedAtMs) return;
|
|
||||||
followedAtMs = atMs;
|
|
||||||
const motion = startMotionPass();
|
const motion = startMotionPass();
|
||||||
|
const moveHidden =
|
||||||
|
motion.wallMs - hiddenFollowedAtWallMs >= HIDDEN_FOLLOW_MS;
|
||||||
|
if (moveHidden) hiddenFollowedAtWallMs = motion.wallMs;
|
||||||
for (const record of state.records.all) {
|
for (const record of state.records.all) {
|
||||||
if (!record.motion) continue;
|
if (!record.motion) continue;
|
||||||
let visual = visualRecords.get(record);
|
let visual = visualRecords.get(record);
|
||||||
if (visual?.position) {
|
if (!visual?.position) {
|
||||||
|
visual = prepareRecordVisual(record);
|
||||||
|
visual.followedAtMs = atMs;
|
||||||
|
} else {
|
||||||
|
const drawn = visual.billboard?.show || visual.modelOwnsVisual;
|
||||||
|
if (visual.followedAtMs === atMs || (!drawn && !moveHidden)) continue;
|
||||||
|
visual.followedAtMs = atMs;
|
||||||
const { lat, lon } = vesselPositionAt(record, atMs);
|
const { lat, lon } = vesselPositionAt(record, atMs);
|
||||||
|
// At either end of its fixes a vessel holds still.
|
||||||
|
if (lat === visual.drawnLat && lon === visual.drawnLon) continue;
|
||||||
moveDrawnVisual(visual, lat, lon, motion);
|
moveDrawnVisual(visual, lat, lon, motion);
|
||||||
} else visual = prepareRecordVisual(record);
|
}
|
||||||
if (visual.billboard) visual.billboard.position = visual.position;
|
if (visual.billboard) visual.billboard.position = visual.position;
|
||||||
}
|
}
|
||||||
askMotionFrame(motion);
|
askMotionFrame(motion);
|
||||||
@@ -490,18 +503,27 @@ export function createRendering({
|
|||||||
askMotionFrame(motion);
|
askMotionFrame(motion);
|
||||||
}
|
}
|
||||||
|
|
||||||
// One pass of moving vessels: whether any drawn vessel is moving, the
|
// One pass of moving vessels: whether any drawn vessel is moving, how far
|
||||||
// largest angle one moved through as seen from the camera (radians), and
|
// the fastest moved (as an angle seen from the camera, radians, and in
|
||||||
// the wall time since the pass before.
|
// metres), the pass's wall time and the wall time since the pass before.
|
||||||
let motionPassWallMs = null;
|
let motionPassWallMs = null;
|
||||||
const motionPass = { camera: null, sinceMs: 0, moving: false, angle: 0 };
|
const motionPass = {
|
||||||
|
camera: null,
|
||||||
|
wallMs: 0,
|
||||||
|
sinceMs: 0,
|
||||||
|
moving: false,
|
||||||
|
angle: 0,
|
||||||
|
metres: 0,
|
||||||
|
};
|
||||||
function startMotionPass() {
|
function startMotionPass() {
|
||||||
const wallMs = performance.now();
|
const wallMs = performance.now();
|
||||||
motionPass.camera = state.viewer?.camera ?? null;
|
motionPass.camera = state.viewer?.camera ?? null;
|
||||||
|
motionPass.wallMs = wallMs;
|
||||||
motionPass.sinceMs =
|
motionPass.sinceMs =
|
||||||
motionPassWallMs === null ? 0 : wallMs - motionPassWallMs;
|
motionPassWallMs === null ? 0 : wallMs - motionPassWallMs;
|
||||||
motionPass.moving = false;
|
motionPass.moving = false;
|
||||||
motionPass.angle = 0;
|
motionPass.angle = 0;
|
||||||
|
motionPass.metres = 0;
|
||||||
motionPassWallMs = wallMs;
|
motionPassWallMs = wallMs;
|
||||||
return motionPass;
|
return motionPass;
|
||||||
}
|
}
|
||||||
@@ -512,14 +534,28 @@ export function createRendering({
|
|||||||
moveRecordVisual(visual, lat, lon);
|
moveRecordVisual(visual, lat, lon);
|
||||||
if (!visual.billboard?.show && !visual.modelOwnsVisual) return;
|
if (!visual.billboard?.show && !visual.modelOwnsVisual) return;
|
||||||
motion.moving = true;
|
motion.moving = true;
|
||||||
|
const metres = Cesium.Cartesian3.distance(from, visual.position);
|
||||||
|
motion.metres = Math.max(motion.metres, metres);
|
||||||
const eye = motion.camera?.positionWC;
|
const eye = motion.camera?.positionWC;
|
||||||
if (!eye) return;
|
if (!eye) return;
|
||||||
const distance = Cesium.Cartesian3.distance(eye, from);
|
const distance = Cesium.Cartesian3.distance(eye, from);
|
||||||
if (distance > 0)
|
if (distance > 0) motion.angle = Math.max(motion.angle, metres / distance);
|
||||||
motion.angle = Math.max(
|
}
|
||||||
motion.angle,
|
|
||||||
Cesium.Cartesian3.distance(from, visual.position) / distance,
|
/** How far the fastest drawn vessel moved on screen in a pass (pixels):
|
||||||
|
* through the perspective view's angle, or across the 2D view's width. */
|
||||||
|
function movedPixels(motion) {
|
||||||
|
const canvas = state.viewer?.canvas;
|
||||||
|
const frustum = motion.camera?.frustum;
|
||||||
|
if (!canvas || !frustum) return 0;
|
||||||
|
if (frustum.fovy > 0 && canvas.clientHeight > 0)
|
||||||
|
return (
|
||||||
|
(motion.angle * canvas.clientHeight) / (2 * Math.tan(frustum.fovy / 2))
|
||||||
);
|
);
|
||||||
|
const width = frustum.right - frustum.left;
|
||||||
|
if (!(frustum.fovy > 0) && width > 0 && canvas.clientWidth > 0)
|
||||||
|
return (motion.metres * canvas.clientWidth) / width;
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The timer for the next vessel frame while one is asked for, and when it
|
// The timer for the next vessel frame while one is asked for, and when it
|
||||||
@@ -534,17 +570,9 @@ export function createRendering({
|
|||||||
function askMotionFrame(motion) {
|
function askMotionFrame(motion) {
|
||||||
if (!motion.moving) return;
|
if (!motion.moving) return;
|
||||||
let waitMs = PLACEMENT_FRAME_MS;
|
let waitMs = PLACEMENT_FRAME_MS;
|
||||||
const canvasHeight = state.viewer?.canvas?.clientHeight;
|
const movedPx = movedPixels(motion);
|
||||||
const fovy = motion.camera?.frustum?.fovy;
|
if (movedPx > 0 && motion.sinceMs > 0)
|
||||||
if (
|
|
||||||
canvasHeight > 0 &&
|
|
||||||
fovy > 0 &&
|
|
||||||
motion.angle > 0 &&
|
|
||||||
motion.sinceMs > 0
|
|
||||||
) {
|
|
||||||
const movedPx = (motion.angle * canvasHeight) / (2 * Math.tan(fovy / 2));
|
|
||||||
waitMs = Math.min(waitMs, (VESSEL_STEP_PX * motion.sinceMs) / movedPx);
|
waitMs = Math.min(waitMs, (VESSEL_STEP_PX * motion.sinceMs) / movedPx);
|
||||||
}
|
|
||||||
// A frame already asked for no later than this one stands.
|
// A frame already asked for no later than this one stands.
|
||||||
const runtime = vesselState._aisRuntime;
|
const runtime = vesselState._aisRuntime;
|
||||||
const dueMs = runtime.now() + waitMs;
|
const dueMs = runtime.now() + waitMs;
|
||||||
@@ -943,6 +971,7 @@ export function createRendering({
|
|||||||
cancelTrailingVisibilityPass,
|
cancelTrailingVisibilityPass,
|
||||||
cancelPlacementFrame,
|
cancelPlacementFrame,
|
||||||
followPlacements,
|
followPlacements,
|
||||||
|
followClockPositions,
|
||||||
applyVesselFocusDeemphasis,
|
applyVesselFocusDeemphasis,
|
||||||
makeOccluder,
|
makeOccluder,
|
||||||
isVisible,
|
isVisible,
|
||||||
|
|||||||
@@ -174,6 +174,13 @@ export function createTesting({
|
|||||||
components.rendering.followPlacements();
|
components.rendering.followPlacements();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Run one world-clock pass at world time `atMs`. Test-only seam. */
|
||||||
|
|
||||||
|
function _runClockPassForTest(atMs) {
|
||||||
|
vesselState.worldNow = () => atMs;
|
||||||
|
components.rendering.followClockPositions();
|
||||||
|
}
|
||||||
|
|
||||||
/** True while a trailing visibility frame is scheduled. Test-only seam. */
|
/** True while a trailing visibility frame is scheduled. Test-only seam. */
|
||||||
|
|
||||||
function _hasTrailingVisibilityPassForTest() {
|
function _hasTrailingVisibilityPassForTest() {
|
||||||
@@ -208,6 +215,7 @@ export function createTesting({
|
|||||||
_runVisibilityPassForTest,
|
_runVisibilityPassForTest,
|
||||||
_prepareVesselVisualForTest,
|
_prepareVesselVisualForTest,
|
||||||
_runPlacementPassForTest,
|
_runPlacementPassForTest,
|
||||||
|
_runClockPassForTest,
|
||||||
_hasTrailingVisibilityPassForTest,
|
_hasTrailingVisibilityPassForTest,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -73,11 +73,10 @@ export function createTracking({
|
|||||||
heightM,
|
heightM,
|
||||||
);
|
);
|
||||||
if (visual.billboard) visual.billboard.position = visual.position;
|
if (visual.billboard) visual.billboard.position = visual.position;
|
||||||
// A placed vessel is drawn again at its placement on the next pass.
|
// A moving vessel is drawn again where it is on the next pass.
|
||||||
if (services.placement) {
|
|
||||||
visual.drawnLat = NaN;
|
visual.drawnLat = NaN;
|
||||||
visual.drawnLon = NaN;
|
visual.drawnLon = NaN;
|
||||||
}
|
visual.followedAtMs = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user