mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 23:32:20 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f04308e45d | ||
|
|
9582f2f28b |
No files matched your search
@@ -0,0 +1,4 @@
|
|||||||
|
- ALPR camera badges no longer take depth samples while the overlay paints, or
|
||||||
|
again each time the layer is switched back on. On a terrain globe their
|
||||||
|
height comes from the loaded terrain and is remembered per camera; some
|
||||||
|
badges move a few metres onto the drawn surface (mazy1998, #1706).
|
||||||
@@ -1513,10 +1513,14 @@ tile streamed in; floors resolve before new badges appear (bounded wait), and a
|
|||||||
badge whose cell has not resolved clamps until it does, then moves in place.
|
badge whose cell has not resolved clamps until it does, then moves in place.
|
||||||
Overlay anchors start on the same floor and write marker properties only on
|
Overlay anchors start on the same floor and write marker properties only on
|
||||||
change. Once the camera is still and tiles have settled, each overlay anchor is
|
change. Once the camera is still and tiles have settled, each overlay anchor is
|
||||||
replaced once by the rendered surface under it (two depth samples per paint,
|
replaced once by the rendered surface under it (accepted only within -30/+40 m
|
||||||
accepted only within -30/+40 m of the floor cell), so badges do not float or
|
of the floor cell), so badges do not float or sink on slopes; wedge corners
|
||||||
sink on slopes; wedge corners follow their own floor cells instead of staying
|
follow their own floor cells instead of staying level with the apex. The
|
||||||
level with the apex. Unknown bearings never acquire a wedge. The selected
|
surface is read outside the overlay paint, one camera per frame: a terrain
|
||||||
|
globe answers from its loaded mesh (`globe.getHeight`) and only photoreal
|
||||||
|
tiles are depth-sampled (`scene.sampleHeight`). Answers are kept per camera
|
||||||
|
until the map source changes, so a refresh or turning the layer off and on
|
||||||
|
samples nothing again. Unknown bearings never acquire a wedge. The selected
|
||||||
card's `DIRECTION` wraps after rounding to whole degrees, so a mapped bearing
|
card's `DIRECTION` wraps after rounding to whole degrees, so a mapped bearing
|
||||||
of 359.5° or more reads `0°` rather than `360°`. The row reads
|
of 359.5° or more reads `0°` rather than `360°`. The row reads
|
||||||
`N nearby · M on screen`; when every loaded camera is outside the view it adds
|
`N nearby · M on screen`; when every loaded camera is outside the view it adds
|
||||||
|
|||||||
+139
-57
@@ -4,10 +4,10 @@ import {
|
|||||||
MARKER_ICON_SIZE,
|
MARKER_ICON_SIZE,
|
||||||
SELECTED_MARKER_ICON_SIZE,
|
SELECTED_MARKER_ICON_SIZE,
|
||||||
MARKER_FLOOR_LIFT_M,
|
MARKER_FLOOR_LIFT_M,
|
||||||
ANCHOR_SAMPLES_PER_PAINT,
|
REMEMBERED_ANCHORS,
|
||||||
SURFACE_BELOW_FLOOR_M,
|
SURFACE_BELOW_FLOOR_M,
|
||||||
SURFACE_ABOVE_FLOOR_M,
|
SURFACE_ABOVE_FLOOR_M,
|
||||||
SURFACE_SAMPLES_PER_PAINT,
|
SURFACE_SAMPLES_PER_FRAME,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
import {
|
import {
|
||||||
MARKER_IMAGE,
|
MARKER_IMAGE,
|
||||||
@@ -19,11 +19,17 @@ import {
|
|||||||
validAlprGroundHeight,
|
validAlprGroundHeight,
|
||||||
} from './visuals.js';
|
} from './visuals.js';
|
||||||
|
|
||||||
|
const nextAnimationFrame = (step) =>
|
||||||
|
typeof requestAnimationFrame === 'function'
|
||||||
|
? requestAnimationFrame(step)
|
||||||
|
: setTimeout(step, 16);
|
||||||
|
|
||||||
/** Per-layer presentation on a caller-owned overlay; Cesium remains the fallback. */
|
/** Per-layer presentation on a caller-owned overlay; Cesium remains the fallback. */
|
||||||
export function createAlprOverlay({
|
export function createAlprOverlay({
|
||||||
state,
|
state,
|
||||||
services,
|
services,
|
||||||
onSurfaceChange = () => {},
|
onSurfaceChange = () => {},
|
||||||
|
requestFrame = nextAnimationFrame,
|
||||||
}) {
|
}) {
|
||||||
let lane,
|
let lane,
|
||||||
removeMapListener,
|
removeMapListener,
|
||||||
@@ -33,7 +39,12 @@ export function createAlprOverlay({
|
|||||||
hits = [],
|
hits = [],
|
||||||
images = [],
|
images = [],
|
||||||
destroyed = false,
|
destroyed = false,
|
||||||
lastCamera = null;
|
lastCamera = null,
|
||||||
|
sampling = false;
|
||||||
|
// Rendered-surface answers by camera id. Entities are rebuilt by a refresh
|
||||||
|
// and by turning the layer off and on; keeping the answers on them meant
|
||||||
|
// every rebuild sampled the same cameras again.
|
||||||
|
const surfaceAnswers = new Map();
|
||||||
const requestPaint = () => {
|
const requestPaint = () => {
|
||||||
if (!destroyed && state.enabled) lane?.requestPaint();
|
if (!destroyed && state.enabled) lane?.requestPaint();
|
||||||
};
|
};
|
||||||
@@ -92,38 +103,72 @@ export function createAlprOverlay({
|
|||||||
entity.gevAlprDisplayPosition = null;
|
entity.gevAlprDisplayPosition = null;
|
||||||
nativeVisible(entity, true);
|
nativeVisible(entity, true);
|
||||||
}
|
}
|
||||||
|
surfaceAnswers.clear();
|
||||||
state.lastAnchorSampleAt = 0;
|
state.lastAnchorSampleAt = 0;
|
||||||
retryUntil = Date.now() + 15000;
|
retryUntil = Date.now() + 15000;
|
||||||
requestPaint();
|
requestPaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
function anchorFor(record, entity, budget) {
|
function rememberSurface(record, height, checked) {
|
||||||
if (entity.gevAlprCanvasPosition) return entity.gevAlprCanvasPosition;
|
surfaceAnswers.delete(record.id);
|
||||||
|
surfaceAnswers.set(record.id, {
|
||||||
|
latitude: record.latitude,
|
||||||
|
longitude: record.longitude,
|
||||||
|
height,
|
||||||
|
checked,
|
||||||
|
});
|
||||||
|
if (surfaceAnswers.size > REMEMBERED_ANCHORS)
|
||||||
|
surfaceAnswers.delete(surfaceAnswers.keys().next().value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rendered surface height under a camera. A terrain globe answers from its
|
||||||
|
* loaded mesh; only photoreal tiles need `sampleHeight`, which renders depth
|
||||||
|
* and compiles its shaders on first use (a freeze of up to 1.9 s at load).
|
||||||
|
*/
|
||||||
|
function surfaceHeight(record, entity) {
|
||||||
const scene = state.viewer.scene;
|
const scene = state.viewer.scene;
|
||||||
const location = Cesium.Cartographic.fromDegrees(
|
const location = Cesium.Cartographic.fromDegrees(
|
||||||
record.longitude,
|
record.longitude,
|
||||||
record.latitude,
|
record.latitude,
|
||||||
);
|
);
|
||||||
// A floor-placed marker anchors where its native badge sits; only a
|
if (scene.globe?.show) return scene.globe.getHeight?.(location);
|
||||||
// marker still waiting for its floor samples the rendered surface, and
|
if (!scene.sampleHeightSupported) return undefined;
|
||||||
// each paint samples a few at most (each sample is a depth render).
|
try {
|
||||||
|
return scene.sampleHeight(location, [entity]);
|
||||||
|
} catch {
|
||||||
|
/* streaming tiles */
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The anchor a paint can have without sampling, or null while it waits for one. */
|
||||||
|
function anchorFor(record, entity) {
|
||||||
|
if (entity.gevAlprCanvasPosition) return entity.gevAlprCanvasPosition;
|
||||||
|
const scene = state.viewer.scene;
|
||||||
|
const answer = surfaceAnswers.get(record.id);
|
||||||
|
if (
|
||||||
|
answer?.latitude === record.latitude &&
|
||||||
|
answer.longitude === record.longitude
|
||||||
|
) {
|
||||||
|
setAnchor(record, entity, answer.height);
|
||||||
|
if (answer.checked)
|
||||||
|
entity.gevAlprSurfaceCheckedFor = entity.gevAlprCanvasPosition;
|
||||||
|
return entity.gevAlprCanvasPosition;
|
||||||
|
}
|
||||||
|
// A floor-placed marker anchors where its native badge sits. A marker
|
||||||
|
// still waiting for its floor reads the terrain globe, or on photoreal
|
||||||
|
// tiles waits for sampleAnchors().
|
||||||
let height;
|
let height;
|
||||||
const floor = services.groundFloor.cachedGroundFloor(
|
const floor = services.groundFloor.cachedGroundFloor(
|
||||||
record.latitude,
|
record.latitude,
|
||||||
record.longitude,
|
record.longitude,
|
||||||
);
|
);
|
||||||
if (validAlprGroundHeight(floor)) height = floor + MARKER_FLOOR_LIFT_M;
|
if (validAlprGroundHeight(floor)) height = floor + MARKER_FLOOR_LIFT_M;
|
||||||
else if (scene.sampleHeightSupported) {
|
else if (scene.globe.show)
|
||||||
if (budget.samples <= 0) return null;
|
height = scene.globe.getHeight?.(
|
||||||
budget.samples -= 1;
|
Cesium.Cartographic.fromDegrees(record.longitude, record.latitude),
|
||||||
try {
|
);
|
||||||
height = scene.sampleHeight(location, [entity]);
|
|
||||||
} catch {
|
|
||||||
/* streaming tiles */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!validAlprGroundHeight(height) && scene.globe.show)
|
|
||||||
height = scene.globe.getHeight?.(location);
|
|
||||||
if (!validAlprGroundHeight(height)) return null;
|
if (!validAlprGroundHeight(height)) return null;
|
||||||
setAnchor(record, entity, height);
|
setAnchor(record, entity, height);
|
||||||
return entity.gevAlprCanvasPosition;
|
return entity.gevAlprCanvasPosition;
|
||||||
@@ -158,14 +203,19 @@ export function createAlprOverlay({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace floor anchors with the rendered surface under each overlay camera.
|
* Sample the rendered surface for the overlay cameras, outside the paint.
|
||||||
* Floors come from ~111 m cells, so on a slope a badge could float or sink
|
*
|
||||||
* by tens of metres. Runs only while the camera is still and tiles have
|
* A camera with no floor yet gets its anchor from a sample. An anchored one
|
||||||
* settled, a few depth samples per paint, and each anchor at most once;
|
* is checked against the surface once: floors come from ~111 m cells, so on
|
||||||
* samples far outside the floor prior (a roof or a streaming tile) are
|
* a slope a badge could float or sink by tens of metres. Checks run only
|
||||||
* rejected. Returns whether anchors remain to check.
|
* while the camera is still and tiles have settled, and samples far outside
|
||||||
|
* the floor prior (a roof or a streaming tile) are rejected. Each frame
|
||||||
|
* takes SURFACE_SAMPLES_PER_FRAME samples, so no frame carries the whole
|
||||||
|
* cohort; sampling inside paint() put up to six depth renders in one frame.
|
||||||
*/
|
*/
|
||||||
function refineAnchors(budget) {
|
function sampleAnchors() {
|
||||||
|
sampling = false;
|
||||||
|
if (destroyed || !state.enabled) return;
|
||||||
const camera = state.viewer.camera;
|
const camera = state.viewer.camera;
|
||||||
const scene = state.viewer.scene;
|
const scene = state.viewer.scene;
|
||||||
const still =
|
const still =
|
||||||
@@ -188,27 +238,41 @@ export function createAlprOverlay({
|
|||||||
position: Cesium.Cartesian3.clone(camera.positionWC),
|
position: Cesium.Cartesian3.clone(camera.positionWC),
|
||||||
direction: Cesium.Cartesian3.clone(camera.directionWC),
|
direction: Cesium.Cartesian3.clone(camera.directionWC),
|
||||||
};
|
};
|
||||||
if (!scene.sampleHeightSupported) return false;
|
const canSample = scene.globe?.show || scene.sampleHeightSupported;
|
||||||
let pending = false;
|
let samples = SURFACE_SAMPLES_PER_FRAME,
|
||||||
|
pending = false,
|
||||||
|
progressed = false,
|
||||||
|
moved = false;
|
||||||
|
// Cameras with no anchor come first: they are not on the overlay yet.
|
||||||
|
for (const record of records) {
|
||||||
|
const entity = state.dataSource.entities.getById(record.id);
|
||||||
|
if (!entity || entity.gevAlprCanvasPosition) continue;
|
||||||
|
// The floor may have resolved since the paint that found none.
|
||||||
|
if (anchorFor(record, entity)) moved = true;
|
||||||
|
else if (!canSample || samples <= 0) pending = true;
|
||||||
|
else {
|
||||||
|
samples -= 1;
|
||||||
|
const sample = surfaceHeight(record, entity);
|
||||||
|
if (validAlprGroundHeight(sample)) {
|
||||||
|
setAnchor(record, entity, sample);
|
||||||
|
rememberSurface(record, sample, false);
|
||||||
|
moved = progressed = true;
|
||||||
|
} else pending = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
const entity = state.dataSource.entities.getById(record.id);
|
const entity = state.dataSource.entities.getById(record.id);
|
||||||
const anchor = entity?.gevAlprCanvasPosition;
|
const anchor = entity?.gevAlprCanvasPosition;
|
||||||
if (!anchor || entity.gevAlprSurfaceCheckedFor === anchor) continue;
|
if (!anchor || !canSample || entity.gevAlprSurfaceCheckedFor === anchor)
|
||||||
if (!still || !surfaceSettled(scene) || budget.samples <= 0) {
|
continue;
|
||||||
|
if (!still || !surfaceSettled(scene) || samples <= 0) {
|
||||||
pending = true;
|
pending = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
budget.samples -= 1;
|
samples -= 1;
|
||||||
let sample;
|
progressed = true;
|
||||||
try {
|
const sample = surfaceHeight(record, entity);
|
||||||
sample = scene.sampleHeight(
|
let height = Cesium.Cartographic.fromCartesian(anchor).height;
|
||||||
Cesium.Cartographic.fromDegrees(record.longitude, record.latitude),
|
|
||||||
[entity],
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
/* streaming tiles */
|
|
||||||
}
|
|
||||||
const current = Cesium.Cartographic.fromCartesian(anchor).height;
|
|
||||||
const floor = services.groundFloor.cachedGroundFloor(
|
const floor = services.groundFloor.cachedGroundFloor(
|
||||||
record.latitude,
|
record.latitude,
|
||||||
record.longitude,
|
record.longitude,
|
||||||
@@ -218,21 +282,43 @@ export function createAlprOverlay({
|
|||||||
(!validAlprGroundHeight(floor) ||
|
(!validAlprGroundHeight(floor) ||
|
||||||
(sample >= floor - SURFACE_BELOW_FLOOR_M &&
|
(sample >= floor - SURFACE_BELOW_FLOOR_M &&
|
||||||
sample <= floor + SURFACE_ABOVE_FLOOR_M));
|
sample <= floor + SURFACE_ABOVE_FLOOR_M));
|
||||||
if (accepted && Math.abs(sample + MARKER_FLOOR_LIFT_M - current) > 1)
|
if (accepted && Math.abs(sample + MARKER_FLOOR_LIFT_M - height) > 1) {
|
||||||
setAnchor(record, entity, sample + MARKER_FLOOR_LIFT_M);
|
height = sample + MARKER_FLOOR_LIFT_M;
|
||||||
|
setAnchor(record, entity, height);
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
entity.gevAlprSurfaceCheckedFor = entity.gevAlprCanvasPosition;
|
entity.gevAlprSurfaceCheckedFor = entity.gevAlprCanvasPosition;
|
||||||
|
// A surface that did not answer is asked again after a rebuild.
|
||||||
|
if (validAlprGroundHeight(sample)) rememberSurface(record, height, true);
|
||||||
if (entity.id === state.selectedId)
|
if (entity.id === state.selectedId)
|
||||||
entity.gevAlprDisplayPosition = entity.gevAlprCanvasPosition;
|
entity.gevAlprDisplayPosition = entity.gevAlprCanvasPosition;
|
||||||
}
|
}
|
||||||
return pending;
|
if (moved) requestPaint();
|
||||||
|
if (!pending || Date.now() >= retryUntil) return;
|
||||||
|
// Take the next ration on the next frame; when nothing could be sampled
|
||||||
|
// (camera moving, tiles streaming, no floor yet) look again in 250 ms.
|
||||||
|
if (progressed) scheduleSampling();
|
||||||
|
else if (!retryTimer)
|
||||||
|
retryTimer = setTimeout(() => {
|
||||||
|
retryTimer = null;
|
||||||
|
scheduleSampling();
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleSampling() {
|
||||||
|
if (sampling || destroyed) return;
|
||||||
|
sampling = true;
|
||||||
|
requestFrame(sampleAnchors);
|
||||||
}
|
}
|
||||||
|
|
||||||
function paint({ ctx, width, height, keyhole, occluder }) {
|
function paint({ ctx, width, height, keyhole, occluder }) {
|
||||||
hits = [];
|
hits = [];
|
||||||
if (!state.enabled || destroyed) return;
|
if (!state.enabled || destroyed) return;
|
||||||
let unresolved = false;
|
let waiting = false;
|
||||||
const painted = [];
|
const painted = [];
|
||||||
const budget = { samples: ANCHOR_SAMPLES_PER_PAINT };
|
const canSample =
|
||||||
|
state.viewer.scene.globe?.show ||
|
||||||
|
state.viewer.scene.sampleHeightSupported;
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
const entity = state.dataSource.entities.getById(record.id);
|
const entity = state.dataSource.entities.getById(record.id);
|
||||||
if (!entity) continue;
|
if (!entity) continue;
|
||||||
@@ -246,12 +332,14 @@ export function createAlprOverlay({
|
|||||||
nativeVisible(entity, true);
|
nativeVisible(entity, true);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const anchor = anchorFor(record, entity, budget);
|
const anchor = anchorFor(record, entity);
|
||||||
if (!anchor) {
|
if (!anchor) {
|
||||||
nativeVisible(entity, true);
|
nativeVisible(entity, true);
|
||||||
unresolved = true;
|
waiting = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (canSample && entity.gevAlprSurfaceCheckedFor !== anchor)
|
||||||
|
waiting = true;
|
||||||
canvasOwned(entity, true);
|
canvasOwned(entity, true);
|
||||||
if (occluder && !occluder.isPointVisible(anchor)) {
|
if (occluder && !occluder.isPointVisible(anchor)) {
|
||||||
setShown(entity, false);
|
setShown(entity, false);
|
||||||
@@ -335,15 +423,9 @@ export function createAlprOverlay({
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
hits.push({ id: record.id, x: origin.x, y: origin.y, radius: size / 2 });
|
hits.push({ id: record.id, x: origin.x, y: origin.y, radius: size / 2 });
|
||||||
}
|
}
|
||||||
const refining = refineAnchors({
|
// Anchors still to find or to check are sampled after this paint, a few
|
||||||
samples: Math.min(budget.samples, SURFACE_SAMPLES_PER_PAINT),
|
// per frame, never inside it.
|
||||||
});
|
if (waiting && !retryTimer && Date.now() < retryUntil) scheduleSampling();
|
||||||
if ((unresolved || refining) && !retryTimer && Date.now() < retryUntil) {
|
|
||||||
retryTimer = setTimeout(() => {
|
|
||||||
retryTimer = null;
|
|
||||||
requestPaint();
|
|
||||||
}, 250);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -45,8 +45,10 @@ function harness(t, count = 2, { floor = null } = {}) {
|
|||||||
active,
|
active,
|
||||||
removed = 0,
|
removed = 0,
|
||||||
paints = 0;
|
paints = 0;
|
||||||
|
const frames = [];
|
||||||
const overlay = createAlprOverlay({
|
const overlay = createAlprOverlay({
|
||||||
state,
|
state,
|
||||||
|
requestFrame: (step) => frames.push(step),
|
||||||
services: {
|
services: {
|
||||||
groundFloor: { cachedGroundFloor: () => floor },
|
groundFloor: { cachedGroundFloor: () => floor },
|
||||||
overlays: {
|
overlays: {
|
||||||
@@ -106,6 +108,12 @@ function harness(t, count = 2, { floor = null } = {}) {
|
|||||||
entities,
|
entities,
|
||||||
draw,
|
draw,
|
||||||
paint: (occluder) => paint({ ctx, width: 800, height: 600, occluder }),
|
paint: (occluder) => paint({ ctx, width: 800, height: 600, occluder }),
|
||||||
|
/** Run the sampling step queued for the next animation frame, if any. */
|
||||||
|
frame() {
|
||||||
|
const step = frames.shift();
|
||||||
|
step?.();
|
||||||
|
return Boolean(step);
|
||||||
|
},
|
||||||
mapChanged() {
|
mapChanged() {
|
||||||
mapListener({ detail: { status: 'ready' } });
|
mapListener({ detail: { status: 'ready' } });
|
||||||
},
|
},
|
||||||
@@ -243,41 +251,149 @@ test('overlay glyphs and native badges shrink alike with distance', () => {
|
|||||||
assert.ok(Math.abs(markerScale(20_250) - 0.8) < 1e-9);
|
assert.ok(Math.abs(markerScale(20_250) - 0.8) < 1e-9);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a still view replaces floor anchors with the rendered surface, within the floor window', (t) => {
|
/** A photoreal scene: no globe, the rendered surface read by depth samples. */
|
||||||
// Floors come from ~111 m cells; on a slope a badge floated above the mesh.
|
function photoreal(h, heightAt = () => 172) {
|
||||||
const h = harness(t, 2, { floor: 150 });
|
|
||||||
const scene = h.state.viewer.scene;
|
const scene = h.state.viewer.scene;
|
||||||
const camera = h.state.viewer.camera;
|
h.state.viewer.camera.directionWC = Cesium.Cartesian3.UNIT_X;
|
||||||
camera.directionWC = Cesium.Cartesian3.UNIT_X;
|
scene.globe.show = false;
|
||||||
scene.sampleHeightSupported = true;
|
scene.sampleHeightSupported = true;
|
||||||
scene.primitives = { length: 0, get: () => null };
|
scene.primitives = { length: 0, get: () => null };
|
||||||
let samples = 0;
|
const sampled = [];
|
||||||
scene.sampleHeight = (carto) => {
|
scene.sampleHeight = (carto) => {
|
||||||
samples++;
|
sampled.push(carto);
|
||||||
return Cesium.Math.toDegrees(carto.latitude) > 30.00005 ? 400 : 172;
|
return heightAt(Cesium.Math.toDegrees(carto.latitude));
|
||||||
};
|
};
|
||||||
|
return sampled;
|
||||||
|
}
|
||||||
|
|
||||||
|
const anchorHeight = (h, id) =>
|
||||||
|
Cesium.Cartographic.fromCartesian(
|
||||||
|
h.entities.getById(id).gevAlprCanvasPosition,
|
||||||
|
).height;
|
||||||
|
|
||||||
|
/** Run sampling frames until none is queued, waiting out the 250 ms look-again. */
|
||||||
|
function settle(t, h) {
|
||||||
|
for (let i = 0; i < 400; i++) if (!h.frame()) t.mock.timers.tick(250);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('a still view replaces floor anchors with the rendered surface, within the floor window', (t) => {
|
||||||
|
// Floors come from ~111 m cells; on a slope a badge floated above the mesh.
|
||||||
|
t.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
|
||||||
|
const h = harness(t, 2, { floor: 150 });
|
||||||
|
const sampled = photoreal(h, (latitude) => (latitude > 30.00005 ? 400 : 172));
|
||||||
h.overlay.sync(h.records);
|
h.overlay.sync(h.records);
|
||||||
h.paint();
|
h.paint();
|
||||||
const height = (id) =>
|
assert.ok(
|
||||||
Cesium.Cartographic.fromCartesian(
|
Math.abs(anchorHeight(h, 'camera:0') - 151.5) < 0.01,
|
||||||
h.entities.getById(id).gevAlprCanvasPosition,
|
'floor anchor first',
|
||||||
).height;
|
);
|
||||||
|
assert.equal(h.frame(), true, 'the paint queued a sampling frame');
|
||||||
assert.equal(
|
assert.equal(
|
||||||
samples,
|
sampled.length,
|
||||||
0,
|
0,
|
||||||
'no depth samples while the camera may still be moving',
|
'no depth samples while the camera may still be moving',
|
||||||
);
|
);
|
||||||
assert.ok(Math.abs(height('camera:0') - 151.5) < 0.01, 'floor anchor first');
|
settle(t, h);
|
||||||
h.paint();
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
Math.abs(height('camera:0') - 173.5) < 0.01,
|
Math.abs(anchorHeight(h, 'camera:0') - 173.5) < 0.01,
|
||||||
'rendered surface once still',
|
'rendered surface once still',
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
Math.abs(height('camera:1') - 151.5) < 0.01,
|
Math.abs(anchorHeight(h, 'camera:1') - 151.5) < 0.01,
|
||||||
'a rooftop-height sample is rejected',
|
'a rooftop-height sample is rejected',
|
||||||
);
|
);
|
||||||
const taken = samples;
|
assert.equal(sampled.length, 2);
|
||||||
h.paint();
|
h.paint();
|
||||||
assert.equal(samples, taken, 'each anchor is checked once');
|
settle(t, h);
|
||||||
|
assert.equal(sampled.length, 2, 'each anchor is checked once');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a paint never samples the surface; frames take one sample each', (t) => {
|
||||||
|
// Sampling inside paint() put up to six depth renders into one frame, and
|
||||||
|
// the first of them compiles shaders: a freeze of 0.8 to 1.9 s at load.
|
||||||
|
t.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
|
||||||
|
const h = harness(t, 8);
|
||||||
|
const sampled = photoreal(h);
|
||||||
|
h.overlay.sync(h.records);
|
||||||
|
h.paint();
|
||||||
|
h.paint();
|
||||||
|
assert.equal(sampled.length, 0, 'no sample inside a paint');
|
||||||
|
assert.equal(h.draw.length, 0, 'unanchored cameras keep their native badge');
|
||||||
|
for (let taken = 1; taken <= 8; taken++) {
|
||||||
|
assert.equal(h.frame(), true);
|
||||||
|
assert.equal(sampled.length, taken, 'one sample per frame');
|
||||||
|
}
|
||||||
|
for (const record of h.records)
|
||||||
|
assert.ok(Math.abs(anchorHeight(h, record.id) - 172) < 0.01);
|
||||||
|
h.paint();
|
||||||
|
assert.ok(h.draw.length > 0, 'anchored cameras paint on the overlay');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a terrain globe is read from its mesh, never depth-sampled', (t) => {
|
||||||
|
t.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
|
||||||
|
const h = harness(t, 3, { floor: 150 });
|
||||||
|
const sampled = photoreal(h);
|
||||||
|
const scene = h.state.viewer.scene;
|
||||||
|
scene.globe.show = true;
|
||||||
|
scene.globe.tilesLoaded = true;
|
||||||
|
let reads = 0;
|
||||||
|
scene.globe.getHeight = () => {
|
||||||
|
reads++;
|
||||||
|
return 172;
|
||||||
|
};
|
||||||
|
h.overlay.sync(h.records);
|
||||||
|
h.paint();
|
||||||
|
settle(t, h);
|
||||||
|
assert.equal(sampled.length, 0);
|
||||||
|
assert.equal(reads, 3);
|
||||||
|
// The same anchor the depth sample gave: the surface plus the marker lift.
|
||||||
|
for (const record of h.records)
|
||||||
|
assert.ok(Math.abs(anchorHeight(h, record.id) - 173.5) < 0.01);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('surface answers outlive rebuilt markers and a re-enable, until the map changes', (t) => {
|
||||||
|
t.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
|
||||||
|
const h = harness(t, 4, { floor: 150 });
|
||||||
|
const sampled = photoreal(h);
|
||||||
|
h.overlay.sync(h.records);
|
||||||
|
h.paint();
|
||||||
|
settle(t, h);
|
||||||
|
assert.equal(sampled.length, 4);
|
||||||
|
const before = h.records.map((record) => anchorHeight(h, record.id));
|
||||||
|
|
||||||
|
// A refresh or a toggle removes the entities and builds new ones.
|
||||||
|
const rebuild = () => {
|
||||||
|
h.overlay.clear();
|
||||||
|
h.entities.removeAll();
|
||||||
|
for (const record of h.records)
|
||||||
|
h.entities.add({
|
||||||
|
id: record.id,
|
||||||
|
billboard: { image: 'fallback', show: true },
|
||||||
|
polyline: { show: true },
|
||||||
|
polygon: { show: true },
|
||||||
|
});
|
||||||
|
h.overlay.sync(h.records);
|
||||||
|
h.paint();
|
||||||
|
settle(t, h);
|
||||||
|
};
|
||||||
|
rebuild();
|
||||||
|
h.state.enabled = false;
|
||||||
|
h.overlay.clear();
|
||||||
|
h.state.enabled = true;
|
||||||
|
rebuild();
|
||||||
|
assert.equal(sampled.length, 4, 'no camera is sampled again');
|
||||||
|
assert.deepEqual(
|
||||||
|
h.records.map((record) => anchorHeight(h, record.id)),
|
||||||
|
before,
|
||||||
|
);
|
||||||
|
|
||||||
|
// A camera that moved is a different question.
|
||||||
|
h.records[0] = { ...h.records[0], latitude: h.records[0].latitude + 0.001 };
|
||||||
|
rebuild();
|
||||||
|
assert.equal(sampled.length, 5);
|
||||||
|
|
||||||
|
h.mapChanged();
|
||||||
|
h.paint();
|
||||||
|
settle(t, h);
|
||||||
|
assert.equal(sampled.length, 9, 'a new map surface is sampled afresh');
|
||||||
});
|
});
|
||||||
@@ -66,14 +66,15 @@ export const MAX_CANVAS_FRUSTUMS = 64;
|
|||||||
/** Metres a floor-placed marker sits above its resolved ground cell. */
|
/** Metres a floor-placed marker sits above its resolved ground cell. */
|
||||||
export const MARKER_FLOOR_LIFT_M = 1.5;
|
export const MARKER_FLOOR_LIFT_M = 1.5;
|
||||||
|
|
||||||
/** Rendered-surface samples one overlay paint may take (each is a depth render). */
|
/** Rendered-surface samples one frame may take (on photoreal tiles each is a
|
||||||
export const ANCHOR_SAMPLES_PER_PAINT = 4;
|
* depth render of several milliseconds, so they are spread over frames). */
|
||||||
|
export const SURFACE_SAMPLES_PER_FRAME = 1;
|
||||||
|
/** Surface answers kept by camera id, so a refresh or re-enable asks nothing. */
|
||||||
|
export const REMEMBERED_ANCHORS = 2048;
|
||||||
|
|
||||||
/** Accepted rendered-surface window around a floor cell (a cut or an embankment
|
/** Accepted rendered-surface window around a floor cell (a cut or an embankment
|
||||||
* sits within it; a rooftop or an unstreamed tile usually does not). */
|
* sits within it; a rooftop or an unstreamed tile usually does not). */
|
||||||
export const SURFACE_BELOW_FLOOR_M = 30;
|
export const SURFACE_BELOW_FLOOR_M = 30;
|
||||||
/** Rendered-surface refinements per paint while the camera is still. */
|
|
||||||
export const SURFACE_SAMPLES_PER_PAINT = 2;
|
|
||||||
export const SURFACE_ABOVE_FLOOR_M = 40;
|
export const SURFACE_ABOVE_FLOOR_M = 40;
|
||||||
|
|
||||||
/** OSM attribution may collapse after five seconds; full credit stays in Data attribution. */
|
/** OSM attribution may collapse after five seconds; full credit stays in Data attribution. */
|
||||||
|
|||||||
Reference in new issue
Block a user