Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions fixtures/wms/describelayer-response.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE WMS_DescribeLayerResponse SYSTEM "http://schemas.opengis.net/wms/1.1.1/WMS_DescribeLayerResponse.dtd">
<WMS_DescribeLayerResponse version="1.1.1">
<LayerDescription name="my_workspace:my_vector_layer" wfs="https://my-server.com/wfs?" owsURL="https://my-server.com/wfs?" owsType="WFS">
<Query typeName="my_workspace:my_vector_layer"/>
</LayerDescription>
<LayerDescription name="my_workspace:my_raster_layer" owsURL="https://my-server.com/wcs?" owsType="WCS">
<Query typeName="my_workspace:my_raster_layer"/>
</LayerDescription>
</WMS_DescribeLayerResponse>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this fixture looks like a real response. For instance this is the response example from the spec:

<DescribeLayerResponse xmlns="http://www.opengis.net/sld" xmlns:ows="http://www.opengis.net/ows"
xmlns:se="http://www.opengis.net/se" xmlns:wfs="http://schemas.opengis.net/wfs"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:myns="http://myserver/somenamespace"
xsi:schemaLocation="http://www.opengis.net/sld DescribeLayer.xsd">
<Version>1.1.0</Version>
<LayerDescription>
 <owsType>wfs</owsType>
 <se:OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="http://mywfs/server/ogcwebservice"/>
 <TypeName>
 <se:FeatureTypeName>myns:cloudfeatures</se:FeatureTypeName>
 <TypeName>
</LayerDescription>
</DescribeLayerResponse>

Could you maybe do two different fixtures, one for WCS and one for WFS? Also slightly different, e.g. coming from different services. Thank you!

1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type {
WmsVersion,
WmsLayerSummary,
WmsLayerAttribution,
WmsLayerDescription,
} from './wms/model.js';
export { default as WmtsEndpoint } from './wmts/endpoint.js';
export type {
Expand Down
42 changes: 42 additions & 0 deletions src/wms/describelayer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { parseDescribeLayerResponse } from './describelayer.js';
// @ts-expect-error ts-migrate(7016)
import describeLayerResponse from '../../fixtures/wms/describelayer-response.xml';
import { parseXmlString } from '../shared/xml-utils.js';

describe('WMS DescribeLayer', () => {
describe('parseDescribeLayerResponse', () => {
it('parses a vector layer description (owsType WFS)', () => {
const doc = parseXmlString(describeLayerResponse);
const result = parseDescribeLayerResponse(
doc,
'my_workspace:my_vector_layer'
);
expect(result).toEqual({
layerName: 'my_workspace:my_vector_layer',
owsType: 'WFS',
owsUrl: 'https://my-server.com/wfs?',
typeName: 'my_workspace:my_vector_layer',
});
});

it('parses a raster layer description (owsType WCS)', () => {
const doc = parseXmlString(describeLayerResponse);
const result = parseDescribeLayerResponse(
doc,
'my_workspace:my_raster_layer'
);
expect(result).toEqual({
layerName: 'my_workspace:my_raster_layer',
owsType: 'WCS',
owsUrl: 'https://my-server.com/wcs?',
typeName: 'my_workspace:my_raster_layer',
});
});

it('returns null when the layer is not found in the response', () => {
const doc = parseXmlString(describeLayerResponse);
const result = parseDescribeLayerResponse(doc, 'nonexistent:layer');
expect(result).toBeNull();
});
});
});
44 changes: 44 additions & 0 deletions src/wms/describelayer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { XmlDocument } from '@rgrove/parse-xml';
import {
findChildElement,
findChildrenElement,
getElementAttribute,
getRootElement,
} from '../shared/xml-utils.js';
import { WmsLayerDescription } from './model.js';

/**
* Parses a WMS DescribeLayer response document and returns the description
* for the requested layer.
* @param describeLayerDoc The parsed XML document from a DescribeLayer response
* @param layerName The layer name to look for in the response
* @return The layer description, or null if the layer was not found in the response
*/
export function parseDescribeLayerResponse(
describeLayerDoc: XmlDocument,
layerName: string
): WmsLayerDescription | null {
const root = getRootElement(describeLayerDoc);
const layerDescriptions = findChildrenElement(root, 'LayerDescription');

const match = layerDescriptions.find(
(el) => getElementAttribute(el, 'name') === layerName
);
if (!match) return null;

const owsType = getElementAttribute(match, 'owsType');
const owsUrl =
getElementAttribute(match, 'owsURL') || getElementAttribute(match, 'wfs');

const queryEl = findChildElement(match, 'Query');
const typeName = queryEl
? getElementAttribute(queryEl, 'typeName')
: undefined;

return {
layerName,
owsType,
owsUrl,
...(typeName ? { typeName } : {}),
};
}
53 changes: 53 additions & 0 deletions src/wms/endpoint.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import capabilities130 from '../../fixtures/wms/capabilities-brgm-1-3-0.xml';
import capabilitiesStates from '../../fixtures/wms/capabilities-states-1-3-0.xml';
// @ts-expect-error ts-migrate(7016)
import exceptionReportWfs from '../../fixtures/wms/service-exception-report-wfs.xml';
// @ts-expect-error ts-migrate(7016)
import describeLayerResponse from '../../fixtures/wms/describelayer-response.xml';
import WmsEndpoint from './endpoint.js';
import { useCache } from '../shared/cache.js';
import { EndpointError, ServiceExceptionError } from '../shared/errors.js';
Expand Down Expand Up @@ -414,4 +416,55 @@ describe('WmsEndpoint', () => {
);
});
});

