Skip to content

Commit a6664e7

Browse files
committed
Merge pull request #230 from BibMartin/issue228
Create CustomIcon as suggested in #228
2 parents 8a6fca6 + 1115378 commit a6664e7

3 files changed

Lines changed: 131 additions & 29 deletions

File tree

folium/features.py

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@
77
"""
88
from jinja2 import Template
99
import json
10-
import base64
1110

1211
from .utilities import color_brewer, _parse_size, legend_scaler, _locations_mirror, _locations_tolist, write_png,\
13-
mercator_transform
12+
image_to_url
1413
from .six import text_type, binary_type
1514

1615
from .element import Element, Figure, JavascriptLink, CssLink, Div, MacroElement
@@ -557,43 +556,19 @@ def __init__(self, image, bounds, opacity=1., attribution=None, origin='upper',
557556
origin : ['upper' | 'lower'], optional, default 'upper'
558557
Place the [0,0] index of the array in the upper left or lower left
559558
corner of the axes.
560-
561559
colormap : callable, used only for `mono` image.
562560
Function of the form [x -> (r,g,b)] or [x -> (r,g,b,a)]
563561
for transforming a mono image into RGB.
564562
It must output iterables of length 3 or 4, with values between 0. and 1.
565563
Hint : you can use colormaps from `matplotlib.cm`.
566-
567564
mercator_project : bool, default False, used only for array-like image.
568565
Transforms the data to project (longitude,latitude) coordinates to the Mercator projection.
569566
"""
570567
super(ImageOverlay, self).__init__()
571568
self._name = 'ImageOverlay'
572569

573-
if hasattr(image,'read'):
574-
# We got an image file.
575-
if hasattr(image,'name'):
576-
# we try to get the image format from the file name.
577-
fileformat = image.name.lower().split('.')[-1]
578-
else:
579-
fileformat = 'png'
580-
self.url = "data:image/{};base64,{}".format(fileformat,
581-
base64.b64encode(image.read()).decode('utf-8'))
582-
elif (not (isinstance(image,text_type) or isinstance(image,binary_type))) and hasattr(image,'__iter__'):
583-
# We got an array-like object
584-
if mercator_project:
585-
data = mercator_transform(image,
586-
[bounds[0][0], bounds[1][0]],
587-
origin=origin)
588-
else:
589-
data = image
590-
self.url = "data:image/png;base64," +\
591-
base64.b64encode(write_png(data, origin=origin, colormap=colormap)).decode('utf-8')
592-
else:
593-
# We got an url
594-
self.url = json.loads(json.dumps(image))
570+
self.url = image_to_url(image, origin=origin, mercator_project=mercator_project, bounds=bounds)
595571

