diff --git a/packages/engine/Source/Core/IonResource.js b/packages/engine/Source/Core/IonResource.js
index cf10e967c75..b134df15955 100644
--- a/packages/engine/Source/Core/IonResource.js
+++ b/packages/engine/Source/Core/IonResource.js
@@ -7,6 +7,14 @@ 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}.
@@ -16,7 +24,7 @@ import RuntimeError from "./RuntimeError.js";
* @augments Resource
*
* @param {object} endpoint The result of the Cesium ion asset endpoint service.
- * @param {Resource} endpointResource The resource used to retrieve the endpoint.
+ * @param {Resource} endpointResource The original resource used to retrieve the endpoint.
*
* @see Ion
* @see IonImageryProvider
@@ -39,12 +47,6 @@ function IonResource(endpoint, endpointResource) {
retryAttempts: 1,
retryCallback: retryCallback,
};
- } else if (["GOOGLE_2D_MAPS", "AZURE_MAPS"].includes(externalType)) {
- options = {
- url: endpoint.options.url,
- retryAttempts: 1,
- retryCallback: retryCallback,
- };
} else if (
externalType === "3DTILES" ||
externalType === "STK_TERRAIN_SERVER"
@@ -76,6 +78,13 @@ function IonResource(endpoint, endpointResource) {
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)) {
@@ -205,7 +214,7 @@ IonResource.prototype._makeRequest = function (options) {
return Resource.prototype._makeRequest.call(this, options);
}
- addClientHeaders(options);
+ options.headers = addClientHeaders(options.headers);
options.headers.Authorization = `Bearer ${this._ionEndpoint.accessToken}`;
return Resource.prototype._makeRequest.call(this, options);
@@ -239,20 +248,26 @@ IonResource._createEndpointResource = function (assetId, options) {
};
}
- addClientHeaders(resourceOptions);
+ resourceOptions.headers = addClientHeaders(resourceOptions.headers);
return server.getDerivedResource(resourceOptions);
};
-function addClientHeaders(options) {
- if (!defined(options.headers)) {
- options.headers = {};
- }
- options.headers["X-Cesium-Client"] = "CesiumJS";
+/**
+ * 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") {
- options.headers["X-Cesium-Client-Version"] = CESIUM_VERSION;
+ headers["X-Cesium-Client-Version"] = CESIUM_VERSION;
}
+
+ return headers;
}
function retryCallback(that, error) {
@@ -280,20 +295,13 @@ function retryCallback(that, error) {
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;
- // Reset the session token for Google 2D imagery
- if (newEndpoint.externalType === "GOOGLE_2D_MAPS") {
- ionRoot.setQueryParameters({
- session: newEndpoint.options.session,
- key: newEndpoint.options.key,
- });
- }
- if (newEndpoint.externalType === "AZURE_MAPS") {
- ionRoot.setQueryParameters({
- "subscription-key": newEndpoint.options["subscription-key"],
- });
- }
return ionRoot._ionEndpoint;
})
.finally(function (newEndpoint) {
diff --git a/packages/engine/Source/Core/Resource.js b/packages/engine/Source/Core/Resource.js
index 7e4416e89ac..74063f180c2 100644
--- a/packages/engine/Source/Core/Resource.js
+++ b/packages/engine/Source/Core/Resource.js
@@ -917,7 +917,7 @@ Resource.prototype.fetchImage = function (options) {
this.isBlobUri ||
(!this.hasHeaders && !preferBlob)
) {
- return fetchImage({
+ return this._fetchImage({
resource: this,
flipY: flipY,
skipColorSpaceConversion: skipColorSpaceConversion,
@@ -957,8 +957,7 @@ Resource.prototype.fetchImage = function (options) {
url: blobUrl,
});
- return fetchImage({
- resource: generatedBlobResource,
+ return generatedBlobResource._fetchImage({
flipY: flipY,
skipColorSpaceConversion: skipColorSpaceConversion,
preferImageBitmap: false,
@@ -997,16 +996,15 @@ Resource.prototype.fetchImage = function (options) {
/**
* Fetches an image and returns a promise to it.
- *
* @param {object} [options] An object with the following properties.
- * @param {Resource} [options.resource] Resource object that points to an image to fetch.
* @param {boolean} [options.preferImageBitmap] If true, image will be decoded during fetch and an ImageBitmap is returned.
* @param {boolean} [options.flipY] If true, image will be vertically flipped during decode. Only applies if the browser supports createImageBitmap.
* @param {boolean} [options.skipColorSpaceConversion=false] If true, any custom gamma or color profiles in the image will be ignored. Only applies if the browser supports createImageBitmap.
+ * @returns {Promise|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if the request has been throttled and cannot be made at this time.
* @private
*/
-function fetchImage(options) {
- const resource = options.resource;
+Resource.prototype._fetchImage = function (options) {
+ const resource = this;
const flipY = options.flipY;
const skipColorSpaceConversion = options.skipColorSpaceConversion;
const preferImageBitmap = options.preferImageBitmap;
@@ -1050,8 +1048,7 @@ function fetchImage(options) {
request.state = RequestState.UNISSUED;
request.deferred = undefined;
- return fetchImage({
- resource: resource,
+ return resource._fetchImage({
flipY: flipY,
skipColorSpaceConversion: skipColorSpaceConversion,
preferImageBitmap: preferImageBitmap,
@@ -1060,7 +1057,7 @@ function fetchImage(options) {
return Promise.reject(e);
});
});
-}
+};
/**
* Creates a Resource and calls fetchImage() on it.
@@ -1962,6 +1959,7 @@ Resource._Implementations.createImage = function (
flipY,
skipColorSpaceConversion,
preferImageBitmap,
+ headers,
) {
const url = request.url;
// Passing an Image to createImageBitmap will force it to run on the main thread
@@ -1985,7 +1983,7 @@ Resource._Implementations.createImage = function (
responseType,
method,
undefined,
- undefined,
+ headers,
xhrDeferred,
undefined,
undefined,
diff --git a/packages/engine/Source/Scene/Azure2DImageryProvider.js b/packages/engine/Source/Scene/Azure2DImageryProvider.js
index 25d43e1cbbe..24a9d3f453a 100644
--- a/packages/engine/Source/Scene/Azure2DImageryProvider.js
+++ b/packages/engine/Source/Scene/Azure2DImageryProvider.js
@@ -14,7 +14,7 @@ const trailingSlashRegex = /\/$/;
*
* @property {object} options Object with the following properties:
* @property {string} [options.url="https://atlas.microsoft.com/"] The Azure server url.
- * @property {string} [options.tilesetId="microsoft.imagery"] The Azure tileset ID. Valid options are {@link microsoft.imagery}, {@link microsoft.base.road}, and {@link microsoft.base.labels.road}
+ * @property {string} options.tilesetId="microsoft.imagery" The Azure tileset ID. Valid options are {@link microsoft.imagery}, {@link microsoft.base.road}, and {@link microsoft.base.labels.road}
* @property {string} options.subscriptionKey The public subscription key for the imagery.
* @property {Ellipsoid} [options.ellipsoid=Ellipsoid.default] The ellipsoid. If not specified, the default ellipsoid is used.
* @property {number} [options.minimumLevel=0] The minimum level-of-detail supported by the imagery provider. Take care when specifying
@@ -41,13 +41,13 @@ const trailingSlashRegex = /\/$/;
*/
function Azure2DImageryProvider(options) {
options = options ?? {};
- options.maximumLevel = options.maximumLevel ?? 22;
- options.minimumLevel = options.minimumLevel ?? 0;
+ const maximumLevel = options.maximumLevel ?? 22;
+ const minimumLevel = options.minimumLevel ?? 0;
+ const tilesetId = options.tilesetId ?? "microsoft.imagery";
const subscriptionKey =
options.subscriptionKey ?? options["subscription-key"];
//>>includeStart('debug', pragmas.debug);
- Check.defined("options.tilesetId", options.tilesetId);
Check.defined("options.subscriptionKey", subscriptionKey);
//>>includeEnd('debug');
@@ -66,7 +66,7 @@ function Azure2DImageryProvider(options) {
resource.setQueryParameters({
"api-version": "2024-04-01",
- tilesetId: options.tilesetId,
+ tilesetId: tilesetId,
zoom: `{z}`,
x: `{x}`,
y: `{y}`,
@@ -83,6 +83,8 @@ function Azure2DImageryProvider(options) {
const provider = new UrlTemplateImageryProvider({
...options,
+ maximumLevel,
+ minimumLevel,
url: resource,
credit: credit,
});
diff --git a/packages/engine/Source/Scene/Google2DImageryProvider.js b/packages/engine/Source/Scene/Google2DImageryProvider.js
index cba241de418..27a812311c3 100644
--- a/packages/engine/Source/Scene/Google2DImageryProvider.js
+++ b/packages/engine/Source/Scene/Google2DImageryProvider.js
@@ -5,8 +5,9 @@ import DeveloperError from "../Core/DeveloperError.js";
import Resource from "../Core/Resource.js";
import IonResource from "../Core/IonResource.js";
import Check from "../Core/Check.js";
-import UrlTemplateImageryProvider from "./UrlTemplateImageryProvider.js";
import GoogleMaps from "../Core/GoogleMaps.js";
+import { GOOGLE_2D_MAPS as createFromIonEndpoint } from "./IonImageryProviderFactory.js";
+import UrlTemplateImageryProvider from "./UrlTemplateImageryProvider.js";
const trailingSlashRegex = /\/$/;
@@ -124,8 +125,6 @@ function Google2DImageryProvider(options) {
// This will be defined for ion resources
this._tileCredits = resource.credits;
this._attributionsByLevel = undefined;
- // Asynchronously request and populate _attributionsByLevel
- this.getViewportCredits();
}
Object.defineProperties(Google2DImageryProvider.prototype, {
@@ -353,10 +352,6 @@ Google2DImageryProvider.fromIonAssetId = async function (options) {
},
);
- const endpoint = await endpointResource.fetchJson();
- const endpointOptions = { ...endpoint.options };
- delete endpointOptions.url;
-
const providerOptions = {
language: options.language,
region: options.region,
@@ -367,11 +362,15 @@ Google2DImageryProvider.fromIonAssetId = async function (options) {
credit: options.credit,
};
- return new Google2DImageryProvider({
- ...endpointOptions,
+ const endpoint = await endpointResource.fetchJson();
+ const url = endpoint.options.url;
+ delete endpoint.options.url;
+ endpoint.options = {
+ ...endpoint.options,
...providerOptions,
- url: new IonResource(endpoint, endpointResource),
- });
+ };
+
+ return createFromIonEndpoint(url, endpoint, endpointResource);
};
/**
@@ -485,7 +484,21 @@ Google2DImageryProvider.prototype.requestImage = function (
level,
request,
) {
- return this._imageryProvider.requestImage(x, y, level, request);
+ const promise = this._imageryProvider.requestImage(x, y, level, request);
+
+ // If the requestImage call returns undefined, it couldn't be scheduled this frame. Make sure to return undefined so this can be handled upstream.
+ if (!defined(promise)) {
+ return undefined;
+ }
+
+ // Asynchronously request and populate _attributionsByLevel if it hasn't been already. We do this here so that the promise can be properly awaited.
+ if (promise && !defined(this._attributionsByLevel)) {
+ return Promise.all([promise, this.getViewportCredits()]).then(
+ (results) => results[0],
+ );
+ }
+
+ return promise;
};
/**
diff --git a/packages/engine/Source/Scene/IonImageryProvider.js b/packages/engine/Source/Scene/IonImageryProvider.js
index ba30b7962a8..6c4e95f2937 100644
--- a/packages/engine/Source/Scene/IonImageryProvider.js
+++ b/packages/engine/Source/Scene/IonImageryProvider.js
@@ -1,72 +1,11 @@
import Check from "../Core/Check.js";
+import clone from "../Core/clone.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import Event from "../Core/Event.js";
import IonResource from "../Core/IonResource.js";
import RuntimeError from "../Core/RuntimeError.js";
-import ArcGisMapServerImageryProvider from "./ArcGisMapServerImageryProvider.js";
-import BingMapsImageryProvider from "./BingMapsImageryProvider.js";
-import TileMapServiceImageryProvider from "./TileMapServiceImageryProvider.js";
-import GoogleEarthEnterpriseMapsProvider from "./GoogleEarthEnterpriseMapsProvider.js";
-import MapboxImageryProvider from "./MapboxImageryProvider.js";
-import SingleTileImageryProvider from "./SingleTileImageryProvider.js";
-import UrlTemplateImageryProvider from "./UrlTemplateImageryProvider.js";
-import WebMapServiceImageryProvider from "./WebMapServiceImageryProvider.js";
-import WebMapTileServiceImageryProvider from "./WebMapTileServiceImageryProvider.js";
-import Google2DImageryProvider from "./Google2DImageryProvider.js";
-import Azure2DImageryProvider from "./Azure2DImageryProvider.js";
-
-// These values are the list of supported external imagery
-// assets in the Cesium ion beta. They are subject to change.
-const ImageryProviderAsyncMapping = {
- ARCGIS_MAPSERVER: ArcGisMapServerImageryProvider.fromUrl,
- BING: async (url, options) => {
- return BingMapsImageryProvider.fromUrl(url, options);
- },
- GOOGLE_EARTH: async (url, options) => {
- const channel = options.channel;
- delete options.channel;
- return GoogleEarthEnterpriseMapsProvider.fromUrl(url, channel, options);
- },
- MAPBOX: (url, options) => {
- return new MapboxImageryProvider({
- url: url,
- ...options,
- });
- },
- SINGLE_TILE: SingleTileImageryProvider.fromUrl,
- TMS: TileMapServiceImageryProvider.fromUrl,
- URL_TEMPLATE: (url, options) => {
- return new UrlTemplateImageryProvider({
- url: url,
- ...options,
- });
- },
- WMS: (url, options) => {
- return new WebMapServiceImageryProvider({
- url: url,
- ...options,
- });
- },
- WMTS: (url, options) => {
- return new WebMapTileServiceImageryProvider({
- url: url,
- ...options,
- });
- },
- GOOGLE_2D_MAPS: (ionResource, options) => {
- return new Google2DImageryProvider({
- ...options,
- url: ionResource,
- });
- },
- AZURE_MAPS: (ionResource, options) => {
- return new Azure2DImageryProvider({
- ...options,
- url: ionResource,
- });
- },
-};
+import IonImageryProviderFactory from "./IonImageryProviderFactory.js";
/**
* @typedef {object} IonImageryProvider.ConstructorOptions
@@ -297,41 +236,33 @@ IonImageryProvider.fromAssetId = async function (assetId, options) {
IonImageryProvider._endpointCache[cacheKey] = promise;
}
- const endpoint = await promise;
+ let endpoint = await promise;
if (endpoint.type !== "IMAGERY") {
throw new RuntimeError(
`Cesium ion asset ${assetId} is not an imagery asset.`,
);
}
- let imageryProvider;
const externalType = endpoint.externalType;
- if (!defined(externalType)) {
- imageryProvider = await TileMapServiceImageryProvider.fromUrl(
- new IonResource(endpoint, endpointResource),
- );
- } else {
- const factory = ImageryProviderAsyncMapping[externalType];
+ let factory = IonImageryProviderFactory.defaultFactoryCallback;
+
+ // Make a copy before editing since this object reference is cached;
+ endpoint = clone(endpoint, true);
+ endpoint.options = endpoint.options ?? {};
+ const url = endpoint.options?.url;
+ delete options.url;
+
+ if (defined(externalType)) {
+ factory = IonImageryProviderFactory[externalType];
if (!defined(factory)) {
throw new RuntimeError(
`Unrecognized Cesium ion imagery type: ${externalType}`,
);
}
- // Make a copy before editing since this object reference is cached;
- const options = { ...endpoint.options };
- const url = options.url;
- delete options.url;
- if (["GOOGLE_2D_MAPS", "AZURE_MAPS"].includes(endpoint.externalType)) {
- imageryProvider = await factory(
- new IonResource(endpoint, endpointResource),
- options,
- );
- } else {
- imageryProvider = await factory(url, options);
- }
}
+ const imageryProvider = await factory(url, endpoint, endpointResource);
const provider = new IonImageryProvider(options);
imageryProvider.errorEvent.addEventListener(function (tileProviderError) {
diff --git a/packages/engine/Source/Scene/IonImageryProviderFactory.js b/packages/engine/Source/Scene/IonImageryProviderFactory.js
new file mode 100644
index 00000000000..88f91a5df7b
--- /dev/null
+++ b/packages/engine/Source/Scene/IonImageryProviderFactory.js
@@ -0,0 +1,194 @@
+import IonResource from "../Core/IonResource.js";
+import ArcGisMapServerImageryProvider from "./ArcGisMapServerImageryProvider.js";
+import BingMapsImageryProvider from "./BingMapsImageryProvider.js";
+import TileMapServiceImageryProvider from "./TileMapServiceImageryProvider.js";
+import GoogleEarthEnterpriseMapsProvider from "./GoogleEarthEnterpriseMapsProvider.js";
+import MapboxImageryProvider from "./MapboxImageryProvider.js";
+import SingleTileImageryProvider from "./SingleTileImageryProvider.js";
+import UrlTemplateImageryProvider from "./UrlTemplateImageryProvider.js";
+import WebMapServiceImageryProvider from "./WebMapServiceImageryProvider.js";
+import WebMapTileServiceImageryProvider from "./WebMapTileServiceImageryProvider.js";
+import Google2DImageryProvider from "./Google2DImageryProvider.js";
+import Azure2DImageryProvider from "./Azure2DImageryProvider.js";
+
+/**
+ * An asynchronous callback function that creates an ImageryProvider from
+ * a Cesium ion asset endpoint.
+ * @template {ImageryProvider} T
+ * @callback IonImageryProviderFactoryCallback
+ * @param {string} url The URL of the imagery service.
+ * @param {object} endpoint The result of the Cesium ion asset endpoint service.
+ * @param {Resource} endpointResource The original resource used to retrieve the endpoint.
+ * @returns {Promise} A promise that resolves to an instance of an ImageryProvider.
+ * @private
+ */
+
+/**
+ * @private
+ * @type {IonImageryProviderFactoryCallback}
+ */
+export const defaultFactoryCallback = async (url, endpoint, endpointResource) =>
+ TileMapServiceImageryProvider.fromUrl(
+ new IonResource(endpoint, endpointResource),
+ );
+
+/**
+ * @private
+ * @type {IonImageryProviderFactoryCallback}
+ */
+export const ARCGIS_MAPSERVER = async (url, { options }) =>
+ ArcGisMapServerImageryProvider.fromUrl(url, options);
+
+/**
+ * @private
+ * @type {IonImageryProviderFactoryCallback}
+ */
+export const BING = async (url, { options }) => {
+ return BingMapsImageryProvider.fromUrl(url, options);
+};
+
+/**
+ * @private
+ * @type {IonImageryProviderFactoryCallback}
+ */
+export const GOOGLE_EARTH = async (url, { options }) => {
+ const channel = options.channel;
+ delete options.channel;
+ return GoogleEarthEnterpriseMapsProvider.fromUrl(url, channel, options);
+};
+
+/**
+ * @private
+ * @type {IonImageryProviderFactoryCallback}
+ */
+export const MAPBOX = async (url, { options }) => {
+ return new MapboxImageryProvider({
+ url: url,
+ ...options,
+ });
+};
+
+/**
+ * @private
+ * @type {IonImageryProviderFactoryCallback}
+ */
+export const SINGLE_TILE = async (url, { options }) =>
+ SingleTileImageryProvider.fromUrl(url, options);
+
+/**
+ * @private
+ * @type {IonImageryProviderFactoryCallback}
+ */
+export const TMS = async (url, { options }) =>
+ TileMapServiceImageryProvider.fromUrl(url, options);
+
+/**
+ * @private
+ * @type {IonImageryProviderFactoryCallback}
+ */
+export const URL_TEMPLATE = async (url, { options }) => {
+ return new UrlTemplateImageryProvider({
+ url: url,
+ ...options,
+ });
+};
+
+/**
+ * @private
+ * @type {IonImageryProviderFactoryCallback}
+ */
+export const WMS = async (url, { options }) => {
+ return new WebMapServiceImageryProvider({
+ url: url,
+ ...options,
+ });
+};
+
+/**
+ * @private
+ * @type {IonImageryProviderFactoryCallback}
+ */
+export const WMTS = async (url, { options }) => {
+ return new WebMapTileServiceImageryProvider({
+ url: url,
+ ...options,
+ });
+};
+
+/**
+ * @private
+ * @type {IonImageryProviderFactoryCallback}
+ */
+export const GOOGLE_2D_MAPS = async (url, endpoint, endpointResource) => {
+ delete endpoint.externalType;
+ endpoint.url = url;
+
+ const ionResource = new IonResource(endpoint, endpointResource);
+
+ const callback = (ionRoot, endpoint) => {
+ delete endpoint.externalType;
+ endpoint.url = url;
+
+ const { options } = endpoint;
+ ionRoot.setQueryParameters({
+ session: options.session,
+ key: options.key,
+ });
+ };
+
+ ionResource.refreshCallback = callback;
+ return new Google2DImageryProvider({
+ ...endpoint.options,
+ url: ionResource,
+ });
+};
+
+/**
+ * @private
+ * @type {IonImageryProviderFactoryCallback}
+ */
+export const AZURE_MAPS = async (url, endpoint, endpointResource) => {
+ delete endpoint.externalType;
+ endpoint.url = url;
+
+ const ionResource = new IonResource(endpoint, endpointResource);
+
+ const callback = (ionRoot, endpoint) => {
+ delete endpoint.externalType;
+ endpoint.url = url;
+
+ const { options } = endpoint;
+ ionRoot.setQueryParameters({
+ "subscription-key": options["subscription-key"],
+ });
+ };
+
+ ionResource.refreshCallback = callback;
+ return new Azure2DImageryProvider({
+ ...endpoint.options,
+ url: ionResource,
+ });
+};
+
+/**
+ * Mapping of supported external imagery asset types returned from Cesium ion to their
+ * corresponding ImageryProvider constructors.
+ * @private
+ * @type {object}
+ */
+const IonImageryProviderFactory = {
+ ARCGIS_MAPSERVER,
+ BING,
+ GOOGLE_EARTH,
+ MAPBOX,
+ SINGLE_TILE,
+ TMS,
+ URL_TEMPLATE,
+ WMS,
+ WMTS,
+ GOOGLE_2D_MAPS,
+ AZURE_MAPS,
+ defaultFactoryCallback,
+};
+
+export default Object.freeze(IonImageryProviderFactory);
diff --git a/packages/engine/Specs/Core/IonResourceSpec.js b/packages/engine/Specs/Core/IonResourceSpec.js
index fe073840afc..3a2cbfcd10d 100644
--- a/packages/engine/Specs/Core/IonResourceSpec.js
+++ b/packages/engine/Specs/Core/IonResourceSpec.js
@@ -1,5 +1,4 @@
import {
- defer,
Ion,
IonResource,
RequestErrorEvent,
@@ -87,50 +86,55 @@ describe("Core/IonResource", function () {
});
function testNonImageryExternalResource(externalEndpoint) {
- const resourceEndpoint = IonResource._createEndpointResource(123890213);
- spyOn(IonResource, "_createEndpointResource").and.returnValue(
- resourceEndpoint,
- );
- spyOn(resourceEndpoint, "fetchJson").and.returnValue(
- Promise.resolve(externalEndpoint),
- );
-
- return IonResource.fromAssetId(123890213).then(function (resource) {
+ it(`fromAssetId returns basic Resource for external type "${externalEndpoint.externalType}"`, async function () {
+ const resourceEndpoint = IonResource._createEndpointResource(123890213);
+ spyOn(IonResource, "_createEndpointResource").and.returnValue(
+ resourceEndpoint,
+ );
+ spyOn(resourceEndpoint, "fetchJson").and.returnValue(
+ Promise.resolve(externalEndpoint),
+ );
+
+ const resource = await IonResource.fromAssetId(123890213);
expect(resource.url).toEqual(externalEndpoint.options.url);
expect(resource.headers.Authorization).toBeUndefined();
expect(resource.retryCallback).toBeUndefined();
});
}
- it("fromAssetId returns basic Resource for external 3D tilesets", function () {
- return testNonImageryExternalResource({
- type: "3DTILES",
- externalType: "3DTILES",
- options: { url: "http://test.invalid/tileset.json" },
- attributions: [],
- });
+ testNonImageryExternalResource({
+ type: "3DTILES",
+ externalType: "3DTILES",
+ options: { url: "http://test.invalid/tileset.json" },
+ attributions: [],
});
- it("fromAssetId returns basic Resource for external 3D tilesets", function () {
- return testNonImageryExternalResource({
- type: "TERRAIN",
- externalType: "STK_TERRAIN_SERVER",
- options: { url: "http://test.invalid/world" },
- attributions: [],
- });
+ testNonImageryExternalResource({
+ type: "TERRAIN",
+ externalType: "STK_TERRAIN_SERVER",
+ options: { url: "http://test.invalid/world" },
+ attributions: [],
});
- it("fromAssetId rejects for external imagery", function () {
- return testNonImageryExternalResource({
+ it("fromAssetId rejects for external imagery", async function () {
+ const externalEndpoint = {
type: "IMAGERY",
externalType: "URL_TEMPLATE",
url: "http://test.invalid/world",
attributions: [],
- })
- .then(fail)
- .catch(function (e) {
- expect(e).toBeInstanceOf(RuntimeError);
- });
+ };
+
+ const resourceEndpoint = IonResource._createEndpointResource(123890213);
+ spyOn(IonResource, "_createEndpointResource").and.returnValue(
+ resourceEndpoint,
+ );
+ spyOn(resourceEndpoint, "fetchJson").and.returnValue(
+ Promise.resolve(externalEndpoint),
+ );
+
+ await expectAsync(IonResource.fromAssetId(123890213)).toBeRejectedWithError(
+ RuntimeError,
+ );
});
it("createEndpointResource creates expected values with default parameters", function () {
@@ -278,7 +282,7 @@ describe("Core/IonResource", function () {
describe("retryCallback", function () {
let endpointResource;
- let resource;
+ let originalResource;
let retryCallback;
beforeEach(function () {
@@ -286,76 +290,111 @@ describe("Core/IonResource", function () {
url: "https://api.test.invalid",
access_token: "not_the_token",
});
- resource = new IonResource(endpoint, endpointResource);
- retryCallback = resource.retryCallback;
+ originalResource = new IonResource(endpoint, endpointResource);
+ retryCallback = originalResource.retryCallback;
});
it("returns false when error is undefined", function () {
- return retryCallback(resource, undefined).then(function (result) {
+ return retryCallback(originalResource, undefined).then(function (result) {
expect(result).toBe(false);
});
});
it("returns false when error is non-401", function () {
const error = new RequestErrorEvent(404);
- return retryCallback(resource, error).then(function (result) {
+ return retryCallback(originalResource, error).then(function (result) {
expect(result).toBe(false);
});
});
it("returns false when error is event with non-Image target", function () {
const event = { target: {} };
- return retryCallback(resource, event).then(function (result) {
+ return retryCallback(originalResource, event).then(function (result) {
expect(result).toBe(false);
});
});
- function testCallback(resource, event) {
- const deferred = defer();
- spyOn(endpointResource, "fetchJson").and.returnValue(deferred.promise);
+ function testCallback(eventName, resourceCallback, eventCallback) {
+ it(`works with ${eventName}`, async function () {
+ const resource = resourceCallback();
+ const newEndpoint = {
+ type: "3DTILES",
+ url: `https://assets.cesium.com/${assetId}`,
+ accessToken: "not_not_really_a_refresh_token",
+ };
+
+ spyOn(endpointResource, "fetchJson").and.returnValue(
+ Promise.resolve(newEndpoint),
+ );
+
+ const promise = retryCallback(resource, eventCallback());
- const newEndpoint = {
- type: "3DTILES",
- url: `https://assets.cesium.com/${assetId}`,
- accessToken: "not_not_really_a_refresh_token",
- };
+ // A concurrent second retry should re-use the same pending promise
+ const promise2 = retryCallback(resource, eventCallback());
+ expect(promise._pendingPromise).toBe(promise2._pendingPromise);
- const promise = retryCallback(resource, event);
- const resultPromise = promise.then(function (result) {
+ const result = await promise;
expect(result).toBe(true);
expect(resource._ionEndpoint).toBe(newEndpoint);
+
+ // Updates root endpoint
+ expect(originalResource._ionEndpoint).toBe(resource._ionEndpoint);
+ expect(originalResource.headers.Authorization).toEqual(
+ resource.headers.Authorization,
+ );
+
+ expect(endpointResource.fetchJson).toHaveBeenCalled();
+ await expectAsync(promise2).not.toBePending();
});
- expect(endpointResource.fetchJson).toHaveBeenCalledWith();
+ it(`works with refresh callback and ${eventName}`, async function () {
+ const resource = resourceCallback();
+ const newEndpoint = {
+ type: "3DTILES",
+ url: `https://assets.cesium.com/${assetId}`,
+ accessToken: "not_not_really_a_refresh_token",
+ };
+
+ const refreshCallbackSpy = jasmine.createSpy("refreshCallback");
+ resource.refreshCallback = (ionRoot, endpoint) => {
+ refreshCallbackSpy();
+ expect(ionRoot).toBe(originalResource);
+ expect(endpoint).toEqual(newEndpoint);
+ };
+
+ spyOn(endpointResource, "fetchJson").and.returnValue(
+ Promise.resolve(newEndpoint),
+ );
- //A second retry should re-use the same pending promise
- const promise2 = retryCallback(resource, event);
- expect(promise._pendingPromise).toBe(promise2._pendingPromise);
+ const promise = retryCallback(resource, eventCallback());
- deferred.resolve(newEndpoint);
+ const result = await promise;
+ expect(result).toBe(true);
+ expect(resource._ionEndpoint).toBe(newEndpoint);
- return resultPromise;
+ expect(endpointResource.fetchJson).toHaveBeenCalled();
+ expect(refreshCallbackSpy).toHaveBeenCalled();
+ });
}
- it("works when error is a 401", function () {
- const error = new RequestErrorEvent(401);
- return testCallback(resource, error);
- });
+ testCallback(
+ "401 response",
+ () => originalResource,
+ () => new RequestErrorEvent(401),
+ );
- it("works when error is event with Image target", function () {
- const event = { target: new Image() };
- return testCallback(resource, event);
- });
+ testCallback(
+ "Image target event",
+ () => originalResource,
+ () => ({
+ target: new Image(),
+ }),
+ );
- it("works with derived resource and sets root access_token", function () {
- const derived = resource.getDerivedResource("1");
- const error = new RequestErrorEvent(401);
- return testCallback(derived, error).then(function () {
- expect(derived._ionEndpoint).toBe(resource._ionEndpoint);
- expect(derived.headers.Authorization).toEqual(
- resource.headers.Authorization,
- );
- });
- });
+ testCallback(
+ "derrived resource",
+ () => originalResource.getDerivedResource("1"),
+ () => new RequestErrorEvent(401),
+ );
});
});
diff --git a/packages/engine/Specs/Scene/Azure2DImageryProviderSpec.js b/packages/engine/Specs/Scene/Azure2DImageryProviderSpec.js
index 395cb7b801b..bef5c5d85dc 100644
--- a/packages/engine/Specs/Scene/Azure2DImageryProviderSpec.js
+++ b/packages/engine/Specs/Scene/Azure2DImageryProviderSpec.js
@@ -34,16 +34,6 @@ describe("Scene/Azure2DImageryProvider", function () {
);
});
- it("requires tilesetId to be specified", function () {
- expect(function () {
- return new Azure2DImageryProvider({
- subscriptionKey: "a-subscription-key",
- });
- }).toThrowDeveloperError(
- "options.tilesetId is required, actual value was undefined",
- );
- });
-
it("requestImage returns a promise for an image and loads it for cross-origin use", function () {
const provider = new Azure2DImageryProvider({
subscriptionKey: "test-subscriptionKey",
diff --git a/packages/engine/Specs/Scene/IonImageryProviderSpec.js b/packages/engine/Specs/Scene/IonImageryProviderSpec.js
index 7c117ea36f4..00345f1018e 100644
--- a/packages/engine/Specs/Scene/IonImageryProviderSpec.js
+++ b/packages/engine/Specs/Scene/IonImageryProviderSpec.js
@@ -14,43 +14,48 @@ import {
UrlTemplateImageryProvider,
WebMapServiceImageryProvider,
WebMapTileServiceImageryProvider,
+ Google2DImageryProvider,
+ Azure2DImageryProvider,
} from "../../index.js";
describe("Scene/IonImageryProvider", function () {
- async function createTestProviderAsync(endpointData) {
- endpointData = endpointData ?? {
- type: "IMAGERY",
- url: "http://test.invalid/layer",
- accessToken: "not_really_a_refresh_token",
- attributions: [],
- };
+ let defaultAssetId;
+ let defaultEndpoint;
+ let defaultConstructorOptions;
- const assetId = 12335;
- const options = {};
+ function setUpTestEndpoint({
+ assetId = defaultAssetId,
+ endpoint = defaultEndpoint,
+ constructorOptions = defaultConstructorOptions,
+ } = {}) {
const endpointResource = IonResource._createEndpointResource(
assetId,
- options,
+ constructorOptions,
);
+
spyOn(IonResource, "_createEndpointResource").and.returnValue(
endpointResource,
);
spyOn(endpointResource, "fetchJson").and.returnValue(
- Promise.resolve(endpointData),
+ Promise.resolve(endpoint),
);
- const provider = await IonImageryProvider.fromAssetId(assetId, options);
-
- expect(IonResource._createEndpointResource).toHaveBeenCalledWith(
- assetId,
- options,
- );
- return provider;
+ return endpointResource;
}
beforeEach(function () {
RequestScheduler.clearForSpecs();
IonImageryProvider._endpointCache = {};
+
+ defaultAssetId = 12335;
+ defaultEndpoint = {
+ type: "IMAGERY",
+ url: "http://test.invalid/layer",
+ accessToken: "not_really_a_refresh_token",
+ attributions: [],
+ };
+ defaultConstructorOptions = {};
});
it("conforms to ImageryProvider interface", function () {
@@ -64,13 +69,17 @@ describe("Scene/IonImageryProvider", function () {
});
it("fromAssetId throws with non-imagery asset", async function () {
- await expectAsync(
- createTestProviderAsync({
+ setUpTestEndpoint({
+ endpoint: {
type: "3DTILES",
url: "http://test.invalid/layer",
accessToken: "not_really_a_refresh_token",
attributions: [],
- }),
+ },
+ });
+
+ await expectAsync(
+ IonImageryProvider.fromAssetId(defaultAssetId, defaultConstructorOptions),
).toBeRejectedWithError(
RuntimeError,
"Cesium ion asset 12335 is not an imagery asset.",
@@ -78,13 +87,17 @@ describe("Scene/IonImageryProvider", function () {
});
it("fromAssetId rejects with unknown external asset type", async function () {
- await expectAsync(
- createTestProviderAsync({
+ setUpTestEndpoint({
+ endpoint: {
type: "IMAGERY",
externalType: "TUBELCANE",
options: { url: "http://test.invalid/layer" },
attributions: [],
- }),
+ },
+ });
+
+ await expectAsync(
+ IonImageryProvider.fromAssetId(defaultAssetId, defaultConstructorOptions),
).toBeRejectedWithError(
RuntimeError,
"Unrecognized Cesium ion imagery type: TUBELCANE",
@@ -92,54 +105,31 @@ describe("Scene/IonImageryProvider", function () {
});
it("fromAssetId resolves to created provider", async function () {
- const provider = await createTestProviderAsync();
+ setUpTestEndpoint();
+
+ const provider = await IonImageryProvider.fromAssetId(
+ defaultAssetId,
+ defaultConstructorOptions,
+ );
expect(provider).toBeInstanceOf(IonImageryProvider);
expect(provider.errorEvent).toBeDefined();
expect(provider._imageryProvider).toBeInstanceOf(
UrlTemplateImageryProvider,
);
- });
- it("Uses previously fetched endpoint cache", async function () {
- const endpointData = {
- type: "IMAGERY",
- url: "http://test.invalid/layer",
- accessToken: "not_really_a_refresh_token",
- attributions: [],
- };
-
- const assetId = 12335;
- const options = {
- accessToken: "token",
- server: "http://test.invalid",
- };
- const endpointResource = IonResource._createEndpointResource(
- assetId,
- options,
- );
- spyOn(IonResource, "_createEndpointResource").and.returnValue(
- endpointResource,
- );
- spyOn(endpointResource, "fetchJson").and.returnValue(
- Promise.resolve(endpointData),
+ expect(IonResource._createEndpointResource).toHaveBeenCalledWith(
+ defaultAssetId,
+ defaultConstructorOptions,
);
-
- expect(endpointResource.fetchJson.calls.count()).toBe(0);
- await IonImageryProvider.fromAssetId(assetId, options);
- expect(endpointResource.fetchJson.calls.count()).toBe(1);
-
- // Same as options but in a different order to verify cache is order independant.
- const options2 = {
- accessToken: "token",
- server: "http://test.invalid",
- };
- await IonImageryProvider.fromAssetId(assetId, options2);
- //Since the data is cached, fetchJson is not called again.
- expect(endpointResource.fetchJson.calls.count()).toBe(1);
});
it("propagates called to underlying imagery provider resolves when ready", async function () {
- const provider = await createTestProviderAsync();
+ setUpTestEndpoint();
+
+ const provider = await IonImageryProvider.fromAssetId(
+ defaultAssetId,
+ defaultConstructorOptions,
+ );
const internalProvider = provider._imageryProvider;
expect(provider.rectangle).toBe(internalProvider.rectangle);
expect(provider.tileWidth).toBe(internalProvider.tileWidth);
@@ -178,165 +168,220 @@ describe("Scene/IonImageryProvider", function () {
expect(credits).toContain(innerCredit);
});
- it("handles server-sent credits", async function () {
- const serverCredit = {
- html: 'Text',
- collapsible: false,
- };
- const provider = await createTestProviderAsync({
- type: "IMAGERY",
- url: "http://test.invalid/layer",
- accessToken: "not_really_a_refresh_token",
- attributions: [serverCredit],
+ async function testExternalImagery(type, ImageryClass, options) {
+ it(`works with type "${type}"`, async function () {
+ setUpTestEndpoint({
+ endpoint: {
+ type: "IMAGERY",
+ externalType: type,
+ options: options,
+ attributions: [],
+ },
+ });
+
+ const provider = await IonImageryProvider.fromAssetId(
+ defaultAssetId,
+ defaultConstructorOptions,
+ );
+
+ expect(provider._imageryProvider).toBeInstanceOf(ImageryClass);
});
- const credits = provider.getTileCredits(0, 0, 0);
- const credit = credits[0];
- expect(credit).toBeInstanceOf(Credit);
- expect(credit.html).toEqual(serverCredit.html);
- expect(credit.showOnScreen).toEqual(!serverCredit.collapsible);
- });
+ it(`works with cached type "${type}"`, async function () {
+ const endpointResource = setUpTestEndpoint({
+ endpoint: {
+ type: "IMAGERY",
+ externalType: type,
+ options: options,
+ attributions: [],
+ },
+ });
- async function testExternalImagery(type, options, ImageryClass) {
- const provider = await createTestProviderAsync({
- type: "IMAGERY",
- externalType: type,
- options: options,
- attributions: [],
+ const providerA = await IonImageryProvider.fromAssetId(
+ defaultAssetId,
+ defaultConstructorOptions,
+ );
+ expect(providerA._imageryProvider).toBeInstanceOf(ImageryClass);
+
+ expect(endpointResource.fetchJson.calls.count()).toBe(1);
+
+ const providerB = await IonImageryProvider.fromAssetId(
+ defaultAssetId,
+ // Same options, but different reference
+ { ...defaultConstructorOptions },
+ );
+ expect(providerB._imageryProvider).toBeInstanceOf(ImageryClass);
+
+ expect(endpointResource.fetchJson.calls.count()).toBe(1);
+ });
+
+ it(`works with type "${type}" and server-sent credits`, async function () {
+ const serverCredit = {
+ html: 'Text',
+ collapsible: false,
+ };
+
+ setUpTestEndpoint({
+ endpoint: {
+ type: "IMAGERY",
+ externalType: type,
+ options: options,
+ attributions: [serverCredit],
+ },
+ });
+
+ const provider = await IonImageryProvider.fromAssetId(
+ defaultAssetId,
+ defaultConstructorOptions,
+ );
+
+ const credits = provider.getTileCredits(0, 0, 0);
+ const credit = credits[0];
+ expect(credit).toBeInstanceOf(Credit);
+ expect(credit.html).toEqual(serverCredit.html);
+ expect(credit.showOnScreen).toEqual(!serverCredit.collapsible);
});
- expect(provider._imageryProvider).toBeInstanceOf(ImageryClass);
}
- it("createImageryProvider works with ARCGIS_MAPSERVER", function () {
- spyOn(Resource._Implementations, "loadWithXhr").and.callFake(
- function (
- url,
- responseType,
- method,
- data,
- headers,
- deferred,
- overrideMimeType,
- ) {
- deferred.resolve(
- JSON.stringify({ imageUrl: "", imageUrlSubdomains: [], zoomMax: 0 }),
- );
- },
- );
- return testExternalImagery(
- "ARCGIS_MAPSERVER",
- { url: "http://test.invalid" },
- ArcGisMapServerImageryProvider,
- );
+ describe("ARCGIS_MAPSERVER", function () {
+ beforeEach(function () {
+ spyOn(Resource._Implementations, "loadWithXhr").and.callFake(
+ function (
+ url,
+ responseType,
+ method,
+ data,
+ headers,
+ deferred,
+ overrideMimeType,
+ ) {
+ deferred.resolve(
+ JSON.stringify({
+ imageUrl: "",
+ imageUrlSubdomains: [],
+ zoomMax: 0,
+ }),
+ );
+ },
+ );
+ });
+
+ testExternalImagery("ARCGIS_MAPSERVER", ArcGisMapServerImageryProvider, {
+ url: "http://test.invalid",
+ });
});
- it("createImageryProvider works with BING", function () {
- spyOn(Resource._Implementations, "loadWithXhr").and.callFake(
- function (
- url,
- responseType,
- method,
- data,
- headers,
- deferred,
- overrideMimeType,
- ) {
- deferred.resolve(
- JSON.stringify({
- resourceSets: [
- {
- resources: [
- { imageUrl: "", imageUrlSubdomains: [], zoomMax: 0 },
- ],
- },
- ],
- }),
- );
- },
- );
- return testExternalImagery(
- "BING",
- { url: "http://test.invalid", key: "" },
- BingMapsImageryProvider,
- );
+ describe("BING", function () {
+ beforeEach(function () {
+ spyOn(Resource._Implementations, "loadWithXhr").and.callFake(
+ function (
+ url,
+ responseType,
+ method,
+ data,
+ headers,
+ deferred,
+ overrideMimeType,
+ ) {
+ deferred.resolve(
+ JSON.stringify({
+ resourceSets: [
+ {
+ resources: [
+ { imageUrl: "", imageUrlSubdomains: [], zoomMax: 0 },
+ ],
+ },
+ ],
+ }),
+ );
+ },
+ );
+ });
+
+ testExternalImagery("BING", BingMapsImageryProvider, {
+ url: "http://test.invalid",
+ key: "",
+ });
});
- it("createImageryProvider works with GOOGLE_EARTH", function () {
- spyOn(Resource._Implementations, "loadWithXhr").and.callFake(
- function (
- url,
- responseType,
- method,
- data,
- headers,
- deferred,
- overrideMimeType,
- ) {
- deferred.resolve(JSON.stringify({ layers: [{ id: 0, version: "" }] }));
- },
- );
+ describe("GOOGLE_EARTH", function () {
+ beforeEach(function () {
+ spyOn(Resource._Implementations, "loadWithXhr").and.callFake(
+ function (
+ url,
+ responseType,
+ method,
+ data,
+ headers,
+ deferred,
+ overrideMimeType,
+ ) {
+ deferred.resolve(
+ JSON.stringify({ layers: [{ id: 0, version: "" }] }),
+ );
+ },
+ );
+ });
- return testExternalImagery(
- "GOOGLE_EARTH",
- { url: "http://test.invalid", channel: 0 },
- GoogleEarthEnterpriseMapsProvider,
- );
+ testExternalImagery("GOOGLE_EARTH", GoogleEarthEnterpriseMapsProvider, {
+ url: "http://test.invalid",
+ channel: 0,
+ });
});
- it("createImageryProvider works with MAPBOX", function () {
- return testExternalImagery(
- "MAPBOX",
- { accessToken: "test-token", url: "http://test.invalid", mapId: 1 },
- MapboxImageryProvider,
- );
+ testExternalImagery("MAPBOX", MapboxImageryProvider, {
+ accessToken: "test-token",
+ url: "http://test.invalid",
+ mapId: 1,
});
- it("createImageryProvider works with SINGLE_TILE", function () {
- spyOn(Resource._Implementations, "createImage").and.callFake(
- function (request, crossOrigin, deferred) {
- deferred.resolve({
- height: 16,
- width: 16,
- });
- },
- );
+ describe("SINGLE_TILE", function () {
+ beforeEach(function () {
+ spyOn(Resource._Implementations, "createImage").and.callFake(
+ function (request, crossOrigin, deferred) {
+ deferred.resolve({
+ height: 16,
+ width: 16,
+ });
+ },
+ );
+ });
- return testExternalImagery(
- "SINGLE_TILE",
- { url: "http://test.invalid" },
- SingleTileImageryProvider,
- );
+ testExternalImagery("SINGLE_TILE", SingleTileImageryProvider, {
+ url: "http://test.invalid",
+ });
});
- it("createImageryProvider works with TMS", function () {
- return testExternalImagery(
- "TMS",
- { url: "http://test.invalid" },
- UrlTemplateImageryProvider,
- );
+ testExternalImagery("TMS", UrlTemplateImageryProvider, {
+ url: "http://test.invalid",
});
- it("createImageryProvider works with URL_TEMPLATE", function () {
- return testExternalImagery(
- "URL_TEMPLATE",
- { url: "http://test.invalid" },
- UrlTemplateImageryProvider,
- );
+ testExternalImagery("URL_TEMPLATE", UrlTemplateImageryProvider, {
+ url: "http://test.invalid",
});
- it("createImageryProvider works with WMS", function () {
- return testExternalImagery(
- "WMS",
- { url: "http://test.invalid", layers: [] },
- WebMapServiceImageryProvider,
- );
+ testExternalImagery("WMS", WebMapServiceImageryProvider, {
+ url: "http://test.invalid",
+ layers: [],
});
- it("createImageryProvider works with WMTS", function () {
- return testExternalImagery(
- "WMTS",
- { url: "http://test.invalid", layer: "", style: "", tileMatrixSetID: 1 },
- WebMapTileServiceImageryProvider,
- );
+ testExternalImagery("WMTS", WebMapTileServiceImageryProvider, {
+ url: "http://test.invalid",
+ layer: "",
+ style: "",
+ tileMatrixSetID: 1,
+ });
+
+ testExternalImagery("GOOGLE_2D_MAPS", Google2DImageryProvider, {
+ url: "http://test.invalid",
+ key: "",
+ session: "",
+ tileWidth: 256,
+ tileHeight: 256,
+ });
+
+ testExternalImagery("AZURE_MAPS", Azure2DImageryProvider, {
+ url: "http://test.invalid",
+ subscriptionKey: "",
});
});