mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
[Maps] populate WMS layers from getCapabilities response (#32342)
* [Maps] populate WMS layers from getCapabilities response * move wms get capabilities into seperate class * move xml2js from devDependency to dependency * localize get capabilities text * get jest test wired together * implement getCapabilities in WmsClient * display layers and styles in EuiComboBox * extract root common path to avoid UI where options text is all the same prefix * handle case where only single layer is returned in capabilities * feedback on WmsClient * clear layers and styles when serviceUrl changes
This commit is contained in:
parent
6374c9676f
commit
b32b852afa
7 changed files with 674 additions and 166 deletions
|
@ -129,7 +129,6 @@
|
||||||
"typescript": "^3.3.3333",
|
"typescript": "^3.3.3333",
|
||||||
"vinyl-fs": "^3.0.2",
|
"vinyl-fs": "^3.0.2",
|
||||||
"xml-crypto": "^0.10.1",
|
"xml-crypto": "^0.10.1",
|
||||||
"xml2js": "^0.4.19",
|
|
||||||
"yargs": "4.8.1"
|
"yargs": "4.8.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -285,6 +284,7 @@
|
||||||
"unstated": "^2.1.1",
|
"unstated": "^2.1.1",
|
||||||
"uuid": "3.0.1",
|
"uuid": "3.0.1",
|
||||||
"venn.js": "0.2.9",
|
"venn.js": "0.2.9",
|
||||||
|
"xml2js": "^0.4.19",
|
||||||
"xregexp": "3.2.0"
|
"xregexp": "3.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
@ -1,165 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { Fragment } from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
EuiFieldText,
|
|
||||||
EuiFormRow,
|
|
||||||
} from '@elastic/eui';
|
|
||||||
|
|
||||||
import { AbstractTMSSource } from './tms_source';
|
|
||||||
import { TileLayer } from '../tile_layer';
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters';
|
|
||||||
|
|
||||||
export class WMSSource extends AbstractTMSSource {
|
|
||||||
|
|
||||||
static type = 'WMS';
|
|
||||||
static title = i18n.translate('xpack.maps.source.wmsTitle', {
|
|
||||||
defaultMessage: 'Web Map Service'
|
|
||||||
});
|
|
||||||
static description = i18n.translate('xpack.maps.source.wmsDescription', {
|
|
||||||
defaultMessage: 'Maps from OGC Standard WMS'
|
|
||||||
});
|
|
||||||
static icon = 'grid';
|
|
||||||
|
|
||||||
static createDescriptor({ serviceUrl, layers, styles }) {
|
|
||||||
return {
|
|
||||||
type: WMSSource.type,
|
|
||||||
serviceUrl: serviceUrl,
|
|
||||||
layers: layers,
|
|
||||||
styles: styles
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static renderEditor({ onPreviewSource, inspectorAdapters }) {
|
|
||||||
const previewWMS = (options) => {
|
|
||||||
const sourceDescriptor = WMSSource.createDescriptor(options);
|
|
||||||
const source = new WMSSource(sourceDescriptor, inspectorAdapters);
|
|
||||||
onPreviewSource(source);
|
|
||||||
};
|
|
||||||
return (<WMSEditor previewWMS={previewWMS} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getImmutableProperties() {
|
|
||||||
return [
|
|
||||||
{ label: getDataSourceLabel(), value: WMSSource.title },
|
|
||||||
{ label: getUrlLabel(), value: this._descriptor.serviceUrl },
|
|
||||||
{ label: i18n.translate('xpack.maps.source.wms.layersLabel', {
|
|
||||||
defaultMessage: 'Layers'
|
|
||||||
}), value: this._descriptor.layers },
|
|
||||||
{ label: i18n.translate('xpack.maps.source.wms.stylesLabel', {
|
|
||||||
defaultMessage: 'Styles'
|
|
||||||
}), value: this._descriptor.styles },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
_createDefaultLayerDescriptor(options) {
|
|
||||||
return TileLayer.createDescriptor({
|
|
||||||
sourceDescriptor: this._descriptor,
|
|
||||||
...options
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createDefaultLayer(options) {
|
|
||||||
return new TileLayer({
|
|
||||||
layerDescriptor: this._createDefaultLayerDescriptor(options),
|
|
||||||
source: this
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDisplayName() {
|
|
||||||
return this._descriptor.serviceUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
getUrlTemplate() {
|
|
||||||
const styles = this._descriptor.styles || '';
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
return `${this._descriptor.serviceUrl}?bbox={bbox-epsg-3857}&format=image/png&service=WMS&version=1.1.1&request=GetMap&srs=EPSG:3857&transparent=true&width=256&height=256&layers=${this._descriptor.layers}&styles=${styles}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class WMSEditor extends React.Component {
|
|
||||||
|
|
||||||
state = {
|
|
||||||
serviceUrl: '',
|
|
||||||
layers: '',
|
|
||||||
styles: ''
|
|
||||||
}
|
|
||||||
|
|
||||||
_previewIfPossible() {
|
|
||||||
if (this.state.serviceUrl && this.state.layers) {
|
|
||||||
//todo: should really debounce this so we don't get a ton of changes during typing
|
|
||||||
this.props.previewWMS({
|
|
||||||
serviceUrl: this.state.serviceUrl,
|
|
||||||
layers: this.state.layers,
|
|
||||||
styles: this.state.styles
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async _handleServiceUrlChange(e) {
|
|
||||||
await this.setState({
|
|
||||||
serviceUrl: e.target.value
|
|
||||||
});
|
|
||||||
this._previewIfPossible();
|
|
||||||
}
|
|
||||||
|
|
||||||
async _handleLayersChange(e) {
|
|
||||||
await this.setState({
|
|
||||||
layers: e.target.value
|
|
||||||
});
|
|
||||||
this._previewIfPossible();
|
|
||||||
}
|
|
||||||
|
|
||||||
async _handleStylesChange(e) {
|
|
||||||
await this.setState({
|
|
||||||
styles: e.target.value
|
|
||||||
});
|
|
||||||
this._previewIfPossible();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<EuiFormRow label="Url">
|
|
||||||
<EuiFieldText
|
|
||||||
value={this.state.serviceUrl}
|
|
||||||
onChange={(e) => this._handleServiceUrlChange(e)}
|
|
||||||
/>
|
|
||||||
</EuiFormRow>
|
|
||||||
<EuiFormRow
|
|
||||||
label={i18n.translate('xpack.maps.source.wms.layersLabel', {
|
|
||||||
defaultMessage: 'Layers'
|
|
||||||
})}
|
|
||||||
helpText={i18n.translate('xpack.maps.source.wms.layersHelpText', {
|
|
||||||
defaultMessage: 'use comma separated list of layer names'
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<EuiFieldText
|
|
||||||
onChange={(e) => this._handleLayersChange(e)}
|
|
||||||
/>
|
|
||||||
</EuiFormRow>
|
|
||||||
<EuiFormRow
|
|
||||||
label={i18n.translate('xpack.maps.source.wms.stylesLabel', {
|
|
||||||
defaultMessage: 'Styles'
|
|
||||||
})}
|
|
||||||
helpText={i18n.translate('xpack.maps.source.wms.stylesHelpText', {
|
|
||||||
defaultMessage: 'use comma separated list of style names'
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<EuiFieldText
|
|
||||||
onChange={(e) => this._handleStylesChange(e)}
|
|
||||||
/>
|
|
||||||
</EuiFormRow>
|
|
||||||
</Fragment>
|
|
||||||
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { WMSSource } from './wms_source';
|
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { parseString } from 'xml2js';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
|
||||||
|
export class WmsClient {
|
||||||
|
constructor({ serviceUrl }) {
|
||||||
|
this._serviceUrl = serviceUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _fetch(url) {
|
||||||
|
return fetch(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _fetchCapabilities() {
|
||||||
|
const resp = await this._fetch(`${this._serviceUrl}?version=1.1.1&request=GetCapabilities&service=WMS`);
|
||||||
|
if (resp.status >= 400) {
|
||||||
|
throw new Error(`Unable to access ${this.state.serviceUrl}`);
|
||||||
|
}
|
||||||
|
const body = await resp.text();
|
||||||
|
|
||||||
|
const parsePromise = new Promise((resolve, reject) => {
|
||||||
|
parseString(body, (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return await parsePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCapabilities() {
|
||||||
|
const rawCapabilities = await this._fetchCapabilities();
|
||||||
|
|
||||||
|
const { layers, styles } = reduceLayers([], _.get(rawCapabilities, 'WMT_MS_Capabilities.Capability[0].Layer', []));
|
||||||
|
|
||||||
|
return {
|
||||||
|
layers: groupCapabilities(layers),
|
||||||
|
styles: groupCapabilities(styles)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reduceLayers(path, layers) {
|
||||||
|
const emptyCapabilities = {
|
||||||
|
layers: [],
|
||||||
|
styles: [],
|
||||||
|
};
|
||||||
|
function createOption(optionPath, optionTitle, optionName) {
|
||||||
|
return {
|
||||||
|
path: [...optionPath, optionTitle],
|
||||||
|
value: optionName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return layers.reduce((accumulatedCapabilities, layer) => {
|
||||||
|
// Layer is hierarchical, continue traversing
|
||||||
|
if (layer.Layer) {
|
||||||
|
const hierarchicalCapabilities = reduceLayers([...path, layer.Title[0]], layer.Layer);
|
||||||
|
return {
|
||||||
|
layers: [...accumulatedCapabilities.layers, ...hierarchicalCapabilities.layers],
|
||||||
|
styles: [...accumulatedCapabilities.styles, ...hierarchicalCapabilities.styles]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedStyles = [...accumulatedCapabilities.styles];
|
||||||
|
if (_.has(layer, 'Style[0]')) {
|
||||||
|
updatedStyles.push(createOption(
|
||||||
|
path,
|
||||||
|
_.get(layer, 'Style[0].Title[0]'),
|
||||||
|
_.get(layer, 'Style[0].Name[0]')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
layers: [
|
||||||
|
...accumulatedCapabilities.layers,
|
||||||
|
createOption(path, layer.Title[0], layer.Name[0])
|
||||||
|
],
|
||||||
|
styles: updatedStyles
|
||||||
|
};
|
||||||
|
}, emptyCapabilities);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid filling select box option label with text that is all the same
|
||||||
|
// Create a single group from common parts of Layer hierarchy
|
||||||
|
function groupCapabilities(list) {
|
||||||
|
if (list.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let rootCommonPath = list[0].path;
|
||||||
|
for(let listIndex = 1; listIndex < list.length; listIndex++) {
|
||||||
|
if (rootCommonPath.length === 0) {
|
||||||
|
// No commonality in root path, nothing left to verify
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = list[listIndex].path;
|
||||||
|
for(let pathIndex = 0; pathIndex < path.length && pathIndex < rootCommonPath.length; pathIndex++) {
|
||||||
|
if (rootCommonPath[pathIndex] !== path[pathIndex]) {
|
||||||
|
// truncate root common path at location of divergence
|
||||||
|
rootCommonPath = rootCommonPath.slice(0, pathIndex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCommonPath.length === 0 || list.length === 1) {
|
||||||
|
return list.map(({ path, value }) => {
|
||||||
|
return { label: path.join(' - '), value };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{
|
||||||
|
label: rootCommonPath.join(' - '),
|
||||||
|
options: list.map(({ path, value }) => {
|
||||||
|
return { label: path.splice(rootCommonPath.length).join(' - '), value };
|
||||||
|
})
|
||||||
|
}];
|
||||||
|
}
|
|
@ -0,0 +1,198 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { WmsClient } from './wms_client';
|
||||||
|
|
||||||
|
describe('getCapabilities', () => {
|
||||||
|
it('Should extract flat Layer elements', async () => {
|
||||||
|
const wmsClient = new WmsClient({ serviceUrl: 'myWMSUrl' });
|
||||||
|
wmsClient._fetch = () => {
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
text: () => {
|
||||||
|
return `
|
||||||
|
<WMT_MS_Capabilities version="1.1.1">
|
||||||
|
<Capability>
|
||||||
|
<Layer>
|
||||||
|
<Title>layer1</Title>
|
||||||
|
<Name>1</Name>
|
||||||
|
<Style>
|
||||||
|
<Name>default</Name>
|
||||||
|
<Title>defaultStyle</Title>
|
||||||
|
</Style>
|
||||||
|
</Layer>
|
||||||
|
<Layer>
|
||||||
|
<Title>layer2</Title>
|
||||||
|
<Name>2</Name>
|
||||||
|
<Style>
|
||||||
|
<Name>fancy</Name>
|
||||||
|
<Title>fancyStyle</Title>
|
||||||
|
</Style>
|
||||||
|
</Layer>
|
||||||
|
</Capability>
|
||||||
|
</WMT_MS_Capabilities>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const capabilities = await wmsClient.getCapabilities();
|
||||||
|
expect(capabilities.layers).toEqual([
|
||||||
|
{ label: 'layer1', value: '1' },
|
||||||
|
{ label: 'layer2', value: '2' }
|
||||||
|
]);
|
||||||
|
expect(capabilities.styles).toEqual([
|
||||||
|
{ label: 'defaultStyle', value: 'default' },
|
||||||
|
{ label: 'fancyStyle', value: 'fancy' }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Good example of Layer hierarchy in the wild can be found at
|
||||||
|
// https://idpgis.ncep.noaa.gov/arcgis/services/NWS_Forecasts_Guidance_Warnings/NDFD_temp/MapServer/WMSServer
|
||||||
|
it('Should extract hierarchical Layer elements', async () => {
|
||||||
|
const wmsClient = new WmsClient({ serviceUrl: 'myWMSUrl' });
|
||||||
|
wmsClient._fetch = () => {
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
text: () => {
|
||||||
|
return `
|
||||||
|
<WMT_MS_Capabilities version="1.1.1">
|
||||||
|
<Capability>
|
||||||
|
<Layer>
|
||||||
|
<Title><![CDATA[hierarchyLevel1PathA]]></Title>
|
||||||
|
<Layer>
|
||||||
|
<Title>hierarchyLevel2</Title>
|
||||||
|
<Layer>
|
||||||
|
<Title>layer1</Title>
|
||||||
|
<Name>1</Name>
|
||||||
|
<Style>
|
||||||
|
<Name>default</Name>
|
||||||
|
<Title>defaultStyle</Title>
|
||||||
|
</Style>
|
||||||
|
</Layer>
|
||||||
|
<Layer>
|
||||||
|
<Title>layer2</Title>
|
||||||
|
<Name>2</Name>
|
||||||
|
</Layer>
|
||||||
|
</Layer>
|
||||||
|
</Layer>
|
||||||
|
<Layer>
|
||||||
|
<Title>hierarchyLevel1PathB</Title>
|
||||||
|
<Layer>
|
||||||
|
<Title>layer3</Title>
|
||||||
|
<Name>3</Name>
|
||||||
|
<Style>
|
||||||
|
<Name>fancy</Name>
|
||||||
|
<Title>fancyStyle</Title>
|
||||||
|
</Style>
|
||||||
|
</Layer>
|
||||||
|
</Layer>
|
||||||
|
</Capability>
|
||||||
|
</WMT_MS_Capabilities>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const capabilities = await wmsClient.getCapabilities();
|
||||||
|
expect(capabilities.layers).toEqual([
|
||||||
|
{ label: 'hierarchyLevel1PathA - hierarchyLevel2 - layer1', value: '1' },
|
||||||
|
{ label: 'hierarchyLevel1PathA - hierarchyLevel2 - layer2', value: '2' },
|
||||||
|
{ label: 'hierarchyLevel1PathB - layer3', value: '3' }
|
||||||
|
]);
|
||||||
|
expect(capabilities.styles).toEqual([
|
||||||
|
{ label: 'hierarchyLevel1PathA - hierarchyLevel2 - defaultStyle', value: 'default' },
|
||||||
|
{ label: 'hierarchyLevel1PathB - fancyStyle', value: 'fancy' }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should create group from common parts of Layer hierarchy', async () => {
|
||||||
|
const wmsClient = new WmsClient({ serviceUrl: 'myWMSUrl' });
|
||||||
|
wmsClient._fetch = () => {
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
text: () => {
|
||||||
|
return `
|
||||||
|
<WMT_MS_Capabilities version="1.1.1">
|
||||||
|
<Capability>
|
||||||
|
<Layer>
|
||||||
|
<Title>hierarchyLevel1PathA</Title>
|
||||||
|
<Layer>
|
||||||
|
<Title>hierarchyLevel2</Title>
|
||||||
|
<Layer>
|
||||||
|
<Title>layer1</Title>
|
||||||
|
<Name>1</Name>
|
||||||
|
<Style>
|
||||||
|
<Name>default</Name>
|
||||||
|
<Title>defaultStyle</Title>
|
||||||
|
</Style>
|
||||||
|
</Layer>
|
||||||
|
</Layer>
|
||||||
|
</Layer>
|
||||||
|
<Layer>
|
||||||
|
<Title>hierarchyLevel1PathA</Title>
|
||||||
|
<Layer>
|
||||||
|
<Title>hierarchyLevel2</Title>
|
||||||
|
<Layer>
|
||||||
|
<Title>layer2</Title>
|
||||||
|
<Name>2</Name>
|
||||||
|
<Style>
|
||||||
|
<Name>fancy</Name>
|
||||||
|
<Title>fancyStyle</Title>
|
||||||
|
</Style>
|
||||||
|
</Layer>
|
||||||
|
</Layer>
|
||||||
|
</Layer>
|
||||||
|
</Capability>
|
||||||
|
</WMT_MS_Capabilities>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const capabilities = await wmsClient.getCapabilities();
|
||||||
|
expect(capabilities.layers).toEqual([
|
||||||
|
{
|
||||||
|
label: 'hierarchyLevel1PathA - hierarchyLevel2',
|
||||||
|
options: [
|
||||||
|
{ label: 'layer1', value: '1' },
|
||||||
|
{ label: 'layer2', value: '2' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
expect(capabilities.styles).toEqual([
|
||||||
|
{
|
||||||
|
label: 'hierarchyLevel1PathA - hierarchyLevel2',
|
||||||
|
options: [
|
||||||
|
{ label: 'defaultStyle', value: 'default' },
|
||||||
|
{ label: 'fancyStyle', value: 'fancy' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should create not group common hierarchy when there is only a single layer', async () => {
|
||||||
|
const wmsClient = new WmsClient({ serviceUrl: 'myWMSUrl' });
|
||||||
|
wmsClient._fetch = () => {
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
text: () => {
|
||||||
|
return `
|
||||||
|
<WMT_MS_Capabilities version="1.1.1">
|
||||||
|
<Capability>
|
||||||
|
<Layer>
|
||||||
|
<Title>layer1</Title>
|
||||||
|
<Name>1</Name>
|
||||||
|
</Layer>
|
||||||
|
</Capability>
|
||||||
|
</WMT_MS_Capabilities>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const capabilities = await wmsClient.getCapabilities();
|
||||||
|
expect(capabilities.layers).toEqual([
|
||||||
|
{ label: 'layer1', value: '1' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,250 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
import {
|
||||||
|
EuiButton,
|
||||||
|
EuiCallOut,
|
||||||
|
EuiComboBox,
|
||||||
|
EuiFieldText,
|
||||||
|
EuiFormRow,
|
||||||
|
EuiForm,
|
||||||
|
EuiSpacer,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import { WmsClient } from './wms_client';
|
||||||
|
|
||||||
|
const LAYERS_LABEL = i18n.translate('xpack.maps.source.wms.layersLabel', {
|
||||||
|
defaultMessage: 'Layers'
|
||||||
|
});
|
||||||
|
const STYLES_LABEL = i18n.translate('xpack.maps.source.wms.stylesLabel', {
|
||||||
|
defaultMessage: 'Styles'
|
||||||
|
});
|
||||||
|
|
||||||
|
export class WMSCreateSourceEditor extends Component {
|
||||||
|
|
||||||
|
state = {
|
||||||
|
serviceUrl: '',
|
||||||
|
layers: '',
|
||||||
|
styles: '',
|
||||||
|
isLoadingCapabilities: false,
|
||||||
|
getCapabilitiesError: null,
|
||||||
|
hasAttemptedToLoadCapabilities: false,
|
||||||
|
layerOptions: [],
|
||||||
|
styleOptions: [],
|
||||||
|
selectedLayerOptions: [],
|
||||||
|
selectedStyleOptions: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._isMounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._isMounted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_previewIfPossible() {
|
||||||
|
const {
|
||||||
|
serviceUrl,
|
||||||
|
layers,
|
||||||
|
styles
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const sourceConfig = (serviceUrl && layers)
|
||||||
|
? { serviceUrl, layers, styles }
|
||||||
|
: null;
|
||||||
|
this.props.previewWMS(sourceConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadCapabilities = async () => {
|
||||||
|
if (!this.state.serviceUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
hasAttemptedToLoadCapabilities: true,
|
||||||
|
isLoadingCapabilities: true,
|
||||||
|
getCapabilitiesError: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const wmsClient = new WmsClient({ serviceUrl: this.state.serviceUrl });
|
||||||
|
|
||||||
|
let capabilities;
|
||||||
|
try {
|
||||||
|
capabilities = await wmsClient.getCapabilities();
|
||||||
|
} catch (error) {
|
||||||
|
if (this._isMounted) {
|
||||||
|
this.setState({
|
||||||
|
isLoadingCapabilities: false,
|
||||||
|
getCapabilitiesError: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._isMounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
isLoadingCapabilities: false,
|
||||||
|
layerOptions: capabilities.layers,
|
||||||
|
styleOptions: capabilities.styles
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleServiceUrlChange = (e) => {
|
||||||
|
this.setState({
|
||||||
|
serviceUrl: e.target.value,
|
||||||
|
hasAttemptedToLoadCapabilities: false,
|
||||||
|
layerOptions: [],
|
||||||
|
styleOptions: [],
|
||||||
|
selectedLayerOptions: [],
|
||||||
|
selectedStyleOptions: [],
|
||||||
|
layers: '',
|
||||||
|
styles: '',
|
||||||
|
}, this._previewIfPossible);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleLayersChange = (e) => {
|
||||||
|
this.setState({ layers: e.target.value }, this._previewIfPossible);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleLayerOptionsChange = (selectedOptions) => {
|
||||||
|
this.setState({
|
||||||
|
selectedLayerOptions: selectedOptions,
|
||||||
|
layers: selectedOptions.map(selectedOption => {
|
||||||
|
return selectedOption.value;
|
||||||
|
}).join(',')
|
||||||
|
}, this._previewIfPossible);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleStylesChange = (e) => {
|
||||||
|
this.setState({ styles: e.target.value }, this._previewIfPossible);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleStyleOptionsChange = (selectedOptions) => {
|
||||||
|
this.setState({
|
||||||
|
selectedStyleOptions: selectedOptions,
|
||||||
|
styles: selectedOptions.map(selectedOption => {
|
||||||
|
return selectedOption.value;
|
||||||
|
}).join(',')
|
||||||
|
}, this._previewIfPossible);
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderLayerAndStyleInputs() {
|
||||||
|
if (!this.state.hasAttemptedToLoadCapabilities || this.state.isLoadingCapabilities) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.getCapabilitiesError || this.state.layerOptions.length === 0) {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<EuiCallOut
|
||||||
|
title={i18n.translate('xpack.maps.source.wms.getCapabilitiesErrorCalloutTitle', {
|
||||||
|
defaultMessage: 'Unable to load service metadata'
|
||||||
|
})}
|
||||||
|
color="warning"
|
||||||
|
>
|
||||||
|
<p>{this.state.getCapabilitiesError}</p>
|
||||||
|
</EuiCallOut>
|
||||||
|
|
||||||
|
<EuiFormRow
|
||||||
|
label={LAYERS_LABEL}
|
||||||
|
helpText={i18n.translate('xpack.maps.source.wms.layersHelpText', {
|
||||||
|
defaultMessage: 'use comma separated list of layer names'
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<EuiFieldText
|
||||||
|
onChange={this._handleLayersChange}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
|
||||||
|
<EuiFormRow
|
||||||
|
label={STYLES_LABEL}
|
||||||
|
helpText={i18n.translate('xpack.maps.source.wms.stylesHelpText', {
|
||||||
|
defaultMessage: 'use comma separated list of style names'
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<EuiFieldText
|
||||||
|
onChange={this._handleStylesChange}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<EuiFormRow
|
||||||
|
label={LAYERS_LABEL}
|
||||||
|
>
|
||||||
|
<EuiComboBox
|
||||||
|
options={this.state.layerOptions}
|
||||||
|
selectedOptions={this.state.selectedLayerOptions}
|
||||||
|
onChange={this._handleLayerOptionsChange}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
<EuiFormRow
|
||||||
|
label={STYLES_LABEL}
|
||||||
|
>
|
||||||
|
<EuiComboBox
|
||||||
|
options={this.state.styleOptions}
|
||||||
|
selectedOptions={this.state.selectedStyleOptions}
|
||||||
|
onChange={this._handleStyleOptionsChange}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderGetCapabilitiesButton() {
|
||||||
|
if (!this.state.isLoadingCapabilities && this.state.hasAttemptedToLoadCapabilities) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<EuiButton
|
||||||
|
onClick={this._loadCapabilities}
|
||||||
|
isDisabled={!this.state.serviceUrl}
|
||||||
|
isLoading={this.state.isLoadingCapabilities}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.maps.source.wms.getCapabilitiesButtonText"
|
||||||
|
defaultMessage="Load capabilities"
|
||||||
|
/>
|
||||||
|
</EuiButton>
|
||||||
|
|
||||||
|
<EuiSpacer size="m" />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<EuiForm>
|
||||||
|
<EuiFormRow
|
||||||
|
label={i18n.translate('xpack.maps.source.wms.urlLabel', {
|
||||||
|
defaultMessage: 'Url'
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<EuiFieldText
|
||||||
|
value={this.state.serviceUrl}
|
||||||
|
onChange={this._handleServiceUrlChange}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
|
||||||
|
{this._renderGetCapabilitiesButton()}
|
||||||
|
|
||||||
|
{this._renderLayerAndStyleInputs()}
|
||||||
|
|
||||||
|
</EuiForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { AbstractTMSSource } from '../tms_source';
|
||||||
|
import { TileLayer } from '../../tile_layer';
|
||||||
|
import { WMSCreateSourceEditor } from './wms_create_source_editor';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { getDataSourceLabel, getUrlLabel } from '../../../../../common/i18n_getters';
|
||||||
|
|
||||||
|
export class WMSSource extends AbstractTMSSource {
|
||||||
|
|
||||||
|
static type = 'WMS';
|
||||||
|
static title = i18n.translate('xpack.maps.source.wmsTitle', {
|
||||||
|
defaultMessage: 'Web Map Service'
|
||||||
|
});
|
||||||
|
static description = i18n.translate('xpack.maps.source.wmsDescription', {
|
||||||
|
defaultMessage: 'Maps from OGC Standard WMS'
|
||||||
|
});
|
||||||
|
static icon = 'grid';
|
||||||
|
|
||||||
|
static createDescriptor({ serviceUrl, layers, styles }) {
|
||||||
|
return {
|
||||||
|
type: WMSSource.type,
|
||||||
|
serviceUrl: serviceUrl,
|
||||||
|
layers: layers,
|
||||||
|
styles: styles
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static renderEditor({ onPreviewSource, inspectorAdapters }) {
|
||||||
|
const previewWMS = (sourceConfig) => {
|
||||||
|
if (!sourceConfig) {
|
||||||
|
onPreviewSource(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceDescriptor = WMSSource.createDescriptor(sourceConfig);
|
||||||
|
const source = new WMSSource(sourceDescriptor, inspectorAdapters);
|
||||||
|
onPreviewSource(source);
|
||||||
|
};
|
||||||
|
return (<WMSCreateSourceEditor previewWMS={previewWMS} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getImmutableProperties() {
|
||||||
|
return [
|
||||||
|
{ label: getDataSourceLabel(), value: WMSSource.title },
|
||||||
|
{ label: getUrlLabel(), value: this._descriptor.serviceUrl },
|
||||||
|
{
|
||||||
|
label: i18n.translate('xpack.maps.source.wms.layersLabel', {
|
||||||
|
defaultMessage: 'Layers'
|
||||||
|
}),
|
||||||
|
value: this._descriptor.layers
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.translate('xpack.maps.source.wms.stylesLabel', {
|
||||||
|
defaultMessage: 'Styles'
|
||||||
|
}),
|
||||||
|
value: this._descriptor.styles
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
_createDefaultLayerDescriptor(options) {
|
||||||
|
return TileLayer.createDescriptor({
|
||||||
|
sourceDescriptor: this._descriptor,
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createDefaultLayer(options) {
|
||||||
|
return new TileLayer({
|
||||||
|
layerDescriptor: this._createDefaultLayerDescriptor(options),
|
||||||
|
source: this
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDisplayName() {
|
||||||
|
return this._descriptor.serviceUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUrlTemplate() {
|
||||||
|
const styles = this._descriptor.styles || '';
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
return `${this._descriptor.serviceUrl}?bbox={bbox-epsg-3857}&format=image/png&service=WMS&version=1.1.1&request=GetMap&srs=EPSG:3857&transparent=true&width=256&height=256&layers=${this._descriptor.layers}&styles=${styles}`;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue