Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add utility in WMS endpoint to build a GetMap url #48

Merged
merged 4 commits into from
Jun 6, 2024
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
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);
}
Loading