import Check from "./Check.js"; import Credit from "./Credit.js"; import defaultValue from "./defaultValue.js"; import defined from "./defined.js"; import deprecationWarning from "./deprecationWarning.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); } }; /** * @typedef {Object} GoogleEarthEnterpriseTerrainProvider.ConstructorOptions * * Initialization options for GoogleEarthEnterpriseTerrainProvider constructor * * @property {Ellipsoid} [ellipsoid] The ellipsoid. If not specified, the WGS84 ellipsoid is used. * @property {Credit|string} [credit] A credit for the data source, which is displayed on the canvas. * @property {Resource|string} [url] The url of the Google Earth Enterprise server hosting the imagery. Deprecated. * @property {GoogleEarthEnterpriseMetadata} [metadata] A metadata object that can be used to share metadata requests with a GoogleEarthEnterpriseImageryProvider. Deprecated. */ /** *
* To construct a GoogleEarthEnterpriseTerrainProvider, call {@link GoogleEarthEnterpriseTerrainProvider.fromMetadata}. Do not call the constructor directly. *
* * Provides tiled terrain using the Google Earth Enterprise REST API. * * @alias GoogleEarthEnterpriseTerrainProvider * @constructor * * @param {GoogleEarthEnterpriseTerrainProvider.ConstructorOptions} options An object describing initialization options * * @see GoogleEarthEnterpriseTerrainProvider.fromMetadata * @see GoogleEarthEnterpriseMetadata.fromUrl * @see GoogleEarthEnterpriseImageryProvider * @see CesiumTerrainProvider * * @example * const geeMetadata = await GoogleEarthEnterpriseMetadata.fromUrl("http://www.example.com"); * const gee = Cesium.GoogleEarthEnterpriseTerrainProvider.fromMetadata(geeMetadata); * * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} */ function GoogleEarthEnterpriseTerrainProvider(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); 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; if (defined(options.url)) { deprecationWarning( "GoogleEarthEnterpriseTerrainProvider options.url", "options.url was deprecated in CesiumJS 1.104. It will be in CesiumJS 1.107. Use GoogleEarthEnterpriseTerrainProvider.fromMetadata instead." ); const resource = Resource.createIfNeeded(options.url); const that = this; let metadataError; this._readyPromise = GoogleEarthEnterpriseMetadata.fromUrl(resource) .then((metadata) => { if (!metadata.terrainPresent) { const e = new RuntimeError( `The server ${metadata.url} doesn't have terrain` ); return Promise.reject(e); } TileProviderError.reportSuccess(metadataError); that._metadata = metadata; that._ready = true; return true; }) .catch((e) => { metadataError = TileProviderError.reportError( metadataError, that, that._errorEvent, e.message, undefined, undefined, undefined, e ); throw e; }); } else if (defined(options.metadata)) { deprecationWarning( "GoogleEarthEnterpriseTerrainProvider options.metadata", "options.metadata was deprecated in CesiumJS 1.104. It will be in CesiumJS 1.107. Use GoogleEarthEnterpriseTerrainProvider.fromMetadata instead." ); const metadata = options.metadata; this._metadata = metadata; const that = this; this._readyPromise = Promise.resolve(this._metadata._readyPromise).then( () => { if (!metadata.terrainPresent) { throw new RuntimeError( `The server ${metadata.url} doesn't have terrain` ); } that._ready = true; } ); } } 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. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype * @type {TilingScheme} * @readonly */ tilingScheme: { get: function () { 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 * @deprecated */ ready: { get: function () { deprecationWarning( "GoogleEarthEnterpriseTerrainProvider.ready", "GoogleEarthEnterpriseTerrainProvider.ready was deprecated in CesiumJS 1.104. It will be in CesiumJS 1.107." ); return this._ready; }, }, /** * Gets a promise that resolves to true when the provider is ready for use. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype * @type {Promise} * @readonly * @deprecated */ readyPromise: { get: function () { deprecationWarning( "GoogleEarthEnterpriseTerrainProvider.readyPromise", "GoogleEarthEnterpriseTerrainProvider.readyPromise was deprecated in CesiumJS 1.104. It will be in CesiumJS 1.107." ); 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. * @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. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype * @type {boolean} * @readonly */ hasWaterMask: { get: function () { return false; }, }, /** * Gets a value indicating whether or not the requested tiles include vertex normals. * @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 property may be undefined if availability * information is not available. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype * @type {TileAvailability} * @readonly */ availability: { get: function () { return undefined; }, }, }); /** * Creates a GoogleEarthTerrainProvider from GoogleEarthEnterpriseMetadata * * @param {GoogleEarthEnterpriseMetadata} metadata A metadata object that can be used to share metadata requests with a GoogleEarthEnterpriseImageryProvider. * @param {GoogleEarthEnterpriseTerrainProvider.ConstructorOptions} options An object describing initialization options * @returns {GoogleEarthEnterpriseTerrainProvider} * * @see GoogleEarthEnterpriseMetadata.fromUrl * * @exception {RuntimeError} metadata does not specify terrain * * @example * const geeMetadata = await GoogleEarthEnterpriseMetadata.fromUrl("http://www.example.com"); * const gee = Cesium.GoogleEarthEnterpriseTerrainProvider.fromMetadata(geeMetadata); */ GoogleEarthEnterpriseTerrainProvider.fromMetadata = function ( metadata, options ) { //>>includeStart('debug', pragmas.debug); Check.defined("metadata", metadata); //>>includeEnd('debug'); if (!metadata.terrainPresent) { throw new RuntimeError(`The server ${metadata.url} doesn't have terrain`); } const provider = new GoogleEarthEnterpriseTerrainProvider(options); provider._metadata = metadata; provider._readyPromise = Promise.resolve(true); provider._ready = true; return provider; }; 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. 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. */ GoogleEarthEnterpriseTerrainProvider.prototype.requestTileGeometry = function ( x, y, level, request ) { 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;