Skip to content

Commit

Permalink
Add Leaflet.encoded plugin: Enable creating PolyLine and Polygon fr…
Browse files Browse the repository at this point in the history
…om 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 <hans.then@gmail.com>
  • Loading branch information
achieveordie and hansthen authored May 7, 2024
1 parent 55a758a commit 5086929
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 0 deletions.
6 changes: 6 additions & 0 deletions docs/user_guide/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -80,6 +82,10 @@ Plugins
- A control that displays geographic coordinates of the mouse pointer, as it is moved over the map.
* - :doc:`Pattern <plugins/pattern>`
- Add support for pattern fills on Paths.
* - :doc:`Polygon Encoded <plugins/polygon_encoded>`
- Draw a polygon directly from an encoded string.
* - :doc:`Polyline Encoded <plugins/polyline_encoded>`
- Draw a polyline directly from an encoded string.
* - :doc:`Polyline Offset <plugins/polyline_offset>`
- Shift relative pixel offset, without actually changing the actual latitude longitude values.
* - :doc:`Polyline Textpath <plugins/polyline_textpath>`
Expand Down
22 changes: 22 additions & 0 deletions docs/user_guide/plugins/polygon_encoded.md
Original file line number Diff line number Diff line change
@@ -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
```
22 changes: 22 additions & 0 deletions docs/user_guide/plugins/polyline_encoded.md
Original file line number Diff line number Diff line change
@@ -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
```
3 changes: 3 additions & 0 deletions folium/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -55,6 +56,8 @@
"MeasureControl",
"MiniMap",
"MousePosition",
"PolygonFromEncoded",
"PolyLineFromEncoded",
"PolyLineTextPath",
"PolyLineOffset",
"Realtime",
Expand Down
121 changes: 121 additions & 0 deletions folium/plugins/encoded.py
Original file line number Diff line number Diff line change
@@ -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"
82 changes: 82 additions & 0 deletions tests/plugins/test_encoded.py
Original file line number Diff line number Diff line change
@@ -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 = '<script src="https://cdn.jsdelivr.net/npm/polyline-encoded@0.0.9/Polyline.encoded.js"></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 = '<script src="https://cdn.jsdelivr.net/npm/polyline-encoded@0.0.9/Polyline.encoded.js"></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)

0 comments on commit 5086929

Please sign in to comment.