mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -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",
|
||||
"vinyl-fs": "^3.0.2",
|
||||
"xml-crypto": "^0.10.1",
|
||||
"xml2js": "^0.4.19",
|
||||
"yargs": "4.8.1"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -285,6 +284,7 @@
|
|||
"unstated": "^2.1.1",
|
||||
"uuid": "3.0.1",
|
||||
"venn.js": "0.2.9",
|
||||
"xml2js": "^0.4.19",
|
||||
"xregexp": "3.2.0"
|
||||
},
|
||||
"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