diff --git a/CHANGES.md b/CHANGES.md index 1ea874c77ac0..33a24e094fee 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,9 @@ Change Log ### 1.52 - 2018-12-03 +##### Breaking Changes :mega: +* `TerrainProviders` that implement `availability` must now also implement the `loadTileDataAvailability` method. + ##### Additions :tada: * Added functions to get the most detailed height of 3D Tiles on-screen or off-screen. [#7115](https://github.com/AnalyticalGraphicsInc/cesium/pull/7115) * Added `Scene.sampleHeightMostDetailed`, an asynchronous version of `Scene.sampleHeight` that uses the maximum level of detail for 3D Tiles. @@ -10,6 +13,7 @@ Change Log * Added support for high dynamic range rendering. It is enabled by default when supported, but can be disabled with `Scene.highDynamicRange`. [#7017](https://github.com/AnalyticalGraphicsInc/cesium/pull/7017) * Added `Scene.invertClassificationSupported` for checking if invert classification is supported. * Added `computeLineSegmentLineSegmentIntersection` to `Intersections2D`. [#7228](https://github.com/AnalyticalGraphicsInc/Cesium/pull/7228) +* Added ability to load availability progressively from a quantized mesh extension instead of upfront. This will speed up load time and reduce memory usage. [#7196](https://github.com/AnalyticalGraphicsInc/cesium/pull/7196) * Added the ability to apply styles to 3D Tilesets that don't contain features. [#7255](https://github.com/AnalyticalGraphicsInc/Cesium/pull/7255) ##### Fixes :wrench: diff --git a/Source/Core/CesiumTerrainProvider.js b/Source/Core/CesiumTerrainProvider.js index 3124e9b17d05..1033b7bd643e 100644 --- a/Source/Core/CesiumTerrainProvider.js +++ b/Source/Core/CesiumTerrainProvider.js @@ -3,6 +3,7 @@ define([ '../ThirdParty/when', './AttributeCompression', './BoundingSphere', + './Cartesian2', './Cartesian3', './Credit', './defaultValue', @@ -12,11 +13,14 @@ define([ './DeveloperError', './Event', './GeographicTilingScheme', + './getStringFromTypedArray', './HeightmapTerrainData', './IndexDatatype', './Math', './OrientedBoundingBox', './QuantizedMeshTerrainData', + './Request', + './RequestType', './Resource', './RuntimeError', './TerrainProvider', @@ -27,6 +31,7 @@ define([ when, AttributeCompression, BoundingSphere, + Cartesian2, Cartesian3, Credit, defaultValue, @@ -36,11 +41,14 @@ define([ DeveloperError, Event, GeographicTilingScheme, + getStringFromTypedArray, HeightmapTerrainData, IndexDatatype, CesiumMath, OrientedBoundingBox, QuantizedMeshTerrainData, + Request, + RequestType, Resource, RuntimeError, TerrainProvider, @@ -56,7 +64,12 @@ define([ this.availability = layer.availability; this.hasVertexNormals = layer.hasVertexNormals; this.hasWaterMask = layer.hasWaterMask; + this.hasMetadata = layer.hasMetadata; + this.availabilityLevels = layer.availabilityLevels; + this.availabilityTilesLoaded = layer.availabilityTilesLoaded; this.littleEndianExtensionSize = layer.littleEndianExtensionSize; + this.availabilityTilesLoaded = layer.availabilityTilesLoaded; + this.availabilityPromiseCache = {}; } /** @@ -70,6 +83,7 @@ define([ * @param {Resource|String|Promise|Promise} options.url The URL of the Cesium terrain server. * @param {Boolean} [options.requestVertexNormals=false] Flag that indicates if the client should request additional lighting information from the server, in the form of per vertex normals if available. * @param {Boolean} [options.requestWaterMask=false] Flag that indicates if the client should request per tile water masks from the server, if available. + * @param {Boolean} [options.requestMetadata=true] Flag that indicates if the client should request per tile metadata from the server, if available. * @param {Ellipsoid} [options.ellipsoid] The ellipsoid. If not specified, the WGS84 ellipsoid is used. * @param {Credit|String} [options.credit] A credit for the data source, which is displayed on the canvas. * @@ -122,6 +136,14 @@ define([ */ this._requestWaterMask = defaultValue(options.requestWaterMask, false); + /** + * Boolean flag that indicates if the client should request tile metadata from the server. + * @type {Boolean} + * @default true + * @private + */ + this._requestMetadata = defaultValue(options.requestMetadata, true); + this._errorEvent = new Event(); var credit = options.credit; @@ -139,22 +161,23 @@ define([ var that = this; var lastResource; - var metadataResource; + var layerJsonResource; var metadataError; var layers = this._layers = []; var attribution = ''; var overallAvailability = []; + var overallMaxZoom = 0; when(options.url) .then(function(url) { var resource = Resource.createIfNeeded(url); resource.appendForwardSlash(); lastResource = resource; - metadataResource = lastResource.getDerivedResource({ + layerJsonResource = lastResource.getDerivedResource({ url: 'layer.json' }); - var uri = new Uri(metadataResource.url); + var uri = new Uri(layerJsonResource.url); if (uri.authority === 'assets.agi.com') { var deprecationText = 'STK World Terrain at assets.agi.com was shut down on October 1, 2018.'; var deprecationLinkText = 'Check out the new high-resolution Cesium World Terrain for migration instructions.'; @@ -168,7 +191,7 @@ define([ that._tileCredits = resource.credits; } - requestMetadata(); + requestLayerJson(); }) .otherwise(function(e) { deferred.reject(e); @@ -179,18 +202,19 @@ define([ if (!data.format) { message = 'The tile format is not specified in the layer.json file.'; - metadataError = TileProviderError.handleError(metadataError, that, that._errorEvent, message, undefined, undefined, undefined, requestMetadata); + metadataError = TileProviderError.handleError(metadataError, that, that._errorEvent, message, undefined, undefined, undefined, requestLayerJson); return; } if (!data.tiles || data.tiles.length === 0) { message = 'The layer.json file does not specify any tile URL templates.'; - metadataError = TileProviderError.handleError(metadataError, that, that._errorEvent, message, undefined, undefined, undefined, requestMetadata); + metadataError = TileProviderError.handleError(metadataError, that, that._errorEvent, message, undefined, undefined, undefined, requestLayerJson); return; } var hasVertexNormals = false; var hasWaterMask = false; + var hasMetadata = false; var littleEndianExtensionSize = true; var isHeightmap = false; if (data.format === 'heightmap-1.0') { @@ -211,17 +235,41 @@ define([ that._requestWaterMask = true; } else if (data.format.indexOf('quantized-mesh-1.') !== 0) { message = 'The tile format "' + data.format + '" is invalid or not supported.'; - metadataError = TileProviderError.handleError(metadataError, that, that._errorEvent, message, undefined, undefined, undefined, requestMetadata); + metadataError = TileProviderError.handleError(metadataError, that, that._errorEvent, message, undefined, undefined, undefined, requestLayerJson); return; } var tileUrlTemplates = data.tiles; + var maxZoom = data.maxzoom; + overallMaxZoom = Math.max(overallMaxZoom, maxZoom); + // Keeps track of which of the availablity containing tiles have been loaded + var availabilityTilesLoaded; + + // The vertex normals defined in the 'octvertexnormals' extension is identical to the original + // contents of the original 'vertexnormals' extension. 'vertexnormals' extension is now + // deprecated, as the extensionLength for this extension was incorrectly using big endian. + // We maintain backwards compatibility with the legacy 'vertexnormal' implementation + // by setting the _littleEndianExtensionSize to false. Always prefer 'octvertexnormals' + // over 'vertexnormals' if both extensions are supported by the server. + if (defined(data.extensions) && data.extensions.indexOf('octvertexnormals') !== -1) { + hasVertexNormals = true; + } else if (defined(data.extensions) && data.extensions.indexOf('vertexnormals') !== -1) { + hasVertexNormals = true; + littleEndianExtensionSize = false; + } + if (defined(data.extensions) && data.extensions.indexOf('watermask') !== -1) { + hasWaterMask = true; + } + if (defined(data.extensions) && data.extensions.indexOf('metadata') !== -1) { + hasMetadata = true; + } + + var availabilityLevels = data.metadataAvailability; var availableTiles = data.available; var availability; - if (defined(availableTiles)) { + if (defined(availableTiles) && !defined(availabilityLevels)) { availability = new TileAvailability(that._tilingScheme, availableTiles.length); - for (var level = 0; level < availableTiles.length; ++level) { var rangesAtLevel = availableTiles[level]; var yTiles = that._tilingScheme.getNumberOfYTilesAtLevel(level); @@ -237,26 +285,18 @@ define([ availability.addAvailableTileRange(level, range.startX, yStart, range.endX, yEnd); } } - } - - // The vertex normals defined in the 'octvertexnormals' extension is identical to the original - // contents of the original 'vertexnormals' extension. 'vertexnormals' extension is now - // deprecated, as the extensionLength for this extension was incorrectly using big endian. - // We maintain backwards compatibility with the legacy 'vertexnormal' implementation - // by setting the _littleEndianExtensionSize to false. Always prefer 'octvertexnormals' - // over 'vertexnormals' if both extensions are supported by the server. - if (defined(data.extensions) && data.extensions.indexOf('octvertexnormals') !== -1) { - hasVertexNormals = true; - } else if (defined(data.extensions) && data.extensions.indexOf('vertexnormals') !== -1) { - hasVertexNormals = true; - littleEndianExtensionSize = false; - } - if (defined(data.extensions) && data.extensions.indexOf('watermask') !== -1) { - hasWaterMask = true; + } else if (defined(availabilityLevels)) { + availabilityTilesLoaded = new TileAvailability(that._tilingScheme, maxZoom); + availability = new TileAvailability(that._tilingScheme, maxZoom); + overallAvailability[0] = [ + [0, 0, 1, 0] + ]; + availability.addAvailableTileRange(0, 0, 0, 1, 0); } that._hasWaterMask = that._hasWaterMask || hasWaterMask; that._hasVertexNormals = that._hasVertexNormals || hasVertexNormals; + that._hasMetadata = that._hasMetadata || hasMetadata; if (defined(data.attribution)) { if (attribution.length > 0) { attribution += ' '; @@ -272,6 +312,9 @@ define([ availability: availability, hasVertexNormals: hasVertexNormals, hasWaterMask: hasWaterMask, + hasMetadata: hasMetadata, + availabilityLevels: availabilityLevels, + availabilityTilesLoaded: availabilityTilesLoaded, littleEndianExtensionSize: littleEndianExtensionSize })); @@ -285,10 +328,10 @@ define([ url: parentUrl }); lastResource.appendForwardSlash(); // Terrain always expects a directory - metadataResource = lastResource.getDerivedResource({ + layerJsonResource = lastResource.getDerivedResource({ url: 'layer.json' }); - var parentMetadata = metadataResource.fetchJson(); + var parentMetadata = layerJsonResource.fetchJson(); return when(parentMetadata, parseMetadataSuccess, parseMetadataFailure); } @@ -296,8 +339,8 @@ define([ } function parseMetadataFailure(data) { - var message = 'An error occurred while accessing ' + metadataResource.url + '.'; - metadataError = TileProviderError.handleError(metadataError, that, that._errorEvent, message, undefined, undefined, undefined, requestMetadata); + var message = 'An error occurred while accessing ' + layerJsonResource.url + '.'; + metadataError = TileProviderError.handleError(metadataError, that, that._errorEvent, message, undefined, undefined, undefined, requestLayerJson); } function metadataSuccess(data) { @@ -309,7 +352,7 @@ define([ var length = overallAvailability.length; if (length > 0) { - var availability = that._availability = new TileAvailability(that._tilingScheme, length); + var availability = that._availability = new TileAvailability(that._tilingScheme, overallMaxZoom); for (var level = 0; level < length; ++level) { var levelRanges = overallAvailability[level]; for (var i = 0; i < levelRanges.length; ++i) { @@ -351,8 +394,8 @@ define([ parseMetadataFailure(data); } - function requestMetadata() { - when(metadataResource.fetchJson()) + function requestLayerJson() { + when(layerJsonResource.fetchJson()) .then(metadataSuccess) .otherwise(metadataFailure); } @@ -382,7 +425,15 @@ define([ * @constant * @default 2 */ - WATER_MASK: 2 + WATER_MASK: 2, + /** + * A json object contain metadata about the tile + * + * @type {Number} + * @constant + * @default 4 + */ + METADATA: 4 }; function getRequestHeader(extensionsList) { @@ -410,7 +461,8 @@ define([ }); } - function createQuantizedMeshTerrainData(provider, buffer, level, x, y, tmsY, littleEndianExtensionSize) { + function createQuantizedMeshTerrainData(provider, buffer, level, x, y, tmsY, layer) { + var littleEndianExtensionSize = layer.littleEndianExtensionSize; var pos = 0; var cartesian3Elements = 3; var boundingSphereElements = cartesian3Elements + 1; @@ -512,6 +564,30 @@ define([ encodedNormalBuffer = new Uint8Array(buffer, pos, vertexCount * 2); } else if (extensionId === QuantizedMeshExtensionIds.WATER_MASK && provider._requestWaterMask) { waterMaskBuffer = new Uint8Array(buffer, pos, extensionLength); + } else if (extensionId === QuantizedMeshExtensionIds.METADATA && provider._requestMetadata) { + var stringLength = view.getUint32(pos, true); + if (stringLength > 0) { + var jsonString = + getStringFromTypedArray(new Uint8Array(buffer), pos + Uint32Array.BYTES_PER_ELEMENT, stringLength); + var metadata = JSON.parse(jsonString); + var availableTiles = metadata.available; + if (defined(availableTiles)) { + for (var offset = 0; offset < availableTiles.length; ++offset) { + var availableLevel = level + offset + 1; + var rangesAtLevel = availableTiles[offset]; + var yTiles = provider._tilingScheme.getNumberOfYTilesAtLevel(availableLevel); + + for (var rangeIndex = 0; rangeIndex < rangesAtLevel.length; ++rangeIndex) { + var range = rangesAtLevel[rangeIndex]; + var yStart = yTiles - range.endY - 1; + var yEnd = yTiles - range.startY - 1; + provider.availability.addAvailableTileRange(availableLevel, range.startX, yStart, range.endX, yEnd); + layer.availability.addAvailableTileRange(availableLevel, range.startX, yStart, range.endX, yEnd); + } + } + } + } + layer.availabilityTilesLoaded.addAvailableTileRange(level, x, y, x, y); } pos += extensionLength; } @@ -596,6 +672,10 @@ define([ } } + return requestTileGeometry(this, x, y, level, layerToUse, request); + }; + + function requestTileGeometry(provider, x, y, level, layerToUse, request) { if (!defined(layerToUse)) { return when.reject(new RuntimeError('Terrain tile doesn\'t exist')); } @@ -605,17 +685,20 @@ define([ return undefined; } - var yTiles = this._tilingScheme.getNumberOfYTilesAtLevel(level); + var yTiles = provider._tilingScheme.getNumberOfYTilesAtLevel(level); var tmsY = (yTiles - y - 1); var extensionList = []; - if (this._requestVertexNormals && layerToUse.hasVertexNormals) { + if (provider._requestVertexNormals && layerToUse.hasVertexNormals) { extensionList.push(layerToUse.littleEndianExtensionSize ? 'octvertexnormals' : 'vertexnormals'); } - if (this._requestWaterMask && layerToUse.hasWaterMask) { + if (provider._requestWaterMask && layerToUse.hasWaterMask) { extensionList.push('watermask'); } + if (provider._requestMetadata && layerToUse.hasMetadata) { + extensionList.push('metadata'); + } var headers; var query; @@ -649,14 +732,13 @@ define([ return undefined; } - var that = this; return promise.then(function (buffer) { - if (defined(that._heightmapStructure)) { - return createHeightmapTerrainData(that, buffer, level, x, y, tmsY); + if (defined(provider._heightmapStructure)) { + return createHeightmapTerrainData(provider, buffer, level, x, y, tmsY); } - return createQuantizedMeshTerrainData(that, buffer, level, x, y, tmsY, layerToUse.littleEndianExtensionSize); + return createQuantizedMeshTerrainData(provider, buffer, level, x, y, tmsY, layerToUse); }); - }; + } defineProperties(CesiumTerrainProvider.prototype, { /** @@ -772,6 +854,26 @@ define([ } }, + /** + * Gets a value indicating whether or not the requested tiles include metadata. + * This function should not be called before {@link CesiumTerrainProvider#ready} returns true. + * @memberof CesiumTerrainProvider.prototype + * @type {Boolean} + * @exception {DeveloperError} This property must not be called before {@link CesiumTerrainProvider#ready} + */ + hasMetadata : { + get : function() { + //>>includeStart('debug', pragmas.debug) + if (!this._ready) { + throw new DeveloperError('hasMetadata must not be called before the terrain provider is ready.'); + } + //>>includeEnd('debug'); + + // returns true if we can request metadata from the server + return this._hasMetadata && this._requestMetadata; + } + }, + /** * Boolean flag that indicates if the client should request vertex normals from the server. * Vertex normals data is appended to the standard tile mesh data only if the client requests the vertex normals and @@ -798,6 +900,19 @@ define([ } }, + /** + * Boolean flag that indicates if the client should request metadata from the server. + * Metadata is appended to the standard tile mesh data only if the client requests the metadata and + * if the server provides a metadata. + * @memberof CesiumTerrainProvider.prototype + * @type {Boolean} + */ + requestMetadata : { + get : function() { + return this._requestMetadata; + } + }, + /** * Gets an object that can be used to determine availability of terrain from this provider, such as * at points and in rectangles. This function should not be called before @@ -840,9 +955,134 @@ define([ if (!defined(this._availability)) { return undefined; } + if (level > this._availability._maximumLevel) { + return false; + } - return this._availability.isTileAvailable(level, x, y); + if (this._availability.isTileAvailable(level, x, y)) { + // If the tile is listed as available, then we are done + return true; + } + if (!this._hasMetadata) { + // If we don't have any layers with the metadata extension then we don't have this tile + return false; + } + + var layers = this._layers; + var count = layers.length; + for (var i = 0; i < count; ++i) { + var layerResult = checkLayer(this, x, y, level, layers[i], (i===0)); + if (layerResult.result) { + // There is a layer that may or may not have the tile + return undefined; + } + } + + return false; }; + /** + * Makes sure we load availability data for a tile + * + * @param {Number} x The X coordinate of the tile for which to request geometry. + * @param {Number} y The Y coordinate of the tile for which to request geometry. + * @param {Number} level The level of the tile for which to request geometry. + * @returns {undefined|Promise} Undefined if nothing need to be loaded or a Promise that resolves when all required tiles are loaded + */ + CesiumTerrainProvider.prototype.loadTileDataAvailability = function(x, y, level) { + if (!defined(this._availability) || (level > this._availability._maximumLevel) || + (this._availability.isTileAvailable(level, x, y) || (!this._hasMetadata))) { + // We know the tile is either available or not available so nothing to wait on + return undefined; + } + + var layers = this._layers; + var count = layers.length; + for (var i = 0; i < count; ++i) { + var layerResult = checkLayer(this, x, y, level, layers[i], (i===0)); + if (defined(layerResult.promise)) { + return layerResult.promise; + } + } + }; + + function getAvailabilityTile(layer, x, y, level) { + if (level === 0) { + return; + } + + var availabilityLevels = layer.availabilityLevels; + if (level % availabilityLevels === 0) { + level -= availabilityLevels; + } + + var parentLevel = ((level / availabilityLevels) | 0) * availabilityLevels; + var divisor = 1 << (level - parentLevel); + var parentX = (x / divisor) | 0; + var parentY = (y / divisor) | 0; + + return { + level: parentLevel, + x: parentX, + y: parentY + }; + } + + function checkLayer(provider, x, y, level, layer, topLayer) { + if (!defined(layer.availabilityLevels)) { + // It's definitely not in this layer + return { + result: false + }; + } + + var cacheKey; + var deleteFromCache = function () { + delete layer.availabilityPromiseCache[cacheKey]; + }; + var availabilityTilesLoaded = layer.availabilityTilesLoaded; + var availability = layer.availability; + + var tile = getAvailabilityTile(layer, x, y, level); + while(defined(tile)) { + if (availability.isTileAvailable(tile.level, tile.x, tile.y) && + !availabilityTilesLoaded.isTileAvailable(tile.level, tile.x, tile.y)) + { + var requestPromise; + if (!topLayer) { + cacheKey = tile.level + '-' + tile.x + '-' + tile.y; + requestPromise = layer.availabilityPromiseCache[cacheKey]; + if (!defined(requestPromise)) { + // For cutout terrain, if this isn't the top layer the availability tiles + // may never get loaded, so request it here. + var request = new Request({ + throttle: true, + throttleByServer: true, + type: RequestType.TERRAIN + }); + requestPromise = requestTileGeometry(provider, tile.x, tile.y, tile.level, layer, request); + if (defined(requestPromise)) { + layer.availabilityPromiseCache[cacheKey] = requestPromise; + requestPromise.then(deleteFromCache); + } + } + } + + // The availability tile is available, but not loaded, so there + // is still a chance that it may become available at some point + return { + result: true, + promise: requestPromise + }; + } + + tile = getAvailabilityTile(layer, tile.x, tile.y, tile.level); + } + + return { + result: false + }; + } + return CesiumTerrainProvider; }); diff --git a/Source/Core/EllipsoidTerrainProvider.js b/Source/Core/EllipsoidTerrainProvider.js index 8ee50e2fc926..7266b9b11a00 100644 --- a/Source/Core/EllipsoidTerrainProvider.js +++ b/Source/Core/EllipsoidTerrainProvider.js @@ -189,5 +189,17 @@ define([ return undefined; }; + /** + * Makes sure we load availability data for a tile + * + * @param {Number} x The X coordinate of the tile for which to request geometry. + * @param {Number} y The Y coordinate of the tile for which to request geometry. + * @param {Number} level The level of the tile for which to request geometry. + * @returns {undefined|Promise} Undefined if nothing need to be loaded or a Promise that resolves when all required tiles are loaded + */ + EllipsoidTerrainProvider.prototype.loadTileDataAvailability = function(x, y, level) { + return undefined; + }; + return EllipsoidTerrainProvider; }); diff --git a/Source/Core/GoogleEarthEnterpriseTerrainProvider.js b/Source/Core/GoogleEarthEnterpriseTerrainProvider.js index fe0b0c415eeb..3b8207872418 100644 --- a/Source/Core/GoogleEarthEnterpriseTerrainProvider.js +++ b/Source/Core/GoogleEarthEnterpriseTerrainProvider.js @@ -580,6 +580,18 @@ define([ return false; }; + /** + * Makes sure we load availability data for a tile + * + * @param {Number} x The X coordinate of the tile for which to request geometry. + * @param {Number} y The Y coordinate of the tile for which to request geometry. + * @param {Number} level The level of the tile for which to request geometry. + * @returns {undefined|Promise} Undefined if nothing need to be loaded or a Promise that resolves when all required tiles are loaded + */ + GoogleEarthEnterpriseTerrainProvider.prototype.loadTileDataAvailability = function(x, y, level) { + return undefined; + }; + // // Functions to handle imagery packets // diff --git a/Source/Core/TerrainProvider.js b/Source/Core/TerrainProvider.js index 44a28042e1b1..efa99393e8de 100644 --- a/Source/Core/TerrainProvider.js +++ b/Source/Core/TerrainProvider.js @@ -227,5 +227,15 @@ define([ */ TerrainProvider.prototype.getTileDataAvailable = DeveloperError.throwInstantiationError; + /** + * Makes sure we load availability data for a tile + * + * @param {Number} x The X coordinate of the tile for which to request geometry. + * @param {Number} y The Y coordinate of the tile for which to request geometry. + * @param {Number} level The level of the tile for which to request geometry. + * @returns {undefined|Promise} Undefined if nothing need to be loaded or a Promise that resolves when all required tiles are loaded + */ + TerrainProvider.prototype.loadTileDataAvailability = DeveloperError.throwInstantiationError; + return TerrainProvider; }); diff --git a/Source/Core/VRTheWorldTerrainProvider.js b/Source/Core/VRTheWorldTerrainProvider.js index 744a228afed8..4fe88acac15f 100644 --- a/Source/Core/VRTheWorldTerrainProvider.js +++ b/Source/Core/VRTheWorldTerrainProvider.js @@ -355,5 +355,17 @@ define([ return undefined; }; + /** + * Makes sure we load availability data for a tile + * + * @param {Number} x The X coordinate of the tile for which to request geometry. + * @param {Number} y The Y coordinate of the tile for which to request geometry. + * @param {Number} level The level of the tile for which to request geometry. + * @returns {undefined|Promise} Undefined if nothing need to be loaded or a Promise that resolves when all required tiles are loaded + */ + VRTheWorldTerrainProvider.prototype.loadTileDataAvailability = function(x, y, level) { + return undefined; + }; + return VRTheWorldTerrainProvider; }); diff --git a/Source/Core/sampleTerrainMostDetailed.js b/Source/Core/sampleTerrainMostDetailed.js index f2070ebd82b0..2b2a0cc5d300 100644 --- a/Source/Core/sampleTerrainMostDetailed.js +++ b/Source/Core/sampleTerrainMostDetailed.js @@ -1,15 +1,19 @@ define([ '../ThirdParty/when', + './Cartesian2', './defined', './DeveloperError', './sampleTerrain' ], function( when, + Cartesian2, defined, DeveloperError, sampleTerrain) { 'use strict'; + var scratchCartesian2 = new Cartesian2(); + /** * Initiates a sampleTerrain() request at the maximum available tile level for a terrain dataset. * @@ -43,36 +47,70 @@ define([ } //>>includeEnd('debug'); - return terrainProvider.readyPromise.then(function() { - var byLevel = []; + return terrainProvider.readyPromise + .then(function() { + var byLevel = []; + var maxLevels = []; - var availability = terrainProvider.availability; + var availability = terrainProvider.availability; - //>>includeStart('debug', pragmas.debug); - if (!defined(availability)) { - throw new DeveloperError('sampleTerrainMostDetailed requires a terrain provider that has tile availability.'); - } - //>>includeEnd('debug'); + //>>includeStart('debug', pragmas.debug); + if (!defined(availability)) { + throw new DeveloperError('sampleTerrainMostDetailed requires a terrain provider that has tile availability.'); + } + //>>includeEnd('debug'); - for (var i = 0; i < positions.length; ++i) { - var position = positions[i]; - var maxLevel = availability.computeMaximumLevelAtPosition(position); + var promises = []; + for (var i = 0; i < positions.length; ++i) { + var position = positions[i]; + var maxLevel = availability.computeMaximumLevelAtPosition(position); + maxLevels[i] = maxLevel; + if (maxLevel === 0) { + // This is a special case where we have a parent terrain and we are requesting + // heights from an area that isn't covered by the top level terrain at all. + // This will essentially trigger the loading of the parent terrains root tile + terrainProvider.tilingScheme.positionToTileXY(position, 1, scratchCartesian2); + var promise = terrainProvider.loadTileDataAvailability(scratchCartesian2.x, scratchCartesian2.y, 1); + if (defined(promise)) { + promises.push(promise); + } + } - var atLevel = byLevel[maxLevel]; - if (!defined(atLevel)) { - byLevel[maxLevel] = atLevel = []; + var atLevel = byLevel[maxLevel]; + if (!defined(atLevel)) { + byLevel[maxLevel] = atLevel = []; + } + atLevel.push(position); } - atLevel.push(position); - } - return when.all(byLevel.map(function(positionsAtLevel, index) { - if (defined(positionsAtLevel)) { - return sampleTerrain(terrainProvider, index, positionsAtLevel); - } - })).then(function() { - return positions; + return when.all(promises) + .then(function() { + return when.all(byLevel.map(function(positionsAtLevel, index) { + if (defined(positionsAtLevel)) { + return sampleTerrain(terrainProvider, index, positionsAtLevel); + } + })); + }) + .then(function() { + var changedPositions = []; + for (var i = 0; i < positions.length; ++i) { + var position = positions[i]; + var maxLevel = availability.computeMaximumLevelAtPosition(position); + + if (maxLevel !== maxLevels[i]) { + // Now that we loaded the max availability, a higher level has become available + changedPositions.push(position); + } + } + + if (changedPositions.length > 0) { + return sampleTerrainMostDetailed(terrainProvider, changedPositions); + } + }) + .then(function() { + return positions; + }); }); - }); } return sampleTerrainMostDetailed; diff --git a/Specs/Core/CesiumTerrainProviderSpec.js b/Specs/Core/CesiumTerrainProviderSpec.js index 944f5e5fc1b9..93113eac7cd4 100644 --- a/Specs/Core/CesiumTerrainProviderSpec.js +++ b/Specs/Core/CesiumTerrainProviderSpec.js @@ -89,6 +89,10 @@ defineSuite([ }; } + function returnMetadataAvailabilityTileJson() { + return returnTileJson('Data/CesiumTerrainTileJson/MetadataAvailability.tile.json'); + } + function waitForTile(level, x, y, requestNormals, requestWaterMask, f) { var terrainProvider = new CesiumTerrainProvider({ url : 'made/up/url', @@ -652,6 +656,33 @@ defineSuite([ }); }); + it('provides QuantizedMeshTerrainData with Metadata availability', function() { + Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) { + Resource._DefaultImplementations.loadWithXhr('Data/CesiumTerrainTileJson/tile.metadataavailability.terrain', responseType, method, data, headers, deferred); + }; + + returnMetadataAvailabilityTileJson(); + + var terrainProvider = new CesiumTerrainProvider({ + url : 'made/up/url' + }); + + return pollToPromise(function() { + return terrainProvider.ready; + }).then(function() { + expect(terrainProvider.hasMetadata).toBe(true); + expect(terrainProvider._layers[0].availabilityLevels).toBe(10); + expect(terrainProvider.availability.isTileAvailable(0,0,0)).toBe(true); + expect(terrainProvider.availability.isTileAvailable(0,1,0)).toBe(true); + expect(terrainProvider.availability.isTileAvailable(1,0,0)).toBe(false); + + return terrainProvider.requestTileGeometry(0, 0, 0); + }).then(function(loadedData) { + expect(loadedData).toBeInstanceOf(QuantizedMeshTerrainData); + expect(terrainProvider.availability.isTileAvailable(1,0,0)).toBe(true); + }); + }); + it('returns undefined if too many requests are already in progress', function() { var baseUrl = 'made/up/url'; @@ -726,6 +757,29 @@ defineSuite([ }); }); + it('getTileDataAvailable() with Metadata availability', function() { + Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) { + Resource._DefaultImplementations.loadWithXhr('Data/CesiumTerrainTileJson/tile.metadataavailability.terrain', responseType, method, data, headers, deferred); + }; + + returnMetadataAvailabilityTileJson(); + + var terrainProvider = new CesiumTerrainProvider({ + url : 'made/up/url' + }); + + return pollToPromise(function() { + return terrainProvider.ready; + }).then(function() { + expect(terrainProvider.getTileDataAvailable(0,0,0)).toBe(true); + expect(terrainProvider.getTileDataAvailable(0,0,1)).toBeUndefined(); + + return terrainProvider.requestTileGeometry(0, 0, 0); + }).then(function() { + expect(terrainProvider.getTileDataAvailable(0,0,1)).toBe(true); + }); + }); + it('supports a query string in the base URL', function() { Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) { // Just return any old file, as long as its big enough diff --git a/Specs/Data/CesiumTerrainTileJson/MetadataAvailability.tile.json b/Specs/Data/CesiumTerrainTileJson/MetadataAvailability.tile.json new file mode 100644 index 000000000000..d6e8206cacbf --- /dev/null +++ b/Specs/Data/CesiumTerrainTileJson/MetadataAvailability.tile.json @@ -0,0 +1 @@ +{"attribution":"","bounds":[-180,-90,180,90],"bvhlevels":6,"description":"","extensions":["bvh","metadata","octvertexnormals"],"format":"quantized-mesh-1.0","maxzoom":8,"metadataAvailability":10,"minzoom":0,"name":"","projection":"EPSG:4326","scheme":"tms","tiles":["{z}/{x}/{y}.terrain?v={version}"],"version":"1.33.0"} diff --git a/Specs/Data/CesiumTerrainTileJson/tile.metadataavailability.terrain b/Specs/Data/CesiumTerrainTileJson/tile.metadataavailability.terrain new file mode 100644 index 000000000000..d2be946768ed Binary files /dev/null and b/Specs/Data/CesiumTerrainTileJson/tile.metadataavailability.terrain differ