Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 59 additions & 27 deletions folium/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
"""
from jinja2 import Template
import json
import base64

from .utilities import color_brewer, _parse_size, legend_scaler, _locations_mirror, _locations_tolist, write_png,\
mercator_transform
image_to_url
from .six import text_type, binary_type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We needed this re-factoring! Thanks!!

@andrewgiessel this is your baby. Can take a quick look at it before we merge this?


from .element import Element, Figure, JavascriptLink, CssLink, Div, MacroElement
Expand Down Expand Up @@ -557,43 +556,19 @@ def __init__(self, image, bounds, opacity=1., attribution=None, origin='upper',
origin : ['upper' | 'lower'], optional, default 'upper'
Place the [0,0] index of the array in the upper left or lower left
corner of the axes.

colormap : callable, used only for `mono` image.
Function of the form [x -> (r,g,b)] or [x -> (r,g,b,a)]
for transforming a mono image into RGB.
It must output iterables of length 3 or 4, with values between 0. and 1.
Hint : you can use colormaps from `matplotlib.cm`.

mercator_project : bool, default False, used only for array-like image.
Transforms the data to project (longitude,latitude) coordinates to the Mercator projection.
"""
super(ImageOverlay, self).__init__()
self._name = 'ImageOverlay'

if hasattr(image,'read'):
# We got an image file.
if hasattr(image,'name'):
# we try to get the image format from the file name.
fileformat = image.name.lower().split('.')[-1]
else:
fileformat = 'png'
self.url = "data:image/{};base64,{}".format(fileformat,
base64.b64encode(image.read()).decode('utf-8'))
elif (not (isinstance(image,text_type) or isinstance(image,binary_type))) and hasattr(image,'__iter__'):
# We got an array-like object
if mercator_project:
data = mercator_transform(image,
[bounds[0][0], bounds[1][0]],
origin=origin)
else:
data = image
self.url = "data:image/png;base64," +\
base64.b64encode(write_png(data, origin=origin, colormap=colormap)).decode('utf-8')
else:
# We got an url
self.url = json.loads(json.dumps(image))
self.url = image_to_url(image, origin=origin, mercator_project=mercator_project, bounds=bounds)

