Skip to content

Commit

Permalink
Merge pull request #48 from camptocamp/wxs-add-mandatory-version
Browse files Browse the repository at this point in the history
Add utility in WMS endpoint to build a GetMap url
  • Loading branch information
jahow authored Jun 6, 2024
2 parents ad6d9ab + 20432e7 commit ea89a7b
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 31 deletions.
7 changes: 5 additions & 2 deletions app/src/components/wms/WmsEndpoint.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
<div v-if="loading">Loading...</div>
<div v-if="loaded">
<InfoList :info="endpoint.getServiceInfo()"></InfoList>
<ItemsTree :items="endpoint.getLayers()" style="min-height: 200px">
<ItemsTree
:items="endpoint.getLayers()"
style="min-height: 200px; max-height: 400px; overflow: auto"
>
<template v-slot="{ item }">
<div :title="item.abstract">
<template v-if="item.name">
Expand All @@ -33,7 +36,7 @@
<WmsLayerInfo
v-if="selectedLayer"
:layer="selectedLayer"
:endpoint-url="url"
:endpoint="endpoint"
></WmsLayerInfo>
</div>
<div v-if="error">Error: {{ error }}</div>
Expand Down
34 changes: 17 additions & 17 deletions app/src/components/wms/WmsLayerInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@

