You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

700 lines
20 KiB
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;
},
},
/**
* 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 TerrainProvider#ready} returns true. This property may be undefined if availability
* information is not available.
* @memberof ArcGISTiledElevationTerrainProvider.prototype
* @type {TileAvailability}
*/
availability: {
get: function () {
//>>includeStart('debug', pragmas.debug)
if (!this._ready) {
throw new DeveloperError(
"availability must not be called before the terrain provider is ready."
);
}
//>>includeEnd('debug');
return this._tilesAvailable;
},
},
});
/**
* 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<void>} 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: false,
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(
level,
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;