import Credit from "./Credit.js"; import defaultValue from "./defaultValue.js"; import defined from "./defined.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"; const TerrainState = { UNKNOWN: 0, NONE: 1, SELF: 2, PARENT: 3, }; const 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) { const terrainCache = this._terrainCache; const 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) { const terrainCache = this._terrainCache; const keys = Object.keys(terrainCache); const count = keys.length; for (let i = 0; i < count; ++i) { const k = keys[i]; const 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 * const geeMetadata = new GoogleEarthEnterpriseMetadata('http://www.earthenterprise.org/3d'); * const 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'); let metadata; if (defined(options.metadata)) { metadata = options.metadata; } else { const 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, }); let 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; const that = this; let metadataError; this._readyPromise = metadata.readyPromise .then(function (result) { if (!metadata.terrainPresent) { const 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 Promise.reject(e); } TileProviderError.handleSuccess(metadataError); that._ready = result; return result; }) .catch(function (e) { metadataError = TileProviderError.handleError( metadataError, that, that._errorEvent, e.message, undefined, undefined, undefined, e ); return Promise.reject(e); }); } Object.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} * @readonly */ 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} * @readonly */ 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} * @readonly */ availability: { get: function () { return undefined; }, }, }); const taskProcessor = new TaskProcessor("decodeGoogleEarthEnterprisePacket"); // 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) { let childMask = info.getChildBitmask(); if (info.terrainState === TerrainState.PARENT) { childMask = 0; for (let i = 0; i < 4; ++i) { const 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'); const quadKey = GoogleEarthEnterpriseMetadata.tileXYToQuadKey(x, y, level); const terrainCache = this._terrainCache; const metadata = this._metadata; const info = metadata.getTileInformationFromQuadKey(quadKey); // Check if this tile is even possibly available if (!defined(info)) { return Promise.reject(new RuntimeError("Terrain tile doesn't exist")); } let 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 const buffer = terrainCache.get(quadKey); if (defined(buffer)) { const credit = metadata.providers[info.terrainProvider]; return Promise.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 Promise.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 Promise.reject(new RuntimeError("Terrain tile doesn't exist")); } // Figure out where we are getting the terrain and what version let parentInfo; let q = quadKey; let 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 Promise.reject(new RuntimeError("Terrain tile doesn't exist")); } // Load that terrain const terrainPromises = this._terrainPromises; const terrainRequests = this._terrainRequests; let sharedPromise; let 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; const 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 const requestedInfo = metadata.getTileInformationFromQuadKey(q); requestedInfo.terrainState = TerrainState.SELF; terrainCache.add(q, terrainTiles[0]); const provider = requestedInfo.terrainProvider; // Add children to cache const count = terrainTiles.length - 1; for (let j = 0; j < count; ++j) { const childKey = q + j.toString(); const 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 Promise.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.finally(function () { delete terrainPromises[q]; delete terrainRequests[q]; }); } return sharedPromise .then(function () { const buffer = terrainCache.get(quadKey); if (defined(buffer)) { const 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 Promise.reject(new RuntimeError("Failed to load terrain.")); }) .catch(function (error) { if (sharedRequest.state === RequestState.CANCELLED) { request.state = sharedRequest.state; return Promise.reject(error); } info.terrainState = TerrainState.NONE; return Promise.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} Undefined if not supported, otherwise true or false. */ GoogleEarthEnterpriseTerrainProvider.prototype.getTileDataAvailable = function ( x, y, level ) { const metadata = this._metadata; let quadKey = GoogleEarthEnterpriseMetadata.tileXYToQuadKey(x, y, level); const 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 } const 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); const 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 const request = new Request({ throttle: false, 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} */ 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;