diff --git a/Apps/Sandcastle/gallery/Google 2D Tiles with Custom Styles.html b/Apps/Sandcastle/gallery/Google 2D Tiles with Custom Styles.html new file mode 100644 index 000000000000..5ed86eec1d26 --- /dev/null +++ b/Apps/Sandcastle/gallery/Google 2D Tiles with Custom Styles.html @@ -0,0 +1,94 @@ + + + + + + + + + Cesium Demo + + + + + +
+

Loading...

+
+ + + diff --git a/Apps/Sandcastle/gallery/Google 2D Tiles with Custom Styles.jpg b/Apps/Sandcastle/gallery/Google 2D Tiles with Custom Styles.jpg new file mode 100644 index 000000000000..dbae29b468fd Binary files /dev/null and b/Apps/Sandcastle/gallery/Google 2D Tiles with Custom Styles.jpg differ diff --git a/Apps/Sandcastle/gallery/Google 2D Tiles.html b/Apps/Sandcastle/gallery/Google 2D Tiles.html new file mode 100644 index 000000000000..6727b831cae0 --- /dev/null +++ b/Apps/Sandcastle/gallery/Google 2D Tiles.html @@ -0,0 +1,69 @@ + + + + + + + + + Cesium Demo + + + + + +
+

Loading...

+
+ + + diff --git a/Apps/Sandcastle/gallery/Google 2D Tiles.jpg b/Apps/Sandcastle/gallery/Google 2D Tiles.jpg new file mode 100644 index 000000000000..ef0192279ce0 Binary files /dev/null and b/Apps/Sandcastle/gallery/Google 2D Tiles.jpg differ diff --git a/Apps/Sandcastle/gallery/Imagery Assets available from ion.html b/Apps/Sandcastle/gallery/Imagery Assets available from ion.html new file mode 100644 index 000000000000..2ffc5ca96ecf --- /dev/null +++ b/Apps/Sandcastle/gallery/Imagery Assets available from ion.html @@ -0,0 +1,98 @@ + + + + + + + + + Cesium Demo + + + + + +
+

Loading...

+
+ + + diff --git a/Apps/Sandcastle/gallery/Imagery Assets available from ion.jpg b/Apps/Sandcastle/gallery/Imagery Assets available from ion.jpg new file mode 100644 index 000000000000..87f660174690 Binary files /dev/null and b/Apps/Sandcastle/gallery/Imagery Assets available from ion.jpg differ diff --git a/Apps/Sandcastle/gallery/development/Azure 2D Tiles.html b/Apps/Sandcastle/gallery/development/Azure 2D Tiles.html new file mode 100644 index 000000000000..8efeef92c891 --- /dev/null +++ b/Apps/Sandcastle/gallery/development/Azure 2D Tiles.html @@ -0,0 +1,72 @@ + + + + + + + + + Cesium Demo + + + + + +
+

Loading...

