Skip to content

Commit 435a85c

Browse files
author
Martin Journois
committed
Merge branch 'master' into features
Conflicts: folium/utilities.py
2 parents ee7817c + 9219daa commit 435a85c

File tree

10 files changed

+552
-314
lines changed

10 files changed

+552
-314
lines changed

.travis.yml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@ before_install:
1111
- wget http://bit.ly/miniconda -O miniconda.sh
1212
- bash miniconda.sh -b -p $HOME/miniconda
1313
- export PATH="$HOME/miniconda/bin:$PATH"
14-
- conda update --yes conda
15-
- travis_retry conda create --yes -n test $CONDA pip jinja2 pandas mock six nose
14+
- conda update --yes --all
15+
- travis_retry conda create --yes -n test $CONDA --file requirements.txt
1616
- source activate test
17+
- travis_retry conda install --yes pip mock pytest pandas
1718
- travis_retry pip install vincent
1819

19-
install:
20-
- python setup.py install
21-
2220
script:
23-
- cd tests && nosetests --verbose --nocapture folium_tests.py
21+
- python setup.py test
22+
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"## Introduction to `folium.plugins`"
8+
]
9+
},
10+
{
11+
"cell_type": "markdown",
12+
"metadata": {},
13+
"source": [
14+
"Just a small description of `plugins`, what they are, how they work, and how to implement one."
15+
]
16+
},
17+
{
18+
"cell_type": "markdown",
19+
"metadata": {},
20+
"source": [
21+
"### Template structure"
22+
]
23+
},
24+
{
25+
"cell_type": "markdown",
26+
"metadata": {},
27+
"source": [
28+
"To undestand how plugins work, you have to know how Folium template is made. Basically, it is made of four parts :\n",
29+
"\n",
30+
"* The **header** : where one defines imports of CSS stylesheets and Javascript external scripts. For example, the link to Leaflet library and stylesheets are there.\n",
31+
"* The **css** : where one defines specific styles.\n",
32+
"* The **html** body : where one defines the document object model. For example, the map(s)' `div`s are defined there.\n",
33+
"* The **javascript** : where the Leaflet script is written. For example, the map objects, layers, markers... are defined here.\n",
34+
"\n",
35+
"Now, a plugin is an object that inherits from `folium.plugins.Plugin`, whose goal is to write *things* into these four parts."
36+
]
37+
},
38+
{
39+
"cell_type": "markdown",
40+
"metadata": {},
41+
"source": [
42+
"### Quick and dirty example"
43+
]
44+
},
45+
{
46+
"cell_type": "markdown",
47+
"metadata": {},
48+
"source": [
49+
"This is an example of plugin, that imports a Javascript library, defines a css class, creates a new div, defines a Javascript object and add it to the map.\n",
50+
"```python\n",
51+
"from folium.plugins.plugin import Plugin\n",
52+
"\n",
53+
"class MyPlugin(Plugin):\n",
54+
" def __init__(self, data):\n",
55+
" \"\"\"Creates a MyPlugin plugin to append into a map with\n",
56+
" Map.add_plugin.\n",
57+
" \"\"\"\n",
58+
" super(MyPlugin, self).__init__() # We call Plugin.__init__.\n",
59+
" # This will (in particular) define self.object_id as a random hexadecimal string (unique).\n",
60+
"\n",
61+
" self.plugin_name = 'MyPlugin' # This will help to name variables in html and js.\n",
62+
"\n",
63+
" def render_header(self, nb):\n",
64+
" \"\"\"Generates the header part of the plugin.\"\"\"\n",
65+
" return \"\"\"\n",
66+
" <link rel=\"stylesheet\" href=\"https://myplugin_stylesheet.css\">\n",
67+
" <script src=\"https://myplugin_script.js\">\n",
68+
" \"\"\" if nb==0 else \"\"\n",
69+
"\n",
70+
" def render_css(self, nb):\n",
71+
" \"\"\"Generates the css part of the plugin.\"\"\"\n",
72+
" return \"\"\"\n",
73+
" #myplugin {\n",
74+
" color: #ff00ff,\n",
75+
" width: 314px\n",
76+
" };\n",
77+
" \"\"\"\n",
78+
"\n",
79+
" def render_html(self, nb):\n",
80+
" \"\"\"Generates the html part of the plugin.\"\"\"\n",
81+
" return \"\"\"\n",
82+
" <div id=\"myplugin{id}\"></div>\n",
83+
" \"\"\".format(id = self.object_name)\n",
84+
"\n",
85+
" def render_js(self, nb):\n",
86+
" \"\"\"Generates the Javascript part of the plugin.\"\"\"\n",
87+
" return \"\"\"\n",
88+
" var MyPlugin_{id} = myplugin_script.someObject();\n",
89+
" map.addLayer(MyPlugin_{id});\n",
90+
" \"\"\".format(id = self.object_name)\n",
91+
"```\n",
92+
"Note that you may be willing to put on a map several instances of the same plugin. But you don't want the header to be written several times. This is why each method has a `nb` argument, that will be incremented at each instance's render.\n",
93+
"Hence the line\n",
94+
"\n",
95+
" if nb==0 else \"\""
96+
]
97+
},
98+
{
99+
"cell_type": "markdown",
100+
"metadata": {},
101+
"source": [
102+
"### More sophisticated example"
103+
]
104+
},
105+
{
106+
"cell_type": "markdown",
107+
"metadata": {},
108+
"source": [
109+
"TODO : write something interesting here."
110+
]
111+
}
112+
],
113+
"metadata": {
114+
"kernelspec": {
115+
"display_name": "Python 2",
116+
"language": "python",
117+
"name": "python2"
118+
},
119+
"language_info": {
120+
"codemirror_mode": {
121+
"name": "ipython",
122+
"version": 2
123+
},
124+
"file_extension": ".py",
125+
"mimetype": "text/x-python",
126+
"name": "python",
127+
"nbconvert_exporter": "python",
128+
"pygments_lexer": "ipython2",
129+
"version": "2.7.10"
130+
}
131+
},
132+
"nbformat": 4,
133+
"nbformat_minor": 0
134+
}

