cesium
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
597 lines (519 loc) • 23.2 kB
JavaScript
import when from '../ThirdParty/when.js';
import Cartesian2 from './Cartesian2.js';
import Credit from './Credit.js';
import defaultValue from './defaultValue.js';
import defined from './defined.js';
import DeveloperError from './DeveloperError.js';
import Ellipsoid from './Ellipsoid.js';
import Event from './Event.js';
import GeographicTilingScheme from './GeographicTilingScheme.js';
import HeightmapEncoding from './HeightmapEncoding.js';
import HeightmapTerrainData from './HeightmapTerrainData.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 TerrainProvider from './TerrainProvider.js';
import TileAvailability from './TileAvailability.js';
import TileProviderError from './TileProviderError.js';
import WebMercatorTilingScheme from './WebMercatorTilingScheme.js';
var ALL_CHILDREN = 15;
/**
* A {@link TerrainProvider} that produces terrain geometry by tessellating height maps
* retrieved from Elevation Tiles of an an ArcGIS ImageService.
*
* @alias ArcGISTiledElevationTerrainProvider
* @constructor
*
* @param {Object} options Object with the following properties:
* @param {Resource|String|Promise<Resource>|Promise<String>} options.url The URL of the ArcGIS ImageServer service.
* @param {String} [options.token] The authorization token to use to connect to the service.
* @param {Ellipsoid} [options.ellipsoid] The ellipsoid. If the tilingScheme is specified,
* this parameter is ignored and the tiling scheme's ellipsoid is used instead.
* If neither parameter is specified, the WGS84 ellipsoid is used.
*
* @example
* var terrainProvider = new Cesium.ArcGISTiledElevationTerrainProvider({
* url : 'https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer',
* token : 'KED1aF_I4UzXOHy3BnhwyBHU4l5oY6rO6walkmHoYqGp4XyIWUd5YZUC1ZrLAzvV40pR6gBXQayh0eFA8m6vPg..'
* });
* viewer.terrainProvider = terrainProvider;
*
* @see TerrainProvider
*/
function ArcGISTiledElevationTerrainProvider(options) {
//>>includeStart('debug', pragmas.debug);
if (!defined(options) || !defined(options.url)) {
throw new DeveloperError('options.url is required.');
}
//>>includeEnd('debug');
this._resource = undefined;
this._credit = undefined;
this._tilingScheme = undefined;
this._levelZeroMaximumGeometricError = undefined;
this._maxLevel = undefined;
this._terrainDataStructure = undefined;
this._ready = false;
this._width = undefined;
this._height = undefined;
this._encoding = undefined;
var token = options.token;
this._hasAvailability = false;
this._tilesAvailable = undefined;
this._tilesAvailablityLoaded = undefined;
this._availableCache = {};
var that = this;
var ellipsoid = defaultValue(options.ellipsoid, Ellipsoid.WGS84);
this._readyPromise = when(options.url)
.then(function(url) {
var resource = Resource.createIfNeeded(url);
resource.appendForwardSlash();
if (defined(token)) {
resource = resource.getDerivedResource({
queryParameters: {
token: token
}
});
}
that._resource = resource;
var metadataResource = resource.getDerivedResource({
queryParameters: {
f: 'pjson'
}
});
return metadataResource.fetchJson();
})
.then(function(metadata) {
var copyrightText = metadata.copyrightText;
if (defined(copyrightText)) {
that._credit = new Credit(copyrightText);
}
var spatialReference = metadata.spatialReference;
var wkid = defaultValue(spatialReference.latestWkid, spatialReference.wkid);
var extent = metadata.extent;
var tilingSchemeOptions = {
ellipsoid: ellipsoid
};
if (wkid === 4326) {
tilingSchemeOptions.rectangle = Rectangle.fromDegrees(extent.xmin, extent.ymin,
extent.xmax, extent.ymax);
that._tilingScheme = new GeographicTilingScheme(tilingSchemeOptions);
} else if (wkid === 3857) {
tilingSchemeOptions.rectangleSouthwestInMeters = new Cartesian2(extent.xmin, extent.ymin);
tilingSchemeOptions.rectangleNortheastInMeters = new Cartesian2(extent.xmax, extent.ymax);
that._tilingScheme = new WebMercatorTilingScheme(tilingSchemeOptions);
} else {
return when.reject(new RuntimeError('Invalid spatial reference'));
}
var tileInfo = metadata.tileInfo;
if (!defined(tileInfo)) {
return when.reject(new RuntimeError('tileInfo is required'));
}
that._width = tileInfo.rows + 1;
that._height = tileInfo.cols + 1;
that._encoding = (tileInfo.format === 'LERC') ? HeightmapEncoding.LERC : HeightmapEncoding.NONE;
that._lodCount = tileInfo.lods.length - 1;
var hasAvailability = that._hasAvailability = (metadata.capabilities.indexOf('Tilemap') !== -1);
if (hasAvailability) {
that._tilesAvailable = new TileAvailability(that._tilingScheme, that._lodCount);
that._tilesAvailable.addAvailableTileRange(0, 0, 0,
that._tilingScheme.getNumberOfXTilesAtLevel(0), that._tilingScheme.getNumberOfYTilesAtLevel(0));
that._tilesAvailablityLoaded = new TileAvailability(that._tilingScheme, that._lodCount);
}
that._levelZeroMaximumGeometricError = TerrainProvider.getEstimatedLevelZeroGeometricErrorForAHeightmap(that._tilingScheme.ellipsoid, that._width, that._tilingScheme.getNumberOfXTilesAtLevel(0));
if (metadata.bandCount > 1) {
console.log('ArcGISTiledElevationTerrainProvider: Terrain data has more than 1 band. Using the first one.');
}
that._terrainDataStructure = {
elementMultiplier: 1.0,
lowestEncodedHeight: metadata.minValues[0],
highestEncodedHeight: metadata.maxValues[0]
};
that._ready = true;
return true;
})
.otherwise(function(error) {
var message = 'An error occurred while accessing ' + that._resource.url + '.';
TileProviderError.handleError(undefined, that, that._errorEvent, message);
return when.reject(error);
});
this._errorEvent = new Event();
}
Object.defineProperties(ArcGISTiledElevationTerrainProvider.prototype, {
/**
* Gets an event that is raised when the terrain 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 ArcGISTiledElevationTerrainProvider.prototype
* @type {Event}
*/
errorEvent: {
get: function() {
return this._errorEvent;
}
},
/**
* 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 ArcGISTiledElevationTerrainProvider#ready} returns true.
* @memberof ArcGISTiledElevationTerrainProvider.prototype
* @type {Credit}
*/
credit: {
get: function() {
//>>includeStart('debug', pragmas.debug);
if (!this.ready) {
throw new DeveloperError('credit must not be called before ready returns true.');
}
//>>includeEnd('debug');
return this._credit;
}
},
/**
* Gets the tiling scheme used by this provider. This function should
* not be called before {@link ArcGISTiledElevationTerrainProvider#ready} returns true.
* @memberof ArcGISTiledElevationTerrainProvider.prototype
* @type {GeographicTilingScheme}
*/
tilingScheme: {
get: function() {
//>>includeStart('debug', pragmas.debug);
if (!this.ready) {
throw new DeveloperError('tilingScheme must not be called before ready returns true.');
}
//>>includeEnd('debug');
return this._tilingScheme;
}
},
/**
* Gets a value indicating whether or not the provider is ready for use.
* @memberof ArcGISTiledElevationTerrainProvider.prototype
* @type {Boolean}
*/
ready: {
get: function() {
return this._ready;
}
},
/**
* Gets a promise that resolves to true when the provider is ready for use.
* @memberof ArcGISTiledElevationTerrainProvider.prototype
* @type {Promise.<Boolean>}
* @readonly
*/
readyPromise: {
get: function() {
return this._readyPromise;
}
},
/**
* 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 ArcGISTiledElevationTerrainProvider#ready} returns true.
* @memberof ArcGISTiledElevationTerrainProvider.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 ArcGISTiledElevationTerrainProvider#ready} returns true.
* @memberof ArcGISTiledElevationTerrainProvider.prototype
* @type {Boolean}
*/
hasVertexNormals: {
get: function() {
return false;
}
}
});
/**
* Requests the geometry for a given tile. This function should not be called before
* {@link ArcGISTiledElevationTerrainProvider#ready} returns true. The result includes terrain
* data and indicates that all 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.
*/
ArcGISTiledElevationTerrainProvider.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 tileResource = this._resource.getDerivedResource({
url: 'tile/' + level + '/' + y + '/' + x,
request: request
});
var hasAvailability = this._hasAvailability;
var availabilityPromise = when.resolve(true);
var availabilityRequest;
if (hasAvailability && !defined(isTileAvailable(this, level + 1, x * 2, y * 2))) {
// We need to load child availability
var availabilityResult = requestAvailability(this, level + 1, x * 2, y * 2);
availabilityPromise = availabilityResult.promise;
availabilityRequest = availabilityResult.request;
}
var promise = tileResource.fetchArrayBuffer();
if (!defined(promise) || !defined(availabilityPromise)) {
return undefined;
}
var that = this;
var tilesAvailable = this._tilesAvailable;
return when.join(promise, availabilityPromise)
.then(function(result) {
return new HeightmapTerrainData({
buffer: result[0],
width: that._width,
height: that._height,
childTileMask: hasAvailability ? tilesAvailable.computeChildMaskForTile(level, x, y) : ALL_CHILDREN,
structure: that._terrainDataStructure,
encoding: that._encoding
});
})
.otherwise(function(error) {
if (defined(availabilityRequest) && (availabilityRequest.state === RequestState.CANCELLED)) {
request.cancel();
// Don't reject the promise till the request is actually cancelled
// Otherwise it will think the request failed, but it didn't.
return request.deferred.promise
.always(function() {
request.state = RequestState.CANCELLED;
return when.reject(error);
});
}
return when.reject(error);
});
};
function isTileAvailable(that, level, x, y) {
if (!that._hasAvailability) {
return undefined;
}
var tilesAvailablityLoaded = that._tilesAvailablityLoaded;
var tilesAvailable = that._tilesAvailable;
if (level > that._lodCount) {
return false;
}
// Check if tiles are known to be available
if (tilesAvailable.isTileAvailable(level, x, y)) {
return true;
}
// or to not be available
if (tilesAvailablityLoaded.isTileAvailable(level, x, y)) {
return false;
}
return undefined;
}
/**
* 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.
*/
ArcGISTiledElevationTerrainProvider.prototype.getLevelMaximumGeometricError = function(level) {
//>>includeStart('debug', pragmas.debug);
if (!this.ready) {
throw new DeveloperError('getLevelMaximumGeometricError must not be called before ready returns true.');
}
//>>includeEnd('debug');
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.
*/
ArcGISTiledElevationTerrainProvider.prototype.getTileDataAvailable = function(x, y, level) {
if (!this._hasAvailability) {
return undefined;
}
var result = isTileAvailable(this, level, x, y);
if (defined(result)) {
return result;
}
requestAvailability(this, level, x, y);
return undefined;
};
/**
* 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
*/
ArcGISTiledElevationTerrainProvider.prototype.loadTileDataAvailability = function(x, y, level) {
return undefined;
};
function findRange(origin, width, height, data) {
var endCol = width - 1;
var endRow = height - 1;
var value = data[origin.y * width + origin.x];
var endingIndices = [];
var range = {
startX: origin.x,
startY: origin.y,
endX: 0,
endY: 0
};
var corner = new Cartesian2(origin.x + 1, origin.y + 1);
var doneX = false;
var doneY = false;
while (!(doneX && doneY)) {
// We want to use the original value when checking Y,
// so get it before it possibly gets incremented
var endX = corner.x;
// If we no longer move in the Y direction we need to check the corner tile in X pass
var endY = doneY ? (corner.y + 1) : corner.y;
// Check X range
if (!doneX) {
for (var y = origin.y; y < endY; ++y) {
if (data[y * width + corner.x] !== value) {
doneX = true;
break;
}
}
if (doneX) {
endingIndices.push(new Cartesian2(corner.x, origin.y));
// Use the last good column so we can continue with Y
--corner.x;
--endX;
range.endX = corner.x;
} else if (corner.x === endCol) {
range.endX = corner.x;
doneX = true;
} else {
++corner.x;
}
}
// Check Y range - The corner tile is checked here
if (!doneY) {
var col = corner.y * width;
for (var x = origin.x; x <= endX; ++x) {
if (data[col + x] !== value) {
doneY = true;
break;
}
}
if (doneY) {
endingIndices.push(new Cartesian2(origin.x, corner.y));
// Use the last good row so we can continue with X
--corner.y;
range.endY = corner.y;
} else if (corner.y === endRow) {
range.endY = corner.y;
doneY = true;
} else {
++corner.y;
}
}
}
return {
endingIndices: endingIndices,
range: range,
value: value
};
}
function computeAvailability(x, y, width, height, data) {
var ranges = [];
var singleValue = data.every(function(val) {
return val === data[0];
});
if (singleValue) {
if (data[0] === 1) {
ranges.push({
startX: x,
startY: y,
endX: x + width - 1,
endY: y + height - 1
});
}
return ranges;
}
var positions = [new Cartesian2(0, 0)];
while (positions.length > 0) {
var origin = positions.pop();
var result = findRange(origin, width, height, data);
if (result.value === 1) {
// Convert range into the array into global tile coordinates
var range = result.range;
range.startX += x;
range.endX += x;
range.startY += y;
range.endY += y;
ranges.push(range);
}
var endingIndices = result.endingIndices;
if (endingIndices.length > 0) {
positions = positions.concat(endingIndices);
}
}
return ranges;
}
function requestAvailability(that, level, x, y) {
if (!that._hasAvailability) {
return {};
}
// Fetch 128x128 availability list, so we make the minimum amount of requests
var xOffset = Math.floor(x / 128) * 128;
var yOffset = Math.floor(y / 128) * 128;
var dim = Math.min(1 << level, 128);
var url = 'tilemap/' + level + '/' + yOffset + '/' + xOffset + '/' + dim + '/' + dim;
var availableCache = that._availableCache;
if (defined(availableCache[url])) {
return availableCache[url];
}
var request = new Request({
throttle: true,
throttleByServer: true,
type: RequestType.TERRAIN
});
var tilemapResource = that._resource.getDerivedResource({
url: url,
request: request
});
var promise = tilemapResource.fetchJson();
if (!defined(promise)) {
return {};
}
promise = promise
.then(function(result) {
var available = computeAvailability(xOffset, yOffset, dim, dim, result.data);
// Mark whole area as having availability loaded
that._tilesAvailablityLoaded.addAvailableTileRange(xOffset, yOffset, xOffset + dim, yOffset + dim);
var tilesAvailable = that._tilesAvailable;
for (var i = 0; i < available.length; ++i) {
var range = available[i];
tilesAvailable.addAvailableTileRange(level, range.startX, range.startY, range.endX, range.endY);
}
// Conveniently return availability of original tile
return isTileAvailable(that, level, x, y);
});
availableCache[url] = {
promise: promise,
request: request
};
promise = promise
.always(function(result) {
delete availableCache[url];
return result;
});
return {
promise: promise,
request: request
};
}
export default ArcGISTiledElevationTerrainProvider;