diff --git a/CHANGES.md b/CHANGES.md index 432dff7bc1a0..164db8c154d8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,8 @@ Change Log ========== +### 1.39 - 2017-11-01 +* Added support for the layer.json `parentUrl` property in `CesiumTerrainProvider` to allow for compositing of tilesets. + ### 1.38 - 2017-10-02 * Breaking changes diff --git a/Source/Core/CesiumTerrainProvider.js b/Source/Core/CesiumTerrainProvider.js index 9816f5adb63b..762c3ca5a5cb 100644 --- a/Source/Core/CesiumTerrainProvider.js +++ b/Source/Core/CesiumTerrainProvider.js @@ -20,6 +20,7 @@ define([ './QuantizedMeshTerrainData', './Request', './RequestType', + './RuntimeError', './TerrainProvider', './TileAvailability', './TileProviderError' @@ -45,11 +46,21 @@ define([ QuantizedMeshTerrainData, Request, RequestType, + RuntimeError, TerrainProvider, TileAvailability, TileProviderError) { 'use strict'; + function LayerInformation(layer) { + this.isHeightmap = layer.isHeightmap; + this.tileUrlTemplates = layer.tileUrlTemplates; + this.availability = layer.availability; + this.hasVertexNormals = layer.hasVertexNormals; + this.hasWaterMask = layer.hasWaterMask; + this.littleEndianExtensionSize = layer.littleEndianExtensionSize; + } + /** * A {@link TerrainProvider} that accesses terrain data in a Cesium terrain format. * The format is described on the @@ -114,14 +125,8 @@ define([ this._heightmapStructure = undefined; this._hasWaterMask = false; - - /** - * Boolean flag that indicates if the Terrain Server can provide vertex normals. - * @type {Boolean} - * @default false - * @private - */ this._hasVertexNormals = false; + /** * Boolean flag that indicates if the client should request vertex normals from the server. * @type {Boolean} @@ -129,7 +134,7 @@ define([ * @private */ this._requestVertexNormals = defaultValue(options.requestVertexNormals, false); - this._littleEndianExtensionSize = true; + /** * Boolean flag that indicates if the client should request tile watermasks from the server. * @type {Boolean} @@ -139,7 +144,6 @@ define([ this._requestWaterMask = defaultValue(options.requestWaterMask, false); this._errorEvent = new Event(); - this._availability = undefined; var credit = options.credit; if (typeof credit === 'string') { @@ -147,9 +151,12 @@ define([ } this._credit = credit; + this._availability = undefined; + this._ready = false; this._readyPromise = when.defer(); + var lastUrl = this._url; var metadataUrl = joinUrls(this._url, 'layer.json'); if (defined(this._proxy)) { metadataUrl = this._proxy.getURL(metadataUrl); @@ -158,7 +165,11 @@ define([ var that = this; var metadataError; - function metadataSuccess(data) { + var layers = this._layers = []; + var attribution = ''; + var overallAvailability = []; + + function parseMetadataSuccess(data) { var message; if (!data.format) { @@ -173,8 +184,14 @@ define([ return; } + var hasVertexNormals = false; + var hasWaterMask = false; + var littleEndianExtensionSize = true; + var isHeightmap = false; if (data.format === 'heightmap-1.0') { - that._heightmapStructure = { + isHeightmap = true; + if (!defined(that._heightmapStructure)) { + that._heightmapStructure = { heightScale : 1.0 / 5.0, heightOffset : -1000.0, elementsPerHeight : 1, @@ -184,7 +201,8 @@ define([ lowestEncodedHeight : 0, highestEncodedHeight : 256 * 256 - 1 }; - that._hasWaterMask = true; + } + hasWaterMask = true; that._requestWaterMask = true; } else if (data.format.indexOf('quantized-mesh-1.') !== 0) { message = 'The tile format "' + data.format + '" is invalid or not supported.'; @@ -192,37 +210,39 @@ define([ return; } - that._tileUrlTemplates = data.tiles; - for (var i = 0; i < that._tileUrlTemplates.length; ++i) { - var template = new Uri(that._tileUrlTemplates[i]); - var baseUri = new Uri(that._url); + var tileUrlTemplates = data.tiles; + for (var i = 0; i < tileUrlTemplates.length; ++i) { + var template = new Uri(tileUrlTemplates[i]); + var baseUri = new Uri(lastUrl); if (template.authority && !baseUri.authority) { baseUri.authority = template.authority; baseUri.scheme = template.scheme; } - that._tileUrlTemplates[i] = joinUrls(baseUri, template).toString().replace('{version}', data.version); + tileUrlTemplates[i] = joinUrls(baseUri, template).toString().replace('{version}', data.version); } var availableTiles = data.available; - + var availability; if (defined(availableTiles)) { - that._availability = new TileAvailability(that._tilingScheme, availableTiles.length); + 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); + if (!defined(overallAvailability[level])) { + overallAvailability[level] = []; + } for (var rangeIndex = 0; rangeIndex < rangesAtLevel.length; ++rangeIndex) { var range = rangesAtLevel[rangeIndex]; - that._availability.addAvailableTileRange(level, range.startX, yTiles - range.endY - 1, range.endX, yTiles - range.startY - 1); + var yStart = yTiles - range.endY - 1; + var yEnd = yTiles - range.startY - 1; + overallAvailability[level].push([range.startX, yStart, range.endX, yEnd]); + availability.addAvailableTileRange(level, range.startX, yStart, range.endX, yEnd); } } } - if (!defined(that._credit) && defined(data.attribution) && data.attribution !== null) { - that._credit = new Credit(data.attribution); - } - // 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. @@ -230,17 +250,82 @@ define([ // 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) { - that._hasVertexNormals = true; + hasVertexNormals = true; } else if (defined(data.extensions) && data.extensions.indexOf('vertexnormals') !== -1) { - that._hasVertexNormals = true; - that._littleEndianExtensionSize = false; + hasVertexNormals = true; + littleEndianExtensionSize = false; } if (defined(data.extensions) && data.extensions.indexOf('watermask') !== -1) { - that._hasWaterMask = true; + hasWaterMask = true; + } + + that._hasWaterMask = that._hasWaterMask || hasWaterMask; + that._hasVertexNormals = that._hasVertexNormals || hasVertexNormals; + if (defined(data.attribution)) { + if (attribution.length > 0) { + attribution += ' '; + } + attribution += data.attribution; + } + + layers.push(new LayerInformation({ + isHeightmap: isHeightmap, + tileUrlTemplates: tileUrlTemplates, + availability: availability, + hasVertexNormals: hasVertexNormals, + hasWaterMask: hasWaterMask, + littleEndianExtensionSize: littleEndianExtensionSize + })); + + var parentUrl = data.parentUrl; + if (defined(parentUrl)) { + if (!defined(availability)) { + console.log('A layer.json can\'t have a parentUrl if it does\'t have an available array.'); + return when.resolve(); + } + lastUrl = joinUrls(lastUrl, parentUrl); + metadataUrl = joinUrls(lastUrl, 'layer.json'); + if (defined(that._proxy)) { + metadataUrl = that._proxy.getURL(metadataUrl); + } + var parentMetadata = loadJson(metadataUrl); + return when(parentMetadata, parseMetadataSuccess, parseMetadataFailure); } - that._ready = true; - that._readyPromise.resolve(true); + return when.resolve(); + } + + function parseMetadataFailure(data) { + var message = 'An error occurred while accessing ' + metadataUrl + '.'; + metadataError = TileProviderError.handleError(metadataError, that, that._errorEvent, message, undefined, undefined, undefined, requestMetadata); + } + + function metadataSuccess(data) { + parseMetadataSuccess(data) + .then(function() { + if (defined(metadataError)) { + return; + } + + var length = overallAvailability.length; + if (length > 0) { + var availability = that._availability = new TileAvailability(that._tilingScheme, length); + for (var level = 0; level < length; ++level) { + var levelRanges = overallAvailability[level]; + for (var i = 0; i < levelRanges.length; ++i) { + var range = levelRanges[i]; + availability.addAvailableTileRange(level, range[0], range[1], range[2], range[3]); + } + } + } + + if (!defined(that._credit) && attribution.length > 0) { + that._credit = new Credit(attribution); + } + + that._ready = true; + that._readyPromise.resolve(true); + }); } function metadataFailure(data) { @@ -257,8 +342,7 @@ define([ }); return; } - var message = 'An error occurred while accessing ' + metadataUrl + '.'; - metadataError = TileProviderError.handleError(metadataError, that, that._errorEvent, message, undefined, undefined, undefined, requestMetadata); + parseMetadataFailure(data); } function requestMetadata() { @@ -320,7 +404,7 @@ define([ }); } - function createQuantizedMeshTerrainData(provider, buffer, level, x, y, tmsY) { + function createQuantizedMeshTerrainData(provider, buffer, level, x, y, tmsY, littleEndianExtensionSize) { var pos = 0; var cartesian3Elements = 3; var boundingSphereElements = cartesian3Elements + 1; @@ -431,7 +515,7 @@ define([ while (pos < view.byteLength) { var extensionId = view.getUint8(pos, true); pos += Uint8Array.BYTES_PER_ELEMENT; - var extensionLength = view.getUint32(pos, provider._littleEndianExtensionSize); + var extensionLength = view.getUint32(pos, littleEndianExtensionSize); pos += Uint32Array.BYTES_PER_ELEMENT; if (extensionId === QuantizedMeshExtensionIds.OCT_VERTEX_NORMALS && provider._requestVertexNormals) { @@ -505,7 +589,27 @@ define([ } //>>includeEnd('debug'); - var urlTemplates = this._tileUrlTemplates; + var layers = this._layers; + var layerToUse; + var layerCount = layers.length; + + if (layerCount === 1) { // Optimized path for single layers + layerToUse = layers[0]; + } else { + for (var i = 0; i < layerCount; ++i) { + var layer = layers[i]; + if (!defined(layer.availability) || layer.availability.isTileAvailable(level, x, y)) { + layerToUse = layer; + break; + } + } + } + + if (!defined(layerToUse)) { + return when.reject(new RuntimeError('Terrain tile doesn\'t exist')); + } + + var urlTemplates = layerToUse.tileUrlTemplates; if (urlTemplates.length === 0) { return undefined; } @@ -522,10 +626,10 @@ define([ } var extensionList = []; - if (this._requestVertexNormals && this._hasVertexNormals) { - extensionList.push(this._littleEndianExtensionSize ? 'octvertexnormals' : 'vertexnormals'); + if (this._requestVertexNormals && layerToUse.hasVertexNormals) { + extensionList.push(layerToUse.littleEndianExtensionSize ? 'octvertexnormals' : 'vertexnormals'); } - if (this._requestWaterMask && this._hasWaterMask) { + if (this._requestWaterMask && layerToUse.hasWaterMask) { extensionList.push('watermask'); } @@ -540,7 +644,7 @@ define([ if (defined(that._heightmapStructure)) { return createHeightmapTerrainData(that, buffer, level, x, y, tmsY); } - return createQuantizedMeshTerrainData(that, buffer, level, x, y, tmsY); + return createQuantizedMeshTerrainData(that, buffer, level, x, y, tmsY, layerToUse.littleEndianExtensionSize); }); }; @@ -723,10 +827,11 @@ define([ * @returns {Boolean} Undefined if not supported, otherwise true or false. */ CesiumTerrainProvider.prototype.getTileDataAvailable = function(x, y, level) { - if (!defined(this.availability)) { + if (!defined(this._availability)) { return undefined; } - return this.availability.isTileAvailable(level, x, y); + + return this._availability.isTileAvailable(level, x, y); }; return CesiumTerrainProvider; diff --git a/Specs/Core/CesiumTerrainProviderSpec.js b/Specs/Core/CesiumTerrainProviderSpec.js index c9c8a0b32b02..f6999c85bf15 100644 --- a/Specs/Core/CesiumTerrainProviderSpec.js +++ b/Specs/Core/CesiumTerrainProviderSpec.js @@ -73,6 +73,20 @@ defineSuite([ return returnTileJson('Data/CesiumTerrainTileJson/PartialAvailability.tile.json'); } + function returnParentUrlTileJson() { + var paths = ['Data/CesiumTerrainTileJson/ParentUrl.tile.json', + 'Data/CesiumTerrainTileJson/Parent.tile.json']; + var i = 0; + var oldLoad = loadWithXhr.load; + loadWithXhr.load = function(url, responseType, method, data, headers, deferred, overrideMimeType) { + if (url.indexOf('layer.json') >= 0) { + loadWithXhr.defaultLoad(paths[i++], responseType, method, data, headers, deferred); + } else { + return oldLoad(url, responseType, method, data, headers, deferred, overrideMimeType); + } + }; + } + function waitForTile(level, x, y, requestNormals, requestWaterMask, f) { var terrainProvider = new CesiumTerrainProvider({ url : 'made/up/url', @@ -247,6 +261,42 @@ defineSuite([ }); }); + it('requests parent layer.json', function() { + returnParentUrlTileJson(); + + var provider = new CesiumTerrainProvider({ + url : 'made/up/url', + requestVertexNormals : true, + requestWaterMask : true + }); + + return pollToPromise(function() { + return provider.ready; + }).then(function() { + expect(provider.credit.text).toBe('This is a child tileset! This amazing data is courtesy The Amazing Data Source!'); + expect(provider.requestVertexNormals).toBe(true); + expect(provider.requestWaterMask).toBe(true); + expect(provider.hasVertexNormals).toBe(false); // Neither tileset has them + expect(provider.hasWaterMask).toBe(true); // The child tileset has them + expect(provider.availability.isTileAvailable(1, 2, 1)).toBe(true); // Both have this + expect(provider.availability.isTileAvailable(1, 3, 1)).toBe(true); // Parent has this, but child doesn't + expect(provider.availability.isTileAvailable(2, 0, 0)).toBe(false); // Neither has this + + var layers = provider._layers; + expect(layers.length).toBe(2); + expect(layers[0].hasVertexNormals).toBe(false); + expect(layers[0].hasWaterMask).toBe(true); + expect(layers[0].availability.isTileAvailable(1, 2, 1)).toBe(true); + expect(layers[0].availability.isTileAvailable(1, 3, 1)).toBe(false); + expect(layers[0].availability.isTileAvailable(2, 0, 0)).toBe(false); + expect(layers[1].hasVertexNormals).toBe(false); + expect(layers[1].hasWaterMask).toBe(false); + expect(layers[1].availability.isTileAvailable(1, 2, 1)).toBe(true); + expect(layers[1].availability.isTileAvailable(1, 3, 1)).toBe(true); + expect(layers[1].availability.isTileAvailable(2, 0, 0)).toBe(false); + }); + }); + it('raises an error if layer.json does not specify a format', function() { returnTileJson('Data/CesiumTerrainTileJson/NoFormat.tile.json'); diff --git a/Specs/Data/CesiumTerrainTileJson/Parent.tile.json b/Specs/Data/CesiumTerrainTileJson/Parent.tile.json new file mode 100644 index 000000000000..7651c494254b --- /dev/null +++ b/Specs/Data/CesiumTerrainTileJson/Parent.tile.json @@ -0,0 +1,28 @@ +{ + "tilejson": "2.1.0", + "format" : "quantized-mesh-1.0", + "version" : "1.0.0", + "scheme" : "tms", + "attribution" : "This amazing data is courtesy The Amazing Data Source!", + "tiles" : [ + "{z}/{x}/{y}.terrain?v={version}" + ], + "available" : [ + [ + { + "startX" : 0, + "startY" : 0, + "endX" : 1, + "endY" : 0 + } + ], + [ + { + "startX" : 0, + "startY" : 0, + "endX" : 3, + "endY" : 1 + } + ] + ] +} diff --git a/Specs/Data/CesiumTerrainTileJson/ParentUrl.tile.json b/Specs/Data/CesiumTerrainTileJson/ParentUrl.tile.json new file mode 100644 index 000000000000..baf406ba4b0c --- /dev/null +++ b/Specs/Data/CesiumTerrainTileJson/ParentUrl.tile.json @@ -0,0 +1,33 @@ +{ + "tilejson": "2.1.0", + "format" : "quantized-mesh-1.0", + "version" : "1.0.0", + "scheme" : "tms", + "attribution" : "This is a child tileset!", + "tiles" : [ + "{z}/{x}/{y}.terrain?v={version}" + ], + "extensions" : [ + "watermask" + ], + "available" : [ + [ + { + "startX" : 0, + "startY" : 0, + "endX" : 1, + "endY" : 0 + } + ], + [ + { + "startX" : 0, + "startY" : 0, + "endX" : 2, + "endY" : 1 + } + ] + ], + "parentUrl": "./" +} +