<script>
import InfoList from '../presentation/InfoList.vue';
export default {
name: 'WmsLayerInfo',
components: { InfoList },
props: {
/** @type {{ new(): WmsLayerFull}} */
layer: Object,
endpointUrl: String,
/** @type {{ new(): WmsEndpoint}} */
endpoint: Object,
},
data: () => ({
selectedStyle: '',
Expand Down Expand Up @@ -68,23 +70,21 @@ export default {
if (!(this.selectedCrs in this.layer.boundingBoxes)) {
return '';
}
const bbox = this.layer.boundingBoxes[this.selectedCrs];
const ratio = (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
const extent = this.layer.boundingBoxes[this.selectedCrs];
const ratio = (extent[2] - extent[0]) / (extent[3] - extent[1]);
const maxDimension = 500;
const width = Math.round(ratio > 1 ? maxDimension : maxDimension * ratio);
const height = Math.round(width / ratio);
const urlObj = new URL(this.endpointUrl);
urlObj.searchParams.set('SERVICE', 'WMS');
urlObj.searchParams.set('REQUEST', 'GetMap');
urlObj.searchParams.set('LAYERS', this.layer.name);
urlObj.searchParams.set('STYLES', this.selectedStyle);
urlObj.searchParams.set('WIDTH', width.toString());
urlObj.searchParams.set('HEIGHT', height.toString());
urlObj.searchParams.set('FORMAT', 'image/png');
urlObj.searchParams.set('CRS', this.selectedCrs);
urlObj.searchParams.set('BBOX', bbox.join(','));
return urlObj.toString();
const widthPx = Math.round(
ratio > 1 ? maxDimension : maxDimension * ratio
);
const heightPx = Math.round(widthPx / ratio);
return this.endpoint.getMapUrl([this.layer.name], {
extent,
widthPx,
heightPx,
crs: this.selectedCrs,
styles: [this.selectedStyle],
outputFormat: 'image/png',
});
},
},
};
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type {
WmsLayerFull,
WmsVersion,
WmsLayerSummary,
WmtsLayerAttribution,
WmsLayerAttribution,
} from './wms/model.js';
export { default as WmtsEndpoint } from './wmts/endpoint.js';
export type {
Expand Down
8 changes: 3 additions & 5 deletions src/wms/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
GenericEndpointInfo,
LayerStyle,
} from '../shared/models.js';
import { WmtsLayerAttribution, WmsLayerFull, WmsVersion } from './model.js';
import { WmsLayerAttribution, WmsLayerFull, WmsVersion } from './model.js';

/**
* Will read a WMS version from the capabilities doc
Expand Down Expand Up @@ -74,7 +74,7 @@ function parseLayer(
version: WmsVersion,
inheritedSrs: CrsCode[] = [],
inheritedStyles: LayerStyle[] = [],
inheritedAttribution: WmtsLayerAttribution = null,
inheritedAttribution: WmsLayerAttribution = null,
inheritedBoundingBoxes: Record<CrsCode, BoundingBox> = null
): WmsLayerFull {
const srsTag = version === '1.3.0' ? 'CRS' : 'SRS';
Expand Down Expand Up @@ -159,9 +159,7 @@ function parseLayerStyle(styleEl: XmlElement): LayerStyle {
};
}

function parseLayerAttribution(
attributionEl: XmlElement
): WmtsLayerAttribution {
function parseLayerAttribution(attributionEl: XmlElement): WmsLayerAttribution {
const logoUrl = getElementAttribute(
findChildElement(
findChildElement(attributionEl, 'LogoURL'),
Expand Down
17 changes: 17 additions & 0 deletions src/wms/endpoint.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,21 @@ describe('WmsEndpoint', () => {
});
});
});

describe('#generateGetMapUrl', () => {
it('generates a correct URL', async () => {
await endpoint.isReady();
expect(
endpoint.getMapUrl(['layer1', 'layer2'], {
widthPx: 100,
heightPx: 200,
crs: 'EPSG:4326',
extent: [10, 20, 100, 200],
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'
);
});
});
});
58 changes: 54 additions & 4 deletions src/wms/endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { parseWmsCapabilities } from '../worker/index.js';
import { useCache } from '../shared/cache.js';
import { setQueryParams } from '../shared/http-utils.js';
import { GenericEndpointInfo } from '../shared/models.js';
import {
BoundingBox,
CrsCode,
GenericEndpointInfo,
MimeType,
} from '../shared/models.js';
import { WmsLayerFull, WmsLayerSummary, WmsVersion } from './model.js';
import { generateGetMapUrl } from './url.js';

/**
* Represents a WMS endpoint advertising several layers arranged in a tree structure.
*/
export default class WmsEndpoint {
private _capabilitiesUrl: string;
private _capabilitiesPromise: Promise<void>;
private _info: GenericEndpointInfo | null;
private _layers: WmsLayerFull[] | null;
Expand All @@ -18,7 +25,7 @@ export default class WmsEndpoint {
* initialize the endpoint
*/
constructor(url: string) {
const capabilitiesUrl = setQueryParams(url, {
this._capabilitiesUrl = setQueryParams(url, {
SERVICE: 'WMS',
REQUEST: 'GetCapabilities',
});
Expand All @@ -27,10 +34,10 @@ export default class WmsEndpoint {
* This fetches the capabilities doc and parses its contents
*/
this._capabilitiesPromise = useCache(
() => parseWmsCapabilities(capabilitiesUrl),
() => parseWmsCapabilities(this._capabilitiesUrl),
'WMS',
'CAPABILITIES',
capabilitiesUrl
this._capabilitiesUrl
).then(({ info, layers, version }) => {
this._info = info;
this._layers = layers;
Expand Down Expand Up @@ -120,4 +127,47 @@ export default class WmsEndpoint {
getVersion() {
return this._version;
}

/**
* Returns a URL that can be used to query an image from one or several layers
* @param layers List of layers to render
* @param {Object} options
* @param {number} options.widthPx
* @param {number} options.heightPx
* @param {CrsCode} options.crs Coordinate reference system to use for the image
* @param {BoundingBox} options.extent Expressed in the requested CRS
* @param {MimeType} options.outputFormat
* @param {string} [options.styles] List of styles to use, one for each layer requested; leave out or use empty string for default style
* @returns Returns null if endpoint is not ready
*/
getMapUrl(
layers: string[],
options: {
widthPx: number;
heightPx: number;
crs: CrsCode;
extent: BoundingBox;
outputFormat: MimeType;
styles?: string[];
}
) {
if (!this._layers) {
return null;
}
const { widthPx, heightPx, crs, extent, outputFormat, styles } = options;
// TODO: check supported CRS
// TODO: check supported output formats
// TODO: check supported styles
return generateGetMapUrl(
this._capabilitiesUrl,
this._version,
layers.join(','),
widthPx,
heightPx,
crs,
extent,
outputFormat,
styles !== undefined ? styles.join(',') : ''
);
}
}
4 changes: 2 additions & 2 deletions src/wms/model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BoundingBox, CrsCode, LayerStyle } from '../shared/models.js';

export type WmtsLayerAttribution = {
export type WmsLayerAttribution = {
title?: string;
url?: string;
logoUrl?: string;
Expand Down Expand Up @@ -32,7 +32,7 @@ export type WmsLayerFull = {
* Dict of bounding boxes where keys are CRS codes
*/
boundingBoxes: Record<CrsCode, BoundingBox>;
attribution?: WmtsLayerAttribution;
attribution?: WmsLayerAttribution;
/**
* Not defined if the layer is a leaf in the tree
*/
Expand Down
37 changes: 37 additions & 0 deletions src/wms/url.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { generateGetMapUrl } from './url.js';

describe('generateGetMapUrl', () => {
it('generates a correct URL (v1.1.0, no styles)', () => {
expect(
generateGetMapUrl(
'http://example.com/wms',
'1.1.0',
'layer1,layer2',
100,
200,
'EPSG:4326',
[10, 20, 100, 200],
'image/png'
)
).toBe(
'http://example.com/wms?SERVICE=WMS&REQUEST=GetMap&VERSION=1.1.0&LAYERS=layer1%2Clayer2&STYLES=&WIDTH=100&HEIGHT=200&FORMAT=image%2Fpng&SRS=EPSG%3A4326&BBOX=10%2C20%2C100%2C200'
);
});
it('generates a correct URL (v1.3.0, with styles)', () => {
expect(
generateGetMapUrl(
'http://example.com/wms',
'1.3.0',
'layer1,layer2',
100,
200,
'EPSG:4326',
[10, 20, 100, 200],
'image/png',
'style1,style2'
)
).toBe(
'http://example.com/wms?SERVICE=WMS&REQUEST=GetMap&VERSION=1.3.0&LAYERS=layer1%2Clayer2&STYLES=style1%2Cstyle2&WIDTH=100&HEIGHT=200&FORMAT=image%2Fpng&CRS=EPSG%3A4326&BBOX=10%2C20%2C100%2C200'
);
});
});
44 changes: 44 additions & 0 deletions src/wms/url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { setQueryParams } from '../shared/http-utils.js';
import { BoundingBox, CrsCode, MimeType } from '../shared/models.js';
import { WmsVersion } from './model.js';

/**
* Generates an URL for a GetMap operation
* @param serviceUrl
* @param version
* @param layers Comma-separated list of layers to render
* @param widthPx
* @param heightPx
* @param crs Coordinate reference system to use for the image
* @param extent Expressed in the requested CRS
* @param outputFormat
* @param [styles] Comma-separated list of styles to use; leave out for default style
*/
export function generateGetMapUrl(
serviceUrl: string,
version: WmsVersion,
layers: string,
widthPx: number,
heightPx: number,
crs: CrsCode,
extent: BoundingBox,
outputFormat: MimeType,
styles?: string
): string {
const crsParam = version === '1.3.0' ? 'CRS' : 'SRS';

const newParams = {
SERVICE: 'WMS',
REQUEST: 'GetMap',
VERSION: version,
LAYERS: layers,
STYLES: styles ?? '',
};
newParams['WIDTH'] = widthPx.toString();
newParams['HEIGHT'] = heightPx.toString();
newParams['FORMAT'] = outputFormat ?? 'image/png';
newParams[crsParam] = crs;
newParams['BBOX'] = extent.join(',');

return setQueryParams(serviceUrl, newParams);
}

0 comments on commit ea89a7b

Please sign in to comment.