Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
16 changes: 16 additions & 0 deletions fixtures/wms/describelayer-wcs.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<DescribeLayerResponse xmlns="http://www.opengis.net/sld"
xmlns:ows="http://www.opengis.net/ows"
xmlns:se="http://www.opengis.net/se"
xmlns:wcs="http://schemas.opengis.net/wcs"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:imagery="http://www.geodata-service.org/imagery" xsi:schemaLocation="http://www.opengis.net/sld DescribeLayer.xsd">
<Version>1.1.0</Version>
<LayerDescription>
<owsType>wcs</owsType>
<se:OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple" xlink:href="https://www.geodata-service.org/ows/wcs"/>
<TypeName>
<se:CoverageTypeName>imagery:ortho_coverage</se:CoverageTypeName>
</TypeName>
</LayerDescription>
</DescribeLayerResponse>
16 changes: 16 additions & 0 deletions fixtures/wms/describelayer-wfs.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<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:geodata="http://www.example.com/geodata" 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="https://www.example.com/geoserver/wfs"/>
<TypeName>
<se:FeatureTypeName>geodata:geography_vector</se:FeatureTypeName>
</TypeName>
</LayerDescription>
</DescribeLayerResponse>
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
45 changes: 45 additions & 0 deletions src/wms/describelayer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { parseDescribeLayerResponse } from './describelayer.js';
// @ts-expect-error ts-migrate(7016)
import describeLayerWfs from '../../fixtures/wms/describelayer-wfs.xml';
// @ts-expect-error ts-migrate(7016)
import describeLayerWcs from '../../fixtures/wms/describelayer-wcs.xml';
import { parseXmlString } from '../shared/xml-utils.js';

describe('WMS DescribeLayer', () => {
describe('parseDescribeLayerResponse', () => {
it('parses a vector layer description (owsType WFS)', () => {
const doc = parseXmlString(describeLayerWfs);
const result = parseDescribeLayerResponse(
doc,
'geodata:geography_vector'
);
expect(result).toEqual({
layerName: 'geodata:geography_vector',
owsType: 'wfs',
owsUrl: 'https://www.example.com/geoserver/wfs',
typeName: 'geodata:geography_vector',
});
});

it('parses a raster layer description (owsType WCS)', () => {
const doc = parseXmlString(describeLayerWcs);
const result = parseDescribeLayerResponse(doc, 'imagery:ortho_coverage');
expect(result).toEqual({
layerName: 'imagery:ortho_coverage',
owsType: 'wcs',
owsUrl: 'https://www.geodata-service.org/ows/wcs',
typeName: 'imagery:ortho_coverage',
});
});

it('returns null when the response contains no layer description', () => {
const emptyResponse = `<?xml version="1.0" encoding="UTF-8"?>
<DescribeLayerResponse xmlns="http://www.opengis.net/sld">
<Version>1.1.0</Version>
</DescribeLayerResponse>`;
const doc = parseXmlString(emptyResponse);
const result = parseDescribeLayerResponse(doc, 'nonexistent:layer');
expect(result).toBeNull();
});
});
});
42 changes: 42 additions & 0 deletions src/wms/describelayer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { XmlDocument } from '@rgrove/parse-xml';
import {
findChildElement,
getElementAttribute,
getElementText,
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 match = findChildElement(root, 'LayerDescription');
if (!match) return null;

const owsType = getElementText(
findChildElement(match, 'owsType')
) as WmsLayerDescription['owsType'];
const onlineResource = findChildElement(match, 'OnlineResource');
const owsUrl = getElementAttribute(onlineResource, 'xlink:href');

const typeNameEl = findChildElement(match, 'TypeName');
const typeName =
getElementText(findChildElement(typeNameEl, 'FeatureTypeName')) ||
getElementText(findChildElement(typeNameEl, 'CoverageTypeName'));

return {
layerName,
owsType,
owsUrl,
...(typeName ? { typeName } : {}),
};
}
65 changes: 65 additions & 0 deletions src/wms/endpoint.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ 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 describeLayerWfs from '../../fixtures/wms/describelayer-wfs.xml';
// @ts-expect-error ts-migrate(7016)
import describeLayerWcs from '../../fixtures/wms/describelayer-wcs.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 +418,65 @@ describe('WmsEndpoint', () => {
);
});
});

describe('#describeLayer', () => {
beforeEach(() => {
globalThis.fetchResponseFactory = (url) => {
if (url.indexOf('DescribeLayer') > -1) {
if (url.indexOf('imagery') > -1) return describeLayerWcs;
return describeLayerWfs;
}
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('geodata:geography_vector');
expect(result).toEqual({
layerName: 'geodata:geography_vector',
owsType: 'wfs',
owsUrl: 'https://www.example.com/geoserver/wfs',
typeName: 'geodata:geography_vector',
});
});

it('returns the layer description for a raster layer', async () => {
await endpoint.isReady();
const result = await endpoint.describeLayer('imagery:ortho_coverage');
expect(result).toEqual({
layerName: 'imagery:ortho_coverage',
owsType: 'wcs',
owsUrl: 'https://www.geodata-service.org/ows/wcs',
typeName: 'imagery:ortho_coverage',
});
});

it('returns null when the response contains no layer description', async () => {
const emptyDescribeLayer = `<?xml version="1.0" encoding="UTF-8"?>
<DescribeLayerResponse xmlns="http://www.opengis.net/sld">
<Version>1.1.0</Version>
</DescribeLayerResponse>`;
globalThis.fetchResponseFactory = (url) => {
if (url.indexOf('DescribeLayer') > -1) return emptyDescribeLayer;
return capabilities130;
};
endpoint = new WmsEndpoint(
'https://my.test.service/ogc/wms?service=wms&request=GetMap&aa=bb'
);
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();
await expect(endpoint.describeLayer('usa:states')).resolves.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> {
if (!this._layers) {
return Promise.resolve(null);
}
const describeLayerBaseUrl = this.getOperationUrl('DescribeLayer');
if (!describeLayerBaseUrl) {
return Promise.resolve(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: 'wcs' | 'wfs';
owsUrl: string;
typeName?: string;
};

export type WmsVersion = '1.1.0' | '1.1.1' | '1.3.0';
20 changes: 20 additions & 0 deletions src/wms/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,23 @@ 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

SLD_VERSION: '1.1.0',
});
}