From 1d63524dfdd125c6e7a4fdae848a5376154bf60c Mon Sep 17 00:00:00 2001 From: eastagiletracker <310448263+eastagiletracker@users.noreply.github.com> Date: Sat, 5 Sep 2026 06:07:05 +0700 Subject: [PATCH] Support WMTS services that omit the optional ows:OperationsMetadata getOperations dereferenced Capabilities["ows:OperationsMetadata"]["ows:Operation"] unconditionally, so a WMTS GetCapabilities document without that optional section made the catalog search throw a TypeError and list no layers at all. getOperations now returns an empty array when the section is absent, and getRequestEncoding falls back to RESTful when the layers declare a tile ResourceURL, keeping the previous KVP default otherwise. --- web/client/api/__tests__/WMTS-test.js | 21 +++++ web/client/api/catalog/__tests__/WMTS-test.js | 19 ++++- ...pabilities-rest-no-operations-metadata.xml | 83 +++++++++++++++++++ web/client/utils/WMTSUtils.js | 22 ++++- web/client/utils/__tests__/WMTSUtils-test.js | 22 +++++ 5 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 web/client/test-resources/wmts/GetCapabilities-rest-no-operations-metadata.xml diff --git a/web/client/api/__tests__/WMTS-test.js b/web/client/api/__tests__/WMTS-test.js index 500df1d3d3b..1c2bd097771 100644 --- a/web/client/api/__tests__/WMTS-test.js +++ b/web/client/api/__tests__/WMTS-test.js @@ -10,6 +10,7 @@ import expect from 'expect'; import API, { getLayerTileMatrixSetsInfo, parseUrl } from '../WMTS'; import { getGetTileURL } from '../../utils/WMTSUtils'; +import { castArray } from 'lodash'; import MockAdapter from 'axios-mock-adapter'; import axios from '../../libs/ajax'; let mockAxios; @@ -58,6 +59,26 @@ describe('Test correctness of the WMTS APIs', () => { } }); }); + it('GetRecords RESTful without OperationsMetadata', (done) => { + API.getRecords('base/web/client/test-resources/wmts/GetCapabilities-rest-no-operations-metadata.xml', 0, 2, '').then((result) => { + try { + expect(result).toExist(); + expect(result.numberOfRecordsMatched).toBe(2); + result.records.map(record => { + expect(record.requestEncoding).toBe('RESTful'); + expect(record.queryable).toBe(false); + expect(record.GetTileURL).toNotExist(); + expect(getGetTileURL(record)).toEqual(castArray(record.ResourceURL).map(({ $: v }) => v.template)); + }); + expect(result.records[0].style).toBe("default"); + expect(result.records[0].format).toBe("image/png"); + expect(getGetTileURL(result.records[0])).toEqual(["https://maps.sampleServer.org/basemap/baselayer/{TileMatrixSet}/{TileMatrix}/{TileCol}/{TileRow}.png"]); + done(); + } catch (ex) { + done(ex); + } + }).catch(done); + }); it('GetRecords KVP for GeoServer 2.15', (done) => { // GS 2.15 has ResourceURLs together with KVP. This checks that the proper URL is returned by getTileURL, used to generate the layer. See #3796 API.getRecords('base/web/client/test-resources/wmts/GetCapabilities-1.0.0_gs_2.15.xml', 0, 3, '').then((result) => { diff --git a/web/client/api/catalog/__tests__/WMTS-test.js b/web/client/api/catalog/__tests__/WMTS-test.js index 2845ee43199..d8f99be4364 100644 --- a/web/client/api/catalog/__tests__/WMTS-test.js +++ b/web/client/api/catalog/__tests__/WMTS-test.js @@ -8,7 +8,7 @@ import expect from 'expect'; -import { getCatalogRecords } from '../WMTS'; +import { getCatalogRecords, getLayerFromRecord, textSearch } from '../WMTS'; describe('Test correctness of the WMTS APIs', () => { it('wmts', () => { @@ -142,6 +142,23 @@ describe('Test correctness of the WMTS APIs', () => { expect(records.length).toBe(1); expect(records[0].references[0].url).toBe(undefined); }); + it('wmts layer from a RESTful service without OperationsMetadata', (done) => { + textSearch('base/web/client/test-resources/wmts/GetCapabilities-rest-no-operations-metadata.xml', 1, 2, '').then((result) => { + try { + const records = getCatalogRecords(result, {}); + expect(records.length).toBe(2); + expect(records[0].requestEncoding).toBe('RESTful'); + const layer = getLayerFromRecord(records[0]); + expect(layer.type).toBe('wmts'); + expect(layer.name).toBe('baselayer'); + expect(layer.requestEncoding).toBe('RESTful'); + expect(layer.url).toBe('https://maps.sampleServer.org/basemap/baselayer/{TileMatrixSet}/{TileMatrix}/{TileCol}/{TileRow}.png'); + done(); + } catch (ex) { + done(ex); + } + }).catch(done); + }); it('wmts capabilities url', () => { const wmtsRecords = [{ GetTileURL: "tileURL" }]; const records = getCatalogRecords({ records: wmtsRecords }); diff --git a/web/client/test-resources/wmts/GetCapabilities-rest-no-operations-metadata.xml b/web/client/test-resources/wmts/GetCapabilities-rest-no-operations-metadata.xml new file mode 100644 index 00000000000..4db27a124d3 --- /dev/null +++ b/web/client/test-resources/wmts/GetCapabilities-rest-no-operations-metadata.xml @@ -0,0 +1,83 @@ + + + + sample server without operations metadata + RESTful WMTS that omits the optional ows:OperationsMetadata section + OGC WMTS + 1.0.0 + none + none + + + City of Nowhere + + + + + Base Layer + Base layer of the sample server + + -180.0 -90.0 + 180.0 90.0 + + baselayer + + image/png + + google3857 + + + + + Overlay Layer + Overlay layer of the sample server + + -180.0 -90.0 + 180.0 90.0 + + overlaylayer + + image/png + + google3857 + + + + + google3857 + urn:ogc:def:crs:EPSG:6.18.3:3857 + + 0 + 559082264.029 + -20037508.3428 20037508.3428 + 256 + 256 + 1 + 1 + + + 1 + 279541132.014 + -20037508.3428 20037508.3428 + 256 + 256 + 2 + 2 + + + 2 + 139770566.007 + -20037508.3428 20037508.3428 + 256 + 256 + 4 + 4 + + + + + diff --git a/web/client/utils/WMTSUtils.js b/web/client/utils/WMTSUtils.js index bd1c67e1641..250b9df0bbd 100644 --- a/web/client/utils/WMTSUtils.js +++ b/web/client/utils/WMTSUtils.js @@ -56,9 +56,26 @@ export const getTileMatrixSet = (tileMatrixSet, srs, allowedSRS, matrixIds = {}, */ export const getRequestEncoding = json => { const operations = WMTSUtils.getOperations(json); + if (operations.length === 0) { + // ows:OperationsMetadata is optional, so the encoding can only be deduced + // from the layers: a tile ResourceURL is what a RESTful service advertises + return WMTSUtils.hasTileResourceURL(json) ? "RESTful" : "KVP"; + } return WMTSUtils.getOperation(operations, "GetTile", "KVP") ? "KVP" : WMTSUtils.getOperation(operations, "GetTile", "RESTful") && "RESTful"; }; -export const getOperations = (json = {}) => castArray(json.Capabilities["ows:OperationsMetadata"]["ows:Operation"]); +/** + * Gets the 'Operations' array from the WMTS Capabilities json parsed object. + * Returns an empty array when the optional 'ows:OperationsMetadata' section is missing + */ +export const getOperations = (json = {}) => { + const operations = json?.Capabilities?.["ows:OperationsMetadata"]?.["ows:Operation"]; + return operations ? castArray(operations) : []; +}; +/** + * Checks if at least one layer of the WMTS Capabilities json parsed object declares a tile ResourceURL + */ +export const hasTileResourceURL = (json = {}) => castArray(json?.Capabilities?.Contents?.Layer || []) + .some((layer) => castArray(layer.ResourceURL || []).some((resource) => resource?.$?.resourceType === "tile")); /** * gets the first operation of the type and with the name provided from the 'Operations' array of the WMTS Capabilities json parsed object */ @@ -213,5 +230,6 @@ export const generateGeoServerWMTSUrl = (options) => { WMTSUtils = { getOperations, - getOperation + getOperation, + hasTileResourceURL }; diff --git a/web/client/utils/__tests__/WMTSUtils-test.js b/web/client/utils/__tests__/WMTSUtils-test.js index 4d6d4f816ae..999656e7b8c 100644 --- a/web/client/utils/__tests__/WMTSUtils-test.js +++ b/web/client/utils/__tests__/WMTSUtils-test.js @@ -11,6 +11,7 @@ import xml2js from 'xml2js'; import * as WMTSUtils from '../WMTSUtils'; import restCapabilities from 'raw-loader!../../test-resources/wmts/GetCapabilities-rest.xml'; import kvpCapabilities from 'raw-loader!../../test-resources/wmts/GetCapabilities-1.0.0.xml'; +import restCapabilitiesNoOperationsMetadata from 'raw-loader!../../test-resources/wmts/GetCapabilities-rest-no-operations-metadata.xml'; describe('Test the WMTSUtils', () => { it('get matrix ids with object', () => { @@ -54,6 +55,27 @@ describe('Test the WMTSUtils', () => { done(); }); }); + it('wmts rest without OperationsMetadata', (done) => { + xml2js.parseString(restCapabilitiesNoOperationsMetadata, { explicitArray: false }, (ignore, json) => { + const operations = WMTSUtils.getOperations(json); + expect(operations).toEqual([]); + expect(WMTSUtils.getOperation(operations, "GetTile", "KVP")).toNotExist(); + expect(WMTSUtils.getOperation(operations, "GetTile", "RESTful")).toNotExist(); + // OperationsMetadata is optional, the RESTful encoding is advertised by the tile ResourceURL + expect(WMTSUtils.getRequestEncoding(json)).toBe("RESTful"); + const tileURLs = WMTSUtils.getGetTileURL({ ...json.Capabilities.Contents.Layer[0], requestEncoding: "RESTful" }); + expect(tileURLs).toEqual(["https://maps.sampleServer.org/basemap/baselayer/{TileMatrixSet}/{TileMatrix}/{TileCol}/{TileRow}.png"]); + done(); + }); + }); + it('getOperations with empty capabilities', () => { + expect(WMTSUtils.getOperations()).toEqual([]); + expect(WMTSUtils.getOperations({})).toEqual([]); + expect(WMTSUtils.getOperations({ Capabilities: {} })).toEqual([]); + }); + it('getRequestEncoding without OperationsMetadata and without tile ResourceURL', () => { + expect(WMTSUtils.getRequestEncoding({ Capabilities: { Contents: { Layer: { "ows:Identifier": "layer" } } } })).toBe("KVP"); + }); it('parseTileMatrixSetOption', () => { const layer = WMTSUtils.parseTileMatrixSetOption({ availableTileMatrixSets: {