folium/folium.py

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,8 @@ def simple_marker(self, location=None, popup=None,
359359
self.template_vars.setdefault(name, []).append(append)
360360

361361
@iter_obj('div_mark')
362-
def div_markers(self, locations=None, popups=None, marker_size=10, popup_width=300):
362+
def div_markers(self, locations=None, popups=None,
363+
marker_size=10, popup_width=300):
363364
"""Create a simple div marker on the map, with optional
364365
popup text or Vincent visualization. Useful for marking points along a
365366
line.
@@ -368,11 +369,13 @@ def div_markers(self, locations=None, popups=None, marker_size=10, popup_width=3
368369
----------
369370
locations: list of locations, where each location is an array
370371
Latitude and Longitude of Marker (Northing, Easting)
371-
popup: list of popups, each popup should be a string or tuple, default 'Pop Text'
372+
popup: list of popups, each popup should be a string or tuple.
373+
Default 'Pop Text'
372374
Input text or visualization for object. Can pass either text,
373375
or a tuple of the form (Vincent object, 'vis_path.json')
374376
It is possible to adjust the width of text/HTML popups
375-
using the optional keywords `popup_width`. (Leaflet default is 300px.)
377+
using the optional keywords `popup_width`.
378+
(Leaflet default is 300px.)
376379
marker_size
377380
default is 5
378381
@@ -382,13 +385,19 @@ def div_markers(self, locations=None, popups=None, marker_size=10, popup_width=3
382385
383386
Example
384387
-------
385-
>>>map.div_markers(locations=[[37.421114, -122.128314], [37.391637, -122.085416], [37.388832, -122.087709]], popups=['1437494575531', '1437492135937', '1437493590434'])
388+
>>> map.div_markers(locations=[[37.421114, -122.128314],
389+
... [37.391637, -122.085416],
390+
... [37.388832, -122.087709]],
391+
... popups=['1437494575531',
392+
... '1437492135937',
393+
... '1437493590434'])
386394
387395
"""
388396
call_cnt = self.mark_cnt['div_mark']
389397
if locations is None or popups is None:
390398
raise RuntimeError("Both locations and popups are mandatory")
391-
for (point_cnt, (location, popup)) in enumerate(zip(locations, popups)):
399+
for (point_cnt, (location, popup)) in enumerate(zip(locations,
400+
popups)):
392401
marker_num = 'div_marker_{0}_{1}'.format(call_cnt, point_cnt)
393402

394403
icon_temp = self.env.get_template('static_div_icon.js')
@@ -404,9 +413,12 @@ def div_markers(self, locations=None, popups=None, marker_size=10, popup_width=3
404413
'icon': "{'icon':"+icon_name+"}"
405414
})
406415

