Ask for heights through one service per scene

About 20 sites called scene.sampleHeight, globe.getHeight,
scene.clampToHeight or the MostDetailed variants directly, each with its own
idea of a miss (undefined, NaN, a throw). They now ask heightsFor(scene) in
src/queries/heights.js:

- now(point, { against, exclude }) answers from what is loaded, or null;
  against is 'surface' (what is drawn) or 'terrain' (loaded terrain only).
- clampNow(position) moves a position onto the drawn surface, or null.
- batch() and clampBatch() wait for the most detailed tiles: null per
  missed point, a rejection when the whole query fails.
- supported() reports { surface, clamp } without allocating.

Cesium still answers now() synchronously, so nothing changes on screen and
nothing is slower: in the live app 1,000 calls cost the same through the
service as direct (terrain 0.5-0.7 ms, surface 2-6 ms a call, run-to-run
noise larger than the difference). Every site already treated a miss as
"hold the last value or fall back"; a renderer that can only answer from a
cache or asynchronously can now plug in at one place.

- A Bhote Koshi evidence point whose terrain sample misses stays at its
  fallback elevation instead of reading the miss as sea level.
- The mesh floor sampler skips a scene without depth-texture support
  instead of spending its budget on probes that throw.
- The world annotation probe's trace reads hit/missed.
- A source scan forbids direct height calls outside the service.

The service is exported as ./queries/heights and documented in
docs/APPLICATION.md.
This commit is contained in:
Sameh Khamis committed 2026-11-04 09:24:30 -08:00
1 parent b51f1bf7c5
commit d51ea4007b
32 files changed
+704 -210

No files matched your search

