From ee641dfe6d7ee81d1fe4a47e8a03681e6ab9959a Mon Sep 17 00:00:00 2001 From: Edward Nash Date: Mon, 23 Sep 2024 16:43:59 +0200 Subject: [PATCH 1/6] Read operation URLs from WMS capabilities WMS Capabilities documents self-report the URLs to be used for operations. These self-reported URLs should be used rather than the URL used to retrieve the capabiliites. --- src/shared/models.ts | 4 +++ src/wms/capabilities.spec.ts | 40 ++++++++++++++++++++++++ src/wms/capabilities.ts | 59 +++++++++++++++++++++++++++++++----- src/wms/endpoint.spec.ts | 35 +++++++++++++++++++++ src/wms/endpoint.ts | 38 +++++++++++++++++++++-- src/worker/index.ts | 7 ++++- src/worker/worker.ts | 1 + 7 files changed, 173 insertions(+), 11 deletions(-) diff --git a/src/shared/models.ts b/src/shared/models.ts index c5eba38..438b83a 100644 --- a/src/shared/models.ts +++ b/src/shared/models.ts @@ -30,6 +30,10 @@ export interface FetchOptions { integrity?: string; } +export type OperationName = string; +export type HttpMethod = 'Get' | 'Post'; +export type OperationUrl = Partial>; + export interface LayerStyle { name: string; title: string; diff --git a/src/wms/capabilities.spec.ts b/src/wms/capabilities.spec.ts index 6fb4202..26d518c 100644 --- a/src/wms/capabilities.spec.ts +++ b/src/wms/capabilities.spec.ts @@ -1,6 +1,7 @@ import { readInfoFromCapabilities, readLayersFromCapabilities, + readOperationUrlsFromCapabilities, readVersionFromCapabilities, } from './capabilities.js'; // @ts-expect-error ts-migrate(7016) @@ -318,4 +319,43 @@ describe('WMS capabilities', () => { expect(readInfoFromCapabilities(doc)).toEqual(expectedInfo); }); }); + + describe('readOperationUrlsFromCapabilities', () => { + const expectedUrls = { + GetCapabilities: { + Get: 'http://geoservices.brgm.fr/geologie?language=fre&', + Post: 'http://geoservices.brgm.fr/geologie?language=fre&', + }, + GetMap: { + Get: 'http://geoservices.brgm.fr/geologie?language=fre&', + Post: 'http://geoservices.brgm.fr/geologie?language=fre&', + }, + GetFeatureInfo: { + Get: 'http://geoservices.brgm.fr/geologie?language=fre&', + Post: 'http://geoservices.brgm.fr/geologie?language=fre&', + }, + DescribeLayer: { + Get: 'http://geoservices.brgm.fr/geologie?language=fre&', + Post: 'http://geoservices.brgm.fr/geologie?language=fre&', + }, + GetLegendGraphic: { + Get: 'http://geoservices.brgm.fr/geologie?language=fre&', + Post: 'http://geoservices.brgm.fr/geologie?language=fre&', + }, + GetStyles: { + Get: 'http://geoservices.brgm.fr/geologie?language=fre&', + Post: 'http://geoservices.brgm.fr/geologie?language=fre&', + }, + }; + + it('reads the operations URLs (1.3.0)', () => { + const doc = parseXmlString(capabilities130); + expect(readOperationUrlsFromCapabilities(doc)).toEqual(expectedUrls); + }); + + it('reads the operations URLs (1.1.1)', () => { + const doc = parseXmlString(capabilities111); + expect(readOperationUrlsFromCapabilities(doc)).toEqual(expectedUrls); + }); + }); }); diff --git a/src/wms/capabilities.ts b/src/wms/capabilities.ts index 71a9803..f9596d6 100644 --- a/src/wms/capabilities.ts +++ b/src/wms/capabilities.ts @@ -1,20 +1,46 @@ -import { - findChildElement, - findChildrenElement, - getElementAttribute, - getElementText, - getRootElement, -} from '../shared/xml-utils.js'; -import { hasInvertedCoordinates } from '../shared/crs-utils.js'; import { XmlDocument, XmlElement } from '@rgrove/parse-xml'; +import { hasInvertedCoordinates } from '../shared/crs-utils.js'; import { BoundingBox, CrsCode, GenericEndpointInfo, LayerStyle, + type OperationName, + type OperationUrl, } from '../shared/models.js'; +import { + findChildElement, + findChildrenElement, + getChildrenElement, + getElementAttribute, + getElementName, + getElementText, + getRootElement, + stripNamespace, +} from '../shared/xml-utils.js'; import { WmsLayerAttribution, WmsLayerFull, WmsVersion } from './model.js'; +/** + * Will read all operation URLs from the capabilities doc + * @param capabilitiesDoc Capabilities document + * @return The parsed operations URLs + */ +export function readOperationUrlsFromCapabilities( + capabilitiesDoc: XmlDocument +) { + const urls: Record = {}; + const capability = findChildElement( + getRootElement(capabilitiesDoc), + 'Capability' + ); + const request = findChildElement(capability, 'Request'); + getChildrenElement(request).forEach((operation) => { + const operationName = stripNamespace(getElementName(operation)); + urls[operationName] = parseOperation(operation); + }); + return urls; +} + /** * Will read a WMS version from the capabilities doc * @param capabilitiesDoc Capabilities document @@ -85,6 +111,23 @@ export function readInfoFromCapabilities( }; } +/** + * Parse an operation definition from a WMS capabilities (e.g. GetMap) + * @param operation Operation element + */ +function parseOperation(operation: XmlElement): OperationUrl { + const urls: OperationUrl = {}; + const dcpType = findChildrenElement(operation, 'DCPType'); + const http = dcpType.flatMap((d) => findChildElement(d, 'HTTP')); + const methods = http.flatMap((h) => getChildrenElement(h)); + methods.forEach((method) => { + const onlineResource = findChildElement(method, 'OnlineResource'); + const methodName = stripNamespace(getElementName(method)); + urls[methodName] = getElementAttribute(onlineResource, 'xlink:href'); + }); + return urls; +} + /** * Parse a layer in a capabilities doc */ diff --git a/src/wms/endpoint.spec.ts b/src/wms/endpoint.spec.ts index c6bfdf4..27d70de 100644 --- a/src/wms/endpoint.spec.ts +++ b/src/wms/endpoint.spec.ts @@ -216,4 +216,39 @@ describe('WmsEndpoint', () => { ); }); }); + + describe('#getCapabilitiesUrl', () => { + it('returns the URL used for the request before the capabilities are retrieved', async () => { + expect(endpoint.getCapabilitiesUrl()).toBe( + 'https://my.test.service/ogc/wms?aa=bb&SERVICE=WMS&REQUEST=GetCapabilities' + ); + await endpoint.isReady(); + }); + + it('returns the self-reported URL after the capabilities are retrieved', async () => { + await endpoint.isReady(); + expect(endpoint.getCapabilitiesUrl()).toBe( + 'http://geoservices.brgm.fr/geologie?language=fre&SERVICE=WMS&REQUEST=GetCapabilities' + ); + }); + }); + + describe('#getOperationUrl', () => { + it('returns NULL before the document is loaded', async () => { + expect(endpoint.getOperationUrl('GetMap')).toBeNull(); + await endpoint.isReady(); + }); + + it('returns undefined for a non-existant operation', async () => { + await endpoint.isReady(); + expect(endpoint.getOperationUrl('foo')).toBeUndefined(); + }); + + it('returns the correct URL for an existant operation', async () => { + await endpoint.isReady(); + expect(endpoint.getOperationUrl('GetMap')).toBe( + 'http://geoservices.brgm.fr/geologie?language=fre&' + ); + }); + }); }); diff --git a/src/wms/endpoint.ts b/src/wms/endpoint.ts index c46d4ba..ec9c792 100644 --- a/src/wms/endpoint.ts +++ b/src/wms/endpoint.ts @@ -5,7 +5,10 @@ import { BoundingBox, CrsCode, GenericEndpointInfo, + type HttpMethod, MimeType, + type OperationName, + type OperationUrl, } from '../shared/models.js'; import { WmsLayerFull, WmsLayerSummary, WmsVersion } from './model.js'; import { generateGetMapUrl } from './url.js'; @@ -18,6 +21,7 @@ export default class WmsEndpoint { private _capabilitiesPromise: Promise; private _info: GenericEndpointInfo | null; private _layers: WmsLayerFull[] | null; + private _url: Record; private _version: WmsVersion | null; /** @@ -38,9 +42,10 @@ export default class WmsEndpoint { 'WMS', 'CAPABILITIES', this._capabilitiesUrl - ).then(({ info, layers, version }) => { + ).then(({ info, layers, url, version }) => { this._info = info; this._layers = layers; + this._url = url; this._version = version; }); } @@ -159,7 +164,7 @@ export default class WmsEndpoint { // TODO: check supported output formats // TODO: check supported styles return generateGetMapUrl( - this._capabilitiesUrl, + this.getOperationUrl('GetMap') || this._capabilitiesUrl, this._version, layers.join(','), widthPx, @@ -170,4 +175,33 @@ export default class WmsEndpoint { styles !== undefined ? styles.join(',') : '' ); } + + /** + * Returns the Capabilities URL of the WMS + * + * This is the URL reported by the service if available, otherwise the URL + * passed to the constructor + */ + getCapabilitiesUrl() { + const baseUrl = this.getOperationUrl('GetCapabilities'); + if (!baseUrl) { + return this._capabilitiesUrl; + } + return setQueryParams(baseUrl, { + SERVICE: 'WMS', + REQUEST: 'GetCapabilities', + }); + } + + /** + * Returns the URL reported by the WMS for the given operation + * @param operationName e.g. GetMap, GetCapabilities, etc. + * @param method HTTP method + */ + getOperationUrl(operationName: OperationName, method: HttpMethod = 'Get') { + if (!this._url) { + return null; + } + return this._url[operationName]?.[method]; + } } diff --git a/src/worker/index.ts b/src/worker/index.ts index 6e0ff04..d048025 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -1,5 +1,9 @@ import { sendTaskRequest } from './utils.js'; -import { GenericEndpointInfo } from '../shared/models.js'; +import { + GenericEndpointInfo, + OperationName, + OperationUrl, +} from '../shared/models.js'; import { setFetchOptionsUpdateCallback } from '../shared/http-utils.js'; import { WmtsEndpointInfo, WmtsLayer, WmtsMatrixSet } from '../wmts/model.js'; import { @@ -43,6 +47,7 @@ function getWorkerInstance() { */ export function parseWmsCapabilities(capabilitiesUrl: string): Promise<{ version: WmsVersion; + url: Record; info: GenericEndpointInfo; layers: WmsLayerFull[]; }> { diff --git a/src/worker/worker.ts b/src/worker/worker.ts index dd8e1a7..feb9245 100644 --- a/src/worker/worker.ts +++ b/src/worker/worker.ts @@ -15,6 +15,7 @@ addTaskHandler('parseWmsCapabilities', globalThis, ({ url }: { url: string }) => queryXmlDocument(url).then((xmlDoc) => ({ info: wmsCapabilities.readInfoFromCapabilities(xmlDoc), layers: wmsCapabilities.readLayersFromCapabilities(xmlDoc), + url: wmsCapabilities.readOperationUrlsFromCapabilities(xmlDoc), version: wmsCapabilities.readVersionFromCapabilities(xmlDoc), })) ); From aad50a04f07091437ddf2e4583bfd5c97eda3f37 Mon Sep 17 00:00:00 2001 From: Edward Nash Date: Mon, 23 Sep 2024 16:44:29 +0200 Subject: [PATCH 2/6] Use self-reported GetMap-URL for test --- src/wms/endpoint.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wms/endpoint.spec.ts b/src/wms/endpoint.spec.ts index 27d70de..f5f031e 100644 --- a/src/wms/endpoint.spec.ts +++ b/src/wms/endpoint.spec.ts @@ -212,7 +212,7 @@ describe('WmsEndpoint', () => { outputFormat: 'image/png', }) ).toBe( - 'https://my.test.service/ogc/wms?aa=bb&SERVICE=WMS&REQUEST=GetMap&VERSION=1.3.0&LAYERS=layer1%2Clayer2&STYLES=&WIDTH=100&HEIGHT=200&FORMAT=image%2Fpng&CRS=EPSG%3A4326&BBOX=10%2C20%2C100%2C200' + 'http://geoservices.brgm.fr/geologie?language=fre&SERVICE=WMS&REQUEST=GetMap&VERSION=1.3.0&LAYERS=layer1%2Clayer2&STYLES=&WIDTH=100&HEIGHT=200&FORMAT=image%2Fpng&CRS=EPSG%3A4326&BBOX=10%2C20%2C100%2C200' ); }); }); From 8d94b6c2fc8ce45eea123c77b9315ef5fe8bc1cf Mon Sep 17 00:00:00 2001 From: Edward Nash Date: Mon, 23 Sep 2024 16:46:28 +0200 Subject: [PATCH 3/6] Skip testing results before WMS caps loaded Due to the mocking of the async loading of the capabilities the loading occurs instantaneously and the state before they are loaded is not tested correctly --- src/wms/endpoint.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wms/endpoint.spec.ts b/src/wms/endpoint.spec.ts index f5f031e..f14b958 100644 --- a/src/wms/endpoint.spec.ts +++ b/src/wms/endpoint.spec.ts @@ -218,7 +218,7 @@ describe('WmsEndpoint', () => { }); describe('#getCapabilitiesUrl', () => { - it('returns the URL used for the request before the capabilities are retrieved', async () => { + it.skip('returns the URL used for the request before the capabilities are retrieved', async () => { expect(endpoint.getCapabilitiesUrl()).toBe( 'https://my.test.service/ogc/wms?aa=bb&SERVICE=WMS&REQUEST=GetCapabilities' ); @@ -234,7 +234,7 @@ describe('WmsEndpoint', () => { }); describe('#getOperationUrl', () => { - it('returns NULL before the document is loaded', async () => { + it.skip('returns NULL before the document is loaded', async () => { expect(endpoint.getOperationUrl('GetMap')).toBeNull(); await endpoint.isReady(); }); From 26220fb5096e10cd6899f507404a10952a12f0f2 Mon Sep 17 00:00:00 2001 From: Edward Nash Date: Tue, 24 Sep 2024 10:32:43 +0200 Subject: [PATCH 4/6] Read operation URLs from WFS capabilities WFS Capabilities documents self-report the URLs to be used for operations. These self-reported URLs should be used rather than the URL used to retrieve the capabiliites. --- src/wfs/capabilities.spec.ts | 117 +++++++++++++++++++++++++++++++++++ src/wfs/capabilities.ts | 68 ++++++++++++++++++++ src/wfs/endpoint.spec.ts | 35 +++++++++++ src/wfs/endpoint.ts | 36 ++++++++++- src/worker/index.ts | 1 + src/worker/worker.ts | 1 + 6 files changed, 257 insertions(+), 1 deletion(-) diff --git a/src/wfs/capabilities.spec.ts b/src/wfs/capabilities.spec.ts index 67f50ab..1128ea2 100644 --- a/src/wfs/capabilities.spec.ts +++ b/src/wfs/capabilities.spec.ts @@ -10,6 +10,7 @@ import capabilities200_noFormats from '../../fixtures/wfs/capabilities-geo2franc import { readFeatureTypesFromCapabilities, readInfoFromCapabilities, + readOperationUrlsFromCapabilities, readVersionFromCapabilities, } from './capabilities.js'; @@ -250,4 +251,120 @@ describe('WFS capabilities', () => { }); }); }); + + describe('readOperationUrlsFromCapabilities', () => { + it('reads the operation URLs (2.0.0)', () => { + const doc = parseXmlString(capabilities200); + const expectedUrls = { + GetCapabilities: { + Get: 'https://www.pigma.org/geoserver/wfs', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + DescribeFeatureType: { + Get: 'https://www.pigma.org/geoserver/wfs', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + GetFeature: { + Get: 'https://www.pigma.org/geoserver/wfs', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + GetPropertyValue: { + Get: 'https://www.pigma.org/geoserver/wfs', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + ListStoredQueries: { + Get: 'https://www.pigma.org/geoserver/wfs', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + DescribeStoredQueries: { + Get: 'https://www.pigma.org/geoserver/wfs', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + CreateStoredQuery: { + Post: 'https://www.pigma.org/geoserver/wfs', + }, + DropStoredQuery: { + Get: 'https://www.pigma.org/geoserver/wfs', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + LockFeature: { + Get: 'https://www.pigma.org/geoserver/wfs', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + GetFeatureWithLock: { + Get: 'https://www.pigma.org/geoserver/wfs', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + Transaction: { + Get: 'https://www.pigma.org/geoserver/wfs', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + }; + expect(readOperationUrlsFromCapabilities(doc)).toEqual(expectedUrls); + }); + it('reads the operation URLs (1.1.0)', () => { + const doc = parseXmlString(capabilities110); + const expectedUrls = { + GetCapabilities: { + Get: 'https://www.pigma.org/geoserver/wfs', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + DescribeFeatureType: { + Get: 'https://www.pigma.org/geoserver/wfs', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + GetFeature: { + Get: 'https://www.pigma.org/geoserver/wfs', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + GetGmlObject: { + Get: 'https://www.pigma.org/geoserver/wfs', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + LockFeature: { + Get: 'https://www.pigma.org/geoserver/wfs', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + GetFeatureWithLock: { + Get: 'https://www.pigma.org/geoserver/wfs', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + Transaction: { + Get: 'https://www.pigma.org/geoserver/wfs', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + }; + expect(readOperationUrlsFromCapabilities(doc)).toEqual(expectedUrls); + }); + it('reads the operation URLs (1.0.0)', () => { + const doc = parseXmlString(capabilities100); + const expectedUrls = { + GetCapabilities: { + Get: 'https://www.pigma.org/geoserver/wfs?request=GetCapabilities', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + DescribeFeatureType: { + Get: 'https://www.pigma.org/geoserver/wfs?request=DescribeFeatureType', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + GetFeature: { + Get: 'https://www.pigma.org/geoserver/wfs?request=GetFeature', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + Transaction: { + Get: 'https://www.pigma.org/geoserver/wfs?request=Transaction', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + LockFeature: { + Get: 'https://www.pigma.org/geoserver/wfs?request=LockFeature', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + GetFeatureWithLock: { + Get: 'https://www.pigma.org/geoserver/wfs?request=GetFeatureWithLock', + Post: 'https://www.pigma.org/geoserver/wfs', + }, + }; + expect(readOperationUrlsFromCapabilities(doc)).toEqual(expectedUrls); + }); + }); }); diff --git a/src/wfs/capabilities.ts b/src/wfs/capabilities.ts index ea7445a..2b5a99a 100644 --- a/src/wfs/capabilities.ts +++ b/src/wfs/capabilities.ts @@ -6,6 +6,7 @@ import { getElementName, getElementText, getRootElement, + stripNamespace, } from '../shared/xml-utils.js'; import { simplifyEpsgUrn } from '../shared/crs-utils.js'; import { XmlDocument, XmlElement } from '@rgrove/parse-xml'; @@ -13,9 +14,44 @@ import { BoundingBox, GenericEndpointInfo, MimeType, + type OperationName, + type OperationUrl, } from '../shared/models.js'; import { WfsFeatureTypeInternal, WfsVersion } from './model.js'; +/** + * Will read the operation URLS from the capabilities doc + * @param capabilitiesDoc Capabilities document + */ +export function readOperationUrlsFromCapabilities( + capabilitiesDoc: XmlDocument +): Record { + const urls: Record = {}; + const capabilities = getRootElement(capabilitiesDoc); + const operationsMetadata = findChildElement( + capabilities, + 'OperationsMetadata' + ); + if (operationsMetadata) { + // WFS 1.1.0 or 2.0.0 + findChildrenElement(operationsMetadata, 'Operation').forEach( + (operation) => { + const name = getElementAttribute(operation, 'name'); + urls[name] = parseOperation110(operation); + } + ); + } else { + // WFS 1.0.0 + const capability = findChildElement(capabilities, 'Capability'); + const request = findChildElement(capability, 'Request'); + getChildrenElement(request).forEach((operation) => { + const name = stripNamespace(getElementName(operation)); + urls[name] = parseOperation100(operation); + }); + } + return urls; +} + /** * Will read a WFS version from the capabilities doc * @param capabilitiesDoc Capabilities document @@ -120,6 +156,38 @@ export function readFeatureTypesFromCapabilities( ); } +/** + * Parse an operation definition from a WFS 1.0.0 capabilities (e.g. GetFeature) + * @param operation Operation element + */ +function parseOperation100(operation: XmlElement): OperationUrl { + const urls: OperationUrl = {}; + const dcpType = findChildrenElement(operation, 'DCPType'); + const http = dcpType.flatMap((d) => findChildrenElement(d, 'HTTP')); + const methods = http.flatMap((h) => getChildrenElement(h)); + methods.forEach((method) => { + const methodName = stripNamespace(getElementName(method)); + urls[methodName] = getElementAttribute(method, 'onlineResource'); + }); + return urls; +} + +/** + * Parse an operation definition from a WFS 1.1+ capabilities (e.g. GetFeature) + * @param operation Operation element + */ +function parseOperation110(operation: XmlElement): OperationUrl { + const urls: OperationUrl = {}; + const dcpType = findChildrenElement(operation, 'DCP'); + const http = dcpType.flatMap((d) => findChildElement(d, 'HTTP')); + const methods = http.flatMap((h) => getChildrenElement(h)); + methods.forEach((method) => { + const methodName = stripNamespace(getElementName(method)); + urls[methodName] = getElementAttribute(method, 'xlink:href'); + }); + return urls; +} + /** * Parse a feature type in a capabilities doc */ diff --git a/src/wfs/endpoint.spec.ts b/src/wfs/endpoint.spec.ts index a6ebd86..1044848 100644 --- a/src/wfs/endpoint.spec.ts +++ b/src/wfs/endpoint.spec.ts @@ -513,4 +513,39 @@ describe('WfsEndpoint', () => { expect(endpoint.supportsStartIndex()).toBeTruthy(); }); }); + + describe('#getCapabilitiesUrl', () => { + it('returns the URL used for the request before the capabilities are retrieved', async () => { + expect(endpoint.getCapabilitiesUrl()).toBe( + 'https://my.test.service/ogc/wms?aa=bb&SERVICE=WMS&REQUEST=GetCapabilities' + ); + await endpoint.isReady(); + }); + + it('returns the self-reported URL after the capabilities are retrieved', async () => { + await endpoint.isReady(); + expect(endpoint.getCapabilitiesUrl()).toBe( + 'https://www.pigma.org/geoserver/wfs?SERVICE=WMS&REQUEST=GetCapabilities' + ); + }); + }); + + describe('#getOperationUrl', () => { + it('returns NULL before the document is loaded', async () => { + expect(endpoint.getOperationUrl('GetMap')).toBeNull(); + await endpoint.isReady(); + }); + + it('returns undefined for a non-existant operation', async () => { + await endpoint.isReady(); + expect(endpoint.getOperationUrl('foo')).toBeUndefined(); + }); + + it('returns the correct URL for an existant operation', async () => { + await endpoint.isReady(); + expect(endpoint.getOperationUrl('GetFeature')).toBe( + 'https://www.pigma.org/geoserver/wfs' + ); + }); + }); }); diff --git a/src/wfs/endpoint.ts b/src/wfs/endpoint.ts index d5e3a92..4211449 100644 --- a/src/wfs/endpoint.ts +++ b/src/wfs/endpoint.ts @@ -14,7 +14,10 @@ import { BoundingBox, CrsCode, GenericEndpointInfo, + type HttpMethod, MimeType, + type OperationName, + type OperationUrl, } from '../shared/models.js'; import { WfsFeatureTypeBrief, @@ -32,6 +35,7 @@ export default class WfsEndpoint { private _capabilitiesPromise: Promise; private _info: GenericEndpointInfo | null; private _featureTypes: WfsFeatureTypeInternal[] | null; + private _url: Record; private _version: WfsVersion | null; /** @@ -53,9 +57,10 @@ export default class WfsEndpoint { 'WFS', 'CAPABILITIES', this._capabilitiesUrl - ).then(({ info, featureTypes, version }) => { + ).then(({ info, featureTypes, url, version }) => { this._info = info; this._featureTypes = featureTypes; + this._url = url; this._version = version; }); } @@ -318,4 +323,33 @@ export default class WfsEndpoint { startIndex ); } + + /** + * Returns the Capabilities URL of the WMS + * + * This is the URL reported by the service if available, otherwise the URL + * passed to the constructor + */ + getCapabilitiesUrl() { + const baseUrl = this.getOperationUrl('GetCapabilities'); + if (!baseUrl) { + return this._capabilitiesUrl; + } + return setQueryParams(baseUrl, { + SERVICE: 'WMS', + REQUEST: 'GetCapabilities', + }); + } + + /** + * Returns the URL reported by the WFS for the given operation + * @param operationName e.g. GetFeature, GetCapabilities, etc. + * @param method HTTP method + */ + getOperationUrl(operationName: OperationName, method: HttpMethod = 'Get') { + if (!this._url) { + return null; + } + return this._url[operationName]?.[method]; + } } diff --git a/src/worker/index.ts b/src/worker/index.ts index d048025..9c94037 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -62,6 +62,7 @@ export function parseWmsCapabilities(capabilitiesUrl: string): Promise<{ */ export function parseWfsCapabilities(capabilitiesUrl: string): Promise<{ version: WfsVersion; + url: Record; info: GenericEndpointInfo; featureTypes: WfsFeatureTypeInternal[]; }> { diff --git a/src/worker/worker.ts b/src/worker/worker.ts index feb9245..4a8b94c 100644 --- a/src/worker/worker.ts +++ b/src/worker/worker.ts @@ -24,6 +24,7 @@ addTaskHandler('parseWfsCapabilities', globalThis, ({ url }: { url: string }) => queryXmlDocument(url).then((xmlDoc) => ({ info: wfsCapabilities.readInfoFromCapabilities(xmlDoc), featureTypes: wfsCapabilities.readFeatureTypesFromCapabilities(xmlDoc), + url: wfsCapabilities.readOperationUrlsFromCapabilities(xmlDoc), version: wfsCapabilities.readVersionFromCapabilities(xmlDoc), })) ); From 11a22aa27e8af7d891184947238ded923833f9ef Mon Sep 17 00:00:00 2001 From: Edward Nash Date: Tue, 24 Sep 2024 10:33:35 +0200 Subject: [PATCH 5/6] Skip testing results before WFS caps loaded Due to the mocking of the async loading of the capabilities the loading occurs instantaneously and the state before they are loaded is not tested correctly --- src/wfs/endpoint.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wfs/endpoint.spec.ts b/src/wfs/endpoint.spec.ts index 1044848..09b4a74 100644 --- a/src/wfs/endpoint.spec.ts +++ b/src/wfs/endpoint.spec.ts @@ -515,7 +515,7 @@ describe('WfsEndpoint', () => { }); describe('#getCapabilitiesUrl', () => { - it('returns the URL used for the request before the capabilities are retrieved', async () => { + it.skip('returns the URL used for the request before the capabilities are retrieved', async () => { expect(endpoint.getCapabilitiesUrl()).toBe( 'https://my.test.service/ogc/wms?aa=bb&SERVICE=WMS&REQUEST=GetCapabilities' ); @@ -531,7 +531,7 @@ describe('WfsEndpoint', () => { }); describe('#getOperationUrl', () => { - it('returns NULL before the document is loaded', async () => { + it.skip('returns NULL before the document is loaded', async () => { expect(endpoint.getOperationUrl('GetMap')).toBeNull(); await endpoint.isReady(); }); From e28b81559879ad549225757c4bb0c402fb9d562c Mon Sep 17 00:00:00 2001 From: Edward Nash Date: Tue, 24 Sep 2024 10:34:04 +0200 Subject: [PATCH 6/6] Use self-reported URLs for all WFS operations --- src/wfs/endpoint.spec.ts | 6 +++--- src/wfs/endpoint.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/wfs/endpoint.spec.ts b/src/wfs/endpoint.spec.ts index 09b4a74..a7b866d 100644 --- a/src/wfs/endpoint.spec.ts +++ b/src/wfs/endpoint.spec.ts @@ -445,7 +445,7 @@ describe('WfsEndpoint', () => { outputFormat: 'application/gml+xml; version=3.2', }) ).toEqual( - 'https://my.test.service/ogc/wfs?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=cd16%3Ahierarchisation_l&OUTPUTFORMAT=application%2Fgml%2Bxml%3B+version%3D3.2&COUNT=200' + 'https://www.pigma.org/geoserver/wfs?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=cd16%3Ahierarchisation_l&OUTPUTFORMAT=application%2Fgml%2Bxml%3B+version%3D3.2&COUNT=200' ); }); it('returns a GetFeature requesting geojson url for a given feature type', () => { @@ -454,7 +454,7 @@ describe('WfsEndpoint', () => { asJson: true, }) ).toEqual( - 'https://my.test.service/ogc/wfs?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=cd16%3Acomptages_routiers_l&OUTPUTFORMAT=application%2Fjson' + 'https://www.pigma.org/geoserver/wfs?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=cd16%3Acomptages_routiers_l&OUTPUTFORMAT=application%2Fjson' ); }); it('returns a GetFeature with a bbox and output crs for a given feature type', () => { @@ -464,7 +464,7 @@ describe('WfsEndpoint', () => { outputCrs: 'EPSG:2154', }) ).toEqual( - 'https://my.test.service/ogc/wfs?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=cd16%3Ahierarchisation_l&SRSNAME=EPSG%3A2154&BBOX=1%2C2%2C3%2C4' + 'https://www.pigma.org/geoserver/wfs?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=cd16%3Ahierarchisation_l&SRSNAME=EPSG%3A2154&BBOX=1%2C2%2C3%2C4' ); }); it('throws an error if the feature type was not found', () => { diff --git a/src/wfs/endpoint.ts b/src/wfs/endpoint.ts index 4211449..4d323e3 100644 --- a/src/wfs/endpoint.ts +++ b/src/wfs/endpoint.ts @@ -146,12 +146,12 @@ export default class WfsEndpoint { return useCache( () => { const describeUrl = generateDescribeFeatureTypeUrl( - this._capabilitiesUrl, + this.getOperationUrl('DescribeFeatureType'), this._version, name ); const getFeatureUrl = generateGetFeatureUrl( - this._capabilitiesUrl, + this.getOperationUrl('GetFeature'), this._version, name, undefined, @@ -310,7 +310,7 @@ export default class WfsEndpoint { ); } return generateGetFeatureUrl( - this._capabilitiesUrl, + this.getOperationUrl('GetFeature'), this._version, internalFeatureType.name, format,