@cesium/engine
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
756 lines (671 loc) • 22.4 kB
JavaScript
import Cartesian2 from "./Cartesian2.js";
import Check from "./Check.js";
import Credit from "./Credit.js";
import Frozen from "./Frozen.js";
import defined from "./defined.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";
const ALL_CHILDREN = 15;
/**
* @typedef {Object} ArcGISTiledElevationTerrainProvider.ConstructorOptions
*
* Initialization options for the ArcGISTiledElevationTerrainProvider constructor
*
* @property {string} [token] The authorization token to use to connect to the service.
* @property {Ellipsoid} [ellipsoid=Ellipsoid.default] 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 default ellipsoid is used.
*/
/**
* Used to track creation details while fetching initial metadata
*
* @constructor
* @private
*
* @param {ArcGISTiledElevationTerrainProvider.ConstructorOptions} [options] An object describing initialization options.
*/
function TerrainProviderBuilder(options) {
this.ellipsoid = options.ellipsoid ?? Ellipsoid.default;
this.credit = undefined;
this.tilingScheme = undefined;
this.height = undefined;
this.width = undefined;
this.encoding = undefined;
this.lodCount = undefined;
this.hasAvailability = false;
this.tilesAvailable = undefined;
this.tilesAvailabilityLoaded = undefined;
this.levelZeroMaximumGeometricError = undefined;
this.terrainDataStructure = undefined;
}
/**
* Complete ArcGISTiledElevationTerrainProvider creation based on builder values.
*
* @private
*
* @param {ArcGISTiledElevationTerrainProvider} provider
*/
TerrainProviderBuilder.prototype.build = function (provider) {
provider._credit = this.credit;
provider._tilingScheme = this.tilingScheme;
provider._height = this.height;
provider._width = this.width;
provider._encoding = this.encoding;
provider._lodCount = this.lodCount;
provider._hasAvailability = this.hasAvailability;
provider._tilesAvailable = this.tilesAvailable;
provider._tilesAvailabilityLoaded = this.tilesAvailabilityLoaded;
provider._levelZeroMaximumGeometricError =
this.levelZeroMaximumGeometricError;
provider._terrainDataStructure = this.terrainDataStructure;
};
function parseMetadataSuccess(terrainProviderBuilder, metadata) {
const copyrightText = metadata.copyrightText;
if (defined(copyrightText)) {
terrainProviderBuilder.credit = new Credit(copyrightText);
}
const spatialReference = metadata.spatialReference;
const wkid = spatialReference.latestWkid ?? spatialReference.wkid;
const extent = metadata.extent;
const tilingSchemeOptions = {
ellipsoid: terrainProviderBuilder.ellipsoid,
};
if (wkid === 4326) {
tilingSchemeOptions.rectangle = Rectangle.fromDegrees(
extent.xmin,
extent.ymin,
extent.xmax,
extent.ymax,
);
terrainProviderBuilder.tilingScheme = new GeographicTilingScheme(
tilingSchemeOptions,
);
} else if (wkid === 3857) {
// Clamp extent to EPSG 3857 bounds
const epsg3857Bounds =
Math.PI * terrainProviderBuilder.ellipsoid.maximumRadius;
if (metadata.extent.xmax > epsg3857Bounds) {
metadata.extent.xmax = epsg3857Bounds;
}
if (metadata.extent.ymax > epsg3857Bounds) {
metadata.extent.ymax = epsg3857Bounds;
}
if (metadata.extent.xmin < -epsg3857Bounds) {
metadata.extent.xmin = -epsg3857Bounds;
}
if (metadata.extent.ymin < -epsg3857Bounds) {
metadata.extent.ymin = -epsg3857Bounds;
}
tilingSchemeOptions.rectangleSouthwestInMeters = new Cartesian2(
extent.xmin,
extent.ymin,
);
tilingSchemeOptions.rectangleNortheastInMeters = new Cartesian2(
extent.xmax,
extent.ymax,
);
terrainProviderBuilder.tilingScheme = new WebMercatorTilingScheme(
tilingSchemeOptions,
);
} else {
throw new RuntimeError("Invalid spatial reference");
}
const tileInfo = metadata.tileInfo;
if (!defined(tileInfo)) {
throw new RuntimeError("tileInfo is required");
}
terrainProviderBuilder.width = tileInfo.rows + 1;
terrainProviderBuilder.height = tileInfo.cols + 1;
terrainProviderBuilder.encoding =
tileInfo.format === "LERC"
? HeightmapEncoding.LERC
: HeightmapEncoding.NONE;
terrainProviderBuilder.lodCount = tileInfo.lods.length - 1;
const hasAvailability = (terrainProviderBuilder.hasAvailability =
metadata.capabilities.indexOf("Tilemap") !== -1);
if (hasAvailability) {
terrainProviderBuilder.tilesAvailable = new TileAvailability(
terrainProviderBuilder.tilingScheme,
terrainProviderBuilder.lodCount,
);
terrainProviderBuilder.tilesAvailable.addAvailableTileRange(
0,
0,
0,
terrainProviderBuilder.tilingScheme.getNumberOfXTilesAtLevel(0),
terrainProviderBuilder.tilingScheme.getNumberOfYTilesAtLevel(0),
);
terrainProviderBuilder.tilesAvailabilityLoaded = new TileAvailability(
terrainProviderBuilder.tilingScheme,
terrainProviderBuilder.lodCount,
);
}
terrainProviderBuilder.levelZeroMaximumGeometricError =
TerrainProvider.getEstimatedLevelZeroGeometricErrorForAHeightmap(
terrainProviderBuilder.tilingScheme.ellipsoid,
terrainProviderBuilder.width,
terrainProviderBuilder.tilingScheme.getNumberOfXTilesAtLevel(0),
);
if (metadata.bandCount > 1) {
console.log(
"ArcGISTiledElevationTerrainProvider: Terrain data has more than 1 band. Using the first one.",
);
}
if (defined(metadata.minValues) && defined(metadata.maxValues)) {
terrainProviderBuilder.terrainDataStructure = {
elementMultiplier: 1.0,
lowestEncodedHeight: metadata.minValues[0],
highestEncodedHeight: metadata.maxValues[0],
};
} else {
terrainProviderBuilder.terrainDataStructure = {
elementMultiplier: 1.0,
};
}
}
async function requestMetadata(
terrainProviderBuilder,
metadataResource,
provider,
) {
try {
const metadata = await metadataResource.fetchJson();
parseMetadataSuccess(terrainProviderBuilder, metadata);
} catch (error) {
const message = `An error occurred while accessing ${metadataResource}.`;
TileProviderError.reportError(
undefined,
provider,
defined(provider) ? provider._errorEvent : undefined,
message,
);
throw error;
}
}
/**
* <div class="notice">
* To construct a CesiumTerrainProvider, call {@link ArcGISTiledElevationTerrainProvider.fromUrl}. Do not call the constructor directly.
* </div>
*
* A {@link TerrainProvider} that produces terrain geometry by tessellating height maps
* retrieved from Elevation Tiles of an an ArcGIS ImageService.
*
* @alias ArcGISTiledElevationTerrainProvider
* @constructor
*
* @param {CesiumTerrainProvider.ConstructorOptions} [options] A url or an object describing initialization options
*
* @example
* const terrainProvider = await Cesium.ArcGISTiledElevationTerrainProvider.fromUrl("https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer", {
* token: "KED1aF_I4UzXOHy3BnhwyBHU4l5oY6rO6walkmHoYqGp4XyIWUd5YZUC1ZrLAzvV40pR6gBXQayh0eFA8m6vPg.."
* });
* viewer.terrainProvider = terrainProvider;
*
* @see TerrainProvider
*/
function ArcGISTiledElevationTerrainProvider(options) {
options = options ?? Frozen.EMPTY_OBJECT;
this._resource = undefined;
this._credit = undefined;
this._tilingScheme = undefined;
this._levelZeroMaximumGeometricError = undefined;
this._maxLevel = undefined;
this._terrainDataStructure = undefined;
this._width = undefined;
this._height = undefined;
this._encoding = undefined;
this._lodCount = undefined;
this._hasAvailability = false;
this._tilesAvailable = undefined;
this._tilesAvailabilityLoaded = undefined;
this._availableCache = {};
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}
* @readonly
*/
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.
* @memberof ArcGISTiledElevationTerrainProvider.prototype
* @type {Credit}
* @readonly
*/
credit: {
get: function () {
return this._credit;
},
},
/**
* Gets the tiling scheme used by this provider.
* @memberof ArcGISTiledElevationTerrainProvider.prototype
* @type {GeographicTilingScheme}
* @readonly
*/
tilingScheme: {
get: function () {
return this._tilingScheme;
},
},
/**
* 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 ArcGISTiledElevationTerrainProvider.prototype
* @type {boolean}
* @readonly
*/
hasWaterMask: {
get: function () {
return false;
},
},
/**
* Gets a value indicating whether or not the requested tiles include vertex normals.
* @memberof ArcGISTiledElevationTerrainProvider.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 ArcGISTiledElevationTerrainProvider.prototype
* @type {TileAvailability|undefined}
* @readonly
*/
availability: {
get: function () {
return this._tilesAvailable;
},
},
});
/**
* Creates a {@link TerrainProvider} that produces terrain geometry by tessellating height maps
* retrieved from Elevation Tiles of an an ArcGIS ImageService.
*
* @param {Resource|String|Promise<Resource>|Promise<String>} url The URL of the ArcGIS ImageServer service.
* @param {ArcGISTiledElevationTerrainProvider.ConstructorOptions} [options] A url or an object describing initialization options.
* @returns {Promise<ArcGISTiledElevationTerrainProvider>}
*
* @example
* const terrainProvider = await Cesium.ArcGISTiledElevationTerrainProvider.fromUrl("https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer", {
* token: "KED1aF_I4UzXOHy3BnhwyBHU4l5oY6rO6walkmHoYqGp4XyIWUd5YZUC1ZrLAzvV40pR6gBXQayh0eFA8m6vPg.."
* });
* viewer.terrainProvider = terrainProvider;
*
* @exception {RuntimeError} metadata specifies invalid spatial reference
* @exception {RuntimeError} metadata does not specify tileInfo
*/
ArcGISTiledElevationTerrainProvider.fromUrl = async function (url, options) {
//>>includeStart('debug', pragmas.debug);
Check.defined("url", url);
//>>includeEnd('debug');
options = options ?? Frozen.EMPTY_OBJECT;
url = await Promise.resolve(url);
let resource = Resource.createIfNeeded(url);
resource.appendForwardSlash();
if (defined(options.token)) {
resource = resource.getDerivedResource({
queryParameters: {
token: options.token,
},
});
}
const metadataResource = resource.getDerivedResource({
queryParameters: {
f: "pjson",
},
});
const terrainProviderBuilder = new TerrainProviderBuilder(options);
await requestMetadata(terrainProviderBuilder, metadataResource);
const provider = new ArcGISTiledElevationTerrainProvider(options);
terrainProviderBuilder.build(provider);
provider._resource = resource;
return provider;
};
/**
* Requests the geometry for a given tile. 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,
) {
const tileResource = this._resource.getDerivedResource({
url: `tile/${level}/${y}/${x}`,
request: request,
});
const hasAvailability = this._hasAvailability;
let availabilityPromise = Promise.resolve(true);
let availabilityRequest;
if (
hasAvailability &&
!defined(isTileAvailable(this, level + 1, x * 2, y * 2))
) {
// We need to load child availability
const availabilityResult = requestAvailability(
this,
level + 1,
x * 2,
y * 2,
);
availabilityPromise = availabilityResult.promise;
availabilityRequest = availabilityResult.request;
}
const promise = tileResource.fetchArrayBuffer();
if (!defined(promise) || !defined(availabilityPromise)) {
return undefined;
}
const that = this;
const tilesAvailable = this._tilesAvailable;
return Promise.all([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,
});
})
.catch(async 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.
try {
await request.deferred?.promise;
} catch {
// Eat this error
}
request.state = RequestState.CANCELLED;
return Promise.reject(error);
}
return Promise.reject(error);
});
};
function isTileAvailable(that, level, x, y) {
if (!that._hasAvailability) {
return undefined;
}
const tilesAvailabilityLoaded = that._tilesAvailabilityLoaded;
const 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 (tilesAvailabilityLoaded.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) {
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.
*/
ArcGISTiledElevationTerrainProvider.prototype.getTileDataAvailable = function (
x,
y,
level,
) {
if (!this._hasAvailability) {
return undefined;
}
const 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} This provider does not support loading availability.
*/
ArcGISTiledElevationTerrainProvider.prototype.loadTileDataAvailability =
function (x, y, level) {
return undefined;
};
function findRange(origin, width, height, data) {
const endCol = width - 1;
const endRow = height - 1;
const value = data[origin.y * width + origin.x];
const endingIndices = [];
const range = {
startX: origin.x,
startY: origin.y,
endX: 0,
endY: 0,
};
const corner = new Cartesian2(origin.x + 1, origin.y + 1);
let doneX = false;
let doneY = false;
while (!(doneX && doneY)) {
// We want to use the original value when checking Y,
// so get it before it possibly gets incremented
let endX = corner.x;
// If we no longer move in the Y direction we need to check the corner tile in X pass
const endY = doneY ? corner.y + 1 : corner.y;
// Check X range
if (!doneX) {
for (let 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) {
const col = corner.y * width;
for (let 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) {
const ranges = [];
const 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;
}
let positions = [new Cartesian2(0, 0)];
while (positions.length > 0) {
const origin = positions.pop();
const result = findRange(origin, width, height, data);
if (result.value === 1) {
// Convert range into the array into global tile coordinates
const range = result.range;
range.startX += x;
range.endX += x;
range.startY += y;
range.endY += y;
ranges.push(range);
}
const 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
const xOffset = Math.floor(x / 128) * 128;
const yOffset = Math.floor(y / 128) * 128;
const dim = Math.min(1 << level, 128);
const url = `tilemap/${level}/${yOffset}/${xOffset}/${dim}/${dim}`;
const availableCache = that._availableCache;
if (defined(availableCache[url])) {
return availableCache[url];
}
const request = new Request({
throttle: false,
throttleByServer: true,
type: RequestType.TERRAIN,
});
const tilemapResource = that._resource.getDerivedResource({
url: url,
request: request,
});
let promise = tilemapResource.fetchJson();
if (!defined(promise)) {
return {};
}
promise = promise.then(function (result) {
const available = computeAvailability(
xOffset,
yOffset,
dim,
dim,
result.data,
);
// Mark whole area as having availability loaded
that._tilesAvailabilityLoaded.addAvailableTileRange(
level,
xOffset,
yOffset,
xOffset + dim,
yOffset + dim,
);
const tilesAvailable = that._tilesAvailable;
for (let i = 0; i < available.length; ++i) {
const 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.finally(function (result) {
delete availableCache[url];
return result;
});
return {
promise: promise,
request: request,
};
}
export default ArcGISTiledElevationTerrainProvider;