407-
popup_out = self._popup_render(popup=popup, mk_name='div_marker_{0}_'.format(call_cnt),
416+
mk_name = 'div_marker_{0}_'.format(call_cnt)
417+
popup_out = self._popup_render(popup=popup,
418+
mk_name=mk_name,
408419
count=point_cnt, width=popup_width)
409-
add_mark = 'map.addLayer(div_marker_{0}_{1})'.format(call_cnt, point_cnt)
420+
add_mark = 'map.addLayer(div_marker_{0}_{1})'.format(call_cnt,
421+
point_cnt)
410422
append = (icon, marker, popup_out, add_mark)
411423
self.template_vars.setdefault('div_markers', []).append(append)
412424

@@ -716,8 +728,8 @@ def add_plugin(self, plugin):
716728
Parameters
717729
----------
718730
plugin: folium.plugins object
719-
A plugin to be added to the map. It has to implement the methods
720-
`render_html`, `render_css` and `render_js`.
731+
A plugin to be added to the map. It has to implement the
732+
methods `render_html`, `render_css` and `render_js`.
721733
"""
722734
plugin.add_to_map(self)
723735

@@ -800,7 +812,7 @@ def _popup_render(self, popup=None, mk_name=None, count=None,
800812
else:
801813
width += 75
802814
height += 50
803-
max_width = self.map_size['width']
815+
max_width = max([self.map_size['width'], width])
804816
vega_id = '#' + div_id
805817
popup_temp = self.env.get_template('vega_marker.js')
806818
return popup_temp.render({'mark': mark, 'div_id': div_id,
@@ -1016,33 +1028,37 @@ def json_style(style_cnt, line_color, line_weight, line_opacity,
10161028

10171029
@iter_obj('image_overlay')
10181030
def image_overlay(self, data, opacity=0.25, min_lat=-90.0, max_lat=90.0,
1019-
min_lon=-180.0, max_lon=180.0, image_name=None, filename=None):
1020-
"""Simple image overlay of raster data from a numpy array. This is a lightweight
1021-
way to overlay geospatial data on top of a map. If your data is high res, consider
1022-
implementing a WMS server and adding a WMS layer.
1031+
min_lon=-180.0, max_lon=180.0, image_name=None,
1032+
filename=None):
1033+
"""
1034+
Simple image overlay of raster data from a numpy array. This is a
1035+
lightweight way to overlay geospatial data on top of a map. If your
1036+
data is high res, consider implementing a WMS server and adding a WMS
1037+
layer.
10231038
1024-
This function works by generating a PNG file from a numpy array. If you do not
1025-
specifiy a filename, it will embed the image inline. Otherwise, it saves the file in the
1026-
current directory, and then adds it as an image overlay layer in leaflet.js.
1027-
By default, the image is placed and stretched using bounds that cover the
1028-
entire globe.
1039+
This function works by generating a PNG file from a numpy array. If
1040+
you do not specify a filename, it will embed the image inline.
1041+
Otherwise, it saves the file in the current directory, and then adds
1042+
it as an image overlay layer in leaflet.js. By default, the image is
1043+
placed and stretched using bounds that cover the entire globe.
10291044
10301045
Parameters
10311046
----------
1032-
data: numpy array OR url string, required.
1033-
if numpy array, must be a image format, i.e., NxM (mono), NxMx3 (rgb), or NxMx4 (rgba)
1047+
data: numpy array OR url string, required.
1048+
if numpy array, must be a image format,
1049+
i.e., NxM (mono), NxMx3 (rgb), or NxMx4 (rgba)
10341050
if url, must be a valid url to a image (local or external)
10351051
opacity: float, default 0.25
1036-
Image layer opacity in range 0 (completely transparent) to 1 (opaque)
1052+
Image layer opacity in range 0 (transparent) to 1 (opaque)
10371053
min_lat: float, default -90.0
10381054
max_lat: float, default 90.0
10391055
min_lon: float, default -180.0
10401056
max_lon: float, default 180.0
10411057
image_name: string, default None
10421058
The name of the layer object in leaflet.js
10431059
filename: string, default None
1044-
Optional file name of output.png for image overlay. If None, we use a
1045-
inline PNG.
1060+
Optional file name of output.png for image overlay.
1061+
Use `None` for inline PNG.
10461062
10471063
Output
10481064
------
@@ -1053,18 +1069,17 @@ def image_overlay(self, data, opacity=0.25, min_lat=-90.0, max_lat=90.0,
10531069
# assumes a map object `m` has been created
10541070
>>> import numpy as np
10551071
>>> data = np.random.random((100,100))
1056-
1072+
10571073
# to make a rgba from a specific matplotlib colormap:
10581074
>>> import matplotlib.cm as cm
10591075
>>> cmapper = cm.cm.ColorMapper('jet')
10601076
>>> data2 = cmapper.to_rgba(np.random.random((100,100)))
1061-
1062-
# place the data over all of the globe (will be pretty pixelated!)
1077+
>>> # Place the data over all of the globe (will be pretty pixelated!)
10631078
>>> m.image_overlay(data)
1079+
>>> # Put it only over a single city (Paris).
1080+
>>> m.image_overlay(data, min_lat=48.80418, max_lat=48.90970,
1081+
... min_lon=2.25214, max_lon=2.44731)
10641082
1065-
# put it only over a single city (Paris)
1066-
>>> m.image_overlay(data, min_lat=48.80418, max_lat=48.90970, min_lon=2.25214, max_lon=2.44731)
1067-
10681083
"""
10691084