+32
View File
@@ -131,6 +131,38 @@ reads true only while no hold is out. A caller-supplied `createScene` that creat
`defer(() => inputFor(viewer).destroy())` right after it; the standalone scene
does this and returns the dispatcher as `scene.input`.
## Height queries
`gods-eye-view/queries/heights` exports `heightsFor(scene)`, the one place
height queries go. Layers, tools and the camera ask it instead of calling
`scene.sampleHeight`, `globe.getHeight`, `scene.clampToHeight` or the
`*MostDetailed` variants themselves, so a renderer that answers heights another
way plugs in without touching them.
```js
import { heightsFor } from 'gods-eye-view/queries/heights';
const heights = heightsFor(viewer.scene);
const ground = heights.now({ lon, lat }, { exclude: [ownMarkers] });
if (ground !== null) place(ground);
```
| Member | What it answers |
| ----------------------------- | ------------------------------------------------------------------------- |
| `now(point, opts)` | Height in metres from what is loaded, or `null` |
| `clampNow(position, opts)` | The `Cartesian3` moved onto the drawn surface, or `null` |
| `batch(points, opts)` | A promise of heights once the most detailed tiles stream; `null` per miss |
| `clampBatch(positions, opts)` | A promise of clamped copies of the positions; `null` per miss |
| `supported()` | `{ surface, clamp }`: whether the drawn surface can be queried at all |
A point is `{ lon, lat }` in degrees or a `Cesium.Cartographic` in radians.
`against: 'surface'` (the default) reads what is drawn, 3D tiles and globe;
`against: 'terrain'` reads the loaded terrain tiles alone. `exclude` lists the
primitives and entities a query sees through. Every miss is `null`, including
a Cesium throw for missing depth-texture support, and every caller treats it
as one. Cesium answers `now()` synchronously from the scene, so it costs what
the direct call did.
## Standalone wiring
`src/main.js` reads the existing browser configuration and starts
+1
View File
@@ -229,6 +229,7 @@
"./application/operations": "./src/app/operations.js",
"./data/entity-records": "./src/data/entityRecords.js",
"./input": "./src/input/dispatcher.js",
"./queries/heights": "./src/queries/heights.js",
"./data/lifecycle": "./src/data/lifecycle.js",
"./ui/navigation": "./src/ui/navigationController.js",
"./ui/share-restoration": "./src/ui/shareRestoration.js",
+2
View File
@@ -47,6 +47,8 @@
"src/input/testDoubles.mjs",
"src/input/sensorPick.js",
"src/input/dispatcher.js",
"src/queries/heights.js",
"src/queries/heights.test.mjs",
"docs/CODE-BOUNDARIES.md",
"src/main.js",
"docs/APPLICATION.md",
+27 -11
View File
@@ -15,7 +15,8 @@
"src/input/dispatcher.js",
"src/input/sensorPick.js",
"src/data/infrastructureOverlayEntry.js",
"src/sources/infrastructureData.js"
"src/sources/infrastructureData.js",
"src/queries/heights.js"
],
"external": ["cesium"]
},
@@ -1859,7 +1860,8 @@
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json"
"src/i18n/locales/zh-Hant.json",
"src/queries/heights.js"
],
"external": ["cesium", "pbf", "@mapbox/vector-tile"]
},
@@ -1900,7 +1902,8 @@
"src/input/sensorPick.js",
"src/data/scenePick.js",
"src/data/pickRegistry.js",
"src/input/dispatcher.js"
"src/input/dispatcher.js",
"src/queries/heights.js"
],
"external": ["cesium"]
},
@@ -2046,7 +2049,8 @@
"src/data/scenePick.js",
"src/input/sensorPick.js",
"src/data/pickRegistry.js",
"src/input/dispatcher.js"
"src/input/dispatcher.js",
"src/queries/heights.js"
],
"external": ["cesium", "pbf", "@mapbox/vector-tile", "mapillary-js"]
},
@@ -2089,7 +2093,8 @@
"src/input/sensorPick.js",
"src/data/scenePick.js",
"src/data/pickRegistry.js",
"src/input/dispatcher.js"
"src/input/dispatcher.js",
"src/queries/heights.js"
],
"external": ["cesium", "@mapbox/vector-tile", "pbf"]
},
@@ -3327,7 +3332,8 @@
"src/data/local_data/wof_neighborhoods/files.js",
"src/data/entityRecords.js",
"src/input/sensorPick.js",
"src/input/dispatcher.js"
"src/input/dispatcher.js",
"src/queries/heights.js"
],
"external": [
"hls.js",
@@ -3952,7 +3958,8 @@
"src/data/local_data/wof_neighborhoods/files.js",
"src/data/entityRecords.js",
"src/input/sensorPick.js",
"src/input/dispatcher.js"
"src/input/dispatcher.js",
"src/queries/heights.js"
],
"external": [
"hls.js",
@@ -4039,7 +4046,8 @@
"src/data/placeBoundaries.js",
"src/data/placeNames.js",
"src/data/local_data/us_census_places/files.js",
"src/data/local_data/wof_neighborhoods/files.js"
"src/data/local_data/wof_neighborhoods/files.js",
"src/queries/heights.js"
],
"external": [
"cesium",
@@ -4647,7 +4655,8 @@
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/input/sensorPick.js",
"src/input/dispatcher.js"
"src/input/dispatcher.js",
"src/queries/heights.js"
],
"external": ["cesium", "egm96-universal", "pbf"]
},
@@ -4703,7 +4712,8 @@
"src/input/sensorPick.js",
"src/data/scenePick.js",
"src/data/pickRegistry.js",
"src/input/dispatcher.js"
"src/input/dispatcher.js",
"src/queries/heights.js"
],
"external": ["cesium", "pbf"]
},
@@ -4869,7 +4879,8 @@
"src/data/scenePick.js",
"src/input/sensorPick.js",
"src/data/pickRegistry.js",
"src/input/dispatcher.js"
"src/input/dispatcher.js",
"src/queries/heights.js"
],
"external": ["cesium"]
},
@@ -5101,5 +5112,10 @@
"src/i18n/marker.js"
],
"external": ["cesium"]
},
"heights": {
"exports": ["./queries/heights"],
"modules": ["src/queries/heights.js"],
"external": ["cesium"]
}
}
@@ -540,8 +540,9 @@ test('buildings share one probe per frame and preserve probe geometry, filtering
const detail = parent.detail;
assert.equal(detail.index, index);
assert.equal(detail.status, trace === firstTrace ? 'recorded' : 'missing');
// The fourth probe finds nothing and the fifth throws: both are misses.
assert.equal(detail.clampResult,
trace === firstTrace && index === 4 ? 'threw' : 'returned');
trace === firstTrace && index !== 3 && index !== 4 ? 'hit' : 'missed');
assert.ok(Number.isFinite(detail.clampStart) && Number.isFinite(detail.clampEnd));
assert.ok(parent.start <= detail.clampStart && detail.clampStart <= detail.clampEnd && detail.clampEnd <= parent.end);
if (index === 7) {
+11 -18
View File
@@ -45,6 +45,7 @@ import {
import { createOutlineRungs } from './outlineRungs.js';
import { unavailablePlaceSearch } from '../search/placeSearch.js';
import { isPickedWorldPosition } from '../data/scenePick.js';
import { heightsFor } from '../queries/heights.js';
/** Prepare only local dateline outlines for planar admission and centering. */
function localOutlineFrame(fp) {
@@ -2351,26 +2352,18 @@ export function createAnnotationResolver({
function sampleGroundHeight(viewer, lon, lat) {
const scene = viewer?.scene;
if (!scene) return 0;
try {
if (
scene.clampToHeightSupported &&
typeof scene.clampToHeight === 'function'
) {
const carto = Cesium.Cartographic.fromDegrees(lon, lat);
const surface = Cesium.Cartographic.toCartesian(carto);
const clamped = scene.clampToHeight(surface);
if (clamped) {
const h = Cesium.Cartographic.fromCartesian(clamped).height;
if (Number.isFinite(h)) return h;
}
const heights = heightsFor(scene);
// A miss is tiles not ready: fall through to the terrain, then 0.
if (heights.supported().clamp) {
const surface = Cesium.Cartesian3.fromDegrees(lon, lat, 0);
const clamped = heights.clampNow(surface);
if (clamped) {
const h = Cesium.Cartographic.fromCartesian(clamped)?.height;
if (Number.isFinite(h)) return h;
}
} catch {
/* tiles not ready — fall through */
}
const globeHeight = scene.globe?.getHeight?.(
Cesium.Cartographic.fromDegrees(lon, lat),
);
return Number.isFinite(globeHeight) && globeHeight > 0 ? globeHeight : 0;
const globeHeight = heights.now({ lon, lat }, { against: 'terrain' });
return globeHeight !== null && globeHeight > 0 ? globeHeight : 0;
}
function shortLabel(formattedAddress) {
+5 -7
View File
@@ -9,6 +9,7 @@ import { nextTick } from 'vue';
import { useScreenAnnotationsStore } from '../ui/stores/screenAnnotations.js';
import { applicationPinia } from '../ui/vueRuntime.js';
import { readEntityRecord } from '../data/entityRecords.js';
import { heightsFor } from '../queries/heights.js';
/** A label whose detailed outline could not be resolved. */
function outlineUnavailableLabel(label) {
@@ -142,13 +143,10 @@ export function createScreenAnnotationRenderer(
return cached.h;
}
try {
if (
scene.clampToHeightSupported &&
typeof scene.clampToHeight === 'function'
) {
const c = scene.clampToHeight(
Cesium.Cartesian3.fromDegrees(lon, lat, 0),
);
const heights = heightsFor(scene);
if (heights.supported().clamp) {
// A miss is tiles not ready.
const c = heights.clampNow(Cesium.Cartesian3.fromDegrees(lon, lat, 0));
if (c) {
const h = Cesium.Cartographic.fromCartesian(c).height;
if (Number.isFinite(h) && h > -430 && h < 9000) {
+4 -7
View File
@@ -2,6 +2,7 @@ import { recordWorkSlice } from '../sources/phaseTiming.js';
import { ringCentroid } from '../sources/featureGeometry.js';
import * as Cesium from 'cesium';
import { showOsmCredit, hideOsmCredit } from '../data/dataCredits.js';
import { heightsFor } from '../queries/heights.js';
/**
* World-space annotation renderer (Direction A).
@@ -174,20 +175,16 @@ export function createWorldAnnotationRenderer(viewer) {
const sliceStart = performance.now();
let error;
try {
if (
viewer.scene.clampToHeightSupported &&
typeof viewer.scene.clampToHeight === 'function'
) {
if (heightsFor(viewer.scene).supported().clamp) {
const [lon, lat] = task.points[task.index++];
timing.probes += 1;
try {
const position = Cesium.Cartesian3.fromDegrees(lon, lat, 0);
let c;
note?.('clampStart');
if (detail) detail.clampResult = 'threw';
try {
c = viewer.scene.clampToHeight(position);
if (detail) detail.clampResult = 'returned';
c = heightsFor(viewer.scene).clampNow(position);
if (detail) detail.clampResult = c ? 'hit' : 'missed';
} finally {
note?.('clampEnd');
}
+10 -11
View File
@@ -16,6 +16,7 @@
*/
import * as Cesium from 'cesium';
import { ownCameraArrival } from './data/cameraArrival.js';
import { heightsFor } from './queries/heights.js';
/** Minimum eye height above the rendered surface for a usable view. */
export const MIN_EYE_CLEARANCE_M = 120;
@@ -37,18 +38,16 @@ const probeScratch = new Cesium.Cartographic();
function sampleSurfaceM(scene, cells) {
let heightM = Number.NaN;
let sampled = 0;
if (typeof scene?.sampleHeight !== 'function') return { heightM, sampled };
if (!scene) return { heightM, sampled };
const heights = heightsFor(scene);
for (const cell of cells) {
try {
const height = scene.sampleHeight(
Cesium.Cartographic.fromDegrees(cell.lon, cell.lat, 0, probeScratch),
);
if (!Number.isFinite(height)) continue;
sampled += 1;
heightM = Number.isFinite(heightM) ? Math.max(heightM, height) : height;
} catch {
/* tiles not ready for this cell */
}
// A miss is a cell whose tiles are not ready.
const height = heights.now(
Cesium.Cartographic.fromDegrees(cell.lon, cell.lat, 0, probeScratch),
);
if (height === null) continue;
sampled += 1;
heightM = Number.isFinite(heightM) ? Math.max(heightM, height) : height;
}
return { heightM, sampled };
}
+10 -16
View File
@@ -29,6 +29,7 @@ import {
holdContinuousRender,
releaseContinuousRender,
} from './renderGovernor.js';
import { heightsFor } from './queries/heights.js';
let _isCockpitActive = null;
/** °/s by speed word — orbit; pan uses fractions of view height/s. */
@@ -559,25 +560,18 @@ function acquireCorridorFloor(state, takeWhole) {
export function probeMeshFloorM(scene, cells) {
const requested = Array.isArray(cells) ? cells.length : 0;
const empty = { heightM: Number.NaN, sampled: 0, requested };
if (!scene || typeof scene.sampleHeight !== 'function' || !requested)
return empty;
if (!scene || !requested) return empty;
const heights = heightsFor(scene);
let heightM = Number.NaN;
let sampled = 0;
for (const cell of cells) {
try {
const carto = Cesium.Cartographic.fromDegrees(
cell.lon,
cell.lat,
0,
_probeCarto,
);
const height = scene.sampleHeight(carto);
if (!Number.isFinite(height)) continue;
sampled += 1;
heightM = Number.isFinite(heightM) ? Math.max(heightM, height) : height;
} catch {
/* tiles not ready for this cell */
}
// A miss is a cell whose tiles are not ready.
const height = heights.now(
Cesium.Cartographic.fromDegrees(cell.lon, cell.lat, 0, _probeCarto),
);
if (height === null) continue;
sampled += 1;
heightM = Number.isFinite(heightM) ? Math.max(heightM, height) : height;
}
return { heightM, sampled, requested };
}
+13 -22
View File
@@ -28,6 +28,7 @@ import { createImagerySplit } from '../ui/imagerySplit.js';
import { formatDate, t, tn, msg, onLocaleChange } from '../i18n/index.js';
import { useBhoteKoshiPanelStore } from '../ui/stores/bhoteKoshiPanel.js';
import { applicationPinia } from '../ui/vueRuntime.js';
import { heightsFor } from '../queries/heights.js';
export const BHOTE_KOSHI_LAYER_ID = 'bhote-koshi-2026';
export const BHOTE_KOSHI_OVERLAY_SOURCE_ID = 'bhote-koshi-witnesses';
@@ -122,20 +123,11 @@ const DEFAULT_EVENT_LOADER = async () => {
const DEFAULT_IMAGERY_PROVIDER_FACTORY = (url, options) =>
Cesium.SingleTileImageryProvider.fromUrl(url, options);
const DEFAULT_TERRAIN_SAMPLER = async (viewer, observations) => {
if (
!viewer?.terrainProvider ||
!viewer.terrainProvider.availability ||
typeof Cesium.sampleTerrainMostDetailed !== 'function'
) {
return null;
}
const cartographics = observations.map((observation) =>
Cesium.Cartographic.fromDegrees(observation.lon, observation.lat),
);
return Cesium.sampleTerrainMostDetailed(
viewer.terrainProvider,
cartographics,
);
if (!viewer?.scene || !viewer.terrainProvider?.availability) return null;
const heights = await heightsFor(viewer.scene).batch(observations, {
against: 'terrain',
});
return heights.map((height) => ({ height }));
};
const DEFAULT_MEDIA_LOADER = async (url, { signal } = {}) => {
if (typeof createImageBitmap !== 'function') {
@@ -1197,7 +1189,8 @@ export function createBhoteKoshiEventLayer({
}
throwIfEnableCancelled(signal, _enabled);
return observations.map((observation, index) => {
const sampledHeight = Number(sampled?.[index]?.height);
// A miss is null or undefined, never sea level.
const sampledHeight = Number(sampled?.[index]?.height ?? Number.NaN);
const fallbackHeight = Number(observation.fallbackElevationM) || 0;
return Cesium.Cartesian3.fromDegrees(
observation.lon,
@@ -1573,14 +1566,12 @@ export function createBhoteKoshiEventLayer({
// a single high sample can make the front dive backward in screen space.
if (_sceneControls.evidencePathElevation !== 'source') {
// Sample once while preparing the shot, never from an animation frame.
try {
height = _viewer?.scene?.sampleHeight?.(
position,
[_floodCore, _floodHalo, _surgePoint].filter(Boolean),
);
} catch {}
const heights = _viewer?.scene ? heightsFor(_viewer.scene) : null;
height = heights?.now(position, {
exclude: [_floodCore, _floodHalo, _surgePoint].filter(Boolean),
});
if (!Number.isFinite(height))
height = _viewer?.scene?.globe?.getHeight?.(position);
height = heights?.now(position, { against: 'terrain' });
}
if (!Number.isFinite(height)) height = point.fallbackElevationM;
return Cesium.Cartesian3.fromDegrees(point.lon, point.lat, height + 8);
+40
View File
@@ -2992,3 +2992,43 @@ test('Bhote Koshi corridor remains ordered downstream and inside the imagery rec
priorChainage = point.chainageM;
}
});
test('an evidence point whose terrain sample misses sits at its fallback elevation, not sea level', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
installEventDom();
globalThis.window = { setTimeout: () => 71, clearTimeout() {} };
const viewer = eventViewer();
const publications = [];
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
terrainSampler: async (_viewer, observations) => observations.map(() => ({ height: null })),
mediaLoader: async () => {
const error = new Error('fixture media intentionally unavailable');
error.name = 'AbortError';
throw error;
},
overlayHost: { setEntries(_source, entries) { publications.push(entries); }, setVisible() {}, clearSource() {} },
renderHost: { request() {}, hold() {}, release() {} },
});
try {
layer.setParams({
presentation: 'scene-beat',
sceneSurface: 'evidence-beat',
beatId: 'gyirong-border-gate',
}, { origin: 'scene' });
await layer.enable(viewer, { origin: 'scene' });
const card = publications.at(-1).find((entry) => entry.id === 'evidence-card-gyirong-border-gate');
const fallback = event.evidenceSpine.find(({ id }) => id === 'gyirong-border-gate').fallbackElevationM;
assert.ok(fallback > 100, 'the fixture has a real fallback elevation');
const height = Cesium.Cartographic.fromCartesian(card.position()).height;
assert.ok(Math.abs(height - (fallback + 18)) < 1, `card at ${height} m`);
await layer.disable();
} finally {
await layer.destroy();
globalThis.document = originalDocument;
globalThis.window = originalWindow;
}
});
+4 -2
View File
@@ -3,6 +3,7 @@ import * as Cesium from 'cesium';
import { createImportImageBudget } from './geoImportImages.js';
import { createImportedPins } from './geoImportPins.js';
import { msg } from '../i18n/marker.js';
import { heightsFor } from '../queries/heights.js';
/** Largest file accepted, in bytes. */
const MAX_IMPORT_BYTES = 50 * 1024 * 1024;
@@ -1272,10 +1273,11 @@ async function settleGroundPins(dataSource, viewer, signal) {
// a buried starting point can make the downward mesh ray miss the surface.
const ellipsoid =
viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider;
const heights = viewer.scene ? heightsFor(viewer.scene) : null;
for (let i = 0; i < positions.length; i++) {
const sampled = ellipsoid
? 0
: viewer.scene?.globe?.getHeight?.(positions[i]);
: heights?.now(positions[i], { against: 'terrain' });
positions[i].height = mesh
? (batch[i].groundHeight ??
Math.max(0, Number.isFinite(sampled) ? sampled : 0))
@@ -1295,7 +1297,7 @@ async function settleGroundPins(dataSource, viewer, signal) {
indices.push(i);
if (indices.length) {
const resolved = await boundedImport(
viewer.scene.clampToHeightMostDetailed(
heights.clampBatch(
indices.map((i) => {
const c = positions[i];
return Cesium.Cartesian3.fromRadians(
+37
View File
@@ -1262,3 +1262,40 @@ test('KML namespace repair ignores declaration text inside quoted attribute valu
/note="xmlns:xsi='x'" xmlns:xsi="http:\/\/www.w3.org\/2001\/XMLSchema-instance">/,
);
});
test('a failed mesh clamp fails the import instead of leaving pins at sea level', async () => {
const previous = Cesium.PinBuilder.prototype.fromColor;
Cesium.PinBuilder.prototype.fromColor = () => ({
tagName: 'CANVAS',
toDataURL: () => 'data:image/png;base64,local',
});
try {
const viewer = {
terrainProvider: new Cesium.EllipsoidTerrainProvider(),
camera: {
positionCartographic: { height: 100000 },
computeViewRectangle: () => Cesium.Rectangle.MAX_VALUE,
},
scene: {
frameState: { mode: Cesium.SceneMode.SCENE3D },
globe: { show: false, getHeight: () => 0 },
clampToHeightSupported: true,
clampToHeightMostDetailed: async () => {
throw new Error('tile request failed');
},
},
};
await assert.rejects(
loadGeoFile(
new File(
[JSON.stringify({ type: 'Point', coordinates: [1, 2] })],
'surface.geojson',
),
{ viewer },
),
/Ground heights could not be read/,
);
} finally {
Cesium.PinBuilder.prototype.fromColor = previous;
}
});
+1
View File
@@ -187,6 +187,7 @@ test('consumer build includes only infrastructure code and resolves assets under
const sources = Object.keys(entry.modules).filter((id) => id.endsWith('.js'));
assert.deepEqual(sources.map((id) => id.split('/').at(-1)).sort(), [
'dispatcher.js',
'heights.js',
'infrastructure.js',
'infrastructureData.js',
'infrastructureOverlayEntry.js',
+12 -15
View File
@@ -11,6 +11,7 @@ import {
applyInfraEvictionGrace,
shouldRecomputeInfraLod,
} from './localGeojsonLod.js';
import { heightsFor } from '../queries/heights.js';
const DEFAULT_LABEL_MAX = 900;
const DEFAULT_LABEL_GRID_PX = 132;
@@ -1114,9 +1115,11 @@ function refreshLocalTerrainFloor(viewer, record) {
// Camera motion can refine terrain after the first successful depth sample.
// Reuse the loaded mesh on existing bounded walks; never arm a timer or do
// another GPU readback just to maintain this floor. Keep roof elevations.
const height = globe.getHeight?.(record.carto);
const height = heightsFor(viewer.scene).now(record.carto, {
against: 'terrain',
});
if (
!Number.isFinite(height) ||
height === null ||
Math.abs(height) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M ||
height <= record.groundHeight
)
@@ -1131,26 +1134,20 @@ function sampleLocalGroundHeight(viewer, record, now) {
record.lastGroundSampleMs = now;
const globe = viewer.scene.globe;
if (globe?.show && globe.tilesLoaded === false) return false;
let sampled;
try {
sampled = viewer.scene.sampleHeight(record.carto, [record.entity]);
} catch {
return false; // tiles not ready; retry on a later bounded update
}
if (
!Number.isFinite(sampled) ||
Math.abs(sampled) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M
)
const heights = heightsFor(viewer.scene);
// A miss is tiles not ready: retry on a later bounded update.
let sampled = heights.now(record.carto, { exclude: [record.entity] });
if (sampled === null || Math.abs(sampled) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M)
return false;
// A coarse scene-depth sample can lie below the terrain already loaded by
// the visible globe. Keep that terrain as a floor while allowing roofs and
// valid below-sea-level elevations. Photoreal scenes hide the globe, so its
// inactive terrain must not constrain their geometry.
const terrainHeight = globe?.show
? globe.getHeight?.(record.carto)
: undefined;
? heights.now(record.carto, { against: 'terrain' })
: null;
if (
Number.isFinite(terrainHeight) &&
terrainHeight !== null &&
Math.abs(terrainHeight) <= GROUND_SAMPLE_MAX_ABS_HEIGHT_M
) {
sampled = Math.max(sampled, terrainHeight);
+16
View File
@@ -55,6 +55,7 @@ function fakeScene(sampleHeight, { cameraHeightM = 900, tilesLoaded = true, show
Object.defineProperty(tileset, 'tilesLoaded', { value: tilesLoaded, configurable: true });
Object.defineProperty(tileset, 'show', { value: show, configurable: true });
return {
sampleHeightSupported: true,
sampleHeight,
camera: { positionCartographic: { height: cameraHeightM } },
primitives: { length: 1, get: () => tileset },
@@ -137,3 +138,18 @@ test('sampler: a cell just across the antimeridian is still near the viewer', ()
sampleMeshFloorCells(scene, [{ lat: -16.81, lon: 179.999 }], { viewerLat: -16.81, viewerLon: -179.999 });
assert.equal(calls, 2, 'about 200 m apart, so both cells are probed');
});
test('sampler: a scene without depth-texture support is never probed', async () => {
reset();
const cell = nextSite();
await seedDem(cell, 120.0);
let calls = 0;
const scene = fakeScene(() => {
calls++;
throw new Error('depth textures unsupported');
});
scene.sampleHeightSupported = false;
sampleMeshFloorCells(scene, [cell], { viewerLat: cell.lat, viewerLon: cell.lon });
assert.equal(calls, 0);
assert.equal(cachedMeshFloor(cell.lat, cell.lon), null);
});
+9 -15
View File
@@ -19,6 +19,7 @@ import {
validAlprGroundHeight,
} from './visuals.js';
import { alprMarkers } from './markerState.js';
import { heightsFor } from '../../queries/heights.js';
/** Per-layer presentation on a caller-owned overlay; Cesium remains the fallback. */
export function createAlprOverlay({
@@ -121,14 +122,11 @@ export function createAlprOverlay({
else if (scene.sampleHeightSupported) {
if (budget.samples <= 0) return null;
budget.samples -= 1;
try {
height = scene.sampleHeight(location, [entity]);
} catch {
/* streaming tiles */
}
// A miss is tiles still streaming.
height = heightsFor(scene).now(location, { exclude: [entity] });
}
if (!validAlprGroundHeight(height) && scene.globe.show)
height = scene.globe.getHeight?.(location);
height = heightsFor(scene).now(location, { against: 'terrain' });
if (!validAlprGroundHeight(height)) return null;
setAnchor(record, entity, height);
return alprMarkers.read(entity)?.canvasPosition;
@@ -202,15 +200,11 @@ export function createAlprOverlay({
continue;
}
budget.samples -= 1;
let sample;
try {
sample = scene.sampleHeight(
Cesium.Cartographic.fromDegrees(record.longitude, record.latitude),
[entity],
);
} catch {
/* streaming tiles */
}
// A miss is tiles still streaming.
const sample = heightsFor(scene).now(
{ lon: record.longitude, lat: record.latitude },
{ exclude: [entity] },
);
const current = Cesium.Cartographic.fromCartesian(anchor).height;
const floor = services.groundFloor.cachedGroundFloor(
record.latitude,
+13 -18
View File
@@ -25,6 +25,7 @@ import {
} from './visuals.js';
import { entityRecord, readEntityRecord } from '../../data/entityRecords.js';
import { alprMarkers } from './markerState.js';
import { heightsFor } from '../../queries/heights.js';
export function createAlprPresentation({ state, services, source }) {
const { governorRequestRender } = services.render;
@@ -518,16 +519,13 @@ export function createAlprPresentation({ state, services, source }) {
record.longitude,
record.latitude,
);
let height;
if (state.viewer.scene.sampleHeightSupported) {
try {
height = state.viewer.scene.sampleHeight(location, [nearest]);
} catch {
/* tiles may still be streaming */
}
}
const heights = heightsFor(state.viewer.scene);
// A miss is tiles still streaming.
let height = state.viewer.scene.sampleHeightSupported
? heights.now(location, { exclude: [nearest] })
: null;
if (!validAlprGroundHeight(height) && state.viewer.scene.globe.show)
height = state.viewer.scene.globe.getHeight?.(location);
height = heights.now(location, { against: 'terrain' });
if (!validAlprGroundHeight(height))
height = cachedGroundFloor(record.latitude, record.longitude);
const center = Cesium.Cartesian3.fromDegrees(
@@ -598,16 +596,13 @@ export function createAlprPresentation({ state, services, source }) {
record.longitude,
record.latitude,
);
let height;
if (scene.sampleHeightSupported) {
try {
height = scene.sampleHeight(location, [entity]);
} catch {
/* tiles may still be streaming */
}
}
const heights = heightsFor(scene);
// A miss is tiles still streaming.
let height = scene.sampleHeightSupported
? heights.now(location, { exclude: [entity] })
: null;
if (!validAlprGroundHeight(height) && scene.globe.show)
height = scene.globe.getHeight?.(location);
height = heights.now(location, { against: 'terrain' });
if (!validAlprGroundHeight(height))
height = cachedGroundFloor(record.latitude, record.longitude);
if (!validAlprGroundHeight(height)) return;
+5 -6
View File
@@ -12,6 +12,7 @@ import {
MAX_TOTAL_POINTS,
COLOR_OUTLINE,
} from './policy.js';
import { heightsFor } from '../../queries/heights.js';
export function createRendering({
state: layerState,
@@ -100,12 +101,10 @@ export function createRendering({
let height = POINT_HEIGHT_OFFSET_M;
// Sample terrain height so points sit on ground rather than at ellipsoid level
if (layerState._viewer?.scene?.sampleHeightSupported) {
const carto = Cesium.Cartographic.fromDegrees(lon, lat);
const sampled = layerState._viewer.scene.sampleHeight(carto);
if (Number.isFinite(sampled)) {
height = sampled + POINT_HEIGHT_OFFSET_M;
}
const scene = layerState._viewer?.scene;
if (scene?.sampleHeightSupported) {
const sampled = heightsFor(scene).now({ lon, lat });
if (sampled !== null) height = sampled + POINT_HEIGHT_OFFSET_M;
}
return Cesium.Cartesian3.fromDegrees(lon, lat, height);
+3 -1
View File
@@ -1,5 +1,6 @@
import * as Cesium from 'cesium';
import { plausibleSurfaceHeight, sampledSurfaceHeight } from './view.js';
import { heightsFor } from '../../queries/heights.js';
/** Fly the globe camera once to frame a photo after it opens. */
export function createCameraFraming({ state }) {
@@ -30,7 +31,8 @@ export function createCameraFraming({ state }) {
const scene = state.viewer?.scene;
const carto = Cesium.Cartographic.fromDegrees(lon, lat);
let height = sampledSurfaceHeight(scene, carto);
if (height === null) height = scene?.globe?.getHeight?.(carto);
if (height === null && scene)
height = heightsFor(scene).now(carto, { against: 'terrain' });
if (!plausibleSurfaceHeight(height)) height = fallback;
return plausibleSurfaceHeight(height) ? height : 0;
}
+10 -2
View File
@@ -799,7 +799,11 @@ test('under Google 3D the ground is measured again once its tiles load, without
const viewer = fakeViewer();
// 4 km up the ellipsoid, 2 km over a city whose tiles are still loading.
let ground;
viewer.camera.positionCartographic = { height: 4000 };
viewer.camera.positionCartographic = {
longitude: 0,
latitude: 0,
height: 4000,
};
viewer.scene.globe = { show: false };
viewer.scene.sampleHeightSupported = true;
viewer.scene.sampleHeight = () => ground;
@@ -823,7 +827,11 @@ test('an unknown ground is measured only a few times, and not after the layer is
t.mock.timers.enable({ apis: ['setTimeout'] });
const viewer = fakeViewer();
let samples = 0;
viewer.camera.positionCartographic = { height: 4000 };
viewer.camera.positionCartographic = {
longitude: 0,
latitude: 0,
height: 4000,
};
viewer.scene.globe = { show: false };
viewer.scene.sampleHeightSupported = true;
viewer.scene.sampleHeight = () => {
+6 -8
View File
@@ -1,5 +1,6 @@
import * as Cesium from 'cesium';
import { wrapLon } from './tileMath.js';
import { heightsFor } from '../../queries/heights.js';
/** Equirectangular metres between {lon, lat} points, the short way round ±180°; street scale. */
export function metresBetween(a, b) {
@@ -21,12 +22,9 @@ export function plausibleSurfaceHeight(height) {
* underground before tiles load).
*/
export function sampledSurfaceHeight(scene, carto) {
let height = null;
try {
if (scene?.sampleHeightSupported) height = scene.sampleHeight(carto);
} catch {
/* not sampleable yet */
}
const height = scene?.sampleHeightSupported
? heightsFor(scene).now(carto)
: null;
return plausibleSurfaceHeight(height) ? height : null;
}
@@ -41,8 +39,8 @@ export function groundUnderCamera(viewer) {
if (!carto || !scene) return null;
const globe = scene.globe;
if (globe && globe.show !== false) {
const ground = globe.getHeight?.(carto);
if (Number.isFinite(ground)) return ground;
const ground = heightsFor(scene).now(carto, { against: 'terrain' });
if (ground !== null) return ground;
}
const sampled = sampledSurfaceHeight(scene, carto);
return sampled !== null && sampled < carto.height ? sampled : null;
+10 -10
View File
@@ -12,6 +12,7 @@ import {
TILE_CACHE_MAX_ENTRIES,
FAST_FETCH_ALTITUDE,
} from './policy.js';
import { heightsFor } from '../../queries/heights.js';
/** Retain complete parsed snapshots under both an LRU entry cap and a byte budget. */
export function cacheRoadSnapshot(
@@ -273,16 +274,15 @@ export function createIngestion({
const scene = layerState._viewer?.scene;
const location = Cesium.Cartographic.fromDegrees(centerLon, centerLat);
let estimated = services.ground?.cachedGroundFloor?.(centerLat, centerLon);
if (!Number.isFinite(estimated)) {
try {
estimated = scene?.globe?.show
? scene.globe.getHeight?.(location)
: scene?.sampleHeightSupported
? scene.sampleHeight(location, [layerState._pointCollection])
: undefined;
} catch {
/* the local mesh can still be loading */
}
// A miss is the local mesh still loading.
if (!Number.isFinite(estimated) && scene) {
estimated = scene.globe?.show
? heightsFor(scene).now(location, { against: 'terrain' })
: scene.sampleHeightSupported
? heightsFor(scene).now(location, {
exclude: [layerState._pointCollection],
})
: undefined;
}
if (Number.isFinite(estimated) && Math.abs(estimated) <= 9000)
layerState._viewHeightEstimate = estimated;
+5 -4
View File
@@ -1,6 +1,7 @@
import { phaseTiming } from '../../sources/phaseTiming.js';
import * as Cesium from 'cesium';
import { DOT_HEIGHT_OFFSET, MAX_WAYPOINTS_PER_ROAD } from './policy.js';
import { heightsFor } from '../../queries/heights.js';
function surfaceTilesReady(scene) {
if (scene.globe?.show) return scene.globe.tilesLoaded !== false;
@@ -333,7 +334,8 @@ export async function prepareRoadSurfaces(
let sampled;
// sampleHeight reads the locally rendered mesh, not unresolved/offscreen
// tiles. Global tilesLoaded can stay false while this street is usable.
if (scene.globe?.show) sampled = scene.globe.getHeight?.(carto);
if (scene.globe?.show)
sampled = heightsFor(scene).now(carto, { against: 'terrain' });
else {
const estimate = Cesium.Cartographic.fromCartesian(
road.waypoints[i],
@@ -351,9 +353,8 @@ export async function prepareRoadSurfaces(
const shown = (excluded || []).filter((p) => p?.show);
for (const primitive of shown) primitive.show = false;
try {
sampled = scene.sampleHeight(carto, excluded);
} catch {
/* local mesh missing */
// A miss is the local mesh still missing.
sampled = heightsFor(scene).now(carto, { exclude: excluded });
} finally {
for (const primitive of shown) primitive.show = true;
}
+7 -4
View File
@@ -7,6 +7,7 @@ import {
HEIGHT_SAMPLE_MAX_ALTITUDE_M,
withinViewBounds,
} from './policy.js';
import { heightsFor } from '../../queries/heights.js';
/** Surface preparation off the render path, shared by markers and history. */
export function createHeight({ state, services, parts }) {
@@ -63,14 +64,16 @@ export function createHeight({ state, services, parts }) {
return { cell, height: fixtureFloor, prior: null };
// On a terrain globe, use the loaded surface under the marker. This is
// synchronous cache work and does not launch a terrain request.
const globe = state._viewer?.scene?.globe;
if (globe?.show === true) {
const scene = state._viewer?.scene;
if (scene?.globe?.show === true) {
Cesium.Cartographic.fromDegrees(lon, lat, 0, terrainPoint);
const rendered = globe.getHeight?.(terrainPoint);
const rendered = heightsFor(scene).now(terrainPoint, {
against: 'terrain',
});
const flat =
state._viewer?.terrainProvider instanceof
Cesium.EllipsoidTerrainProvider;
if (Number.isFinite(rendered) || flat)
if (rendered !== null || flat)
return {
cell,
height: (flat ? 0 : rendered) + GROUND_FLOOR_LIFT_M,
+4 -1
View File
@@ -12,6 +12,7 @@ import { unavailablePlaceSearch } from './search/placeSearch.js';
import { cancelCameraArrival } from './data/cameraArrival.js';
import { guardCameraAboveGround } from './cameraGroundGuard.js';
import { CITY_POIS } from './cityPois.js';
import { heightsFor } from './queries/heights.js';
/** Newest landmark arrival; an older post-arrival ground guard yields to it. */
let arrivalGeneration = 0;
@@ -109,7 +110,9 @@ export function flyToLandmark(viewer, lat, lon, options = {}) {
// Sample terrain height (sync — uses loaded tiles; 0 if globe/terrain not ready)
const targetCartographic = Cesium.Cartographic.fromDegrees(lon, lat);
const sampledHeight = viewer.scene.globe?.getHeight(targetCartographic);
const sampledHeight = heightsFor(viewer.scene).now(targetCartographic, {
against: 'terrain',
});
// Use sampled height if available, otherwise the cached ground-floor
// elevation (same ellipsoidal datum), then the pre-baked city elevation.
+161
View File
@@ -0,0 +1,161 @@
/**
* @module queries/heights
* @description The one place height queries go: how high the rendered
* surface or the terrain is under a point, now or after streaming.
*
* `now()` answers from what is already loaded and may return `null`: tiles
* not streamed yet, no depth support, a point off screen. Every caller treats
* that as a miss, holding its last value or placing first and patching when
* `batch()` resolves. Cesium answers `now()` synchronously from the scene,
* so nothing changes on screen; a renderer that can only answer from a cache
* or asynchronously plugs in here without touching the callers.
*
* `against` picks the surface: `'surface'` is what is drawn (3D tiles and the
* globe), `'terrain'` the loaded terrain tiles alone. `exclude` lists what the
* query must see through, typically the caller's own markers.
*/
import * as Cesium from 'cesium';
/** @typedef {'surface'|'terrain'} HeightSurface */
/**
* A point in degrees, or a `Cesium.Cartographic` in radians.
* @typedef {{lon: number, lat: number}|Cesium.Cartographic} HeightPoint
*/
/**
* @typedef {object} HeightOptions
* @property {HeightSurface} [against] Default `'surface'`.
* @property {object[]} [exclude] Primitives and entities to see through.
*/
/** @type {WeakMap<object, HeightService>} scene -> service */
const services = new WeakMap();
const scratch = new Cesium.Cartographic();
/** `supported()` answers, shared so a per-frame check allocates nothing. */
const SUPPORT = [
[
Object.freeze({ surface: false, clamp: false }),
Object.freeze({ surface: false, clamp: true }),
],
[
Object.freeze({ surface: true, clamp: false }),
Object.freeze({ surface: true, clamp: true }),
],
];
const NO_OPTIONS = Object.freeze({});
/** Plain finite metres, or null. */
const metres = (height) => (Number.isFinite(height) ? height : null);
/**
* The point as a Cartographic in radians at height 0. Degrees go through a
* shared scratch unless `result` is given; a Cartographic is used as it is.
* @param {HeightPoint} point
* @param {Cesium.Cartographic} [result]
* @returns {Cesium.Cartographic|null}
*/
function cartographicOf(point, result = scratch) {
if (Number.isFinite(point?.longitude) && Number.isFinite(point?.latitude))
return point;
const lon = Number(point?.lon);
const lat = Number(point?.lat);
if (!Number.isFinite(lon) || !Number.isFinite(lat)) return null;
return Cesium.Cartographic.fromDegrees(lon, lat, 0, result);
}
/**
* @typedef {object} HeightService
* @property {(point: HeightPoint, options?: HeightOptions) => number|null} now
* Height in metres above the ellipsoid from what is loaded, or null.
* @property {(position: Cesium.Cartesian3, options?: {exclude?: object[]}) => Cesium.Cartesian3|null} clampNow
* `position` moved onto the drawn surface along the ellipsoid normal, or null.
* @property {(points: HeightPoint[], options?: HeightOptions) => Promise<Array<number|null>>} batch
* Heights once the most detailed tiles have streamed; null per miss. A
* failed query (a tile request error, a destroyed scene) rejects, so the
* caller can tell it from points that are merely uncovered.
* @property {(positions: Cesium.Cartesian3[], options?: {exclude?: object[]}) => Promise<Array<Cesium.Cartesian3|null>>} clampBatch
* Clamped copies of `positions`; null per miss, rejecting as `batch` does.
* @property {() => {surface: boolean, clamp: boolean}} supported
* Whether the drawn surface can be queried at all (Cesium needs depth
* textures); terrain queries are always attempted.
*/
/**
* The scene's height service, made on first use.
* @param {object} scene A Cesium scene (or a test double).
* @returns {HeightService}
*/
export function heightsFor(scene) {
let service = services.get(scene);
if (!service) {
service = createHeights(scene);
services.set(scene, service);
}
return service;
}
function createHeights(scene) {
const terrainProvider = () =>
scene.terrainProvider ?? scene.globe?.terrainProvider;
return {
now(point, { against = 'surface', exclude } = NO_OPTIONS) {
const carto = cartographicOf(point);
if (!carto) return null;
try {
if (against === 'terrain')
return metres(scene.globe?.getHeight?.(carto));
// Cesium throws without depth-texture support: that is a miss.
return metres(scene.sampleHeight?.(carto, exclude));
} catch {
return null;
}
},
clampNow(position, { exclude } = NO_OPTIONS) {
if (!position) return null;
try {
return scene.clampToHeight?.(position, exclude) ?? null;
} catch {
return null;
}
},
async batch(points, { against = 'surface', exclude } = NO_OPTIONS) {
const cartos = points.map((point) => {
const carto = cartographicOf(point, new Cesium.Cartographic());
return (
carto && new Cesium.Cartographic(carto.longitude, carto.latitude)
);
});
const valid = cartos.filter(Boolean);
if (!valid.length) return cartos.map(() => null);
if (against === 'terrain') {
const provider = terrainProvider();
if (!provider) return cartos.map(() => null);
await Cesium.sampleTerrainMostDetailed(provider, valid);
} else {
if (typeof scene.sampleHeightMostDetailed !== 'function')
return cartos.map(() => null);
await scene.sampleHeightMostDetailed(valid, exclude);
}
return cartos.map((carto) => (carto ? metres(carto.height) : null));
},
async clampBatch(positions, { exclude } = NO_OPTIONS) {
if (typeof scene.clampToHeightMostDetailed !== 'function')
return positions.map(() => null);
const clones = positions.map((p) => Cesium.Cartesian3.clone(p));
const clamped = await scene.clampToHeightMostDetailed(clones, exclude);
return clamped.map((p) => p ?? null);
},
supported() {
return SUPPORT[+(scene.sampleHeightSupported === true)][
+(scene.clampToHeightSupported === true)
];
},
};
}
+218
View File
@@ -0,0 +1,218 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import * as Cesium from 'cesium';
import { heightsFor } from './heights.js';
const ROOT = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'..',
'..',
);
const AUSTIN = { lon: -97.74, lat: 30.27 };
function scene(overrides = {}) {
const calls = [];
return {
calls,
sampleHeightSupported: true,
clampToHeightSupported: true,
sampleHeight(carto, exclude) {
calls.push(['sampleHeight', carto, exclude]);
return 150;
},
clampToHeight(position, exclude) {
calls.push(['clampToHeight', position, exclude]);
return Cesium.Cartesian3.fromDegrees(AUSTIN.lon, AUSTIN.lat, 150);
},
globe: { getHeight: () => 140 },
...overrides,
};
}
test('now reads the drawn surface, seeing through what the caller excludes', () => {
const s = scene();
const own = [{ name: 'own marker' }];
assert.equal(heightsFor(s).now(AUSTIN, { exclude: own }), 150);
const [[, carto, exclude]] = s.calls;
assert.ok(
Math.abs(Cesium.Math.toDegrees(carto.longitude) - AUSTIN.lon) < 1e-9,
);
assert.equal(exclude, own);
});
test('now takes a Cartographic as it is', () => {
const s = scene();
const carto = Cesium.Cartographic.fromDegrees(AUSTIN.lon, AUSTIN.lat);
heightsFor(s).now(carto);
assert.equal(s.calls[0][1], carto);
});
test('a miss is null, whatever form it takes', () => {
for (const sampleHeight of [
() => undefined,
() => Number.NaN,
() => {
throw new Error('depth textures unsupported');
},
]) {
assert.equal(heightsFor(scene({ sampleHeight })).now(AUSTIN), null);
}
assert.equal(
heightsFor(scene({ sampleHeight: undefined })).now(AUSTIN),
null,
);
assert.equal(heightsFor(scene()).now({ lon: 'x', lat: 1 }), null);
});
test('terrain reads the loaded terrain tiles only', () => {
const s = scene();
assert.equal(heightsFor(s).now(AUSTIN, { against: 'terrain' }), 140);
assert.equal(s.calls.length, 0, 'the drawn surface is not sampled');
assert.equal(
heightsFor(scene({ globe: undefined })).now(AUSTIN, { against: 'terrain' }),
null,
);
});
test('clampNow moves a position onto the surface, or misses with null', () => {
const s = scene();
const own = [{}];
const position = Cesium.Cartesian3.fromDegrees(AUSTIN.lon, AUSTIN.lat, 0);
const clamped = heightsFor(s).clampNow(position, { exclude: own });
assert.ok(
Math.abs(Cesium.Cartographic.fromCartesian(clamped).height - 150) < 1e-6,
);
assert.equal(s.calls[0][2], own);
assert.equal(
heightsFor(scene({ clampToHeight: () => undefined })).clampNow(position),
null,
);
});
test('batch answers each point once the tiles stream, null per miss', async () => {
let seen;
const s = scene({
async sampleHeightMostDetailed(cartos, exclude) {
seen = exclude;
cartos.forEach((c, i) => (c.height = i === 1 ? undefined : 100 + i));
return cartos;
},
});
const own = [{}];
const heights = await heightsFor(s).batch(
[
AUSTIN,
{ lon: 0, lat: 0 },
Cesium.Cartographic.fromDegrees(1, 1, 999),
{ lon: 'bad' },
],
{ exclude: own },
);
assert.deepEqual(heights, [100, null, 102, null]);
assert.equal(seen, own);
});
test('batch leaves the caller’s Cartographics alone', async () => {
const s = scene({
async sampleHeightMostDetailed(cartos) {
cartos.forEach((c) => (c.height = 5));
return cartos;
},
});
const carto = Cesium.Cartographic.fromDegrees(1, 1, 999);
await heightsFor(s).batch([carto]);
assert.equal(carto.height, 999);
});
test('batch misses everything when the scene cannot stream, and rejects when it fails', async () => {
assert.deepEqual(await heightsFor(scene()).batch([AUSTIN]), [null]);
const failing = scene({
sampleHeightMostDetailed: async () => {
throw new Error('scene destroyed');
},
clampToHeightMostDetailed: async () => {
throw new Error('scene destroyed');
},
});
await assert.rejects(heightsFor(failing).batch([AUSTIN]), /destroyed/);
await assert.rejects(
heightsFor(failing).clampBatch([Cesium.Cartesian3.UNIT_X]),
/destroyed/,
);
assert.deepEqual(
await heightsFor(scene()).batch([AUSTIN], { against: 'terrain' }),
[null],
'no terrain provider, no terrain heights',
);
});
test('clampBatch clamps copies and misses per position', async () => {
const s = scene({
async clampToHeightMostDetailed(positions) {
return positions.map((p, i) => (i === 0 ? p : undefined));
},
});
const position = Cesium.Cartesian3.fromDegrees(AUSTIN.lon, AUSTIN.lat, 0);
const [first, second] = await heightsFor(s).clampBatch([position, position]);
assert.notEqual(first, position, 'the caller’s position is not mutated');
assert.equal(second, null);
});
test('supported reports whether the drawn surface can be queried', () => {
assert.deepEqual(heightsFor(scene()).supported(), {
surface: true,
clamp: true,
});
assert.deepEqual(
heightsFor(
scene({
sampleHeightSupported: false,
clampToHeightSupported: undefined,
}),
).supported(),
{ surface: false, clamp: false },
);
});
test('supported answers without allocating', () => {
const s = scene();
assert.equal(heightsFor(s).supported(), heightsFor(s).supported());
});
test('one service per scene', () => {
const s = scene();
assert.equal(heightsFor(s), heightsFor(s));
assert.notEqual(heightsFor(s), heightsFor(scene()));
});
test('height queries go through the service', () => {
// A renderer that answers heights from a cache or asynchronously plugs in
// at src/queries/heights.js; a site that asks Cesium directly bypasses it.
const SERVICE = 'src/queries/heights.js';
const NAMES =
'sampleHeight|clampToHeight|sampleHeightMostDetailed|clampToHeightMostDetailed|getHeight';
const direct = new RegExp(
`\\.(${NAMES})\\b|\\[\\s*['"\`](${NAMES})['"\`]\\s*\\]|\\bsampleTerrain(MostDetailed)?\\b`,
);
const walk = (dir) =>
fs
.readdirSync(path.join(ROOT, dir), { withFileTypes: true })
.flatMap((entry) => {
const rel = path.join(dir, entry.name);
if (entry.isDirectory()) return walk(rel);
return /\.(js|vue)$/.test(entry.name) ? [rel] : [];
});
const offenders = walk('src').filter((file) => {
if (file === SERVICE) return false;
const code = fs
.readFileSync(path.join(ROOT, file), 'utf8')
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/^\s*\/\/.*$/gm, '');
return direct.test(code);
});
assert.deepEqual(offenders, []);
});
+11 -15
View File
@@ -47,6 +47,7 @@
*/
import * as Cesium from 'cesium';
import { heightsFor } from '../queries/heights.js';
/** Taxi threshold: a cached snap answers directly for moves up to this far from
* the sampled spot (m). Past it the value is demoted to a held last-known and a
@@ -347,21 +348,16 @@ export function createGroundSnap({ groundFloor }) {
};
if (!_tilesReady(viewer)) return miss();
windowCount += 1;
let sampled;
try {
const carto = Cesium.Cartographic.fromCartesian(
pos,
Cesium.Ellipsoid.WGS84,
scratchCarto,
);
// sampleHeight throws when unsupported (no depth textures) — that's a miss.
sampled = viewer.scene.sampleHeight(
carto,
getExclusions ? getExclusions() : undefined,
);
} catch {
sampled = undefined;
}
const carto = Cesium.Cartographic.fromCartesian(
pos,
Cesium.Ellipsoid.WGS84,
scratchCarto,
);
const sampled = carto
? heightsFor(viewer.scene).now(carto, {
exclude: getExclusions ? getExclusions() : undefined,
})
: null;
// Same sanity floor as cctv.js's sampleGroundHeight: a hit far below the
// ellipsoid is pick garbage, not ground.
if (!Number.isFinite(sampled) || sampled < -150) return miss();
+11 -15
View File
@@ -1,5 +1,6 @@
import * as Cesium from 'cesium';
import { approxDistanceKm } from '../data/approxDistance.js';
import { heightsFor } from '../queries/heights.js';
/** Construct an instance-owned meshFloorSampler service with explicit dependencies. */
export function createMeshFloorSampler({
@@ -101,7 +102,8 @@ export function createMeshFloorSampler({
{ excludeObjects = [], viewerLat, viewerLon } = {},
) {
if (!meshFloorPreferred()) return;
if (!scene || typeof scene.sampleHeight !== 'function') return;
// Without depth-texture support every probe throws: nothing to latch.
if (!scene || !heightsFor(scene).supported().surface) return;
if (!Array.isArray(points) || !points.length) return;
// Round-5 hardening (the CCTV projectionTilesReady lesson, ported late):
// while tiles are MID-STREAM a probe returns coarse-LOD heights — real
@@ -129,20 +131,14 @@ export function createMeshFloorSampler({
) {
continue; // too far: tiles not streamed there, probe would be a guaranteed miss
}
let height;
try {
const carto = Cesium.Cartographic.fromDegrees(
cell.lon,
cell.lat,
0,
_scratchProbe,
);
height = scene.sampleHeight(carto, excludeObjects);
sampled += 1;
} catch {
continue; // scene mid-teardown — try again next poll
}
if (!Number.isFinite(height)) continue; // tiles not loaded yet — no negative latch
const height = heightsFor(scene).now(
Cesium.Cartographic.fromDegrees(cell.lon, cell.lat, 0, _scratchProbe),
{ exclude: excludeObjects },
);
// A miss counts against the budget whatever its cause: past the support
// gate a probe only throws mid-teardown, when every later cell would too.
sampled += 1;
if (height === null) continue; // tiles not loaded yet — no negative latch
// Round 5: a REAL Re:Earth prior is REQUIRED before any sample latches.
// - A geoid-fallback prior (cached while the proxy was failing) is a
// sea-level poison that rejected legitimate samples at elevated
+4 -1
View File
@@ -3,6 +3,7 @@ import * as Cesium from 'cesium';
import { guardCameraAboveGround } from '../cameraGroundGuard.js';
import { rangeForBoundingSphere } from '../data/cameraFraming.js';
import { cancelCameraArrival } from '../data/cameraArrival.js';
import { heightsFor } from '../queries/heights.js';
/** All visible positions, path vertices, polygon rings, walls and overlay bounds. */
export function dataSourcePoints(dataSource, time = Cesium.JulianDate.now()) {
@@ -87,7 +88,9 @@ export function flyToDataSource(
Cesium.Cartographic.fromDegrees(points[0][0], points[0][1]);
const lat = Cesium.Math.toDegrees(center.latitude),
lon = Cesium.Math.toDegrees(center.longitude);
const terrain = viewer.scene?.globe?.getHeight?.(center);
const terrain = viewer.scene
? heightsFor(viewer.scene).now(center, { against: 'terrain' })
: null;
const cached = ground?.cachedGroundFloor?.(lat, lon);
const height =
Number.isFinite(terrain) && terrain > 0