@cesium/engine
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
322 lines (277 loc) • 9.71 kB
JavaScript
import Uri from "urijs";
import Check from "./Check.js";
import Credit from "./Credit.js";
import Frozen from "./Frozen.js";
import defined from "./defined.js";
import Ion from "./Ion.js";
import Resource from "./Resource.js";
import RuntimeError from "./RuntimeError.js";
/**
* A function that will be invoked when the access token is refreshed.
* @callback IonResourceRefreshCallback
* @param {IonResource} ionResource The root IonResource being refreshed.
* @param {object} endpoint The result of the Cesium ion asset endpoint service. This may be modified in place by the callback.
* @private
*/
/**
* A {@link Resource} instance that encapsulates Cesium ion asset access.
* This object is normally not instantiated directly, use {@link IonResource.fromAssetId}.
*
* @alias IonResource
* @constructor
* @augments Resource
*
* @param {object} endpoint The result of the Cesium ion asset endpoint service.
* @param {Resource} endpointResource The original resource used to retrieve the endpoint.
*
* @see Ion
* @see IonImageryProvider
* @see createWorldTerrain
* @see https://cesium.com
*/
function IonResource(endpoint, endpointResource) {
//>>includeStart('debug', pragmas.debug);
Check.defined("endpoint", endpoint);
Check.defined("endpointResource", endpointResource);
//>>includeEnd('debug');
let options;
const externalType = endpoint.externalType;
const isExternal = defined(externalType);
if (!isExternal) {
options = {
url: endpoint.url,
retryAttempts: 1,
retryCallback: retryCallback,
};
} else if (
externalType === "3DTILES" ||
externalType === "STK_TERRAIN_SERVER"
) {
// 3D Tiles and STK Terrain Server external assets can still be represented as an IonResource
options = { url: endpoint.options.url };
} else {
//External imagery assets have additional configuration that can't be represented as a Resource
throw new RuntimeError(
"Ion.createResource does not support external imagery assets; use IonImageryProvider instead.",
);
}
Resource.call(this, options);
// The asset endpoint data returned from ion.
this._ionEndpoint = endpoint;
this._ionEndpointDomain = isExternal
? undefined
: new Uri(endpoint.url).authority();
// The endpoint resource to fetch when a new token is needed
this._ionEndpointResource = endpointResource;
// The primary IonResource from which an instance is derived
this._ionRoot = undefined;
// Shared promise for endpooint requests amd credits (only ever set on the root request)
this._pendingPromise = undefined;
this._credits = undefined;
this._isExternal = isExternal;
/**
* A function that, if defined, will be invoked when the access token is refreshed.
* @private
* @type {IonResourceRefreshCallback|undefined}
*/
this.refreshCallback = undefined;
}
if (defined(Object.create)) {
IonResource.prototype = Object.create(Resource.prototype);
IonResource.prototype.constructor = IonResource;
}
/**
* Asynchronously creates an instance.
*
* @param {number} assetId The Cesium ion asset id.
* @param {object} [options] An object with the following properties:
* @param {string} [options.accessToken=Ion.defaultAccessToken] The access token to use.
* @param {string|Resource} [options.server=Ion.defaultServer] The resource to the Cesium ion API server.
* @returns {Promise<IonResource>} A Promise to an instance representing the Cesium ion Asset.
*
* @example
* // Load a Cesium3DTileset with asset ID of 124624234
* try {
* const resource = await Cesium.IonResource.fromAssetId(124624234);
* const tileset = await Cesium.Cesium3DTileset.fromUrl(resource);
* scene.primitives.add(tileset);
* } catch (error) {
* console.error(`Error creating tileset: ${error}`);
* }
*
* @example
* //Load a CZML file with asset ID of 10890
* Cesium.IonResource.fromAssetId(10890)
* .then(function (resource) {
* viewer.dataSources.add(Cesium.CzmlDataSource.load(resource));
* });
*/
IonResource.fromAssetId = function (assetId, options) {
const endpointResource = IonResource._createEndpointResource(
assetId,
options,
);
return endpointResource.fetchJson().then(function (endpoint) {
return new IonResource(endpoint, endpointResource);
});
};
Object.defineProperties(IonResource.prototype, {
/**
* Gets the credits required for attribution of the asset.
*
* @memberof IonResource.prototype
* @type {Credit[]}
* @readonly
*/
credits: {
get: function () {
// Only we're not the root, return its credits;
if (defined(this._ionRoot)) {
return this._ionRoot.credits;
}
// We are the root
if (defined(this._credits)) {
return this._credits;
}
this._credits = IonResource.getCreditsFromEndpoint(
this._ionEndpoint,
this._ionEndpointResource,
);
return this._credits;
},
},
});
/** @private */
IonResource.getCreditsFromEndpoint = function (endpoint, endpointResource) {
const credits = endpoint.attributions.map(Credit.getIonCredit);
const defaultTokenCredit = Ion.getDefaultTokenCredit(
endpointResource.queryParameters.access_token,
);
if (defined(defaultTokenCredit)) {
credits.push(Credit.clone(defaultTokenCredit));
}
return credits;
};
/** @inheritdoc */
IonResource.prototype.clone = function (result) {
// We always want to use the root's information because it's the most up-to-date
const ionRoot = this._ionRoot ?? this;
if (!defined(result)) {
result = new IonResource(
ionRoot._ionEndpoint,
ionRoot._ionEndpointResource,
);
}
result = Resource.prototype.clone.call(this, result);
result._ionRoot = ionRoot;
result._isExternal = this._isExternal;
return result;
};
IonResource.prototype.fetchImage = function (options) {
if (!this._isExternal) {
const userOptions = options;
options = {
preferBlob: true,
};
if (defined(userOptions)) {
options.flipY = userOptions.flipY;
options.preferImageBitmap = userOptions.preferImageBitmap;
}
}
return Resource.prototype.fetchImage.call(this, options);
};
IonResource.prototype._makeRequest = function (options) {
// Don't send ion access token to non-ion servers.
if (
this._isExternal ||
new Uri(this.url).authority() !== this._ionEndpointDomain
) {
return Resource.prototype._makeRequest.call(this, options);
}
options.headers = addClientHeaders(options.headers);
options.headers.Authorization = `Bearer ${this._ionEndpoint.accessToken}`;
return Resource.prototype._makeRequest.call(this, options);
};
/**
* @private
**/
IonResource._createEndpointResource = function (assetId, options) {
//>>includeStart('debug', pragmas.debug);
Check.defined("assetId", assetId);
//>>includeEnd('debug');
options = options ?? Frozen.EMPTY_OBJECT;
let server = options.server ?? Ion.defaultServer;
const accessToken = options.accessToken ?? Ion.defaultAccessToken;
server = Resource.createIfNeeded(server);
const resourceOptions = {
url: `v1/assets/${assetId}/endpoint`,
};
if (defined(accessToken)) {
resourceOptions.queryParameters = { access_token: accessToken };
}
if (defined(options.queryParameters)) {
resourceOptions.queryParameters = {
...resourceOptions.queryParameters,
...options.queryParameters,
};
}
resourceOptions.headers = addClientHeaders(resourceOptions.headers);
return server.getDerivedResource(resourceOptions);
};
/**
* Adds CesiumJS client headers to the provided headers object.
* @private
* @param {object} [headers={}] The headers to modify.
* @returns {object} The modified headers.
*/
function addClientHeaders(headers = {}) {
headers["X-Cesium-Client"] = "CesiumJS";
/* global CESIUM_VERSION */
if (typeof CESIUM_VERSION !== "undefined") {
headers["X-Cesium-Client-Version"] = CESIUM_VERSION;
}
return headers;
}
function retryCallback(that, error) {
const ionRoot = that._ionRoot ?? that;
const endpointResource = ionRoot._ionEndpointResource;
// Image is not available in worker threads, so this avoids
// a ReferenceError
const imageDefined = typeof Image !== "undefined";
// We only want to retry in the case of invalid credentials (401) or image
// requests(since Image failures can not provide a status code)
if (
!defined(error) ||
(error.statusCode !== 401 &&
!(imageDefined && error.target instanceof Image))
) {
return Promise.resolve(false);
}
// We use a shared pending promise for all derived assets, since they share
// a common access_token. If we're already requesting a new token for this
// asset, we wait on the same promise.
if (!defined(ionRoot._pendingPromise)) {
ionRoot._pendingPromise = endpointResource
.fetchJson()
.then(function (newEndpoint) {
const refreshCallback = that.refreshCallback ?? ionRoot.refreshCallback;
if (defined(refreshCallback)) {
refreshCallback(ionRoot, newEndpoint);
}
// Set the token for root resource so new derived resources automatically pick it up
ionRoot._ionEndpoint = newEndpoint;
return ionRoot._ionEndpoint;
})
.finally(function (newEndpoint) {
// Pass or fail, we're done with this promise, the next failure should use a new one.
ionRoot._pendingPromise = undefined;
return newEndpoint;
});
}
return ionRoot._pendingPromise.then(function (newEndpoint) {
// Set the new token and endpoint for this resource
that._ionEndpoint = newEndpoint;
return true;
});
}
export default IonResource;