cesium/packages/engine/Specs/Core/sampleTerrainSpec.js

372 lines
13 KiB
JavaScript

import {
ArcGISTiledElevationTerrainProvider,
Cartographic,
CesiumTerrainProvider,
createWorldTerrainAsync,
defined,
RequestScheduler,
Resource,
sampleTerrain,
} from "../../index.js";
describe("Core/sampleTerrain", function () {
it("queries heights", async function () {
const terrainProvider = await CesiumTerrainProvider.fromUrl(
"Data/CesiumTerrainTileJson/CesiumWorldTerrainSample",
);
const positions = [
Cartographic.fromDegrees(86.93666235421982, 27.97989963555095),
Cartographic.fromDegrees(86.9366623542198, 27.9798996355509),
];
const returnedPosition = await sampleTerrain(terrainProvider, 9, positions);
expect(returnedPosition).toBe(positions);
expect(positions[0].height).toBeGreaterThan(5000);
expect(positions[0].height).toBeLessThan(10000);
expect(positions[1].height).toBeGreaterThan(5000);
expect(positions[1].height).toBeLessThan(10000);
});
it("queries heights from terrain without availability", async function () {
const terrainProvider = await CesiumTerrainProvider.fromUrl(
"Data/CesiumTerrainTileJson/Heightmap",
);
const positions = [
Cartographic.fromDegrees(86.925145, 27.988257),
Cartographic.fromDegrees(87.0, 28.0),
];
const passedPositions = await sampleTerrain(terrainProvider, 9, positions);
expect(passedPositions).toBe(positions);
expect(positions[0].height).toBeGreaterThan(5000);
expect(positions[0].height).toBeLessThan(10000);
expect(positions[1].height).toBeGreaterThan(5000);
expect(positions[1].height).toBeLessThan(10000);
});
it("sets height to undefined if terrain data is not available at the position and specified level", async function () {
const terrainProvider = await CesiumTerrainProvider.fromUrl(
"Data/CesiumTerrainTileJson/CesiumWorldTerrainSample",
);
const positions = [Cartographic.fromDegrees(0.0, 0.0, 0.0)];
await sampleTerrain(terrainProvider, 9, positions);
expect(positions[0].height).toBeUndefined();
});
it("rejects if terrain data is not available and rejectOnTileFail is true", async function () {
const terrainProvider = await CesiumTerrainProvider.fromUrl(
"Data/CesiumTerrainTileJson/CesiumWorldTerrainSample",
);
const positions = [Cartographic.fromDegrees(0.0, 0.0, 0.0)];
await expectAsync(
sampleTerrain(terrainProvider, 9, positions, true),
).toBeRejected();
});
it("rejects if terrain data is not available for the second position and rejectOnTileFail is true", async function () {
const terrainProvider = await CesiumTerrainProvider.fromUrl(
"Data/CesiumTerrainTileJson/CesiumWorldTerrainSample",
);
const positionWithData = Cartographic.fromDegrees(
86.936662354213,
27.979899635557,
);
const positionWithoutData = Cartographic.fromDegrees(0.0, 0.0, 0.0);
const positions = [positionWithData, positionWithoutData];
await expectAsync(
sampleTerrain(terrainProvider, 9, positions, true),
).toBeRejected();
});
it("fills in what it can when given a mix of positions with and without valid tiles", async function () {
const terrainProvider = await CesiumTerrainProvider.fromUrl(
"Data/CesiumTerrainTileJson/CesiumWorldTerrainSample",
);
const positions = [
Cartographic.fromDegrees(86.925145, 27.988257),
Cartographic.fromDegrees(0.0, 89.0, 0.0),
Cartographic.fromDegrees(87.0, 28.0),
];
await sampleTerrain(terrainProvider, 9, positions);
expect(positions[0].height).toBeGreaterThan(5000);
expect(positions[0].height).toBeLessThan(10000);
expect(positions[1].height).toBeUndefined();
expect(positions[2].height).toBeGreaterThan(5000);
expect(positions[2].height).toBeLessThan(10000);
});
it("requires terrainProvider, level, and positions", async function () {
const terrainProvider = await CesiumTerrainProvider.fromUrl(
"Data/CesiumTerrainTileJson/CesiumWorldTerrainSample",
);
const positions = [
Cartographic.fromDegrees(86.93666235421982, 27.97989963555095),
Cartographic.fromDegrees(86.9366623542198, 27.9798996355509),
];
await expectAsync(
sampleTerrain(undefined, 9, positions),
).toBeRejectedWithDeveloperError();
await expectAsync(
sampleTerrain(terrainProvider, undefined, positions),
).toBeRejectedWithDeveloperError();
await expectAsync(
sampleTerrain(terrainProvider, 9, undefined),
).toBeRejectedWithDeveloperError();
});
it("works for a dodgy point right near the edge of a tile", async function () {
const worldTerrain = await createWorldTerrainAsync();
const positions = [
new Cartographic(0.33179290856829535, 0.7363107781851078),
];
await sampleTerrain(worldTerrain, 12, positions);
expect(positions[0].height).toBeDefined();
});
describe("with terrain providers", function () {
beforeEach(function () {
RequestScheduler.clearForSpecs();
});
afterEach(function () {
Resource._Implementations.loadWithXhr =
Resource._DefaultImplementations.loadWithXhr;
});
function spyOnTerrainDataCreateMesh(terrainProvider) {
// do some sneaky spying, so we can check how many times createMesh is called
const originalRequestTileGeometry = terrainProvider.requestTileGeometry;
spyOn(terrainProvider, "requestTileGeometry").and.callFake(
function (x, y, level, request) {
// Call the original function!
return originalRequestTileGeometry
.call(terrainProvider, x, y, level, request)
.then(function (tile) {
spyOn(tile, "createMesh").and.callThrough();
// return the original tile - after we've spied on the createMesh method
return tile;
});
},
);
}
function expectTileAndMeshCounts(
terrainProvider,
numberOfTilesRequested,
wasFirstTileMeshCreated,
) {
// assert how many tiles were requested
expect(terrainProvider.requestTileGeometry.calls.count()).toEqual(
numberOfTilesRequested,
);
// get the first tile that was requested
return (
terrainProvider.requestTileGeometry.calls
.first()
// the return value was the promise of the tile, so wait for that
.returnValue.then(function (terrainData) {
// assert if the mesh was created or not for this tile
expect(terrainData.createMesh.calls.count()).toEqual(
wasFirstTileMeshCreated ? 1 : 0,
);
})
);
}
function endsWith(value, suffix) {
return value.indexOf(suffix, value.length - suffix.length) >= 0;
}
function patchXHRLoad(proxySpec) {
Resource._Implementations.loadWithXhr = function (
url,
responseType,
method,
data,
headers,
deferred,
overrideMimeType,
) {
// find a key (source path) path in the spec which matches (ends with) the requested url
const availablePaths = Object.keys(proxySpec);
let proxiedUrl;
for (let i = 0; i < availablePaths.length; i++) {
const srcPath = availablePaths[i];
if (endsWith(url, srcPath)) {
proxiedUrl = proxySpec[availablePaths[i]];
break;
}
}
// it's a whitelist - meaning you have to proxy every request explicitly
if (!defined(proxiedUrl)) {
throw new Error(
`Unexpected XHR load to url: ${url}; spec includes: ${availablePaths.join(
", ",
)}`,
);
}
// make a real request to the proxied path for the matching source path
return Resource._DefaultImplementations.loadWithXhr(
proxiedUrl,
responseType,
method,
data,
headers,
deferred,
overrideMimeType,
);
};
}
it("should work for Cesium World Terrain", async function () {
const terrainProvider = await CesiumTerrainProvider.fromUrl(
"Data/CesiumTerrainTileJson/CesiumWorldTerrainSample",
);
spyOnTerrainDataCreateMesh(terrainProvider);
const positionA = Cartographic.fromDegrees(
86.93666235421982,
27.97989963555095,
);
const positionB = Cartographic.fromDegrees(
86.9366623542198,
27.9798996355509,
);
const positionC = Cartographic.fromDegrees(
86.936662354213,
27.979899635557,
);
const level = 9;
await sampleTerrain(terrainProvider, level, [
positionA,
positionB,
positionC,
]);
expect(positionA.height).toBeCloseTo(7780, 0);
expect(positionB.height).toBeCloseTo(7780, 0);
expect(positionC.height).toBeCloseTo(7780, 0);
// 1 tile was requested (all positions were close enough on the same tile)
// and the mesh was not created because we're using CWT - which doesn't need the mesh for interpolation
return expectTileAndMeshCounts(terrainProvider, 1, false);
});
it("should work for ArcGIS terrain", async function () {
patchXHRLoad({
"/?f=pjson": "Data/ArcGIS/9_214_379/root.json",
"/tilemap/10/384/640/128/128":
"Data/ArcGIS/9_214_379/tilemap_10_384_640_128_128.json",
"/tile/9/214/379": "Data/ArcGIS/9_214_379/tile_9_214_379.tile",
});
const terrainProvider =
await ArcGISTiledElevationTerrainProvider.fromUrl("made/up/url");
spyOnTerrainDataCreateMesh(terrainProvider);
const positionA = Cartographic.fromDegrees(
86.93666235421982,
27.97989963555095,
);
const positionB = Cartographic.fromDegrees(
86.9366623542198,
27.9798996355509,
);
const positionC = Cartographic.fromDegrees(
86.936662354213,
27.979899635557,
);
const level = 9;
await sampleTerrain(terrainProvider, level, [
positionA,
positionB,
positionC,
]);
// 3 very similar positions
expect(positionA.height).toBeCloseTo(7681, 0);
expect(positionB.height).toBeCloseTo(7681, 0);
expect(positionC.height).toBeCloseTo(7681, 0);
// 1 tile was requested (all positions were close enough on the same tile)
// and the mesh was created once because we're using an ArcGIS tile
await expectTileAndMeshCounts(terrainProvider, 1, true);
});
it("should handle the RequestScheduler throttling the requestTileGeometry requests with a retry", async function () {
patchXHRLoad({
"/?f=pjson": "Data/ArcGIS/9_214_379/root.json",
"/tilemap/10/384/640/128/128":
"Data/ArcGIS/9_214_379/tilemap_10_384_640_128_128.json",
// we need multiple tiles to be requested, the actual value is not so important for this test
"/tile/9/214/379": "Data/ArcGIS/9_214_379/tile_9_214_379.tile",
"/tile/9/214/378": "Data/ArcGIS/9_214_379/tile_9_214_379.tile",
"/tile/9/214/376": "Data/ArcGIS/9_214_379/tile_9_214_379.tile",
});
const terrainProvider =
await ArcGISTiledElevationTerrainProvider.fromUrl("made/up/url");
let i = 0;
const originalRequestTileGeometry = terrainProvider.requestTileGeometry;
spyOn(terrainProvider, "requestTileGeometry").and.callFake(
function (x, y, level, request) {
i++;
if (i === 2 || i === 3) {
// on the 2nd and 3rd requestTileGeometry call, return undefined
// to simulate RequestScheduler throttling the request
return undefined;
}
// otherwise, call the original method
return originalRequestTileGeometry.call(
terrainProvider,
x,
y,
level,
request,
);
},
);
// 3 positions, quite far apart (requires multiple tile requests)
const positionA = Cartographic.fromDegrees(85, 28);
const positionB = Cartographic.fromDegrees(86, 28);
const positionC = Cartographic.fromDegrees(87, 28);
const level = 9;
await sampleTerrain(terrainProvider, level, [
positionA,
positionB,
positionC,
]);
// the order of requests is an implementation detail and not important,
// but it is deterministic, and we can assert that there were some retries in there
const calls = terrainProvider.requestTileGeometry.calls;
expect(calls.count()).toEqual(5);
expect(calls.argsFor(0)).toEqual([376, 214, 9]);
expect(calls.argsFor(1)).toEqual([378, 214, 9]);
// this tile was retried twice, because the 2nd and 3rd call to requestTileGeometry returned undefined as expected
expect(calls.argsFor(2)).toEqual([378, 214, 9]);
expect(calls.argsFor(3)).toEqual([378, 214, 9]);
expect(calls.argsFor(4)).toEqual([379, 214, 9]);
});
});
});