mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2fa15b205 | ||
|
|
52695daf5c | ||
|
|
7bea88b395 | ||
|
|
57bacd98a8 |
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
- Move vessels every frame. Vessels between fixes, and live vessels a host
|
||||||
|
placement places, were redrawn at most every 250 ms with a frame asked for
|
||||||
|
once a second; they are now redrawn every frame, and the next frame is asked
|
||||||
|
for once the fastest drawn vessel will have moved half a pixel on screen
|
||||||
|
(Sameh Khamis, #1747).
|
||||||
@@ -380,9 +380,10 @@ in view while their placed position changes; one it holds still lets the
|
|||||||
render loop idle and is asked again each second, moving on once its placement
|
render loop idle and is asked again each second, moving on once its placement
|
||||||
does. A placed transit vehicle is drawn at its placed point, and a placed
|
does. A placed transit vehicle is drawn at its placed point, and a placed
|
||||||
train where it is placed rather than on its bound railway line. Live vessels
|
train where it is placed rather than on its bound railway line. Live vessels
|
||||||
a provider places move on between refreshes: redrawn in place at most every
|
a provider places move on between refreshes: redrawn in place every frame,
|
||||||
250 ms, with a frame asked for each second while the provider places any
|
with the next frame asked for as soon as the fastest drawn vessel will have
|
||||||
drawn vessel, and none once the layer is off.
|
moved half a pixel on screen, at least once a second while the provider places
|
||||||
|
any drawn vessel, and none once the layer is off.
|
||||||
|
|
||||||
Application catalog composition accepts an optional `streetLevelProviders` list.
|
Application catalog composition accepts an optional `streetLevelProviders` list.
|
||||||
An explicit list replaces the default Mapillary provider; an empty list removes
|
An explicit list replaces the default Mapillary provider; an empty list removes
|
||||||
@@ -1999,7 +2000,8 @@ Playing, the layer stays a snapshot ahead of the time, and play does not pass a
|
|||||||
snapshot still loading for more than 8 seconds. Those snapshots apply as polls
|
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. Records, absences and the Layers panel's age
|
circle, never past the newer one, redrawn every frame with frames asked for
|
||||||
|
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
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import assert from 'node:assert/strict';
|
|||||||
import { createVesselLayer } from './index.js';
|
import { createVesselLayer } from './index.js';
|
||||||
import { VISIBILITY_UPDATE_MS } from './policy.js';
|
import { VISIBILITY_UPDATE_MS } from './policy.js';
|
||||||
import { layerPlacement } from '../../data/placementProvider.js';
|
import { layerPlacement } from '../../data/placementProvider.js';
|
||||||
|
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 = () => {};
|
||||||
|
|
||||||
@@ -75,7 +76,16 @@ function setup({ placement = null } = {}) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const frame = () => layer.testing._runVisibilityPassForTest();
|
const frame = () => layer.testing._runVisibilityPassForTest();
|
||||||
return { layer, camera, requests, holds, timers, advance, frame };
|
return {
|
||||||
|
layer,
|
||||||
|
camera,
|
||||||
|
requests,
|
||||||
|
holds,
|
||||||
|
timers,
|
||||||
|
advance,
|
||||||
|
frame,
|
||||||
|
now: () => clock,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test('a parked camera schedules no frames after the cadence pass', () => {
|
test('a parked camera schedules no frames after the cadence pass', () => {
|
||||||
@@ -196,3 +206,152 @@ test('a placement that places nothing asks for no frames; one that places a draw
|
|||||||
advance(1_000);
|
advance(1_000);
|
||||||
assert.deepEqual(requests, ['ais-vessels-placement']);
|
assert.deepEqual(requests, ['ais-vessels-placement']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a vessel moving visibly asks for its next frame as soon as it moves half a pixel; a slow one once a second', (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;
|
||||||
|
});
|
||||||
|
let spot = { lat: 51.93, lon: 4.05 };
|
||||||
|
const { layer, camera, timers, advance, now } = setup({
|
||||||
|
placement: { layer: () => ({ place: () => spot }) },
|
||||||
|
});
|
||||||
|
// 20 km above the vessel, in a 1,000 px tall view about 57° tall.
|
||||||
|
camera.positionWC = Cesium.Cartesian3.fromDegrees(4.05, 51.93, 20_000);
|
||||||
|
camera.frustum = { fovy: 1 };
|
||||||
|
const record = {
|
||||||
|
mmsi: '111',
|
||||||
|
lat: 51.93,
|
||||||
|
lon: 4.05,
|
||||||
|
billboard: { show: true, position: null },
|
||||||
|
};
|
||||||
|
layer.testing._setVesselStateForTest({
|
||||||
|
viewer: { camera, scene: {}, canvas: { clientHeight: 1000 } },
|
||||||
|
records: [record],
|
||||||
|
});
|
||||||
|
layer.testing._prepareVesselVisualForTest(record);
|
||||||
|
// How long after a pass the next frame is asked for, once the vessel moved
|
||||||
|
// `dLat` in the 16 ms since the pass before.
|
||||||
|
const waitAfterStep = (dLat) => {
|
||||||
|
layer.testing._runPlacementPassForTest();
|
||||||
|
advance(1_000); // whatever frame that asked for comes and goes
|
||||||
|
assert.equal(timers.size, 0);
|
||||||
|
mono += 16;
|
||||||
|
spot = { lat: spot.lat + dLat, lon: spot.lon };
|
||||||
|
layer.testing._runPlacementPassForTest();
|
||||||
|
assert.equal(timers.size, 1);
|
||||||
|
const [timer] = [...timers.values()];
|
||||||
|
return timer.at - now();
|
||||||
|
};
|
||||||
|
// About 11 m in a frame, 20 km away: over half a pixel, so the next frame
|
||||||
|
// is asked for within the frame.
|
||||||
|
assert.ok(waitAfterStep(0.0001) <= 16);
|
||||||
|
// About 1 cm in a frame: half a pixel takes seconds, so once a second.
|
||||||
|
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);
|
||||||
|
});
|
||||||
+148
-55
@@ -31,14 +31,22 @@ import {
|
|||||||
import { createFleetModelLayer } from '../../data/fleetModelInstances.js';
|
import { createFleetModelLayer } from '../../data/fleetModelInstances.js';
|
||||||
import { isAidToNavigation } from '../../data/vesselTypes.js';
|
import { isAidToNavigation } from '../../data/vesselTypes.js';
|
||||||
|
|
||||||
/** How often vessels move on as the world clock plays (wall ms). */
|
/** A moving vessel's next frame is asked for once it would move this far on
|
||||||
const FOLLOW_POSITION_MS = 250;
|
* screen (pixels), so it glides when it moves visibly and the scene idles
|
||||||
/** A frame is asked for this often while a placement moves live vessels. */
|
* when it does not. */
|
||||||
|
const VESSEL_STEP_PX = 0.5;
|
||||||
|
/** 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;
|
||||||
|
|
||||||
|
/** Scratch for where a moved vessel was drawn. */
|
||||||
|
const _movedFrom = new Cesium.Cartesian3();
|
||||||
|
|
||||||
export function createRendering({
|
export function createRendering({
|
||||||
vesselState,
|
vesselState,
|
||||||
services,
|
services,
|
||||||
@@ -82,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,80 +430,166 @@ 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 (followsClock(vesselState)) followClockPositions();
|
if (state.feed.enabled) {
|
||||||
else if (services.placement && state.feed.enabled) followPlacements();
|
if (followsClock(vesselState)) followClockPositions();
|
||||||
|
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:
|
||||||
let followedAtWallMs = -Infinity;
|
// 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 wallMs = performance.now();
|
|
||||||
if (wallMs - followedAtWallMs < FOLLOW_POSITION_MS) return;
|
|
||||||
followedAtWallMs = wallMs;
|
|
||||||
const atMs = vesselState.worldNow();
|
const atMs = vesselState.worldNow();
|
||||||
if (atMs === followedAtMs) return;
|
const motion = startMotionPass();
|
||||||
followedAtMs = atMs;
|
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;
|
||||||
const visual = prepareRecordVisual(record);
|
let visual = visualRecords.get(record);
|
||||||
|
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);
|
||||||
|
// At either end of its fixes a vessel holds still.
|
||||||
|
if (lat === visual.drawnLat && lon === visual.drawnLon) continue;
|
||||||
|
moveDrawnVisual(visual, lat, lon, motion);
|
||||||
|
}
|
||||||
if (visual.billboard) visual.billboard.position = visual.position;
|
if (visual.billboard) visual.billboard.position = visual.position;
|
||||||
}
|
}
|
||||||
|
askMotionFrame(motion);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Live with a host-supplied placement, vessels move on between refreshes:
|
// Live with a host-supplied placement, vessels move on between refreshes:
|
||||||
// drawn where it places them at most every FOLLOW_POSITION_MS, with a frame
|
// drawn where it places them every frame, with the next frame asked for
|
||||||
// asked for every PLACEMENT_FRAME_MS while it places any drawn vessel, so
|
// as soon as a drawn vessel would move visibly, so they move while the
|
||||||
// they move while the scene idles.
|
// scene idles.
|
||||||
let placedAtWallMs = -Infinity;
|
|
||||||
let placing = false;
|
|
||||||
let placementFrameTimer = null;
|
|
||||||
function followPlacements() {
|
function followPlacements() {
|
||||||
const wallMs = performance.now();
|
const nowMs = vesselState._aisRuntime.now();
|
||||||
if (wallMs - placedAtWallMs >= FOLLOW_POSITION_MS) {
|
const motion = startMotionPass();
|
||||||
placedAtWallMs = wallMs;
|
// Drawn vessels only, as a sprite or a model: a hidden one is placed
|
||||||
placing = false;
|
// again when it is drawn.
|
||||||
const nowMs = vesselState._aisRuntime.now();
|
for (const record of state.records.all) {
|
||||||
// Drawn vessels only, as a sprite or a model: a hidden one is placed
|
const visual = visualRecords.get(record);
|
||||||
// again when it is drawn.
|
if (!visual?.position) continue;
|
||||||
for (const record of state.records.all) {
|
if (!visual.billboard?.show && !visual.modelOwnsVisual) continue;
|
||||||
const visual = visualRecords.get(record);
|
const placed = services.placement.place(record.id, nowMs);
|
||||||
if (!visual?.position) continue;
|
if (placed) motion.moving = true;
|
||||||
if (!visual.billboard?.show && !visual.modelOwnsVisual) continue;
|
const { lat, lon } = placed ?? record;
|
||||||
const placed = services.placement.place(record.id, nowMs);
|
if (lat === visual.drawnLat && lon === visual.drawnLon) continue;
|
||||||
if (placed) placing = true;
|
moveDrawnVisual(visual, lat, lon, motion);
|
||||||
const { lat, lon } = placed ?? record;
|
if (visual.billboard) visual.billboard.position = visual.position;
|
||||||
if (lat === visual.drawnLat && lon === visual.drawnLon) continue;
|
|
||||||
moveRecordVisual(visual, lat, lon);
|
|
||||||
if (visual.billboard) visual.billboard.position = visual.position;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (placing && placementFrameTimer === null)
|
askMotionFrame(motion);
|
||||||
placementFrameTimer = vesselState._aisRuntime.setTimeout(() => {
|
}
|
||||||
placementFrameTimer = null;
|
|
||||||
if (state.feed.enabled && !followsClock(vesselState))
|
// One pass of moving vessels: whether any drawn vessel is moving, how far
|
||||||
governorRequestRender('ais-vessels-placement');
|
// the fastest moved (as an angle seen from the camera, radians, and in
|
||||||
}, PLACEMENT_FRAME_MS);
|
// metres), the pass's wall time and the wall time since the pass before.
|
||||||
|
let motionPassWallMs = null;
|
||||||
|
const motionPass = {
|
||||||
|
camera: null,
|
||||||
|
wallMs: 0,
|
||||||
|
sinceMs: 0,
|
||||||
|
moving: false,
|
||||||
|
angle: 0,
|
||||||
|
metres: 0,
|
||||||
|
};
|
||||||
|
function startMotionPass() {
|
||||||
|
const wallMs = performance.now();
|
||||||
|
motionPass.camera = state.viewer?.camera ?? null;
|
||||||
|
motionPass.wallMs = wallMs;
|
||||||
|
motionPass.sinceMs =
|
||||||
|
motionPassWallMs === null ? 0 : wallMs - motionPassWallMs;
|
||||||
|
motionPass.moving = false;
|
||||||
|
motionPass.angle = 0;
|
||||||
|
motionPass.metres = 0;
|
||||||
|
motionPassWallMs = wallMs;
|
||||||
|
return motionPass;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Move a drawn visual, noting how far it moved as seen from the camera. */
|
||||||
|
function moveDrawnVisual(visual, lat, lon, motion) {
|
||||||
|
const from = Cesium.Cartesian3.clone(visual.position, _movedFrom);
|
||||||
|
moveRecordVisual(visual, lat, lon);
|
||||||
|
if (!visual.billboard?.show && !visual.modelOwnsVisual) return;
|
||||||
|
motion.moving = true;
|
||||||
|
const metres = Cesium.Cartesian3.distance(from, visual.position);
|
||||||
|
motion.metres = Math.max(motion.metres, metres);
|
||||||
|
const eye = motion.camera?.positionWC;
|
||||||
|
if (!eye) return;
|
||||||
|
const distance = Cesium.Cartesian3.distance(eye, from);
|
||||||
|
if (distance > 0) motion.angle = Math.max(motion.angle, metres / 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
|
||||||
|
// is due (the AIS runtime's clock).
|
||||||
|
let placementFrameTimer = null;
|
||||||
|
let placementFrameDueMs = 0;
|
||||||
|
/**
|
||||||
|
* While a drawn vessel moves, ask for the next frame when the fastest one
|
||||||
|
* will have moved VESSEL_STEP_PX on screen at the speed this pass saw, and
|
||||||
|
* at most PLACEMENT_FRAME_MS away.
|
||||||
|
*/
|
||||||
|
function askMotionFrame(motion) {
|
||||||
|
if (!motion.moving) return;
|
||||||
|
let waitMs = PLACEMENT_FRAME_MS;
|
||||||
|
const movedPx = movedPixels(motion);
|
||||||
|
if (movedPx > 0 && motion.sinceMs > 0)
|
||||||
|
waitMs = Math.min(waitMs, (VESSEL_STEP_PX * motion.sinceMs) / movedPx);
|
||||||
|
// A frame already asked for no later than this one stands.
|
||||||
|
const runtime = vesselState._aisRuntime;
|
||||||
|
const dueMs = runtime.now() + waitMs;
|
||||||
|
if (placementFrameTimer !== null) {
|
||||||
|
if (placementFrameDueMs <= dueMs) return;
|
||||||
|
runtime.clearTimeout(placementFrameTimer);
|
||||||
|
}
|
||||||
|
placementFrameDueMs = dueMs;
|
||||||
|
placementFrameTimer = runtime.setTimeout(() => {
|
||||||
|
placementFrameTimer = null;
|
||||||
|
if (state.feed.enabled) governorRequestRender('ais-vessels-placement');
|
||||||
|
}, waitMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Stop asking for placement frames (the layer turned off or went away). */
|
/** Stop asking for placement frames (the layer turned off or went away). */
|
||||||
function cancelPlacementFrame() {
|
function cancelPlacementFrame() {
|
||||||
placedAtWallMs = -Infinity;
|
motionPassWallMs = null;
|
||||||
placing = false;
|
|
||||||
if (placementFrameTimer === null) return;
|
if (placementFrameTimer === null) return;
|
||||||
vesselState._aisRuntime.clearTimeout(placementFrameTimer);
|
vesselState._aisRuntime.clearTimeout(placementFrameTimer);
|
||||||
placementFrameTimer = null;
|
placementFrameTimer = null;
|
||||||
@@ -879,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