diff --git a/AUTHORS b/AUTHORS index bdce324..13faeef 100644 --- a/AUTHORS +++ b/AUTHORS @@ -10,6 +10,7 @@ Guido Tapia Kyungwon Chun Lincoln Frias Luca Fiaschi +Martin Thorsen Ranang MLWohls Oscar Moll Pascal Bugnion diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..c048f99 --- /dev/null +++ b/Pipfile @@ -0,0 +1,20 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +pytest = "*" +flake8 = "*" +numpy = "*" +pandas = "*" + +[packages] +six = "*" +ipython = ">=5.3.0" +ipywidgets = ">=7.0.0" +traitlets = ">=4.3.0" +geojson = ">=2.0.0" + +[requires] +python_version = "3.6" diff --git a/gmaps/_version.py b/gmaps/_version.py index dbbddf9..eb75888 100644 --- a/gmaps/_version.py +++ b/gmaps/_version.py @@ -2,7 +2,7 @@ # This file is generated programatically. # Version of the Python package -__version__ = '0.9.1-dev' +__version__ = '0.10.0-dev' # Version of the JS client. This must match the version field in package.json. -CLIENT_VERSION = '0.9.1-dev' +CLIENT_VERSION = '0.10.0-dev' diff --git a/gmaps/drawing.py b/gmaps/drawing.py index 5c22727..ad222cb 100644 --- a/gmaps/drawing.py +++ b/gmaps/drawing.py @@ -1,4 +1,3 @@ - import ipywidgets as widgets from traitlets import ( @@ -11,21 +10,20 @@ from .marker import MarkerOptions from ._docutils import doc_subst - ALLOWED_DRAWING_MODES = { - 'DISABLED', 'MARKER', 'LINE', 'POLYGON', 'CIRCLE', 'DELETE' + 'DISABLED', 'MARKER', 'LINE', 'POLYGON', 'POLYLINE', 'CIRCLE', 'DELETE' } DEFAULT_DRAWING_MODE = 'MARKER' DEFAULT_STROKE_COLOR = '#696969' DEFAULT_FILL_COLOR = '#696969' - _doc_snippets = {} _doc_snippets['params'] = """ :param features: List of features to draw on the map. Features must be one of - :class:`gmaps.Marker`, :class:`gmaps.Line` or :class:`gmaps.Polygon`. + :class:`gmaps.Marker`, :class:`gmaps.Line`, :class:`gmaps.Polygon`, + or :class:`gmaps.Polyline`. :type features: list of features, optional :param marker_options: @@ -65,11 +63,24 @@ the options to the :class:`gmaps.Polygon` constructor. :type polygon_options: :class:`gmaps.PolygonOptions`, `dict` or `None`, optional + + :param polyline_options: + Options controlling how new polylines are drawn on the map. Either pass + in an instance of :class:`gmaps.PolylineOptions`, or a dictionary with + keys `stroke_weight`, `stroke_color`, `stroke_opacity` + (or a subset of these). See + :class:`gmaps.PolylineOptions` for documentation on possible values. + Note that this only affects the initial options of polylines added to + the map by a user. To customise polylines added programatically, pass in + the options to the :class:`gmaps.Polyline` constructor. + :type polyline_options: + :class:`gmaps.PolylineOptions`, `dict` or `None`, optional + """ _doc_snippets['examples'] = """ - You can use the drawing layer to add lines, markers and - polygons to a map: + You can use the drawing layer to add lines, markers, polygons, and + polylines to a map: >>> fig = gmaps.figure() >>> drawing = gmaps.drawing_layer(features=[ @@ -78,6 +89,10 @@ gmaps.Polygon( [(46.72, 6.06), (46.48, 6.49), (46.79, 6.91)], fill_color='red' + ), + gmaps.Polyline( + [(46.72, 6.06), (46.48, 6.49), (46.79, 6.91)], + fill_color='blue' ) ]) >>> fig.add_layer(drawing) @@ -124,7 +139,6 @@ def print_address(feature): fig # display the figure """ - _doc_snippets['stroke_options_params'] = """ :param stroke_color: The stroke color of the line. Colors can be specified as a simple @@ -169,7 +183,7 @@ class DrawingControls(GMapsWidgetMixin, widgets.DOMWidget): _model_name = Unicode('DrawingControlsModel').tag(sync=True) _view_name = Unicode('DrawingControlsView').tag(sync=True) show_controls = Bool(default_value=True, allow_none=False).tag( - sync=True) + sync=True) @doc_subst(_doc_snippets) @@ -419,6 +433,121 @@ def __init__( super(Polygon, self).__init__(**kwargs) +@doc_subst(_doc_snippets) +class PolylineOptions(HasTraits): + """ + Style options for a polyline. + + Pass an instance of this class to :func:`gmaps.drawing_layer` to + control the style of new user-drawn polylines on the map. + + :Examples: + + >>> fig = gmaps.figure() + >>> drawing = gmaps.drawing_layer( + polyline_options=gmaps.PolylineOptions( + stroke_color='red', fill_color=(255, 0, 132)) + ) + >>> fig.add_layer(drawing) + >>> fig # display the figure + + {stroke_options_params} + + {fill_options_params} + """ + stroke_color = geotraitlets.ColorAlpha( + allow_none=False, default_value=DEFAULT_STROKE_COLOR + ).tag(sync=True) + stroke_weight = Float( + min=0.0, allow_none=False, default_value=2.0 + ).tag(sync=True) + stroke_opacity = geotraitlets.StrokeOpacity().tag(sync=True) + + def to_polyline(self, path): + new_polyline = Polyline( + path=path, + stroke_color=self.stroke_color, + stroke_weight=self.stroke_weight, + stroke_opacity=self.stroke_opacity, + ) + return new_polyline + + +@doc_subst(_doc_snippets) +class Polyline(GMapsWidgetMixin, widgets.Widget): + """ + Widget representing a linear overlay of connected line segments on the map + + Add this polyline to a map via the :func:`gmaps.drawing_layer` + function, or by passing it directly to the ``.features`` array + of a :class:`gmaps.Drawing` instance. + + :Examples: + + >>> fig = gmaps.figure() + >>> drawing = gmaps.drawing_layer(features=[ + gmaps.Polyline( + [(46.72, 6.06), (46.48, 6.49), (46.79, 6.91)], + stroke_color='blue' + ) + ]) + >>> fig.add_layer(drawing) + + You can also add a polyline to an existing :class:`gmaps.Drawing` + instance: + + >>> fig = gmaps.figure() + >>> drawing = gmaps.drawing_layer() + >>> fig.add_layer(drawing) + >>> fig # display the figure + + You can now add polylines directly on the map: + + >>> drawing.features = [ + gmaps.Polyline( + [(46.72, 6.06), (46.48, 6.49), (46.79, 6.91)] + stroke_color='blue' + ) + ] + + :param path: + List of (latitude, longitude) pairs denoting each point on the polyline. + Latitudes are expressed as a float between -90 (corresponding to 90 + degrees south) and +90 (corresponding to 90 degrees north). Longitudes + are expressed as a float between -180 (corresponding to 180 degrees + west) and +180 (corresponding to 180 degrees east). + :type path: list of tuples of floats + + {stroke_options_params} + + {fill_options_params} + """ + _view_name = Unicode('PolylineView').tag(sync=True) + _model_name = Unicode('PolylineModel').tag(sync=True) + path = List(geotraitlets.Point(), minlen=3).tag(sync=True) + stroke_color = geotraitlets.ColorAlpha( + allow_none=False, default_value=DEFAULT_STROKE_COLOR + ).tag(sync=True) + stroke_weight = Float( + min=0.0, allow_none=False, default_value=2.0 + ).tag(sync=True) + stroke_opacity = geotraitlets.StrokeOpacity().tag(sync=True) + + def __init__( + self, path, + stroke_color=DEFAULT_STROKE_COLOR, + stroke_weight=2.0, + stroke_opacity=geotraitlets.StrokeOpacity.default_value + ): + kwargs = dict( + path=path, + stroke_color=stroke_color, + stroke_weight=stroke_weight, + stroke_opacity=stroke_opacity, + ) + super(Polyline, self).__init__(**kwargs) + + @doc_subst(_doc_snippets) class CircleOptions(HasTraits): """ @@ -579,9 +708,9 @@ class Drawing(GMapsWidgetMixin, widgets.Widget): :param mode: Initial drawing mode. One of ``DISABLED``, ``MARKER``, ``LINE``, - ``POLYGON``, ``CIRCLE`` or ``DELETE``. Defaults to ``MARKER`` if - ``toolbar_controls.show_controls`` is True, otherwise defaults to - ``DISABLED``. + ``POLYGON``, ``POLYLINE``, ``CIRCLE`` or ``DELETE``. Defaults + to ``MARKER`` if ``toolbar_controls.show_controls`` is True, otherwise + defaults to ``DISABLED``. :type mode: str, optional :param toolbar_controls: @@ -599,6 +728,8 @@ class Drawing(GMapsWidgetMixin, widgets.Widget): LineOptions, allow_none=False) polygon_options = widgets.trait_types.InstanceDict( PolygonOptions, allow_none=False) + polyline_options = widgets.trait_types.InstanceDict( + PolylineOptions, allow_none=False) circle_options = widgets.trait_types.InstanceDict( CircleOptions, allow_none=False) toolbar_controls = Instance(DrawingControls, allow_none=False).tag( @@ -614,6 +745,8 @@ def __init__(self, **kwargs): kwargs['line_options'] = self._default_line_options() if kwargs.get('polygon_options') is None: kwargs['polygon_options'] = self._default_polygon_options() + if kwargs.get('polyline_options') is None: + kwargs['polyline_options'] = self._default_polyline_options() if kwargs.get('circle_options') is None: kwargs['circle_options'] = self._default_circle_options() self._new_feature_callbacks = [] @@ -629,8 +762,8 @@ def on_new_feature(self, callback): Callable to be called when a new feature is added. The callback should take a single argument, the feature that has been added. This can be an instance - of :class:`gmaps.Line`, :class:`gmaps.Marker` or - :class:`gmaps.Polygon`. + of :class:`gmaps.Line`, :class:`gmaps.Marker`, + :class:`gmaps.Polygon`, or :class:`gmaps.Polyline`. :type callback: callable """ self._new_feature_callbacks.append(callback) @@ -662,6 +795,10 @@ def _default_line_options(self): def _default_polygon_options(self): return PolygonOptions() + @default('polyline_options') + def _default_polyline_options(self): + return PolylineOptions() + @default('circle_options') def _default_circle_options(self): return CircleOptions() @@ -703,6 +840,9 @@ def _handle_message(self, _, content, buffers): elif payload['featureType'] == 'POLYGON': path = payload['path'] feature = self.polygon_options.to_polygon(path) + elif payload['featureType'] == 'POLYLINE': + path = payload['path'] + feature = self.polyline_options.to_polyline(path) elif payload['featureType'] == 'CIRCLE': center = payload['center'] radius = payload['radius'] @@ -722,7 +862,7 @@ def _handle_message(self, _, content, buffers): def drawing_layer( features=None, mode=DEFAULT_DRAWING_MODE, show_controls=True, marker_options=None, line_options=None, - polygon_options=None): + polygon_options=None, polyline_options=None): """ Create an interactive drawing layer @@ -737,7 +877,7 @@ def drawing_layer( :param mode: Initial drawing mode. One of ``DISABLED``, - ``MARKER``, ``LINE``, ``POLYGON``, ``CIRCLE`` or + ``MARKER``, ``LINE``, ``POLYGON``, ``POLYLINE``, ``CIRCLE``, or ``DELETE``. Defaults to ``MARKER`` if ``show_controls`` is True, otherwise defaults to ``DISABLED``. :type mode: str, optional @@ -757,6 +897,7 @@ def drawing_layer( 'toolbar_controls': controls, 'marker_options': marker_options, 'line_options': line_options, - 'polygon_options': polygon_options + 'polygon_options': polygon_options, + 'polyline_options': polyline_options } return Drawing(**kwargs) diff --git a/gmaps/tests/test_drawing.py b/gmaps/tests/test_drawing.py index 0d7d7d5..e8340f9 100644 --- a/gmaps/tests/test_drawing.py +++ b/gmaps/tests/test_drawing.py @@ -53,6 +53,17 @@ def new_polygon_message(path): return message +def new_polyline_message(path): + message = { + 'event': 'FEATURE_ADDED', + 'payload': { + 'featureType': 'POLYLINE', + 'path': path + } + } + return message + + def new_circle_message(center, radius): message = { 'event': 'FEATURE_ADDED', @@ -116,6 +127,16 @@ def test_polygon_options_dict(self): layer = drawing.Drawing(polygon_options=options) assert layer.polygon_options.stroke_weight == 12.0 + def test_polyline_options_instance(self): + options = drawing.PolylineOptions(stroke_weight=12.0) + layer = drawing.Drawing(polyline_options=options) + assert layer.polyline_options.stroke_weight == 12.0 + + def test_polyline_options_dict(self): + options = {'stroke_weight': 12.0} + layer = drawing.Drawing(polyline_options=options) + assert layer.polyline_options.stroke_weight == 12.0 + def test_circle_options_instance(self): options = drawing.CircleOptions(stroke_weight=12.0) layer = drawing.Drawing(circle_options=options) @@ -228,6 +249,26 @@ def test_adding_polygon_with_options(self): assert new_polygon.path == path assert new_polygon.stroke_weight == 19.0 + def test_adding_polyline(self): + layer = drawing.Drawing() + path = [(5.0, 10.0), (15.0, 20.0), (25.0, 50.0)] + message = new_polyline_message(path) + layer._handle_custom_msg(message, None) + assert len(layer.features) == 1 + [new_polyline] = layer.features + assert new_polyline.path == path + + def test_adding_polyline_with_options(self): + layer = drawing.Drawing( + polyline_options=drawing.PolylineOptions(stroke_weight=19.0)) + path = [(5.0, 10.0), (15.0, 20.0), (25.0, 50.0)] + message = new_polyline_message(path) + layer._handle_custom_msg(message, None) + assert len(layer.features) == 1 + [new_polyline] = layer.features + assert new_polyline.path == path + assert new_polyline.stroke_weight == 19.0 + def test_adding_circle(self): layer = drawing.Drawing() center = [10.0, 15.0] @@ -302,6 +343,10 @@ def test_with_polygon_options(self): layer = drawing.drawing_layer(polygon_options={'stroke_weight': 12.0}) assert layer.polygon_options.stroke_weight == 12.0 + def test_with_polyline_options(self): + layer = drawing.drawing_layer(polyline_options={'stroke_weight': 12.0}) + assert layer.polyline_options.stroke_weight == 12.0 + class Line(unittest.TestCase): @@ -425,6 +470,68 @@ def test_to_polygon_defaults(self): assert actual_polygon.fill_opacity == expected_polygon.fill_opacity +class Polyline(unittest.TestCase): + + def setUp(self): + self.path = [(10.0, 20.0), (5.0, 30.0), (-5.0, 10.0)] + + def test_path_kwarg(self): + polyline = drawing.Polyline(path=self.path) + assert polyline.get_state()['path'] == self.path + + def test_normal_path_arg(self): + polyline = drawing.Polyline(self.path) + assert polyline.get_state()['path'] == self.path + + def test_missing_path(self): + with self.assertRaises(TypeError): + drawing.Polyline() + + def test_insufficient_points_path(self): + with self.assertRaises(traitlets.TraitError): + path = [(5.0, 30.0), (-5.0, 10.0)] + drawing.Polyline(path) + + def test_defaults(self): + polyline = drawing.Polyline(self.path) + state = polyline.get_state() + assert state['stroke_color'] == drawing.DEFAULT_STROKE_COLOR + assert state['stroke_weight'] == 2.0 + assert state['stroke_opacity'] == 0.6 + + def test_custom_arguments(self): + polyline = drawing.Polyline( + self.path, + stroke_color=(1, 3, 5), + stroke_weight=10.0, + stroke_opacity=0.87, + ) + state = polyline.get_state() + assert state['stroke_color'] == 'rgb(1,3,5)' + assert state['stroke_weight'] == 10.0 + assert state['stroke_opacity'] == 0.87 + + def test_invalid_stroke_opacity(self): + with self.assertRaises(traitlets.TraitError): + drawing.Polyline(self.path, stroke_opacity=-0.2) + with self.assertRaises(traitlets.TraitError): + drawing.Polyline(self.path, stroke_opacity=1.2) + with self.assertRaises(traitlets.TraitError): + drawing.Polyline(self.path, stroke_opacity='not-a-float') + + +class PolylineOptions(unittest.TestCase): + + def test_to_polyline_defaults(self): + path = [(10.0, 20.0), (5.0, 30.0), (-5.0, 10.0)] + expected_polyline = drawing.Polyline(path) + actual_polyline = drawing.PolylineOptions().to_polyline(path) + assert actual_polyline.path == expected_polyline.path + assert actual_polyline.stroke_color == expected_polyline.stroke_color + assert actual_polyline.stroke_weight == expected_polyline.stroke_weight + assert actual_polyline.stroke_opacity == expected_polyline.stroke_opacity + + class Circle(unittest.TestCase): def setUp(self): diff --git a/js/package.json b/js/package.json index ca32a2a..ff03d50 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "jupyter-gmaps", - "version": "0.9.1-dev", + "version": "0.10.0-dev", "description": "Google maps plugin for Jupyter notebooks", "author": "Pascal Bugnion", "main": "src/index.js", @@ -27,7 +27,7 @@ "clean:labextension": "rimraf ../gmaps/labextension", "format": "prettier src/*.js src/services/*.js --write", "lint": "prettier src/*.js src/services/*.js --list-different", - "update": "rimraf dist/ && npm run build", + "update": "rimraf dist/ && npm run build:all", "test": "jest" }, "devDependencies": { diff --git a/js/src/Drawing.js b/js/src/Drawing.js index c8563b5..3e44265 100644 --- a/js/src/Drawing.js +++ b/js/src/Drawing.js @@ -87,6 +87,17 @@ class DrawingMessages { return payload; } + static newPolyline(path) { + const payload = { + event: 'FEATURE_ADDED', + payload: { + featureType: 'POLYLINE', + path, + }, + }; + return payload; + } + static newCircle(center, radius) { const payload = { event: 'FEATURE_ADDED', @@ -281,6 +292,13 @@ export class DrawingLayerView extends GMapsLayerView { this._clickHandler = new PolygonClickHandler(map, path => this.send(DrawingMessages.newPolygon(path)) ); + } else if (mode === 'POLYLINE') { + if (this._clickHandler) { + this._clickHandler.remove(); + } + this._clickHandler = new PolylineClickHandler(map, path => + this.send(DrawingMessages.newPolyline(path)) + ); } else if (mode === 'CIRCLE') { if (this._clickHandler) { this._clickHandler.remove(); @@ -453,6 +471,78 @@ class PolygonClickHandler { } } +class PolylineClickHandler { + constructor(map, onNewPolyline) { + this.map = map; + this.currentPolyline = null; + this.map.setOptions({disableDoubleClickZoom: true}); + this._clickListener = map.addListener('click', event => { + const {latLng} = event; + if (this.currentPolyline === null) { + this.currentPolyline = this._createPolylineStartingAt(latLng); + } else { + this._finishCurrentLine(latLng); + } + }); + this._dblclickListener = map.addListener('dblclick', event => { + if (this.currentPolyline !== null) { + const path = this._completePolyline(); + this.currentPolyline.setMap(null); + this.currentPolyline = null; + if (path.length > 2) { + // Only dispatch an event if there are at + // least three points. Otherwise, it's + // likely to just be user error. + onNewPolyline(path); + } + } + }); + this._moveListener = map.addListener('mousemove', event => { + if (this.currentPolyline !== null) { + const {latLng} = event; + const currentPath = this.currentPolyline.getPath(); + currentPath.setAt(currentPath.getLength() - 1, latLng); + } + }); + } + + onNewFeatures(features) {} + + remove() { + this._clickListener.remove(); + this._dblclickListener.remove(); + this._moveListener.remove(); + if (this.currentPolyline) { + this.currentPolyline.setMap(null); + } + this.map.setOptions({disableDoubleClickZoom: false}); + } + + _createPolylineStartingAt(latLng) { + const path = new google.maps.MVCArray([latLng, latLng]); + const polyline = new google.maps.Polyline({path, clickable: false}); + polyline.setMap(this.map); + return polyline; + } + + _finishCurrentLine(latLng) { + const currentPath = this.currentPolyline.getPath(); + const lastLatLng = currentPath.getAt(currentPath.getLength() - 1); + currentPath.push(lastLatLng); + } + + _completePolyline() { + const currentPath = this.currentPolyline.getPath(); + const pathElems = currentPath + .getArray() + .map(point => latLngToArray(point)); + // last element is duplicate since we always introduce + // two new elements on click. + const path = _.initial(pathElems); + return path; + } +} + class CircleClickHandler { constructor(map, onNewCircle) { this.map = map; @@ -570,6 +660,11 @@ export class DrawingControlsView extends widgets.DOMWidgetView { "Drawing layer: switch to 'polygon' mode" ); this._createButtonEvent($polygonButton, 'POLYGON'); + const $polylineButton = this._createModeButton( + 'gmaps-icon polyline', + "Drawing layer: switch to 'polyline' mode" + ); + this._createButtonEvent($polylineButton, 'POLYLINE'); const $circleButton = this._createModeButton( 'fa fa-circle-o', "Drawing layer: switch to 'circle' mode" @@ -586,6 +681,7 @@ export class DrawingControlsView extends widgets.DOMWidgetView { MARKER: $markerButton, LINE: $lineButton, POLYGON: $polygonButton, + POLYLINE: $polylineButton, CIRCLE: $circleButton, DELETE: $deleteButton, }; @@ -595,6 +691,7 @@ export class DrawingControlsView extends widgets.DOMWidgetView { $markerButton, $lineButton, $polygonButton, + $polylineButton, $circleButton, $deleteButton ); diff --git a/js/src/Polyline.js b/js/src/Polyline.js new file mode 100644 index 0000000..44a4240 --- /dev/null +++ b/js/src/Polyline.js @@ -0,0 +1,77 @@ +import * as widgets from '@jupyter-widgets/base'; + +import {GMapsLayerView, GMapsLayerModel} from './GMapsLayer'; +import {arrayToLatLng} from './services/googleConverters'; + +export class PolylineModel extends GMapsLayerModel { + defaults() { + return { + ...super.defaults(), + _view_name: 'PolylineView', + _model_name: 'PolylineModel', + stroke_color: '#696969', + stroke_weight: 2, + stroke_opacity: 0.6, + }; + } +} + +export class PolylineView extends GMapsLayerView { + render() { + const points = this.model + .get('path') + .map(latLngArray => arrayToLatLng(latLngArray)); + const pathElems = points; + const path = new google.maps.MVCArray(pathElems); + const strokeColor = this.model.get('stroke_color'); + const strokeWeight = this.model.get('stroke_weight'); + const strokeOpacity = this.model.get('stroke_opacity'); + const polylineOptions = { + strokeColor, + strokeWeight, + strokeOpacity, + clickable: false, + }; + this.polyline = new google.maps.Polyline({ + path: path, + ...polylineOptions, + }); + this.polyline.addListener('click', event => this.trigger('click')); + this.modelEvents(); + } + + modelEvents() { + const properties = [ + ['strokeColor', 'stroke_color'], + ['strokeWeight', 'stroke_weight'], + ['strokeOpacity', 'stroke_opacity'], + ]; + + properties.forEach(([nameInView, nameInModel]) => { + const callback = () => { + this.polyline.setOptions({ + [nameInView]: this.model.get(nameInModel), + }); + }; + this.model.on(`change:${nameInModel}`, callback, this); + }); + } + + addToMapView(mapView) { + this.mapView = mapView; + this.polyline.setMap(mapView.map); + } + + removeFromMapView() { + this.mapView = null; + this.polyline.setMap(null); + } + + ensureClickable() { + this.polyline.setOptions({clickable: true}); + } + + restoreClickable() { + this.polyline.setOptions({clickable: false}); + } +} diff --git a/js/src/index.js b/js/src/index.js index 655b311..ad5a304 100644 --- a/js/src/index.js +++ b/js/src/index.js @@ -26,6 +26,7 @@ export * from './Traffic'; export * from './Drawing'; export * from './Line'; export * from './Polygon'; +export * from './Polyline'; export * from './Circle'; export {version} from '../package.json'; diff --git a/js/src/jupyter-gmaps.less b/js/src/jupyter-gmaps.less index ef28e0f..b5371f1 100644 --- a/js/src/jupyter-gmaps.less +++ b/js/src/jupyter-gmaps.less @@ -163,5 +163,10 @@ &.polygon:before { content: "\e901"; } + + &.polyline:before { + content: "\e902"; + } + }