import when from '../ThirdParty/when.js'; import Credit from './Credit.js'; import defaultValue from './defaultValue.js'; import defined from './defined.js'; import defineProperties from './defineProperties.js'; import DeveloperError from './DeveloperError.js'; import Event from './Event.js'; import GeographicTilingScheme from './GeographicTilingScheme.js'; import GoogleEarthEnterpriseMetadata from './GoogleEarthEnterpriseMetadata.js'; import GoogleEarthEnterpriseTerrainData from './GoogleEarthEnterpriseTerrainData.js'; import HeightmapTerrainData from './HeightmapTerrainData.js'; import JulianDate from './JulianDate.js'; import CesiumMath from './Math.js'; import Rectangle from './Rectangle.js'; import Request from './Request.js'; import RequestState from './RequestState.js'; import RequestType from './RequestType.js'; import Resource from './Resource.js'; import RuntimeError from './RuntimeError.js'; import TaskProcessor from './TaskProcessor.js'; import TileProviderError from './TileProviderError.js'; var TerrainState = { UNKNOWN : 0, NONE : 1, SELF : 2, PARENT : 3 }; var julianDateScratch = new JulianDate(); function TerrainCache() { this._terrainCache = {}; this._lastTidy = JulianDate.now(); } TerrainCache.prototype.add = function(quadKey, buffer) { this._terrainCache[quadKey] = { buffer : buffer, timestamp : JulianDate.now() }; }; TerrainCache.prototype.get = function(quadKey) { var terrainCache = this._terrainCache; var result = terrainCache[quadKey]; if (defined(result)) { delete this._terrainCache[quadKey]; return result.buffer; } }; TerrainCache.prototype.tidy = function() { JulianDate.now(julianDateScratch); if (JulianDate.secondsDifference(julianDateScratch, this._lastTidy) > 10) { var terrainCache = this._terrainCache; var keys = Object.keys(terrainCache); var count = keys.length; for (var i = 0; i < count; ++i) { var k = keys[i]; var e = terrainCache[k]; if (JulianDate.secondsDifference(julianDateScratch, e.timestamp) > 10) { delete terrainCache[k]; } } JulianDate.clone(julianDateScratch, this._lastTidy); } }; /** * Provides tiled terrain using the Google Earth Enterprise REST API. * * @alias GoogleEarthEnterpriseTerrainProvider * @constructor * * @param {Object} options Object with the following properties: * @param {Resource|String} options.url The url of the Google Earth Enterprise server hosting the imagery. * @param {GoogleEarthEnterpriseMetadata} options.metadata A metadata object that can be used to share metadata requests with a GoogleEarthEnterpriseImageryProvider. * @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. * * @see GoogleEarthEnterpriseImageryProvider * @see CesiumTerrainProvider * * @example * var geeMetadata = new GoogleEarthEnterpriseMetadata('http://www.earthenterprise.org/3d'); * var gee = new Cesium.GoogleEarthEnterpriseTerrainProvider({ * metadata : geeMetadata * }); * * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} */ function GoogleEarthEnterpriseTerrainProvider(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); //>>includeStart('debug', pragmas.debug); if (!(defined(options.url) || defined(options.metadata))) { throw new DeveloperError('options.url or options.metadata is required.'); } //>>includeEnd('debug'); var metadata; if (defined(options.metadata)) { metadata = options.metadata; } else { var resource = Resource.createIfNeeded(options.url); metadata = new GoogleEarthEnterpriseMetadata(resource); } this._metadata = metadata; this._tilingScheme = new GeographicTilingScheme({ numberOfLevelZeroTilesX : 2, numberOfLevelZeroTilesY : 2, rectangle : new Rectangle(-CesiumMath.PI, -CesiumMath.PI, CesiumMath.PI, CesiumMath.PI), ellipsoid : options.ellipsoid }); var credit = options.credit; if (typeof credit === 'string') { credit = new Credit(credit); } this._credit = credit; // Pulled from Google's documentation this._levelZeroMaximumGeometricError = 40075.16; this._terrainCache = new TerrainCache(); this._terrainPromises = {}; this._terrainRequests = {}; this._errorEvent = new Event(); this._ready = false; var that = this; var metadataError; this._readyPromise = metadata.readyPromise .then(function(result) { if (!metadata.terrainPresent) { var e = new RuntimeError('The server ' + metadata.url + ' doesn\'t have terrain'); metadataError = TileProviderError.handleError(metadataError, that, that._errorEvent, e.message, undefined, undefined, undefined, e); return when.reject(e); } TileProviderError.handleSuccess(metadataError); that._ready = result; return result; }) .otherwise(function(e) { metadataError = TileProviderError.handleError(metadataError, that, that._errorEvent, e.message, undefined, undefined, undefined, e); return when.reject(e); }); } defineProperties(GoogleEarthEnterpriseTerrainProvider.prototype, { /** * Gets the name of the Google Earth Enterprise server url hosting the imagery. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype * @type {String} * @readonly */ url : { get : function() { return this._metadata.url; } }, /** * Gets the proxy used by this provider. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype * @type {Proxy} * @readonly */ proxy : { get : function() { return this._metadata.proxy; } }, /** * Gets the tiling scheme used by this provider. This function should * not be called before {@link GoogleEarthEnterpriseTerrainProvider#ready} returns true. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype * @type {TilingScheme} * @readonly */ tilingScheme : { get : function() { //>>includeStart('debug', pragmas.debug); if (!this._ready) { throw new DeveloperError('tilingScheme must not be called before the imagery provider is ready.'); } //>>includeEnd('debug'); return this._tilingScheme; } }, /** * Gets an event that is raised when the imagery provider encounters an asynchronous error. By subscribing * to the event, you will be notified of the error and can potentially recover from it. Event listeners * are passed an instance of {@link TileProviderError}. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype * @type {Event} * @readonly */ errorEvent : { get : function() { return this._errorEvent; } }, /** * Gets a value indicating whether or not the provider is ready for use. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype * @type {Boolean} * @readonly */ ready : { get : function() { return this._ready; } }, /** * Gets a promise that resolves to true when the provider is ready for use. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype * @type {Promise.} * @readonly */ readyPromise : { get : function() { return this._readyPromise; } }, /** * Gets the credit to display when this terrain provider is active. Typically this is used to credit * the source of the terrain. This function should not be called before {@link GoogleEarthEnterpriseTerrainProvider#ready} returns true. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype * @type {Credit} * @readonly */ credit : { get : function() { return this._credit; } }, /** * Gets a value indicating whether or not the provider includes a water mask. The water mask * indicates which areas of the globe are water rather than land, so they can be rendered * as a reflective surface with animated waves. This function should not be * called before {@link GoogleEarthEnterpriseTerrainProvider#ready} returns true. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype * @type {Boolean} */ hasWaterMask : { get : function() { return false; } }, /** * Gets a value indicating whether or not the requested tiles include vertex normals. * This function should not be called before {@link GoogleEarthEnterpriseTerrainProvider#ready} returns true. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype * @type {Boolean} */ hasVertexNormals : { get : function() { return false; } }, /** * 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 * {@link GoogleEarthEnterpriseTerrainProvider#ready} returns true. This property may be undefined if availability * information is not available. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype * @type {TileAvailability} */ availability : { get : function() { return undefined; } } }); var taskProcessor = new TaskProcessor('decodeGoogleEarthEnterprisePacket', Number.POSITIVE_INFINITY); // If the tile has its own terrain, then you can just use its child bitmask. If it was requested using it's parent // then you need to check all of its children to see if they have terrain. function computeChildMask(quadKey, info, metadata) { var childMask = info.getChildBitmask(); if (info.terrainState === TerrainState.PARENT) { childMask = 0; for (var i = 0; i < 4; ++i) { var child = metadata.getTileInformationFromQuadKey(quadKey + i.toString()); if (defined(child) && child.hasTerrain()) { childMask |= (1 << i); } } } return childMask; } /** * Requests the geometry for a given tile. This function should not be called before * {@link GoogleEarthEnterpriseTerrainProvider#ready} returns true. The result must include terrain data and * may optionally include a water mask and an indication of which child tiles are available. * * @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. * @param {Request} [request] The request object. Intended for internal use only. * @returns {Promise.|undefined} A promise for the requested geometry. If this method * returns undefined instead of a promise, it is an indication that too many requests are already * pending and the request will be retried later. * * @exception {DeveloperError} This function must not be called before {@link GoogleEarthEnterpriseTerrainProvider#ready} * returns true. */ GoogleEarthEnterpriseTerrainProvider.prototype.requestTileGeometry = function(x, y, level, request) { //>>includeStart('debug', pragmas.debug) if (!this._ready) { throw new DeveloperError('requestTileGeometry must not be called before the terrain provider is ready.'); } //>>includeEnd('debug'); var quadKey = GoogleEarthEnterpriseMetadata.tileXYToQuadKey(x, y, level); var terrainCache = this._terrainCache; var metadata = this._metadata; var info = metadata.getTileInformationFromQuadKey(quadKey); // Check if this tile is even possibly available if (!defined(info)) { return when.reject(new RuntimeError('Terrain tile doesn\'t exist')); } var terrainState = info.terrainState; if (!defined(terrainState)) { // First time we have tried to load this tile, so set terrain state to UNKNOWN terrainState = info.terrainState = TerrainState.UNKNOWN; } // If its in the cache, return it var buffer = terrainCache.get(quadKey); if (defined(buffer)) { var credit = metadata.providers[info.terrainProvider]; return when.resolve(new GoogleEarthEnterpriseTerrainData({ buffer : buffer, childTileMask : computeChildMask(quadKey, info, metadata), credits : defined(credit) ? [credit] : undefined, negativeAltitudeExponentBias: metadata.negativeAltitudeExponentBias, negativeElevationThreshold: metadata.negativeAltitudeThreshold })); } // Clean up the cache terrainCache.tidy(); // We have a tile, check to see if no ancestors have terrain or that we know for sure it doesn't if (!info.ancestorHasTerrain) { // We haven't reached a level with terrain, so return the ellipsoid return when.resolve(new HeightmapTerrainData({ buffer : new Uint8Array(16 * 16), width : 16, height : 16 })); } else if (terrainState === TerrainState.NONE) { // Already have info and there isn't any terrain here return when.reject(new RuntimeError('Terrain tile doesn\'t exist')); } // Figure out where we are getting the terrain and what version var parentInfo; var q = quadKey; var terrainVersion = -1; switch (terrainState) { case TerrainState.SELF: // We have terrain and have retrieved it before terrainVersion = info.terrainVersion; break; case TerrainState.PARENT: // We have terrain in our parent q = q.substring(0, q.length - 1); parentInfo = metadata.getTileInformationFromQuadKey(q); terrainVersion = parentInfo.terrainVersion; break; case TerrainState.UNKNOWN: // We haven't tried to retrieve terrain yet if (info.hasTerrain()) { terrainVersion = info.terrainVersion; // We should have terrain } else { q = q.substring(0, q.length - 1); parentInfo = metadata.getTileInformationFromQuadKey(q); if (defined(parentInfo) && parentInfo.hasTerrain()) { terrainVersion = parentInfo.terrainVersion; // Try checking in the parent } } break; } // We can't figure out where to get the terrain if (terrainVersion < 0) { return when.reject(new RuntimeError('Terrain tile doesn\'t exist')); } // Load that terrain var terrainPromises = this._terrainPromises; var terrainRequests = this._terrainRequests; var sharedPromise; var sharedRequest; if (defined(terrainPromises[q])) { // Already being loaded possibly from another child, so return existing promise sharedPromise = terrainPromises[q]; sharedRequest = terrainRequests[q]; } else { // Create new request for terrain sharedRequest = request; var requestPromise = buildTerrainResource(this, q, terrainVersion, sharedRequest).fetchArrayBuffer(); if (!defined(requestPromise)) { return undefined; // Throttled } sharedPromise = requestPromise .then(function(terrain) { if (defined(terrain)) { return taskProcessor.scheduleTask({ buffer : terrain, type : 'Terrain', key : metadata.key }, [terrain]) .then(function(terrainTiles) { // Add requested tile and mark it as SELF var requestedInfo = metadata.getTileInformationFromQuadKey(q); requestedInfo.terrainState = TerrainState.SELF; terrainCache.add(q, terrainTiles[0]); var provider = requestedInfo.terrainProvider; // Add children to cache var count = terrainTiles.length - 1; for (var j = 0; j < count; ++j) { var childKey = q + j.toString(); var child = metadata.getTileInformationFromQuadKey(childKey); if (defined(child)) { terrainCache.add(childKey, terrainTiles[j + 1]); child.terrainState = TerrainState.PARENT; if (child.terrainProvider === 0) { child.terrainProvider = provider; } } } }); } return when.reject(new RuntimeError('Failed to load terrain.')); }); terrainPromises[q] = sharedPromise; // Store promise without delete from terrainPromises terrainRequests[q] = sharedRequest; // Set promise so we remove from terrainPromises just one time sharedPromise = sharedPromise .always(function() { delete terrainPromises[q]; delete terrainRequests[q]; }); } return sharedPromise .then(function() { var buffer = terrainCache.get(quadKey); if (defined(buffer)) { var credit = metadata.providers[info.terrainProvider]; return new GoogleEarthEnterpriseTerrainData({ buffer : buffer, childTileMask : computeChildMask(quadKey, info, metadata), credits : defined(credit) ? [credit] : undefined, negativeAltitudeExponentBias: metadata.negativeAltitudeExponentBias, negativeElevationThreshold: metadata.negativeAltitudeThreshold }); } return when.reject(new RuntimeError('Failed to load terrain.')); }) .otherwise(function(error) { if (sharedRequest.state === RequestState.CANCELLED) { request.state = sharedRequest.state; return when.reject(error); } info.terrainState = TerrainState.NONE; return when.reject(error); }); }; /** * Gets the maximum geometric error allowed in a tile at a given level. * * @param {Number} level The tile level for which to get the maximum geometric error. * @returns {Number} The maximum geometric error. */ GoogleEarthEnterpriseTerrainProvider.prototype.getLevelMaximumGeometricError = function(level) { return this._levelZeroMaximumGeometricError / (1 << level); }; /** * Determines whether data for a tile is available to be loaded. * * @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 {Boolean} Undefined if not supported, otherwise true or false. */ GoogleEarthEnterpriseTerrainProvider.prototype.getTileDataAvailable = function(x, y, level) { var metadata = this._metadata; var quadKey = GoogleEarthEnterpriseMetadata.tileXYToQuadKey(x, y, level); var info = metadata.getTileInformation(x, y, level); if (info === null) { return false; } if (defined(info)) { if (!info.ancestorHasTerrain) { return true; // We'll just return the ellipsoid } var terrainState = info.terrainState; if (terrainState === TerrainState.NONE) { return false; // Terrain is not available } if (!defined(terrainState) || (terrainState === TerrainState.UNKNOWN)) { info.terrainState = TerrainState.UNKNOWN; if (!info.hasTerrain()) { quadKey = quadKey.substring(0, quadKey.length - 1); var parentInfo = metadata.getTileInformationFromQuadKey(quadKey); if (!defined(parentInfo) || !parentInfo.hasTerrain()) { return false; } } } return true; } if (metadata.isValid(quadKey)) { // We will need this tile, so request metadata and return false for now var request = new Request({ throttle : true, throttleByServer : true, type : RequestType.TERRAIN }); metadata.populateSubtree(x, y, level, request); } 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 // function buildTerrainResource(terrainProvider, quadKey, version, request) { version = (defined(version) && version > 0) ? version : 1; return terrainProvider._metadata.resource.getDerivedResource({ url: 'flatfile?f1c-0' + quadKey + '-t.' + version.toString(), request: request }); } export default GoogleEarthEnterpriseTerrainProvider;