self.url = self.url.replace('\n',' ')
self.bounds = json.loads(json.dumps(bounds))
options = {
'opacity': opacity,
Expand All @@ -610,3 +585,60 @@ def __init__(self, image, bounds, opacity=1., attribution=None, origin='upper',
).addTo({{this._parent.get_name()}});
{% endmacro %}
""")

class CustomIcon(Icon):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel so ashamed for wasting an afternoon doing this 😳

This class is pure art!

def __init__(self, icon_image, icon_size=None, icon_anchor=None,
shadow_image=None, shadow_size=None, shadow_anchor=None,
popup_anchor=None):
"""Create a custom icon, based on an image.

Parameters
----------
icon_image : string, file or array-like object
The data you want to use as an icon.
* If string, it will be written directly in the output file.
* If file, it's content will be converted as embeded in the output file.
* If array-like, it will be converted to PNG base64 string and embeded in the output.
icon_size : tuple of 2 int
Size of the icon image in pixels.
icon_anchor : tuple of 2 int
The coordinates of the "tip" of the icon (relative to its top left corner).
The icon will be aligned so that this point is at the marker's geographical location.
shadow_image : string, file or array-like object
The data for the shadow image. If not specified, no shadow image will be created.
shadow_size : tuple of 2 int
Size of the shadow image in pixels.
shadow_anchor : tuple of 2 int
The coordinates of the "tip" of the shadow (relative to its top left corner)
(the same as icon_anchor if not specified).
popup_anchor : tuple of 2 int
The coordinates of the point from which popups will "open", relative to the icon anchor.
"""
super(Icon, self).__init__()
self._name = 'CustomIcon'
self.icon_url = image_to_url(icon_image)
self.icon_size = icon_size
self.icon_anchor = icon_anchor

self.shadow_url = image_to_url(shadow_image) if shadow_image is not None else None
self.shadow_size = shadow_size
self.shadow_anchor = shadow_anchor
self.popup_anchor = popup_anchor

self._template = Template(u"""
{% macro script(this, kwargs) %}

var {{this.get_name()}} = L.icon({
iconUrl: '{{this.icon_url}}',
{% if this.icon_size %}iconSize: [{{this.icon_size[0]}},{{this.icon_size[1]}}],{% endif %}
{% if this.icon_anchor %}iconAnchor: [{{this.icon_anchor[0]}},{{this.icon_anchor[1]}}],{% endif %}

{% if this.shadow_url %}shadowUrl: '{{this.shadow_url}}',{% endif %}
{% if this.shadow_size %}shadowSize: [{{this.shadow_size[0]}},{{this.shadow_size[1]}}],{% endif %}
{% if this.shadow_anchor %}shadowAnchor: [{{this.shadow_anchor[0]}},{{this.shadow_anchor[1]}}],{% endif %}

{% if this.popup_anchor %}popupAnchor: [{{this.popup_anchor[0]}},{{this.popup_anchor[1]}}],{% endif %}
});
{{this._parent.get_name()}}.setIcon({{this.get_name()}});
{% endmacro %}
""")
52 changes: 51 additions & 1 deletion folium/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import math
import zlib
import struct
import json
import base64
from jinja2 import Environment, PackageLoader

try:
Expand All @@ -27,7 +29,7 @@
except ImportError:
np = None

from folium.six import iteritems
from folium.six import iteritems, text_type, binary_type


def get_templates():
Expand Down Expand Up @@ -363,6 +365,54 @@ def mercator_transform(data, lat_bounds, origin='upper', height_out=None):

return out

def image_to_url(image, mercator_project=False, colormap=None, origin='upper', bounds=((-90,-180),(90,180))):
"""Infers the type of an image argument and transforms it into a url.

Parameters
----------
image: string, file or array-like object
* If string, it will be written directly in the output file.
* If file, it's content will be converted as embeded in the output file.
* If array-like, it will be converted to PNG base64 string and embeded in the output.
origin : ['upper' | 'lower'], optional, default 'upper'
Place the [0,0] index of the array in the upper left or lower left
corner of the axes.
colormap : callable, used only for `mono` image.
Function of the form [x -> (r,g,b)] or [x -> (r,g,b,a)]
for transforming a mono image into RGB.
It must output iterables of length 3 or 4, with values between 0. and 1.
Hint : you can use colormaps from `matplotlib.cm`.
mercator_project : bool, default False, used only for array-like image.
Transforms the data to project (longitude,latitude) coordinates to the Mercator projection.
bounds: list-like, default ((-90,-180),(90,180))
Image bounds on the map in the form [[lat_min, lon_min], [lat_max, lon_max]].
Only used if mercator_project is True.
"""
if hasattr(image,'read'):
# We got an image file.
if hasattr(image,'name'):
# we try to get the image format from the file name.
fileformat = image.name.lower().split('.')[-1]
else:
fileformat = 'png'
url = "data:image/{};base64,{}".format(fileformat,
base64.b64encode(image.read()).decode('utf-8'))
elif (not (isinstance(image,text_type) or isinstance(image,binary_type))) and hasattr(image,'__iter__'):
# We got an array-like object
if mercator_project:
data = mercator_transform(image,
[bounds[0][0], bounds[1][0]],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bounds is not defined here- needs to be passed into from the init method of ImageOverlay

origin=origin)
else:
data = image
url = "data:image/png;base64," +\
base64.b64encode(write_png(data, origin=origin, colormap=colormap)).decode('utf-8')
else:
# We got an url
url = json.loads(json.dumps(image))

return url.replace('\n',' ')

def write_png(data, origin='upper', colormap=None):
"""
Tranform an array of data into a PNG string.
Expand Down
22 changes: 21 additions & 1 deletion tests/test_folium.py
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,7 @@ def test_image_overlay(self):
assert ''.join(image_rendered.split()) in ''.join(out.split())

self.setup()
self.map.image_overlay(data)
self.map.image_overlay(data, mercator_project=True)
out = self.map._parent.render()

imageoverlay = [val for key, val in self.map._children.items() if
Expand All @@ -711,3 +711,23 @@ def test_image_overlay(self):
'image_opacity': image_opacity})

assert ''.join(image_rendered.split()) in ''.join(out.split())

def test_custom_icon(self):
"""Test CustomIcon."""
self.setup()

icon_image = "http://leafletjs.com/docs/images/leaf-green.png"
shadow_image = "http://leafletjs.com/docs/images/leaf-shadow.png"

self.map = folium.Map([45,-100], zoom_start=4)
i = folium.features.CustomIcon(icon_image,
icon_size=(38,95),
icon_anchor=(22,94),
shadow_image=shadow_image,
shadow_size=(50,64),
shadow_anchor=(4,62),
popup_anchor=(-3,-76),
)
mk = folium.map.Marker([45,-100], icon=i, popup=folium.map.Popup('Hello'))
self.map.add_children(mk)
out = self.map._parent.render()