10701085
if isinstance(data, str):
@@ -1079,7 +1094,8 @@ def image_overlay(self, data, opacity=0.25, min_lat=-90.0, max_lat=90.0,
10791094
with open(filename, 'wb') as fd:
10801095
fd.write(png_str)
10811096
else:
1082-
filename = "data:image/png;base64,"+base64.b64encode(png_str).decode('utf-8')
1097+
png = "data:image/png;base64,{}".format
1098+
filename = png(base64.b64encode(png_str).decode('utf-8'))
10831099

10841100
if image_name not in self.added_layers:
10851101
if image_name is None:
@@ -1089,7 +1105,7 @@ def image_overlay(self, data, opacity=0.25, min_lat=-90.0, max_lat=90.0,
10891105
image_url = filename
10901106
image_bounds = [[min_lat, min_lon], [max_lat, max_lon]]
10911107
image_opacity = opacity
1092-
1108+
10931109
image_temp = self.env.get_template('image_layer.js')
10941110

10951111
image = image_temp.render({'image_name': image_name,
@@ -1099,7 +1115,7 @@ def image_overlay(self, data, opacity=0.25, min_lat=-90.0, max_lat=90.0,
10991115

11001116
self.template_vars['image_layers'].append(image)
11011117
self.added_layers.append(image_name)
1102-
1118+
11031119
def _build_map(self, html_templ=None, templ_type='string'):
11041120
self._auto_bounds()
11051121
"""Build HTML/JS/CSS from Templates given current map type."""
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://creativecommons.org/licenses/by-sa/3.0">CC BY SA</a>.
1+
Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
https://stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}.jpg
1+
https://stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}.png

0 commit comments

Comments
 (0)