describe('#describeLayer', () => {
beforeEach(() => {
globalThis.fetchResponseFactory = (url) => {
if (url.indexOf('DescribeLayer') > -1) return describeLayerResponse;
return capabilities130;
};
endpoint = new WmsEndpoint(
'https://my.test.service/ogc/wms?service=wms&request=GetMap&aa=bb'
);
});

it('returns the layer description for a vector layer', async () => {
await endpoint.isReady();
const result = await endpoint.describeLayer(
'my_workspace:my_vector_layer'
);
expect(result).toEqual({
layerName: 'my_workspace:my_vector_layer',
owsType: 'WFS',
owsUrl: 'https://my-server.com/wfs?',
typeName: 'my_workspace:my_vector_layer',
});
});

it('returns the layer description for a raster layer', async () => {
await endpoint.isReady();
const result = await endpoint.describeLayer(
'my_workspace:my_raster_layer'
);
expect(result).toEqual({
layerName: 'my_workspace:my_raster_layer',
owsType: 'WCS',
owsUrl: 'https://my-server.com/wcs?',
typeName: 'my_workspace:my_raster_layer',
});
});

it('returns null when the layer is not found in the response', async () => {
await endpoint.isReady();
const result = await endpoint.describeLayer('nonexistent:layer');
expect(result).toBeNull();
});

it('returns null when DescribeLayer is not advertised', async () => {
globalThis.fetchResponseFactory = () => capabilitiesStates;
endpoint = new WmsEndpoint('https://my.test.service/ogc/wms');
await endpoint.isReady();
expect(endpoint.describeLayer('usa:states')).toBeNull();
});
});
});
44 changes: 41 additions & 3 deletions src/wms/endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { parseWmsCapabilities } from '../worker/index.js';
import { useCache } from '../shared/cache.js';
import { setQueryParams } from '../shared/http-utils.js';
import { queryXmlDocument, setQueryParams } from '../shared/http-utils.js';
import {
BoundingBox,
CrsCode,
Expand All @@ -10,8 +10,14 @@ import {
type OperationName,
type OperationUrl,
} from '../shared/models.js';
import { WmsLayerFull, WmsLayerSummary, WmsVersion } from './model.js';
import { generateGetMapUrl } from './url.js';
import {
WmsLayerDescription,
WmsLayerFull,
WmsLayerSummary,
WmsVersion,
} from './model.js';
import { generateDescribeLayerUrl, generateGetMapUrl } from './url.js';
import { parseDescribeLayerResponse } from './describelayer.js';

/**
* Represents a WMS endpoint advertising several layers arranged in a tree structure.
Expand Down Expand Up @@ -200,6 +206,38 @@ export default class WmsEndpoint {
});
}

/**
* Performs a DescribeLayer request for the given layer and returns its description,
* including the underlying OWS type (e.g. "WFS" for vector data).
* @param layerName Layer name to describe
* @return Returns null if the endpoint is not ready or does not advertise DescribeLayer
*/
describeLayer(layerName: string): Promise<WmsLayerDescription | null> | null {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it so that a Promise is always returned, otherwise the API is very awkward

if (!this._layers) {
return null;
}
const describeLayerBaseUrl = this.getOperationUrl('DescribeLayer');
if (!describeLayerBaseUrl) {
return null;
}
return useCache(
() => {
const url = generateDescribeLayerUrl(
describeLayerBaseUrl,
this._version,
layerName
);
return queryXmlDocument(url).then((doc) =>
parseDescribeLayerResponse(doc, layerName)
);
},
'WMS',
'DESCRIBELAYER',
this._capabilitiesUrl,
layerName
);
}

/**
* Returns the URL reported by the WMS for the given operation
* @param operationName e.g. GetMap, GetCapabilities, etc.
Expand Down
7 changes: 7 additions & 0 deletions src/wms/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,11 @@ export type WmsLayerFull = {
children?: WmsLayerFull[];
};

export type WmsLayerDescription = {
layerName: string;
owsType: string;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be typed as 'wcs' | 'wfs' as per the spec

owsUrl: string;
typeName?: string;
};

export type WmsVersion = '1.1.0' | '1.1.1' | '1.3.0';
19 changes: 19 additions & 0 deletions src/wms/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,22 @@ export function generateGetMapUrl(

return setQueryParams(serviceUrl, newParams);
}

/**
* Generates an URL for a DescribeLayer operation
* @param serviceUrl
* @param version
* @param layerName Layer name to describe
*/
export function generateDescribeLayerUrl(
serviceUrl: string,
version: WmsVersion,
layerName: string
): string {
return setQueryParams(serviceUrl, {
SERVICE: 'WMS',
REQUEST: 'DescribeLayer',
VERSION: version,
LAYERS: layerName,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs a SLD_VERSION=1.1.0 param here

});
}