You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1186 lines
40 KiB
JavaScript

import MockTerrainProvider from "../MockTerrainProvider.js";
import TerrainTileProcessor from "../TerrainTileProcessor.js";
import { Cartesian3 } from "../../Source/Cesium.js";
import { Cartographic } from "../../Source/Cesium.js";
import { defined } from "../../Source/Cesium.js";
import { Ellipsoid } from "../../Source/Cesium.js";
import { EventHelper } from "../../Source/Cesium.js";
import { GeographicProjection } from "../../Source/Cesium.js";
import { GeographicTilingScheme } from "../../Source/Cesium.js";
import { Intersect } from "../../Source/Cesium.js";
import { Rectangle } from "../../Source/Cesium.js";
import { Visibility } from "../../Source/Cesium.js";
import { Camera } from "../../Source/Cesium.js";
import { GlobeSurfaceTileProvider } from "../../Source/Cesium.js";
import { GlobeTranslucencyState } from "../../Source/Cesium.js";
import { ImageryLayerCollection } from "../../Source/Cesium.js";
import { QuadtreePrimitive } from "../../Source/Cesium.js";
import { QuadtreeTileLoadState } from "../../Source/Cesium.js";
import { SceneMode } from "../../Source/Cesium.js";
import createScene from "../createScene.js";
import pollToPromise from "../pollToPromise.js";
import { when } from "../../Source/Cesium.js";
describe("Scene/QuadtreePrimitive", function () {
describe("selectTilesForRendering", function () {
var scene;
var camera;
var frameState;
var quadtree;
var mockTerrain;
var tileProvider;
var imageryLayerCollection;
var surfaceShaderSet;
var processor;
var rootTiles;
beforeEach(function () {
scene = {
mapProjection: new GeographicProjection(),
drawingBufferWidth: 1000,
drawingBufferHeight: 1000,
};
camera = new Camera(scene);
frameState = {
frameNumber: 0,
passes: {
render: true,
},
camera: camera,
fog: {
enabled: false,
},
context: {
drawingBufferWidth: scene.drawingBufferWidth,
drawingBufferHeight: scene.drawingBufferHeight,
},
mode: SceneMode.SCENE3D,
commandList: [],
cullingVolume: jasmine.createSpyObj("CullingVolume", [
"computeVisibility",
]),
afterRender: [],
pixelRatio: 1.0,
globeTranslucencyState: new GlobeTranslucencyState(),
};
frameState.cullingVolume.computeVisibility.and.returnValue(
Intersect.INTERSECTING
);
imageryLayerCollection = new ImageryLayerCollection();
surfaceShaderSet = jasmine.createSpyObj("SurfaceShaderSet", [
"getShaderProgram",
]);
mockTerrain = new MockTerrainProvider();
tileProvider = new GlobeSurfaceTileProvider({
terrainProvider: mockTerrain,
imageryLayers: imageryLayerCollection,
surfaceShaderSet: surfaceShaderSet,
});
quadtree = new QuadtreePrimitive({
tileProvider: tileProvider,
});
processor = new TerrainTileProcessor(
frameState,
mockTerrain,
imageryLayerCollection
);
quadtree.render(frameState);
rootTiles = quadtree._levelZeroTiles;
processor.mockWebGL();
});
function process(quadtreePrimitive, callback) {
var deferred = when.defer();
function next() {
++frameState.frameNumber;
quadtree.beginFrame(frameState);
quadtree.render(frameState);
quadtree.endFrame(frameState);
if (callback()) {
setTimeout(next, 0);
} else {
deferred.resolve();
}
}
next();
return deferred.promise;
}
it("must be constructed with a tileProvider", function () {
expect(function () {
return new QuadtreePrimitive();
}).toThrowDeveloperError();
expect(function () {
return new QuadtreePrimitive({});
}).toThrowDeveloperError();
});
it("selects nothing when the root tiles are not yet ready", function () {
quadtree.render(frameState);
expect(quadtree._tilesToRender.length).toBe(0);
});
it("selects root tiles once they are ready", function () {
mockTerrain
.requestTileGeometryWillSucceed(rootTiles[0])
.requestTileGeometryWillSucceed(rootTiles[1])
.createMeshWillSucceed(rootTiles[0])
.createMeshWillSucceed(rootTiles[1]);
return processor.process(rootTiles).then(function () {
quadtree.render(frameState);
// There should be at least one selected tile.
expect(quadtree._tilesToRender.length).toBeGreaterThan(0);
// All selected tiles should be root tiles.
expect(
quadtree._tilesToRender.filter(function (tile) {
return tile.level === 0;
}).length
).toBe(quadtree._tilesToRender.length);
});
});
it("selects deeper tiles once they are renderable", function () {
mockTerrain
.requestTileGeometryWillSucceed(rootTiles[0])
.requestTileGeometryWillSucceed(rootTiles[1])
.createMeshWillSucceed(rootTiles[0])
.createMeshWillSucceed(rootTiles[1]);
rootTiles[0].children.forEach(function (tile) {
mockTerrain
.requestTileGeometryWillSucceed(tile)
.createMeshWillSucceed(tile);
expect(tile.renderable).toBe(false);
});
return processor
.process(rootTiles)
.then(function () {
quadtree.render(frameState);
// All selected tiles should be root tiles.
expect(quadtree._tilesToRender.length).toBeGreaterThan(0);
expect(
quadtree._tilesToRender.filter(function (tile) {
return tile.level === 0;
}).length
).toBe(quadtree._tilesToRender.length);
// Allow the child tiles to load.
return processor.process(rootTiles[0].children);
})
.then(function () {
quadtree.render(frameState);
// Now child tiles should be rendered too.
expect(quadtree._tilesToRender).toContain(
rootTiles[0].southwestChild
);
expect(quadtree._tilesToRender).toContain(
rootTiles[0].southeastChild
);
expect(quadtree._tilesToRender).toContain(
rootTiles[0].northwestChild
);
expect(quadtree._tilesToRender).toContain(
rootTiles[0].northeastChild
);
});
});
it("skips loading levels when tiles are known to be available", function () {
// Mark all tiles through level 2 as available.
rootTiles.forEach(function (tile) {
// level 0 tile
mockTerrain
.willBeAvailable(tile)
.requestTileGeometryWillSucceed(tile)
.createMeshWillSucceed(tile);
tile.children.forEach(function (tile) {
// level 1 tile
mockTerrain
.willBeAvailable(tile)
.requestTileGeometryWillSucceed(tile)
.createMeshWillSucceed(tile);
tile.children.forEach(function (tile) {
// level 2 tile
mockTerrain
.willBeAvailable(tile)
.requestTileGeometryWillSucceed(tile)
.createMeshWillSucceed(tile);
});
});
});
quadtree.preloadAncestors = false;
// Look down at the center of a level 2 tile from a distance that will refine to it.
var lookAtTile = rootTiles[0].southwestChild.northeastChild;
setCameraPosition(
quadtree,
frameState,
Rectangle.center(lookAtTile.rectangle),
lookAtTile.level
);
spyOn(mockTerrain, "requestTileGeometry").and.callThrough();
return process(quadtree, function () {
// Process until the lookAtTile is rendered. That tile's parent (level 1)
// should not be rendered along the way.
expect(quadtree._tilesToRender).not.toContain(lookAtTile.parent);
var lookAtTileRendered =
quadtree._tilesToRender.indexOf(lookAtTile) >= 0;
var continueProcessing = !lookAtTileRendered;
return continueProcessing;
}).then(function () {
// The lookAtTile should be a real tile, not a fill.
expect(quadtree._tilesToRender).toContain(lookAtTile);
expect(lookAtTile.data.fill).toBeUndefined();
expect(lookAtTile.data.vertexArray).toBeDefined();
// The parent of the lookAtTile should not have been requested.
var parent = lookAtTile.parent;
mockTerrain.requestTileGeometry.calls
.allArgs()
.forEach(function (call) {
expect(call.slice(0, 3)).not.toEqual([
parent.x,
parent.y,
parent.level,
]);
});
});
});
it("does not skip loading levels if availability is unknown", function () {
// Mark all tiles through level 2 as available.
rootTiles.forEach(function (tile) {
// level 0 tile
mockTerrain
.requestTileGeometryWillSucceed(tile)
.createMeshWillSucceed(tile);
tile.children.forEach(function (tile) {
// level 1 tile
mockTerrain
.willBeAvailable(tile)
.requestTileGeometryWillSucceed(tile)
.createMeshWillSucceed(tile);
tile.children.forEach(function (tile) {
// level 2 tile
mockTerrain
.willBeUnknownAvailability(tile)
.requestTileGeometryWillSucceed(tile)
.createMeshWillSucceed(tile);
});
});
});
quadtree.preloadAncestors = false;
// Look down at the center of a level 2 tile from a distance that will refine to it.
var lookAtTile = rootTiles[0].southwestChild.northeastChild;
setCameraPosition(
quadtree,
frameState,
Rectangle.center(lookAtTile.rectangle),
lookAtTile.level
);
spyOn(mockTerrain, "requestTileGeometry").and.callThrough();
return process(quadtree, function () {
// Process until the lookAtTile is rendered. That tile's parent (level 1)
// should not be rendered along the way, but it will be loaded.
expect(quadtree._tilesToRender).not.toContain(lookAtTile.parent);
var lookAtTileRendered =
quadtree._tilesToRender.indexOf(lookAtTile) >= 0;
var continueProcessing = !lookAtTileRendered;
return continueProcessing;
}).then(function () {
// The lookAtTile should be a real tile, not a fill.
expect(quadtree._tilesToRender).toContain(lookAtTile);
expect(lookAtTile.data.fill).toBeUndefined();
expect(lookAtTile.data.vertexArray).toBeDefined();
// The parent of the lookAtTile should have been requested before the lookAtTile itself.
var parent = lookAtTile.parent;
var allArgs = mockTerrain.requestTileGeometry.calls.allArgs();
var parentArgsIndex = allArgs.indexOf(
allArgs.filter(function (call) {
return (
call[0] === parent.x &&
call[1] === parent.y &&
call[2] === parent.level
);
})[0]
);
var lookAtArgsIndex = allArgs.indexOf(
allArgs.filter(function (call) {
return (
call[0] === lookAtTile.x &&
call[1] === lookAtTile.y &&
call[2] === lookAtTile.level
);
})[0]
);
expect(parentArgsIndex).toBeLessThan(lookAtArgsIndex);
});
});
it("loads and renders intermediate tiles according to loadingDescendantLimit", function () {
// Mark all tiles through level 2 as available.
rootTiles.forEach(function (tile) {
// level 0 tile
mockTerrain
.willBeAvailable(tile)
.requestTileGeometryWillSucceed(tile)
.createMeshWillSucceed(tile);
tile.children.forEach(function (tile) {
// level 1 tile
mockTerrain
.willBeAvailable(tile)
.requestTileGeometryWillSucceed(tile)
.createMeshWillSucceed(tile);
tile.children.forEach(function (tile) {
// level 2 tile
mockTerrain
.willBeAvailable(tile)
.requestTileGeometryWillSucceed(tile)
.createMeshWillSucceed(tile);
});
});
});
quadtree.preloadAncestors = false;
quadtree.loadingDescendantLimit = 1;
// Look down at the center of a level 2 tile from a distance that will refine to it.
var lookAtTile = rootTiles[0].southwestChild.northeastChild;
setCameraPosition(
quadtree,
frameState,
Rectangle.center(lookAtTile.rectangle),
lookAtTile.level
);
spyOn(mockTerrain, "requestTileGeometry").and.callThrough();
return process(quadtree, function () {
// First the lookAtTile's parent should be rendered.
var lookAtTileParentRendered =
quadtree._tilesToRender.indexOf(lookAtTile.parent) >= 0;
var continueProcessing = !lookAtTileParentRendered;
return continueProcessing;
})
.then(function () {
// The lookAtTile's parent should be a real tile, not a fill.
expect(quadtree._tilesToRender).toContain(lookAtTile.parent);
expect(lookAtTile.parent.data.fill).toBeUndefined();
expect(lookAtTile.parent.data.vertexArray).toBeDefined();
return process(quadtree, function () {
// Then the lookAtTile should be rendered.
var lookAtTileRendered =
quadtree._tilesToRender.indexOf(lookAtTile) >= 0;
var continueProcessing = !lookAtTileRendered;
return continueProcessing;
});
})
.then(function () {
// The lookAtTile should be a real tile, not a fill.
expect(quadtree._tilesToRender).toContain(lookAtTile);
expect(lookAtTile.data.fill).toBeUndefined();
expect(lookAtTile.data.vertexArray).toBeDefined();
});
});
it("continues rendering more detailed tiles when camera zooms out and an appropriate ancestor is not yet renderable", function () {
// Mark all tiles through level 2 as available.
rootTiles.forEach(function (tile) {
// level 0 tile
mockTerrain
.willBeAvailable(tile)
.requestTileGeometryWillSucceed(tile)
.createMeshWillSucceed(tile);
tile.children.forEach(function (tile) {
// level 1 tile
mockTerrain
.willBeAvailable(tile)
.requestTileGeometryWillSucceed(tile)
.createMeshWillSucceed(tile);
tile.children.forEach(function (tile) {
// level 2 tile
mockTerrain
.willBeAvailable(tile)
.requestTileGeometryWillSucceed(tile)
.createMeshWillSucceed(tile);
});
});
});
quadtree.preloadAncestors = false;
// Look down at the center of a level 2 tile from a distance that will refine to it.
var lookAtTile = rootTiles[0].southwestChild.northeastChild;
setCameraPosition(
quadtree,
frameState,
Rectangle.center(lookAtTile.rectangle),
lookAtTile.level
);
spyOn(mockTerrain, "requestTileGeometry").and.callThrough();
return process(quadtree, function () {
// Process until the lookAtTile is rendered. That tile's parent (level 1)
// should not be rendered along the way.
expect(quadtree._tilesToRender).not.toContain(lookAtTile.parent);
var lookAtTileRendered =
quadtree._tilesToRender.indexOf(lookAtTile) >= 0;
var continueProcessing = !lookAtTileRendered;
return continueProcessing;
})
.then(function () {
// Zoom out so the parent tile no longer needs to refine to meet SSE.
setCameraPosition(
quadtree,
frameState,
Rectangle.center(lookAtTile.rectangle),
lookAtTile.parent.level
);
// Select new tiles
quadtree.beginFrame(frameState);
quadtree.render(frameState);
quadtree.endFrame(frameState);
// The lookAtTile should still be rendered, not it's parent.
expect(quadtree._tilesToRender).toContain(lookAtTile);
expect(quadtree._tilesToRender).not.toContain(lookAtTile.parent);
return process(quadtree, function () {
// Eventually the parent should be rendered instead.
var parentRendered =
quadtree._tilesToRender.indexOf(lookAtTile.parent) >= 0;
var continueProcessing = !parentRendered;
return continueProcessing;
});
})
.then(function () {
expect(quadtree._tilesToRender).not.toContain(lookAtTile);
expect(quadtree._tilesToRender).toContain(lookAtTile.parent);
});
});
it("renders a fill for a newly-visible tile", function () {
// Mark all tiles through level 2 as available.
rootTiles.forEach(function (tile) {
// level 0 tile
mockTerrain
.willBeAvailable(tile)
.requestTileGeometryWillSucceed(tile)
.createMeshWillSucceed(tile);
tile.children.forEach(function (tile) {
// level 1 tile
mockTerrain
.willBeAvailable(tile)
.requestTileGeometryWillSucceed(tile)
.createMeshWillSucceed(tile);
tile.children.forEach(function (tile) {
// level 2 tile
mockTerrain
.willBeAvailable(tile)
.requestTileGeometryWillSucceed(tile)
.createMeshWillSucceed(tile);
});
});
});
quadtree.preloadAncestors = false;
var visibleTile = rootTiles[0].southwestChild.northeastChild;
var notVisibleTile = rootTiles[0].southwestChild.northwestChild;
frameState.cullingVolume.computeVisibility.and.callFake(function (
boundingVolume
) {
if (!defined(visibleTile.data)) {
return Intersect.INTERSECTING;
}
if (boundingVolume === visibleTile.data.orientedBoundingBox) {
return Intersect.INTERSECTING;
} else if (boundingVolume === notVisibleTile.data.orientedBoundingBox) {
return Intersect.OUTSIDE;
}
return Intersect.INTERSECTING;
});
// Look down at the center of the visible tile.
setCameraPosition(
quadtree,
frameState,
Rectangle.center(visibleTile.rectangle),
visibleTile.level
);
spyOn(mockTerrain, "requestTileGeometry").and.callThrough();
return process(quadtree, function () {
// Process until the visibleTile is rendered.
var visibleTileRendered =
quadtree._tilesToRender.indexOf(visibleTile) >= 0;
var continueProcessing = !visibleTileRendered;
return continueProcessing;
}).then(function () {
expect(quadtree._tilesToRender).not.toContain(notVisibleTile);
// Now treat the not-visible-tile as visible.
frameState.cullingVolume.computeVisibility.and.returnValue(
Intersect.INTERSECTING
);
// Select new tiles
quadtree.beginFrame(frameState);
quadtree.render(frameState);
quadtree.endFrame(frameState);
// The notVisibleTile should be rendered as a fill.
expect(quadtree._tilesToRender).toContain(visibleTile);
expect(quadtree._tilesToRender).toContain(notVisibleTile);
expect(notVisibleTile.data.fill).toBeDefined();
expect(notVisibleTile.data.vertexArray).toBeUndefined();
});
});
});
describe(
"with mock tile provider",
function () {
var scene;
beforeAll(function () {
scene = createScene();
scene.render();
});
afterAll(function () {
scene.destroyForSpecs();
});
function createSpyTileProvider() {
var result = jasmine.createSpyObj("tileProvider", [
"getQuadtree",
"setQuadtree",
"getReady",
"getTilingScheme",
"getErrorEvent",
"initialize",
"updateImagery",
"beginUpdate",
"endUpdate",
"getLevelMaximumGeometricError",
"loadTile",
"computeTileVisibility",
"showTileThisFrame",
"computeDistanceToTile",
"canRefine",
"isDestroyed",
"destroy",
]);
Object.defineProperties(result, {
quadtree: {
get: result.getQuadtree,
set: result.setQuadtree,
},
ready: {
get: result.getReady,
},
tilingScheme: {
get: result.getTilingScheme,
},
errorEvent: {
get: result.getErrorEvent,
},
});
var tilingScheme = new GeographicTilingScheme();
result.getTilingScheme.and.returnValue(tilingScheme);
result.canRefine.and.callFake(function (tile) {
return tile.renderable;
});
return result;
}
it("calls initialize, beginUpdate, loadTile, and endUpdate", function () {
var tileProvider = createSpyTileProvider();
tileProvider.getReady.and.returnValue(true);
var quadtree = new QuadtreePrimitive({
tileProvider: tileProvider,
});
// determine what tiles to load
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
// load tiles
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
expect(tileProvider.initialize).toHaveBeenCalled();
expect(tileProvider.beginUpdate).toHaveBeenCalled();
expect(tileProvider.loadTile).toHaveBeenCalled();
expect(tileProvider.endUpdate).toHaveBeenCalled();
});
it("shows the root tiles when they are ready and visible", function () {
var tileProvider = createSpyTileProvider();
tileProvider.getReady.and.returnValue(true);
tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
tileProvider.loadTile.and.callFake(function (frameState, tile) {
tile.renderable = true;
});
var quadtree = new QuadtreePrimitive({
tileProvider: tileProvider,
});
// determine what tiles to load
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
// load tiles
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
expect(tileProvider.showTileThisFrame).toHaveBeenCalled();
});
it("stops loading a tile that moves to the DONE state", function () {
var tileProvider = createSpyTileProvider();
tileProvider.getReady.and.returnValue(true);
tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
var calls = 0;
tileProvider.loadTile.and.callFake(function (frameState, tile) {
++calls;
tile.state = QuadtreeTileLoadState.DONE;
});
var quadtree = new QuadtreePrimitive({
tileProvider: tileProvider,
});
// determine what tiles to load
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
// load tiles
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
expect(calls).toBe(2);
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
expect(calls).toBe(2);
});
it("tileLoadProgressEvent is raised when tile loaded and when new children discovered", function () {
var eventHelper = new EventHelper();
var tileProvider = createSpyTileProvider();
tileProvider.getReady.and.returnValue(true);
tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
var quadtree = new QuadtreePrimitive({
tileProvider: tileProvider,
});
var progressEventSpy = jasmine.createSpy("progressEventSpy");
eventHelper.add(quadtree.tileLoadProgressEvent, progressEventSpy);
// Initial update to get the zero-level tiles set up.
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
// load zero-level tiles
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
quadtree.update(scene.frameState);
scene.renderForSpecs();
// There will now be two zero-level tiles in the load queue.
expect(progressEventSpy.calls.mostRecent().args[0]).toEqual(2);
// Change one to loaded and update again
quadtree._levelZeroTiles[0].state = QuadtreeTileLoadState.DONE;
quadtree._levelZeroTiles[1].state = QuadtreeTileLoadState.LOADING;
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
quadtree.update(scene.frameState);
scene.renderForSpecs();
// Now there should only be one left in the update queue
expect(progressEventSpy.calls.mostRecent().args[0]).toEqual(1);
// Simulate the second zero-level child having loaded with two children.
quadtree._levelZeroTiles[1].state = QuadtreeTileLoadState.DONE;
quadtree._levelZeroTiles[1].renderable = true;
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
quadtree.update(scene.frameState);
scene.renderForSpecs();
// Now that tile's four children should be in the load queue.
expect(progressEventSpy.calls.mostRecent().args[0]).toEqual(4);
});
it("forEachLoadedTile does not enumerate tiles in the START state", function () {
var tileProvider = createSpyTileProvider();
tileProvider.getReady.and.returnValue(true);
tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
tileProvider.computeDistanceToTile.and.returnValue(1e-15);
// Load the root tiles.
tileProvider.loadTile.and.callFake(function (frameState, tile) {
tile.state = QuadtreeTileLoadState.DONE;
tile.renderable = true;
});
var quadtree = new QuadtreePrimitive({
tileProvider: tileProvider,
});
// determine what tiles to load
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
// load tiles
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
// Don't load further tiles.
tileProvider.loadTile.and.callFake(function (frameState, tile) {
tile.state = QuadtreeTileLoadState.START;
});
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
quadtree.forEachLoadedTile(function (tile) {
expect(tile.state).not.toBe(QuadtreeTileLoadState.START);
});
});
it("add and remove callbacks to tiles", function () {
var tileProvider = createSpyTileProvider();
tileProvider.getReady.and.returnValue(true);
tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
tileProvider.computeDistanceToTile.and.returnValue(1e-15);
// Load the root tiles.
tileProvider.loadTile.and.callFake(function (frameState, tile) {
tile.state = QuadtreeTileLoadState.DONE;
tile.renderable = true;
tile.data = {
pick: function () {
return undefined;
},
};
});
var quadtree = new QuadtreePrimitive({
tileProvider: tileProvider,
});
var removeFunc = quadtree.updateHeight(
Cartographic.fromDegrees(-72.0, 40.0),
function (position) {}
);
// determine what tiles to load
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
++scene.frameState.frameNumber;
// load tiles
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
var addedCallback = false;
quadtree.forEachLoadedTile(function (tile) {
addedCallback = addedCallback || tile.customData.length > 0;
});
expect(addedCallback).toEqual(true);
removeFunc();
++scene.frameState.frameNumber;
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
var removedCallback = true;
quadtree.forEachLoadedTile(function (tile) {
removedCallback = removedCallback && tile.customData.length === 0;
});
expect(removedCallback).toEqual(true);
});
it("updates heights", function () {
var tileProvider = createSpyTileProvider();
tileProvider.getReady.and.returnValue(true);
tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
tileProvider.computeDistanceToTile.and.returnValue(1e-15);
tileProvider.terrainProvider = {
getTileDataAvailable: function () {
return true;
},
};
var position = Cartesian3.clone(Cartesian3.ZERO);
var updatedPosition = Cartesian3.clone(Cartesian3.UNIT_X);
var currentPosition = position;
// Load the root tiles.
tileProvider.loadTile.and.callFake(function (frameState, tile) {
tile.state = QuadtreeTileLoadState.DONE;
tile.renderable = true;
tile.data = {
pick: function () {
return currentPosition;
},
mesh: {},
};
});
var quadtree = new QuadtreePrimitive({
tileProvider: tileProvider,
});
quadtree.updateHeight(Cartographic.fromDegrees(-72.0, 40.0), function (
p
) {
Cartesian3.clone(p, position);
});
// determine what tiles to load
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
// load tiles
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
expect(position).toEqual(Cartesian3.ZERO);
currentPosition = updatedPosition;
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
expect(position).toEqual(updatedPosition);
});
it("gives correct priority to tile loads", function () {
var tileProvider = createSpyTileProvider();
tileProvider.getReady.and.returnValue(true);
tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
var quadtree = new QuadtreePrimitive({
tileProvider: tileProvider,
});
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
// The root tiles should be in the high priority load queue
expect(quadtree._tileLoadQueueHigh.length).toBe(2);
expect(quadtree._tileLoadQueueHigh).toContain(
quadtree._levelZeroTiles[0]
);
expect(quadtree._tileLoadQueueHigh).toContain(
quadtree._levelZeroTiles[1]
);
expect(quadtree._tileLoadQueueMedium.length).toBe(0);
expect(quadtree._tileLoadQueueLow.length).toBe(0);
// Mark the first root tile renderable (but not done loading)
quadtree._levelZeroTiles[0].renderable = true;
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
// That root tile should now load with low priority while its children should load with high.
expect(quadtree._tileLoadQueueHigh.length).toBe(5);
expect(quadtree._tileLoadQueueHigh).toContain(
quadtree._levelZeroTiles[1]
);
expect(quadtree._tileLoadQueueHigh).toContain(
quadtree._levelZeroTiles[0].children[0]
);
expect(quadtree._tileLoadQueueHigh).toContain(
quadtree._levelZeroTiles[0].children[1]
);
expect(quadtree._tileLoadQueueHigh).toContain(
quadtree._levelZeroTiles[0].children[2]
);
expect(quadtree._tileLoadQueueHigh).toContain(
quadtree._levelZeroTiles[0].children[3]
);
expect(quadtree._tileLoadQueueMedium.length).toBe(0);
expect(quadtree._tileLoadQueueLow.length).toBe(1);
expect(quadtree._tileLoadQueueLow).toContain(
quadtree._levelZeroTiles[0]
);
// Mark the children of that root tile renderable too, so we can refine it
quadtree._levelZeroTiles[0].children[0].renderable = true;
quadtree._levelZeroTiles[0].children[1].renderable = true;
quadtree._levelZeroTiles[0].children[2].renderable = true;
quadtree._levelZeroTiles[0].children[3].renderable = true;
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
expect(quadtree._tileLoadQueueHigh.length).toBe(17); // levelZeroTiles[1] plus levelZeroTiles[0]'s 16 grandchildren
expect(quadtree._tileLoadQueueHigh).toContain(
quadtree._levelZeroTiles[1]
);
expect(quadtree._tileLoadQueueHigh).toContain(
quadtree._levelZeroTiles[0].children[0].children[0]
);
expect(quadtree._tileLoadQueueHigh).toContain(
quadtree._levelZeroTiles[0].children[0].children[1]
);
expect(quadtree._tileLoadQueueHigh).toContain(
quadtree._levelZeroTiles[0].children[0].children[2]
);
expect(quadtree._tileLoadQueueHigh).toContain(
quadtree._levelZeroTiles[0].children[0].children[3]
);
expect(quadtree._tileLoadQueueMedium.length).toBe(0);
expect(quadtree._tileLoadQueueLow.length).toBe(5);
expect(quadtree._tileLoadQueueLow).toContain(
quadtree._levelZeroTiles[0]
);
expect(quadtree._tileLoadQueueLow).toContain(
quadtree._levelZeroTiles[0].children[0]
);
expect(quadtree._tileLoadQueueLow).toContain(
quadtree._levelZeroTiles[0].children[1]
);
expect(quadtree._tileLoadQueueLow).toContain(
quadtree._levelZeroTiles[0].children[2]
);
expect(quadtree._tileLoadQueueLow).toContain(
quadtree._levelZeroTiles[0].children[3]
);
// Mark the children of levelZeroTiles[0] upsampled
quadtree._levelZeroTiles[0].children[0].upsampledFromParent = true;
quadtree._levelZeroTiles[0].children[1].upsampledFromParent = true;
quadtree._levelZeroTiles[0].children[2].upsampledFromParent = true;
quadtree._levelZeroTiles[0].children[3].upsampledFromParent = true;
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
// levelZeroTiles[0] should move to medium priority.
expect(quadtree._tileLoadQueueHigh.length).toBe(1);
expect(quadtree._tileLoadQueueHigh).toContain(
quadtree._levelZeroTiles[1]
);
expect(quadtree._tileLoadQueueMedium.length).toBe(1);
expect(quadtree._tileLoadQueueMedium).toContain(
quadtree._levelZeroTiles[0]
);
expect(quadtree._tileLoadQueueLow.length).toBe(0);
});
it("renders tiles in approximate near-to-far order", function () {
var tileProvider = createSpyTileProvider();
tileProvider.getReady.and.returnValue(true);
tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
var quadtree = new QuadtreePrimitive({
tileProvider: tileProvider,
});
tileProvider.loadTile.and.callFake(function (frameState, tile) {
if (tile.level <= 1) {
tile.state = QuadtreeTileLoadState.DONE;
tile.renderable = true;
}
});
scene.camera.setView({
destination: Cartesian3.fromDegrees(1.0, 1.0, 15000.0),
});
scene.camera.update(scene.mode);
return pollToPromise(function () {
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
return (
quadtree._tilesToRender.filter(function (tile) {
return tile.level === 1;
}).length === 8
);
}).then(function () {
quadtree.update(scene.frameState);
quadtree.beginFrame(scene.frameState);
quadtree.render(scene.frameState);
quadtree.endFrame(scene.frameState);
// Rendered tiles:
// +----+----+----+----+
// |w.nw|w.ne|e.nw|e.ne|
// +----+----+----+----+
// |w.sw|w.se|e.sw|e.se|
// +----+----+----+----+
// camera is located in e.nw (east.northwestChild)
var west = quadtree._levelZeroTiles.filter(function (tile) {
return tile.x === 0;
})[0];
var east = quadtree._levelZeroTiles.filter(function (tile) {
return tile.x === 1;
})[0];
expect(quadtree._tilesToRender[0]).toBe(east.northwestChild);
expect(
quadtree._tilesToRender[1] === east.southwestChild ||
quadtree._tilesToRender[1] === east.northeastChild
).toBe(true);
expect(
quadtree._tilesToRender[2] === east.southwestChild ||
quadtree._tilesToRender[2] === east.northeastChild
).toBe(true);
expect(quadtree._tilesToRender[3]).toBe(east.southeastChild);
expect(quadtree._tilesToRender[4]).toBe(west.northeastChild);
expect(
quadtree._tilesToRender[5] === west.northwestChild ||
quadtree._tilesToRender[5] === west.southeastChild
).toBe(true);
expect(
quadtree._tilesToRender[6] === west.northwestChild ||
quadtree._tilesToRender[6] === west.southeastChild
).toBe(true);
expect(quadtree._tilesToRender[7]).toBe(west.southwestChild);
});
});
},
"WebGL"
);
// Sets the camera to look at a given cartographic position from a distance
// that will produce a screen-space error at that position that will refine to
// a given tile level and no further.
function setCameraPosition(quadtree, frameState, position, level) {
var camera = frameState.camera;
var geometricError = quadtree.tileProvider.getLevelMaximumGeometricError(
level
);
var sse = quadtree.maximumScreenSpaceError * 0.8;
var sseDenominator = camera.frustum.sseDenominator;
var height = frameState.context.drawingBufferHeight;
var distance = (geometricError * height) / (sse * sseDenominator);
var cartesian = Ellipsoid.WGS84.cartographicToCartesian(position);
camera.lookAt(cartesian, new Cartesian3(0.0, 0.0, distance));
}
});