mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 23:32:20 +00:00
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:
1 parent
b51f1bf7c5
commit
d51ea4007b
32 files changed
+704
-210
No files matched your search
@@ -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
|
`defer(() => inputFor(viewer).destroy())` right after it; the standalone scene
|
||||||
does this and returns the dispatcher as `scene.input`.
|
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
|
## Standalone wiring
|
||||||
|
|
||||||
`src/main.js` reads the existing browser configuration and starts
|
`src/main.js` reads the existing browser configuration and starts
|
||||||
|
|||||||
@@ -229,6 +229,7 @@
|
|||||||
"./application/operations": "./src/app/operations.js",
|
"./application/operations": "./src/app/operations.js",
|
||||||
"./data/entity-records": "./src/data/entityRecords.js",
|
"./data/entity-records": "./src/data/entityRecords.js",
|
||||||
"./input": "./src/input/dispatcher.js",
|
"./input": "./src/input/dispatcher.js",
|
||||||
|
"./queries/heights": "./src/queries/heights.js",
|
||||||
"./data/lifecycle": "./src/data/lifecycle.js",
|
"./data/lifecycle": "./src/data/lifecycle.js",
|
||||||
"./ui/navigation": "./src/ui/navigationController.js",
|
"./ui/navigation": "./src/ui/navigationController.js",
|
||||||
"./ui/share-restoration": "./src/ui/shareRestoration.js",
|
"./ui/share-restoration": "./src/ui/shareRestoration.js",
|
||||||
|
|||||||
@@ -47,6 +47,8 @@
|
|||||||
"src/input/testDoubles.mjs",
|
"src/input/testDoubles.mjs",
|
||||||
"src/input/sensorPick.js",
|
"src/input/sensorPick.js",
|
||||||
"src/input/dispatcher.js",
|
"src/input/dispatcher.js",
|
||||||
|
"src/queries/heights.js",
|
||||||
|
"src/queries/heights.test.mjs",
|
||||||
"docs/CODE-BOUNDARIES.md",
|
"docs/CODE-BOUNDARIES.md",
|
||||||
"src/main.js",
|
"src/main.js",
|
||||||
"docs/APPLICATION.md",
|
"docs/APPLICATION.md",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"src/input/dispatcher.js",
|
"src/input/dispatcher.js",
|
||||||
"src/input/sensorPick.js",
|
"src/input/sensorPick.js",
|
||||||
"src/data/infrastructureOverlayEntry.js",
|
"src/data/infrastructureOverlayEntry.js",
|
||||||
"src/sources/infrastructureData.js"
|
"src/sources/infrastructureData.js",
|
||||||
|
"src/queries/heights.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -1859,7 +1860,8 @@
|
|||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
"src/i18n/locales/uk.json",
|
"src/i18n/locales/uk.json",
|
||||||
"src/i18n/locales/zh-CN.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"]
|
"external": ["cesium", "pbf", "@mapbox/vector-tile"]
|
||||||
},
|
},
|
||||||
@@ -1900,7 +1902,8 @@
|
|||||||
"src/input/sensorPick.js",
|
"src/input/sensorPick.js",
|
||||||
"src/data/scenePick.js",
|
"src/data/scenePick.js",
|
||||||
"src/data/pickRegistry.js",
|
"src/data/pickRegistry.js",
|
||||||
"src/input/dispatcher.js"
|
"src/input/dispatcher.js",
|
||||||
|
"src/queries/heights.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -2046,7 +2049,8 @@
|
|||||||
"src/data/scenePick.js",
|
"src/data/scenePick.js",
|
||||||
"src/input/sensorPick.js",
|
"src/input/sensorPick.js",
|
||||||
"src/data/pickRegistry.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"]
|
"external": ["cesium", "pbf", "@mapbox/vector-tile", "mapillary-js"]
|
||||||
},
|
},
|
||||||
@@ -2089,7 +2093,8 @@
|
|||||||
"src/input/sensorPick.js",
|
"src/input/sensorPick.js",
|
||||||
"src/data/scenePick.js",
|
"src/data/scenePick.js",
|
||||||
"src/data/pickRegistry.js",
|
"src/data/pickRegistry.js",
|
||||||
"src/input/dispatcher.js"
|
"src/input/dispatcher.js",
|
||||||
|
"src/queries/heights.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium", "@mapbox/vector-tile", "pbf"]
|
"external": ["cesium", "@mapbox/vector-tile", "pbf"]
|
||||||
},
|
},
|
||||||
@@ -3327,7 +3332,8 @@
|
|||||||
"src/data/local_data/wof_neighborhoods/files.js",
|
"src/data/local_data/wof_neighborhoods/files.js",
|
||||||
"src/data/entityRecords.js",
|
"src/data/entityRecords.js",
|
||||||
"src/input/sensorPick.js",
|
"src/input/sensorPick.js",
|
||||||
"src/input/dispatcher.js"
|
"src/input/dispatcher.js",
|
||||||
|
"src/queries/heights.js"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": [
|
||||||
"hls.js",
|
"hls.js",
|
||||||
@@ -3952,7 +3958,8 @@
|
|||||||
"src/data/local_data/wof_neighborhoods/files.js",
|
"src/data/local_data/wof_neighborhoods/files.js",
|
||||||
"src/data/entityRecords.js",
|
"src/data/entityRecords.js",
|
||||||
"src/input/sensorPick.js",
|
"src/input/sensorPick.js",
|
||||||
"src/input/dispatcher.js"
|
"src/input/dispatcher.js",
|
||||||
|
"src/queries/heights.js"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": [
|
||||||
"hls.js",
|
"hls.js",
|
||||||
@@ -4039,7 +4046,8 @@
|
|||||||
"src/data/placeBoundaries.js",
|
"src/data/placeBoundaries.js",
|
||||||
"src/data/placeNames.js",
|
"src/data/placeNames.js",
|
||||||
"src/data/local_data/us_census_places/files.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": [
|
"external": [
|
||||||
"cesium",
|
"cesium",
|
||||||
@@ -4647,7 +4655,8 @@
|
|||||||
"src/i18n/locales/zh-CN.json",
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/i18n/locales/zh-Hant.json",
|
"src/i18n/locales/zh-Hant.json",
|
||||||
"src/input/sensorPick.js",
|
"src/input/sensorPick.js",
|
||||||
"src/input/dispatcher.js"
|
"src/input/dispatcher.js",
|
||||||
|
"src/queries/heights.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium", "egm96-universal", "pbf"]
|
"external": ["cesium", "egm96-universal", "pbf"]
|
||||||
},
|
},
|
||||||
@@ -4703,7 +4712,8 @@
|
|||||||
"src/input/sensorPick.js",
|
"src/input/sensorPick.js",
|
||||||
"src/data/scenePick.js",
|
"src/data/scenePick.js",
|
||||||
"src/data/pickRegistry.js",
|
"src/data/pickRegistry.js",
|
||||||
"src/input/dispatcher.js"
|
"src/input/dispatcher.js",
|
||||||
|
"src/queries/heights.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium", "pbf"]
|
"external": ["cesium", "pbf"]
|
||||||
},
|
},
|
||||||
@@ -4869,7 +4879,8 @@
|
|||||||
"src/data/scenePick.js",
|
"src/data/scenePick.js",
|
||||||
"src/input/sensorPick.js",
|
"src/input/sensorPick.js",
|
||||||
"src/data/pickRegistry.js",
|
"src/data/pickRegistry.js",
|
||||||
"src/input/dispatcher.js"
|
"src/input/dispatcher.js",
|
||||||
|
"src/queries/heights.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -5101,5 +5112,10 @@
|
|||||||
"src/i18n/marker.js"
|
"src/i18n/marker.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"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;
|
const detail = parent.detail;
|
||||||
assert.equal(detail.index, index);
|
assert.equal(detail.index, index);
|
||||||
assert.equal(detail.status, trace === firstTrace ? 'recorded' : 'missing');
|
assert.equal(detail.status, trace === firstTrace ? 'recorded' : 'missing');
|
||||||
|
// The fourth probe finds nothing and the fifth throws: both are misses.
|
||||||
assert.equal(detail.clampResult,
|
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(Number.isFinite(detail.clampStart) && Number.isFinite(detail.clampEnd));
|
||||||
assert.ok(parent.start <= detail.clampStart && detail.clampStart <= detail.clampEnd && detail.clampEnd <= parent.end);
|
assert.ok(parent.start <= detail.clampStart && detail.clampStart <= detail.clampEnd && detail.clampEnd <= parent.end);
|
||||||
if (index === 7) {
|
if (index === 7) {
|
||||||
|
|||||||
+11
-18
@@ -45,6 +45,7 @@ import {
|
|||||||
import { createOutlineRungs } from './outlineRungs.js';
|
import { createOutlineRungs } from './outlineRungs.js';
|
||||||
import { unavailablePlaceSearch } from '../search/placeSearch.js';
|
import { unavailablePlaceSearch } from '../search/placeSearch.js';
|
||||||
import { isPickedWorldPosition } from '../data/scenePick.js';
|
import { isPickedWorldPosition } from '../data/scenePick.js';
|
||||||
|
import { heightsFor } from '../queries/heights.js';
|
||||||
|
|
||||||
/** Prepare only local dateline outlines for planar admission and centering. */
|
/** Prepare only local dateline outlines for planar admission and centering. */
|
||||||
function localOutlineFrame(fp) {
|
function localOutlineFrame(fp) {
|
||||||
@@ -2351,26 +2352,18 @@ export function createAnnotationResolver({
|
|||||||
function sampleGroundHeight(viewer, lon, lat) {
|
function sampleGroundHeight(viewer, lon, lat) {
|
||||||
const scene = viewer?.scene;
|
const scene = viewer?.scene;
|
||||||
if (!scene) return 0;
|
if (!scene) return 0;
|
||||||
try {
|
const heights = heightsFor(scene);
|
||||||
if (
|
// A miss is tiles not ready: fall through to the terrain, then 0.
|
||||||
scene.clampToHeightSupported &&
|
if (heights.supported().clamp) {
|
||||||
typeof scene.clampToHeight === 'function'
|
const surface = Cesium.Cartesian3.fromDegrees(lon, lat, 0);
|
||||||
) {
|
const clamped = heights.clampNow(surface);
|
||||||
const carto = Cesium.Cartographic.fromDegrees(lon, lat);
|
if (clamped) {
|
||||||
const surface = Cesium.Cartographic.toCartesian(carto);
|
const h = Cesium.Cartographic.fromCartesian(clamped)?.height;
|
||||||
const clamped = scene.clampToHeight(surface);
|
if (Number.isFinite(h)) return h;
|
||||||
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?.(
|
const globeHeight = heights.now({ lon, lat }, { against: 'terrain' });
|
||||||
Cesium.Cartographic.fromDegrees(lon, lat),
|
return globeHeight !== null && globeHeight > 0 ? globeHeight : 0;
|
||||||
);
|
|
||||||
return Number.isFinite(globeHeight) && globeHeight > 0 ? globeHeight : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function shortLabel(formattedAddress) {
|
function shortLabel(formattedAddress) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { nextTick } from 'vue';
|
|||||||
import { useScreenAnnotationsStore } from '../ui/stores/screenAnnotations.js';
|
import { useScreenAnnotationsStore } from '../ui/stores/screenAnnotations.js';
|
||||||
import { applicationPinia } from '../ui/vueRuntime.js';
|
import { applicationPinia } from '../ui/vueRuntime.js';
|
||||||
import { readEntityRecord } from '../data/entityRecords.js';
|
import { readEntityRecord } from '../data/entityRecords.js';
|
||||||
|
import { heightsFor } from '../queries/heights.js';
|
||||||
|
|
||||||
/** A label whose detailed outline could not be resolved. */
|
/** A label whose detailed outline could not be resolved. */
|
||||||
function outlineUnavailableLabel(label) {
|
function outlineUnavailableLabel(label) {
|
||||||
@@ -142,13 +143,10 @@ export function createScreenAnnotationRenderer(
|
|||||||
return cached.h;
|
return cached.h;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (
|
const heights = heightsFor(scene);
|
||||||
scene.clampToHeightSupported &&
|
if (heights.supported().clamp) {
|
||||||
typeof scene.clampToHeight === 'function'
|
// A miss is tiles not ready.
|
||||||
) {
|
const c = heights.clampNow(Cesium.Cartesian3.fromDegrees(lon, lat, 0));
|
||||||
const c = scene.clampToHeight(
|
|
||||||
Cesium.Cartesian3.fromDegrees(lon, lat, 0),
|
|
||||||
);
|
|
||||||
if (c) {
|
if (c) {
|
||||||
const h = Cesium.Cartographic.fromCartesian(c).height;
|
const h = Cesium.Cartographic.fromCartesian(c).height;
|
||||||
if (Number.isFinite(h) && h > -430 && h < 9000) {
|
if (Number.isFinite(h) && h > -430 && h < 9000) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { recordWorkSlice } from '../sources/phaseTiming.js';
|
|||||||
import { ringCentroid } from '../sources/featureGeometry.js';
|
import { ringCentroid } from '../sources/featureGeometry.js';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { showOsmCredit, hideOsmCredit } from '../data/dataCredits.js';
|
import { showOsmCredit, hideOsmCredit } from '../data/dataCredits.js';
|
||||||
|
import { heightsFor } from '../queries/heights.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* World-space annotation renderer (Direction A).
|
* World-space annotation renderer (Direction A).
|
||||||
@@ -174,20 +175,16 @@ export function createWorldAnnotationRenderer(viewer) {
|
|||||||
const sliceStart = performance.now();
|
const sliceStart = performance.now();
|
||||||
let error;
|
let error;
|
||||||
try {
|
try {
|
||||||
if (
|
if (heightsFor(viewer.scene).supported().clamp) {
|
||||||
viewer.scene.clampToHeightSupported &&
|
|
||||||
typeof viewer.scene.clampToHeight === 'function'
|
|
||||||
) {
|
|
||||||
const [lon, lat] = task.points[task.index++];
|
const [lon, lat] = task.points[task.index++];
|
||||||
timing.probes += 1;
|
timing.probes += 1;
|
||||||
try {
|
try {
|
||||||
const position = Cesium.Cartesian3.fromDegrees(lon, lat, 0);
|
const position = Cesium.Cartesian3.fromDegrees(lon, lat, 0);
|
||||||
let c;
|
let c;
|
||||||
note?.('clampStart');
|
note?.('clampStart');
|
||||||
if (detail) detail.clampResult = 'threw';
|
|
||||||
try {
|
try {
|
||||||
c = viewer.scene.clampToHeight(position);
|
c = heightsFor(viewer.scene).clampNow(position);
|
||||||
if (detail) detail.clampResult = 'returned';
|
if (detail) detail.clampResult = c ? 'hit' : 'missed';
|
||||||
} finally {
|
} finally {
|
||||||
note?.('clampEnd');
|
note?.('clampEnd');
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-11
@@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { ownCameraArrival } from './data/cameraArrival.js';
|
import { ownCameraArrival } from './data/cameraArrival.js';
|
||||||
|
import { heightsFor } from './queries/heights.js';
|
||||||
|
|
||||||
/** Minimum eye height above the rendered surface for a usable view. */
|
/** Minimum eye height above the rendered surface for a usable view. */
|
||||||
export const MIN_EYE_CLEARANCE_M = 120;
|
export const MIN_EYE_CLEARANCE_M = 120;
|
||||||
@@ -37,18 +38,16 @@ const probeScratch = new Cesium.Cartographic();
|
|||||||
function sampleSurfaceM(scene, cells) {
|
function sampleSurfaceM(scene, cells) {
|
||||||
let heightM = Number.NaN;
|
let heightM = Number.NaN;
|
||||||
let sampled = 0;
|
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) {
|
for (const cell of cells) {
|
||||||
try {
|
// A miss is a cell whose tiles are not ready.
|
||||||
const height = scene.sampleHeight(
|
const height = heights.now(
|
||||||
Cesium.Cartographic.fromDegrees(cell.lon, cell.lat, 0, probeScratch),
|
Cesium.Cartographic.fromDegrees(cell.lon, cell.lat, 0, probeScratch),
|
||||||
);
|
);
|
||||||
if (!Number.isFinite(height)) continue;
|
if (height === null) continue;
|
||||||
sampled += 1;
|
sampled += 1;
|
||||||
heightM = Number.isFinite(heightM) ? Math.max(heightM, height) : height;
|
heightM = Number.isFinite(heightM) ? Math.max(heightM, height) : height;
|
||||||
} catch {
|
|
||||||
/* tiles not ready for this cell */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return { heightM, sampled };
|
return { heightM, sampled };
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-16
@@ -29,6 +29,7 @@ import {
|
|||||||
holdContinuousRender,
|
holdContinuousRender,
|
||||||
releaseContinuousRender,
|
releaseContinuousRender,
|
||||||
} from './renderGovernor.js';
|
} from './renderGovernor.js';
|
||||||
|
import { heightsFor } from './queries/heights.js';
|
||||||
|
|
||||||
let _isCockpitActive = null;
|
let _isCockpitActive = null;
|
||||||
/** °/s by speed word — orbit; pan uses fractions of view height/s. */
|
/** °/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) {
|
export function probeMeshFloorM(scene, cells) {
|
||||||
const requested = Array.isArray(cells) ? cells.length : 0;
|
const requested = Array.isArray(cells) ? cells.length : 0;
|
||||||
const empty = { heightM: Number.NaN, sampled: 0, requested };
|
const empty = { heightM: Number.NaN, sampled: 0, requested };
|
||||||
if (!scene || typeof scene.sampleHeight !== 'function' || !requested)
|
if (!scene || !requested) return empty;
|
||||||
return empty;
|
const heights = heightsFor(scene);
|
||||||
let heightM = Number.NaN;
|
let heightM = Number.NaN;
|
||||||
let sampled = 0;
|
let sampled = 0;
|
||||||
for (const cell of cells) {
|
for (const cell of cells) {
|
||||||
try {
|
// A miss is a cell whose tiles are not ready.
|
||||||
const carto = Cesium.Cartographic.fromDegrees(
|
const height = heights.now(
|
||||||
cell.lon,
|
Cesium.Cartographic.fromDegrees(cell.lon, cell.lat, 0, _probeCarto),
|
||||||
cell.lat,
|
);
|
||||||
0,
|
if (height === null) continue;
|
||||||
_probeCarto,
|
sampled += 1;
|
||||||
);
|
heightM = Number.isFinite(heightM) ? Math.max(heightM, height) : height;
|
||||||
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 */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return { heightM, sampled, requested };
|
return { heightM, sampled, requested };
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-22
@@ -28,6 +28,7 @@ import { createImagerySplit } from '../ui/imagerySplit.js';
|
|||||||
import { formatDate, t, tn, msg, onLocaleChange } from '../i18n/index.js';
|
import { formatDate, t, tn, msg, onLocaleChange } from '../i18n/index.js';
|
||||||
import { useBhoteKoshiPanelStore } from '../ui/stores/bhoteKoshiPanel.js';
|
import { useBhoteKoshiPanelStore } from '../ui/stores/bhoteKoshiPanel.js';
|
||||||
import { applicationPinia } from '../ui/vueRuntime.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_LAYER_ID = 'bhote-koshi-2026';
|
||||||
export const BHOTE_KOSHI_OVERLAY_SOURCE_ID = 'bhote-koshi-witnesses';
|
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) =>
|
const DEFAULT_IMAGERY_PROVIDER_FACTORY = (url, options) =>
|
||||||
Cesium.SingleTileImageryProvider.fromUrl(url, options);
|
Cesium.SingleTileImageryProvider.fromUrl(url, options);
|
||||||
const DEFAULT_TERRAIN_SAMPLER = async (viewer, observations) => {
|
const DEFAULT_TERRAIN_SAMPLER = async (viewer, observations) => {
|
||||||
if (
|
if (!viewer?.scene || !viewer.terrainProvider?.availability) return null;
|
||||||
!viewer?.terrainProvider ||
|
const heights = await heightsFor(viewer.scene).batch(observations, {
|
||||||
!viewer.terrainProvider.availability ||
|
against: 'terrain',
|
||||||
typeof Cesium.sampleTerrainMostDetailed !== 'function'
|
});
|
||||||
) {
|
return heights.map((height) => ({ height }));
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const cartographics = observations.map((observation) =>
|
|
||||||
Cesium.Cartographic.fromDegrees(observation.lon, observation.lat),
|
|
||||||
);
|
|
||||||
return Cesium.sampleTerrainMostDetailed(
|
|
||||||
viewer.terrainProvider,
|
|
||||||
cartographics,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
const DEFAULT_MEDIA_LOADER = async (url, { signal } = {}) => {
|
const DEFAULT_MEDIA_LOADER = async (url, { signal } = {}) => {
|
||||||
if (typeof createImageBitmap !== 'function') {
|
if (typeof createImageBitmap !== 'function') {
|
||||||
@@ -1197,7 +1189,8 @@ export function createBhoteKoshiEventLayer({
|
|||||||
}
|
}
|
||||||
throwIfEnableCancelled(signal, _enabled);
|
throwIfEnableCancelled(signal, _enabled);
|
||||||
return observations.map((observation, index) => {
|
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;
|
const fallbackHeight = Number(observation.fallbackElevationM) || 0;
|
||||||
return Cesium.Cartesian3.fromDegrees(
|
return Cesium.Cartesian3.fromDegrees(
|
||||||
observation.lon,
|
observation.lon,
|
||||||
@@ -1573,14 +1566,12 @@ export function createBhoteKoshiEventLayer({
|
|||||||
// a single high sample can make the front dive backward in screen space.
|
// a single high sample can make the front dive backward in screen space.
|
||||||
if (_sceneControls.evidencePathElevation !== 'source') {
|
if (_sceneControls.evidencePathElevation !== 'source') {
|
||||||
// Sample once while preparing the shot, never from an animation frame.
|
// Sample once while preparing the shot, never from an animation frame.
|
||||||
try {
|
const heights = _viewer?.scene ? heightsFor(_viewer.scene) : null;
|
||||||
height = _viewer?.scene?.sampleHeight?.(
|
height = heights?.now(position, {
|
||||||
position,
|
exclude: [_floodCore, _floodHalo, _surgePoint].filter(Boolean),
|
||||||
[_floodCore, _floodHalo, _surgePoint].filter(Boolean),
|
});
|
||||||
);
|
|
||||||
} catch {}
|
|
||||||
if (!Number.isFinite(height))
|
if (!Number.isFinite(height))
|
||||||
height = _viewer?.scene?.globe?.getHeight?.(position);
|
height = heights?.now(position, { against: 'terrain' });
|
||||||
}
|
}
|
||||||
if (!Number.isFinite(height)) height = point.fallbackElevationM;
|
if (!Number.isFinite(height)) height = point.fallbackElevationM;
|
||||||
return Cesium.Cartesian3.fromDegrees(point.lon, point.lat, height + 8);
|
return Cesium.Cartesian3.fromDegrees(point.lon, point.lat, height + 8);
|
||||||
|
|||||||
@@ -2992,3 +2992,43 @@ test('Bhote Koshi corridor remains ordered downstream and inside the imagery rec
|
|||||||
priorChainage = point.chainageM;
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -3,6 +3,7 @@ import * as Cesium from 'cesium';
|
|||||||
import { createImportImageBudget } from './geoImportImages.js';
|
import { createImportImageBudget } from './geoImportImages.js';
|
||||||
import { createImportedPins } from './geoImportPins.js';
|
import { createImportedPins } from './geoImportPins.js';
|
||||||
import { msg } from '../i18n/marker.js';
|
import { msg } from '../i18n/marker.js';
|
||||||
|
import { heightsFor } from '../queries/heights.js';
|
||||||
|
|
||||||
/** Largest file accepted, in bytes. */
|
/** Largest file accepted, in bytes. */
|
||||||
const MAX_IMPORT_BYTES = 50 * 1024 * 1024;
|
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.
|
// a buried starting point can make the downward mesh ray miss the surface.
|
||||||
const ellipsoid =
|
const ellipsoid =
|
||||||
viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider;
|
viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider;
|
||||||
|
const heights = viewer.scene ? heightsFor(viewer.scene) : null;
|
||||||
for (let i = 0; i < positions.length; i++) {
|
for (let i = 0; i < positions.length; i++) {
|
||||||
const sampled = ellipsoid
|
const sampled = ellipsoid
|
||||||
? 0
|
? 0
|
||||||
: viewer.scene?.globe?.getHeight?.(positions[i]);
|
: heights?.now(positions[i], { against: 'terrain' });
|
||||||
positions[i].height = mesh
|
positions[i].height = mesh
|
||||||
? (batch[i].groundHeight ??
|
? (batch[i].groundHeight ??
|
||||||
Math.max(0, Number.isFinite(sampled) ? sampled : 0))
|
Math.max(0, Number.isFinite(sampled) ? sampled : 0))
|
||||||
@@ -1295,7 +1297,7 @@ async function settleGroundPins(dataSource, viewer, signal) {
|
|||||||
indices.push(i);
|
indices.push(i);
|
||||||
if (indices.length) {
|
if (indices.length) {
|
||||||
const resolved = await boundedImport(
|
const resolved = await boundedImport(
|
||||||
viewer.scene.clampToHeightMostDetailed(
|
heights.clampBatch(
|
||||||
indices.map((i) => {
|
indices.map((i) => {
|
||||||
const c = positions[i];
|
const c = positions[i];
|
||||||
return Cesium.Cartesian3.fromRadians(
|
return Cesium.Cartesian3.fromRadians(
|
||||||
|
|||||||
@@ -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">/,
|
/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;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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'));
|
const sources = Object.keys(entry.modules).filter((id) => id.endsWith('.js'));
|
||||||
assert.deepEqual(sources.map((id) => id.split('/').at(-1)).sort(), [
|
assert.deepEqual(sources.map((id) => id.split('/').at(-1)).sort(), [
|
||||||
'dispatcher.js',
|
'dispatcher.js',
|
||||||
|
'heights.js',
|
||||||
'infrastructure.js',
|
'infrastructure.js',
|
||||||
'infrastructureData.js',
|
'infrastructureData.js',
|
||||||
'infrastructureOverlayEntry.js',
|
'infrastructureOverlayEntry.js',
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
applyInfraEvictionGrace,
|
applyInfraEvictionGrace,
|
||||||
shouldRecomputeInfraLod,
|
shouldRecomputeInfraLod,
|
||||||
} from './localGeojsonLod.js';
|
} from './localGeojsonLod.js';
|
||||||
|
import { heightsFor } from '../queries/heights.js';
|
||||||
|
|
||||||
const DEFAULT_LABEL_MAX = 900;
|
const DEFAULT_LABEL_MAX = 900;
|
||||||
const DEFAULT_LABEL_GRID_PX = 132;
|
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.
|
// 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
|
// 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.
|
// 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 (
|
if (
|
||||||
!Number.isFinite(height) ||
|
height === null ||
|
||||||
Math.abs(height) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M ||
|
Math.abs(height) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M ||
|
||||||
height <= record.groundHeight
|
height <= record.groundHeight
|
||||||
)
|
)
|
||||||
@@ -1131,26 +1134,20 @@ function sampleLocalGroundHeight(viewer, record, now) {
|
|||||||
record.lastGroundSampleMs = now;
|
record.lastGroundSampleMs = now;
|
||||||
const globe = viewer.scene.globe;
|
const globe = viewer.scene.globe;
|
||||||
if (globe?.show && globe.tilesLoaded === false) return false;
|
if (globe?.show && globe.tilesLoaded === false) return false;
|
||||||
let sampled;
|
const heights = heightsFor(viewer.scene);
|
||||||
try {
|
// A miss is tiles not ready: retry on a later bounded update.
|
||||||
sampled = viewer.scene.sampleHeight(record.carto, [record.entity]);
|
let sampled = heights.now(record.carto, { exclude: [record.entity] });
|
||||||
} catch {
|
if (sampled === null || Math.abs(sampled) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M)
|
||||||
return false; // tiles not ready; retry on a later bounded update
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!Number.isFinite(sampled) ||
|
|
||||||
Math.abs(sampled) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M
|
|
||||||
)
|
|
||||||
return false;
|
return false;
|
||||||
// A coarse scene-depth sample can lie below the terrain already loaded by
|
// 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
|
// 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
|
// valid below-sea-level elevations. Photoreal scenes hide the globe, so its
|
||||||
// inactive terrain must not constrain their geometry.
|
// inactive terrain must not constrain their geometry.
|
||||||
const terrainHeight = globe?.show
|
const terrainHeight = globe?.show
|
||||||
? globe.getHeight?.(record.carto)
|
? heights.now(record.carto, { against: 'terrain' })
|
||||||
: undefined;
|
: null;
|
||||||
if (
|
if (
|
||||||
Number.isFinite(terrainHeight) &&
|
terrainHeight !== null &&
|
||||||
Math.abs(terrainHeight) <= GROUND_SAMPLE_MAX_ABS_HEIGHT_M
|
Math.abs(terrainHeight) <= GROUND_SAMPLE_MAX_ABS_HEIGHT_M
|
||||||
) {
|
) {
|
||||||
sampled = Math.max(sampled, terrainHeight);
|
sampled = Math.max(sampled, terrainHeight);
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ function fakeScene(sampleHeight, { cameraHeightM = 900, tilesLoaded = true, show
|
|||||||
Object.defineProperty(tileset, 'tilesLoaded', { value: tilesLoaded, configurable: true });
|
Object.defineProperty(tileset, 'tilesLoaded', { value: tilesLoaded, configurable: true });
|
||||||
Object.defineProperty(tileset, 'show', { value: show, configurable: true });
|
Object.defineProperty(tileset, 'show', { value: show, configurable: true });
|
||||||
return {
|
return {
|
||||||
|
sampleHeightSupported: true,
|
||||||
sampleHeight,
|
sampleHeight,
|
||||||
camera: { positionCartographic: { height: cameraHeightM } },
|
camera: { positionCartographic: { height: cameraHeightM } },
|
||||||
primitives: { length: 1, get: () => tileset },
|
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 });
|
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');
|
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);
|
||||||
|
});
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
validAlprGroundHeight,
|
validAlprGroundHeight,
|
||||||
} from './visuals.js';
|
} from './visuals.js';
|
||||||
import { alprMarkers } from './markerState.js';
|
import { alprMarkers } from './markerState.js';
|
||||||
|
import { heightsFor } from '../../queries/heights.js';
|
||||||
|
|
||||||
/** 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({
|
||||||
@@ -121,14 +122,11 @@ export function createAlprOverlay({
|
|||||||
else if (scene.sampleHeightSupported) {
|
else if (scene.sampleHeightSupported) {
|
||||||
if (budget.samples <= 0) return null;
|
if (budget.samples <= 0) return null;
|
||||||
budget.samples -= 1;
|
budget.samples -= 1;
|
||||||
try {
|
// A miss is tiles still streaming.
|
||||||
height = scene.sampleHeight(location, [entity]);
|
height = heightsFor(scene).now(location, { exclude: [entity] });
|
||||||
} catch {
|
|
||||||
/* streaming tiles */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!validAlprGroundHeight(height) && scene.globe.show)
|
if (!validAlprGroundHeight(height) && scene.globe.show)
|
||||||
height = scene.globe.getHeight?.(location);
|
height = heightsFor(scene).now(location, { against: 'terrain' });
|
||||||
if (!validAlprGroundHeight(height)) return null;
|
if (!validAlprGroundHeight(height)) return null;
|
||||||
setAnchor(record, entity, height);
|
setAnchor(record, entity, height);
|
||||||
return alprMarkers.read(entity)?.canvasPosition;
|
return alprMarkers.read(entity)?.canvasPosition;
|
||||||
@@ -202,15 +200,11 @@ export function createAlprOverlay({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
budget.samples -= 1;
|
budget.samples -= 1;
|
||||||
let sample;
|
// A miss is tiles still streaming.
|
||||||
try {
|
const sample = heightsFor(scene).now(
|
||||||
sample = scene.sampleHeight(
|
{ lon: record.longitude, lat: record.latitude },
|
||||||
Cesium.Cartographic.fromDegrees(record.longitude, record.latitude),
|
{ exclude: [entity] },
|
||||||
[entity],
|
);
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
/* streaming tiles */
|
|
||||||
}
|
|
||||||
const current = Cesium.Cartographic.fromCartesian(anchor).height;
|
const current = Cesium.Cartographic.fromCartesian(anchor).height;
|
||||||
const floor = services.groundFloor.cachedGroundFloor(
|
const floor = services.groundFloor.cachedGroundFloor(
|
||||||
record.latitude,
|
record.latitude,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
} from './visuals.js';
|
} from './visuals.js';
|
||||||
import { entityRecord, readEntityRecord } from '../../data/entityRecords.js';
|
import { entityRecord, readEntityRecord } from '../../data/entityRecords.js';
|
||||||
import { alprMarkers } from './markerState.js';
|
import { alprMarkers } from './markerState.js';
|
||||||
|
import { heightsFor } from '../../queries/heights.js';
|
||||||
|
|
||||||
export function createAlprPresentation({ state, services, source }) {
|
export function createAlprPresentation({ state, services, source }) {
|
||||||
const { governorRequestRender } = services.render;
|
const { governorRequestRender } = services.render;
|
||||||
@@ -518,16 +519,13 @@ export function createAlprPresentation({ state, services, source }) {
|
|||||||
record.longitude,
|
record.longitude,
|
||||||
record.latitude,
|
record.latitude,
|
||||||
);
|
);
|
||||||
let height;
|
const heights = heightsFor(state.viewer.scene);
|
||||||
if (state.viewer.scene.sampleHeightSupported) {
|
// A miss is tiles still streaming.
|
||||||
try {
|
let height = state.viewer.scene.sampleHeightSupported
|
||||||
height = state.viewer.scene.sampleHeight(location, [nearest]);
|
? heights.now(location, { exclude: [nearest] })
|
||||||
} catch {
|
: null;
|
||||||
/* tiles may still be streaming */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!validAlprGroundHeight(height) && state.viewer.scene.globe.show)
|
if (!validAlprGroundHeight(height) && state.viewer.scene.globe.show)
|
||||||
height = state.viewer.scene.globe.getHeight?.(location);
|
height = heights.now(location, { against: 'terrain' });
|
||||||
if (!validAlprGroundHeight(height))
|
if (!validAlprGroundHeight(height))
|
||||||
height = cachedGroundFloor(record.latitude, record.longitude);
|
height = cachedGroundFloor(record.latitude, record.longitude);
|
||||||
const center = Cesium.Cartesian3.fromDegrees(
|
const center = Cesium.Cartesian3.fromDegrees(
|
||||||
@@ -598,16 +596,13 @@ export function createAlprPresentation({ state, services, source }) {
|
|||||||
record.longitude,
|
record.longitude,
|
||||||
record.latitude,
|
record.latitude,
|
||||||
);
|
);
|
||||||
let height;
|
const heights = heightsFor(scene);
|
||||||
if (scene.sampleHeightSupported) {
|
// A miss is tiles still streaming.
|
||||||
try {
|
let height = scene.sampleHeightSupported
|
||||||
height = scene.sampleHeight(location, [entity]);
|
? heights.now(location, { exclude: [entity] })
|
||||||
} catch {
|
: null;
|
||||||
/* tiles may still be streaming */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!validAlprGroundHeight(height) && scene.globe.show)
|
if (!validAlprGroundHeight(height) && scene.globe.show)
|
||||||
height = scene.globe.getHeight?.(location);
|
height = heights.now(location, { against: 'terrain' });
|
||||||
if (!validAlprGroundHeight(height))
|
if (!validAlprGroundHeight(height))
|
||||||
height = cachedGroundFloor(record.latitude, record.longitude);
|
height = cachedGroundFloor(record.latitude, record.longitude);
|
||||||
if (!validAlprGroundHeight(height)) return;
|
if (!validAlprGroundHeight(height)) return;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
MAX_TOTAL_POINTS,
|
MAX_TOTAL_POINTS,
|
||||||
COLOR_OUTLINE,
|
COLOR_OUTLINE,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
|
import { heightsFor } from '../../queries/heights.js';
|
||||||
|
|
||||||
export function createRendering({
|
export function createRendering({
|
||||||
state: layerState,
|
state: layerState,
|
||||||
@@ -100,12 +101,10 @@ export function createRendering({
|
|||||||
|
|
||||||
let height = POINT_HEIGHT_OFFSET_M;
|
let height = POINT_HEIGHT_OFFSET_M;
|
||||||
// Sample terrain height so points sit on ground rather than at ellipsoid level
|
// Sample terrain height so points sit on ground rather than at ellipsoid level
|
||||||
if (layerState._viewer?.scene?.sampleHeightSupported) {
|
const scene = layerState._viewer?.scene;
|
||||||
const carto = Cesium.Cartographic.fromDegrees(lon, lat);
|
if (scene?.sampleHeightSupported) {
|
||||||
const sampled = layerState._viewer.scene.sampleHeight(carto);
|
const sampled = heightsFor(scene).now({ lon, lat });
|
||||||
if (Number.isFinite(sampled)) {
|
if (sampled !== null) height = sampled + POINT_HEIGHT_OFFSET_M;
|
||||||
height = sampled + POINT_HEIGHT_OFFSET_M;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Cesium.Cartesian3.fromDegrees(lon, lat, height);
|
return Cesium.Cartesian3.fromDegrees(lon, lat, height);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { plausibleSurfaceHeight, sampledSurfaceHeight } from './view.js';
|
import { plausibleSurfaceHeight, sampledSurfaceHeight } from './view.js';
|
||||||
|
import { heightsFor } from '../../queries/heights.js';
|
||||||
|
|
||||||
/** Fly the globe camera once to frame a photo after it opens. */
|
/** Fly the globe camera once to frame a photo after it opens. */
|
||||||
export function createCameraFraming({ state }) {
|
export function createCameraFraming({ state }) {
|
||||||
@@ -30,7 +31,8 @@ export function createCameraFraming({ state }) {
|
|||||||
const scene = state.viewer?.scene;
|
const scene = state.viewer?.scene;
|
||||||
const carto = Cesium.Cartographic.fromDegrees(lon, lat);
|
const carto = Cesium.Cartographic.fromDegrees(lon, lat);
|
||||||
let height = sampledSurfaceHeight(scene, carto);
|
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;
|
if (!plausibleSurfaceHeight(height)) height = fallback;
|
||||||
return plausibleSurfaceHeight(height) ? height : 0;
|
return plausibleSurfaceHeight(height) ? height : 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -799,7 +799,11 @@ test('under Google 3D the ground is measured again once its tiles load, without
|
|||||||
const viewer = fakeViewer();
|
const viewer = fakeViewer();
|
||||||
// 4 km up the ellipsoid, 2 km over a city whose tiles are still loading.
|
// 4 km up the ellipsoid, 2 km over a city whose tiles are still loading.
|
||||||
let ground;
|
let ground;
|
||||||
viewer.camera.positionCartographic = { height: 4000 };
|
viewer.camera.positionCartographic = {
|
||||||
|
longitude: 0,
|
||||||
|
latitude: 0,
|
||||||
|
height: 4000,
|
||||||
|
};
|
||||||
viewer.scene.globe = { show: false };
|
viewer.scene.globe = { show: false };
|
||||||
viewer.scene.sampleHeightSupported = true;
|
viewer.scene.sampleHeightSupported = true;
|
||||||
viewer.scene.sampleHeight = () => ground;
|
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'] });
|
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||||
const viewer = fakeViewer();
|
const viewer = fakeViewer();
|
||||||
let samples = 0;
|
let samples = 0;
|
||||||
viewer.camera.positionCartographic = { height: 4000 };
|
viewer.camera.positionCartographic = {
|
||||||
|
longitude: 0,
|
||||||
|
latitude: 0,
|
||||||
|
height: 4000,
|
||||||
|
};
|
||||||
viewer.scene.globe = { show: false };
|
viewer.scene.globe = { show: false };
|
||||||
viewer.scene.sampleHeightSupported = true;
|
viewer.scene.sampleHeightSupported = true;
|
||||||
viewer.scene.sampleHeight = () => {
|
viewer.scene.sampleHeight = () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { wrapLon } from './tileMath.js';
|
import { wrapLon } from './tileMath.js';
|
||||||
|
import { heightsFor } from '../../queries/heights.js';
|
||||||
|
|
||||||
/** Equirectangular metres between {lon, lat} points, the short way round ±180°; street scale. */
|
/** Equirectangular metres between {lon, lat} points, the short way round ±180°; street scale. */
|
||||||
export function metresBetween(a, b) {
|
export function metresBetween(a, b) {
|
||||||
@@ -21,12 +22,9 @@ export function plausibleSurfaceHeight(height) {
|
|||||||
* underground before tiles load).
|
* underground before tiles load).
|
||||||
*/
|
*/
|
||||||
export function sampledSurfaceHeight(scene, carto) {
|
export function sampledSurfaceHeight(scene, carto) {
|
||||||
let height = null;
|
const height = scene?.sampleHeightSupported
|
||||||
try {
|
? heightsFor(scene).now(carto)
|
||||||
if (scene?.sampleHeightSupported) height = scene.sampleHeight(carto);
|
: null;
|
||||||
} catch {
|
|
||||||
/* not sampleable yet */
|
|
||||||
}
|
|
||||||
return plausibleSurfaceHeight(height) ? height : null;
|
return plausibleSurfaceHeight(height) ? height : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,8 +39,8 @@ export function groundUnderCamera(viewer) {
|
|||||||
if (!carto || !scene) return null;
|
if (!carto || !scene) return null;
|
||||||
const globe = scene.globe;
|
const globe = scene.globe;
|
||||||
if (globe && globe.show !== false) {
|
if (globe && globe.show !== false) {
|
||||||
const ground = globe.getHeight?.(carto);
|
const ground = heightsFor(scene).now(carto, { against: 'terrain' });
|
||||||
if (Number.isFinite(ground)) return ground;
|
if (ground !== null) return ground;
|
||||||
}
|
}
|
||||||
const sampled = sampledSurfaceHeight(scene, carto);
|
const sampled = sampledSurfaceHeight(scene, carto);
|
||||||
return sampled !== null && sampled < carto.height ? sampled : null;
|
return sampled !== null && sampled < carto.height ? sampled : null;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
TILE_CACHE_MAX_ENTRIES,
|
TILE_CACHE_MAX_ENTRIES,
|
||||||
FAST_FETCH_ALTITUDE,
|
FAST_FETCH_ALTITUDE,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
|
import { heightsFor } from '../../queries/heights.js';
|
||||||
|
|
||||||
/** Retain complete parsed snapshots under both an LRU entry cap and a byte budget. */
|
/** Retain complete parsed snapshots under both an LRU entry cap and a byte budget. */
|
||||||
export function cacheRoadSnapshot(
|
export function cacheRoadSnapshot(
|
||||||
@@ -273,16 +274,15 @@ export function createIngestion({
|
|||||||
const scene = layerState._viewer?.scene;
|
const scene = layerState._viewer?.scene;
|
||||||
const location = Cesium.Cartographic.fromDegrees(centerLon, centerLat);
|
const location = Cesium.Cartographic.fromDegrees(centerLon, centerLat);
|
||||||
let estimated = services.ground?.cachedGroundFloor?.(centerLat, centerLon);
|
let estimated = services.ground?.cachedGroundFloor?.(centerLat, centerLon);
|
||||||
if (!Number.isFinite(estimated)) {
|
// A miss is the local mesh still loading.
|
||||||
try {
|
if (!Number.isFinite(estimated) && scene) {
|
||||||
estimated = scene?.globe?.show
|
estimated = scene.globe?.show
|
||||||
? scene.globe.getHeight?.(location)
|
? heightsFor(scene).now(location, { against: 'terrain' })
|
||||||
: scene?.sampleHeightSupported
|
: scene.sampleHeightSupported
|
||||||
? scene.sampleHeight(location, [layerState._pointCollection])
|
? heightsFor(scene).now(location, {
|
||||||
: undefined;
|
exclude: [layerState._pointCollection],
|
||||||
} catch {
|
})
|
||||||
/* the local mesh can still be loading */
|
: undefined;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (Number.isFinite(estimated) && Math.abs(estimated) <= 9000)
|
if (Number.isFinite(estimated) && Math.abs(estimated) <= 9000)
|
||||||
layerState._viewHeightEstimate = estimated;
|
layerState._viewHeightEstimate = estimated;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { phaseTiming } from '../../sources/phaseTiming.js';
|
import { phaseTiming } from '../../sources/phaseTiming.js';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { DOT_HEIGHT_OFFSET, MAX_WAYPOINTS_PER_ROAD } from './policy.js';
|
import { DOT_HEIGHT_OFFSET, MAX_WAYPOINTS_PER_ROAD } from './policy.js';
|
||||||
|
import { heightsFor } from '../../queries/heights.js';
|
||||||
|
|
||||||
function surfaceTilesReady(scene) {
|
function surfaceTilesReady(scene) {
|
||||||
if (scene.globe?.show) return scene.globe.tilesLoaded !== false;
|
if (scene.globe?.show) return scene.globe.tilesLoaded !== false;
|
||||||
@@ -333,7 +334,8 @@ export async function prepareRoadSurfaces(
|
|||||||
let sampled;
|
let sampled;
|
||||||
// sampleHeight reads the locally rendered mesh, not unresolved/offscreen
|
// sampleHeight reads the locally rendered mesh, not unresolved/offscreen
|
||||||
// tiles. Global tilesLoaded can stay false while this street is usable.
|
// 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 {
|
else {
|
||||||
const estimate = Cesium.Cartographic.fromCartesian(
|
const estimate = Cesium.Cartographic.fromCartesian(
|
||||||
road.waypoints[i],
|
road.waypoints[i],
|
||||||
@@ -351,9 +353,8 @@ export async function prepareRoadSurfaces(
|
|||||||
const shown = (excluded || []).filter((p) => p?.show);
|
const shown = (excluded || []).filter((p) => p?.show);
|
||||||
for (const primitive of shown) primitive.show = false;
|
for (const primitive of shown) primitive.show = false;
|
||||||
try {
|
try {
|
||||||
sampled = scene.sampleHeight(carto, excluded);
|
// A miss is the local mesh still missing.
|
||||||
} catch {
|
sampled = heightsFor(scene).now(carto, { exclude: excluded });
|
||||||
/* local mesh missing */
|
|
||||||
} finally {
|
} finally {
|
||||||
for (const primitive of shown) primitive.show = true;
|
for (const primitive of shown) primitive.show = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
HEIGHT_SAMPLE_MAX_ALTITUDE_M,
|
HEIGHT_SAMPLE_MAX_ALTITUDE_M,
|
||||||
withinViewBounds,
|
withinViewBounds,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
|
import { heightsFor } from '../../queries/heights.js';
|
||||||
|
|
||||||
/** Surface preparation off the render path, shared by markers and history. */
|
/** Surface preparation off the render path, shared by markers and history. */
|
||||||
export function createHeight({ state, services, parts }) {
|
export function createHeight({ state, services, parts }) {
|
||||||
@@ -63,14 +64,16 @@ export function createHeight({ state, services, parts }) {
|
|||||||
return { cell, height: fixtureFloor, prior: null };
|
return { cell, height: fixtureFloor, prior: null };
|
||||||
// On a terrain globe, use the loaded surface under the marker. This is
|
// On a terrain globe, use the loaded surface under the marker. This is
|
||||||
// synchronous cache work and does not launch a terrain request.
|
// synchronous cache work and does not launch a terrain request.
|
||||||
const globe = state._viewer?.scene?.globe;
|
const scene = state._viewer?.scene;
|
||||||
if (globe?.show === true) {
|
if (scene?.globe?.show === true) {
|
||||||
Cesium.Cartographic.fromDegrees(lon, lat, 0, terrainPoint);
|
Cesium.Cartographic.fromDegrees(lon, lat, 0, terrainPoint);
|
||||||
const rendered = globe.getHeight?.(terrainPoint);
|
const rendered = heightsFor(scene).now(terrainPoint, {
|
||||||
|
against: 'terrain',
|
||||||
|
});
|
||||||
const flat =
|
const flat =
|
||||||
state._viewer?.terrainProvider instanceof
|
state._viewer?.terrainProvider instanceof
|
||||||
Cesium.EllipsoidTerrainProvider;
|
Cesium.EllipsoidTerrainProvider;
|
||||||
if (Number.isFinite(rendered) || flat)
|
if (rendered !== null || flat)
|
||||||
return {
|
return {
|
||||||
cell,
|
cell,
|
||||||
height: (flat ? 0 : rendered) + GROUND_FLOOR_LIFT_M,
|
height: (flat ? 0 : rendered) + GROUND_FLOOR_LIFT_M,
|
||||||
|
|||||||
+4
-1
@@ -12,6 +12,7 @@ import { unavailablePlaceSearch } from './search/placeSearch.js';
|
|||||||
import { cancelCameraArrival } from './data/cameraArrival.js';
|
import { cancelCameraArrival } from './data/cameraArrival.js';
|
||||||
import { guardCameraAboveGround } from './cameraGroundGuard.js';
|
import { guardCameraAboveGround } from './cameraGroundGuard.js';
|
||||||
import { CITY_POIS } from './cityPois.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. */
|
/** Newest landmark arrival; an older post-arrival ground guard yields to it. */
|
||||||
let arrivalGeneration = 0;
|
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)
|
// Sample terrain height (sync — uses loaded tiles; 0 if globe/terrain not ready)
|
||||||
const targetCartographic = Cesium.Cartographic.fromDegrees(lon, lat);
|
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
|
// Use sampled height if available, otherwise the cached ground-floor
|
||||||
// elevation (same ellipsoidal datum), then the pre-baked city elevation.
|
// elevation (same ellipsoidal datum), then the pre-baked city elevation.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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
@@ -47,6 +47,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as Cesium from 'cesium';
|
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
|
/** 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
|
* 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();
|
if (!_tilesReady(viewer)) return miss();
|
||||||
windowCount += 1;
|
windowCount += 1;
|
||||||
let sampled;
|
const carto = Cesium.Cartographic.fromCartesian(
|
||||||
try {
|
pos,
|
||||||
const carto = Cesium.Cartographic.fromCartesian(
|
Cesium.Ellipsoid.WGS84,
|
||||||
pos,
|
scratchCarto,
|
||||||
Cesium.Ellipsoid.WGS84,
|
);
|
||||||
scratchCarto,
|
const sampled = carto
|
||||||
);
|
? heightsFor(viewer.scene).now(carto, {
|
||||||
// sampleHeight throws when unsupported (no depth textures) — that's a miss.
|
exclude: getExclusions ? getExclusions() : undefined,
|
||||||
sampled = viewer.scene.sampleHeight(
|
})
|
||||||
carto,
|
: null;
|
||||||
getExclusions ? getExclusions() : undefined,
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
sampled = undefined;
|
|
||||||
}
|
|
||||||
// Same sanity floor as cctv.js's sampleGroundHeight: a hit far below the
|
// Same sanity floor as cctv.js's sampleGroundHeight: a hit far below the
|
||||||
// ellipsoid is pick garbage, not ground.
|
// ellipsoid is pick garbage, not ground.
|
||||||
if (!Number.isFinite(sampled) || sampled < -150) return miss();
|
if (!Number.isFinite(sampled) || sampled < -150) return miss();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { approxDistanceKm } from '../data/approxDistance.js';
|
import { approxDistanceKm } from '../data/approxDistance.js';
|
||||||
|
import { heightsFor } from '../queries/heights.js';
|
||||||
|
|
||||||
/** Construct an instance-owned meshFloorSampler service with explicit dependencies. */
|
/** Construct an instance-owned meshFloorSampler service with explicit dependencies. */
|
||||||
export function createMeshFloorSampler({
|
export function createMeshFloorSampler({
|
||||||
@@ -101,7 +102,8 @@ export function createMeshFloorSampler({
|
|||||||
{ excludeObjects = [], viewerLat, viewerLon } = {},
|
{ excludeObjects = [], viewerLat, viewerLon } = {},
|
||||||
) {
|
) {
|
||||||
if (!meshFloorPreferred()) return;
|
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;
|
if (!Array.isArray(points) || !points.length) return;
|
||||||
// Round-5 hardening (the CCTV projectionTilesReady lesson, ported late):
|
// Round-5 hardening (the CCTV projectionTilesReady lesson, ported late):
|
||||||
// while tiles are MID-STREAM a probe returns coarse-LOD heights — real
|
// 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
|
continue; // too far: tiles not streamed there, probe would be a guaranteed miss
|
||||||
}
|
}
|
||||||
let height;
|
const height = heightsFor(scene).now(
|
||||||
try {
|
Cesium.Cartographic.fromDegrees(cell.lon, cell.lat, 0, _scratchProbe),
|
||||||
const carto = Cesium.Cartographic.fromDegrees(
|
{ exclude: excludeObjects },
|
||||||
cell.lon,
|
);
|
||||||
cell.lat,
|
// A miss counts against the budget whatever its cause: past the support
|
||||||
0,
|
// gate a probe only throws mid-teardown, when every later cell would too.
|
||||||
_scratchProbe,
|
sampled += 1;
|
||||||
);
|
if (height === null) continue; // tiles not loaded yet — no negative latch
|
||||||
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
|
|
||||||
// Round 5: a REAL Re:Earth prior is REQUIRED before any sample latches.
|
// 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
|
// - A geoid-fallback prior (cached while the proxy was failing) is a
|
||||||
// sea-level poison that rejected legitimate samples at elevated
|
// sea-level poison that rejected legitimate samples at elevated
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as Cesium from 'cesium';
|
|||||||
import { guardCameraAboveGround } from '../cameraGroundGuard.js';
|
import { guardCameraAboveGround } from '../cameraGroundGuard.js';
|
||||||
import { rangeForBoundingSphere } from '../data/cameraFraming.js';
|
import { rangeForBoundingSphere } from '../data/cameraFraming.js';
|
||||||
import { cancelCameraArrival } from '../data/cameraArrival.js';
|
import { cancelCameraArrival } from '../data/cameraArrival.js';
|
||||||
|
import { heightsFor } from '../queries/heights.js';
|
||||||
|
|
||||||
/** All visible positions, path vertices, polygon rings, walls and overlay bounds. */
|
/** All visible positions, path vertices, polygon rings, walls and overlay bounds. */
|
||||||
export function dataSourcePoints(dataSource, time = Cesium.JulianDate.now()) {
|
export function dataSourcePoints(dataSource, time = Cesium.JulianDate.now()) {
|
||||||
@@ -87,7 +88,9 @@ export function flyToDataSource(
|
|||||||
Cesium.Cartographic.fromDegrees(points[0][0], points[0][1]);
|
Cesium.Cartographic.fromDegrees(points[0][0], points[0][1]);
|
||||||
const lat = Cesium.Math.toDegrees(center.latitude),
|
const lat = Cesium.Math.toDegrees(center.latitude),
|
||||||
lon = Cesium.Math.toDegrees(center.longitude);
|
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 cached = ground?.cachedGroundFloor?.(lat, lon);
|
||||||
const height =
|
const height =
|
||||||
Number.isFinite(terrain) && terrain > 0
|
Number.isFinite(terrain) && terrain > 0
|
||||||
|
|||||||
Reference in new issue
Block a user