From 5086929b126182a41345f04805b61ff2118f59e6 Mon Sep 17 00:00:00 2001 From: Sagar Mishra <54197164+achieveordie@users.noreply.github.com> Date: Tue, 7 May 2024 22:02:35 +0530 Subject: [PATCH] Add `Leaflet.encoded` plugin: Enable creating PolyLine and Polygon from encoded string (#1928) * add plugin to visualize the polyline from an encoded string * correct import in user guide * Update polyline_encoded.py Added type information to for argument to `__init__` * rework encoded plugin to include PolygonFromEncoded * include doc and tests for PolygonFromEncoded * update doc to include the link of the algo * use path_options instead of parse_options * set path_options to an attribute --------- Co-authored-by: Hans Then --- docs/user_guide/plugins.rst | 6 + docs/user_guide/plugins/polygon_encoded.md | 22 ++++ docs/user_guide/plugins/polyline_encoded.md | 22 ++++ folium/plugins/__init__.py | 3 + folium/plugins/encoded.py | 121 ++++++++++++++++++++ tests/plugins/test_encoded.py | 82 +++++++++++++ 6 files changed, 256 insertions(+) create mode 100644 docs/user_guide/plugins/polygon_encoded.md create mode 100644 docs/user_guide/plugins/polyline_encoded.md create mode 100644 folium/plugins/encoded.py create mode 100644 tests/plugins/test_encoded.py diff --git a/docs/user_guide/plugins.rst b/docs/user_guide/plugins.rst index 28f06d62f..e9e18324f 100644 --- a/docs/user_guide/plugins.rst +++ b/docs/user_guide/plugins.rst @@ -22,6 +22,8 @@ Plugins plugins/measure_control plugins/mouse_position plugins/pattern + plugins/polygon_encoded + plugins/polyline_encoded plugins/polyline_offset plugins/polyline_textpath plugins/realtime @@ -80,6 +82,10 @@ Plugins - A control that displays geographic coordinates of the mouse pointer, as it is moved over the map. * - :doc:`Pattern ` - Add support for pattern fills on Paths. + * - :doc:`Polygon Encoded ` + - Draw a polygon directly from an encoded string. + * - :doc:`Polyline Encoded ` + - Draw a polyline directly from an encoded string. * - :doc:`Polyline Offset ` - Shift relative pixel offset, without actually changing the actual latitude longitude values. * - :doc:`Polyline Textpath ` diff --git a/docs/user_guide/plugins/polygon_encoded.md b/docs/user_guide/plugins/polygon_encoded.md new file mode 100644 index 000000000..8024abc47 --- /dev/null +++ b/docs/user_guide/plugins/polygon_encoded.md @@ -0,0 +1,22 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +from folium import plugins +``` + +# PolygonFromEncoded + +Create a Polygon directly from an encoded polyline string. To understand the encoding algorithm +refer to [this](https://developers.google.com/maps/documentation/utilities/polylinealgorithm) link. + +```{code-cell} ipython3 + +m = folium.Map(location=[40, -80], zoom_start=5) + +encoded = r"w`j~FpxivO}jz@qnnCd}~Bsa{@~f`C`lkH" +plugins.PolygonFromEncoded(encoded=encoded).add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/polyline_encoded.md b/docs/user_guide/plugins/polyline_encoded.md new file mode 100644 index 000000000..2826fd958 --- /dev/null +++ b/docs/user_guide/plugins/polyline_encoded.md @@ -0,0 +1,22 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +from folium import plugins +``` + +# PolyLineFromEncoded + +Create a PolyLine directly from an encoded polyline string. To understand the encoding algorithm +refer to [this](https://developers.google.com/maps/documentation/utilities/polylinealgorithm) link. + +```{code-cell} ipython3 + +m = folium.Map(location=[40, -120], zoom_start=5) + +encoded = r"_p~iF~cn~U_ulLn{vA_mqNvxq`@" +plugins.PolyLineFromEncoded(encoded=encoded).add_to(m) + +m +``` diff --git a/folium/plugins/__init__.py b/folium/plugins/__init__.py index 79169002d..ad857f48a 100644 --- a/folium/plugins/__init__.py +++ b/folium/plugins/__init__.py @@ -5,6 +5,7 @@ from folium.plugins.boat_marker import BoatMarker from folium.plugins.draw import Draw from folium.plugins.dual_map import DualMap +from folium.plugins.encoded import PolygonFromEncoded, PolyLineFromEncoded from folium.plugins.fast_marker_cluster import FastMarkerCluster from folium.plugins.feature_group_sub_group import FeatureGroupSubGroup from folium.plugins.float_image import FloatImage @@ -55,6 +56,8 @@ "MeasureControl", "MiniMap", "MousePosition", + "PolygonFromEncoded", + "PolyLineFromEncoded", "PolyLineTextPath", "PolyLineOffset", "Realtime", diff --git a/folium/plugins/encoded.py b/folium/plugins/encoded.py new file mode 100644 index 000000000..14f0adaa3 --- /dev/null +++ b/folium/plugins/encoded.py @@ -0,0 +1,121 @@ +from abc import ABC, abstractmethod + +from jinja2 import Template + +from folium.elements import JSCSSMixin +from folium.features import MacroElement +from folium.vector_layers import path_options + + +class _BaseFromEncoded(JSCSSMixin, MacroElement, ABC): + """Base Interface to create folium objects from encoded strings. + + Derived classes must define `_encoding_type` property which returns the string + representation of the folium object to create from the encoded string. + + Parameters + ---------- + encoded: str + The raw encoded string from the Polyline Encoding Algorithm. See: + https://developers.google.com/maps/documentation/utilities/polylinealgorithm + **kwargs: + Object options as accepted by leaflet. + """ + + _template = Template( + """ + {% macro script(this, kwargs) %} + + var {{ this.get_name() }} = L.{{ this._encoding_type }}.fromEncoded( + {{ this.encoded|tojson }}, + {{ this.options|tojson }} + ).addTo({{ this._parent.get_name() }}); + + {% endmacro %} + """ + ) + + default_js = [ + ( + "polyline-encoded", + "https://cdn.jsdelivr.net/npm/polyline-encoded@0.0.9/Polyline.encoded.js", + ) + ] + + def __init__(self, encoded: str): + super().__init__() + self.encoded = encoded + + @property + @abstractmethod + def _encoding_type(self) -> str: + """An abstract getter to return the type of folium object to create.""" + raise NotImplementedError + + +class PolyLineFromEncoded(_BaseFromEncoded): + """Create PolyLines directly from the encoded string. + + Parameters + ---------- + encoded: str + The raw encoded string from the Polyline Encoding Algorithm. See: + https://developers.google.com/maps/documentation/utilities/polylinealgorithm + **kwargs: + Polyline options as accepted by leaflet. See: + https://leafletjs.com/reference.html#polyline + + Adapted from https://github.com/jieter/Leaflet.encoded + + Examples + -------- + >>> from folium import Map + >>> from folium.plugins import PolyLineFromEncoded + >>> m = Map() + >>> encoded = r"_p~iF~cn~U_ulLn{vA_mqNvxq`@" + >>> PolyLineFromEncoded(encoded=encoded, color="green").add_to(m) + """ + + def __init__(self, encoded: str, **kwargs): + self._name = "PolyLineFromEncoded" + super().__init__(encoded=encoded) + self.options = path_options(line=True, **kwargs) + + @property + def _encoding_type(self) -> str: + """Return the name of folium object created from the encoded.""" + return "Polyline" + + +class PolygonFromEncoded(_BaseFromEncoded): + """Create Polygons directly from the encoded string. + + Parameters + ---------- + encoded: str + The raw encoded string from the Polyline Encoding Algorithm. See: + https://developers.google.com/maps/documentation/utilities/polylinealgorithm + **kwargs: + Polygon options as accepted by leaflet. See: + https://leafletjs.com/reference.html#polygon + + Adapted from https://github.com/jieter/Leaflet.encoded + + Examples + -------- + >>> from folium import Map + >>> from folium.plugins import PolygonFromEncoded + >>> m = Map() + >>> encoded = r"w`j~FpxivO}jz@qnnCd}~Bsa{@~f`C`lkH" + >>> PolygonFromEncoded(encoded=encoded).add_to(m) + """ + + def __init__(self, encoded: str, **kwargs): + self._name = "PolygonFromEncoded" + super().__init__(encoded) + self.options = path_options(line=True, radius=None, **kwargs) + + @property + def _encoding_type(self) -> str: + """Return the name of folium object created from the encoded.""" + return "Polygon" diff --git a/tests/plugins/test_encoded.py b/tests/plugins/test_encoded.py new file mode 100644 index 000000000..3377548a0 --- /dev/null +++ b/tests/plugins/test_encoded.py @@ -0,0 +1,82 @@ +"""Test PolyLineFromEncoded Plugin.""" + +from jinja2 import Template + +from folium import Map +from folium.plugins import PolygonFromEncoded, PolyLineFromEncoded +from folium.utilities import normalize + + +def test_polyline_from_encoded(): + """Test `PolyLineFromEncoded` plugin. + + The test ensures: + - The original JS script is present in the HTML file. + - The rendering from `PolyLineFromEncoded` and the original plugin gives the + same output. + """ + + m = Map([35.0, -120.0], zoom_start=3) + + encoded = r"_p~iF~cn~U_ulLn{vA_mqNvxq`@" + kwargs = {"color": "green"} + polyline = PolyLineFromEncoded(encoded=encoded, **kwargs) + + polyline.add_to(m) + + out = normalize(m._parent.render()) + + script = '' + assert script in out + + tmpl = Template( + """ + var {{this.get_name()}} = L.Polyline.fromEncoded( + {{ this.encoded|tojson }}, + {{ this.options|tojson }} + ).addTo({{this._parent.get_name()}}); + """ + ) + + expected_render = tmpl.render(this=polyline) + actual_render = polyline._template.module.script(polyline) + + assert normalize(expected_render) == normalize(actual_render) + + +def test_polygon_from_encoded(): + """Test `PolygonFromEncoded` plugin. + + The test ensures: + - The original JS script is present in the HTML file. + - The rendering from `PolygonFromEncoded` and the original plugin gives the + same output. + """ + + m = Map([40.0, -80.0], zoom_start=3) + + encoded = r"w`j~FpxivO}jz@qnnCd}~Bsa{@~f`C`lkH" + polygon = PolygonFromEncoded(encoded=encoded, kwargs={}) + + polygon.add_to(m) + + out = normalize(m._parent.render()) + + script = '' + assert script in out + + tmpl = Template( + """ + var {{this.get_name()}} = L.Polygon.fromEncoded( + {{ this.encoded|tojson }}, + {{ this.options|tojson }} + ) + .addTo({{this._parent.get_name()}}); + """ + ) + + expected_render = tmpl.render(this=polygon) + + actual_render = polygon._template.module.script(polygon) + + assert normalize(expected_render) == normalize(actual_render)