diff --git a/packages/kbn-maki/index.js b/packages/kbn-maki/index.js
new file mode 100644
index 000000000000..8158f12794af
--- /dev/null
+++ b/packages/kbn-maki/index.js
@@ -0,0 +1,378 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/* eslint-disable quotes */
+
+// icons from maki version 6.1.0
+export const maki = {
+ svgArray: [
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n"
+ ]
+};
diff --git a/packages/kbn-maki/package.json b/packages/kbn-maki/package.json
new file mode 100644
index 000000000000..862e183800b3
--- /dev/null
+++ b/packages/kbn-maki/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@kbn/maki",
+ "version": "6.1.0",
+ "description": "browser friendly version of @mapbox/maki",
+ "license": "Apache-2.0",
+ "main": "index.js",
+ "devDependencies": {},
+ "dependencies": {},
+ "peerDependencies": {}
+}
diff --git a/packages/kbn-maki/readme.md b/packages/kbn-maki/readme.md
new file mode 100644
index 000000000000..aad509938113
--- /dev/null
+++ b/packages/kbn-maki/readme.md
@@ -0,0 +1,6 @@
+# @kbn/maki
+
+[@mapbox/maki](https://www.npmjs.com/package/@mapbox/maki) only works in node.js.
+See https://github.com/mapbox/maki/issues/462 for details.
+
+@kbn/maki is a browser friendly version of @mapbox/maki
diff --git a/x-pack/package.json b/x-pack/package.json
index 3b84e8d063a9..e318dded2f3e 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -180,6 +180,7 @@
"@kbn/es-query": "1.0.0",
"@kbn/i18n": "1.0.0",
"@kbn/interpreter": "1.0.0",
+ "@kbn/maki": "6.1.0",
"@kbn/ui-framework": "1.0.0",
"@mapbox/mapbox-gl-draw": "^1.1.1",
"@samverschueren/stream-to-observable": "^0.3.0",
diff --git a/x-pack/plugins/maps/common/constants.js b/x-pack/plugins/maps/common/constants.js
index cf00e1c59b27..bc67e46042ae 100644
--- a/x-pack/plugins/maps/common/constants.js
+++ b/x-pack/plugins/maps/common/constants.js
@@ -8,6 +8,8 @@ export const GIS_API_PATH = 'api/maps';
export const EMS_DATA_FILE_PATH = 'ems/file';
export const EMS_DATA_TMS_PATH = 'ems/tms';
export const EMS_META_PATH = 'ems/meta';
+export const SPRITE_PATH = '/maps/sprite';
+export const MAKI_SPRITE_PATH = `${SPRITE_PATH}/maki`;
export const MAP_SAVED_OBJECT_TYPE = 'map';
export const APP_ID = 'maps';
diff --git a/x-pack/plugins/maps/common/parse_xml_string.js b/x-pack/plugins/maps/common/parse_xml_string.js
new file mode 100644
index 000000000000..9d95e0e78280
--- /dev/null
+++ b/x-pack/plugins/maps/common/parse_xml_string.js
@@ -0,0 +1,22 @@
+/*
+ * 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 { parseString } from 'xml2js';
+
+// promise based wrapper around parseString
+export async function parseXmlString(xmlString) {
+ const parsePromise = new Promise((resolve, reject) => {
+ parseString(xmlString, (error, result) => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(result);
+ }
+ });
+ });
+
+ return await parsePromise;
+}
diff --git a/x-pack/plugins/maps/common/parse_xml_string.test.js b/x-pack/plugins/maps/common/parse_xml_string.test.js
new file mode 100644
index 000000000000..fcfc27bc18f8
--- /dev/null
+++ b/x-pack/plugins/maps/common/parse_xml_string.test.js
@@ -0,0 +1,16 @@
+/*
+ * 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 { parseXmlString } from './parse_xml_string';
+
+describe('parseXmlString', () => {
+ it('Should parse xml string into JS object', async () => {
+ const xmlAsObject = await parseXmlString('bar');
+ expect(xmlAsObject).toEqual({
+ foo: 'bar'
+ });
+ });
+});
diff --git a/x-pack/plugins/maps/public/components/map/mb/utils.js b/x-pack/plugins/maps/public/components/map/mb/utils.js
index 19373cbe7bf1..24d8c5272b21 100644
--- a/x-pack/plugins/maps/public/components/map/mb/utils.js
+++ b/x-pack/plugins/maps/public/components/map/mb/utils.js
@@ -6,8 +6,17 @@
import _ from 'lodash';
import mapboxgl from 'mapbox-gl';
+import chrome from 'ui/chrome';
+import { MAKI_SPRITE_PATH } from '../../../../common/constants';
+
+function relativeToAbsolute(url) {
+ const a = document.createElement('a');
+ a.setAttribute('href', url);
+ return a.href;
+}
export async function createMbMapInstance({ node, initialView, scrollZoom }) {
+ const makiUrl = relativeToAbsolute(chrome.addBasePath(MAKI_SPRITE_PATH));
return new Promise((resolve) => {
const options = {
attributionControl: false,
@@ -16,6 +25,7 @@ export async function createMbMapInstance({ node, initialView, scrollZoom }) {
version: 8,
sources: {},
layers: [],
+ sprite: makiUrl
},
scrollZoom
};
diff --git a/x-pack/plugins/maps/public/components/widget_overlay/view_control/set_view/set_view.js b/x-pack/plugins/maps/public/components/widget_overlay/view_control/set_view/set_view.js
index 5df5155c7f06..0faf5d86b2f0 100644
--- a/x-pack/plugins/maps/public/components/widget_overlay/view_control/set_view/set_view.js
+++ b/x-pack/plugins/maps/public/components/widget_overlay/view_control/set_view/set_view.js
@@ -101,7 +101,7 @@ export class SetView extends React.Component {
});
return (
-
+
{latFormRow}
diff --git a/x-pack/plugins/maps/public/shared/layers/sources/wms_source/wms_client.js b/x-pack/plugins/maps/public/shared/layers/sources/wms_source/wms_client.js
index b8c502858794..ab59178dc792 100644
--- a/x-pack/plugins/maps/public/shared/layers/sources/wms_source/wms_client.js
+++ b/x-pack/plugins/maps/public/shared/layers/sources/wms_source/wms_client.js
@@ -5,7 +5,7 @@
*/
import _ from 'lodash';
-import { parseString } from 'xml2js';
+import { parseXmlString } from '../../../../../common/parse_xml_string';
import fetch from 'node-fetch';
import { parse, format } from 'url';
@@ -76,16 +76,7 @@ export class WmsClient {
}
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;
+ return await parseXmlString(body);
}
async getCapabilities() {
diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/__snapshots__/vector_style_symbol_editor.test.js.snap b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/__snapshots__/vector_style_symbol_editor.test.js.snap
new file mode 100644
index 000000000000..34cd441aa549
--- /dev/null
+++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/__snapshots__/vector_style_symbol_editor.test.js.snap
@@ -0,0 +1,94 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Should render icon select when symbolized as Icon 1`] = `
+
+
+
+
+
+`;
+
+exports[`Should render symbol select when symbolized as Circle 1`] = `
+
+
+
+`;
diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/__snapshots__/vector_icon.test.js.snap b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/__snapshots__/vector_icon.test.js.snap
new file mode 100644
index 000000000000..553e1471b61b
--- /dev/null
+++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/__snapshots__/vector_icon.test.js.snap
@@ -0,0 +1,43 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Renders CircleIcon with correct styles when isPointOnly 1`] = `
+
+`;
+
+exports[`Renders LineIcon with correct styles when isLineOnly 1`] = `
+
+`;
+
+exports[`Renders PolygonIcon with correct styles when not line only or not point only 1`] = `
+
+`;
+
+exports[`Renders SymbolIcon with correct styles when isPointOnly and symbolId provided 1`] = `
+
+`;
diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/point_icon.js b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/circle_icon.js
similarity index 97%
rename from x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/point_icon.js
rename to x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/circle_icon.js
index 7a208e1f4609..5efba64360f2 100644
--- a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/point_icon.js
+++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/circle_icon.js
@@ -6,7 +6,7 @@
import React from 'react';
-export const PointIcon = ({ style }) => (
+export const CircleIcon = ({ style }) => (
,
- ,
- ,
+ ,
+ ,
+ ,
];
}
@@ -36,9 +36,9 @@ function getSymbolSizeIcons() {
fill: 'grey',
};
return [
- ,
- ,
- ,
+ ,
+ ,
+ ,
];
}
diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/symbol_icon.js b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/symbol_icon.js
new file mode 100644
index 000000000000..1cff6003e291
--- /dev/null
+++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/symbol_icon.js
@@ -0,0 +1,71 @@
+/*
+ * 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 } from 'react';
+import PropTypes from 'prop-types';
+
+import { getMakiSymbolSvg, styleSvg, buildSrcUrl } from '../../../symbol_utils';
+
+export class SymbolIcon extends Component {
+
+ state = {
+ imgDataUrl: undefined,
+ prevSymbolId: undefined,
+ prevFill: undefined,
+ }
+
+ componentDidMount() {
+ this._isMounted = true;
+ this._loadSymbol(this.props.symbolId, this.props.fill);
+ }
+
+ componentDidUpdate() {
+ this._loadSymbol(this.props.symbolId, this.props.fill);
+ }
+
+ componentWillUnmount() {
+ this._isMounted = false;
+ }
+
+ async _loadSymbol(nextSymbolId, nextFill) {
+ if (nextSymbolId === this.state.prevSymbolId
+ && nextFill === this.state.prevFill) {
+ return;
+ }
+
+ let imgDataUrl;
+ try {
+ const svg = getMakiSymbolSvg(nextSymbolId);
+ const styledSvg = await styleSvg(svg, nextFill);
+ imgDataUrl = buildSrcUrl(styledSvg);
+ } catch (error) {
+ // ignore failures - component will just not display an icon
+ }
+
+ if (this._isMounted) {
+ this.setState({
+ imgDataUrl,
+ prevSymbolId: nextSymbolId,
+ prevFill: nextFill
+ });
+ }
+ }
+
+ render() {
+ if (!this.state.imgDataUrl) {
+ return null;
+ }
+
+ return (
+
+ );
+ }
+}
+
+SymbolIcon.propTypes = {
+ symbolId: PropTypes.string.isRequired,
+ fill: PropTypes.string.isRequired,
+};
diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/vector_icon.js b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/vector_icon.js
index 3a15ea3908ac..619f2fe42dc3 100644
--- a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/vector_icon.js
+++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/vector_icon.js
@@ -8,9 +8,10 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { dynamicColorShape, staticColorShape } from '../style_option_shapes';
-import { PointIcon } from './point_icon';
+import { CircleIcon } from './circle_icon';
import { LineIcon } from './line_icon';
import { PolygonIcon } from './polygon_icon';
+import { SymbolIcon } from './symbol_icon';
import { VectorStyle } from '../../../vector_style';
import { getColorRampCenterColor } from '../../../color_utils';
@@ -62,9 +63,20 @@ export class VectorIcon extends Component {
fill: extractColorFromStyleProperty(this.props.fillColor, 'grey'),
};
- return this.state.isPointsOnly
- ?
- : ;
+ if (!this.state.isPointsOnly) {
+ return ();
+ }
+
+ if (!this.props.symbolId) {
+ return ();
+ }
+
+ return (
+
+ );
}
}
@@ -92,6 +104,7 @@ const colorStylePropertyShape = PropTypes.shape({
VectorIcon.propTypes = {
fillColor: colorStylePropertyShape,
lineColor: colorStylePropertyShape,
+ symbolId: PropTypes.string,
loadIsPointsOnly: PropTypes.func.isRequired,
loadIsLinesOnly: PropTypes.func.isRequired,
};
diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/vector_icon.test.js b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/vector_icon.test.js
new file mode 100644
index 000000000000..27398bb201dd
--- /dev/null
+++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/legend/vector_icon.test.js
@@ -0,0 +1,114 @@
+/*
+ * 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 { shallow } from 'enzyme';
+
+import { VectorIcon } from './vector_icon';
+import { VectorStyle } from '../../../vector_style';
+
+let isPointsOnly = false;
+let isLinesOnly = false;
+const defaultProps = {
+ loadIsPointsOnly: () => { return isPointsOnly; },
+ loadIsLinesOnly: () => { return isLinesOnly; },
+ fillColor: {
+ type: VectorStyle.STYLE_TYPE.STATIC,
+ options: {
+ color: '#ff0000',
+ }
+ },
+ lineColor: {
+ type: VectorStyle.STYLE_TYPE.DYNAMIC,
+ options: {
+ color: 'Blues',
+ }
+ }
+};
+
+function configureIsLinesOnly() {
+ isLinesOnly = true;
+ isPointsOnly = false;
+}
+
+function configureIsPointsOnly() {
+ isLinesOnly = false;
+ isPointsOnly = true;
+}
+
+function configureNotLineOrPointOnly() {
+ isLinesOnly = false;
+ isPointsOnly = false;
+}
+
+test('Renders PolygonIcon with correct styles when not line only or not point only', async () => {
+ configureNotLineOrPointOnly();
+ const component = shallow(
+
+ );
+
+ // Ensure all promises resolve
+ await new Promise(resolve => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ expect(component)
+ .toMatchSnapshot();
+});
+
+test('Renders LineIcon with correct styles when isLineOnly', async () => {
+ configureIsLinesOnly();
+ const component = shallow(
+
+ );
+
+ // Ensure all promises resolve
+ await new Promise(resolve => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ expect(component)
+ .toMatchSnapshot();
+});
+
+test('Renders CircleIcon with correct styles when isPointOnly', async () => {
+ configureIsPointsOnly();
+ const component = shallow(
+
+ );
+
+ // Ensure all promises resolve
+ await new Promise(resolve => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ expect(component)
+ .toMatchSnapshot();
+});
+
+test('Renders SymbolIcon with correct styles when isPointOnly and symbolId provided', async () => {
+ configureIsPointsOnly();
+ const component = shallow(
+
+ );
+
+ // Ensure all promises resolve
+ await new Promise(resolve => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ expect(component)
+ .toMatchSnapshot();
+});
diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/vector_style_editor.js b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/vector_style_editor.js
index ae49226a5d4f..d9ad19347e2b 100644
--- a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/vector_style_editor.js
+++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/vector_style_editor.js
@@ -7,11 +7,15 @@
import _ from 'lodash';
import React, { Component, Fragment } from 'react';
+import chrome from 'ui/chrome';
import { VectorStyleColorEditor } from './color/vector_style_color_editor';
import { VectorStyleSizeEditor } from './size/vector_style_size_editor';
+import { VectorStyleSymbolEditor } from './vector_style_symbol_editor';
import { getDefaultDynamicProperties, getDefaultStaticProperties } from '../../vector_style_defaults';
import { VECTOR_SHAPE_TYPES } from '../../../sources/vector_feature_types';
+import { SYMBOLIZE_AS_CIRCLE } from '../../vector_constants';
import { i18n } from '@kbn/i18n';
+import { SYMBOL_OPTIONS } from '../../symbol_utils';
import { EuiSpacer, EuiButtonGroup } from '@elastic/eui';
@@ -135,16 +139,38 @@ export class VectorStyleEditor extends Component {
}
_renderPointProperties() {
+ let lineColor;
+ let lineWidth;
+ if (this.props.styleProperties.symbol.options.symbolizeAs === SYMBOLIZE_AS_CIRCLE) {
+ lineColor = (
+
+ {this._renderLineColor()}
+
+
+ );
+ lineWidth = (
+
+ {this._renderLineWidth()}
+
+
+ );
+ }
+
return (
+
+
{this._renderFillColor()}
- {this._renderLineColor()}
-
+ {lineColor}
- {this._renderLineWidth()}
-
+ {lineWidth}
{this._renderSymbolSize()}
diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/vector_style_symbol_editor.js b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/vector_style_symbol_editor.js
new file mode 100644
index 000000000000..76d13825a2c4
--- /dev/null
+++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/vector_style_symbol_editor.js
@@ -0,0 +1,132 @@
+/*
+ * 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 {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFormRow,
+ EuiSelect,
+ EuiSpacer,
+ EuiComboBox,
+} from '@elastic/eui';
+
+import { i18n } from '@kbn/i18n';
+import { SYMBOLIZE_AS_CIRCLE, SYMBOLIZE_AS_ICON } from '../../vector_constants';
+import { SymbolIcon } from './legend/symbol_icon';
+
+const SYMBOLIZE_AS_OPTIONS = [
+ {
+ value: SYMBOLIZE_AS_CIRCLE,
+ text: i18n.translate('xpack.maps.vector.symbolAs.circleLabel', {
+ defaultMessage: 'circle'
+ })
+ },
+ {
+ value: SYMBOLIZE_AS_ICON,
+ text: i18n.translate('xpack.maps.vector.symbolAs.IconLabel', {
+ defaultMessage: 'icon'
+ })
+ },
+];
+
+export function VectorStyleSymbolEditor({ styleOptions, handlePropertyChange, symbolOptions, isDarkMode }) {
+ const renderSymbolizeAsSelect = () => {
+ const selectedOption = SYMBOLIZE_AS_OPTIONS.find(({ value }) => {
+ return value === styleOptions.symbolizeAs;
+ });
+
+ const onSymbolizeAsChange = e => {
+ const styleDescriptor = {
+ options: {
+ ...styleOptions,
+ symbolizeAs: e.target.value
+ }
+ };
+ handlePropertyChange('symbol', styleDescriptor);
+ };
+
+ return (
+
+ );
+ };
+
+ const renderSymbolSelect = () => {
+ const selectedOption = symbolOptions.find(({ value }) => {
+ return value === styleOptions.symbolId;
+ });
+
+ const onSymbolChange = selectedOptions => {
+ if (!selectedOptions || selectedOptions.length === 0) {
+ return;
+ }
+
+ const styleDescriptor = {
+ options: {
+ ...styleOptions,
+ symbolId: selectedOptions[0].value
+ }
+ };
+ handlePropertyChange('symbol', styleDescriptor);
+ };
+
+ const renderOption = ({ value, label }) => {
+ return (
+
+
+
+
+
+ {label}
+
+
+ );
+ };
+
+ return (
+
+ );
+ };
+
+ const renderFormRowContent = () => {
+ if (styleOptions.symbolizeAs === SYMBOLIZE_AS_CIRCLE) {
+ return renderSymbolizeAsSelect();
+ }
+
+ return (
+
+ {renderSymbolizeAsSelect()}
+
+ {renderSymbolSelect()}
+
+ );
+ };
+
+ return (
+
+ {renderFormRowContent()}
+
+ );
+}
diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/vector_style_symbol_editor.test.js b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/vector_style_symbol_editor.test.js
new file mode 100644
index 000000000000..4a2c227d8453
--- /dev/null
+++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/vector_style_symbol_editor.test.js
@@ -0,0 +1,52 @@
+/*
+ * 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 { shallow } from 'enzyme';
+
+import { SYMBOLIZE_AS_CIRCLE, SYMBOLIZE_AS_ICON } from '../../vector_constants';
+import { VectorStyleSymbolEditor } from './vector_style_symbol_editor';
+
+const symbolOptions = [
+ { value: 'symbol1', label: 'symbol1' },
+ { value: 'symbol2', label: 'symbol2' }
+];
+
+const defaultProps = {
+ styleOptions: {
+ symbolizeAs: SYMBOLIZE_AS_CIRCLE,
+ symbolId: symbolOptions[0].value,
+ },
+ handlePropertyChange: () => {},
+ symbolOptions,
+ isDarkMode: false
+};
+
+test('Should render symbol select when symbolized as Circle', () => {
+ const component = shallow(
+
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+});
+
+test('Should render icon select when symbolized as Icon', () => {
+ const component = shallow(
+
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+});
diff --git a/x-pack/plugins/maps/public/shared/layers/styles/symbol_utils.js b/x-pack/plugins/maps/public/shared/layers/styles/symbol_utils.js
new file mode 100644
index 000000000000..df2411298524
--- /dev/null
+++ b/x-pack/plugins/maps/public/shared/layers/styles/symbol_utils.js
@@ -0,0 +1,103 @@
+/*
+ * 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 { maki } from '@kbn/maki';
+import xml2js from 'xml2js';
+import { parseXmlString } from '../../../../common/parse_xml_string';
+
+export const LARGE_MAKI_ICON_SIZE = 15;
+const LARGE_MAKI_ICON_SIZE_AS_STRING = LARGE_MAKI_ICON_SIZE.toString();
+export const SMALL_MAKI_ICON_SIZE = 11;
+export const HALF_LARGE_MAKI_ICON_SIZE = Math.ceil(LARGE_MAKI_ICON_SIZE);
+
+export const SYMBOLS = {};
+maki.svgArray.forEach(svgString => {
+ const ID_FRAG = 'id="';
+ const index = svgString.indexOf(ID_FRAG);
+ if (index !== -1) {
+ const idStartIndex = index + ID_FRAG.length;
+ const idEndIndex = svgString.substring(idStartIndex).indexOf('"') + idStartIndex;
+ const fullSymbolId = svgString.substring(idStartIndex, idEndIndex);
+ const symbolId = fullSymbolId.substring(0, fullSymbolId.length - 3); // remove '-15' or '-11' from id
+ const symbolSize = fullSymbolId.substring(fullSymbolId.length - 2); // grab last 2 chars from id
+ // only show large icons, small/large icon selection will based on configured size style
+ if (symbolSize === LARGE_MAKI_ICON_SIZE_AS_STRING) {
+ SYMBOLS[symbolId] = svgString;
+ }
+ }
+});
+
+export const SYMBOL_OPTIONS = Object.keys(SYMBOLS).map(symbolId => {
+ return ({
+ value: symbolId,
+ label: symbolId,
+ });
+});
+
+export function getMakiSymbolSvg(symbolId) {
+ if (!SYMBOLS[symbolId]) {
+ throw new Error(`Unable to find symbol: ${symbolId}`);
+ }
+ return SYMBOLS[symbolId];
+}
+
+export function getMakiSymbolAnchor(symbolId) {
+ switch (symbolId) {
+ case 'embassy-11':
+ case 'embassy-15':
+ case 'marker-11':
+ case 'marker-15':
+ case 'marker-stroked-11':
+ case 'marker-stroked-15':
+ return 'bottom';
+ default:
+ return 'center';
+ }
+}
+
+
+export function buildSrcUrl(svgString) {
+ const domUrl = window.URL || window.webkitURL || window;
+ const svg = new Blob([svgString], { type: 'image/svg+xml' });
+ return domUrl.createObjectURL(svg);
+}
+
+export async function styleSvg(svgString, fill) {
+ const svgXml = await parseXmlString(svgString);
+ if (fill) {
+ svgXml.svg.$.style = `fill: ${fill};`;
+ }
+ const builder = new xml2js.Builder();
+ return builder.buildObject(svgXml);
+}
+
+function addImageToMap(imageUrl, imageId, symbolId, mbMap) {
+ return new Promise((resolve, reject) => {
+ const img = new Image(LARGE_MAKI_ICON_SIZE, LARGE_MAKI_ICON_SIZE);
+ img.onload = () => {
+ mbMap.addImage(imageId, img);
+ resolve();
+ };
+ img.onerror = (err) => {
+ reject(err);
+ };
+ img.src = imageUrl;
+ });
+}
+
+export async function loadImage(imageId, symbolId, color, mbMap) {
+ let symbolSvg;
+ try {
+ symbolSvg = getMakiSymbolSvg(symbolId);
+ } catch(error) {
+ return;
+ }
+
+ const styledSvg = await styleSvg(symbolSvg, color);
+ const imageUrl = buildSrcUrl(styledSvg);
+
+ await addImageToMap(imageUrl, imageId, symbolId, mbMap);
+}
diff --git a/x-pack/plugins/maps/public/shared/layers/styles/symbol_utils.test.js b/x-pack/plugins/maps/public/shared/layers/styles/symbol_utils.test.js
new file mode 100644
index 000000000000..b62f385680da
--- /dev/null
+++ b/x-pack/plugins/maps/public/shared/layers/styles/symbol_utils.test.js
@@ -0,0 +1,29 @@
+/*
+ * 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 { getMakiSymbolSvg, styleSvg } from './symbol_utils';
+
+describe('getMakiSymbolSvg', () => {
+ it('Should load symbol svg', () => {
+ const svgString = getMakiSymbolSvg('aerialway');
+ expect(svgString.length).toBe(624);
+ });
+});
+
+describe('styleSvg', () => {
+ it('Should not add style property when fill not provided', async () => {
+ const unstyledSvgString = '';
+ const styledSvg = await styleSvg(unstyledSvgString);
+ expect(styledSvg.split('\n')[1]).toBe('