cesium
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
581 lines (521 loc) • 24 kB
JavaScript
import when from '../ThirdParty/when.js';
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';
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);
});
}
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.<Boolean>}
* @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.<TerrainData>|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;