+
+ + + diff --git a/Apps/Sandcastle/gallery/development/Azure 2D Tiles.jpg b/Apps/Sandcastle/gallery/development/Azure 2D Tiles.jpg new file mode 100644 index 000000000000..3d4b9e1d1297 Binary files /dev/null and b/Apps/Sandcastle/gallery/development/Azure 2D Tiles.jpg differ diff --git a/CHANGES.md b/CHANGES.md index a9af9e028671..34b98ce9d85c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ #### Additions :tada: +- Adds Google2DImageryProvider to load imagery from [Google Maps](https://developers.google.com/maps/documentation/tile/2d-tiles-overview) [#12913](https://github.com/CesiumGS/cesium/pull/12913) - Adds an async factory method for the Material class that allows callers to wait on resource loading. [#10566](https://github.com/CesiumGS/cesium/issues/10566) ## 1.133 - 2025-09-02 diff --git a/packages/engine/Source/Core/GoogleMaps.js b/packages/engine/Source/Core/GoogleMaps.js index 7ec935068796..ae447657c782 100644 --- a/packages/engine/Source/Core/GoogleMaps.js +++ b/packages/engine/Source/Core/GoogleMaps.js @@ -25,10 +25,10 @@ GoogleMaps.defaultApiKey = undefined; * Gets or sets the default Google Map Tiles API endpoint. * * @type {string|Resource} - * @default https://tile.googleapis.com/v1/ + * @default https://tile.googleapis.com/ */ GoogleMaps.mapTilesApiEndpoint = new Resource({ - url: "https://tile.googleapis.com/v1/", + url: "https://tile.googleapis.com/", }); GoogleMaps.getDefaultCredit = function () { diff --git a/packages/engine/Source/Core/IonResource.js b/packages/engine/Source/Core/IonResource.js index 6bf12684a2fd..cf10e967c75c 100644 --- a/packages/engine/Source/Core/IonResource.js +++ b/packages/engine/Source/Core/IonResource.js @@ -39,6 +39,12 @@ 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" @@ -84,7 +90,7 @@ if (defined(Object.create)) { * @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} A Promise to am instance representing the Cesium ion Asset. + * @returns {Promise} A Promise to an instance representing the Cesium ion Asset. * * @example * // Load a Cesium3DTileset with asset ID of 124624234 @@ -207,7 +213,7 @@ IonResource.prototype._makeRequest = function (options) { /** * @private - */ + **/ IonResource._createEndpointResource = function (assetId, options) { //>>includeStart('debug', pragmas.debug); Check.defined("assetId", assetId); @@ -226,6 +232,13 @@ IonResource._createEndpointResource = function (assetId, options) { resourceOptions.queryParameters = { access_token: accessToken }; } + if (defined(options.queryParameters)) { + resourceOptions.queryParameters = { + ...resourceOptions.queryParameters, + ...options.queryParameters, + }; + } + addClientHeaders(resourceOptions); return server.getDerivedResource(resourceOptions); @@ -267,9 +280,21 @@ function retryCallback(that, error) { ionRoot._pendingPromise = endpointResource .fetchJson() .then(function (newEndpoint) { - //Set the token for root resource so new derived resources automatically pick it up + // Set the token for root resource so new derived resources automatically pick it up ionRoot._ionEndpoint = newEndpoint; - return 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) { // Pass or fail, we're done with this promise, the next failure should use a new one. @@ -284,4 +309,5 @@ function retryCallback(that, error) { return true; }); } + export default IonResource; diff --git a/packages/engine/Source/Scene/Azure2DImageryProvider.js b/packages/engine/Source/Scene/Azure2DImageryProvider.js new file mode 100644 index 000000000000..25d43e1cbbe1 --- /dev/null +++ b/packages/engine/Source/Scene/Azure2DImageryProvider.js @@ -0,0 +1,308 @@ +import Check from "../Core/Check.js"; +import Credit from "../Core/Credit.js"; +import defined from "../Core/defined.js"; +import Resource from "../Core/Resource.js"; +import IonResource from "../Core/IonResource.js"; +import UrlTemplateImageryProvider from "./UrlTemplateImageryProvider.js"; + +const trailingSlashRegex = /\/$/; + +/** + * @typedef {object} Azure2DImageryProvider.ConstructorOptions + * + * Initialization options for the Azure2DImageryProvider constructor + * + * @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.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 + * this that the number of tiles at the minimum level is small, such as four or less. A larger number is likely + * to result in rendering problems. + * @property {number} [options.maximumLevel=22] The maximum level-of-detail supported by the imagery provider. + * @property {Rectangle} [options.rectangle=Rectangle.MAX_VALUE] The rectangle, in radians, covered by the image. + */ + +/** + * Provides 2D image tiles from Azure. + * + * @alias Azure2DImageryProvider + * @constructor + * @private + * @param {Azure2DImageryProvider.ConstructorOptions} options Object describing initialization options + * + * @example + * // Azure 2D imagery provider + * const azureImageryProvider = new Cesium.Azure2DImageryProvider({ + * subscriptionKey: "subscription-key", + * tilesetId: "microsoft.base.road" + * }); + */ +function Azure2DImageryProvider(options) { + options = options ?? {}; + options.maximumLevel = options.maximumLevel ?? 22; + options.minimumLevel = options.minimumLevel ?? 0; + + const subscriptionKey = + options.subscriptionKey ?? options["subscription-key"]; + //>>includeStart('debug', pragmas.debug); + Check.defined("options.tilesetId", options.tilesetId); + Check.defined("options.subscriptionKey", subscriptionKey); + //>>includeEnd('debug'); + + const resource = + options.url instanceof IonResource + ? options.url + : Resource.createIfNeeded(options.url ?? "https://atlas.microsoft.com/"); + + let templateUrl = resource.getUrlComponent(); + if (!trailingSlashRegex.test(templateUrl)) { + templateUrl += "/"; + } + templateUrl += `map/tile`; + + resource.url = templateUrl; + + resource.setQueryParameters({ + "api-version": "2024-04-01", + tilesetId: options.tilesetId, + zoom: `{z}`, + x: `{x}`, + y: `{y}`, + "subscription-key": subscriptionKey, + }); + + let credit; + if (defined(options.credit)) { + credit = options.credit; + if (typeof credit === "string") { + credit = new Credit(credit); + } + } + + const provider = new UrlTemplateImageryProvider({ + ...options, + url: resource, + credit: credit, + }); + provider._resource = resource; + this._imageryProvider = provider; + + // This will be defined for ion resources + this._tileCredits = resource.credits; +} + +Object.defineProperties(Azure2DImageryProvider.prototype, { + /** + * Gets the URL of the Azure 2D Imagery server. + * @memberof Azure2DImageryProvider.prototype + * @type {string} + * @readonly + */ + url: { + get: function () { + return this._imageryProvider.url; + }, + }, + + /** + * Gets the rectangle, in radians, of the imagery provided by the instance. + * @memberof Azure2DImageryProvider.prototype + * @type {Rectangle} + * @readonly + */ + rectangle: { + get: function () { + return this._imageryProvider.rectangle; + }, + }, + + /** + * Gets the width of each tile, in pixels. + * @memberof Azure2DImageryProvider.prototype + * @type {number} + * @readonly + */ + tileWidth: { + get: function () { + return this._imageryProvider.tileWidth; + }, + }, + + /** + * Gets the height of each tile, in pixels. + * @memberof Azure2DImageryProvider.prototype + * @type {number} + * @readonly + */ + tileHeight: { + get: function () { + return this._imageryProvider.tileHeight; + }, + }, + + /** + * Gets the maximum level-of-detail that can be requested. + * @memberof Azure2DImageryProvider.prototype + * @type {number|undefined} + * @readonly + */ + maximumLevel: { + get: function () { + return this._imageryProvider.maximumLevel; + }, + }, + + /** + * Gets the minimum level-of-detail that can be requested. Generally, + * a minimum level should only be used when the rectangle of the imagery is small + * enough that the number of tiles at the minimum level is small. An imagery + * provider with more than a few tiles at the minimum level will lead to + * rendering problems. + * @memberof Azure2DImageryProvider.prototype + * @type {number} + * @readonly + */ + minimumLevel: { + get: function () { + return this._imageryProvider.minimumLevel; + }, + }, + + /** + * Gets the tiling scheme used by the provider. + * @memberof Azure2DImageryProvider.prototype + * @type {TilingScheme} + * @readonly + */ + tilingScheme: { + get: function () { + return this._imageryProvider.tilingScheme; + }, + }, + + /** + * Gets the tile discard policy. If not undefined, the discard policy is responsible + * for filtering out "missing" tiles via its shouldDiscardImage function. If this function + * returns undefined, no tiles are filtered. + * @memberof Azure2DImageryProvider.prototype + * @type {TileDiscardPolicy} + * @readonly + */ + tileDiscardPolicy: { + get: function () { + return this._imageryProvider.tileDiscardPolicy; + }, + }, + + /** + * Gets an event that is raised when the imagery 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 Azure2DImageryProvider.prototype + * @type {Event} + * @readonly + */ + errorEvent: { + get: function () { + return this._imageryProvider.errorEvent; + }, + }, + + /** + * Gets the credit to display when this imagery provider is active. Typically this is used to credit + * the source of the imagery. + * @memberof Azure2DImageryProvider.prototype + * @type {Credit} + * @readonly + */ + credit: { + get: function () { + return this._imageryProvider.credit; + }, + }, + + /** + * Gets the proxy used by this provider. + * @memberof Azure2DImageryProvider.prototype + * @type {Proxy} + * @readonly + */ + proxy: { + get: function () { + return this._imageryProvider.proxy; + }, + }, + + /** + * Gets a value indicating whether or not the images provided by this imagery provider + * include an alpha channel. If this property is false, an alpha channel, if present, will + * be ignored. If this property is true, any images without an alpha channel will be treated + * as if their alpha is 1.0 everywhere. When this property is false, memory usage + * and texture upload time are reduced. + * @memberof Azure2DImageryProvider.prototype + * @type {boolean} + * @readonly + */ + hasAlphaChannel: { + get: function () { + return this._imageryProvider.hasAlphaChannel; + }, + }, +}); + +/** + * Gets the credits to be displayed when a given tile is displayed. + * + * @param {number} x The tile X coordinate. + * @param {number} y The tile Y coordinate. + * @param {number} level The tile level; + * @returns {Credit[]|undefined} The credits to be displayed when the tile is displayed. + */ +Azure2DImageryProvider.prototype.getTileCredits = function (x, y, level) { + return this._imageryProvider.getTileCredits(x, y, level); +}; + +/** + * Requests the image for a given tile. + * + * @param {number} x The tile X coordinate. + * @param {number} y The tile Y coordinate. + * @param {number} level The tile level. + * @param {Request} [request] The request object. Intended for internal use only. + * @returns {Promise|undefined} A promise for the image that will resolve when the image is available, or + * undefined if there are too many active requests to the server, and the request should be retried later. + */ +Azure2DImageryProvider.prototype.requestImage = function ( + x, + y, + level, + request, +) { + return this._imageryProvider.requestImage(x, y, level, request); +}; + +/** + * Picking features is not currently supported by this imagery provider, so this function simply returns + * undefined. + * + * @param {number} x The tile X coordinate. + * @param {number} y The tile Y coordinate. + * @param {number} level The tile level. + * @param {number} longitude The longitude at which to pick features. + * @param {number} latitude The latitude at which to pick features. + * @return {undefined} Undefined since picking is not supported. + */ +Azure2DImageryProvider.prototype.pickFeatures = function ( + x, + y, + level, + longitude, + latitude, +) { + return undefined; +}; + +// Exposed for tests +export default Azure2DImageryProvider; diff --git a/packages/engine/Source/Scene/CreditDisplay.js b/packages/engine/Source/Scene/CreditDisplay.js index 249cc76d66f2..29dbc4ead935 100644 --- a/packages/engine/Source/Scene/CreditDisplay.js +++ b/packages/engine/Source/Scene/CreditDisplay.js @@ -194,7 +194,7 @@ function appendCss(container) { .cesium-credit-lightbox.cesium-credit-lightbox-expanded { border: 1px solid #444; border-radius: 5px; - max-width: 370px; + max-width: 470px; } .cesium-credit-lightbox.cesium-credit-lightbox-mobile { height: 100%; diff --git a/packages/engine/Source/Scene/Google2DImageryProvider.js b/packages/engine/Source/Scene/Google2DImageryProvider.js new file mode 100644 index 000000000000..d9f62cc1c314 --- /dev/null +++ b/packages/engine/Source/Scene/Google2DImageryProvider.js @@ -0,0 +1,614 @@ +import Credit from "../Core/Credit.js"; +import Frozen from "../Core/Frozen.js"; +import defined from "../Core/defined.js"; +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"; + +const trailingSlashRegex = /\/$/; + +/** + * @typedef {Object} Google2DImageryProvider.ConstructorOptions + * + * Initialization options for the Google2DImageryProvider constructor + * + * @property {object} options Object with the following properties: + * @property {string} options.key The Google api key to send with tile requests. + * @property {string} options.session The Google session token that tracks the current state of your map and viewport. + * @property {string|Resource|IonResource} options.url The Google 2D maps endpoint. + * @property {string} options.tileWidth The width of each tile in pixels. + * @property {string} options.tileHeight The height of each tile in pixels. + * @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 + * this that the number of tiles at the minimum level is small, such as four or less. A larger number is likely + * to result in rendering problems. + * @property {number} [options.maximumLevel=22] The maximum level-of-detail supported by the imagery provider. + * @property {Rectangle} [options.rectangle=Rectangle.MAX_VALUE] The rectangle, in radians, covered by the image. + */ + +/** + *
+ * This object is normally not instantiated directly, use {@link Google2DImageryProvider.fromIonAssetId} or {@link Google2DImageryProvider.fromUrl}. + *
+ * + * + * Provides 2D image tiles from {@link https://developers.google.com/maps/documentation/tile/2d-tiles-overview|Google 2D Tiles}. + * + * Google 2D Tiles can only be used with the Google geocoder. + * + * @alias Google2DImageryProvider + * @constructor + * + * @param {Google2DImageryProvider.ConstructorOptions} options Object describing initialization options + * + * @example + * // Google 2D imagery provider + * const googleTilesProvider = Cesium.Google2DImageryProvider.fromIonAssetId({ + * assetId: 3830184 + * }); + * @example + * // Use your own Google api key + * Cesium.GoogleMaps.defaultApiKey = "your-api-key"; + * + * const googleTilesProvider = Cesium.Google2DImageryProvider.fromUrl({ + * mapType: "SATELLITE" + * }); + * + + * + * @see {@link https://developers.google.com/maps/documentation/tile/2d-tiles-overview} + * @see {@link https://developers.google.com/maps/documentation/tile/session_tokens} + * @see {@link https://en.wikipedia.org/wiki/IETF_language_tag|IETF Language Tags} + * @see {@link https://cldr.unicode.org/|Common Locale Data Repository region identifiers} + */ + +function Google2DImageryProvider(options) { + options = options ?? Frozen.EMPTY_OBJECT; + this._maximumLevel = options.maximumLevel ?? 22; + this._minimumLevel = options.minimumLevel ?? 0; + + //>>includeStart("debug", pragmas.debug); + Check.defined("options.session", options.session); + Check.defined("options.tileWidth", options.tileWidth); + Check.defined("options.tileHeight", options.tileHeight); + Check.defined("options.key", options.key); + //>>includeEnd("debug"); + + this._session = options.session; + this._key = options.key; + this._tileWidth = options.tileWidth; + this._tileHeight = options.tileHeight; + + const resource = + options.url instanceof IonResource + ? options.url + : Resource.createIfNeeded(options.url ?? GoogleMaps.mapTilesApiEndpoint); + + let templateUrl = resource.getUrlComponent(); + if (!trailingSlashRegex.test(templateUrl)) { + templateUrl += "/"; + } + const tilesUrl = `${templateUrl}v1/2dtiles/{z}/{x}/{y}`; + this._viewportUrl = `${templateUrl}tile/v1/viewport`; + + resource.url = tilesUrl; + + resource.setQueryParameters({ + session: encodeURIComponent(options.session), + key: encodeURIComponent(options.key), + }); + + let credit; + if (defined(options.credit)) { + credit = options.credit; + if (typeof credit === "string") { + credit = new Credit(credit); + } + } + + const provider = new UrlTemplateImageryProvider({ + url: resource, + credit: credit, + tileWidth: options.tileWidth, + tileHeight: options.tileHeight, + ellipsoid: options.ellipsoid, + rectangle: options.rectangle, + maximumLevel: this._maximumLevel, + minimumLevel: this._minimumLevel, + }); + provider._resource = resource; + this._imageryProvider = provider; + + // 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, { + /** + * Gets the URL of the Google 2D Imagery server. + * @memberof Google2DImageryProvider.prototype + * @type {string} + * @readonly + */ + url: { + get: function () { + return this._imageryProvider.url; + }, + }, + + /** + * Gets the rectangle, in radians, of the imagery provided by the instance. + * @memberof Google2DImageryProvider.prototype + * @type {Rectangle} + * @readonly + */ + rectangle: { + get: function () { + return this._imageryProvider.rectangle; + }, + }, + + /** + * Gets the width of each tile, in pixels. + * @memberof Google2DImageryProvider.prototype + * @type {number} + * @readonly + */ + tileWidth: { + get: function () { + return this._imageryProvider.tileWidth; + }, + }, + + /** + * Gets the height of each tile, in pixels. + * @memberof Google2DImageryProvider.prototype + * @type {number} + * @readonly + */ + tileHeight: { + get: function () { + return this._imageryProvider.tileHeight; + }, + }, + + /** + * Gets the maximum level-of-detail that can be requested. + * @memberof Google2DImageryProvider.prototype + * @type {number|undefined} + * @readonly + */ + maximumLevel: { + get: function () { + return this._imageryProvider.maximumLevel; + }, + }, + + /** + * Gets the minimum level-of-detail that can be requested. Generally, + * a minimum level should only be used when the rectangle of the imagery is small + * enough that the number of tiles at the minimum level is small. An imagery + * provider with more than a few tiles at the minimum level will lead to + * rendering problems. + * @memberof Google2DImageryProvider.prototype + * @type {number} + * @readonly + */ + minimumLevel: { + get: function () { + return this._imageryProvider.minimumLevel; + }, + }, + + /** + * Gets the tiling scheme used by the provider. + * @memberof Google2DImageryProvider.prototype + * @type {TilingScheme} + * @readonly + */ + tilingScheme: { + get: function () { + return this._imageryProvider.tilingScheme; + }, + }, + + /** + * Gets the tile discard policy. If not undefined, the discard policy is responsible + * for filtering out "missing" tiles via its shouldDiscardImage function. If this function + * returns undefined, no tiles are filtered. + * @memberof Google2DImageryProvider.prototype + * @type {TileDiscardPolicy} + * @readonly + */ + tileDiscardPolicy: { + get: function () { + return this._imageryProvider.tileDiscardPolicy; + }, + }, + + /** + * Gets an event that is raised when the imagery 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 Google2DImageryProvider.prototype + * @type {Event} + * @readonly + */ + errorEvent: { + get: function () { + return this._imageryProvider.errorEvent; + }, + }, + + /** + * Gets the credit to display when this imagery provider is active. Typically this is used to credit + * the source of the imagery. + * @memberof Google2DImageryProvider.prototype + * @type {Credit} + * @readonly + */ + credit: { + get: function () { + return this._imageryProvider.credit; + }, + }, + + /** + * Gets the proxy used by this provider. + * @memberof Google2DImageryProvider.prototype + * @type {Proxy} + * @readonly + */ + proxy: { + get: function () { + return this._imageryProvider.proxy; + }, + }, + + /** + * Gets a value indicating whether or not the images provided by this imagery provider + * include an alpha channel. If this property is false, an alpha channel, if present, will + * be ignored. If this property is true, any images without an alpha channel will be treated + * as if their alpha is 1.0 everywhere. When this property is false, memory usage + * and texture upload time are reduced. + * @memberof Google2DImageryProvider.prototype + * @type {boolean} + * @readonly + */ + hasAlphaChannel: { + get: function () { + return this._imageryProvider.hasAlphaChannel; + }, + }, +}); + +/** + * Creates an {@link ImageryProvider} which provides 2D global tiled imagery from {@link https://developers.google.com/maps/documentation/tile/2d-tiles-overview|Google 2D Tiles}, streamed using the Cesium ion REST API. + * @param {object} options Object with the following properties: + * @param {string} options.assetId The Cesium ion asset id. + * @param {"satellite" | "terrain" | "roadmap"} [options.mapType="satellite"] The map type of the Google map imagery. Valid options are satellite, terrain, and roadmap. If overlayLayerType is set, mapType is ignored and a transparent overlay is returned. If overlayMapType is undefined, then a basemap of mapType is returned. layerRoadmap overlayLayerType is included in terrain and roadmap mapTypes. + * @param {string} [options.language="en_US"] an IETF language tag that specifies the language used to display information on the tiles + * @param {string} [options.region="US"] A Common Locale Data Repository region identifier (two uppercase letters) that represents the physical location of the user. + * @param {"layerRoadmap" | "layerStreetview" | "layerTraffic"} [options.overlayLayerType] Returns a transparent overlay map with the specified layerType. If no value is provided, a basemap of mapType is returned. Use multiple instances of Google2DImageryProvider to add multiple Google Maps overlays to a scene. layerRoadmap is included in terrain and roadmap mapTypes, so adding as overlay to terrain or roadmap has no effect. + * @param {Object} [options.styles] An array of JSON style objects that specify the appearance and detail level of map features such as roads, parks, and built-up areas. Styling is used to customize the standard Google base map. The styles parameter is valid only if the mapType is roadmap. For the complete style syntax, see the ({@link https://developers.google.com/maps/documentation/tile/style-reference|Google Style Reference}). + * @param {Ellipsoid} [options.ellipsoid=Ellipsoid.default] The ellipsoid. If not specified, the default ellipsoid is used. + * @param {number} [options.minimumLevel=0] The minimum level-of-detail supported by the imagery provider. Take care when specifying + * this that the number of tiles at the minimum level is small, such as four or less. A larger number is likely + * to result in rendering problems. + * @param {number} [options.maximumLevel=22] The maximum level-of-detail supported by the imagery provider. + * @param {Rectangle} [options.rectangle=Rectangle.MAX_VALUE] The rectangle, in radians, covered by the image. + * @param {Credit|string} [options.credit] A credit for the data source, which is displayed on the canvas. + * + * @returns {Promise} A promise that resolves to the created Google2DImageryProvider. + * + * @example + * // Google 2D imagery provider + * const googleTilesProvider = Cesium.Google2DImageryProvider.fromIonAssetId({ + * assetId: 3830184 + * }); + * @example + * // Google 2D roadmap overlay with custom styles + * const googleTileProvider = Cesium.Google2DImageryProvider.fromIonAssetId({ + * assetId: 3830184, + * overlayLayerType: "layerRoadmap", + * styles: [ + * { + * stylers: [{ hue: "#00ffe6" }, { saturation: -20 }], + * }, + * { + * featureType: "road", + * elementType: "geometry", + * stylers: [{ lightness: 100 }, { visibility: "simplified" }], + * }, + * ], + * }); + */ +Google2DImageryProvider.fromIonAssetId = async function (options) { + options = options ?? {}; + options.mapType = options.mapType ?? "satellite"; + options.language = options.language ?? "en_US"; + options.region = options.region ?? "US"; + + const overlayLayerType = options.overlayLayerType; + //>>includeStart("debug", pragmas.debug); + if (defined(overlayLayerType)) { + Check.typeOf.string("options.overlayLayerType", overlayLayerType); + } + Check.defined("options.assetId", options.assetId); + //>>includeEnd("debug"); + + const queryOptions = buildQueryOptions(options); + + const endpointResource = IonResource._createEndpointResource( + options.assetId, + { + queryParameters: { + options: JSON.stringify(queryOptions), + }, + }, + ); + + const endpoint = await endpointResource.fetchJson(); + const endpointOptions = { ...endpoint.options }; + delete endpointOptions.url; + + const providerOptions = { + language: options.language, + region: options.region, + ellipsoid: options.ellipsoid, + minimumLevel: options.minimumLevel, + maximumLevel: options.maximumLevel, + rectangle: options.rectangle, + credit: options.credit, + }; + + return new Google2DImageryProvider({ + ...endpointOptions, + ...providerOptions, + url: new IonResource(endpoint, endpointResource), + }); +}; + +/** + * Creates an {@link ImageryProvider} which provides 2D global tiled imagery from {@link https://developers.google.com/maps/documentation/tile/2d-tiles-overview|Google 2D Tiles}. + * @param {object} options Object with the following properties: + * @param {string} [options.key=GoogleMaps.defaultApiKey] Your API key to access Google 2D Tiles. See {@link https://developers.google.com/maps/documentation/javascript/get-api-key} for instructions on how to create your own key. + * @param {"satellite" | "terrain" | "roadmap"} [options.mapType="satellite"] The map type of the Google map imagery. Valid options are satellite, terrain, and roadmap. If overlayLayerType is set, mapType is ignored and a transparent overlay is returned. If overlayMapType is undefined, then a basemap of mapType is returned. layerRoadmap overlayLayerType is included in terrain and roadmap mapTypes. + * @param {string} [options.language="en_US"] an IETF language tag that specifies the language used to display information on the tiles + * @param {string} [options.region="US"] A Common Locale Data Repository region identifier (two uppercase letters) that represents the physical location of the user. + * @param {"layerRoadmap" | "layerStreetview" | "layerTraffic"} [options.overlayLayerType] Returns a transparent overlay map with the specified layerType. If no value is provided, a basemap of mapType is returned. Use multiple instances of Google2DImageryProvider to add multiple Google Maps overlays to a scene. layerRoadmap is included in terrain and roadmap mapTypes, so adding as overlay to terrain or roadmap has no effect. + * @param {Object} [options.styles] An array of JSON style objects that specify the appearance and detail level of map features such as roads, parks, and built-up areas. Styling is used to customize the standard Google base map. The styles parameter is valid only if the mapType is roadmap. For the complete style syntax, see the ({@link https://developers.google.com/maps/documentation/tile/style-reference|Google Style Reference}). + * @param {Ellipsoid} [options.ellipsoid=Ellipsoid.default] The ellipsoid. If not specified, the default ellipsoid is used. + * @param {number} [options.minimumLevel=0] The minimum level-of-detail supported by the imagery provider. Take care when specifying + * this that the number of tiles at the minimum level is small, such as four or less. A larger number is likely + * to result in rendering problems. + * @param {number} [options.maximumLevel=22] The maximum level-of-detail supported by the imagery provider. + * @param {Rectangle} [options.rectangle=Rectangle.MAX_VALUE] The rectangle, in radians, covered by the image. + * @param {Credit|string} [options.credit] A credit for the data source, which is displayed on the canvas. + * + * @returns {Promise} A promise that resolves to the created Google2DImageryProvider. + * + * @example + * // Use your own Google api key + * Cesium.GoogleMaps.defaultApiKey = "your-api-key"; + * + * const googleTilesProvider = Cesium.Google2DImageryProvider.fromUrl({ + * mapType: "satellite" + * }); + * @example + * // Google 2D roadmap overlay with custom styles + * Cesium.GoogleMaps.defaultApiKey = "your-api-key"; + * + * const googleTileProvider = Cesium.Google2DImageryProvider.fromUrl({ + * overlayLayerType: "layerRoadmap", + * styles: [ + * { + * stylers: [{ hue: "#00ffe6" }, { saturation: -20 }], + * }, + * { + * featureType: "road", + * elementType: "geometry", + * stylers: [{ lightness: 100 }, { visibility: "simplified" }], + * }, + * ], + * }); + */ +Google2DImageryProvider.fromUrl = async function (options) { + options = options ?? {}; + options.mapType = options.mapType ?? "satellite"; + options.language = options.language ?? "en_US"; + options.region = options.region ?? "US"; + options.url = options.url ?? GoogleMaps.mapTilesApiEndpoint; + options.key = options.key ?? GoogleMaps.defaultApiKey; + + const overlayLayerType = options.overlayLayerType; + //>>includeStart("debug", pragmas.debug); + if (defined(overlayLayerType)) { + Check.typeOf.string("overlayLayerType", overlayLayerType); + } + if (!defined(options.key) && !defined(GoogleMaps.defaultApiKey)) { + throw new DeveloperError( + "options.key or GoogleMaps.defaultApiKey is required.", + ); + } + //>>includeEnd("debug"); + + const sessionJson = await createGoogleImagerySession(options); + return new Google2DImageryProvider({ + ...sessionJson, + ...options, + credit: options.credit ?? GoogleMaps.getDefaultCredit(), + }); +}; + +/** + * Gets the credits to be displayed when a given tile is displayed. + * + * @param {number} x The tile X coordinate. + * @param {number} y The tile Y coordinate. + * @param {number} level The tile level; + * @returns {Credit[]|undefined} The credits to be displayed when the tile is displayed. + */ +Google2DImageryProvider.prototype.getTileCredits = function (x, y, level) { + const hasAttributions = defined(this._attributionsByLevel); + + if (!hasAttributions || !defined(this._tileCredits)) { + return undefined; + } + + const innerCredits = this._attributionsByLevel.get(level); + if (!defined(this._tileCredits)) { + return innerCredits; + } + + return this._tileCredits.concat(innerCredits); +}; + +/** + * Requests the image for a given tile. + * + * @param {number} x The tile X coordinate. + * @param {number} y The tile Y coordinate. + * @param {number} level The tile level. + * @param {Request} [request] The request object. Intended for internal use only. + * @returns {Promise|undefined} A promise for the image that will resolve when the image is available, or + * undefined if there are too many active requests to the server, and the request should be retried later. + */ +Google2DImageryProvider.prototype.requestImage = function ( + x, + y, + level, + request, +) { + return this._imageryProvider.requestImage(x, y, level, request); +}; + +/** + * Picking features is not currently supported by this imagery provider, so this function simply returns + * undefined. + * + * @param {number} x The tile X coordinate. + * @param {number} y The tile Y coordinate. + * @param {number} level The tile level. + * @param {number} longitude The longitude at which to pick features. + * @param {number} latitude The latitude at which to pick features. + * @return {undefined} Undefined since picking is not supported. + */ +Google2DImageryProvider.prototype.pickFeatures = function ( + x, + y, + level, + longitude, + latitude, +) { + return undefined; +}; + +/** + * Get attribution for imagery from Google Maps to display in the credits + * @private + * @return {Promise>} The list of attribution sources to display in the credits. + */ +Google2DImageryProvider.prototype.getViewportCredits = async function () { + const maximumLevel = this._maximumLevel; + + const promises = []; + for (let level = 0; level < maximumLevel + 1; level++) { + promises.push( + fetchViewportAttribution( + this._viewportUrl, + this._key, + this._session, + level, + ), + ); + } + const results = await Promise.all(promises); + + const attributionsByLevel = new Map(); + for (let level = 0; level < maximumLevel + 1; level++) { + const credits = []; + const attributions = results[level]; + if (attributions) { + const levelCredits = new Credit(attributions); + credits.push(levelCredits); + } + attributionsByLevel.set(level, credits); + } + + this._attributionsByLevel = attributionsByLevel; + + return attributionsByLevel; +}; + +async function fetchViewportAttribution(url, key, session, level) { + const viewport = await Resource.fetch({ + url: url, + queryParameters: { + key, + session, + zoom: level, + north: 90, + south: -90, + east: 180, + west: -180, + }, + data: JSON.stringify(Frozen.EMPTY_OBJECT), + }); + const viewportJson = JSON.parse(viewport); + return viewportJson.copyright; +} + +function buildQueryOptions(options) { + const { mapType, overlayLayerType, styles } = options; + + const queryOptions = { + mapType, + overlay: false, + }; + + if (mapType === "terrain" && !defined(overlayLayerType)) { + queryOptions.layerTypes = ["layerRoadmap"]; + } + + if (defined(overlayLayerType)) { + queryOptions.mapType = "satellite"; + queryOptions.overlay = true; + queryOptions.layerTypes = [overlayLayerType]; + } + if (defined(styles)) { + queryOptions.styles = styles; + } + return queryOptions; +} + +async function createGoogleImagerySession(options) { + const { language, region, key, url } = options; + + const queryOptions = buildQueryOptions(options); + + let baseUrl = url.url ?? url; + if (!trailingSlashRegex.test(baseUrl)) { + baseUrl += "/"; + } + + const response = await Resource.post({ + url: `${baseUrl}v1/createSession`, + queryParameters: { key: key }, + data: JSON.stringify({ + ...queryOptions, + language, + region, + }), + }); + const responseJson = JSON.parse(response); + return responseJson; +} + +export default Google2DImageryProvider; diff --git a/packages/engine/Source/Scene/IonImageryProvider.js b/packages/engine/Source/Scene/IonImageryProvider.js index 0d03a03a84f6..ea07c55f333d 100644 --- a/packages/engine/Source/Scene/IonImageryProvider.js +++ b/packages/engine/Source/Scene/IonImageryProvider.js @@ -13,6 +13,8 @@ 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. @@ -52,6 +54,18 @@ const ImageryProviderAsyncMapping = { ...options, }); }, + GOOGLE_2D_MAPS: (ionResource, options) => { + return new Google2DImageryProvider({ + ...options, + url: ionResource, + }); + }, + AZURE_MAPS: (ionResource, options) => { + return new Azure2DImageryProvider({ + ...options, + url: ionResource, + }); + }, }; /** @@ -308,7 +322,14 @@ IonImageryProvider.fromAssetId = async function (assetId, options) { const options = { ...endpoint.options }; const url = options.url; delete options.url; - imageryProvider = await factory(url, options); + if (["GOOGLE_2D_MAPS", "AZURE_MAPS"].includes(endpoint.externalType)) { + imageryProvider = await factory( + new IonResource(endpoint, endpointResource), + options, + ); + } else { + imageryProvider = await factory(url, options); + } } const provider = new IonImageryProvider(options); diff --git a/packages/engine/Source/Scene/createGooglePhotorealistic3DTileset.js b/packages/engine/Source/Scene/createGooglePhotorealistic3DTileset.js index 0217d4cd8a77..56212aa663d0 100644 --- a/packages/engine/Source/Scene/createGooglePhotorealistic3DTileset.js +++ b/packages/engine/Source/Scene/createGooglePhotorealistic3DTileset.js @@ -88,7 +88,7 @@ async function createGooglePhotorealistic3DTileset(apiOptions, tilesetOptions) { } const resource = new Resource({ - url: `${GoogleMaps.mapTilesApiEndpoint}3dtiles/root.json`, + url: `${GoogleMaps.mapTilesApiEndpoint}v1/3dtiles/root.json`, queryParameters: { key: key, }, diff --git a/packages/engine/Specs/Scene/Azure2DImageryProviderSpec.js b/packages/engine/Specs/Scene/Azure2DImageryProviderSpec.js new file mode 100644 index 000000000000..395cb7b801b9 --- /dev/null +++ b/packages/engine/Specs/Scene/Azure2DImageryProviderSpec.js @@ -0,0 +1,194 @@ +import { + Math as CesiumMath, + Rectangle, + Request, + RequestScheduler, + Resource, + WebMercatorTilingScheme, + Imagery, + ImageryLayer, + ImageryProvider, + ImageryState, + Azure2DImageryProvider, +} from "../../index.js"; + +import pollToPromise from "../../../../Specs/pollToPromise.js"; + +describe("Scene/Azure2DImageryProvider", function () { + afterEach(function () { + Resource._Implementations.createImage = + Resource._DefaultImplementations.createImage; + }); + + it("conforms to ImageryProvider interface", function () { + expect(Azure2DImageryProvider).toConformToInterface(ImageryProvider); + }); + + it("requires the subscription key to be specified", function () { + expect(function () { + return new Azure2DImageryProvider({ + tilesetId: "a-tileset-id", + }); + }).toThrowDeveloperError( + "options.subscriptionKey is required, actual value was undefined", + ); + }); + + 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", + tilesetId: "a-tileset-id", + }); + + expect(provider.url).toEqual( + "https://atlas.microsoft.com/map/tile?api-version=2024-04-01&tilesetId=a-tileset-id&zoom={z}&x={x}&y={y}&subscription-key=test-subscriptionKey", + ); + expect(provider.tileWidth).toEqual(256); + expect(provider.tileHeight).toEqual(256); + expect(provider.maximumLevel).toBe(22); + expect(provider.tilingScheme).toBeInstanceOf(WebMercatorTilingScheme); + expect(provider.rectangle).toEqual(new WebMercatorTilingScheme().rectangle); + + spyOn(Resource._Implementations, "createImage").and.callFake( + function (request, crossOrigin, deferred) { + // Just return any old image. + Resource._DefaultImplementations.createImage( + new Request({ url: "Data/Images/Red16x16.png" }), + crossOrigin, + deferred, + ); + }, + ); + + return provider.requestImage(0, 0, 0).then(function (image) { + expect(Resource._Implementations.createImage).toHaveBeenCalled(); + expect(image).toBeImageOrImageBitmap(); + }); + }); + + it("rectangle passed to constructor does not affect tile numbering", function () { + const rectangle = new Rectangle(0.1, 0.2, 0.3, 0.4); + const provider = new Azure2DImageryProvider({ + subscriptionKey: "test-subscriptionKey", + tilesetId: "a-tileset-id", + rectangle: rectangle, + }); + + expect(provider.tileWidth).toEqual(256); + expect(provider.tileHeight).toEqual(256); + expect(provider.maximumLevel).toBe(22); + expect(provider.tilingScheme).toBeInstanceOf(WebMercatorTilingScheme); + expect(provider.rectangle).toEqualEpsilon(rectangle, CesiumMath.EPSILON14); + expect(provider.tileDiscardPolicy).toBeUndefined(); + + spyOn(Resource._Implementations, "createImage").and.callFake( + function (request, crossOrigin, deferred) { + expect(request.url).toContain("zoom=0&x=0&y=0"); + + // Just return any old image. + Resource._DefaultImplementations.createImage( + new Request({ url: "Data/Images/Red16x16.png" }), + crossOrigin, + deferred, + ); + }, + ); + + return provider.requestImage(0, 0, 0).then(function (image) { + expect(Resource._Implementations.createImage).toHaveBeenCalled(); + expect(image).toBeImageOrImageBitmap(); + }); + }); + + it("uses maximumLevel passed to constructor", function () { + const provider = new Azure2DImageryProvider({ + subscriptionKey: "test-subscriptionKey", + tilesetId: "a-tileset-id", + maximumLevel: 5, + }); + expect(provider.maximumLevel).toEqual(5); + }); + + it("uses minimumLevel passed to constructor", function () { + const provider = new Azure2DImageryProvider({ + subscriptionKey: "test-subscriptionKey", + tilesetId: "a-tileset-id", + minimumLevel: 1, + }); + expect(provider.minimumLevel).toEqual(1); + }); + + it("turns the supplied credit into a logo", function () { + const creditText = "Thanks to our awesome made up source of this imagery!"; + const providerWithCredit = new Azure2DImageryProvider({ + subscriptionKey: "test-subscriptionKey", + tilesetId: "a-tileset-id", + credit: creditText, + }); + expect(providerWithCredit.credit.html).toEqual(creditText); + }); + + it("raises error event when image cannot be loaded", function () { + const provider = new Azure2DImageryProvider({ + subscriptionKey: "test-subscriptionKey", + tilesetId: "a-tileset-id", + }); + + const layer = new ImageryLayer(provider); + + let tries = 0; + provider.errorEvent.addEventListener(function (error) { + expect(error.timesRetried).toEqual(tries); + ++tries; + if (tries < 3) { + error.retry = true; + } + setTimeout(function () { + RequestScheduler.update(); + }, 1); + }); + + Resource._Implementations.createImage = function ( + request, + crossOrigin, + deferred, + ) { + if (tries === 2) { + // Succeed after 2 tries + Resource._DefaultImplementations.createImage( + new Request({ url: "Data/Images/Red16x16.png" }), + crossOrigin, + deferred, + ); + } else { + // fail + setTimeout(function () { + deferred.reject(); + }, 1); + } + }; + + const imagery = new Imagery(layer, 0, 0, 0); + imagery.addReference(); + layer._requestImagery(imagery); + RequestScheduler.update(); + + return pollToPromise(function () { + return imagery.state === ImageryState.RECEIVED; + }).then(function () { + expect(imagery.image).toBeImageOrImageBitmap(); + expect(tries).toEqual(2); + imagery.releaseReference(); + }); + }); +}); diff --git a/packages/engine/Specs/Scene/Google2DImageryProviderSpec.js b/packages/engine/Specs/Scene/Google2DImageryProviderSpec.js new file mode 100644 index 000000000000..d2e5efa40ed7 --- /dev/null +++ b/packages/engine/Specs/Scene/Google2DImageryProviderSpec.js @@ -0,0 +1,226 @@ +import { + Math as CesiumMath, + Rectangle, + Request, + RequestScheduler, + Resource, + WebMercatorTilingScheme, + Imagery, + ImageryLayer, + ImageryProvider, + ImageryState, + Google2DImageryProvider, +} from "../../index.js"; + +import pollToPromise from "../../../../Specs/pollToPromise.js"; + +describe("Scene/Google2DImageryProvider", function () { + beforeEach(function () { + RequestScheduler.clearForSpecs(); + spyOn( + Google2DImageryProvider.prototype, + "getViewportCredits", + ).and.returnValue(Promise.resolve("")); + }); + + afterEach(function () { + Resource._Implementations.createImage = + Resource._DefaultImplementations.createImage; + }); + + it("conforms to ImageryProvider interface", function () { + expect(Google2DImageryProvider).toConformToInterface(ImageryProvider); + }); + + it("requires the session token to be specified", function () { + expect(function () { + return new Google2DImageryProvider({}); + }).toThrowDeveloperError(); + }); + + it("requires the tileWidth to be specified", function () { + expect(function () { + return new Google2DImageryProvider({ + session: "a-session-token", + }); + }).toThrowDeveloperError(); + }); + + it("requires the key to be specified", function () { + expect(function () { + return new Google2DImageryProvider({ + session: "a-session-token", + tileHeight: 256, + tileWidth: 256, + }); + }).toThrowDeveloperError(); + }); + + it("fromIonAssetId throws if assetId is not provided", async function () { + await expectAsync( + Google2DImageryProvider.fromIonAssetId(), + ).toBeRejectedWithDeveloperError( + "options.assetId 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 Google2DImageryProvider({ + session: "test-session-token", + key: "test-key", + tileWidth: 256, + tileHeight: 256, + }); + + expect(provider.url).toEqual( + "https://tile.googleapis.com/v1/2dtiles/{z}/{x}/{y}?session=test-session-token&key=test-key", + ); + expect(provider.tileWidth).toEqual(256); + expect(provider.tileHeight).toEqual(256); + expect(provider.maximumLevel).toBe(22); + expect(provider.tilingScheme).toBeInstanceOf(WebMercatorTilingScheme); + expect(provider.rectangle).toEqual(new WebMercatorTilingScheme().rectangle); + + spyOn(Resource._Implementations, "createImage").and.callFake( + function (request, crossOrigin, deferred) { + // Just return any old image. + Resource._DefaultImplementations.createImage( + new Request({ url: "Data/Images/Red16x16.png" }), + crossOrigin, + deferred, + ); + }, + ); + + return provider.requestImage(0, 0, 0).then(function (image) { + expect(Resource._Implementations.createImage).toHaveBeenCalled(); + expect(image).toBeImageOrImageBitmap(); + }); + }); + + it("rectangle passed to constructor does not affect tile numbering", function () { + const rectangle = new Rectangle(0.1, 0.2, 0.3, 0.4); + const provider = new Google2DImageryProvider({ + session: "test-session-token", + key: "test-key", + tileWidth: 256, + tileHeight: 256, + rectangle: rectangle, + }); + + expect(provider.tileWidth).toEqual(256); + expect(provider.tileHeight).toEqual(256); + expect(provider.maximumLevel).toBe(22); + expect(provider.tilingScheme).toBeInstanceOf(WebMercatorTilingScheme); + expect(provider.rectangle).toEqualEpsilon(rectangle, CesiumMath.EPSILON14); + expect(provider.tileDiscardPolicy).toBeUndefined(); + + spyOn(Resource._Implementations, "createImage").and.callFake( + function (request, crossOrigin, deferred) { + expect(request.url).toContain("/0/0/0"); + + // Just return any old image. + Resource._DefaultImplementations.createImage( + new Request({ url: "Data/Images/Red16x16.png" }), + crossOrigin, + deferred, + ); + }, + ); + + return provider.requestImage(0, 0, 0).then(function (image) { + expect(Resource._Implementations.createImage).toHaveBeenCalled(); + expect(image).toBeImageOrImageBitmap(); + }); + }); + + it("uses maximumLevel passed to constructor", function () { + const provider = new Google2DImageryProvider({ + session: "test-session-token", + key: "test-key", + tileWidth: 256, + tileHeight: 256, + maximumLevel: 5, + }); + expect(provider.maximumLevel).toEqual(5); + }); + + it("uses minimumLevel passed to constructor", function () { + const provider = new Google2DImageryProvider({ + session: "test-session-token", + key: "test-key", + tileWidth: 256, + tileHeight: 256, + minimumLevel: 1, + }); + expect(provider.minimumLevel).toEqual(1); + }); + + it("turns the supplied credit into a logo", function () { + const creditText = "Thanks to our awesome made up source of this imagery!"; + const providerWithCredit = new Google2DImageryProvider({ + session: "test-session-token", + key: "test-key", + tileWidth: 256, + tileHeight: 256, + credit: creditText, + }); + expect(providerWithCredit.credit.html).toEqual(creditText); + }); + + it("raises error event when image cannot be loaded", function () { + const provider = new Google2DImageryProvider({ + session: "test-session-token", + key: "test-key", + tileWidth: 256, + tileHeight: 256, + }); + + const layer = new ImageryLayer(provider); + + let tries = 0; + provider.errorEvent.addEventListener(function (error) { + expect(error.timesRetried).toEqual(tries); + ++tries; + if (tries < 3) { + error.retry = true; + } + setTimeout(function () { + RequestScheduler.update(); + }, 1); + }); + + Resource._Implementations.createImage = function ( + request, + crossOrigin, + deferred, + ) { + if (tries === 2) { + // Succeed after 2 tries + Resource._DefaultImplementations.createImage( + new Request({ url: "Data/Images/Red16x16.png" }), + crossOrigin, + deferred, + ); + } else { + // fail + setTimeout(function () { + deferred.reject(); + }, 1); + } + }; + + const imagery = new Imagery(layer, 0, 0, 0); + imagery.addReference(); + layer._requestImagery(imagery); + RequestScheduler.update(); + + return pollToPromise(function () { + return imagery.state === ImageryState.RECEIVED; + }).then(function () { + expect(imagery.image).toBeImageOrImageBitmap(); + expect(tries).toEqual(2); + imagery.releaseReference(); + }); + }); +}); diff --git a/packages/sandcastle/gallery/azure-2d-tiles/index.html b/packages/sandcastle/gallery/azure-2d-tiles/index.html new file mode 100644 index 000000000000..f30fef07b9d4 --- /dev/null +++ b/packages/sandcastle/gallery/azure-2d-tiles/index.html @@ -0,0 +1,6 @@ + +
+

Loading...

+
diff --git a/packages/sandcastle/gallery/azure-2d-tiles/main.js b/packages/sandcastle/gallery/azure-2d-tiles/main.js new file mode 100644 index 000000000000..a1bc4d6d9e64 --- /dev/null +++ b/packages/sandcastle/gallery/azure-2d-tiles/main.js @@ -0,0 +1,36 @@ +import * as Cesium from "cesium"; + +Cesium.Ion.defaultServer = "https://api.ion-staging.cesium.com"; +Cesium.Ion.defaultAccessToken = ""; + +const assetId = 1683; + +const azure = Cesium.ImageryLayer.fromProviderAsync( + Cesium.IonImageryProvider.fromAssetId(assetId), +); + +const viewer = new Cesium.Viewer("cesiumContainer", { + animation: false, + baseLayer: false, + baseLayerPicker: false, + geocoder: Cesium.IonGeocodeProviderType.GOOGLE, + timeline: false, + sceneModePicker: false, + navigationHelpButton: false, + homeButton: false, + terrainProvider: await Cesium.CesiumTerrainProvider.fromIonAssetId(1), +}); +viewer.geocoder.viewModel.keepExpanded = true; + +viewer.imageryLayers.add(azure); + +viewer.scene.camera.flyTo({ + duration: 0, + destination: new Cesium.Rectangle.fromDegrees( + //Philly + -75.280266, + 39.867004, + -74.955763, + 40.137992, + ), +}); diff --git a/packages/sandcastle/gallery/azure-2d-tiles/sandcastle.yaml b/packages/sandcastle/gallery/azure-2d-tiles/sandcastle.yaml new file mode 100644 index 000000000000..1846931c7f63 --- /dev/null +++ b/packages/sandcastle/gallery/azure-2d-tiles/sandcastle.yaml @@ -0,0 +1,8 @@ +legacyId: Azure 2D Tiles.html +title: Azure 2D Tiles +description: Global imagery data from Azure Maps. +development: true +labels: + - Imagery + - Development +thumbnail: thumbnail.jpg diff --git a/packages/sandcastle/gallery/azure-2d-tiles/thumbnail.jpg b/packages/sandcastle/gallery/azure-2d-tiles/thumbnail.jpg new file mode 100644 index 000000000000..3d4b9e1d1297 Binary files /dev/null and b/packages/sandcastle/gallery/azure-2d-tiles/thumbnail.jpg differ diff --git a/packages/sandcastle/gallery/google-2d-tiles-with-custom-styles/index.html b/packages/sandcastle/gallery/google-2d-tiles-with-custom-styles/index.html new file mode 100644 index 000000000000..f30fef07b9d4 --- /dev/null +++ b/packages/sandcastle/gallery/google-2d-tiles-with-custom-styles/index.html @@ -0,0 +1,6 @@ + +
+

Loading...

+
diff --git a/packages/sandcastle/gallery/google-2d-tiles-with-custom-styles/main.js b/packages/sandcastle/gallery/google-2d-tiles-with-custom-styles/main.js new file mode 100644 index 000000000000..07312db1bc2a --- /dev/null +++ b/packages/sandcastle/gallery/google-2d-tiles-with-custom-styles/main.js @@ -0,0 +1,54 @@ +import * as Cesium from "cesium"; + +const assetId = 3830184; + +const base = Cesium.ImageryLayer.fromProviderAsync( + Cesium.Google2DImageryProvider.fromIonAssetId({ + assetId, + mapType: "satellite", + }), +); + +const overlay = Cesium.ImageryLayer.fromProviderAsync( + Cesium.Google2DImageryProvider.fromIonAssetId({ + assetId, + overlayLayerType: "layerRoadmap", + styles: [ + { + stylers: [{ hue: "#00ffe6" }, { saturation: -20 }], + }, + { + featureType: "road", + elementType: "geometry", + stylers: [{ lightness: 100 }, { visibility: "simplified" }], + }, + ], + }), +); + +const viewer = new Cesium.Viewer("cesiumContainer", { + animation: false, + baseLayer: false, + baseLayerPicker: false, + geocoder: Cesium.IonGeocodeProviderType.GOOGLE, + timeline: false, + sceneModePicker: false, + navigationHelpButton: false, + homeButton: false, + terrainProvider: await Cesium.CesiumTerrainProvider.fromIonAssetId(1), +}); +viewer.geocoder.viewModel.keepExpanded = true; + +viewer.imageryLayers.add(base); +viewer.imageryLayers.add(overlay); + +viewer.scene.camera.flyTo({ + duration: 0, + destination: new Cesium.Rectangle.fromDegrees( + //Philly + -75.280266, + 39.867004, + -74.955763, + 40.137992, + ), +}); diff --git a/packages/sandcastle/gallery/google-2d-tiles-with-custom-styles/sandcastle.yaml b/packages/sandcastle/gallery/google-2d-tiles-with-custom-styles/sandcastle.yaml new file mode 100644 index 000000000000..87cb2d8a1652 --- /dev/null +++ b/packages/sandcastle/gallery/google-2d-tiles-with-custom-styles/sandcastle.yaml @@ -0,0 +1,7 @@ +legacyId: Google 2D Tiles with Roadmap Styles.html +title: Google 2D Tiles with Custom Styles +description: Imagery tiles from Google Maps with additional parameters to create overlays and custom styles. +labels: + - Imagery + - Showcases +thumbnail: thumbnail.jpg diff --git a/packages/sandcastle/gallery/google-2d-tiles-with-custom-styles/thumbnail.jpg b/packages/sandcastle/gallery/google-2d-tiles-with-custom-styles/thumbnail.jpg new file mode 100644 index 000000000000..dbae29b468fd Binary files /dev/null and b/packages/sandcastle/gallery/google-2d-tiles-with-custom-styles/thumbnail.jpg differ diff --git a/packages/sandcastle/gallery/google-2d-tiles/index.html b/packages/sandcastle/gallery/google-2d-tiles/index.html new file mode 100644 index 000000000000..f30fef07b9d4 --- /dev/null +++ b/packages/sandcastle/gallery/google-2d-tiles/index.html @@ -0,0 +1,6 @@ + +
+

Loading...

+
diff --git a/packages/sandcastle/gallery/google-2d-tiles/main.js b/packages/sandcastle/gallery/google-2d-tiles/main.js new file mode 100644 index 000000000000..b3dbe601c80b --- /dev/null +++ b/packages/sandcastle/gallery/google-2d-tiles/main.js @@ -0,0 +1,33 @@ +import * as Cesium from "cesium"; + +const assetId = 3830184; + +const google = Cesium.ImageryLayer.fromProviderAsync( + Cesium.IonImageryProvider.fromAssetId(assetId), +); + +const viewer = new Cesium.Viewer("cesiumContainer", { + animation: false, + baseLayer: false, + baseLayerPicker: false, + geocoder: Cesium.IonGeocodeProviderType.GOOGLE, + timeline: false, + sceneModePicker: false, + navigationHelpButton: false, + homeButton: false, + terrainProvider: await Cesium.CesiumTerrainProvider.fromIonAssetId(1), +}); +viewer.geocoder.viewModel.keepExpanded = true; + +viewer.imageryLayers.add(google); + +viewer.scene.camera.flyTo({ + duration: 0, + destination: new Cesium.Rectangle.fromDegrees( + //Philly + -75.280266, + 39.867004, + -74.955763, + 40.137992, + ), +}); diff --git a/packages/sandcastle/gallery/google-2d-tiles/sandcastle.yaml b/packages/sandcastle/gallery/google-2d-tiles/sandcastle.yaml new file mode 100644 index 000000000000..b0eaf04bf722 --- /dev/null +++ b/packages/sandcastle/gallery/google-2d-tiles/sandcastle.yaml @@ -0,0 +1,7 @@ +legacyId: Google 2D Tiles.html +title: Google 2D Tiles +description: Global imagery data from Google Maps. +labels: + - Imagery + - Showcases +thumbnail: thumbnail.jpg diff --git a/packages/sandcastle/gallery/google-2d-tiles/thumbnail.jpg b/packages/sandcastle/gallery/google-2d-tiles/thumbnail.jpg new file mode 100644 index 000000000000..ef0192279ce0 Binary files /dev/null and b/packages/sandcastle/gallery/google-2d-tiles/thumbnail.jpg differ diff --git a/packages/sandcastle/gallery/imagery-assets-available-from-ion/index.html b/packages/sandcastle/gallery/imagery-assets-available-from-ion/index.html new file mode 100644 index 000000000000..f30fef07b9d4 --- /dev/null +++ b/packages/sandcastle/gallery/imagery-assets-available-from-ion/index.html @@ -0,0 +1,6 @@ + +
+

Loading...

+
diff --git a/packages/sandcastle/gallery/imagery-assets-available-from-ion/main.js b/packages/sandcastle/gallery/imagery-assets-available-from-ion/main.js new file mode 100644 index 000000000000..a8fa9a1449b8 --- /dev/null +++ b/packages/sandcastle/gallery/imagery-assets-available-from-ion/main.js @@ -0,0 +1,63 @@ +import * as Cesium from "cesium"; +import Sandcastle from "Sandcastle"; + +const viewer = new Cesium.Viewer("cesiumContainer", { + animation: false, + baseLayer: false, + baseLayerPicker: false, + geocoder: Cesium.IonGeocodeProviderType.GOOGLE, + timeline: false, + sceneModePicker: false, + navigationHelpButton: false, + homeButton: false, + terrainProvider: await Cesium.CesiumTerrainProvider.fromIonAssetId(1), +}); +viewer.geocoder.viewModel.keepExpanded = true; + +const menuOptions = []; + +const dropdownOptions = [ + { label: "Google Maps 2D Contour", assetId: 3830186 }, + { label: "Google Maps 2D Labels Only", assetId: 3830185 }, + { label: "Google Maps 2D Roadmap", assetId: 3830184 }, + { label: "Google Maps 2D Satellite", assetId: 3830182 }, + { label: "Google Maps 2D Satellite with Labels", assetId: 3830183 }, + { label: "Bing Maps Aerial", assetId: 2 }, + { label: "Bing Maps Aerial with Labels", assetId: 3 }, + { label: "Bing Maps Road", assetId: 4 }, + { label: "Bing Maps Labels Only", assetId: 2411391 }, + { label: "Sentinel-2", assetId: 3954 }, +]; + +function showLayer(assetId) { + viewer.imageryLayers.removeAll(true); + const layer = Cesium.ImageryLayer.fromProviderAsync( + Cesium.IonImageryProvider.fromAssetId(assetId), + ); + viewer.imageryLayers.add(layer); +} + +dropdownOptions.forEach((opt) => { + const option = { + text: opt.label, + onselect: function () { + showLayer(opt.assetId); + }, + }; + menuOptions.push(option); +}); + +Sandcastle.addToolbarMenu(menuOptions); + +showLayer(3830186); + +viewer.scene.camera.flyTo({ + duration: 0, + destination: new Cesium.Rectangle.fromDegrees( + //Philly + -75.280266, + 39.867004, + -74.955763, + 40.137992, + ), +}); diff --git a/packages/sandcastle/gallery/imagery-assets-available-from-ion/sandcastle.yaml b/packages/sandcastle/gallery/imagery-assets-available-from-ion/sandcastle.yaml new file mode 100644 index 000000000000..1cc92a0d20f8 --- /dev/null +++ b/packages/sandcastle/gallery/imagery-assets-available-from-ion/sandcastle.yaml @@ -0,0 +1,6 @@ +legacyId: Imagery Assets available from ion.html +title: Imagery Assets available from ion +description: Global imagery assets available from Cesium ion. +labels: + - Showcases +thumbnail: thumbnail.jpg diff --git a/packages/sandcastle/gallery/imagery-assets-available-from-ion/thumbnail.jpg b/packages/sandcastle/gallery/imagery-assets-available-from-ion/thumbnail.jpg new file mode 100644 index 000000000000..87f660174690 Binary files /dev/null and b/packages/sandcastle/gallery/imagery-assets-available-from-ion/thumbnail.jpg differ diff --git a/packages/widgets/Source/BaseLayerPicker/createDefaultImageryProviderViewModels.js b/packages/widgets/Source/BaseLayerPicker/createDefaultImageryProviderViewModels.js index 4238fdb61446..a999c31b23af 100644 --- a/packages/widgets/Source/BaseLayerPicker/createDefaultImageryProviderViewModels.js +++ b/packages/widgets/Source/BaseLayerPicker/createDefaultImageryProviderViewModels.js @@ -302,6 +302,79 @@ of the world.\nhttp://www.openstreetmap.org", }), ); + providerViewModels.push( + new ProviderViewModel({ + name: "Google Maps Satellite", + iconUrl: buildModuleUrl( + "Widgets/Images/ImageryProviders/googleSatellite.png", + ), + tooltip: "Imagery from Google Maps", + category: "Cesium ion", + creationFunction: function () { + return IonImageryProvider.fromAssetId(3830182); + }, + }), + ); + + providerViewModels.push( + new ProviderViewModel({ + name: "Google Maps Satellite with Labels", + iconUrl: buildModuleUrl( + "Widgets/Images/ImageryProviders/googleSatelliteLabels.png", + ), + tooltip: "Imagery with place labels from Google Maps", + category: "Cesium ion", + creationFunction: function () { + return IonImageryProvider.fromAssetId(3830183); + }, + }), + ); + + providerViewModels.push( + new ProviderViewModel({ + name: "Google Maps Roadmap", + iconUrl: buildModuleUrl( + "Widgets/Images/ImageryProviders/googleRoadmap.png", + ), + tooltip: + "Labeled roads and other features on a base landscape from Google Maps", + category: "Cesium ion", + creationFunction: function () { + return IonImageryProvider.fromAssetId(3830184); + }, + }), + ); + + providerViewModels.push( + new ProviderViewModel({ + name: "Google Maps Labels Only", + iconUrl: buildModuleUrl( + "Widgets/Images/ImageryProviders/googleLabels.png", + ), + tooltip: + "Place labels from Google Maps to combine with other imagery such as Sentinel-2", + category: "Cesium ion", + creationFunction: function () { + return IonImageryProvider.fromAssetId(3830185); + }, + }), + ); + + providerViewModels.push( + new ProviderViewModel({ + name: "Google Maps Contour", + iconUrl: buildModuleUrl( + "Widgets/Images/ImageryProviders/googleContour.png", + ), + tooltip: + "Hillshade mapping, contour lines, natural features (roadmap features hidden) from Google Maps", + category: "Cesium ion", + creationFunction: function () { + return IonImageryProvider.fromAssetId(3830186); + }, + }), + ); + return providerViewModels; } export default createDefaultImageryProviderViewModels; diff --git a/packages/widgets/Source/Images/ImageryProviders/googleContour.png b/packages/widgets/Source/Images/ImageryProviders/googleContour.png new file mode 100644 index 000000000000..618cc1214f16 Binary files /dev/null and b/packages/widgets/Source/Images/ImageryProviders/googleContour.png differ diff --git a/packages/widgets/Source/Images/ImageryProviders/googleLabels.png b/packages/widgets/Source/Images/ImageryProviders/googleLabels.png new file mode 100644 index 000000000000..7f8cf65a17aa Binary files /dev/null and b/packages/widgets/Source/Images/ImageryProviders/googleLabels.png differ diff --git a/packages/widgets/Source/Images/ImageryProviders/googleRoadmap.png b/packages/widgets/Source/Images/ImageryProviders/googleRoadmap.png new file mode 100644 index 000000000000..c0db555bcf8c Binary files /dev/null and b/packages/widgets/Source/Images/ImageryProviders/googleRoadmap.png differ diff --git a/packages/widgets/Source/Images/ImageryProviders/googleSatellite.png b/packages/widgets/Source/Images/ImageryProviders/googleSatellite.png new file mode 100644 index 000000000000..84fadbe4fdfa Binary files /dev/null and b/packages/widgets/Source/Images/ImageryProviders/googleSatellite.png differ diff --git a/packages/widgets/Source/Images/ImageryProviders/googleSatelliteLabels.png b/packages/widgets/Source/Images/ImageryProviders/googleSatelliteLabels.png new file mode 100644 index 000000000000..e2133a18da9f Binary files /dev/null and b/packages/widgets/Source/Images/ImageryProviders/googleSatelliteLabels.png differ