diff --git a/Apps/Sandcastle/gallery/development/Azure 2D Tiles.html b/Apps/Sandcastle/gallery/Azure 2D Tiles.html similarity index 100% rename from Apps/Sandcastle/gallery/development/Azure 2D Tiles.html rename to Apps/Sandcastle/gallery/Azure 2D Tiles.html diff --git a/Apps/Sandcastle/gallery/development/Azure 2D Tiles.jpg b/Apps/Sandcastle/gallery/Azure 2D Tiles.jpg similarity index 100% rename from Apps/Sandcastle/gallery/development/Azure 2D Tiles.jpg rename to Apps/Sandcastle/gallery/Azure 2D Tiles.jpg diff --git a/Apps/Sandcastle/gallery/Imagery Layers.html b/Apps/Sandcastle/gallery/Imagery Layers.html index 34ee6db78f38..8fc5abd0ed5b 100644 --- a/Apps/Sandcastle/gallery/Imagery Layers.html +++ b/Apps/Sandcastle/gallery/Imagery Layers.html @@ -30,9 +30,9 @@ "use strict"; //Sandcastle_Begin const viewer = new Cesium.Viewer("cesiumContainer", { - baseLayer: Cesium.ImageryLayer.fromWorldImagery({ - style: Cesium.IonWorldImageryStyle.AERIAL_WITH_LABELS, - }), + baseLayer: Cesium.ImageryLayer.fromProviderAsync( + Cesium.IonImageryProvider.fromAssetId(3830183), + ), baseLayerPicker: false, }); const layers = viewer.scene.imageryLayers; diff --git a/packages/engine/Source/Scene/Azure2DImageryProvider.js b/packages/engine/Source/Scene/Azure2DImageryProvider.js index 24a9d3f453a1..c6856f607617 100644 --- a/packages/engine/Source/Scene/Azure2DImageryProvider.js +++ b/packages/engine/Source/Scene/Azure2DImageryProvider.js @@ -1,4 +1,5 @@ import Check from "../Core/Check.js"; +import Frozen from "../Core/Frozen.js"; import Credit from "../Core/Credit.js"; import defined from "../Core/defined.js"; import Resource from "../Core/Resource.js"; @@ -29,7 +30,6 @@ const trailingSlashRegex = /\/$/; * * @alias Azure2DImageryProvider * @constructor - * @private * @param {Azure2DImageryProvider.ConstructorOptions} options Object describing initialization options * * @example @@ -41,16 +41,18 @@ const trailingSlashRegex = /\/$/; */ function Azure2DImageryProvider(options) { options = options ?? {}; - const maximumLevel = options.maximumLevel ?? 22; - const minimumLevel = options.minimumLevel ?? 0; const tilesetId = options.tilesetId ?? "microsoft.imagery"; + this._maximumLevel = options.maximumLevel ?? 22; + this._minimumLevel = options.minimumLevel ?? 0; - const subscriptionKey = + this._subscriptionKey = options.subscriptionKey ?? options["subscription-key"]; //>>includeStart('debug', pragmas.debug); - Check.defined("options.subscriptionKey", subscriptionKey); + Check.defined("options.subscriptionKey", this._subscriptionKey); //>>includeEnd('debug'); + this._tilesetId = options.tilesetId; + const resource = options.url instanceof IonResource ? options.url @@ -60,19 +62,23 @@ function Azure2DImageryProvider(options) { if (!trailingSlashRegex.test(templateUrl)) { templateUrl += "/"; } - templateUrl += `map/tile`; - resource.url = templateUrl; + const tilesUrl = `${templateUrl}map/tile`; + this._viewportUrl = `${templateUrl}map/attribution`; + + resource.url = tilesUrl; resource.setQueryParameters({ "api-version": "2024-04-01", tilesetId: tilesetId, + "subscription-key": this._subscriptionKey, zoom: `{z}`, x: `{x}`, y: `{y}`, - "subscription-key": subscriptionKey, }); + this._resource = resource; + let credit; if (defined(options.credit)) { credit = options.credit; @@ -83,8 +89,8 @@ function Azure2DImageryProvider(options) { const provider = new UrlTemplateImageryProvider({ ...options, - maximumLevel, - minimumLevel, + maximumLevel: this._maximumLevel, + minimumLevel: this._minimumLevel, url: resource, credit: credit, }); @@ -93,6 +99,7 @@ function Azure2DImageryProvider(options) { // This will be defined for ion resources this._tileCredits = resource.credits; + this._attributionsByLevel = undefined; } Object.defineProperties(Azure2DImageryProvider.prototype, { @@ -263,7 +270,18 @@ Object.defineProperties(Azure2DImageryProvider.prototype, { * @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); + 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); }; /** @@ -282,7 +300,21 @@ Azure2DImageryProvider.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; }; /** @@ -306,5 +338,57 @@ Azure2DImageryProvider.prototype.pickFeatures = function ( return undefined; }; +/** + * Get attribution for imagery from Azure Maps to display in the credits + * @private + * @return {Promise>} The list of attribution sources to display in the credits. + */ +Azure2DImageryProvider.prototype.getViewportCredits = async function () { + const maximumLevel = this._maximumLevel; + + const promises = []; + for (let level = 0; level < maximumLevel + 1; level++) { + promises.push( + fetchViewportAttribution( + this._resource, + this._viewportUrl, + this._subscriptionKey, + this._tilesetId, + 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].join(","); + if (attributions) { + const levelCredits = new Credit(attributions); + credits.push(levelCredits); + } + attributionsByLevel.set(level, credits); + } + + this._attributionsByLevel = attributionsByLevel; + + return attributionsByLevel; +}; + +async function fetchViewportAttribution(resource, url, key, tilesetId, level) { + const viewportResource = resource.getDerivedResource({ + url, + queryParameters: { + zoom: level, + bounds: "-180,-90,180,90", + }, + data: JSON.stringify(Frozen.EMPTY_OBJECT), + }); + + const viewportJson = await viewportResource.fetchJson(); + return viewportJson.copyrights; +} + // Exposed for tests export default Azure2DImageryProvider; diff --git a/packages/engine/Source/Scene/Google2DImageryProvider.js b/packages/engine/Source/Scene/Google2DImageryProvider.js index 27a812311c3f..45dbe10e2e02 100644 --- a/packages/engine/Source/Scene/Google2DImageryProvider.js +++ b/packages/engine/Source/Scene/Google2DImageryProvider.js @@ -101,6 +101,8 @@ function Google2DImageryProvider(options) { key: encodeURIComponent(options.key), }); + this._resource = resource.clone(); + let credit; if (defined(options.credit)) { credit = options.credit; @@ -312,7 +314,7 @@ Object.defineProperties(Google2DImageryProvider.prototype, { * }); * @example * // Google 2D roadmap overlay with custom styles - * const googleTileProvider = Cesium.Google2DImageryProvider.fromIonAssetId({ + * const googleTilesProvider = Cesium.Google2DImageryProvider.fromIonAssetId({ * assetId: 3830184, * overlayLayerType: "layerRoadmap", * styles: [ @@ -403,7 +405,7 @@ Google2DImageryProvider.fromIonAssetId = async function (options) { * // Google 2D roadmap overlay with custom styles * Cesium.GoogleMaps.defaultApiKey = "your-api-key"; * - * const googleTileProvider = Cesium.Google2DImageryProvider.fromUrl({ + * const googleTilesProvider = Cesium.Google2DImageryProvider.fromUrl({ * overlayLayerType: "layerRoadmap", * styles: [ * { @@ -533,12 +535,7 @@ Google2DImageryProvider.prototype.getViewportCredits = async function () { const promises = []; for (let level = 0; level < maximumLevel + 1; level++) { promises.push( - fetchViewportAttribution( - this._viewportUrl, - this._key, - this._session, - level, - ), + fetchViewportAttribution(this._resource, this._viewportUrl, level), ); } const results = await Promise.all(promises); @@ -559,12 +556,10 @@ Google2DImageryProvider.prototype.getViewportCredits = async function () { return attributionsByLevel; }; -async function fetchViewportAttribution(url, key, session, level) { - const viewport = await Resource.fetch({ - url: url, +async function fetchViewportAttribution(resource, url, level) { + const viewportResource = resource.getDerivedResource({ + url, queryParameters: { - key, - session, zoom: level, north: 90, south: -90, @@ -573,7 +568,7 @@ async function fetchViewportAttribution(url, key, session, level) { }, data: JSON.stringify(Frozen.EMPTY_OBJECT), }); - const viewportJson = JSON.parse(viewport); + const viewportJson = await viewportResource.fetchJson(); return viewportJson.copyright; } diff --git a/packages/engine/Specs/Scene/Azure2DImageryProviderSpec.js b/packages/engine/Specs/Scene/Azure2DImageryProviderSpec.js index bef5c5d85dcd..d40525926641 100644 --- a/packages/engine/Specs/Scene/Azure2DImageryProviderSpec.js +++ b/packages/engine/Specs/Scene/Azure2DImageryProviderSpec.js @@ -40,8 +40,10 @@ describe("Scene/Azure2DImageryProvider", function () { tilesetId: "a-tileset-id", }); + provider._attributionsByLevel = {}; + 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", + "https://atlas.microsoft.com/map/tile?api-version=2024-04-01&tilesetId=a-tileset-id&subscription-key=test-subscriptionKey&zoom={z}&x={x}&y={y}", ); expect(provider.tileWidth).toEqual(256); expect(provider.tileHeight).toEqual(256); @@ -74,6 +76,8 @@ describe("Scene/Azure2DImageryProvider", function () { rectangle: rectangle, }); + provider._attributionsByLevel = {}; + expect(provider.tileWidth).toEqual(256); expect(provider.tileHeight).toEqual(256); expect(provider.maximumLevel).toBe(22); @@ -133,6 +137,7 @@ describe("Scene/Azure2DImageryProvider", function () { subscriptionKey: "test-subscriptionKey", tilesetId: "a-tileset-id", }); + provider._attributionsByLevel = {}; const layer = new ImageryLayer(provider); diff --git a/packages/sandcastle/gallery/imagery-layers/main.js b/packages/sandcastle/gallery/imagery-layers/main.js index 591d0ad00ad5..4b6f9d1f6fe4 100644 --- a/packages/sandcastle/gallery/imagery-layers/main.js +++ b/packages/sandcastle/gallery/imagery-layers/main.js @@ -1,9 +1,9 @@ import * as Cesium from "cesium"; const viewer = new Cesium.Viewer("cesiumContainer", { - baseLayer: Cesium.ImageryLayer.fromWorldImagery({ - style: Cesium.IonWorldImageryStyle.AERIAL_WITH_LABELS, - }), + baseLayer: Cesium.ImageryLayer.fromProviderAsync( + Cesium.IonImageryProvider.fromAssetId(3830183), + ), baseLayerPicker: false, }); const layers = viewer.scene.imageryLayers; diff --git a/packages/widgets/Source/BaseLayerPicker/createDefaultImageryProviderViewModels.js b/packages/widgets/Source/BaseLayerPicker/createDefaultImageryProviderViewModels.js index a999c31b23af..8f7c807fcbfc 100644 --- a/packages/widgets/Source/BaseLayerPicker/createDefaultImageryProviderViewModels.js +++ b/packages/widgets/Source/BaseLayerPicker/createDefaultImageryProviderViewModels.js @@ -347,30 +347,42 @@ of the world.\nhttp://www.openstreetmap.org", providerViewModels.push( new ProviderViewModel({ - name: "Google Maps Labels Only", + name: "Google Maps Contour", iconUrl: buildModuleUrl( - "Widgets/Images/ImageryProviders/googleLabels.png", + "Widgets/Images/ImageryProviders/googleContour.png", ), tooltip: - "Place labels from Google Maps to combine with other imagery such as Sentinel-2", + "Hillshade mapping, contour lines, natural features (roadmap features hidden) from Google Maps", category: "Cesium ion", creationFunction: function () { - return IonImageryProvider.fromAssetId(3830185); + return IonImageryProvider.fromAssetId(3830186); }, }), ); providerViewModels.push( new ProviderViewModel({ - name: "Google Maps Contour", + name: "Azure Maps Aerial", iconUrl: buildModuleUrl( - "Widgets/Images/ImageryProviders/googleContour.png", + "Widgets/Images/ImageryProviders/azureAerial.png", ), + tooltip: "Imagery from Azure Maps", + category: "Cesium ion", + creationFunction: function () { + return IonImageryProvider.fromAssetId(3891168); + }, + }), + ); + + providerViewModels.push( + new ProviderViewModel({ + name: "Azure Maps Roads", + iconUrl: buildModuleUrl("Widgets/Images/ImageryProviders/azureRoads.png"), tooltip: - "Hillshade mapping, contour lines, natural features (roadmap features hidden) from Google Maps", + "Labeled roads and other features on a base landscape from Azure Maps", category: "Cesium ion", creationFunction: function () { - return IonImageryProvider.fromAssetId(3830186); + return IonImageryProvider.fromAssetId(3891169); }, }), ); diff --git a/packages/widgets/Source/Images/ImageryProviders/azureAerial.png b/packages/widgets/Source/Images/ImageryProviders/azureAerial.png new file mode 100644 index 000000000000..7542b42e170f Binary files /dev/null and b/packages/widgets/Source/Images/ImageryProviders/azureAerial.png differ diff --git a/packages/widgets/Source/Images/ImageryProviders/azureRoads.png b/packages/widgets/Source/Images/ImageryProviders/azureRoads.png new file mode 100644 index 000000000000..80d3f2976c89 Binary files /dev/null and b/packages/widgets/Source/Images/ImageryProviders/azureRoads.png differ diff --git a/packages/widgets/Source/Images/ImageryProviders/googleLabels.png b/packages/widgets/Source/Images/ImageryProviders/googleLabels.png deleted file mode 100644 index 7f8cf65a17aa..000000000000 Binary files a/packages/widgets/Source/Images/ImageryProviders/googleLabels.png and /dev/null differ