diff --git a/CHANGES.md b/CHANGES.md index 5e130ddca95..d15a438526b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ - Added `token`, `mapServerData`, and `parameters` properties to `ArcGisMapServerImageryProvider.ConstructorOptions`. - Added `Ion.defaultTokenMessage` to customize the credit message shown on the map when using default Ion token. - Added split terrain feature. +- Added WMTS `GetFeatureInfo` support to `WebMapTileServiceImageryProvider`, including new constructor options and accompanying specs. ## 1.134 - 2025-10-01 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 8be2fa30647..f7c77e317e3 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -432,3 +432,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to Cesiu - [Pamela Augustine](https://github.com/pamelaAugustine) - [宋时旺](https://github.com/BlockCnFuture) - [Marco Zhan](https://github.com/marcoYxz) +- [Shubham Sharma](https://github.com/ShubhamSharmaFAO) diff --git a/packages/engine/Source/Scene/WebMapTileServiceImageryProvider.js b/packages/engine/Source/Scene/WebMapTileServiceImageryProvider.js index 8c4db5a4738..9d085720b9d 100644 --- a/packages/engine/Source/Scene/WebMapTileServiceImageryProvider.js +++ b/packages/engine/Source/Scene/WebMapTileServiceImageryProvider.js @@ -5,9 +5,11 @@ import defined from "../Core/defined.js"; import DeveloperError from "../Core/DeveloperError.js"; import Event from "../Core/Event.js"; import Resource from "../Core/Resource.js"; +import UrlTemplateImageryProvider from "./UrlTemplateImageryProvider.js"; import WebMercatorTilingScheme from "../Core/WebMercatorTilingScheme.js"; import ImageryProvider from "./ImageryProvider.js"; import TimeDynamicImagery from "./TimeDynamicImagery.js"; +import GetFeatureInfoFormat from "./GetFeatureInfoFormat.js"; const defaultParameters = Object.freeze({ service: "WMTS", @@ -40,6 +42,15 @@ const defaultParameters = Object.freeze({ * @property {string|string[]} [subdomains='abc'] The subdomains to use for the {s} placeholder in the URL template. * If this parameter is a single string, each character in the string is a subdomain. If it is * an array, each element in the array is a subdomain. + * @property {boolean} [enablePickFeatures=true] If true, {@link WebMapTileServiceImageryProvider#pickFeatures} will invoke + * the GetFeatureInfo operation on the WMTS server and return the features included in the response. If false, + * {@link WebMapTileServiceImageryProvider#pickFeatures} will immediately return undefined (indicating no pickable features) + * without communicating with the server. Set this property to false if you know your WMTS server does not support + * GetFeatureInfo or if you don't want this provider's features to be pickable. + * @property {GetFeatureInfoFormat[]} [getFeatureInfoFormats=WebMapTileServiceImageryProvider.DefaultGetFeatureInfoFormats] The formats + * in which to try WMTS GetFeatureInfo requests. + * @property {Resource|string} [getFeatureInfoUrl] The GetFeatureInfo URL of the WMTS service. If not specified, the value of url is used. + * @property {object} [getFeatureInfoParameters] Additional parameters to include in GetFeatureInfo requests. Keys are lowercased internally. */ /** @@ -220,6 +231,75 @@ function WebMapTileServiceImageryProvider(options) { } else { this._subdomains = ["a", "b", "c"]; } + + this._getFeatureInfoFormats = + options.getFeatureInfoFormats ?? + WebMapTileServiceImageryProvider.DefaultGetFeatureInfoFormats; + + this._enablePickFeatures = options.enablePickFeatures ?? true; + + this._getFeatureInfoUrl = options.getFeatureInfoUrl ?? options.url; + const pickFeatureResource = Resource.createIfNeeded(this._getFeatureInfoUrl); + + pickFeatureResource.setQueryParameters( + WebMapTileServiceImageryProvider.GetFeatureInfoDefaultParameters, + false, + ); + + if (defined(options.getFeatureInfoParameters)) { + pickFeatureResource.setQueryParameters( + objectToLowercase(options.getFeatureInfoParameters), + ); + } + + pickFeatureResource.setQueryParameters( + { + tilematrix: "{TileMatrix}", + tilerow: "{TileRow}", + tilecol: "{TileCol}", + tilematrixset: this._tileMatrixSetID, + layer: this._layer, + style: this._style, + infoformat: "{format}", + i: "{i}", + j: "{j}", + }, + false, + ); + + if (defined(this._dimensions)) { + pickFeatureResource.setQueryParameters(this._dimensions); + } + + pickFeatureResource.setTemplateValues( + { + layer: this._layer, + Layer: this._layer, + style: this._style, + Style: this._style, + TileMatrixSet: this._tileMatrixSetID, + }, + true, + ); + + const pickFeaturesCustomTags = createPickFeaturesCustomTags(this); + + this._pickFeaturesProvider = new UrlTemplateImageryProvider({ + url: this._resource.clone(), + pickFeaturesUrl: pickFeatureResource, + tilingScheme: this._tilingScheme, + rectangle: this._rectangle, + tileWidth: this._tileWidth, + tileHeight: this._tileHeight, + minimumLevel: this._minimumLevel, + maximumLevel: this._maximumLevel, + subdomains: this._subdomains, + getFeatureInfoFormats: this._getFeatureInfoFormats, + enablePickFeatures: this._enablePickFeatures, + customTags: pickFeaturesCustomTags, + }); + + this.enablePickFeatures = this._enablePickFeatures; } function requestImage(imageryProvider, col, row, level, request, interval) { @@ -320,6 +400,27 @@ Object.defineProperties(WebMapTileServiceImageryProvider.prototype, { return this._tileWidth; }, }, + /** + * Gets or sets a value indicating whether feature picking is enabled. If true, {@link WebMapTileServiceImageryProvider#pickFeatures} will + * invoke the GetFeatureInfo service on the WMTS server and attempt to interpret the features included in the response. If false, + * {@link WebMapTileServiceImageryProvider#pickFeatures} will immediately return undefined (indicating no pickable + * features) without communicating with the server. Set this property to false if you know your data + * source does not support picking features or if you don't want this provider's features to be pickable. + * @memberof WebMapTileServiceImageryProvider.prototype + * @type {boolean} + * @default true + */ + enablePickFeatures: { + get: function () { + return this._enablePickFeatures; + }, + set: function (enablePickFeatures) { + this._enablePickFeatures = enablePickFeatures; + if (defined(this._pickFeaturesProvider)) { + this._pickFeaturesProvider.enablePickFeatures = enablePickFeatures; + } + }, + }, /** * Gets the height of each tile, in pixels. @@ -434,6 +535,18 @@ Object.defineProperties(WebMapTileServiceImageryProvider.prototype, { }, }, + /** + * Gets the GetFeatureInfo URL of the WMTS server. + * @memberof WebMapTileServiceImageryProvider.prototype + * @type {Resource|string} + * @readonly + */ + getFeatureInfoUrl: { + get: function () { + return this._getFeatureInfoUrl; + }, + }, + /** * 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 @@ -553,15 +666,19 @@ WebMapTileServiceImageryProvider.prototype.requestImage = function ( }; /** - * Picking features is not currently supported by this imagery provider, so this function simply returns - * undefined. + * Asynchronously determines what features, if any, are located at a given longitude and latitude within + * a tile. This function should not be called before {@link ImageryProvider#ready} returns true. * * @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. + * @return {Promise|undefined} A promise for the picked features that will resolve when the asynchronous + * picking completes. The resolved value is an array of {@link ImageryLayerFeatureInfo} + * instances. The array may be empty if no features are found at the given location. + * + * @exception {DeveloperError} pickFeatures must not be called before the imagery provider is ready. */ WebMapTileServiceImageryProvider.prototype.pickFeatures = function ( x, @@ -570,6 +687,91 @@ WebMapTileServiceImageryProvider.prototype.pickFeatures = function ( longitude, latitude, ) { - return undefined; + if ( + !this.enablePickFeatures || + this._getFeatureInfoFormats.length === 0 || + !defined(this._pickFeaturesProvider) + ) { + return undefined; + } + + const pickFeaturesProvider = this._pickFeaturesProvider; + pickFeaturesProvider.enablePickFeatures = this.enablePickFeatures; + + const pickResource = pickFeaturesProvider._pickFeaturesResource; + + if (defined(this._dimensions)) { + if (this._useKvp) { + pickResource.setQueryParameters(this._dimensions); + } else { + pickResource.setTemplateValues(this._dimensions); + } + } + + const timeDynamicImagery = this._timeDynamicImagery; + if (defined(timeDynamicImagery)) { + const currentInterval = timeDynamicImagery.currentInterval; + if (defined(currentInterval) && defined(currentInterval.data)) { + if (this._useKvp) { + pickResource.setQueryParameters(currentInterval.data); + } else { + pickResource.setTemplateValues(currentInterval.data); + } + } + } + + return pickFeaturesProvider.pickFeatures(x, y, level, longitude, latitude); }; + +WebMapTileServiceImageryProvider.GetFeatureInfoDefaultParameters = + Object.freeze({ + service: "WMTS", + version: "1.0.0", + request: "GetFeatureInfo", + }); + +WebMapTileServiceImageryProvider.DefaultGetFeatureInfoFormats = Object.freeze([ + Object.freeze(new GetFeatureInfoFormat("json", "application/json")), + Object.freeze(new GetFeatureInfoFormat("xml", "text/xml")), + Object.freeze(new GetFeatureInfoFormat("text", "text/html")), +]); + +function createPickFeaturesCustomTags(imageryProvider) { + function getTileMatrix(level) { + const labels = imageryProvider._tileMatrixLabels; + return defined(labels) ? labels[level] : level.toString(); + } + + return { + TileMatrix: function (provider, x, y, level) { + return getTileMatrix(level); + }, + tilematrix: function (provider, x, y, level) { + return getTileMatrix(level); + }, + TileRow: function (provider, x, y) { + return y.toString(); + }, + tilerow: function (provider, x, y) { + return y.toString(); + }, + TileCol: function (provider, x, y) { + return x.toString(); + }, + tilecol: function (provider, x, y) { + return x.toString(); + }, + }; +} + +function objectToLowercase(obj) { + const result = {}; + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + result[key.toLowerCase()] = obj[key]; + } + } + return result; +} + export default WebMapTileServiceImageryProvider; diff --git a/packages/engine/Specs/Scene/WebMapTileServiceImageryProviderSpec.js b/packages/engine/Specs/Scene/WebMapTileServiceImageryProviderSpec.js index f16abebc577..a0d43cbbbea 100644 --- a/packages/engine/Specs/Scene/WebMapTileServiceImageryProviderSpec.js +++ b/packages/engine/Specs/Scene/WebMapTileServiceImageryProviderSpec.js @@ -6,9 +6,11 @@ import { GeographicTilingScheme, Imagery, ImageryLayer, + ImageryLayerFeatureInfo, ImageryProvider, ImageryState, JulianDate, + GetFeatureInfoFormat, objectToQuery, queryToObject, Request, @@ -295,9 +297,10 @@ describe("Scene/WebMapTileServiceImageryProvider", function () { expect(provider.proxy).toBeUndefined(); }); - // non default parameters values it("uses parameters passed to constructor", function () { - const tilingScheme = new GeographicTilingScheme(); + const tilingScheme = new GeographicTilingScheme({ + numberOfLevelZeroTilesX: 1, + }); const rectangle = new WebMercatorTilingScheme().rectangle; const provider = new WebMapTileServiceImageryProvider({ layer: "someLayer", @@ -722,4 +725,411 @@ describe("Scene/WebMapTileServiceImageryProvider", function () { expect(lastUrl).toEqual(uri.toString()); }); }); + + it("returns undefined if getFeatureInfoFormats is empty", function () { + const provider = new WebMapTileServiceImageryProvider({ + layer: "someLayer", + style: "someStyle", + url: "http://wmts.invalid", + tileMatrixSetID: "someTMS", + getFeatureInfoFormats: [], + }); + + expect(provider.pickFeatures(0, 0, 0, 0.0, 0.0)).toBeUndefined(); + }); + + it("returns undefined if enablePickFeatures is false", function () { + const provider = new WebMapTileServiceImageryProvider({ + layer: "someLayer", + style: "someStyle", + url: "http://wmts.invalid", + tileMatrixSetID: "someTMS", + }); + + provider.enablePickFeatures = false; + expect(provider.enablePickFeatures).toBe(false); + expect(provider.pickFeatures(0, 0, 0, 0.0, 0.0)).toBeUndefined(); + }); + + it("initializes GetFeatureInfo picking wiring (KVP)", function () { + const provider = new WebMapTileServiceImageryProvider({ + layer: "someLayer", + style: "someStyle", + url: "http://wmts.invalid", + tileMatrixSetID: "someTMS", + }); + + expect(provider._resource).toBeDefined(); + expect(provider._layer).toBeDefined(); + expect(provider._style).toBeDefined(); + expect(provider._tileMatrixSetID).toBeDefined(); + expect(provider._pickFeaturesProvider).toBeDefined(); + expect(provider._pickFeaturesProvider._pickFeaturesResource).toBeDefined(); + expect( + provider._getFeatureInfoFormats && provider._getFeatureInfoFormats.length, + ).toBeGreaterThan(0); + expect(provider.enablePickFeatures).toBe(true); + expect(provider.getFeatureInfoUrl).toBeDefined(); + }); + + it("initializes GetFeatureInfo picking wiring (REST template)", function () { + const provider = new WebMapTileServiceImageryProvider({ + layer: "someLayer", + style: "someStyle", + url: "http://wmts.invalid/{Style}/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}.png", + tileMatrixSetID: "someTMS", + }); + + expect(provider._resource).toBeDefined(); + expect(provider._layer).toBeDefined(); + expect(provider._style).toBeDefined(); + expect(provider._tileMatrixSetID).toBeDefined(); + expect(provider._pickFeaturesProvider).toBeDefined(); + expect(provider._pickFeaturesProvider._pickFeaturesResource).toBeDefined(); + expect( + provider._getFeatureInfoFormats && provider._getFeatureInfoFormats.length, + ).toBeGreaterThan(0); + expect(provider.enablePickFeatures).toBe(true); + expect(provider.getFeatureInfoUrl).toBeDefined(); + }); + + it("does not return undefined when enablePickFeatures is toggled to true", function () { + const provider = new WebMapTileServiceImageryProvider({ + layer: "someLayer", + style: "someStyle", + url: "http://wmts.invalid", + tileMatrixSetID: "someTMS", + }); + + provider.enablePickFeatures = false; + + spyOn(Resource.prototype, "fetchJson").and.returnValue( + Promise.resolve({ + type: "FeatureCollection", + features: [], + }), + ); + + provider.enablePickFeatures = true; + expect(provider.enablePickFeatures).toBe(true); + + const pickPromise = provider.pickFeatures(0, 0, 0, 0.0, 0.0); + expect(pickPromise).toBeDefined(); + + return pickPromise.then(function (features) { + expect(Array.isArray(features)).toBe(true); + expect(features.length).toBe(0); + }); + }); + + it("builds GetFeatureInfo request and parses JSON response", function () { + const tilingScheme = new GeographicTilingScheme({ + numberOfLevelZeroTilesX: 1, + }); + const provider = new WebMapTileServiceImageryProvider({ + layer: "someLayer", + style: "someStyle", + url: "http://wmts.invalid", + tileMatrixSetID: "someTMS", + tileMatrixLabels: ["level-zero"], + tilingScheme: tilingScheme, + format: "image/png", + getFeatureInfoFormats: [new GetFeatureInfoFormat("json")], + }); + + spyOn(Resource.prototype, "fetchJson").and.callFake(function () { + const url = this.getUrlComponent(true); + const uri = new Uri(url); + const params = queryToObject(uri.query()); + expect(params.request).toBe("GetFeatureInfo"); + expect(params.service).toBe("WMTS"); + expect(params.version).toBe("1.0.0"); + expect(params.layer).toBe("someLayer"); + expect(params.style).toBe("someStyle"); + expect(params.tilematrixset).toBe("someTMS"); + expect(params.tilematrix).toBe("level-zero"); + expect(params.tilecol).toBe("0"); + expect(params.tilerow).toBe("0"); + expect(params.i).toBe("128"); + expect(params.j).toBe("128"); + expect(params.infoformat).toBe("application/json"); + + return Promise.resolve({ + type: "FeatureCollection", + features: [ + { + type: "Feature", + geometry: { + type: "Point", + coordinates: [0, 0], + }, + properties: { + name: "Feature name", + }, + }, + ], + }); + }); + + return provider.pickFeatures(0, 0, 0, 0.0, 0.0).then(function (features) { + expect(Resource.prototype.fetchJson).toHaveBeenCalled(); + expect(features.length).toBe(1); + + const featureInfo = features[0]; + expect(featureInfo).toBeInstanceOf(ImageryLayerFeatureInfo); + expect(featureInfo.name).toBe("Feature name"); + expect(featureInfo.position.longitude).toBeCloseTo(0, 10); + expect(featureInfo.position.latitude).toBeCloseTo(0, 10); + }); + }); + + it("uses getFeatureInfoUrl in options for picking", function () { + const featureUrl = "http://wmts.invalid/feature"; + const provider = new WebMapTileServiceImageryProvider({ + layer: "someLayer", + style: "someStyle", + url: "http://wmts.invalid", + tileMatrixSetID: "someTMS", + getFeatureInfoUrl: featureUrl, + getFeatureInfoFormats: [new GetFeatureInfoFormat("json")], + }); + + spyOn(Resource.prototype, "fetchJson").and.callFake(function () { + const url = this.getUrlComponent(true); + expect(url.indexOf(featureUrl)).toBe(0); + return Promise.resolve({ type: "FeatureCollection", features: [] }); + }); + + return provider.pickFeatures(0, 0, 0, 0.0, 0.0).then(function (features) { + expect(Array.isArray(features)).toBe(true); + }); + }); + + it("includes dimensions in GetFeatureInfo KVP requests", function () { + let capturedUrl; + spyOn(Resource.prototype, "fetchJson").and.callFake(function () { + capturedUrl = this.getUrlComponent(true); + return Promise.resolve({ type: "FeatureCollection", features: [] }); + }); + + const provider = new WebMapTileServiceImageryProvider({ + layer: "someLayer", + style: "someStyle", + url: "http://wmts.invalid/kvp", + tileMatrixSetID: "someTMS", + dimensions: { FOO: "BAR" }, + getFeatureInfoFormats: [new GetFeatureInfoFormat("json")], + }); + + return provider.pickFeatures(0, 0, 0, 0.0, 0.0).then(function () { + const uri = new Uri(capturedUrl); + const params = queryToObject(uri.query()); + expect(params.FOO).toBe("BAR"); + }); + }); + + it("includes dimensions in GetFeatureInfo RESTful requests via template values", function () { + let capturedUrl; + spyOn(Resource.prototype, "fetchJson").and.callFake(function () { + capturedUrl = this.getUrlComponent(true); + return Promise.resolve({ type: "FeatureCollection", features: [] }); + }); + + const provider = new WebMapTileServiceImageryProvider({ + layer: "someLayer", + style: "someStyle", + url: "http://wmts.invalid/{FOO}", + tileMatrixSetID: "someTMS", + dimensions: { FOO: "BAR" }, + getFeatureInfoFormats: [new GetFeatureInfoFormat("json")], + }); + + return provider.pickFeatures(0, 0, 0, 0.0, 0.0).then(function () { + expect(capturedUrl).toStartWith("http://wmts.invalid/BAR"); + }); + }); + + it("includes time-dynamic template values in GetFeatureInfo RESTful requests", function () { + let capturedUrl; + spyOn(Resource.prototype, "fetchJson").and.callFake(function () { + capturedUrl = this.getUrlComponent(true); + return Promise.resolve({ type: "FeatureCollection", features: [] }); + }); + + const times = TimeIntervalCollection.fromIso8601({ + iso8601: "2017-04-26/2017-04-30/P1D", + dataCallback: function (interval, index) { + return { Time: JulianDate.toIso8601(interval.start) }; + }, + }); + const clock = new Clock({ + currentTime: JulianDate.fromIso8601("2017-04-26"), + shouldAnimate: false, + }); + + const provider = new WebMapTileServiceImageryProvider({ + layer: "someLayer", + style: "someStyle", + url: "http://wmts.invalid/{Time}", + tileMatrixSetID: "someTMS", + clock: clock, + times: times, + getFeatureInfoFormats: [new GetFeatureInfoFormat("json")], + }); + + return provider.pickFeatures(0, 0, 0, 0.0, 0.0).then(function () { + expect(decodeURIComponent(capturedUrl)).toContain( + "/2017-04-26T00:00:00Z", + ); + }); + }); + + it("applies getFeatureInfoParameters with lowercased keys", function () { + let capturedUrl; + spyOn(Resource.prototype, "fetchJson").and.callFake(function () { + capturedUrl = this.getUrlComponent(true); + return Promise.resolve({ type: "FeatureCollection", features: [] }); + }); + + const provider = new WebMapTileServiceImageryProvider({ + layer: "someLayer", + style: "someStyle", + url: "http://wmts.invalid", + tileMatrixSetID: "someTMS", + getFeatureInfoParameters: { CUSTOM_PARAM: "foo" }, + getFeatureInfoFormats: [new GetFeatureInfoFormat("json")], + }); + + return provider.pickFeatures(0, 0, 0, 0.0, 0.0).then(function () { + const uri = new Uri(capturedUrl); + const params = queryToObject(uri.query()); + expect(params.custom_param).toBe("foo"); + }); + }); + + it("includes time-dynamic parameters in GetFeatureInfo requests", function () { + let capturedUrl; + spyOn(Resource.prototype, "fetchJson").and.callFake(function () { + capturedUrl = this.getUrlComponent(true); + return Promise.resolve({ type: "FeatureCollection", features: [] }); + }); + + const times = TimeIntervalCollection.fromIso8601({ + iso8601: "2017-04-26/2017-04-30/P1D", + dataCallback: function (interval, index) { + return { Time: JulianDate.toIso8601(interval.start) }; + }, + }); + const clock = new Clock({ + currentTime: JulianDate.fromIso8601("2017-04-26"), + shouldAnimate: false, + }); + + const provider = new WebMapTileServiceImageryProvider({ + layer: "someLayer", + style: "someStyle", + url: "http://wmts.invalid", + tileMatrixSetID: "someTMS", + clock: clock, + times: times, + getFeatureInfoFormats: [new GetFeatureInfoFormat("json")], + }); + + return provider.pickFeatures(0, 0, 0, 0.0, 0.0).then(function () { + const uri = new Uri(capturedUrl); + const params = queryToObject(uri.query()); + expect(params.Time).toBe("2017-04-26T00:00:00Z"); + }); + }); + + it("sets correct infoformat for XML and HTML requests", function () { + const providerXml = new WebMapTileServiceImageryProvider({ + layer: "someLayer", + style: "someStyle", + url: "http://wmts.invalid", + tileMatrixSetID: "someTMS", + getFeatureInfoFormats: [new GetFeatureInfoFormat("xml")], + }); + + spyOn(Resource.prototype, "fetchXML").and.callFake(function () { + const url = this.getUrlComponent(true); + const uri = new Uri(url); + const params = queryToObject(uri.query()); + expect(params.infoformat).toBe("text/xml"); + const parser = new DOMParser(); + return Promise.resolve( + parser.parseFromString("", "application/xml"), + ); + }); + + const providerHtml = new WebMapTileServiceImageryProvider({ + layer: "someLayer", + style: "someStyle", + url: "http://wmts.invalid", + tileMatrixSetID: "someTMS", + getFeatureInfoFormats: [new GetFeatureInfoFormat("html")], + }); + + spyOn(Resource.prototype, "fetchText").and.callFake(function () { + const url = this.getUrlComponent(true); + const uri = new Uri(url); + const params = queryToObject(uri.query()); + expect(params.infoformat).toBe("text/html"); + return Promise.resolve("x"); + }); + + return providerXml + .pickFeatures(0, 0, 0, 0.0, 0.0) + .then(function () { + return providerHtml.pickFeatures(0, 0, 0, 0.0, 0.0); + }) + .then(function () { + expect(true).toBe(true); + }); + }); + + it("falls back to the next format when the first request fails", function () { + function fallbackProcessor(response) { + expect(response.custom).toBe(true); + const feature = new ImageryLayerFeatureInfo(); + feature.name = "Fallback feature"; + return [feature]; + } + + const formats = [ + new GetFeatureInfoFormat("json"), + new GetFeatureInfoFormat("foo", "application/foo", fallbackProcessor), + ]; + + const provider = new WebMapTileServiceImageryProvider({ + layer: "someLayer", + style: "someStyle", + url: "http://wmts.invalid", + tileMatrixSetID: "someTMS", + tilingScheme: new GeographicTilingScheme(), + getFeatureInfoFormats: formats, + }); + + spyOn(Resource.prototype, "fetchJson").and.returnValue( + Promise.reject(new Error("no json")), + ); + + spyOn(Resource.prototype, "fetch").and.callFake(function (options) { + const url = (options && options.url) || this.getUrlComponent(true); + const uri = new Uri(url); + const params = queryToObject(uri.query()); + expect(params.infoformat).toBe("application/foo"); + expect(options.responseType).toBe("application/foo"); + return Promise.resolve({ + custom: true, + }); + }); + + return provider.pickFeatures(0, 0, 0, 0.0, 0.0).then(function (features) { + expect(Resource.prototype.fetchJson).toHaveBeenCalled(); + expect(Resource.prototype.fetch).toHaveBeenCalled(); + expect(features.length).toBe(1); + expect(features[0].name).toBe("Fallback feature"); + }); + }); });