596-
self.url = self.url.replace('\n',' ')
597572
self.bounds = json.loads(json.dumps(bounds))
598573
options = {
599574
'opacity': opacity,
@@ -610,3 +585,60 @@ def __init__(self, image, bounds, opacity=1., attribution=None, origin='upper',
610585
).addTo({{this._parent.get_name()}});
611586
{% endmacro %}
612587
""")
588+
589+
class CustomIcon(Icon):
590+
def __init__(self, icon_image, icon_size=None, icon_anchor=None,
591+
shadow_image=None, shadow_size=None, shadow_anchor=None,
592+
popup_anchor=None):
593+
"""Create a custom icon, based on an image.
594+
595+
Parameters
596+
----------
597+
icon_image : string, file or array-like object
598+
The data you want to use as an icon.
599+
* If string, it will be written directly in the output file.
600+
* If file, it's content will be converted as embeded in the output file.
601+
* If array-like, it will be converted to PNG base64 string and embeded in the output.
602+
icon_size : tuple of 2 int
603+
Size of the icon image in pixels.
604+
icon_anchor : tuple of 2 int
605+
The coordinates of the "tip" of the icon (relative to its top left corner).
606+
The icon will be aligned so that this point is at the marker's geographical location.
607+
shadow_image : string, file or array-like object
608+
The data for the shadow image. If not specified, no shadow image will be created.
609+
shadow_size : tuple of 2 int
610+
Size of the shadow image in pixels.
611+
shadow_anchor : tuple of 2 int
612+
The coordinates of the "tip" of the shadow (relative to its top left corner)
613+
(the same as icon_anchor if not specified).
614+
popup_anchor : tuple of 2 int
615+
The coordinates of the point from which popups will "open", relative to the icon anchor.
616+
"""
617+
super(Icon, self).__init__()
618+
self._name = 'CustomIcon'
619+
self.icon_url = image_to_url(icon_image)
620+
self.icon_size = icon_size
621+
self.icon_anchor = icon_anchor
622+
623+
self.shadow_url = image_to_url(shadow_image) if shadow_image is not None else None
624+
self.shadow_size = shadow_size
625+
self.shadow_anchor = shadow_anchor
626+
self.popup_anchor = popup_anchor
627+
628+
self._template = Template(u"""
629+
{% macro script(this, kwargs) %}
630+
631+
var {{this.get_name()}} = L.icon({
632+
iconUrl: '{{this.icon_url}}',
633+
{% if this.icon_size %}iconSize: [{{this.icon_size[0]}},{{this.icon_size[1]}}],{% endif %}
634+
{% if this.icon_anchor %}iconAnchor: [{{this.icon_anchor[0]}},{{this.icon_anchor[1]}}],{% endif %}
635+
636+
{% if this.shadow_url %}shadowUrl: '{{this.shadow_url}}',{% endif %}
637+
{% if this.shadow_size %}shadowSize: [{{this.shadow_size[0]}},{{this.shadow_size[1]}}],{% endif %}
638+
{% if this.shadow_anchor %}shadowAnchor: [{{this.shadow_anchor[0]}},{{this.shadow_anchor[1]}}],{% endif %}
639+
640+
{% if this.popup_anchor %}popupAnchor: [{{this.popup_anchor[0]}},{{this.popup_anchor[1]}}],{% endif %}
641+
});
642+
{{this._parent.get_name()}}.setIcon({{this.get_name()}});
643+
{% endmacro %}
644+
""")

folium/utilities.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import math
1616
import zlib
1717
import struct
18+
import json
19+
import base64
1820
from jinja2 import Environment, PackageLoader
1921

2022
try:
@@ -27,7 +29,7 @@
2729
except ImportError:
2830
np = None
2931

30-
from folium.six import iteritems
32+
from folium.six import iteritems, text_type, binary_type
3133

3234

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

364366
return out
365367

368+
def image_to_url(image, mercator_project=False, colormap=None, origin='upper', bounds=((-90,-180),(90,180))):
369+
"""Infers the type of an image argument and transforms it into a url.
370+
371+
Parameters
372+
----------
373+
image: string, file or array-like object
374+
* If string, it will be written directly in the output file.
375+
* If file, it's content will be converted as embeded in the output file.
376+
* If array-like, it will be converted to PNG base64 string and embeded in the output.
377+
origin : ['upper' | 'lower'], optional, default 'upper'
378+
Place the [0,0] index of the array in the upper left or lower left
379+
corner of the axes.
380+
colormap : callable, used only for `mono` image.
381+
Function of the form [x -> (r,g,b)] or [x -> (r,g,b,a)]
382+
for transforming a mono image into RGB.
383+
It must output iterables of length 3 or 4, with values between 0. and 1.
384+
Hint : you can use colormaps from `matplotlib.cm`.
385+
mercator_project : bool, default False, used only for array-like image.
386+
Transforms the data to project (longitude,latitude) coordinates to the Mercator projection.
387+
bounds: list-like, default ((-90,-180),(90,180))
388+
Image bounds on the map in the form [[lat_min, lon_min], [lat_max, lon_max]].
389+
Only used if mercator_project is True.
390+
"""
391+
if hasattr(image,'read'):
392+
# We got an image file.
393+
if hasattr(image,'name'):
394+
# we try to get the image format from the file name.
395+
fileformat = image.name.lower().split('.')[-1]
396+
else:
397+
fileformat = 'png'
398+
url = "data:image/{};base64,{}".format(fileformat,
399+
base64.b64encode(image.read()).decode('utf-8'))
400+
elif (not (isinstance(image,text_type) or isinstance(image,binary_type))) and hasattr(image,'__iter__'):
401+
# We got an array-like object
402+
if mercator_project:
403+
data = mercator_transform(image,
404+
[bounds[0][0], bounds[1][0]],
405+
origin=origin)
406+
else:
407+
data = image
408+
url = "data:image/png;base64," +\
409+
base64.b64encode(write_png(data, origin=origin, colormap=colormap)).decode('utf-8')
410+
else:
411+
# We got an url
412+
url = json.loads(json.dumps(image))
413+
414+
return url.replace('\n',' ')
415+
366416
def write_png(data, origin='upper', colormap=None):
367417
"""
368418
Tranform an array of data into a PNG string.

tests/test_folium.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -700,7 +700,7 @@ def test_image_overlay(self):
700700
assert ''.join(image_rendered.split()) in ''.join(out.split())
701701

702702
self.setup()
703-
self.map.image_overlay(data)
703+
self.map.image_overlay(data, mercator_project=True)
704704
out = self.map._parent.render()
705705

706706
imageoverlay = [val for key, val in self.map._children.items() if
@@ -713,3 +713,23 @@ def test_image_overlay(self):
713713
'image_opacity': image_opacity})
714714

715715
assert ''.join(image_rendered.split()) in ''.join(out.split())
716+
717+
def test_custom_icon(self):
718+
"""Test CustomIcon."""
719+
self.setup()
720+
721+
icon_image = "http://leafletjs.com/docs/images/leaf-green.png"
722+
shadow_image = "http://leafletjs.com/docs/images/leaf-shadow.png"
723+
724+
self.map = folium.Map([45,-100], zoom_start=4)
725+
i = folium.features.CustomIcon(icon_image,
726+
icon_size=(38,95),
727+
icon_anchor=(22,94),
728+
shadow_image=shadow_image,
729+
shadow_size=(50,64),
730+
shadow_anchor=(4,62),
731+
popup_anchor=(-3,-76),
732+
)
733+
mk = folium.map.Marker([45,-100], icon=i, popup=folium.map.Popup('Hello'))
734+
self.map.add_children(mk)
735+
out = self.map._parent.render()

0 commit comments

Comments
 (0)