diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 93299a7e13eb..d4797e40a5ab 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -17,6 +17,7 @@ define([ '../Core/getStringFromTypedArray', '../Core/Intersect', '../Core/joinUrls', + '../Core/JulianDate', '../Core/loadArrayBuffer', '../Core/Matrix3', '../Core/Matrix4', @@ -60,6 +61,7 @@ define([ getStringFromTypedArray, Intersect, joinUrls, + JulianDate, loadArrayBuffer, Matrix3, Matrix4, @@ -118,8 +120,6 @@ define([ */ this.computedTransform = computedTransform; - this._transformDirty = true; - this._boundingVolume = this.createBoundingVolume(header.boundingVolume, computedTransform); this._boundingVolume2D = undefined; @@ -212,6 +212,7 @@ define([ this._contentState = contentState; this._contentReadyToProcessPromise = undefined; this._contentReadyPromise = undefined; + this._expiredContent = undefined; this._requestServer = requestServer; @@ -261,6 +262,30 @@ define([ */ this.replacementNode = undefined; + var expire = header.expire; + var expireDuration; + var expireDate; + if (defined(expire)) { + expireDuration = expire.duration; + if (defined(expire.date)) { + expireDate = JulianDate.fromIso8601(expire.date); + } + } + + /** + * The time in seconds after the tile's content is ready when the content expires and new content is requested. + * + * @type {Number} + */ + this.expireDuration = expireDuration; + + /** + * The date when the content expires and new content is requested. + * + * @type {JulianDate} + */ + this.expireDate = expireDate; + // Members that are updated every frame for tree traversal and rendering optimizations: /** @@ -393,26 +418,28 @@ define([ }, /** - * Whether the computedTransform has changed this frame. - * - * @memberof Cesium3DTile.prototype - * - * @type {Boolean} * @readonly + * @private */ - transformDirty : { + requestServer : { get : function() { - return this._transformDirty; + return this._requestServer; } }, /** + * Determines if the tile has available content to render. true if the tile's + * content is ready or if it has expired content that renders while new content loads; otherwise, + * false. + * + * @memberof Cesium3DTile.prototype + * + * @type {Boolean} * @readonly - * @private */ - requestServer : { + contentAvailable : { get : function() { - return this._requestServer; + return this.contentReady || (defined(this._expiredContent) && this._contentState !== Cesium3DTileContentState.FAILED); } }, @@ -446,6 +473,21 @@ define([ } }, + /** + * Determines if the tile's content is expired. true if tile's + * content is expired; otherwise, false. + * + * @memberof Cesium3DTile.prototype + * + * @type {Boolean} + * @readonly + */ + contentExpired : { + get : function() { + return this._contentState === Cesium3DTileContentState.EXPIRED; + } + }, + /** * Gets the promise that will be resolved when the tile's content is ready to process. * This happens after the content is downloaded but before the content is ready @@ -487,6 +529,38 @@ define([ } }); + var scratchJulianDate = new JulianDate(); + + /** + * Update whether the tile has expired. + * + * @private + */ + Cesium3DTile.prototype.updateExpiration = function() { + if (defined(this.expireDate) && this.contentReady && !this.hasEmptyContent) { + var now = JulianDate.now(scratchJulianDate); + if (JulianDate.lessThan(this.expireDate, now)) { + this._contentState = Cesium3DTileContentState.EXPIRED; + this._expiredContent = this._content; + } + } + }; + + function updateExpireDate(tile) { + if (defined(tile.expireDuration)) { + var expireDurationDate = JulianDate.now(scratchJulianDate); + JulianDate.addSeconds(expireDurationDate, tile.expireDuration, expireDurationDate); + + if (defined(tile.expireDate)) { + if (JulianDate.lessThan(tile.expireDate, expireDurationDate)) { + JulianDate.clone(expireDurationDate, tile.expireDate); + } + } else { + tile.expireDate = JulianDate.clone(expireDurationDate); + } + } + } + /** * Requests the tile's content. *

@@ -506,9 +580,16 @@ define([ return false; } + var url = this._contentUrl; + if (defined(this.expireDate)) { + // Append a query parameter of the tile expiration date to prevent caching + var timestampQuery = '?expired=' + this.expireDate.toString(); + url = joinUrls(url, timestampQuery, false); + } + var distance = this.distanceToCamera; var promise = RequestScheduler.schedule(new Request({ - url : this._contentUrl, + url : url, server : this._requestServer, requestFunction : loadArrayBuffer, type : RequestType.TILES3D, @@ -525,7 +606,8 @@ define([ promise.then(function(arrayBuffer) { if (that.isDestroyed()) { - return when.reject('tileset is destroyed'); + // Tile is unloaded before the content finishes loading + return when.reject('tile is destroyed'); } var uint8Array = new Uint8Array(arrayBuffer); var magic = getMagic(uint8Array); @@ -546,6 +628,16 @@ define([ that._contentReadyToProcessPromise.resolve(content); content.readyPromise.then(function(content) { + if (that.isDestroyed()) { + // Tile is unloaded before the content finishes processing + return when.reject('tile is destroyed'); + } + updateExpireDate(that); + + // Refresh style for expired content + that.lastSelectedFrameNumber = 0; + that.lastStyleTime = 0; + that._contentState = Cesium3DTileContentState.READY; that._contentReadyPromise.resolve(content); }).otherwise(function(error) { @@ -595,10 +687,6 @@ define([ this.replacementNode = undefined; - // Restore properties set per frame to their defaults - this.distanceToCamera = 0; - this.visibilityPlaneMask = 0; - this.selected = false; this.lastSelectedFrameNumber = 0; this.lastStyleTime = 0; @@ -786,9 +874,8 @@ define([ Cesium3DTile.prototype.updateTransform = function(parentTransform) { parentTransform = defaultValue(parentTransform, Matrix4.IDENTITY); var computedTransform = Matrix4.multiply(parentTransform, this.transform, scratchTransform); - var transformDirty = !Matrix4.equals(computedTransform, this.computedTransform); - if (transformDirty) { - this._transformDirty = true; + var transformChanged = !Matrix4.equals(computedTransform, this.computedTransform); + if (transformChanged) { Matrix4.clone(computedTransform, this.computedTransform); // Update the bounding volumes @@ -851,6 +938,20 @@ define([ } } + function updateContent(tile, tileset, frameState) { + var content = tile._content; + var expiredContent = tile._expiredContent; + + if (defined(expiredContent) && !tile.contentReady) { + // Render the expired content while the content loads + expiredContent.update(tileset, frameState); + return; + } + + tile._expiredContent = tile._expiredContent && tile._expiredContent.destroy(); + content.update(tileset, frameState); + } + /** * Get the draw commands needed to render this tile. * @@ -859,8 +960,7 @@ define([ Cesium3DTile.prototype.update = function(tileset, frameState) { var initCommandLength = frameState.commandList.length; applyDebugSettings(this, tileset, frameState); - this._content.update(tileset, frameState); - this._transformDirty = false; + updateContent(this, tileset, frameState); this._commandsLength = frameState.commandList.length - initCommandLength; }; @@ -895,7 +995,9 @@ define([ * @private */ Cesium3DTile.prototype.destroy = function() { + // For the interval between new content being requested and downloaded, expiredContent === content, so don't destroy twice this._content = this._content && this._content.destroy(); + this._expiredContent = this._expiredContent && !this._expiredContent.isDestroyed() && this._expiredContent.destroy(); this._debugBoundingVolume = this._debugBoundingVolume && this._debugBoundingVolume.destroy(); this._debugContentBoundingVolume = this._debugContentBoundingVolume && this._debugContentBoundingVolume.destroy(); this._debugViewerRequestVolume = this._debugViewerRequestVolume && this._debugViewerRequestVolume.destroy(); diff --git a/Source/Scene/Cesium3DTileContentState.js b/Source/Scene/Cesium3DTileContentState.js index 986e821b3e33..98fdad9a96bf 100644 --- a/Source/Scene/Cesium3DTileContentState.js +++ b/Source/Scene/Cesium3DTileContentState.js @@ -13,8 +13,9 @@ define([ LOADING : 1, // Is waiting on a pending request PROCESSING : 2, // Request received. Contents are being processed for rendering. Depending on the content, it might make its own requests for external data. READY : 3, // Ready to render. - FAILED : 4 // Request failed. + EXPIRED : 4, // Is expired and will be unloaded once new content is loaded. + FAILED : 5 // Request failed. }; return freezeObject(Cesium3DTileContentState); -}); \ No newline at end of file +}); diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 8a9366fea9ee..44946b40d22c 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -483,7 +483,7 @@ define([ this.allTilesLoaded = new Event(); /** - * The event fired to indicate that a tile's content was unloaded from the cache. + * The event fired to indicate that a tile's content was unloaded. *

* The unloaded {@link Cesium3DTile} is passed to the event listener. *

@@ -1213,6 +1213,27 @@ define([ /////////////////////////////////////////////////////////////////////////// + function destroySubtree(tileset, tile) { + var root = tile; + var stats = tileset._statistics; + var stack = scratchStack; + stack.push(tile); + while (stack.length > 0) { + tile = stack.pop(); + var children = tile.children; + var length = children.length; + for (var i = 0; i < length; ++i) { + stack.push(children[i]); + } + if (tile !== root) { + unloadTileFromCache(tileset, tile); + tile.destroy(); + --stats.numberTotal; + } + } + root.children = []; + } + function requestContent(tileset, tile, outOfCore) { if (!outOfCore) { return; @@ -1223,7 +1244,7 @@ define([ } var stats = tileset._statistics; - + var expired = tile.contentExpired; var requested = tile.requestContent(); if (!requested) { @@ -1231,6 +1252,15 @@ define([ return; } + if (expired) { + if (tile.hasRenderableContent) { + decrementPointAndFeatureLoadCounts(tileset, tile.content); + --tileset._statistics.numberContentReady; + } else if (tile.hasTilesetContent) { + destroySubtree(tileset, tile); + } + } + ++stats.numberOfPendingRequests; var removeFunction = removeFromProcessingQueue(tileset, tile); @@ -1277,12 +1307,17 @@ define([ // Remove from processing queue tileset._processingQueue.splice(index, 1); --tileset._statistics.numberProcessing; + if (tile.hasRenderableContent) { // RESEARCH_IDEA: ability to unload tiles (without content) for an // external tileset when all the tiles are unloaded. - ++tileset._statistics.numberContentReady; incrementPointAndFeatureLoadCounts(tileset, tile.content); - tile.replacementNode = tileset._replacementList.add(tile); + ++tileset._statistics.numberContentReady; + + // Add to the tile cache. Previously expired tiles are already in the cache. + if (!defined(tile.replacementNode)) { + tile.replacementNode = tileset._replacementList.add(tile); + } } } else { // Not in processing queue @@ -1465,7 +1500,6 @@ define([ var selectedTiles = tileset._selectedTiles; var length = selectedTiles.length; var tileVisible = tileset.tileVisible; - var tile, i; var bivariateVisibilityTest = tileset.skipLODs && tileset._hasMixedContent && frameState.context.stencilBuffer && length > 0; @@ -1547,14 +1581,28 @@ define([ } } - function unloadTiles(tileset, frameState) { - var trimTiles = tileset._trimTiles; - tileset._trimTiles = false; + function unloadTileFromCache(tileset, tile) { + var node = tile.replacementNode; + if (!defined(node)) { + return; + } var stats = tileset._statistics; var replacementList = tileset._replacementList; var tileUnload = tileset.tileUnload; + tileUnload.raiseEvent(tile); + replacementList.remove(node); + decrementPointAndFeatureLoadCounts(tileset, tile.content); + --stats.numberContentReady; + } + + function unloadTiles(tileset) { + var trimTiles = tileset._trimTiles; + tileset._trimTiles = false; + + var replacementList = tileset._replacementList; + var totalMemoryUsageInBytes = tileset.totalMemoryUsageInBytes; var maximumMemoryUsageInBytes = tileset._maximumMemoryUsage * 1024 * 1024; @@ -1566,16 +1614,9 @@ define([ var node = replacementList.head; while ((node !== sentinel) && ((totalMemoryUsageInBytes > maximumMemoryUsageInBytes) || trimTiles)) { var tile = node.item; - - decrementPointAndFeatureLoadCounts(tileset, tile.content); - tileUnload.raiseEvent(tile); - tile.unloadContent(); - - var currentNode = node; node = node.next; - replacementList.remove(currentNode); - - --stats.numberContentReady; + unloadTileFromCache(tileset, tile); + tile.unloadContent(); totalMemoryUsageInBytes = tileset.totalMemoryUsageInBytes; } } @@ -1664,7 +1705,7 @@ define([ updateTiles(this, frameState); if (outOfCore) { - unloadTiles(this, frameState); + unloadTiles(this); } // Events are raised (added to the afterRender queue) here since promises diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 7badf1bc328f..da541691cf31 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -64,10 +64,7 @@ define([ return; } - if (root.contentUnloaded) { - loadTile(root, frameState); - return; - } + loadTile(root, frameState); if (!tileset.skipLODs) { // just execute base traversal and add tiles to _desiredTiles @@ -117,7 +114,7 @@ define([ } var loadedTile = original._ancestorWithLoadedContent; - if (original.hasRenderableContent && original.contentReady) { + if (original.hasRenderableContent && original.contentAvailable) { loadedTile = original; } @@ -134,13 +131,13 @@ define([ for (var j = 0; j < childrenLength; ++j) { var child = children[j]; touch(tileset, child, outOfCore); - if (child.contentReady) { + if (child.contentAvailable) { child.selected = true; child._finalResolution = true; child._selectedFrame = frameState.frameNumber; } if (child._depth - original._depth < 2) { // prevent traversing too far - if (!child.contentReady || child.refine === Cesium3DTileRefine.ADD) { + if (!child.contentAvailable || child.refine === Cesium3DTileRefine.ADD) { descendantStack.push(child); } } @@ -237,7 +234,7 @@ define([ function selectTile(tileset, tile, frameState) { // There may also be a tight box around just the tile's contents, e.g., for a city, we may be // zoomed into a neighborhood and can cull the skyscrapers in the root node. - if (tile.contentReady && ( + if (tile.contentAvailable && ( (tile.visibilityPlaneMask === CullingVolume.MASK_INSIDE) || (tile.contentVisibility(frameState) !== Intersect.OUTSIDE) )) { @@ -317,7 +314,7 @@ define([ // content cannot be replaced until all of the nearest descendants with content are all loaded if (replacementWithContent) { if (!child.hasEmptyContent) { - allReady = allReady && child.contentReady; + allReady = allReady && child.contentAvailable; } else { allReady = allReady && this.internalDFS.execute(child); } @@ -336,6 +333,11 @@ define([ tileset._desiredTiles.push(tile); } + // Stop traversal on the subtree since it will be destroyed + if (tile.hasTilesetContent && tile.contentExpired) { + return false; + } + // stop traversal when we've attained the desired level of error if (tile._screenSpaceError <= baseScreenSpaceError && !tile.hasTilesetContent) { // update children so the leaf handler can check if any are visible for the children union bound optimization @@ -401,7 +403,7 @@ define([ var child = children[i]; loadTile(child, this.frameState); touch(this.tileset, child, this.outOfCore); - if (!tile.contentReady) { + if (!tile.contentAvailable) { this.allLoaded = false; } } @@ -489,6 +491,11 @@ define([ var tileset = this.tileset; var maximumScreenSpaceError = tileset._maximumScreenSpaceError; + // Stop traversal on the subtree since it will be destroyed + if (tile.hasTilesetContent && tile.contentExpired) { + return emptyArray; + } + if (!tile.hasTilesetContent) { if (hasAdditiveContent(tile)) { tileset._desiredTiles.push(tile); @@ -593,13 +600,14 @@ define([ tile._finalResolution = false; computeSSE(tile, frameState); touch(tileset, tile, outOfCore); + tile.updateExpiration(); tile._ancestorWithContent = undefined; tile._ancestorWithLoadedContent = undefined; var parent = tile.parent; if (defined(parent)) { var replace = parent.refine === Cesium3DTileRefine.REPLACE; tile._ancestorWithContent = (replace && parent.hasRenderableContent) ? parent : parent._ancestorWithContent; - tile._ancestorWithLoadedContent = (replace && parent.hasRenderableContent && parent.contentReady) ? parent : parent._ancestorWithLoadedContent; + tile._ancestorWithLoadedContent = (replace && parent.hasRenderableContent && parent.contentAvailable) ? parent : parent._ancestorWithLoadedContent; } } @@ -621,7 +629,7 @@ define([ } function loadTile(tile, frameState) { - if (tile.contentUnloaded && tile._requestedFrame !== frameState.frameNumber) { + if ((tile.contentUnloaded || tile.contentExpired) && tile._requestedFrame !== frameState.frameNumber) { tile._requestedFrame = frameState.frameNumber; computeSSE(tile, frameState); tile._requestHeap.insert(tile); @@ -816,4 +824,4 @@ define([ Cesium3DTilesetTraversal.SkipTraversal = SkipTraversal; return Cesium3DTilesetTraversal; -}); \ No newline at end of file +}); diff --git a/Source/Scene/PointCloud3DTileContent.js b/Source/Scene/PointCloud3DTileContent.js index 21bc51446ced..db138636504f 100644 --- a/Source/Scene/PointCloud3DTileContent.js +++ b/Source/Scene/PointCloud3DTileContent.js @@ -115,6 +115,7 @@ define([ this._quantizedVolumeScale = undefined; this._quantizedVolumeOffset = undefined; + this._modelMatrix = Matrix4.clone(Matrix4.IDENTITY); this._mode = undefined; /** @@ -1151,7 +1152,10 @@ define([ * Part of the {@link Cesium3DTileContent} interface. */ PointCloud3DTileContent.prototype.update = function(tileset, frameState) { - var updateModelMatrix = this._tile.transformDirty || this._mode !== frameState.mode; + var modelMatrix = this._tile.computedTransform; + var modelMatrixChanged = !Matrix4.equals(this._modelMatrix, modelMatrix); + var updateModelMatrix = modelMatrixChanged || this._mode !== frameState.mode; + this._mode = frameState.mode; if (!defined(this._drawCommand)) { @@ -1164,17 +1168,18 @@ define([ } if (updateModelMatrix) { + Matrix4.clone(modelMatrix, this._modelMatrix); if (defined(this._rtcCenter)) { - Matrix4.multiplyByTranslation(this._tile.computedTransform, this._rtcCenter, this._drawCommand.modelMatrix); + Matrix4.multiplyByTranslation(modelMatrix, this._rtcCenter, this._drawCommand.modelMatrix); } else if (defined(this._quantizedVolumeOffset)) { - Matrix4.multiplyByTranslation(this._tile.computedTransform, this._quantizedVolumeOffset, this._drawCommand.modelMatrix); + Matrix4.multiplyByTranslation(modelMatrix, this._quantizedVolumeOffset, this._drawCommand.modelMatrix); } else { - Matrix4.clone(this._tile.computedTransform, this._drawCommand.modelMatrix); + Matrix4.clone(modelMatrix, this._drawCommand.modelMatrix); } if (frameState.mode !== SceneMode.SCENE3D) { var projection = frameState.mapProjection; - var modelMatrix = this._drawCommand.modelMatrix; + modelMatrix = this._drawCommand.modelMatrix; var translation = Matrix4.getColumn(modelMatrix, 3, scratchComputedTranslation); if (!Cartesian4.equals(translation, Cartesian4.UNIT_W)) { Transforms.basisTo2D(projection, modelMatrix, modelMatrix); diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedExpiration/batchedExpiration.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedExpiration/batchedExpiration.b3dm new file mode 100644 index 000000000000..b3a7e3cb36ce Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedExpiration/batchedExpiration.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedExpiration/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedExpiration/tileset.json new file mode 100644 index 000000000000..9682054f143d --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedExpiration/tileset.json @@ -0,0 +1,44 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 12.83180232718587 + } + }, + "geometricError": 70, + "root": { + "expire": { + "duration": 5 + }, + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedExpiration.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/ll.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/ll.b3dm new file mode 100644 index 000000000000..b97c4367854c Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/ll.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/lr.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/lr.b3dm new file mode 100644 index 000000000000..641627aafc62 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/lr.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/parent.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/parent.b3dm new file mode 100644 index 000000000000..8f1a47aee7e3 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/parent.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/subtree.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/subtree.json new file mode 100644 index 000000000000..1451c6355150 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/subtree.json @@ -0,0 +1,86 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 70, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 70, + "refine": "add", + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.31968, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ll.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.6988424218, + -1.3196390408203893, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "lr.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.698874, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ur.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.698874, + -1.31968, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ul.b3dm" + } + } + ] + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/tileset.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/tileset.json new file mode 100644 index 000000000000..e9099172b26b --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/tileset.json @@ -0,0 +1,72 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3197192952275933, + "maximum": -1.319644104024109 + }, + "Latitude": { + "minimum": 0.698848878034009, + "maximum": 0.6989046192460953 + }, + "Height": { + "minimum": 6.161747192963958, + "maximum": 84.83180232718587 + } + }, + "geometricError": 240, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 88 + ] + }, + "geometricError": 70, + "refine": "add", + "content": { + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 88 + ] + }, + "url": "parent.b3dm" + }, + "children": [ + { + "expire": { + "duration": 5 + }, + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 70, + "content": { + "url": "subtree.json" + } + } + ] + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/ul.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/ul.b3dm new file mode 100644 index 000000000000..30c904d18b81 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/ul.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/ur.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/ur.b3dm new file mode 100644 index 000000000000..e4ea60dcf5c1 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/ur.b3dm differ diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index 4e93ca2659e8..595a86d1abb6 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -5,7 +5,9 @@ defineSuite([ 'Core/Color', 'Core/defaultValue', 'Core/defined', + 'Core/getStringFromTypedArray', 'Core/HeadingPitchRange', + 'Core/JulianDate', 'Core/loadWithXhr', 'Core/Math', 'Core/Matrix4', @@ -32,7 +34,9 @@ defineSuite([ Color, defaultValue, defined, + getStringFromTypedArray, HeadingPitchRange, + JulianDate, loadWithXhr, CesiumMath, Matrix4, @@ -111,14 +115,16 @@ defineSuite([ var tilesetReplacementWithViewerRequestVolumeUrl = './Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume'; var tilesetWithExternalResourcesUrl = './Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources'; + var tilesetSubtreeExpirationUrl = './Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration'; + var tilesetSubtreeUrl = './Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/subtree.json'; + var batchedExpirationUrl = './Data/Cesium3DTiles/Batched/BatchedExpiration'; + var batchedColorsB3dmUrl = './Data/Cesium3DTiles/Batched/BatchedColors/batchedColors.b3dm'; var styleUrl = './Data/Cesium3DTiles/Style/style.json'; var pointCloudUrl = './Data/Cesium3DTiles/PointCloud/PointCloudRGB'; var pointCloudBatchedUrl = './Data/Cesium3DTiles/PointCloud/PointCloudBatched'; - var originalMaximumRequests; - beforeAll(function() { scene = createScene(); }); @@ -135,12 +141,10 @@ defineSuite([ camera.frustum.aspectRatio = scene.drawingBufferWidth / scene.drawingBufferHeight; camera.frustum.fov = CesiumMath.toRadians(60.0); - originalMaximumRequests = RequestScheduler.maximumRequests; viewAllTiles(); }); afterEach(function() { - RequestScheduler.maximumRequests = originalMaximumRequests; scene.primitives.removeAll(); // Wait for any pending requests to complete before ending each test @@ -412,15 +416,7 @@ defineSuite([ expect(stats.numberProcessing).toEqual(0); return Cesium3DTilesTester.waitForReady(scene, tileset).then(function() { - // Check that root tile is requested - expect(stats.visited).toEqual(0); - expect(stats.numberOfCommands).toEqual(0); - expect(stats.numberOfPendingRequests).toEqual(1); - expect(stats.numberProcessing).toEqual(0); - - // Update and check that child tiles are now requested - scene.renderForSpecs(); - + // Check that root and children are requested expect(stats.visited).toEqual(5); expect(stats.numberOfCommands).toEqual(0); expect(stats.numberOfPendingRequests).toEqual(5); @@ -1609,6 +1605,7 @@ defineSuite([ }); it('destroys before tile finishes loading', function() { + viewRootOnly(); var tileset = scene.primitives.add(new Cesium3DTileset({ url : tilesetUrl })); @@ -2509,4 +2506,173 @@ defineSuite([ }); }); + it('tile expires', function() { + return Cesium3DTilesTester.loadTileset(scene, batchedExpirationUrl).then(function(tileset) { + // Intercept the request and load content that produces more draw commands, to simulate fetching new content after the original expires + var originalLoad = loadWithXhr.load; + spyOn(loadWithXhr, 'load').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) { + originalLoad(batchedColorsB3dmUrl, responseType, method, data, headers, deferred, overrideMimeType); + }); + var tile = tileset._root; + var stats = tileset._statistics; + var expiredContent; + + // Check that expireDuration and expireDate are correctly set + var expireDate = JulianDate.addSeconds(JulianDate.now(), 5.0, new JulianDate()); + expect(JulianDate.secondsDifference(tile.expireDate, expireDate)).toEqualEpsilon(0.0, CesiumMath.EPSILON1); + expect(tile.expireDuration).toBe(5.0); + expect(tile.contentExpired).toBe(false); + expect(tile.contentReady).toBe(true); + expect(tile.contentAvailable).toBe(true); + expect(tile._expiredContent).toBeUndefined(); + + // Check stats + expect(stats.numberOfCommands).toBe(1); + expect(stats.numberTotal).toBe(1); + + // Trigger expiration to happen next frame + tile.expireDate = JulianDate.addSeconds(JulianDate.now(), -1.0, new JulianDate()); + + // Stays in the expired state until the request goes through + var originalMaxmimumRequests = RequestScheduler.maximumRequests; + RequestScheduler.maximumRequests = 0; // Artificially limit Request Scheduler so the request won't go through + scene.renderForSpecs(); + RequestScheduler.maximumRequests = originalMaxmimumRequests; + expiredContent = tile._expiredContent; + expect(tile.contentExpired).toBe(true); + expect(tile.contentAvailable).toBe(true); // Expired content now exists + expect(expiredContent).toBeDefined(); + + // Expired content renders while new content loads in + expect(stats.numberOfCommands).toBe(1); + expect(stats.numberTotal).toBe(1); + + // Request goes through, now in the LOADING state + scene.renderForSpecs(); + expect(tile.contentExpired).toBe(false); + expect(tile.contentReady).toBe(false); + expect(tile.contentAvailable).toBe(true); + expect(tile._contentState).toBe(Cesium3DTileContentState.LOADING); + expect(tile._expiredContent).toBeDefined(); // Still holds onto expired content until the content state is READY + + // Check that url contains a query param with the timestamp + var url = loadWithXhr.load.calls.first().args[0]; + expect(url.indexOf('expired=') >= 0).toBe(true); + + // Stats are still the same + expect(stats.numberOfCommands).toBe(1); + expect(stats.numberTotal).toBe(1); + + return pollToPromise(function() { + scene.renderForSpecs(); + expect(stats.numberOfCommands).toBe(1); // Still renders expired content + return tile.contentReady; + }).then(function() { + scene.renderForSpecs(); + + // Expired content is destroyed + expect(tile._expiredContent).toBeUndefined(); + expect(expiredContent.isDestroyed()).toBe(true); + + // Stats for new content + expect(stats.numberOfCommands).toBe(10); + expect(stats.numberTotal).toBe(1); + }); + }); + }); + + function modifySubtreeBuffer(arrayBuffer) { + var uint8Array = new Uint8Array(arrayBuffer); + var jsonString = getStringFromTypedArray(uint8Array); + var json = JSON.parse(jsonString); + json.root.children.splice(0, 1); + + jsonString = JSON.stringify(json); + var length = jsonString.length; + uint8Array = new Uint8Array(length); + for (var i = 0; i < length; i++) { + uint8Array[i] = jsonString.charCodeAt(i); + } + return uint8Array.buffer; + } + + it('tile with tileset content expires', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetSubtreeExpirationUrl).then(function(tileset) { + // Intercept the request and load a subtree with one less child. Still want to make an actual request to simulate + // real use cases instead of immediately returning a pre-created array buffer. + spyOn(loadWithXhr, 'load').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) { + var newDeferred = when.defer(); + loadWithXhr.defaultLoad(tilesetSubtreeUrl, responseType, method, data, headers, newDeferred, overrideMimeType); + newDeferred.promise.then(function(arrayBuffer) { + deferred.resolve(modifySubtreeBuffer(arrayBuffer)); + }); + }); + + var subtreeRoot = tileset._root.children[0]; + var subtreeChildren = subtreeRoot.children[0].children; + var childrenLength = subtreeChildren.length; + var stats = tileset._statistics; + + // Check stats + expect(stats.numberOfCommands).toBe(5); + expect(stats.numberTotal).toBe(7); + expect(stats.numberContentReady).toBe(5); + + // Trigger expiration to happen next frame + subtreeRoot.expireDate = JulianDate.addSeconds(JulianDate.now(), -1.0, new JulianDate()); + + // Listen to tile unload events + var spyUpdate = jasmine.createSpy('listener'); + tileset.tileUnload.addEventListener(spyUpdate); + + // Tiles in the subtree are removed from the cache and destroyed. + scene.renderForSpecs(); // Becomes expired + scene.renderForSpecs(); // Makes request + expect(subtreeRoot.children).toEqual([]); + for (var i = 0; i < childrenLength; ++i) { + expect(subtreeChildren[0].isDestroyed()).toBe(true); + } + expect(spyUpdate.calls.count()).toEqual(4); + + // Remove the spy so new tiles load in normally + loadWithXhr.load = loadWithXhr.defaultLoad; + + // Wait for the new tileset content to come in with one less leaf + return pollToPromise(function() { + scene.renderForSpecs(); + return subtreeRoot.contentReady && tileset.tilesLoaded; + }).then(function() { + scene.renderForSpecs(); + expect(stats.numberOfCommands).toBe(4); + expect(stats.numberTotal).toBe(6); + expect(stats.numberContentReady).toBe(4); + }); + }); + }); + + it('tile expires and request fails', function() { + return Cesium3DTilesTester.loadTileset(scene, batchedExpirationUrl).then(function(tileset) { + spyOn(loadWithXhr, 'load').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) { + deferred.reject(); + }); + var tile = tileset._root; + var stats = tileset._statistics; + + // Trigger expiration to happen next frame + tile.expireDate = JulianDate.addSeconds(JulianDate.now(), -1.0, new JulianDate()); + + // After update the tile is expired + scene.renderForSpecs(); + + // Make request (it will fail) + scene.renderForSpecs(); + + // Render scene + scene.renderForSpecs(); + expect(tile._contentState).toBe(Cesium3DTileContentState.FAILED); + expect(stats.numberOfCommands).toBe(0); + expect(stats.numberTotal).toBe(1); + }); + }); + }, 'WebGL');