From 95a8c0f281f2e28c135288ca0a8d5c48eb9819c0 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Mon, 7 Sep 2015 12:04:47 +0200 Subject: [PATCH 01/36] Break folium.py and start plugging Elements --- folium/features.py | 4 +- folium/folium.py | 2320 +++++++++++++++++++++--------------------- folium/map.py | 2 + tests/test_folium.py | 66 +- 4 files changed, 1200 insertions(+), 1192 deletions(-) diff --git a/folium/features.py b/folium/features.py index 07748bfe61..c4f62c0da5 100644 --- a/folium/features.py +++ b/folium/features.py @@ -40,8 +40,8 @@ def __init__(self, url, name=None, { format:'{{ this.format }}', transparent: {{ this.transparent.__str__().lower() }}, - layers:'{{ this.layers }}', - attribution:'{{this.attribution}}' + layers:'{{ this.layers }}' + {% if this.attribution %}, attribution:'{{this.attribution}}'{% endif %} } ).addTo({{this._parent.get_name()}}); diff --git a/folium/folium.py b/folium/folium.py index 83c9f0377b..5a7f5f41be 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -7,248 +7,244 @@ """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import division +#from __future__ import absolute_import +#from __future__ import print_function +#from __future__ import division -import codecs -import functools -import json -from uuid import uuid4 +import warnings +#import codecs +#import functools +#import json +#from uuid import uuid4 -from jinja2 import Environment, PackageLoader -from pkg_resources import resource_string +#from jinja2 import Environment, PackageLoader +#from pkg_resources import resource_string -from folium import utilities -from folium.six import text_type, binary_type, iteritems +#from folium import utilities +#from folium.six import text_type, binary_type, iteritems +from .map import Map as _Map +from .features import WmsTileLayer -import sys -import base64 +#import sys +#import base64 -ENV = Environment(loader=PackageLoader('folium', 'templates')) +#ENV = Environment(loader=PackageLoader('folium', 'templates')) def initialize_notebook(): """Initialize the IPython notebook display elements.""" - try: - from IPython.core.display import display, HTML - except ImportError: - print("IPython Notebook could not be loaded.") - - lib_css = ENV.get_template('ipynb_init_css.html') - lib_js = ENV.get_template('ipynb_init_js.html') - leaflet_dvf = ENV.get_template('leaflet-dvf.markers.min.js') - - display(HTML(lib_css.render())) - display(HTML(lib_js.render({'leaflet_dvf': leaflet_dvf.render()}))) - - -def iter_obj(type): - """Decorator to keep count of different map object types in self.mk_cnt.""" - def decorator(func): - @functools.wraps(func) - def wrapper(self, *args, **kwargs): - self.mark_cnt[type] = self.mark_cnt.get(type, 0) + 1 - func_result = func(self, *args, **kwargs) - return func_result - return wrapper - return decorator - - -class Map(object): - """Create a Map with Folium.""" - - def __init__(self, location=None, width='100%', height='100%', - tiles='OpenStreetMap', API_key=None, max_zoom=18, min_zoom=1, - zoom_start=10, attr=None, min_lat=-90, max_lat=90, - min_lon=-180, max_lon=180): - """Create a Map with Folium and Leaflet.js - - Generate a base map of given width and height with either default - tilesets or a custom tileset URL. The following tilesets are built-in - to Folium. Pass any of the following to the "tiles" keyword: - - "OpenStreetMap" - - "MapQuest Open" - - "MapQuest Open Aerial" - - "Mapbox Bright" (Limited levels of zoom for free tiles) - - "Mapbox Control Room" (Limited levels of zoom for free tiles) - - "Stamen" (Terrain, Toner, and Watercolor) - - "Cloudmade" (Must pass API key) - - "Mapbox" (Must pass API key) - - "CartoDB" (positron and dark_matter) - You can pass a custom tileset to Folium by passing a Leaflet-style - URL to the tiles parameter: - http://{s}.yourtiles.com/{z}/{x}/{y}.png - - Parameters - ---------- - location: tuple or list, default None - Latitude and Longitude of Map (Northing, Easting). - width: pixel int or percentage string (default: '100%') - Width of the map. - height: pixel int or percentage string (default: '100%') - Height of the map. - tiles: str, default 'OpenStreetMap' - Map tileset to use. Can use defaults or pass a custom URL. - API_key: str, default None - API key for Cloudmade or Mapbox tiles. - max_zoom: int, default 18 - Maximum zoom depth for the map. - zoom_start: int, default 10 - Initial zoom level for the map. - attr: string, default None - Map tile attribution; only required if passing custom tile URL. - - Returns - ------- - Folium Map Object - - Examples - -------- - >>>map = folium.Map(location=[45.523, -122.675], width=750, height=500) - >>>map = folium.Map(location=[45.523, -122.675], - tiles='Mapbox Control Room') - >>>map = folium.Map(location=(45.523, -122.675), max_zoom=20, - tiles='Cloudmade', API_key='YourKey') - >>>map = folium.Map(location=[45.523, -122.675], zoom_start=2, - tiles=('http://{s}.tiles.mapbox.com/v3/' - 'mapbox.control-room/{z}/{x}/{y}.png'), - attr='Mapbox attribution') - - """ - - # Inits. - self.map_path = None - self.render_iframe = False - self.map_type = 'base' - self.map_id = '_'.join(['folium', uuid4().hex]) - - # Mark counter, JSON, Plugins. - self.mark_cnt = {} - self.json_data = {} - self.plugins = {} - - # No location means we will use automatic bounds and ignore zoom - self.location = location - - # If location is not passed, we center the map at 0,0 - if not location: - location = [0, 0] - zoom_start = min_zoom - - # Map Size Parameters. - try: - if isinstance(width, int): - width_type = 'px' - assert width > 0 - else: - width_type = '%' - width = int(width.strip('%')) - assert 0 <= width <= 100 - except: - msg = "Cannot parse width {!r} as {!r}".format - raise ValueError(msg(width, width_type)) - self.width = width - - try: - if isinstance(height, int): - height_type = 'px' - assert height > 0 - else: - height_type = '%' - height = int(height.strip('%')) - assert 0 <= height <= 100 - except: - msg = "Cannot parse height {!r} as {!r}".format - raise ValueError(msg(height, height_type)) - self.height = height - - self.map_size = {'width': width, 'height': height} - self._size = ('style="width: {0}{1}; height: {2}{3}"' - .format(width, width_type, height, height_type)) - # Templates. - self.env = ENV - self.template_vars = dict(lat=location[0], - lon=location[1], - size=self._size, - max_zoom=max_zoom, - zoom_level=zoom_start, - map_id=self.map_id, - min_zoom=min_zoom, - min_lat=min_lat, - max_lat=max_lat, - min_lon=min_lon, - max_lon=max_lon) - - # Tiles. - self.tiles = ''.join(tiles.lower().strip().split()) - if self.tiles in ('cloudmade', 'mapbox') and not API_key: - raise ValueError('You must pass an API key if using Cloudmade' - ' or non-default Mapbox tiles.') - - self.default_tiles = ['openstreetmap', 'mapboxcontrolroom', - 'mapquestopen', 'mapquestopenaerial', - 'mapboxbright', 'mapbox', 'cloudmade', - 'stamenterrain', 'stamentoner', - 'stamenwatercolor', - 'cartodbpositron', 'cartodbdark_matter'] - - self.tile_types = {} - for tile in self.default_tiles: - tile_path = 'tiles/%s' % tile - self.tile_types[tile] = { - 'templ': self.env.get_template('%s/%s' % (tile_path, - 'tiles.txt')), - 'attr': self.env.get_template('%s/%s' % (tile_path, - 'attr.txt')), - } - - if self.tiles in self.tile_types: - self.template_vars['Tiles'] = (self.tile_types[self.tiles]['templ'] - .render(API_key=API_key)) - self.template_vars['attr'] = (self.tile_types[self.tiles]['attr'] - .render()) - else: - self.template_vars['Tiles'] = tiles - if not attr: - raise ValueError('Custom tiles must' - ' also be passed an attribution') - if isinstance(attr, binary_type): - attr = text_type(attr, 'utf8') - self.template_vars['attr'] = attr - self.tile_types.update({'Custom': {'template': tiles, - 'attr': attr}}) - - self.added_layers = [] - self.template_vars.setdefault('wms_layers', []) - self.template_vars.setdefault('tile_layers', []) - self.template_vars.setdefault('image_layers', []) - - @iter_obj('simple') - def add_tile_layer(self, tile_name=None, tile_url=None, active=False): - """Adds a simple tile layer. - - Parameters - ---------- - tile_name: string - name of the tile layer - tile_url: string - url location of the tile layer - active: boolean - should the layer be active when added - """ - if tile_name not in self.added_layers: - tile_name = tile_name.replace(" ", "_") - tile_temp = self.env.get_template('tile_layer.js') - - tile = tile_temp.render({'tile_name': tile_name, - 'tile_url': tile_url}) - - self.template_vars.setdefault('tile_layers', []).append((tile)) - - self.added_layers.append({tile_name: tile_url}) - - @iter_obj('simple') + warnings.warn("%s is deprecated and no longer required." % ("initialize_notebook",), + FutureWarning, stacklevel=2) + pass + + +#def iter_obj(type): +# """Decorator to keep count of different map object types in self.mk_cnt.""" +# def decorator(func): +# @functools.wraps(func) +# def wrapper(self, *args, **kwargs): +# self.mark_cnt[type] = self.mark_cnt.get(type, 0) + 1 +# func_result = func(self, *args, **kwargs) +# return func_result +# return wrapper +# return decorator + + +class Map(_Map): + """This class inherits from the map.Map object in order to provide bindings to + former folium API. + """ +# def __init__(self, location=None, width='100%', height='100%', +# tiles='OpenStreetMap', API_key=None, max_zoom=18, min_zoom=1, +# zoom_start=10, attr=None, min_lat=-90, max_lat=90, +# min_lon=-180, max_lon=180): +# """Create a Map with Folium and Leaflet.js +# +# Generate a base map of given width and height with either default +# tilesets or a custom tileset URL. The following tilesets are built-in +# to Folium. Pass any of the following to the "tiles" keyword: +# - "OpenStreetMap" +# - "MapQuest Open" +# - "MapQuest Open Aerial" +# - "Mapbox Bright" (Limited levels of zoom for free tiles) +# - "Mapbox Control Room" (Limited levels of zoom for free tiles) +# - "Stamen" (Terrain, Toner, and Watercolor) +# - "Cloudmade" (Must pass API key) +# - "Mapbox" (Must pass API key) +# - "CartoDB" (positron and dark_matter) +# You can pass a custom tileset to Folium by passing a Leaflet-style +# URL to the tiles parameter: +# http://{s}.yourtiles.com/{z}/{x}/{y}.png +# +# Parameters +# ---------- +# location: tuple or list, default None +# Latitude and Longitude of Map (Northing, Easting). +# width: pixel int or percentage string (default: '100%') +# Width of the map. +# height: pixel int or percentage string (default: '100%') +# Height of the map. +# tiles: str, default 'OpenStreetMap' +# Map tileset to use. Can use defaults or pass a custom URL. +# API_key: str, default None +# API key for Cloudmade or Mapbox tiles. +# max_zoom: int, default 18 +# Maximum zoom depth for the map. +# zoom_start: int, default 10 +# Initial zoom level for the map. +# attr: string, default None +# Map tile attribution; only required if passing custom tile URL. +# +# Returns +# ------- +# Folium Map Object +# +# Examples +# -------- +# >>>map = folium.Map(location=[45.523, -122.675], width=750, height=500) +# >>>map = folium.Map(location=[45.523, -122.675], +# tiles='Mapbox Control Room') +# >>>map = folium.Map(location=(45.523, -122.675), max_zoom=20, +# tiles='Cloudmade', API_key='YourKey') +# >>>map = folium.Map(location=[45.523, -122.675], zoom_start=2, +# tiles=('http://{s}.tiles.mapbox.com/v3/' +# 'mapbox.control-room/{z}/{x}/{y}.png'), +# attr='Mapbox attribution') +# +# """ +# +# # Inits. +# self.map_path = None +# self.render_iframe = False +# self.map_type = 'base' +# self.map_id = '_'.join(['folium', uuid4().hex]) +# +# # Mark counter, JSON, Plugins. +# self.mark_cnt = {} +# self.json_data = {} +# self.plugins = {} +# +# # No location means we will use automatic bounds and ignore zoom +# self.location = location +# +# # If location is not passed, we center the map at 0,0 +# if not location: +# location = [0, 0] +# zoom_start = min_zoom +# +# # Map Size Parameters. +# try: +# if isinstance(width, int): +# width_type = 'px' +# assert width > 0 +# else: +# width_type = '%' +# width = int(width.strip('%')) +# assert 0 <= width <= 100 +# except: +# msg = "Cannot parse width {!r} as {!r}".format +# raise ValueError(msg(width, width_type)) +# self.width = width +# +# try: +# if isinstance(height, int): +# height_type = 'px' +# assert height > 0 +# else: +# height_type = '%' +# height = int(height.strip('%')) +# assert 0 <= height <= 100 +# except: +# msg = "Cannot parse height {!r} as {!r}".format +# raise ValueError(msg(height, height_type)) +# self.height = height +# +# self.map_size = {'width': width, 'height': height} +# self._size = ('style="width: {0}{1}; height: {2}{3}"' +# .format(width, width_type, height, height_type)) +# # Templates. +# self.env = ENV +# self.template_vars = dict(lat=location[0], +# lon=location[1], +# size=self._size, +# max_zoom=max_zoom, +# zoom_level=zoom_start, +# map_id=self.map_id, +# min_zoom=min_zoom, +# min_lat=min_lat, +# max_lat=max_lat, +# min_lon=min_lon, +# max_lon=max_lon) +# +# # Tiles. +# self.tiles = ''.join(tiles.lower().strip().split()) +# if self.tiles in ('cloudmade', 'mapbox') and not API_key: +# raise ValueError('You must pass an API key if using Cloudmade' +# ' or non-default Mapbox tiles.') +# +# self.default_tiles = ['openstreetmap', 'mapboxcontrolroom', +# 'mapquestopen', 'mapquestopenaerial', +# 'mapboxbright', 'mapbox', 'cloudmade', +# 'stamenterrain', 'stamentoner', +# 'stamenwatercolor', +# 'cartodbpositron', 'cartodbdark_matter'] +# +# self.tile_types = {} +# for tile in self.default_tiles: +# tile_path = 'tiles/%s' % tile +# self.tile_types[tile] = { +# 'templ': self.env.get_template('%s/%s' % (tile_path, +# 'tiles.txt')), +# 'attr': self.env.get_template('%s/%s' % (tile_path, +# 'attr.txt')), +# } +# +# if self.tiles in self.tile_types: +# self.template_vars['Tiles'] = (self.tile_types[self.tiles]['templ'] +# .render(API_key=API_key)) +# self.template_vars['attr'] = (self.tile_types[self.tiles]['attr'] +# .render()) +# else: +# self.template_vars['Tiles'] = tiles +# if not attr: +# raise ValueError('Custom tiles must' +# ' also be passed an attribution') +# if isinstance(attr, binary_type): +# attr = text_type(attr, 'utf8') +# self.template_vars['attr'] = attr +# self.tile_types.update({'Custom': {'template': tiles, +# 'attr': attr}}) +# +# self.added_layers = [] +# self.template_vars.setdefault('wms_layers', []) +# self.template_vars.setdefault('tile_layers', []) +# self.template_vars.setdefault('image_layers', []) +# +# @iter_obj('simple') +# def add_tile_layer(self, tile_name=None, tile_url=None, active=False): +# """Adds a simple tile layer. +# +# Parameters +# ---------- +# tile_name: string +# name of the tile layer +# tile_url: string +# url location of the tile layer +# active: boolean +# should the layer be active when added +# """ +# if tile_name not in self.added_layers: +# tile_name = tile_name.replace(" ", "_") +# tile_temp = self.env.get_template('tile_layer.js') +# +# tile = tile_temp.render({'tile_name': tile_name, +# 'tile_url': tile_url}) +# +# self.template_vars.setdefault('tile_layers', []).append((tile)) +# +# self.added_layers.append({tile_name: tile_url}) +# +# @iter_obj('simple') def add_wms_layer(self, wms_name=None, wms_url=None, wms_format=None, wms_layers=None, wms_transparent=True): """Adds a simple tile layer. @@ -260,468 +256,475 @@ def add_wms_layer(self, wms_name=None, wms_url=None, wms_format=None, wms_url : string url of wms layer """ - if wms_name not in self.added_layers: - wms_name = wms_name.replace(" ", "_") - wms_temp = self.env.get_template('wms_layer.js') - - wms = wms_temp.render({ - 'wms_name': wms_name, - 'wms_url': wms_url, - 'wms_format': wms_format, - 'wms_layer_names': wms_layers, - 'wms_transparent': str(wms_transparent).lower()}) - self.template_vars.setdefault('wms_layers', []).append((wms)) - self.added_layers.append({wms_name: wms_url}) - - @iter_obj('simple') - def add_layers_to_map(self): - """ - Required function to actually add the layers to the HTML packet. - """ - layers_temp = self.env.get_template('add_layers.js') - - data_string = '' - for i, layer in enumerate(self.added_layers): - name = list(layer.keys())[0] - if i < len(self.added_layers)-1: - term_string = ",\n" - else: - term_string += "\n" - data_string += '\"{}\": {}'.format(name, name, term_string) - - data_layers = layers_temp.render({'layers': data_string}) - self.template_vars.setdefault('data_layers', []).append((data_layers)) - - @iter_obj('simple') - def simple_marker(self, location=None, popup=None, - marker_color='blue', marker_icon='info-sign', - clustered_marker=False, icon_angle=0, popup_width=300): - """Create a simple stock Leaflet marker on the map, with optional - popup text or Vincent visualization. - - Parameters - ---------- - location: tuple or list, default None - Latitude and Longitude of Marker (Northing, Easting) - popup: string or tuple, default 'Pop Text' - Input text or visualization for object. Can pass either text, - or a tuple of the form (Vincent object, 'vis_path.json') - It is possible to adjust the width of text/HTML popups - using the optional keywords `popup_width` (default is 300px). - marker_color - color of marker you want - marker_icon - icon from (http://getbootstrap.com/components/) you want on the - marker - clustered_marker - boolean of whether or not you want the marker clustered with - other markers - - Returns - ------- - Marker names and HTML in obj.template_vars - - Example - ------- - >>>map.simple_marker(location=[45.5, -122.3], popup='Portland, OR') - >>>map.simple_marker(location=[45.5, -122.3], popup=(vis, 'vis.json')) - - """ - count = self.mark_cnt['simple'] - - mark_temp = self.env.get_template('simple_marker.js') - - marker_num = 'marker_{0}'.format(count) - add_line = "{'icon':"+marker_num+"_icon}" - - icon_temp = self.env.get_template('simple_icon.js') - icon = icon_temp.render({'icon': marker_icon, - 'icon_name': marker_num+"_icon", - 'markerColor': marker_color, - 'icon_angle': icon_angle}) - - # Get marker and popup. - marker = mark_temp.render({'marker': 'marker_' + str(count), - 'lat': location[0], - 'lon': location[1], - 'icon': add_line - }) - - popup_out = self._popup_render(popup=popup, mk_name='marker_', - count=count, width=popup_width) - if clustered_marker: - add_mark = 'clusteredmarkers.addLayer(marker_{0})'.format(count) - name = 'cluster_markers' - else: - add_mark = 'map.addLayer(marker_{0})'.format(count) - name = 'custom_markers' - append = (icon, marker, popup_out, add_mark) - self.template_vars.setdefault(name, []).append(append) - - @iter_obj('div_mark') - def div_markers(self, locations=None, popups=None, - marker_size=10, popup_width=300): - """Create a simple div marker on the map, with optional - popup text or Vincent visualization. Useful for marking points along a - line. - - Parameters - ---------- - locations: list of locations, where each location is an array - Latitude and Longitude of Marker (Northing, Easting) - popup: list of popups, each popup should be a string or tuple. - Default 'Pop Text' - Input text or visualization for object. Can pass either text, - or a tuple of the form (Vincent object, 'vis_path.json') - It is possible to adjust the width of text/HTML popups - using the optional keywords `popup_width`. - (Leaflet default is 300px.) - marker_size - default is 5 - - Returns - ------- - Marker names and HTML in obj.template_vars - - Example - ------- - >>> map.div_markers(locations=[[37.421114, -122.128314], - ... [37.391637, -122.085416], - ... [37.388832, -122.087709]], - ... popups=['1437494575531', - ... '1437492135937', - ... '1437493590434']) - - """ - call_cnt = self.mark_cnt['div_mark'] - if locations is None or popups is None: - raise RuntimeError("Both locations and popups are mandatory") - for (point_cnt, (location, popup)) in enumerate(zip(locations, - popups)): - marker_num = 'div_marker_{0}_{1}'.format(call_cnt, point_cnt) - - icon_temp = self.env.get_template('static_div_icon.js') - icon_name = marker_num+"_icon" - icon = icon_temp.render({'icon_name': icon_name, - 'size': marker_size}) - - mark_temp = self.env.get_template('simple_marker.js') - # Get marker and popup. - marker = mark_temp.render({'marker': marker_num, - 'lat': location[0], - 'lon': location[1], - 'icon': "{'icon':"+icon_name+"}" - }) - - mk_name = 'div_marker_{0}_'.format(call_cnt) - popup_out = self._popup_render(popup=popup, - mk_name=mk_name, - count=point_cnt, width=popup_width) - add_mark = 'map.addLayer(div_marker_{0}_{1})'.format(call_cnt, - point_cnt) - append = (icon, marker, popup_out, add_mark) - self.template_vars.setdefault('div_markers', []).append(append) - - @iter_obj('line') - def line(self, locations, - line_color=None, line_opacity=None, line_weight=None, - popup=None, popup_width=300): - """Add a line to the map with optional styles. - - Parameters - ---------- - locations: list of points (latitude, longitude) - Latitude and Longitude of line (Northing, Easting) - line_color: string, default Leaflet's default ('#03f') - line_opacity: float, default Leaflet's default (0.5) - line_weight: float, default Leaflet's default (5) - popup: string or tuple, default 'Pop Text' - Input text or visualization for object. Can pass either text, - or a tuple of the form (Vincent object, 'vis_path.json') - It is possible to adjust the width of text/HTML popups - using the optional keywords `popup_width` (default is 300px). - - Note: If the optional styles are omitted, they will not be included - in the HTML output and will obtain the Leaflet defaults listed above. - - Example - ------- - >>>map.line(locations=[(45.5, -122.3), (42.3, -71.0)]) - >>>map.line(locations=[(45.5, -122.3), (42.3, -71.0)], - line_color='red', line_opacity=1.0) - - """ - count = self.mark_cnt['line'] - - line_temp = self.env.get_template('polyline.js') - - polyline_opts = {'color': line_color, 'weight': line_weight, - 'opacity': line_opacity} - - varname = 'line_{}'.format(count) - line_rendered = line_temp.render({'line': varname, - 'locations': locations, - 'options': polyline_opts}) - - popup_out = self._popup_render(popup=popup, mk_name='line_', - count=count, width=popup_width) - - add_line = 'map.addLayer({});'.format(varname) - append = (line_rendered, popup_out, add_line) - self.template_vars.setdefault('lines', []).append((append)) - - @iter_obj('multiline') - def multiline(self, locations, line_color=None, line_opacity=None, - line_weight=None): - """Add a multiPolyline to the map with optional styles. - - A multiPolyline is single layer that consists of several polylines that - share styling/popup. - - Parameters - ---------- - locations: list of lists of points (latitude, longitude) - Latitude and Longitude of line (Northing, Easting) - line_color: string, default Leaflet's default ('#03f') - line_opacity: float, default Leaflet's default (0.5) - line_weight: float, default Leaflet's default (5) - - Note: If the optional styles are omitted, they will not be included - in the HTML output and will obtain the Leaflet defaults listed above. - - Example - ------- - # FIXME: Add another example. - >>> m.multiline(locations=[[(45.5236, -122.675), (45.5236, -122.675)], - [(45.5237, -122.675), (45.5237, -122.675)], - [(45.5238, -122.675), (45.5238, -122.675)]]) - >>> m.multiline(locations=[[(45.5236, -122.675), (45.5236, -122.675)], - [(45.5237, -122.675), (45.5237, -122.675)], - [(45.5238, -122.675), (45.5238, -122.675)]], - line_color='red', line_weight=2, - line_opacity=1.0) - """ - - count = self.mark_cnt['multiline'] - - multiline_temp = self.env.get_template('multi_polyline.js') - - multiline_opts = {'color': line_color, 'weight': line_weight, - 'opacity': line_opacity} - - varname = 'multiline_{}'.format(count) - multiline_rendered = multiline_temp.render({'multiline': varname, - 'locations': locations, - 'options': multiline_opts}) - - add_multiline = 'map.addLayer({});'.format(varname) - append = (multiline_rendered, add_multiline) - self.template_vars.setdefault('multilines', []).append(append) - - @iter_obj('circle') - def circle_marker(self, location=None, radius=500, popup=None, - line_color='black', fill_color='black', - fill_opacity=0.6, popup_width=300): - """Create a simple circle marker on the map, with optional popup text - or Vincent visualization. - - Parameters - ---------- - location: tuple or list, default None - Latitude and Longitude of Marker (Northing, Easting) - radius: int, default 500 - Circle radius, in pixels - popup: string or tuple, default 'Pop Text' - Input text or visualization for object. Can pass either text, - or a tuple of the form (Vincent object, 'vis_path.json') - It is possible to adjust the width of text/HTML popups - using the optional keywords `popup_width` (default is 300px). - line_color: string, default black - Line color. Can pass hex value here as well. - fill_color: string, default black - Fill color. Can pass hex value here as well. - fill_opacity: float, default 0.6 - Circle fill opacity - - Returns - ------- - Circle names and HTML in obj.template_vars - - Example - ------- - >>>map.circle_marker(location=[45.5, -122.3], - radius=1000, popup='Portland, OR') - >>>map.circle_marker(location=[45.5, -122.3], - radius=1000, popup=(bar_chart, 'bar_data.json')) - - """ - count = self.mark_cnt['circle'] - - circle_temp = self.env.get_template('circle_marker.js') - - circle = circle_temp.render({'circle': 'circle_' + str(count), - 'radius': radius, - 'lat': location[0], 'lon': location[1], - 'line_color': line_color, - 'fill_color': fill_color, - 'fill_opacity': fill_opacity}) - - popup_out = self._popup_render(popup=popup, mk_name='circle_', - count=count, width=popup_width) - - add_mark = 'map.addLayer(circle_{0})'.format(count) - - self.template_vars.setdefault('markers', []).append((circle, - popup_out, - add_mark)) - - @iter_obj('polygon') - def polygon_marker(self, location=None, line_color='black', line_opacity=1, - line_weight=2, fill_color='blue', fill_opacity=1, - num_sides=4, rotation=0, radius=15, popup=None, - popup_width=300): - """Custom markers using the Leaflet Data Vis Framework. - - - Parameters - ---------- - location: tuple or list, default None - Latitude and Longitude of Marker (Northing, Easting) - line_color: string, default 'black' - Marker line color - line_opacity: float, default 1 - Line opacity, scale 0-1 - line_weight: int, default 2 - Stroke weight in pixels - fill_color: string, default 'blue' - Marker fill color - fill_opacity: float, default 1 - Marker fill opacity - num_sides: int, default 4 - Number of polygon sides - rotation: int, default 0 - Rotation angle in degrees - radius: int, default 15 - Marker radius, in pixels - popup: string or tuple, default 'Pop Text' - Input text or visualization for object. Can pass either text, - or a tuple of the form (Vincent object, 'vis_path.json') - It is possible to adjust the width of text/HTML popups - using the optional keywords `popup_width` (default is 300px). - - Returns - ------- - Polygon marker names and HTML in obj.template_vars - - """ - - count = self.mark_cnt['polygon'] - - poly_temp = self.env.get_template('poly_marker.js') - - polygon = poly_temp.render({'marker': 'polygon_' + str(count), - 'lat': location[0], - 'lon': location[1], - 'line_color': line_color, - 'line_opacity': line_opacity, - 'line_weight': line_weight, - 'fill_color': fill_color, - 'fill_opacity': fill_opacity, - 'num_sides': num_sides, - 'rotation': rotation, - 'radius': radius}) - - popup_out = self._popup_render(popup=popup, mk_name='polygon_', - count=count, width=popup_width) - - add_mark = 'map.addLayer(polygon_{0})'.format(count) - - self.template_vars.setdefault('markers', []).append((polygon, - popup_out, - add_mark)) - # Update JS/CSS and other Plugin files. - js_temp = self.env.get_template('dvf_js_ref.txt').render() - self.template_vars.update({'dvf_js': js_temp}) - - polygon_js = resource_string('folium', - 'plugins/leaflet-dvf.markers.min.js') - - self.plugins.update({'leaflet-dvf.markers.min.js': polygon_js}) - - def lat_lng_popover(self): - """Enable popovers to display Lat and Lon on each click.""" - - latlng_temp = self.env.get_template('lat_lng_popover.js') - self.template_vars.update({'lat_lng_pop': latlng_temp.render()}) - - def click_for_marker(self, popup=None): - """Enable the addition of markers via clicking on the map. The marker - popup defaults to Lat/Lon, but custom text can be passed via the - popup parameter. Double click markers to remove them. - - Parameters - ---------- - popup: - Custom popup text - - Example - ------- - >>>map.click_for_marker(popup='Your Custom Text') - - """ - latlng = '"Latitude: " + lat + "
Longitude: " + lng ' - click_temp = self.env.get_template('click_for_marker.js') - if popup: - popup_txt = ''.join(['"', popup, '"']) - else: - popup_txt = latlng - click_str = click_temp.render({'popup': popup_txt}) - self.template_vars.update({'click_pop': click_str}) - - def fit_bounds(self, bounds, padding_top_left=None, - padding_bottom_right=None, padding=None, max_zoom=None): - """Fit the map to contain a bounding box with the maximum zoom level possible. - - Parameters - ---------- - bounds: list of (latitude, longitude) points - Bounding box specified as two points [southwest, northeast] - padding_top_left: (x, y) point, default None - Padding in the top left corner. Useful if some elements in - the corner, such as controls, might obscure objects you're zooming - to. - padding_bottom_right: (x, y) point, default None - Padding in the bottom right corner. - padding: (x, y) point, default None - Equivalent to setting both top left and bottom right padding to - the same value. - max_zoom: int, default None - Maximum zoom to be used. - - Example - ------- - >>> map.fit_bounds([[52.193636, -2.221575], [52.636878, -1.139759]]) - - """ - options = { - 'paddingTopLeft': padding_top_left, - 'paddingBottomRight': padding_bottom_right, - 'padding': padding, - 'maxZoom': max_zoom, - } - fit_bounds_options = {} - for key, opt in options.items(): - if opt: - fit_bounds_options[key] = opt - fit_bounds = self.env.get_template('fit_bounds.js') - fit_bounds_str = fit_bounds.render({ - 'bounds': json.dumps(bounds), - 'fit_bounds_options': json.dumps(fit_bounds_options, - sort_keys=True), - }) - - self.template_vars.update({'fit_bounds': fit_bounds_str}) - + warnings.warn("%s is deprecated. Use %s instead" % ("Map.add_wms_layer", + "Map.add_children(WmsTileLayer(...))"), + FutureWarning, stacklevel=2) + wms = WmsTileLayer(wms_url, name=wms_name, format=wms_format, layers=wms_layers, + transparent=wms_transparent, attribution=None) + self.add_children(wms, name=wms_name) + +# if wms_name not in self.added_layers: +# wms_name = wms_name.replace(" ", "_") +# wms_temp = self.env.get_template('wms_layer.js') +# +# wms = wms_temp.render({ +# 'wms_name': wms_name, +# 'wms_url': wms_url, +# 'wms_format': wms_format, +# 'wms_layer_names': wms_layers, +# 'wms_transparent': str(wms_transparent).lower()}) +# self.template_vars.setdefault('wms_layers', []).append((wms)) +# self.added_layers.append({wms_name: wms_url}) +# +# @iter_obj('simple') +# def add_layers_to_map(self): +# """ +# Required function to actually add the layers to the HTML packet. +# """ +# layers_temp = self.env.get_template('add_layers.js') +# +# data_string = '' +# for i, layer in enumerate(self.added_layers): +# name = list(layer.keys())[0] +# if i < len(self.added_layers)-1: +# term_string = ",\n" +# else: +# term_string += "\n" +# data_string += '\"{}\": {}'.format(name, name, term_string) +# +# data_layers = layers_temp.render({'layers': data_string}) +# self.template_vars.setdefault('data_layers', []).append((data_layers)) +# +# @iter_obj('simple') +# def simple_marker(self, location=None, popup=None, +# marker_color='blue', marker_icon='info-sign', +# clustered_marker=False, icon_angle=0, popup_width=300): +# """Create a simple stock Leaflet marker on the map, with optional +# popup text or Vincent visualization. +# +# Parameters +# ---------- +# location: tuple or list, default None +# Latitude and Longitude of Marker (Northing, Easting) +# popup: string or tuple, default 'Pop Text' +# Input text or visualization for object. Can pass either text, +# or a tuple of the form (Vincent object, 'vis_path.json') +# It is possible to adjust the width of text/HTML popups +# using the optional keywords `popup_width` (default is 300px). +# marker_color +# color of marker you want +# marker_icon +# icon from (http://getbootstrap.com/components/) you want on the +# marker +# clustered_marker +# boolean of whether or not you want the marker clustered with +# other markers +# +# Returns +# ------- +# Marker names and HTML in obj.template_vars +# +# Example +# ------- +# >>>map.simple_marker(location=[45.5, -122.3], popup='Portland, OR') +# >>>map.simple_marker(location=[45.5, -122.3], popup=(vis, 'vis.json')) +# +# """ +# count = self.mark_cnt['simple'] +# +# mark_temp = self.env.get_template('simple_marker.js') +# +# marker_num = 'marker_{0}'.format(count) +# add_line = "{'icon':"+marker_num+"_icon}" +# +# icon_temp = self.env.get_template('simple_icon.js') +# icon = icon_temp.render({'icon': marker_icon, +# 'icon_name': marker_num+"_icon", +# 'markerColor': marker_color, +# 'icon_angle': icon_angle}) +# +# # Get marker and popup. +# marker = mark_temp.render({'marker': 'marker_' + str(count), +# 'lat': location[0], +# 'lon': location[1], +# 'icon': add_line +# }) +# +# popup_out = self._popup_render(popup=popup, mk_name='marker_', +# count=count, width=popup_width) +# if clustered_marker: +# add_mark = 'clusteredmarkers.addLayer(marker_{0})'.format(count) +# name = 'cluster_markers' +# else: +# add_mark = 'map.addLayer(marker_{0})'.format(count) +# name = 'custom_markers' +# append = (icon, marker, popup_out, add_mark) +# self.template_vars.setdefault(name, []).append(append) +# +# @iter_obj('div_mark') +# def div_markers(self, locations=None, popups=None, +# marker_size=10, popup_width=300): +# """Create a simple div marker on the map, with optional +# popup text or Vincent visualization. Useful for marking points along a +# line. +# +# Parameters +# ---------- +# locations: list of locations, where each location is an array +# Latitude and Longitude of Marker (Northing, Easting) +# popup: list of popups, each popup should be a string or tuple. +# Default 'Pop Text' +# Input text or visualization for object. Can pass either text, +# or a tuple of the form (Vincent object, 'vis_path.json') +# It is possible to adjust the width of text/HTML popups +# using the optional keywords `popup_width`. +# (Leaflet default is 300px.) +# marker_size +# default is 5 +# +# Returns +# ------- +# Marker names and HTML in obj.template_vars +# +# Example +# ------- +# >>> map.div_markers(locations=[[37.421114, -122.128314], +# ... [37.391637, -122.085416], +# ... [37.388832, -122.087709]], +# ... popups=['1437494575531', +# ... '1437492135937', +# ... '1437493590434']) +# +# """ +# call_cnt = self.mark_cnt['div_mark'] +# if locations is None or popups is None: +# raise RuntimeError("Both locations and popups are mandatory") +# for (point_cnt, (location, popup)) in enumerate(zip(locations, +# popups)): +# marker_num = 'div_marker_{0}_{1}'.format(call_cnt, point_cnt) +# +# icon_temp = self.env.get_template('static_div_icon.js') +# icon_name = marker_num+"_icon" +# icon = icon_temp.render({'icon_name': icon_name, +# 'size': marker_size}) +# +# mark_temp = self.env.get_template('simple_marker.js') +# # Get marker and popup. +# marker = mark_temp.render({'marker': marker_num, +# 'lat': location[0], +# 'lon': location[1], +# 'icon': "{'icon':"+icon_name+"}" +# }) +# +# mk_name = 'div_marker_{0}_'.format(call_cnt) +# popup_out = self._popup_render(popup=popup, +# mk_name=mk_name, +# count=point_cnt, width=popup_width) +# add_mark = 'map.addLayer(div_marker_{0}_{1})'.format(call_cnt, +# point_cnt) +# append = (icon, marker, popup_out, add_mark) +# self.template_vars.setdefault('div_markers', []).append(append) +# +# @iter_obj('line') +# def line(self, locations, +# line_color=None, line_opacity=None, line_weight=None, +# popup=None, popup_width=300): +# """Add a line to the map with optional styles. +# +# Parameters +# ---------- +# locations: list of points (latitude, longitude) +# Latitude and Longitude of line (Northing, Easting) +# line_color: string, default Leaflet's default ('#03f') +# line_opacity: float, default Leaflet's default (0.5) +# line_weight: float, default Leaflet's default (5) +# popup: string or tuple, default 'Pop Text' +# Input text or visualization for object. Can pass either text, +# or a tuple of the form (Vincent object, 'vis_path.json') +# It is possible to adjust the width of text/HTML popups +# using the optional keywords `popup_width` (default is 300px). +# +# Note: If the optional styles are omitted, they will not be included +# in the HTML output and will obtain the Leaflet defaults listed above. +# +# Example +# ------- +# >>>map.line(locations=[(45.5, -122.3), (42.3, -71.0)]) +# >>>map.line(locations=[(45.5, -122.3), (42.3, -71.0)], +# line_color='red', line_opacity=1.0) +# +# """ +# count = self.mark_cnt['line'] +# +# line_temp = self.env.get_template('polyline.js') +# +# polyline_opts = {'color': line_color, 'weight': line_weight, +# 'opacity': line_opacity} +# +# varname = 'line_{}'.format(count) +# line_rendered = line_temp.render({'line': varname, +# 'locations': locations, +# 'options': polyline_opts}) +# +# popup_out = self._popup_render(popup=popup, mk_name='line_', +# count=count, width=popup_width) +# +# add_line = 'map.addLayer({});'.format(varname) +# append = (line_rendered, popup_out, add_line) +# self.template_vars.setdefault('lines', []).append((append)) +# +# @iter_obj('multiline') +# def multiline(self, locations, line_color=None, line_opacity=None, +# line_weight=None): +# """Add a multiPolyline to the map with optional styles. +# +# A multiPolyline is single layer that consists of several polylines that +# share styling/popup. +# +# Parameters +# ---------- +# locations: list of lists of points (latitude, longitude) +# Latitude and Longitude of line (Northing, Easting) +# line_color: string, default Leaflet's default ('#03f') +# line_opacity: float, default Leaflet's default (0.5) +# line_weight: float, default Leaflet's default (5) +# +# Note: If the optional styles are omitted, they will not be included +# in the HTML output and will obtain the Leaflet defaults listed above. +# +# Example +# ------- +# # FIXME: Add another example. +# >>> m.multiline(locations=[[(45.5236, -122.675), (45.5236, -122.675)], +# [(45.5237, -122.675), (45.5237, -122.675)], +# [(45.5238, -122.675), (45.5238, -122.675)]]) +# >>> m.multiline(locations=[[(45.5236, -122.675), (45.5236, -122.675)], +# [(45.5237, -122.675), (45.5237, -122.675)], +# [(45.5238, -122.675), (45.5238, -122.675)]], +# line_color='red', line_weight=2, +# line_opacity=1.0) +# """ +# +# count = self.mark_cnt['multiline'] +# +# multiline_temp = self.env.get_template('multi_polyline.js') +# +# multiline_opts = {'color': line_color, 'weight': line_weight, +# 'opacity': line_opacity} +# +# varname = 'multiline_{}'.format(count) +# multiline_rendered = multiline_temp.render({'multiline': varname, +# 'locations': locations, +# 'options': multiline_opts}) +# +# add_multiline = 'map.addLayer({});'.format(varname) +# append = (multiline_rendered, add_multiline) +# self.template_vars.setdefault('multilines', []).append(append) +# +# @iter_obj('circle') +# def circle_marker(self, location=None, radius=500, popup=None, +# line_color='black', fill_color='black', +# fill_opacity=0.6, popup_width=300): +# """Create a simple circle marker on the map, with optional popup text +# or Vincent visualization. +# +# Parameters +# ---------- +# location: tuple or list, default None +# Latitude and Longitude of Marker (Northing, Easting) +# radius: int, default 500 +# Circle radius, in pixels +# popup: string or tuple, default 'Pop Text' +# Input text or visualization for object. Can pass either text, +# or a tuple of the form (Vincent object, 'vis_path.json') +# It is possible to adjust the width of text/HTML popups +# using the optional keywords `popup_width` (default is 300px). +# line_color: string, default black +# Line color. Can pass hex value here as well. +# fill_color: string, default black +# Fill color. Can pass hex value here as well. +# fill_opacity: float, default 0.6 +# Circle fill opacity +# +# Returns +# ------- +# Circle names and HTML in obj.template_vars +# +# Example +# ------- +# >>>map.circle_marker(location=[45.5, -122.3], +# radius=1000, popup='Portland, OR') +# >>>map.circle_marker(location=[45.5, -122.3], +# radius=1000, popup=(bar_chart, 'bar_data.json')) +# +# """ +# count = self.mark_cnt['circle'] +# +# circle_temp = self.env.get_template('circle_marker.js') +# +# circle = circle_temp.render({'circle': 'circle_' + str(count), +# 'radius': radius, +# 'lat': location[0], 'lon': location[1], +# 'line_color': line_color, +# 'fill_color': fill_color, +# 'fill_opacity': fill_opacity}) +# +# popup_out = self._popup_render(popup=popup, mk_name='circle_', +# count=count, width=popup_width) +# +# add_mark = 'map.addLayer(circle_{0})'.format(count) +# +# self.template_vars.setdefault('markers', []).append((circle, +# popup_out, +# add_mark)) +# +# @iter_obj('polygon') +# def polygon_marker(self, location=None, line_color='black', line_opacity=1, +# line_weight=2, fill_color='blue', fill_opacity=1, +# num_sides=4, rotation=0, radius=15, popup=None, +# popup_width=300): +# """Custom markers using the Leaflet Data Vis Framework. +# +# +# Parameters +# ---------- +# location: tuple or list, default None +# Latitude and Longitude of Marker (Northing, Easting) +# line_color: string, default 'black' +# Marker line color +# line_opacity: float, default 1 +# Line opacity, scale 0-1 +# line_weight: int, default 2 +# Stroke weight in pixels +# fill_color: string, default 'blue' +# Marker fill color +# fill_opacity: float, default 1 +# Marker fill opacity +# num_sides: int, default 4 +# Number of polygon sides +# rotation: int, default 0 +# Rotation angle in degrees +# radius: int, default 15 +# Marker radius, in pixels +# popup: string or tuple, default 'Pop Text' +# Input text or visualization for object. Can pass either text, +# or a tuple of the form (Vincent object, 'vis_path.json') +# It is possible to adjust the width of text/HTML popups +# using the optional keywords `popup_width` (default is 300px). +# +# Returns +# ------- +# Polygon marker names and HTML in obj.template_vars +# +# """ +# +# count = self.mark_cnt['polygon'] +# +# poly_temp = self.env.get_template('poly_marker.js') +# +# polygon = poly_temp.render({'marker': 'polygon_' + str(count), +# 'lat': location[0], +# 'lon': location[1], +# 'line_color': line_color, +# 'line_opacity': line_opacity, +# 'line_weight': line_weight, +# 'fill_color': fill_color, +# 'fill_opacity': fill_opacity, +# 'num_sides': num_sides, +# 'rotation': rotation, +# 'radius': radius}) +# +# popup_out = self._popup_render(popup=popup, mk_name='polygon_', +# count=count, width=popup_width) +# +# add_mark = 'map.addLayer(polygon_{0})'.format(count) +# +# self.template_vars.setdefault('markers', []).append((polygon, +# popup_out, +# add_mark)) +# # Update JS/CSS and other Plugin files. +# js_temp = self.env.get_template('dvf_js_ref.txt').render() +# self.template_vars.update({'dvf_js': js_temp}) +# +# polygon_js = resource_string('folium', +# 'plugins/leaflet-dvf.markers.min.js') +# +# self.plugins.update({'leaflet-dvf.markers.min.js': polygon_js}) +# +# def lat_lng_popover(self): +# """Enable popovers to display Lat and Lon on each click.""" +# +# latlng_temp = self.env.get_template('lat_lng_popover.js') +# self.template_vars.update({'lat_lng_pop': latlng_temp.render()}) +# +# def click_for_marker(self, popup=None): +# """Enable the addition of markers via clicking on the map. The marker +# popup defaults to Lat/Lon, but custom text can be passed via the +# popup parameter. Double click markers to remove them. +# +# Parameters +# ---------- +# popup: +# Custom popup text +# +# Example +# ------- +# >>>map.click_for_marker(popup='Your Custom Text') +# +# """ +# latlng = '"Latitude: " + lat + "
Longitude: " + lng ' +# click_temp = self.env.get_template('click_for_marker.js') +# if popup: +# popup_txt = ''.join(['"', popup, '"']) +# else: +# popup_txt = latlng +# click_str = click_temp.render({'popup': popup_txt}) +# self.template_vars.update({'click_pop': click_str}) +# +# def fit_bounds(self, bounds, padding_top_left=None, +# padding_bottom_right=None, padding=None, max_zoom=None): +# """Fit the map to contain a bounding box with the maximum zoom level possible. +# +# Parameters +# ---------- +# bounds: list of (latitude, longitude) points +# Bounding box specified as two points [southwest, northeast] +# padding_top_left: (x, y) point, default None +# Padding in the top left corner. Useful if some elements in +# the corner, such as controls, might obscure objects you're zooming +# to. +# padding_bottom_right: (x, y) point, default None +# Padding in the bottom right corner. +# padding: (x, y) point, default None +# Equivalent to setting both top left and bottom right padding to +# the same value. +# max_zoom: int, default None +# Maximum zoom to be used. +# +# Example +# ------- +# >>> map.fit_bounds([[52.193636, -2.221575], [52.636878, -1.139759]]) +# +# """ +# options = { +# 'paddingTopLeft': padding_top_left, +# 'paddingBottomRight': padding_bottom_right, +# 'padding': padding, +# 'maxZoom': max_zoom, +# } +# fit_bounds_options = {} +# for key, opt in options.items(): +# if opt: +# fit_bounds_options[key] = opt +# fit_bounds = self.env.get_template('fit_bounds.js') +# fit_bounds_str = fit_bounds.render({ +# 'bounds': json.dumps(bounds), +# 'fit_bounds_options': json.dumps(fit_bounds_options, +# sort_keys=True), +# }) +# +# self.template_vars.update({'fit_bounds': fit_bounds_str}) +# def add_plugin(self, plugin): """Adds a plugin to the map. @@ -731,465 +734,468 @@ def add_plugin(self, plugin): A plugin to be added to the map. It has to implement the methods `render_html`, `render_css` and `render_js`. """ - plugin.add_to_map(self) - - def _auto_bounds(self): - if 'fit_bounds' in self.template_vars: - return - # Get count for each feature type - ft_names = ["marker", "line", "circle", "polygon", "multiline"] - ft_names = [i for i in ft_names if i in self.mark_cnt] - - # Make a comprehensive list of all the features we want to fit - feat_str = ["{name}_{count}".format(name=ft_name, - count=self.mark_cnt[ft_name]) - for ft_name in ft_names for - count in range(1, self.mark_cnt[ft_name]+1)] - feat_str = "[" + ', '.join(feat_str) + "]" - - fit_bounds = self.env.get_template('fit_bounds.js') - fit_bounds_str = fit_bounds.render({ - 'autobounds': not self.location, - 'features': feat_str, - 'fit_bounds_options': json.dumps({'padding': [30, 30]}), - }) - - self.template_vars.update({'fit_bounds': fit_bounds_str.strip()}) - - def _popup_render(self, popup=None, mk_name=None, count=None, - width=300): - """Popup renderer: either text or Vincent/Vega. - - Parameters - ---------- - popup: str or Vincent tuple, default None - String for text popup, or tuple of (Vincent object, json_path) - mk_name: str, default None - Type of marker. Simple, Circle, etc. - count: int, default None - Count of marker - """ - if not popup: - return '' - else: - if sys.version_info >= (3, 0): - utype, stype = str, bytes - else: - utype, stype = unicode, str - - if isinstance(popup, (utype, stype)): - popup_temp = self.env.get_template('simple_popup.js') - if isinstance(popup, utype): - popup_txt = popup.encode('ascii', 'xmlcharrefreplace') - else: - popup_txt = popup - if sys.version_info >= (3, 0): - popup_txt = popup_txt.decode() - pop_txt = json.dumps(str(popup_txt)) - return popup_temp.render({'pop_name': mk_name + str(count), - 'pop_txt': pop_txt, 'width': width}) - elif isinstance(popup, tuple): - # Update template with JS libs. - vega_temp = self.env.get_template('vega_ref.txt').render() - jquery_temp = self.env.get_template('jquery_ref.txt').render() - d3_temp = self.env.get_template('d3_ref.txt').render() - vega_parse = self.env.get_template('vega_parse.js').render() - self.template_vars.update({'vega': vega_temp, - 'd3': d3_temp, - 'jquery': jquery_temp, - 'vega_parse': vega_parse}) - - # Parameters for Vega template. - vega = popup[0] - mark = ''.join([mk_name, str(count)]) - json_out = popup[1] - div_id = popup[1].split('.')[0] - width = vega.width - height = vega.height - if isinstance(vega.padding, dict): - width += vega.padding['left']+vega.padding['right'] - height += vega.padding['top']+vega.padding['bottom'] - else: - width += 75 - height += 50 - max_width = max([self.map_size['width'], width]) - vega_id = '#' + div_id - popup_temp = self.env.get_template('vega_marker.js') - return popup_temp.render({'mark': mark, 'div_id': div_id, - 'width': width, 'height': height, - 'max_width': max_width, - 'json_out': json_out, - 'vega_id': vega_id}) - else: - raise TypeError("Unrecognized popup type: {!r}".format(popup)) - - @iter_obj('geojson') - def geo_json(self, geo_path=None, geo_str=None, data_out='data.json', - data=None, columns=None, key_on=None, threshold_scale=None, - fill_color='blue', fill_opacity=0.6, line_color='black', - line_weight=1, line_opacity=1, legend_name=None, - topojson=None, reset=False): - """Apply a GeoJSON overlay to the map. - - Plot a GeoJSON overlay on the base map. There is no requirement - to bind data (passing just a GeoJSON plots a single-color overlay), - but there is a data binding option to map your columnar data to - different feature objects with a color scale. - - If data is passed as a Pandas dataframe, the "columns" and "key-on" - keywords must be included, the first to indicate which DataFrame - columns to use, the second to indicate the layer in the GeoJSON - on which to key the data. The 'columns' keyword does not need to be - passed for a Pandas series. - - Colors are generated from color brewer (http://colorbrewer2.org/) - sequential palettes on a D3 threshold scale. The scale defaults to the - following quantiles: [0, 0.5, 0.75, 0.85, 0.9]. A custom scale can be - passed to `threshold_scale` of length <=6, in order to match the - color brewer range. - - TopoJSONs can be passed as "geo_path", but the "topojson" keyword must - also be passed with the reference to the topojson objects to convert. - See the topojson.feature method in the TopoJSON API reference: - https://github.com/mbostock/topojson/wiki/API-Reference - - - Parameters - ---------- - geo_path: string, default None - URL or File path to your GeoJSON data - geo_str: string, default None - String of GeoJSON, alternative to geo_path - data_out: string, default 'data.json' - Path to write Pandas DataFrame/Series to JSON if binding data - data: Pandas DataFrame or Series, default None - Data to bind to the GeoJSON. - columns: dict or tuple, default None - If the data is a Pandas DataFrame, the columns of data to be bound. - Must pass column 1 as the key, and column 2 the values. - key_on: string, default None - Variable in the GeoJSON file to bind the data to. Must always - start with 'feature' and be in JavaScript objection notation. - Ex: 'feature.id' or 'feature.properties.statename'. - threshold_scale: list, default None - Data range for D3 threshold scale. Defaults to the following range - of quantiles: [0, 0.5, 0.75, 0.85, 0.9], rounded to the nearest - order-of-magnitude integer. Ex: 270 rounds to 200, 5600 to 6000. - fill_color: string, default 'blue' - Area fill color. Can pass a hex code, color name, or if you are - binding data, one of the following color brewer palettes: - 'BuGn', 'BuPu', 'GnBu', 'OrRd', 'PuBu', 'PuBuGn', 'PuRd', 'RdPu', - 'YlGn', 'YlGnBu', 'YlOrBr', and 'YlOrRd'. - fill_opacity: float, default 0.6 - Area fill opacity, range 0-1. - line_color: string, default 'black' - GeoJSON geopath line color. - line_weight: int, default 1 - GeoJSON geopath line weight. - line_opacity: float, default 1 - GeoJSON geopath line opacity, range 0-1. - legend_name: string, default None - Title for data legend. If not passed, defaults to columns[1]. - topojson: string, default None - If using a TopoJSON, passing "objects.yourfeature" to the topojson - keyword argument will enable conversion to GeoJSON. - reset: boolean, default False - Remove all current geoJSON layers, start with new layer - - Output - ------ - GeoJSON data layer in obj.template_vars - - Example - ------- - >>> m.geo_json(geo_path='us-states.json', line_color='blue', - line_weight=3) - >>> m.geo_json(geo_path='geo.json', data=df, - columns=['Data 1', 'Data 2'], - key_on='feature.properties.myvalue', fill_color='PuBu', - threshold_scale=[0, 20, 30, 40, 50, 60]) - >>> m.geo_json(geo_path='countries.json', topojson='objects.countries') - """ - - if reset: - reset_vars = ['json_paths', 'func_vars', 'color_scales', - 'geo_styles', 'gjson_layers', 'map_legends', - 'topo_convert'] - for var in reset_vars: - self.template_vars.update({var: []}) - self.mark_cnt['geojson'] = 1 - - def json_style(style_cnt, line_color, line_weight, line_opacity, - fill_color, fill_opacity, quant_fill): - """Generate JSON styling function from template""" - style_temp = self.env.get_template('geojson_style.js') - style = style_temp.render({'style': style_cnt, - 'line_color': line_color, - 'line_weight': line_weight, - 'line_opacity': line_opacity, - 'fill_color': fill_color, - 'fill_opacity': fill_opacity, - 'quantize_fill': quant_fill}) - return style - - # Set map type to geojson. - self.map_type = 'geojson' - - # Get JSON map layer template pieces, convert TopoJSON if necessary. - # geo_str is really a hack. - if geo_path: - geo_path = ".defer(d3.json, '{0}')".format(geo_path) - elif geo_str: - fmt = (".defer(function(callback)" - "{{callback(null, JSON.parse('{}'))}})").format - geo_path = fmt(geo_str) - if topojson is None: - map_var = '_'.join(['gjson', str(self.mark_cnt['geojson'])]) - layer_var = map_var - else: - map_var = '_'.join(['tjson', str(self.mark_cnt['geojson'])]) - topo_obj = '.'.join([map_var, topojson]) - layer_var = '_'.join(['topo', str(self.mark_cnt['geojson'])]) - topo_templ = self.env.get_template('topo_func.js') - topo_func = topo_templ.render({'map_var': layer_var, - 't_var': map_var, - 't_var_obj': topo_obj}) - topo_lib = self.env.get_template('topojson_ref.txt').render() - self.template_vars.update({'topojson': topo_lib}) - self.template_vars.setdefault('topo_convert', - []).append(topo_func) - - style_count = '_'.join(['style', str(self.mark_cnt['geojson'])]) - - # Get Data binding pieces if available. - if data is not None: - - import pandas as pd - - # Create DataFrame with only the relevant columns. - if isinstance(data, pd.DataFrame): - data = pd.concat([data[columns[0]], data[columns[1]]], axis=1) - - # Save data to JSON. - self.json_data[data_out] = utilities.transform_data(data) - - # Add data to queue. - d_path = ".defer(d3.json, '{0}')".format(data_out) - self.template_vars.setdefault('json_paths', []).append(d_path) - - # Add data variable to makeMap function. - data_var = '_'.join(['data', str(self.mark_cnt['geojson'])]) - self.template_vars.setdefault('func_vars', []).append(data_var) - - # D3 Color scale. - series = data[columns[1]] - if threshold_scale and len(threshold_scale) > 6: - raise ValueError - domain = threshold_scale or utilities.split_six(series=series) - if len(domain) > 253: - raise ValueError('The threshold scale must be length <= 253') - if not utilities.color_brewer(fill_color): - raise ValueError('Please pass a valid color brewer code to ' - 'fill_local. See docstring for valid codes.') - - palette = utilities.color_brewer(fill_color, len(domain)) - d3range = palette[0: len(domain) + 1] - tick_labels = utilities.legend_scaler(domain) - - color_temp = self.env.get_template('d3_threshold.js') - d3scale = color_temp.render({'domain': domain, - 'range': d3range}) - self.template_vars.setdefault('color_scales', []).append(d3scale) - - # Create legend. - name = legend_name or columns[1] - leg_templ = self.env.get_template('d3_map_legend.js') - legend = leg_templ.render({'lin_max': int(domain[-1]*1.1), - 'tick_labels': tick_labels, - 'caption': name}) - self.template_vars.setdefault('map_legends', []).append(legend) - - # Style with color brewer colors. - matchColor = 'color(matchKey({0}, {1}))'.format(key_on, data_var) - style = json_style(style_count, line_color, line_weight, - line_opacity, None, fill_opacity, matchColor) - else: - style = json_style(style_count, line_color, line_weight, - line_opacity, fill_color, fill_opacity, None) - - layer = ('gJson_layer_{0} = L.geoJson({1}, {{style: {2},' - 'onEachFeature: onEachFeature}}).addTo(map)' - .format(self.mark_cnt['geojson'], layer_var, style_count)) - - self.template_vars.setdefault('json_paths', []).append(geo_path) - self.template_vars.setdefault('func_vars', []).append(map_var) - self.template_vars.setdefault('geo_styles', []).append(style) - self.template_vars.setdefault('gjson_layers', []).append(layer) - - @iter_obj('image_overlay') - def image_overlay(self, data, opacity=0.25, min_lat=-90.0, max_lat=90.0, - min_lon=-180.0, max_lon=180.0, image_name=None, - filename=None): - """ - Simple image overlay of raster data from a numpy array. This is a - lightweight way to overlay geospatial data on top of a map. If your - data is high res, consider implementing a WMS server and adding a WMS - layer. - - This function works by generating a PNG file from a numpy array. If - you do not specify a filename, it will embed the image inline. - Otherwise, it saves the file in the current directory, and then adds - it as an image overlay layer in leaflet.js. By default, the image is - placed and stretched using bounds that cover the entire globe. - - Parameters - ---------- - data: numpy array OR url string, required. - if numpy array, must be a image format, - i.e., NxM (mono), NxMx3 (rgb), or NxMx4 (rgba) - if url, must be a valid url to a image (local or external) - opacity: float, default 0.25 - Image layer opacity in range 0 (transparent) to 1 (opaque) - min_lat: float, default -90.0 - max_lat: float, default 90.0 - min_lon: float, default -180.0 - max_lon: float, default 180.0 - image_name: string, default None - The name of the layer object in leaflet.js - filename: string, default None - Optional file name of output.png for image overlay. - Use `None` for inline PNG. - - Output - ------ - Image overlay data layer in obj.template_vars - - Examples - ------- - # assumes a map object `m` has been created - >>> import numpy as np - >>> data = np.random.random((100,100)) - - # to make a rgba from a specific matplotlib colormap: - >>> import matplotlib.cm as cm - >>> cmapper = cm.cm.ColorMapper('jet') - >>> data2 = cmapper.to_rgba(np.random.random((100,100))) - >>> # Place the data over all of the globe (will be pretty pixelated!) - >>> m.image_overlay(data) - >>> # Put it only over a single city (Paris). - >>> m.image_overlay(data, min_lat=48.80418, max_lat=48.90970, - ... min_lon=2.25214, max_lon=2.44731) - - """ - - if isinstance(data, str): - filename = data - else: - try: - png_str = utilities.write_png(data) - except Exception as e: - raise e - - if filename is not None: - with open(filename, 'wb') as fd: - fd.write(png_str) - else: - png = "data:image/png;base64,{}".format - filename = png(base64.b64encode(png_str).decode('utf-8')) - - if image_name not in self.added_layers: - if image_name is None: - image_name = "Image_Overlay" - else: - image_name = image_name.replace(" ", "_") - image_url = filename - image_bounds = [[min_lat, min_lon], [max_lat, max_lon]] - image_opacity = opacity - - image_temp = self.env.get_template('image_layer.js') - - image = image_temp.render({'image_name': image_name, - 'image_url': image_url, - 'image_bounds': image_bounds, - 'image_opacity': image_opacity}) - - self.template_vars['image_layers'].append(image) - self.added_layers.append(image_name) - - def _build_map(self, html_templ=None, templ_type='string'): - self._auto_bounds() - """Build HTML/JS/CSS from Templates given current map type.""" - if html_templ is None: - map_types = {'base': 'fol_template.html', - 'geojson': 'geojson_template.html'} - - # Check current map type. - type_temp = map_types[self.map_type] - - html_templ = self.env.get_template(type_temp) - else: - if templ_type == 'string': - html_templ = self.env.from_string(html_templ) - - self.HTML = html_templ.render(self.template_vars, plugins=self.plugins) - - def create_map(self, path='map.html', plugin_data_out=True, template=None): - """Write Map output to HTML and data output to JSON if available. - - Parameters: - ----------- - path: string, default 'map.html' - Path for HTML output for map - plugin_data_out: boolean, default True - If using plugins such as awesome markers, write all plugin - data such as JS/CSS/images to path - template: string, default None - Custom template to render - - """ - self.map_path = path - self._build_map(template) - - with codecs.open(path, 'w', 'utf8') as f: - f.write(self.HTML) - - if self.json_data: - for path, data in iteritems(self.json_data): - with open(path, 'w') as g: - json.dump(data, g) - - if self.plugins and plugin_data_out: - for name, plugin in iteritems(self.plugins): - with open(name, 'w') as f: - if isinstance(plugin, binary_type): - plugin = text_type(plugin, 'utf8') - f.write(plugin) - - def _repr_html_(self): - """Build the HTML representation for IPython.""" - map_types = {'base': 'ipynb_repr.html', - 'geojson': 'ipynb_iframe.html'} - - # Check current map type. - type_temp = map_types[self.map_type] - if self.render_iframe: - type_temp = 'ipynb_iframe.html' - templ = self.env.get_template(type_temp) - self._build_map(html_templ=templ, templ_type='temp') - if self.map_type == 'geojson' or self.render_iframe: - if not self.map_path: - raise ValueError('Use create_map to set the path!') - return templ.render(path=self.map_path, width=self.width, - height=self.height) - return self.HTML - - def display(self): - """Display the visualization inline in the IPython notebook. - - This is deprecated, use the following instead:: - - from IPython.display import display - display(viz) - """ - from IPython.core.display import display, HTML - display(HTML(self._repr_html_())) + warnings.warn("%s is deprecated. Use %s instead" % ("add_plugin", "add_children"), + FutureWarning, stacklevel=2) + self.add_children(plugin) + +# def _auto_bounds(self): +# if 'fit_bounds' in self.template_vars: +# return +# # Get count for each feature type +# ft_names = ["marker", "line", "circle", "polygon", "multiline"] +# ft_names = [i for i in ft_names if i in self.mark_cnt] +# +# # Make a comprehensive list of all the features we want to fit +# feat_str = ["{name}_{count}".format(name=ft_name, +# count=self.mark_cnt[ft_name]) +# for ft_name in ft_names for +# count in range(1, self.mark_cnt[ft_name]+1)] +# feat_str = "[" + ', '.join(feat_str) + "]" +# +# fit_bounds = self.env.get_template('fit_bounds.js') +# fit_bounds_str = fit_bounds.render({ +# 'autobounds': not self.location, +# 'features': feat_str, +# 'fit_bounds_options': json.dumps({'padding': [30, 30]}), +# }) +# +# self.template_vars.update({'fit_bounds': fit_bounds_str.strip()}) +# +# def _popup_render(self, popup=None, mk_name=None, count=None, +# width=300): +# """Popup renderer: either text or Vincent/Vega. +# +# Parameters +# ---------- +# popup: str or Vincent tuple, default None +# String for text popup, or tuple of (Vincent object, json_path) +# mk_name: str, default None +# Type of marker. Simple, Circle, etc. +# count: int, default None +# Count of marker +# """ +# if not popup: +# return '' +# else: +# if sys.version_info >= (3, 0): +# utype, stype = str, bytes +# else: +# utype, stype = unicode, str +# +# if isinstance(popup, (utype, stype)): +# popup_temp = self.env.get_template('simple_popup.js') +# if isinstance(popup, utype): +# popup_txt = popup.encode('ascii', 'xmlcharrefreplace') +# else: +# popup_txt = popup +# if sys.version_info >= (3, 0): +# popup_txt = popup_txt.decode() +# pop_txt = json.dumps(str(popup_txt)) +# return popup_temp.render({'pop_name': mk_name + str(count), +# 'pop_txt': pop_txt, 'width': width}) +# elif isinstance(popup, tuple): +# # Update template with JS libs. +# vega_temp = self.env.get_template('vega_ref.txt').render() +# jquery_temp = self.env.get_template('jquery_ref.txt').render() +# d3_temp = self.env.get_template('d3_ref.txt').render() +# vega_parse = self.env.get_template('vega_parse.js').render() +# self.template_vars.update({'vega': vega_temp, +# 'd3': d3_temp, +# 'jquery': jquery_temp, +# 'vega_parse': vega_parse}) +# +# # Parameters for Vega template. +# vega = popup[0] +# mark = ''.join([mk_name, str(count)]) +# json_out = popup[1] +# div_id = popup[1].split('.')[0] +# width = vega.width +# height = vega.height +# if isinstance(vega.padding, dict): +# width += vega.padding['left']+vega.padding['right'] +# height += vega.padding['top']+vega.padding['bottom'] +# else: +# width += 75 +# height += 50 +# max_width = max([self.map_size['width'], width]) +# vega_id = '#' + div_id +# popup_temp = self.env.get_template('vega_marker.js') +# return popup_temp.render({'mark': mark, 'div_id': div_id, +# 'width': width, 'height': height, +# 'max_width': max_width, +# 'json_out': json_out, +# 'vega_id': vega_id}) +# else: +# raise TypeError("Unrecognized popup type: {!r}".format(popup)) +# +# @iter_obj('geojson') +# def geo_json(self, geo_path=None, geo_str=None, data_out='data.json', +# data=None, columns=None, key_on=None, threshold_scale=None, +# fill_color='blue', fill_opacity=0.6, line_color='black', +# line_weight=1, line_opacity=1, legend_name=None, +# topojson=None, reset=False): +# """Apply a GeoJSON overlay to the map. +# +# Plot a GeoJSON overlay on the base map. There is no requirement +# to bind data (passing just a GeoJSON plots a single-color overlay), +# but there is a data binding option to map your columnar data to +# different feature objects with a color scale. +# +# If data is passed as a Pandas dataframe, the "columns" and "key-on" +# keywords must be included, the first to indicate which DataFrame +# columns to use, the second to indicate the layer in the GeoJSON +# on which to key the data. The 'columns' keyword does not need to be +# passed for a Pandas series. +# +# Colors are generated from color brewer (http://colorbrewer2.org/) +# sequential palettes on a D3 threshold scale. The scale defaults to the +# following quantiles: [0, 0.5, 0.75, 0.85, 0.9]. A custom scale can be +# passed to `threshold_scale` of length <=6, in order to match the +# color brewer range. +# +# TopoJSONs can be passed as "geo_path", but the "topojson" keyword must +# also be passed with the reference to the topojson objects to convert. +# See the topojson.feature method in the TopoJSON API reference: +# https://github.com/mbostock/topojson/wiki/API-Reference +# +# +# Parameters +# ---------- +# geo_path: string, default None +# URL or File path to your GeoJSON data +# geo_str: string, default None +# String of GeoJSON, alternative to geo_path +# data_out: string, default 'data.json' +# Path to write Pandas DataFrame/Series to JSON if binding data +# data: Pandas DataFrame or Series, default None +# Data to bind to the GeoJSON. +# columns: dict or tuple, default None +# If the data is a Pandas DataFrame, the columns of data to be bound. +# Must pass column 1 as the key, and column 2 the values. +# key_on: string, default None +# Variable in the GeoJSON file to bind the data to. Must always +# start with 'feature' and be in JavaScript objection notation. +# Ex: 'feature.id' or 'feature.properties.statename'. +# threshold_scale: list, default None +# Data range for D3 threshold scale. Defaults to the following range +# of quantiles: [0, 0.5, 0.75, 0.85, 0.9], rounded to the nearest +# order-of-magnitude integer. Ex: 270 rounds to 200, 5600 to 6000. +# fill_color: string, default 'blue' +# Area fill color. Can pass a hex code, color name, or if you are +# binding data, one of the following color brewer palettes: +# 'BuGn', 'BuPu', 'GnBu', 'OrRd', 'PuBu', 'PuBuGn', 'PuRd', 'RdPu', +# 'YlGn', 'YlGnBu', 'YlOrBr', and 'YlOrRd'. +# fill_opacity: float, default 0.6 +# Area fill opacity, range 0-1. +# line_color: string, default 'black' +# GeoJSON geopath line color. +# line_weight: int, default 1 +# GeoJSON geopath line weight. +# line_opacity: float, default 1 +# GeoJSON geopath line opacity, range 0-1. +# legend_name: string, default None +# Title for data legend. If not passed, defaults to columns[1]. +# topojson: string, default None +# If using a TopoJSON, passing "objects.yourfeature" to the topojson +# keyword argument will enable conversion to GeoJSON. +# reset: boolean, default False +# Remove all current geoJSON layers, start with new layer +# +# Output +# ------ +# GeoJSON data layer in obj.template_vars +# +# Example +# ------- +# >>> m.geo_json(geo_path='us-states.json', line_color='blue', +# line_weight=3) +# >>> m.geo_json(geo_path='geo.json', data=df, +# columns=['Data 1', 'Data 2'], +# key_on='feature.properties.myvalue', fill_color='PuBu', +# threshold_scale=[0, 20, 30, 40, 50, 60]) +# >>> m.geo_json(geo_path='countries.json', topojson='objects.countries') +# """ +# +# if reset: +# reset_vars = ['json_paths', 'func_vars', 'color_scales', +# 'geo_styles', 'gjson_layers', 'map_legends', +# 'topo_convert'] +# for var in reset_vars: +# self.template_vars.update({var: []}) +# self.mark_cnt['geojson'] = 1 +# +# def json_style(style_cnt, line_color, line_weight, line_opacity, +# fill_color, fill_opacity, quant_fill): +# """Generate JSON styling function from template""" +# style_temp = self.env.get_template('geojson_style.js') +# style = style_temp.render({'style': style_cnt, +# 'line_color': line_color, +# 'line_weight': line_weight, +# 'line_opacity': line_opacity, +# 'fill_color': fill_color, +# 'fill_opacity': fill_opacity, +# 'quantize_fill': quant_fill}) +# return style +# +# # Set map type to geojson. +# self.map_type = 'geojson' +# +# # Get JSON map layer template pieces, convert TopoJSON if necessary. +# # geo_str is really a hack. +# if geo_path: +# geo_path = ".defer(d3.json, '{0}')".format(geo_path) +# elif geo_str: +# fmt = (".defer(function(callback)" +# "{{callback(null, JSON.parse('{}'))}})").format +# geo_path = fmt(geo_str) +# if topojson is None: +# map_var = '_'.join(['gjson', str(self.mark_cnt['geojson'])]) +# layer_var = map_var +# else: +# map_var = '_'.join(['tjson', str(self.mark_cnt['geojson'])]) +# topo_obj = '.'.join([map_var, topojson]) +# layer_var = '_'.join(['topo', str(self.mark_cnt['geojson'])]) +# topo_templ = self.env.get_template('topo_func.js') +# topo_func = topo_templ.render({'map_var': layer_var, +# 't_var': map_var, +# 't_var_obj': topo_obj}) +# topo_lib = self.env.get_template('topojson_ref.txt').render() +# self.template_vars.update({'topojson': topo_lib}) +# self.template_vars.setdefault('topo_convert', +# []).append(topo_func) +# +# style_count = '_'.join(['style', str(self.mark_cnt['geojson'])]) +# +# # Get Data binding pieces if available. +# if data is not None: +# +# import pandas as pd +# +# # Create DataFrame with only the relevant columns. +# if isinstance(data, pd.DataFrame): +# data = pd.concat([data[columns[0]], data[columns[1]]], axis=1) +# +# # Save data to JSON. +# self.json_data[data_out] = utilities.transform_data(data) +# +# # Add data to queue. +# d_path = ".defer(d3.json, '{0}')".format(data_out) +# self.template_vars.setdefault('json_paths', []).append(d_path) +# +# # Add data variable to makeMap function. +# data_var = '_'.join(['data', str(self.mark_cnt['geojson'])]) +# self.template_vars.setdefault('func_vars', []).append(data_var) +# +# # D3 Color scale. +# series = data[columns[1]] +# if threshold_scale and len(threshold_scale) > 6: +# raise ValueError +# domain = threshold_scale or utilities.split_six(series=series) +# if len(domain) > 253: +# raise ValueError('The threshold scale must be length <= 253') +# if not utilities.color_brewer(fill_color): +# raise ValueError('Please pass a valid color brewer code to ' +# 'fill_local. See docstring for valid codes.') +# +# palette = utilities.color_brewer(fill_color, len(domain)) +# d3range = palette[0: len(domain) + 1] +# tick_labels = utilities.legend_scaler(domain) +# +# color_temp = self.env.get_template('d3_threshold.js') +# d3scale = color_temp.render({'domain': domain, +# 'range': d3range}) +# self.template_vars.setdefault('color_scales', []).append(d3scale) +# +# # Create legend. +# name = legend_name or columns[1] +# leg_templ = self.env.get_template('d3_map_legend.js') +# legend = leg_templ.render({'lin_max': int(domain[-1]*1.1), +# 'tick_labels': tick_labels, +# 'caption': name}) +# self.template_vars.setdefault('map_legends', []).append(legend) +# +# # Style with color brewer colors. +# matchColor = 'color(matchKey({0}, {1}))'.format(key_on, data_var) +# style = json_style(style_count, line_color, line_weight, +# line_opacity, None, fill_opacity, matchColor) +# else: +# style = json_style(style_count, line_color, line_weight, +# line_opacity, fill_color, fill_opacity, None) +# +# layer = ('gJson_layer_{0} = L.geoJson({1}, {{style: {2},' +# 'onEachFeature: onEachFeature}}).addTo(map)' +# .format(self.mark_cnt['geojson'], layer_var, style_count)) +# +# self.template_vars.setdefault('json_paths', []).append(geo_path) +# self.template_vars.setdefault('func_vars', []).append(map_var) +# self.template_vars.setdefault('geo_styles', []).append(style) +# self.template_vars.setdefault('gjson_layers', []).append(layer) +# +# @iter_obj('image_overlay') +# def image_overlay(self, data, opacity=0.25, min_lat=-90.0, max_lat=90.0, +# min_lon=-180.0, max_lon=180.0, image_name=None, +# filename=None): +# """ +# Simple image overlay of raster data from a numpy array. This is a +# lightweight way to overlay geospatial data on top of a map. If your +# data is high res, consider implementing a WMS server and adding a WMS +# layer. +# +# This function works by generating a PNG file from a numpy array. If +# you do not specify a filename, it will embed the image inline. +# Otherwise, it saves the file in the current directory, and then adds +# it as an image overlay layer in leaflet.js. By default, the image is +# placed and stretched using bounds that cover the entire globe. +# +# Parameters +# ---------- +# data: numpy array OR url string, required. +# if numpy array, must be a image format, +# i.e., NxM (mono), NxMx3 (rgb), or NxMx4 (rgba) +# if url, must be a valid url to a image (local or external) +# opacity: float, default 0.25 +# Image layer opacity in range 0 (transparent) to 1 (opaque) +# min_lat: float, default -90.0 +# max_lat: float, default 90.0 +# min_lon: float, default -180.0 +# max_lon: float, default 180.0 +# image_name: string, default None +# The name of the layer object in leaflet.js +# filename: string, default None +# Optional file name of output.png for image overlay. +# Use `None` for inline PNG. +# +# Output +# ------ +# Image overlay data layer in obj.template_vars +# +# Examples +# ------- +# # assumes a map object `m` has been created +# >>> import numpy as np +# >>> data = np.random.random((100,100)) +# +# # to make a rgba from a specific matplotlib colormap: +# >>> import matplotlib.cm as cm +# >>> cmapper = cm.cm.ColorMapper('jet') +# >>> data2 = cmapper.to_rgba(np.random.random((100,100))) +# >>> # Place the data over all of the globe (will be pretty pixelated!) +# >>> m.image_overlay(data) +# >>> # Put it only over a single city (Paris). +# >>> m.image_overlay(data, min_lat=48.80418, max_lat=48.90970, +# ... min_lon=2.25214, max_lon=2.44731) +# +# """ +# +# if isinstance(data, str): +# filename = data +# else: +# try: +# png_str = utilities.write_png(data) +# except Exception as e: +# raise e +# +# if filename is not None: +# with open(filename, 'wb') as fd: +# fd.write(png_str) +# else: +# png = "data:image/png;base64,{}".format +# filename = png(base64.b64encode(png_str).decode('utf-8')) +# +# if image_name not in self.added_layers: +# if image_name is None: +# image_name = "Image_Overlay" +# else: +# image_name = image_name.replace(" ", "_") +# image_url = filename +# image_bounds = [[min_lat, min_lon], [max_lat, max_lon]] +# image_opacity = opacity +# +# image_temp = self.env.get_template('image_layer.js') +# +# image = image_temp.render({'image_name': image_name, +# 'image_url': image_url, +# 'image_bounds': image_bounds, +# 'image_opacity': image_opacity}) +# +# self.template_vars['image_layers'].append(image) +# self.added_layers.append(image_name) +# +# def _build_map(self, html_templ=None, templ_type='string'): +# self._auto_bounds() +# """Build HTML/JS/CSS from Templates given current map type.""" +# if html_templ is None: +# map_types = {'base': 'fol_template.html', +# 'geojson': 'geojson_template.html'} +# +# # Check current map type. +# type_temp = map_types[self.map_type] +# +# html_templ = self.env.get_template(type_temp) +# else: +# if templ_type == 'string': +# html_templ = self.env.from_string(html_templ) +# +# self.HTML = html_templ.render(self.template_vars, plugins=self.plugins) +# +# def create_map(self, path='map.html', plugin_data_out=True, template=None): +# """Write Map output to HTML and data output to JSON if available. +# +# Parameters: +# ----------- +# path: string, default 'map.html' +# Path for HTML output for map +# plugin_data_out: boolean, default True +# If using plugins such as awesome markers, write all plugin +# data such as JS/CSS/images to path +# template: string, default None +# Custom template to render +# +# """ +# self.map_path = path +# self._build_map(template) +# +# with codecs.open(path, 'w', 'utf8') as f: +# f.write(self.HTML) +# +# if self.json_data: +# for path, data in iteritems(self.json_data): +# with open(path, 'w') as g: +# json.dump(data, g) +# +# if self.plugins and plugin_data_out: +# for name, plugin in iteritems(self.plugins): +# with open(name, 'w') as f: +# if isinstance(plugin, binary_type): +# plugin = text_type(plugin, 'utf8') +# f.write(plugin) +# +# def _repr_html_(self): +# """Build the HTML representation for IPython.""" +# map_types = {'base': 'ipynb_repr.html', +# 'geojson': 'ipynb_iframe.html'} +# +# # Check current map type. +# type_temp = map_types[self.map_type] +# if self.render_iframe: +# type_temp = 'ipynb_iframe.html' +# templ = self.env.get_template(type_temp) +# self._build_map(html_templ=templ, templ_type='temp') +# if self.map_type == 'geojson' or self.render_iframe: +# if not self.map_path: +# raise ValueError('Use create_map to set the path!') +# return templ.render(path=self.map_path, width=self.width, +# height=self.height) +# return self.HTML +# +# def display(self): +# """Display the visualization inline in the IPython notebook. +# +# This is deprecated, use the following instead:: +# +# from IPython.display import display +# display(viz) +# """ +# from IPython.core.display import display, HTML +# display(HTML(self._repr_html_())) +# \ No newline at end of file diff --git a/folium/map.py b/folium/map.py index ff2fd017bb..d0e1373ce7 100644 --- a/folium/map.py +++ b/folium/map.py @@ -85,6 +85,8 @@ def __init__(self, location=None, width='100%', height='100%', self.location = location self.zoom_start = zoom_start + Figure().add_children(self) + # Map Size Parameters. self.width = _parse_size(width) self.height = _parse_size(height) diff --git a/tests/test_folium.py b/tests/test_folium.py index f873f0a604..895b58b690 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -52,7 +52,7 @@ class TestFolium(object): def setup(self): """Setup Folium Map.""" - with mock.patch('folium.folium.uuid4') as uuid4: + with mock.patch('folium.element.uuid4') as uuid4: uuid4().hex = '0' * 32 self.map = folium.Map(location=[45.5236, -122.6750], width=900, height=400, max_zoom=20, zoom_start=4) @@ -61,30 +61,30 @@ def setup(self): def test_init(self): """Test map initialization.""" - assert self.map.map_type == 'base' - assert self.map.mark_cnt == {} + assert self.map.get_name() == 'map_00000000000000000000000000000000' + assert self.map.get_root() == self.map._parent assert self.map.location == [45.5236, -122.6750] - assert self.map.map_size == {'width': 900, 'height': 400} - - tmpl = {'Tiles': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - 'attr': ('Map data (c) ' - 'OpenStreetMap contributors'), - 'map_id': 'folium_' + '0' * 32, - 'lat': 45.5236, - 'lon': -122.675, - 'max_zoom': 20, - 'size': 'style="width: 900px; height: 400px"', - 'zoom_level': 4, - 'tile_layers': [], - 'wms_layers': [], - 'image_layers': [], - 'min_zoom': 1, - 'min_lat': -90, - 'max_lat': 90, - 'min_lon': -180, - 'max_lon': 180} - - assert self.map.template_vars == tmpl + assert self.map.zoom_start == 4 + assert self.map.max_lat == 90 + assert self.map.min_lat == -90 + assert self.map.max_lon == 180 + assert self.map.min_lon == -180 + assert self.map.position == 'relative' + assert self.map.height == (400, 'px') + assert self.map.width == (900, 'px') + assert self.map.left == (0, '%') + assert self.map.top == (0, '%') + assert self.map.to_dict() == { + "name": "Map", + "id": "00000000000000000000000000000000", + "children": { + "openstreetmap": { + "name": "TileLayer", + "id": "00000000000000000000000000000000", + "children": {} + } + } + } def test_cloudmade(self): """Test cloudmade tiles and the API key.""" @@ -93,7 +93,7 @@ def test_cloudmade(self): map = folium.Map(location=[45.5236, -122.6750], tiles='cloudmade', API_key='###') - assert map.template_vars['Tiles'] == ('http://{s}.tile.cloudmade.com' + assert map._children['cloudmade'].tiles == ('http://{s}.tile.cloudmade.com' '/###/997/256/{z}/{x}/{y}.png') def test_builtin_tile(self): @@ -103,11 +103,11 @@ def test_builtin_tile(self): for tiles in default_tiles: map = folium.Map(location=[45.5236, -122.6750], tiles=tiles) tiles = ''.join(tiles.lower().strip().split()) - url = map.tile_types[tiles]['templ'].render() - attr = map.tile_types[tiles]['attr'].render() + url = map._env.get_template('tiles/{}/tiles.txt'.format(tiles)).render() + attr = map._env.get_template('tiles/{}/attr.txt'.format(tiles)).render() - assert map.template_vars['Tiles'] == url - assert map.template_vars['attr'] == attr + assert map._children[tiles].tiles == url + assert map._children[tiles].attr == attr def test_custom_tile(self): """Test custom tile URLs.""" @@ -119,8 +119,8 @@ def test_custom_tile(self): folium.Map(location=[45.5236, -122.6750], tiles=url) map = folium.Map(location=[45.52, -122.67], tiles=url, attr=attr) - assert map.template_vars['Tiles'] == url - assert map.template_vars['attr'] == attr + assert map._children[url].tiles == url + assert map._children[url].attr == attr def test_wms_layer(self): """Test WMS layer URLs.""" @@ -138,12 +138,12 @@ def test_wms_layer(self): wms_transparent=True) wms_temp = self.env.get_template('wms_layer.js') - wms = wms_temp.render({'wms_name': wms_name, + wms = wms_temp.render({'wms_name': map._children[wms_name].get_name(), 'wms_url': wms_url, 'wms_format': wms_format, 'wms_layer_names': wms_layers, 'wms_transparent': 'true'}) - assert map.template_vars['wms_layers'][0] == wms + assert ''.join(wms.split())[:-1] in ''.join(map.get_root().render().split()) def test_simple_marker(self): """Test simple marker addition.""" From c6acba203280061c271e4f71237e0918531ebda4 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Mon, 7 Sep 2015 14:25:41 +0200 Subject: [PATCH 02/36] Fix test_simple_marker --- folium/folium.py | 92 +++++++++++++++++++------------- folium/map.py | 6 ++- folium/templates/simple_popup.js | 8 ++- tests/test_folium.py | 37 ++++++++----- 4 files changed, 89 insertions(+), 54 deletions(-) diff --git a/folium/folium.py b/folium/folium.py index 5a7f5f41be..b36c3de7b4 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -7,7 +7,7 @@ """ -#from __future__ import absolute_import +from __future__ import absolute_import #from __future__ import print_function #from __future__ import division @@ -21,9 +21,11 @@ #from pkg_resources import resource_string #from folium import utilities -#from folium.six import text_type, binary_type, iteritems +from folium.six import text_type, binary_type#, iteritems from .map import Map as _Map -from .features import WmsTileLayer +from .element import Element, Figure, JavascriptLink, CssLink, Div, MacroElement +from .map import Map, TileLayer, Icon, Marker, Popup +from .features import WmsTileLayer, RegularPolygonMarker, Vega, GeoJson, GeoJsonStyle, MarkerCluster #import sys #import base64 @@ -296,40 +298,56 @@ def add_wms_layer(self, wms_name=None, wms_url=None, wms_format=None, # self.template_vars.setdefault('data_layers', []).append((data_layers)) # # @iter_obj('simple') -# def simple_marker(self, location=None, popup=None, -# marker_color='blue', marker_icon='info-sign', -# clustered_marker=False, icon_angle=0, popup_width=300): -# """Create a simple stock Leaflet marker on the map, with optional -# popup text or Vincent visualization. -# -# Parameters -# ---------- -# location: tuple or list, default None -# Latitude and Longitude of Marker (Northing, Easting) -# popup: string or tuple, default 'Pop Text' -# Input text or visualization for object. Can pass either text, -# or a tuple of the form (Vincent object, 'vis_path.json') -# It is possible to adjust the width of text/HTML popups -# using the optional keywords `popup_width` (default is 300px). -# marker_color -# color of marker you want -# marker_icon -# icon from (http://getbootstrap.com/components/) you want on the -# marker -# clustered_marker -# boolean of whether or not you want the marker clustered with -# other markers -# -# Returns -# ------- -# Marker names and HTML in obj.template_vars -# -# Example -# ------- -# >>>map.simple_marker(location=[45.5, -122.3], popup='Portland, OR') -# >>>map.simple_marker(location=[45.5, -122.3], popup=(vis, 'vis.json')) -# -# """ + def simple_marker(self, location=None, popup=None, + marker_color='blue', marker_icon='info-sign', + clustered_marker=False, icon_angle=0, popup_width=300): + """Create a simple stock Leaflet marker on the map, with optional + popup text or Vincent visualization. + + Parameters + ---------- + location: tuple or list, default None + Latitude and Longitude of Marker (Northing, Easting) + popup: string or tuple, default 'Pop Text' + Input text or visualization for object. Can pass either text, + or a tuple of the form (Vincent object, 'vis_path.json') + It is possible to adjust the width of text/HTML popups + using the optional keywords `popup_width` (default is 300px). + marker_color + color of marker you want + marker_icon + icon from (http://getbootstrap.com/components/) you want on the + marker + clustered_marker + boolean of whether or not you want the marker clustered with + other markers + + Returns + ------- + Marker names and HTML in obj.template_vars + + Example + ------- + >>>map.simple_marker(location=[45.5, -122.3], popup='Portland, OR') + >>>map.simple_marker(location=[45.5, -122.3], popup=(vis, 'vis.json')) + + """ + warnings.warn("%s is deprecated. Use %s instead" % ("simple_marker", "add_children(Marker)"), + FutureWarning, stacklevel=2) + if clustered_marker: + raise ValueError("%s is deprecated. Use %s instead" % ("clustered_marker", "MarkerCluster")) + if isinstance(popup, text_type) or isinstance(popup, binary_type): + popup_ = Popup(popup, max_width=popup_width) + elif isinstance(popup, tuple): + popup_ = Popup(Vega(json.loads(popup[0].to_json()), + width="100%", height="100%"), + max_width=popup_width) + else: + popup_ = None + marker = Marker(location, + popup=popup_, + icon=Icon(color=marker_color, icon=marker_icon, angle=icon_angle)) + self.add_children(marker) # count = self.mark_cnt['simple'] # # mark_temp = self.env.get_template('simple_marker.js') diff --git a/folium/map.py b/folium/map.py index d0e1373ce7..9d48b9ba44 100644 --- a/folium/map.py +++ b/folium/map.py @@ -267,6 +267,10 @@ def __init__(self, location, popup=None, icon=None): super(Marker, self).__init__() self._name = 'Marker' self.location = location + if icon is not None: + self.add_children(icon) + if popup is not None: + self.add_children(popup) self._template = Template(u""" {% macro script(this, kwargs) %} @@ -294,7 +298,7 @@ def __init__(self, html, max_width=300): self.script._parent = self if isinstance(html, Element): - self.html.add_children(html) + self.add_children(html) elif isinstance(html, text_type) or isinstance(html,binary_type): self.html.add_children(Html(text_type(html))) diff --git a/folium/templates/simple_popup.js b/folium/templates/simple_popup.js index 8ee17a5f9f..6b73c3a1f5 100644 --- a/folium/templates/simple_popup.js +++ b/folium/templates/simple_popup.js @@ -1,2 +1,6 @@ -{{ pop_name }}.bindPopup({{ pop_txt }}); - {{ pop_name }}._popup.options.maxWidth = {{ width }}; +var {{ pop_name }} = L.popup({maxWidth: '{{ width }}'}); +var {{ html_name }} = + $('
+ {{ pop_txt }} +
')[0]; +{{ pop_name }}.setContent({{ html_name }}); diff --git a/tests/test_folium.py b/tests/test_folium.py index 895b58b690..2cba7f9c92 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -17,6 +17,7 @@ import folium from folium.six import PY3 from folium.plugins import ScrollZoomToggler, MarkerCluster +from folium.map import Popup rootpath = os.path.abspath(os.path.dirname(__file__)) @@ -153,28 +154,36 @@ def test_simple_marker(self): # Single Simple marker. self.map.simple_marker(location=[45.50, -122.7]) - mark_1 = mark_templ.render({'marker': 'marker_1', 'lat': 45.50, + marker_1 = self.map._children.values()[-1] + mark_1 = mark_templ.render({'marker': marker_1.get_name(), + 'lat': 45.50, 'lon': -122.7, - 'icon': "{'icon':marker_1_icon}"}) - assert self.map.template_vars['custom_markers'][0][1] == mark_1 - assert self.map.template_vars['custom_markers'][0][2] == "" + 'icon': "{icon:new L.Icon.Default()}"}) + assert ''.join(mark_1.split())[:-1] in ''.join(self.map.get_root().render().split()) + #assert self.map.template_vars['custom_markers'][0][2] == "" # Test Simple marker addition. self.map.simple_marker(location=[45.60, -122.8], popup='Hi') - mark_2 = mark_templ.render({'marker': 'marker_2', 'lat': 45.60, + marker_2 = self.map._children.values()[-1] + popup_2 = marker_2._children.values()[-1] + html_2 = popup_2.html._children.values()[0] + mark_2 = mark_templ.render({'marker': marker_2.get_name(), + 'lat': 45.60, 'lon': -122.8, - 'icon': "{'icon':marker_2_icon}"}) - popup_2 = popup_templ.render({'pop_name': 'marker_2', - 'pop_txt': json.dumps('Hi'), - 'width': 300}) - assert self.map.mark_cnt['simple'] == 2 - assert self.map.template_vars['custom_markers'][1][1] == mark_2 - assert self.map.template_vars['custom_markers'][1][2] == popup_2 + 'icon': "{icon:new L.Icon.Default()}"}) + pop_2 = popup_templ.render({'pop_name': popup_2.get_name(), + 'pop_txt': 'Hi', + 'html_name': html_2.get_name(), + 'width': 300}) + #assert self.map.mark_cnt['simple'] == 2 + assert ''.join(mark_2.split())[:-1] in ''.join(self.map.get_root().render().split()) + assert ''.join(pop_2.split())[:-1] in ''.join(self.map.get_root().render().split()) + #assert self.map.template_vars['custom_markers'][1][2] == pop_2 # Test no popup. self.map.simple_marker(location=[45.60, -122.8]) - nopopup = '' - assert self.map.template_vars['custom_markers'][2][2] == nopopup + for child in self.map._children.values()[-1]._children.values(): + assert not isinstance(child, Popup) def test_div_markers(self): '''Test div marker list addition''' From 988621b3609241b3e49b021febf1806e708d7c3b Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Mon, 7 Sep 2015 15:27:32 +0200 Subject: [PATCH 03/36] Fix test_div_markers --- folium/features.py | 18 ++++++++++ folium/folium.py | 77 ++++++++++++++++++++++------------------- tests/test_folium.py | 82 +++++++++++++++++++++----------------------- 3 files changed, 100 insertions(+), 77 deletions(-) diff --git a/folium/features.py b/folium/features.py index c4f62c0da5..b75bb1d6d8 100644 --- a/folium/features.py +++ b/folium/features.py @@ -288,3 +288,21 @@ def render(self, **kwargs): figure.header.add_children(\ CssLink("https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.4.0/MarkerCluster.Default.css"), name="marker_cluster_default_css") + +class DivIcon(MacroElement): + def __init__(self, width=30, height=30): + """TODO : docstring here""" + super(DivIcon, self).__init__() + self._name = 'DivIcon' + self.width = width + self.height = height + + self._template = Template(u""" + {% macro script(this, kwargs) %} + var {{this.get_name()}} = L.divIcon({ + className: 'leaflet-div-icon', + 'iconSize': [{{ this.width }},{{ this.height }}] + }); + {{this._parent.get_name()}}.setIcon({{this.get_name()}}); + {% endmacro %} + """) diff --git a/folium/folium.py b/folium/folium.py index b36c3de7b4..eae74c1abc 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -25,7 +25,7 @@ from .map import Map as _Map from .element import Element, Figure, JavascriptLink, CssLink, Div, MacroElement from .map import Map, TileLayer, Icon, Marker, Popup -from .features import WmsTileLayer, RegularPolygonMarker, Vega, GeoJson, GeoJsonStyle, MarkerCluster +from .features import WmsTileLayer, RegularPolygonMarker, Vega, GeoJson, GeoJsonStyle, MarkerCluster, DivIcon #import sys #import base64 @@ -380,40 +380,47 @@ def simple_marker(self, location=None, popup=None, # self.template_vars.setdefault(name, []).append(append) # # @iter_obj('div_mark') -# def div_markers(self, locations=None, popups=None, -# marker_size=10, popup_width=300): -# """Create a simple div marker on the map, with optional -# popup text or Vincent visualization. Useful for marking points along a -# line. -# -# Parameters -# ---------- -# locations: list of locations, where each location is an array -# Latitude and Longitude of Marker (Northing, Easting) -# popup: list of popups, each popup should be a string or tuple. -# Default 'Pop Text' -# Input text or visualization for object. Can pass either text, -# or a tuple of the form (Vincent object, 'vis_path.json') -# It is possible to adjust the width of text/HTML popups -# using the optional keywords `popup_width`. -# (Leaflet default is 300px.) -# marker_size -# default is 5 -# -# Returns -# ------- -# Marker names and HTML in obj.template_vars -# -# Example -# ------- -# >>> map.div_markers(locations=[[37.421114, -122.128314], -# ... [37.391637, -122.085416], -# ... [37.388832, -122.087709]], -# ... popups=['1437494575531', -# ... '1437492135937', -# ... '1437493590434']) -# -# """ + def div_markers(self, locations=None, popups=None, + marker_size=10, popup_width=300): + """Create a simple div marker on the map, with optional + popup text or Vincent visualization. Useful for marking points along a + line. + + Parameters + ---------- + locations: list of locations, where each location is an array + Latitude and Longitude of Marker (Northing, Easting) + popup: list of popups, each popup should be a string or tuple. + Default 'Pop Text' + Input text or visualization for object. Can pass either text, + or a tuple of the form (Vincent object, 'vis_path.json') + It is possible to adjust the width of text/HTML popups + using the optional keywords `popup_width`. + (Leaflet default is 300px.) + marker_size + default is 5 + + Returns + ------- + Marker names and HTML in obj.template_vars + + Example + ------- + >>> map.div_markers(locations=[[37.421114, -122.128314], + ... [37.391637, -122.085416], + ... [37.388832, -122.087709]], + ... popups=['1437494575531', + ... '1437492135937', + ... '1437493590434']) + + """ + warnings.warn("%s is deprecated. Use %s instead" % ("div_markers", "Marker.add_children(DivIcon)"), + FutureWarning, stacklevel=2) + for location, popup in zip(locations,popups): + marker = Marker(location, + popup = Popup(popup), + icon = DivIcon(width=marker_size, height=marker_size)) + self.add_children(marker) # call_cnt = self.mark_cnt['div_mark'] # if locations is None or popups is None: # raise RuntimeError("Both locations and popups are mandatory") diff --git a/tests/test_folium.py b/tests/test_folium.py index 2cba7f9c92..bd1f939dd3 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -17,7 +17,9 @@ import folium from folium.six import PY3 from folium.plugins import ScrollZoomToggler, MarkerCluster -from folium.map import Popup +from folium.element import Html +from folium.map import Popup, Marker, Icon +from folium.features import DivIcon rootpath = os.path.abspath(os.path.dirname(__file__)) @@ -149,6 +151,7 @@ def test_wms_layer(self): def test_simple_marker(self): """Test simple marker addition.""" + self.map = folium.Map(location=[44, -73], zoom_start=3) mark_templ = self.env.get_template('simple_marker.js') popup_templ = self.env.get_template('simple_popup.js') @@ -187,56 +190,51 @@ def test_simple_marker(self): def test_div_markers(self): '''Test div marker list addition''' + self.map = folium.Map(location=[37.421114, -122.128314]) icon_templ = self.env.get_template('static_div_icon.js') mark_templ = self.env.get_template('simple_marker.js') popup_templ = self.env.get_template('simple_popup.js') # Test with popups (expected use case). - self.map.div_markers(locations=[[37.421114, -122.128314], - [37.391637, -122.085416], - [37.388832, -122.087709]], - popups=['1437494575531', - '1437492135937', - '1437493590434']) - icon_1 = icon_templ.render({'icon_name': 'div_marker_1_0_icon', - 'size': 10}) - mark_1 = mark_templ.render({'marker': 'div_marker_1_0', - 'lat': 37.421114, - 'lon': -122.128314, - 'icon': "{'icon':div_marker_1_0_icon}"}) - popup_1 = popup_templ.render({'pop_name': 'div_marker_1_0', - 'pop_txt': '"1437494575531"', - 'width': 300}) - assert self.map.mark_cnt['div_mark'] == 1 - assert self.map.template_vars['div_markers'][0][0] == icon_1 - assert self.map.template_vars['div_markers'][0][1] == mark_1 - assert self.map.template_vars['div_markers'][0][2] == popup_1 - - # Second set of markers with popups to test the numbering. - self.map.div_markers(locations=[[37.421114, -122.128314], - [37.391637, -122.085416], - [37.388832, -122.087709]], - popups=['1437494575531', - '1437492135937', - '1437493590434']) - icon_2 = icon_templ.render({'icon_name': 'div_marker_2_1_icon', - 'size': 10}) - mark_2 = mark_templ.render({'marker': 'div_marker_2_1', - 'lat': 37.391637, - 'lon': -122.085416, - 'icon': "{'icon':div_marker_2_1_icon}"}) - popup_2 = popup_templ.render({'pop_name': 'div_marker_2_1', - 'pop_txt': '"1437492135937"', - 'width': 300}) - assert self.map.mark_cnt['div_mark'] == 2 - assert self.map.template_vars['div_markers'][4][0] == icon_2 - assert self.map.template_vars['div_markers'][4][1] == mark_2 - assert self.map.template_vars['div_markers'][4][2] == popup_2 + locations = [[37.421114, -122.128314], + [37.391637, -122.085416], + [37.388832, -122.087709]] + popups = ['1437494575531', '1437492135937', '1437493590434'] + + self.map.div_markers(locations=locations, popups=popups) + + markers = [marker for marker in self.map._children.values() if isinstance(marker,Marker)] + assert len(markers)==3 + + for marker, location, pop in zip(markers, locations, popups): + icon = marker._children.values()[0] + popup = marker._children.values()[1] + html = popup.html._children.values()[0] + + assert isinstance(icon,DivIcon) + assert isinstance(popup,Popup) + assert isinstance(html,Html) + + icon_1 = icon_templ.render({'icon_name': icon.get_name(), + 'size': 10}) + mark_1 = mark_templ.render({'marker': marker.get_name(), + 'lat': location[0], + 'lon': location[1], + 'icon': "{icon:new L.Icon.Default()}"}) + popup_1 = popup_templ.render({'pop_name': popup.get_name(), + 'html_name' : html.get_name(), + 'pop_txt': '{}'.format(pop), + 'width': 300}) + + out = ''.join(self.map.get_root().render().split()) + assert ''.join(icon_1.split())[:-1] in out + assert ''.join(mark_1.split())[:-1] in out + assert ''.join(popup_1.split())[:-1] in out # Test no popup. If there are no popups, # then we should get a RuntimeError. - with pytest.raises(RuntimeError): + with pytest.raises(TypeError): self.map.div_markers([[45.60, -122.8]]) def test_circle_marker(self): From c91801a5b6a7bb197f8ac50241fd438c7e38c68e Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Mon, 7 Sep 2015 16:02:01 +0200 Subject: [PATCH 04/36] Fix test_circle_marker --- folium/features.py | 54 +++++++++++++ folium/folium.py | 188 +++++++++++++------------------------------ tests/test_folium.py | 15 ++-- 3 files changed, 122 insertions(+), 135 deletions(-) diff --git a/folium/features.py b/folium/features.py index b75bb1d6d8..0a8485199f 100644 --- a/folium/features.py +++ b/folium/features.py @@ -306,3 +306,57 @@ def __init__(self, width=30, height=30): {{this._parent.get_name()}}.setIcon({{this.get_name()}}); {% endmacro %} """) + +class CircleMarker(MacroElement): + def __init__(self, location, radius=500, color='black', + fill_color='black', fill_opacity=0.6, popup=None): + """Create a simple stock Leaflet marker on the map, with optional + popup text or Vincent visualization. + + Parameters + ---------- + location: tuple or list, default None + Latitude and Longitude of Marker (Northing, Easting) + popup: string or tuple, default 'Pop Text' + Input text or visualization for object. Can pass either text, + or a tuple of the form (Vincent object, 'vis_path.json') + It is possible to adjust the width of text/HTML popups + using the optional keywords `popup_width` (default is 300px). + icon: Icon plugin + the Icon plugin to use to render the marker. + + Returns + ------- + Marker names and HTML in obj.template_vars + + Example + ------- + >>>map.simple_marker(location=[45.5, -122.3], popup='Portland, OR') + >>>map.simple_marker(location=[45.5, -122.3], popup=(vis, 'vis.json')) + + """ + super(CircleMarker, self).__init__() + self._name = 'CircleMarker' + self.location = location + self.radius = radius + self.color = color + self.fill_color = fill_color + self.fill_opacity = fill_opacity + if popup is not None: + self.add_children(popup) + + self._template = Template(u""" + {% macro script(this, kwargs) %} + + var {{this.get_name()}} = L.circle( + [{{this.location[0]}},{{this.location[1]}}], + {{ this.radius }}, + { + color: '{{ this.color }}', + fillColor: '{{ this.fill_color }}', + fillOpacity: {{ this.fill_opacity }} + } + ) + .addTo({{this._parent.get_name()}}); + {% endmacro %} + """) diff --git a/folium/folium.py b/folium/folium.py index eae74c1abc..f74464af8e 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -25,7 +25,8 @@ from .map import Map as _Map from .element import Element, Figure, JavascriptLink, CssLink, Div, MacroElement from .map import Map, TileLayer, Icon, Marker, Popup -from .features import WmsTileLayer, RegularPolygonMarker, Vega, GeoJson, GeoJsonStyle, MarkerCluster, DivIcon +from .features import WmsTileLayer, RegularPolygonMarker, Vega, GeoJson, GeoJsonStyle, MarkerCluster, DivIcon,\ + CircleMarker #import sys #import base64 @@ -265,19 +266,6 @@ def add_wms_layer(self, wms_name=None, wms_url=None, wms_format=None, transparent=wms_transparent, attribution=None) self.add_children(wms, name=wms_name) -# if wms_name not in self.added_layers: -# wms_name = wms_name.replace(" ", "_") -# wms_temp = self.env.get_template('wms_layer.js') -# -# wms = wms_temp.render({ -# 'wms_name': wms_name, -# 'wms_url': wms_url, -# 'wms_format': wms_format, -# 'wms_layer_names': wms_layers, -# 'wms_transparent': str(wms_transparent).lower()}) -# self.template_vars.setdefault('wms_layers', []).append((wms)) -# self.added_layers.append({wms_name: wms_url}) -# # @iter_obj('simple') # def add_layers_to_map(self): # """ @@ -348,38 +336,7 @@ def simple_marker(self, location=None, popup=None, popup=popup_, icon=Icon(color=marker_color, icon=marker_icon, angle=icon_angle)) self.add_children(marker) -# count = self.mark_cnt['simple'] -# -# mark_temp = self.env.get_template('simple_marker.js') -# -# marker_num = 'marker_{0}'.format(count) -# add_line = "{'icon':"+marker_num+"_icon}" -# -# icon_temp = self.env.get_template('simple_icon.js') -# icon = icon_temp.render({'icon': marker_icon, -# 'icon_name': marker_num+"_icon", -# 'markerColor': marker_color, -# 'icon_angle': icon_angle}) -# -# # Get marker and popup. -# marker = mark_temp.render({'marker': 'marker_' + str(count), -# 'lat': location[0], -# 'lon': location[1], -# 'icon': add_line -# }) -# -# popup_out = self._popup_render(popup=popup, mk_name='marker_', -# count=count, width=popup_width) -# if clustered_marker: -# add_mark = 'clusteredmarkers.addLayer(marker_{0})'.format(count) -# name = 'cluster_markers' -# else: -# add_mark = 'map.addLayer(marker_{0})'.format(count) -# name = 'custom_markers' -# append = (icon, marker, popup_out, add_mark) -# self.template_vars.setdefault(name, []).append(append) -# -# @iter_obj('div_mark') + def div_markers(self, locations=None, popups=None, marker_size=10, popup_width=300): """Create a simple div marker on the map, with optional @@ -421,34 +378,6 @@ def div_markers(self, locations=None, popups=None, popup = Popup(popup), icon = DivIcon(width=marker_size, height=marker_size)) self.add_children(marker) -# call_cnt = self.mark_cnt['div_mark'] -# if locations is None or popups is None: -# raise RuntimeError("Both locations and popups are mandatory") -# for (point_cnt, (location, popup)) in enumerate(zip(locations, -# popups)): -# marker_num = 'div_marker_{0}_{1}'.format(call_cnt, point_cnt) -# -# icon_temp = self.env.get_template('static_div_icon.js') -# icon_name = marker_num+"_icon" -# icon = icon_temp.render({'icon_name': icon_name, -# 'size': marker_size}) -# -# mark_temp = self.env.get_template('simple_marker.js') -# # Get marker and popup. -# marker = mark_temp.render({'marker': marker_num, -# 'lat': location[0], -# 'lon': location[1], -# 'icon': "{'icon':"+icon_name+"}" -# }) -# -# mk_name = 'div_marker_{0}_'.format(call_cnt) -# popup_out = self._popup_render(popup=popup, -# mk_name=mk_name, -# count=point_cnt, width=popup_width) -# add_mark = 'map.addLayer(div_marker_{0}_{1})'.format(call_cnt, -# point_cnt) -# append = (icon, marker, popup_out, add_mark) -# self.template_vars.setdefault('div_markers', []).append(append) # # @iter_obj('line') # def line(self, locations, @@ -547,62 +476,61 @@ def div_markers(self, locations=None, popups=None, # self.template_vars.setdefault('multilines', []).append(append) # # @iter_obj('circle') -# def circle_marker(self, location=None, radius=500, popup=None, -# line_color='black', fill_color='black', -# fill_opacity=0.6, popup_width=300): -# """Create a simple circle marker on the map, with optional popup text -# or Vincent visualization. -# -# Parameters -# ---------- -# location: tuple or list, default None -# Latitude and Longitude of Marker (Northing, Easting) -# radius: int, default 500 -# Circle radius, in pixels -# popup: string or tuple, default 'Pop Text' -# Input text or visualization for object. Can pass either text, -# or a tuple of the form (Vincent object, 'vis_path.json') -# It is possible to adjust the width of text/HTML popups -# using the optional keywords `popup_width` (default is 300px). -# line_color: string, default black -# Line color. Can pass hex value here as well. -# fill_color: string, default black -# Fill color. Can pass hex value here as well. -# fill_opacity: float, default 0.6 -# Circle fill opacity -# -# Returns -# ------- -# Circle names and HTML in obj.template_vars -# -# Example -# ------- -# >>>map.circle_marker(location=[45.5, -122.3], -# radius=1000, popup='Portland, OR') -# >>>map.circle_marker(location=[45.5, -122.3], -# radius=1000, popup=(bar_chart, 'bar_data.json')) -# -# """ -# count = self.mark_cnt['circle'] -# -# circle_temp = self.env.get_template('circle_marker.js') -# -# circle = circle_temp.render({'circle': 'circle_' + str(count), -# 'radius': radius, -# 'lat': location[0], 'lon': location[1], -# 'line_color': line_color, -# 'fill_color': fill_color, -# 'fill_opacity': fill_opacity}) -# -# popup_out = self._popup_render(popup=popup, mk_name='circle_', -# count=count, width=popup_width) -# -# add_mark = 'map.addLayer(circle_{0})'.format(count) -# -# self.template_vars.setdefault('markers', []).append((circle, -# popup_out, -# add_mark)) -# + def circle_marker(self, location=None, radius=500, popup=None, + line_color='black', fill_color='black', + fill_opacity=0.6, popup_width=300): + """Create a simple circle marker on the map, with optional popup text + or Vincent visualization. + + Parameters + ---------- + location: tuple or list, default None + Latitude and Longitude of Marker (Northing, Easting) + radius: int, default 500 + Circle radius, in pixels + popup: string or tuple, default 'Pop Text' + Input text or visualization for object. Can pass either text, + or a tuple of the form (Vincent object, 'vis_path.json') + It is possible to adjust the width of text/HTML popups + using the optional keywords `popup_width` (default is 300px). + line_color: string, default black + Line color. Can pass hex value here as well. + fill_color: string, default black + Fill color. Can pass hex value here as well. + fill_opacity: float, default 0.6 + Circle fill opacity + + Returns + ------- + Circle names and HTML in obj.template_vars + + Example + ------- + >>>map.circle_marker(location=[45.5, -122.3], + radius=1000, popup='Portland, OR') + >>>map.circle_marker(location=[45.5, -122.3], + radius=1000, popup=(bar_chart, 'bar_data.json')) + + """ + warnings.warn("%s is deprecated. Use %s instead" % ("circle_marker", + "add_children(CircleMarker)"), + FutureWarning, stacklevel=2) + if isinstance(popup, text_type) or isinstance(popup, binary_type): + popup_ = Popup(popup, max_width=popup_width) + elif isinstance(popup, tuple): + popup_ = Popup(Vega(json.loads(popup[0].to_json()), + width="100%", height="100%"), + max_width=popup_width) + else: + popup_ = None + marker = CircleMarker(location, + radius=radius, + color=line_color, + fill_color=fill_color, + fill_opacity=fill_opacity, + popup=popup_) + self.add_children(marker) + # @iter_obj('polygon') # def polygon_marker(self, location=None, line_color='black', line_opacity=1, # line_weight=2, fill_color='blue', fill_opacity=1, diff --git a/tests/test_folium.py b/tests/test_folium.py index bd1f939dd3..bc13973c6d 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -19,7 +19,7 @@ from folium.plugins import ScrollZoomToggler, MarkerCluster from folium.element import Html from folium.map import Popup, Marker, Icon -from folium.features import DivIcon +from folium.features import DivIcon, CircleMarker rootpath = os.path.abspath(os.path.dirname(__file__)) @@ -240,25 +240,30 @@ def test_div_markers(self): def test_circle_marker(self): """Test circle marker additions.""" + self.map = folium.Map(location=[45.60, -122.8]) circ_templ = self.env.get_template('circle_marker.js') # Single Circle marker. self.map.circle_marker(location=[45.60, -122.8], popup='Hi') - circle_1 = circ_templ.render({'circle': 'circle_1', 'lat': 45.60, + marker = self.map._children.values()[-1] + circle_1 = circ_templ.render({'circle': marker.get_name(), + 'lat': 45.60, 'lon': -122.8, 'radius': 500, 'line_color': 'black', 'fill_color': 'black', 'fill_opacity': 0.6}) - assert self.map.template_vars['markers'][0][0] == circle_1 + assert ''.join(circle_1.split())[:-1] in ''.join(self.map.get_root().render().split()) # Second circle marker. self.map.circle_marker(location=[45.70, -122.9], popup='Hi') - circle_2 = circ_templ.render({'circle': 'circle_2', 'lat': 45.70, + marker = self.map._children.values()[-1] + circle_2 = circ_templ.render({'circle': marker.get_name(), + 'lat': 45.70, 'lon': -122.9, 'radius': 500, 'line_color': 'black', 'fill_color': 'black', 'fill_opacity': 0.6}) - assert self.map.template_vars['markers'][1][0] == circle_2 + assert ''.join(circle_2.split())[:-1] in ''.join(self.map.get_root().render().split()) def test_poly_marker(self): """Test polygon marker.""" From cf27f98d270fb6b9af178c2aa00f8c13433a67b5 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Mon, 7 Sep 2015 16:27:39 +0200 Subject: [PATCH 05/36] Fix test_poly_marker --- folium/features.py | 5 +- folium/folium.py | 129 +++++++++++++++++++------------------------ tests/test_folium.py | 8 ++- 3 files changed, 64 insertions(+), 78 deletions(-) diff --git a/folium/features.py b/folium/features.py index 0a8485199f..69b1b42bae 100644 --- a/folium/features.py +++ b/folium/features.py @@ -49,7 +49,7 @@ def __init__(self, url, name=None, """) class RegularPolygonMarker(MacroElement): - def __init__(self, location, popup=None, icon=None, + def __init__(self, location, popup=None, color='black', opacity=1, weight=2, fill_color='blue', fill_opacity=1, number_of_sides=4, rotation=0, radius=15): @@ -57,7 +57,6 @@ def __init__(self, location, popup=None, icon=None, super(RegularPolygonMarker, self).__init__() self._name = 'RegularPolygonMarker' self.location = location - self.icon = "new L.Icon.Default()" if icon is None else icon self.color = color self.opacity = opacity self.weight = weight @@ -66,6 +65,8 @@ def __init__(self, location, popup=None, icon=None, self.number_of_sides= number_of_sides self.rotation = rotation self.radius = radius + if popup is not None: + self.add_children(popup) self._template = Template(u""" {% macro script(this, kwargs) %} diff --git a/folium/folium.py b/folium/folium.py index f74464af8e..e4baa73d0e 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -531,79 +531,62 @@ def circle_marker(self, location=None, radius=500, popup=None, popup=popup_) self.add_children(marker) -# @iter_obj('polygon') -# def polygon_marker(self, location=None, line_color='black', line_opacity=1, -# line_weight=2, fill_color='blue', fill_opacity=1, -# num_sides=4, rotation=0, radius=15, popup=None, -# popup_width=300): -# """Custom markers using the Leaflet Data Vis Framework. -# -# -# Parameters -# ---------- -# location: tuple or list, default None -# Latitude and Longitude of Marker (Northing, Easting) -# line_color: string, default 'black' -# Marker line color -# line_opacity: float, default 1 -# Line opacity, scale 0-1 -# line_weight: int, default 2 -# Stroke weight in pixels -# fill_color: string, default 'blue' -# Marker fill color -# fill_opacity: float, default 1 -# Marker fill opacity -# num_sides: int, default 4 -# Number of polygon sides -# rotation: int, default 0 -# Rotation angle in degrees -# radius: int, default 15 -# Marker radius, in pixels -# popup: string or tuple, default 'Pop Text' -# Input text or visualization for object. Can pass either text, -# or a tuple of the form (Vincent object, 'vis_path.json') -# It is possible to adjust the width of text/HTML popups -# using the optional keywords `popup_width` (default is 300px). -# -# Returns -# ------- -# Polygon marker names and HTML in obj.template_vars -# -# """ -# -# count = self.mark_cnt['polygon'] -# -# poly_temp = self.env.get_template('poly_marker.js') -# -# polygon = poly_temp.render({'marker': 'polygon_' + str(count), -# 'lat': location[0], -# 'lon': location[1], -# 'line_color': line_color, -# 'line_opacity': line_opacity, -# 'line_weight': line_weight, -# 'fill_color': fill_color, -# 'fill_opacity': fill_opacity, -# 'num_sides': num_sides, -# 'rotation': rotation, -# 'radius': radius}) -# -# popup_out = self._popup_render(popup=popup, mk_name='polygon_', -# count=count, width=popup_width) -# -# add_mark = 'map.addLayer(polygon_{0})'.format(count) -# -# self.template_vars.setdefault('markers', []).append((polygon, -# popup_out, -# add_mark)) -# # Update JS/CSS and other Plugin files. -# js_temp = self.env.get_template('dvf_js_ref.txt').render() -# self.template_vars.update({'dvf_js': js_temp}) -# -# polygon_js = resource_string('folium', -# 'plugins/leaflet-dvf.markers.min.js') -# -# self.plugins.update({'leaflet-dvf.markers.min.js': polygon_js}) -# + def polygon_marker(self, location=None, line_color='black', line_opacity=1, + line_weight=2, fill_color='blue', fill_opacity=1, + num_sides=4, rotation=0, radius=15, popup=None, + popup_width=300): + """Custom markers using the Leaflet Data Vis Framework. + + + Parameters + ---------- + location: tuple or list, default None + Latitude and Longitude of Marker (Northing, Easting) + line_color: string, default 'black' + Marker line color + line_opacity: float, default 1 + Line opacity, scale 0-1 + line_weight: int, default 2 + Stroke weight in pixels + fill_color: string, default 'blue' + Marker fill color + fill_opacity: float, default 1 + Marker fill opacity + num_sides: int, default 4 + Number of polygon sides + rotation: int, default 0 + Rotation angle in degrees + radius: int, default 15 + Marker radius, in pixels + popup: string or tuple, default 'Pop Text' + Input text or visualization for object. Can pass either text, + or a tuple of the form (Vincent object, 'vis_path.json') + It is possible to adjust the width of text/HTML popups + using the optional keywords `popup_width` (default is 300px). + + Returns + ------- + Polygon marker names and HTML in obj.template_vars + + """ + warnings.warn("%s is deprecated. Use %s instead" % ("polygon_marker", + "add_children(RegularPolygonMarker)"), + FutureWarning, stacklevel=2) + if isinstance(popup, text_type) or isinstance(popup, binary_type): + popup_ = Popup(popup, max_width=popup_width) + elif isinstance(popup, tuple): + popup_ = Popup(Vega(json.loads(popup[0].to_json()), + width="100%", height="100%"), + max_width=popup_width) + else: + popup_ = None + marker = RegularPolygonMarker(location, popup=popup_, color=line_color, + opacity=line_opacity, weight=line_weight, + fill_color=fill_color, fill_opacity=fill_opacity, + number_of_sides=num_sides, rotation=rotation, + radius=radius) + self.add_children(marker) + # def lat_lng_popover(self): # """Enable popovers to display Lat and Lon on each click.""" # diff --git a/tests/test_folium.py b/tests/test_folium.py index bc13973c6d..329bed9392 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -268,9 +268,12 @@ def test_circle_marker(self): def test_poly_marker(self): """Test polygon marker.""" + self.map = folium.Map(location=[45.5, -122.5]) poly_temp = self.env.get_template('poly_marker.js') - polygon = poly_temp.render({'marker': 'polygon_1', + self.map.polygon_marker(location=[45.5, -122.5]) + marker = self.map._children.values()[-1] + polygon = poly_temp.render({'marker': marker.get_name(), 'lat': 45.5, 'lon': -122.5, 'line_color': 'black', @@ -282,8 +285,7 @@ def test_poly_marker(self): 'rotation': 0, 'radius': 15}) - self.map.polygon_marker(location=[45.5, -122.5]) - assert self.map.template_vars['markers'][0][0] == polygon + assert (''.join(polygon.split()))[-1] in ''.join(self.map.get_root().render().split()) def test_latlng_pop(self): """Test lat/lon popovers.""" From 92a851c1f9dbdfd9abab4a98b3246141a764e61c Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Mon, 7 Sep 2015 16:42:21 +0200 Subject: [PATCH 06/36] Fix test_latlng_pop --- folium/features.py | 46 ++++++++++++++--------------- folium/folium.py | 15 +++++----- folium/templates/lat_lng_popover.js | 8 ++--- tests/test_folium.py | 8 +++-- 4 files changed, 39 insertions(+), 38 deletions(-) diff --git a/folium/features.py b/folium/features.py index 69b1b42bae..dccf7bcabc 100644 --- a/folium/features.py +++ b/folium/features.py @@ -311,30 +311,7 @@ def __init__(self, width=30, height=30): class CircleMarker(MacroElement): def __init__(self, location, radius=500, color='black', fill_color='black', fill_opacity=0.6, popup=None): - """Create a simple stock Leaflet marker on the map, with optional - popup text or Vincent visualization. - - Parameters - ---------- - location: tuple or list, default None - Latitude and Longitude of Marker (Northing, Easting) - popup: string or tuple, default 'Pop Text' - Input text or visualization for object. Can pass either text, - or a tuple of the form (Vincent object, 'vis_path.json') - It is possible to adjust the width of text/HTML popups - using the optional keywords `popup_width` (default is 300px). - icon: Icon plugin - the Icon plugin to use to render the marker. - - Returns - ------- - Marker names and HTML in obj.template_vars - - Example - ------- - >>>map.simple_marker(location=[45.5, -122.3], popup='Portland, OR') - >>>map.simple_marker(location=[45.5, -122.3], popup=(vis, 'vis.json')) - + """TODO : docstring here """ super(CircleMarker, self).__init__() self._name = 'CircleMarker' @@ -361,3 +338,24 @@ def __init__(self, location, radius=500, color='black', .addTo({{this._parent.get_name()}}); {% endmacro %} """) + +class LatLngPopup(MacroElement): + def __init__(self): + """TODO : docstring here + """ + super(LatLngPopup, self).__init__() + self._name = 'LatLngPopup' + + self._template = Template(u""" + {% macro script(this, kwargs) %} + var {{this.get_name()}} = L.popup(); + function latLngPop(e) { + {{this.get_name()}} + .setLatLng(e.latlng) + .setContent("Latitude: " + e.latlng.lat.toFixed(4) + + "
Longitude: " + e.latlng.lng.toFixed(4)) + .openOn({{this._parent.get_name()}}); + } + {{this._parent.get_name()}}.on('click', latLngPop); + {% endmacro %} + """) diff --git a/folium/folium.py b/folium/folium.py index e4baa73d0e..c503fff253 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -26,7 +26,7 @@ from .element import Element, Figure, JavascriptLink, CssLink, Div, MacroElement from .map import Map, TileLayer, Icon, Marker, Popup from .features import WmsTileLayer, RegularPolygonMarker, Vega, GeoJson, GeoJsonStyle, MarkerCluster, DivIcon,\ - CircleMarker + CircleMarker, LatLngPopup #import sys #import base64 @@ -587,12 +587,13 @@ def polygon_marker(self, location=None, line_color='black', line_opacity=1, radius=radius) self.add_children(marker) -# def lat_lng_popover(self): -# """Enable popovers to display Lat and Lon on each click.""" -# -# latlng_temp = self.env.get_template('lat_lng_popover.js') -# self.template_vars.update({'lat_lng_pop': latlng_temp.render()}) -# + def lat_lng_popover(self): + """Enable popovers to display Lat and Lon on each click.""" + warnings.warn("%s is deprecated. Use %s instead" % ("lat_lng_popover", + "add_children(LatLngPopup)"), + FutureWarning, stacklevel=2) + self.add_children(LatLngPopup()) + # def click_for_marker(self, popup=None): # """Enable the addition of markers via clicking on the map. The marker # popup defaults to Lat/Lon, but custom text can be passed via the diff --git a/folium/templates/lat_lng_popover.js b/folium/templates/lat_lng_popover.js index 04ebe2b774..c7de1bce9d 100644 --- a/folium/templates/lat_lng_popover.js +++ b/folium/templates/lat_lng_popover.js @@ -1,10 +1,10 @@ -var popup = L.popup(); +var {{popup}} = L.popup(); function latLngPop(e) { - popup.setLatLng(e.latlng) + {{popup}}.setLatLng(e.latlng) .setContent("Latitude: " + e.latlng.lat.toFixed(4) + "
Longitude: " + e.latlng.lng.toFixed(4)) - .openOn(map); + .openOn({{map}}); } -map.on('click', latLngPop); \ No newline at end of file +{{map}}.on('click', latLngPop); \ No newline at end of file diff --git a/tests/test_folium.py b/tests/test_folium.py index 329bed9392..edd419b4d2 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -19,7 +19,7 @@ from folium.plugins import ScrollZoomToggler, MarkerCluster from folium.element import Html from folium.map import Popup, Marker, Icon -from folium.features import DivIcon, CircleMarker +from folium.features import DivIcon, CircleMarker, LatLngPopup rootpath = os.path.abspath(os.path.dirname(__file__)) @@ -291,8 +291,10 @@ def test_latlng_pop(self): """Test lat/lon popovers.""" self.map.lat_lng_popover() - pop_templ = self.env.get_template('lat_lng_popover.js').render() - assert self.map.template_vars['lat_lng_pop'] == pop_templ + pop = self.map._children.values()[-1] + pop_templ = self.env.get_template('lat_lng_popover.js').render(popup=pop.get_name(), + map=self.map.get_name()) + assert (''.join(pop_templ.split()))[:-1] in ''.join(self.map.get_root().render().split()) def test_click_for_marker(self): """Test click for marker functionality.""" From 5eec84b0a2b60abafced7847dfa91f3b7daab637 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Mon, 7 Sep 2015 17:12:18 +0200 Subject: [PATCH 07/36] Fix test_click_for_marker --- folium/features.py | 26 ++++++++++++++++ folium/folium.py | 46 +++++++++++++--------------- folium/templates/click_for_marker.js | 6 ++-- tests/test_folium.py | 11 ++++--- 4 files changed, 57 insertions(+), 32 deletions(-) diff --git a/folium/features.py b/folium/features.py index dccf7bcabc..4768cd18cc 100644 --- a/folium/features.py +++ b/folium/features.py @@ -359,3 +359,29 @@ def __init__(self): {{this._parent.get_name()}}.on('click', latLngPop); {% endmacro %} """) + +class ClickForMarker(MacroElement): + def __init__(self, popup=None): + """TODO : docstring here + """ + super(ClickForMarker, self).__init__() + self._name = 'ClickForMarker' + + if popup: + self.popup = ''.join(['"', popup, '"']) + else: + self.popup = '"Latitude: " + lat + "
Longitude: " + lng ' + + self._template = Template(u""" + {% macro script(this, kwargs) %} + function newMarker(e){ + var new_mark = L.marker().setLatLng(e.latlng).addTo({{this._parent.get_name()}}); + new_mark.dragging.enable(); + new_mark.on('dblclick', function(e){ {{this._parent.get_name()}}.removeLayer(e.target)}) + var lat = e.latlng.lat.toFixed(4), + lng = e.latlng.lng.toFixed(4); + new_mark.bindPopup({{ this.popup }}); + }; + {{this._parent.get_name()}}.on('click', newMarker); + {% endmacro %} + """) diff --git a/folium/folium.py b/folium/folium.py index c503fff253..1a20b311be 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -26,7 +26,7 @@ from .element import Element, Figure, JavascriptLink, CssLink, Div, MacroElement from .map import Map, TileLayer, Icon, Marker, Popup from .features import WmsTileLayer, RegularPolygonMarker, Vega, GeoJson, GeoJsonStyle, MarkerCluster, DivIcon,\ - CircleMarker, LatLngPopup + CircleMarker, LatLngPopup, ClickForMarker #import sys #import base64 @@ -594,30 +594,26 @@ def lat_lng_popover(self): FutureWarning, stacklevel=2) self.add_children(LatLngPopup()) -# def click_for_marker(self, popup=None): -# """Enable the addition of markers via clicking on the map. The marker -# popup defaults to Lat/Lon, but custom text can be passed via the -# popup parameter. Double click markers to remove them. -# -# Parameters -# ---------- -# popup: -# Custom popup text -# -# Example -# ------- -# >>>map.click_for_marker(popup='Your Custom Text') -# -# """ -# latlng = '"Latitude: " + lat + "
Longitude: " + lng ' -# click_temp = self.env.get_template('click_for_marker.js') -# if popup: -# popup_txt = ''.join(['"', popup, '"']) -# else: -# popup_txt = latlng -# click_str = click_temp.render({'popup': popup_txt}) -# self.template_vars.update({'click_pop': click_str}) -# + def click_for_marker(self, popup=None): + """Enable the addition of markers via clicking on the map. The marker + popup defaults to Lat/Lon, but custom text can be passed via the + popup parameter. Double click markers to remove them. + + Parameters + ---------- + popup: + Custom popup text + + Example + ------- + >>>map.click_for_marker(popup='Your Custom Text') + + """ + warnings.warn("%s is deprecated. Use %s instead" % ("click_for_marker", + "add_children(ClickForMarker)"), + FutureWarning, stacklevel=2) + self.add_children(ClickForMarker(popup=popup)) + # def fit_bounds(self, bounds, padding_top_left=None, # padding_bottom_right=None, padding=None, max_zoom=None): # """Fit the map to contain a bounding box with the maximum zoom level possible. diff --git a/folium/templates/click_for_marker.js b/folium/templates/click_for_marker.js index 2151869468..2dd5f8021c 100644 --- a/folium/templates/click_for_marker.js +++ b/folium/templates/click_for_marker.js @@ -1,9 +1,9 @@ function newMarker(e){ - var new_mark = L.marker().setLatLng(e.latlng).addTo(map); + var new_mark = L.marker().setLatLng(e.latlng).addTo({{map}}); new_mark.dragging.enable(); - new_mark.on('dblclick', function(e){map.removeLayer(e.target)}) + new_mark.on('dblclick', function(e){ {{map}}.removeLayer(e.target)}) var lat = e.latlng.lat.toFixed(4), lng = e.latlng.lng.toFixed(4); new_mark.bindPopup({{ popup }}); }; -map.on('click', newMarker) \ No newline at end of file +{{map}}.on('click', newMarker) \ No newline at end of file diff --git a/tests/test_folium.py b/tests/test_folium.py index edd419b4d2..bc0d76ceb7 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -300,17 +300,20 @@ def test_click_for_marker(self): """Test click for marker functionality.""" # Lat/lon popover. + self.map = folium.Map([46,3]) self.map.click_for_marker() click_templ = self.env.get_template('click_for_marker.js') click = click_templ.render({'popup': ('"Latitude: " + lat + "
' - 'Longitude: " + lng ')}) - assert self.map.template_vars['click_pop'] == click + 'Longitude: " + lng '), + 'map' : self.map.get_name()}) + assert (''.join(click.split()))[:-1] in ''.join(self.map.get_root().render().split()) # Custom popover. self.map.click_for_marker(popup='Test') click_templ = self.env.get_template('click_for_marker.js') - click = click_templ.render({'popup': '"Test"'}) - assert self.map.template_vars['click_pop'] == click + click = click_templ.render({'popup': '"Test"', + 'map' : self.map.get_name()}) + assert (''.join(click.split()))[:-1] in ''.join(self.map.get_root().render().split()) def test_vega_popup(self): """Test vega popups.""" From 98b1d416e1ebdd5ee72299a3709ff97732ac89f0 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Mon, 7 Sep 2015 17:48:57 +0200 Subject: [PATCH 08/36] Fix test_vega_popup --- folium/features.py | 4 +--- folium/folium.py | 2 +- folium/templates/vega_marker.js | 13 ++++++------- folium/templates/vega_parse.js | 2 +- tests/test_folium.py | 25 ++++++++++++++++++------- 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/folium/features.py b/folium/features.py index 4768cd18cc..ca9e8e34cd 100644 --- a/folium/features.py +++ b/folium/features.py @@ -120,9 +120,7 @@ def render(self, **kwargs): self.json = json.dumps(self.data) self._parent.html.add_children(Element(Template(""" -
-
+
""").render(this=self, kwargs=kwargs)), name=self.get_name()) self._parent.script.add_children(Element(Template(""" diff --git a/folium/folium.py b/folium/folium.py index 1a20b311be..11e0f3aee5 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -14,7 +14,7 @@ import warnings #import codecs #import functools -#import json +import json #from uuid import uuid4 #from jinja2 import Environment, PackageLoader diff --git a/folium/templates/vega_marker.js b/folium/templates/vega_marker.js index 131838b3f0..8f0760bd67 100644 --- a/folium/templates/vega_marker.js +++ b/folium/templates/vega_marker.js @@ -1,7 +1,6 @@ -{{ mark }}.on('click', function() { - var div = $('
')[0]; - {{ mark }}.bindPopup(div); - {{ mark }}._popup.options.maxWidth = {{ max_width }}; - {{ mark }}.openPopup(); - parse('{{ json_out }}', '{{ vega_id }}'); - }); \ No newline at end of file +var {{vega}} = $('
')[0]; + {{popup}}.setContent({{vega}}); + +{{marker}}.bindPopup({{popup}}); + +vega_parse({{vega_json}},{{vega}}); \ No newline at end of file diff --git a/folium/templates/vega_parse.js b/folium/templates/vega_parse.js index c9ececdcd1..2d8e9a5089 100644 --- a/folium/templates/vega_parse.js +++ b/folium/templates/vega_parse.js @@ -1,4 +1,4 @@ -function parse(spec, div) { +function vega_parse(spec, div) { vg.parse.spec(spec, function(chart) { chart({el:div}).update(); }); diff --git a/tests/test_folium.py b/tests/test_folium.py index bc0d76ceb7..74cda32298 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -318,17 +318,28 @@ def test_click_for_marker(self): def test_vega_popup(self): """Test vega popups.""" + self.map = folium.Map([45.60, -122.8]) + + vega_templ = self.env.get_template('vega_marker.js') + vega_parse = self.env.get_template('vega_parse.js') + vis = vincent.Bar(width=675 - 75, height=350 - 50, no_data=True) + data = json.loads(vis.to_json()) self.map.simple_marker(location=[45.60, -122.8], popup=(vis, 'vis.json')) - popup_temp = self.env.get_template('vega_marker.js') - vega = popup_temp.render({'mark': 'marker_1', 'div_id': 'vis', - 'width': 675, 'height': 350, - 'max_width': 900, - 'json_out': 'vis.json', - 'vega_id': '#vis'}) - assert self.map.template_vars['custom_markers'][0][2] == vega + + marker = self.map._children.values()[-1] + popup = marker._children.values()[-1] + vega = popup._children.values()[-1] + vega_str = vega_templ.render({'vega': vega.get_name(), + 'popup':popup.get_name(), + 'marker':marker.get_name(), + 'vega_json':json.dumps(data), + }) + out = ''.join(self.map.get_root().render().split()) + assert ''.join(vega_parse.render().split()) in out + assert (''.join(vega_str.split()))[:-1] in out def test_geo_json(self): """Test geojson method.""" From b7ae4cc37b30b2d081c872577171d53b0136ac42 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Mon, 7 Sep 2015 18:30:40 +0200 Subject: [PATCH 09/36] cleanup simple_popup.js --- folium/templates/simple_popup.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/folium/templates/simple_popup.js b/folium/templates/simple_popup.js index 6b73c3a1f5..c2dfc8629d 100644 --- a/folium/templates/simple_popup.js +++ b/folium/templates/simple_popup.js @@ -1,6 +1,4 @@ var {{ pop_name }} = L.popup({maxWidth: '{{ width }}'}); var {{ html_name }} = - $('
- {{ pop_txt }} -
')[0]; + $('
{{ pop_txt }}
')[0]; {{ pop_name }}.setContent({{ html_name }}); From 3ef265848d8b2ce861baad30a5607b479c4b425f Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Mon, 7 Sep 2015 20:07:43 +0200 Subject: [PATCH 10/36] Fix py3 OrderedDict.values() specificity --- tests/test_folium.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/test_folium.py b/tests/test_folium.py index 74cda32298..fa204b1eb5 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -157,7 +157,7 @@ def test_simple_marker(self): # Single Simple marker. self.map.simple_marker(location=[45.50, -122.7]) - marker_1 = self.map._children.values()[-1] + marker_1 = list(self.map._children.values())[-1] mark_1 = mark_templ.render({'marker': marker_1.get_name(), 'lat': 45.50, 'lon': -122.7, @@ -167,9 +167,9 @@ def test_simple_marker(self): # Test Simple marker addition. self.map.simple_marker(location=[45.60, -122.8], popup='Hi') - marker_2 = self.map._children.values()[-1] - popup_2 = marker_2._children.values()[-1] - html_2 = popup_2.html._children.values()[0] + marker_2 = list(self.map._children.values())[-1] + popup_2 = list(marker_2._children.values())[-1] + html_2 = list(popup_2.html._children.values())[0] mark_2 = mark_templ.render({'marker': marker_2.get_name(), 'lat': 45.60, 'lon': -122.8, @@ -185,7 +185,7 @@ def test_simple_marker(self): # Test no popup. self.map.simple_marker(location=[45.60, -122.8]) - for child in self.map._children.values()[-1]._children.values(): + for child in list(self.map._children.values())[-1]._children.values(): assert not isinstance(child, Popup) def test_div_markers(self): @@ -208,9 +208,9 @@ def test_div_markers(self): assert len(markers)==3 for marker, location, pop in zip(markers, locations, popups): - icon = marker._children.values()[0] - popup = marker._children.values()[1] - html = popup.html._children.values()[0] + icon = list(marker._children.values())[0] + popup = list(marker._children.values())[1] + html = list(popup.html._children.values())[0] assert isinstance(icon,DivIcon) assert isinstance(popup,Popup) @@ -245,7 +245,7 @@ def test_circle_marker(self): # Single Circle marker. self.map.circle_marker(location=[45.60, -122.8], popup='Hi') - marker = self.map._children.values()[-1] + marker = list(self.map._children.values())[-1] circle_1 = circ_templ.render({'circle': marker.get_name(), 'lat': 45.60, 'lon': -122.8, 'radius': 500, @@ -256,7 +256,7 @@ def test_circle_marker(self): # Second circle marker. self.map.circle_marker(location=[45.70, -122.9], popup='Hi') - marker = self.map._children.values()[-1] + marker = list(self.map._children.values())[-1] circle_2 = circ_templ.render({'circle': marker.get_name(), 'lat': 45.70, 'lon': -122.9, 'radius': 500, @@ -272,7 +272,7 @@ def test_poly_marker(self): poly_temp = self.env.get_template('poly_marker.js') self.map.polygon_marker(location=[45.5, -122.5]) - marker = self.map._children.values()[-1] + marker = list(self.map._children.values())[-1] polygon = poly_temp.render({'marker': marker.get_name(), 'lat': 45.5, 'lon': -122.5, @@ -291,7 +291,7 @@ def test_latlng_pop(self): """Test lat/lon popovers.""" self.map.lat_lng_popover() - pop = self.map._children.values()[-1] + pop = list(self.map._children.values())[-1] pop_templ = self.env.get_template('lat_lng_popover.js').render(popup=pop.get_name(), map=self.map.get_name()) assert (''.join(pop_templ.split()))[:-1] in ''.join(self.map.get_root().render().split()) @@ -329,9 +329,9 @@ def test_vega_popup(self): self.map.simple_marker(location=[45.60, -122.8], popup=(vis, 'vis.json')) - marker = self.map._children.values()[-1] - popup = marker._children.values()[-1] - vega = popup._children.values()[-1] + marker = list(self.map._children.values())[-1] + popup = list(marker._children.values())[-1] + vega = list(popup._children.values())[-1] vega_str = vega_templ.render({'vega': vega.get_name(), 'popup':popup.get_name(), 'marker':marker.get_name(), From 2311b9f427c0fda8338dec26382e5280e124d5e8 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Wed, 9 Sep 2015 18:51:18 +0200 Subject: [PATCH 11/36] geo_json replacement --- folium/features.py | 175 +++++++++++++++++++--- folium/folium.py | 354 +++++++++++++++++++-------------------------- 2 files changed, 304 insertions(+), 225 deletions(-) diff --git a/folium/features.py b/folium/features.py index ca9e8e34cd..600e2c80ee 100644 --- a/folium/features.py +++ b/folium/features.py @@ -8,7 +8,7 @@ from jinja2 import Template import json -from .utilities import color_brewer, _parse_size +from .utilities import color_brewer, _parse_size, legend_scaler from .element import Element, Figure, JavascriptLink, CssLink, Div, MacroElement from .map import Map, TileLayer, Icon, Marker, Popup @@ -196,8 +196,46 @@ def __init__(self, data): {% endmacro %} """) +class TopoJson(MacroElement): + def __init__(self, data, object_path): + """TODO docstring here. + """ + super(TopoJson, self).__init__() + self._name = 'TopoJson' + if 'read' in dir(data): + self.data = data.read() + elif type(data) is dict: + self.data = json.dumps(data) + else: + self.data = data + + self.object_path = object_path + + self._template = Template(u""" + {% macro script(this, kwargs) %} + var {{this.get_name()}}_data = {{this.data}}; + var {{this.get_name()}} = L.geoJson(topojson.feature( + {{this.get_name()}}_data, + {{this.get_name()}}_data.{{this.object_path}} + )).addTo({{this._parent.get_name()}}); + {% endmacro %} + """) + def render(self,**kwargs): + super(TopoJson,self).render(**kwargs) + + figure = self.get_root() + assert isinstance(figure,Figure), ("You cannot render this Element " + "if it's not in a Figure.") + + figure.header.add_children(\ + JavascriptLink("https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.9/topojson.min.js"), + name='topojson') + class GeoJsonStyle(MacroElement): - def __init__(self, color_domain, color_code, color_data=None, key_on='feature.properties.color'): + def __init__(self, color_domain, color_code, color_data=None, + key_on='feature.properties.color', + weight=1, opacity=1, color='black', + fill_opacity=0.6, dash_array='3'): """TODO : docstring here. """ super(GeoJsonStyle, self).__init__() @@ -208,30 +246,45 @@ def __init__(self, color_domain, color_code, color_data=None, key_on='feature.pr self.color_data = json.dumps(color_data) self.key_on = key_on + self.weight = weight + self.opacity = opacity + self.color = color + self.fill_color = color_code + self.fill_opacity = fill_opacity + self.dash_array = dash_array + self._template = Template(u""" {% macro script(this, kwargs) %} - var {{this.get_name()}} = { - color_scale : d3.scale.threshold() - .domain({{this.color_domain}}) - .range({{this.color_range}}), - color_data : {{this.color_data}}, - color_function : function(feature) { - {% if this.color_data=='null' %} - return this.color_scale({{this.key_on}}); - {% else %} - return this.color_scale(this.color_data[{{this.key_on}}]); - {% endif %} - }, - }; + {% if not this.color_range %} + var {{this.get_name()}} = { + color_function : function(feature) { + return '{{this.fill_color}}'; + }, + }; + {%else%} + var {{this.get_name()}} = { + color_scale : d3.scale.threshold() + .domain({{this.color_domain}}) + .range({{this.color_range}}), + color_data : {{this.color_data}}, + color_function : function(feature) { + {% if this.color_data=='null' %} + return this.color_scale({{this.key_on}}); + {% else %} + return this.color_scale(this.color_data[{{this.key_on}}]); + {% endif %} + }, + }; + {%endif%} {{this._parent.get_name()}}.setStyle(function(feature) { return { fillColor: {{this.get_name()}}.color_function(feature), - weight: 2, - opacity: 1, - color: 'white', - dashArray: '3', - fillOpacity: 0.7 + weight: {{this.weight}}, + opacity: {{this.opacity}}, + color: '{{this.color}}', + fillOpacity: {{this.fill_opacity}}, + dashArray: '{{this.dash_array}}' }; }); {% endmacro %} @@ -247,6 +300,88 @@ def render(self,**kwargs): JavascriptLink("https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"), name='d3') +class ColorScale(MacroElement): + def __init__(self, color_domain, color_code, caption=""): + """TODO : docstring here. + """ + super(ColorScale, self).__init__() + self._name = 'ColorScale' + + self.color_domain = color_domain + self.color_range = color_brewer(color_code, n=len(color_domain)) + self.tick_labels=legend_scaler(self.color_domain) + + self.caption = caption + self.fill_color = color_code + + self._template = Template(u""" + {% macro script(this, kwargs) %} + var {{this.get_name()}} = {}; + + {%if this.color_range %} + {{this.get_name()}}.color = d3.scale.threshold() + .domain({{this.color_domain}}) + .range({{this.color_range}}); + {%else%} + {{this.get_name()}}.color = d3.scale.threshold() + .domain([{{ this.color_domain[0] }}, {{ this.color_domain[-1] }}]) + .range(['{{ this.fill_color }}', '{{ this.fill_color }}']); + {%endif%} + + {{this.get_name()}}.x = d3.scale.linear() + .domain([{{ this.color_domain[0] }}, {{ this.color_domain[-1] }}]) + .range([0, 400]); + + {{this.get_name()}}.legend = L.control({position: 'topright'}); + {{this.get_name()}}.legend.onAdd = function (map) {var div = L.DomUtil.create('div', 'legend'); return div}; + {{this.get_name()}}.legend.addTo({{this._parent.get_name()}}); + + {{this.get_name()}}.xAxis = d3.svg.axis() + .scale({{this.get_name()}}.x) + .orient("top") + .tickSize(1) + .tickValues({{ this.tick_labels }}); + + {{this.get_name()}}.svg = d3.select(".legend.leaflet-control").append("svg") + .attr("id", 'legend') + .attr("width", 450) + .attr("height", 40); + + {{this.get_name()}}.g = {{this.get_name()}}.svg.append("g") + .attr("class", "key") + .attr("transform", "translate(25,16)"); + + {{this.get_name()}}.g.selectAll("rect") + .data({{this.get_name()}}.color.range().map(function(d, i) { + return { + x0: i ? {{this.get_name()}}.x({{this.get_name()}}.color.domain()[i - 1]) : {{this.get_name()}}.x.range()[0], + x1: i < {{this.get_name()}}.color.domain().length ? {{this.get_name()}}.x({{this.get_name()}}.color.domain()[i]) : {{this.get_name()}}.x.range()[1], + z: d + }; + })) + .enter().append("rect") + .attr("height", 10) + .attr("x", function(d) { return d.x0; }) + .attr("width", function(d) { return d.x1 - d.x0; }) + .style("fill", function(d) { return d.z; }); + + {{this.get_name()}}.g.call({{this.get_name()}}.xAxis).append("text") + .attr("class", "caption") + .attr("y", 21) + .text('{{ this.caption }}'); + {% endmacro %} + """) + def render(self,**kwargs): + super(ColorScale,self).render(**kwargs) + + figure = self.get_root() + assert isinstance(figure,Figure), ("You cannot render this Element " + "if it's not in a Figure.") + + figure.header.add_children(\ + JavascriptLink("https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"), + name='d3') + class MarkerCluster(MacroElement): """Adds a MarkerCluster layer on the map.""" def __init__(self): diff --git a/folium/folium.py b/folium/folium.py index 11e0f3aee5..21ac829a4f 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -26,7 +26,7 @@ from .element import Element, Figure, JavascriptLink, CssLink, Div, MacroElement from .map import Map, TileLayer, Icon, Marker, Popup from .features import WmsTileLayer, RegularPolygonMarker, Vega, GeoJson, GeoJsonStyle, MarkerCluster, DivIcon,\ - CircleMarker, LatLngPopup, ClickForMarker + CircleMarker, LatLngPopup, ClickForMarker, ColorScale, TopoJson #import sys #import base64 @@ -760,210 +760,154 @@ def add_plugin(self, plugin): # 'vega_id': vega_id}) # else: # raise TypeError("Unrecognized popup type: {!r}".format(popup)) -# -# @iter_obj('geojson') -# def geo_json(self, geo_path=None, geo_str=None, data_out='data.json', -# data=None, columns=None, key_on=None, threshold_scale=None, -# fill_color='blue', fill_opacity=0.6, line_color='black', -# line_weight=1, line_opacity=1, legend_name=None, -# topojson=None, reset=False): -# """Apply a GeoJSON overlay to the map. -# -# Plot a GeoJSON overlay on the base map. There is no requirement -# to bind data (passing just a GeoJSON plots a single-color overlay), -# but there is a data binding option to map your columnar data to -# different feature objects with a color scale. -# -# If data is passed as a Pandas dataframe, the "columns" and "key-on" -# keywords must be included, the first to indicate which DataFrame -# columns to use, the second to indicate the layer in the GeoJSON -# on which to key the data. The 'columns' keyword does not need to be -# passed for a Pandas series. -# -# Colors are generated from color brewer (http://colorbrewer2.org/) -# sequential palettes on a D3 threshold scale. The scale defaults to the -# following quantiles: [0, 0.5, 0.75, 0.85, 0.9]. A custom scale can be -# passed to `threshold_scale` of length <=6, in order to match the -# color brewer range. -# -# TopoJSONs can be passed as "geo_path", but the "topojson" keyword must -# also be passed with the reference to the topojson objects to convert. -# See the topojson.feature method in the TopoJSON API reference: -# https://github.com/mbostock/topojson/wiki/API-Reference -# -# -# Parameters -# ---------- -# geo_path: string, default None -# URL or File path to your GeoJSON data -# geo_str: string, default None -# String of GeoJSON, alternative to geo_path -# data_out: string, default 'data.json' -# Path to write Pandas DataFrame/Series to JSON if binding data -# data: Pandas DataFrame or Series, default None -# Data to bind to the GeoJSON. -# columns: dict or tuple, default None -# If the data is a Pandas DataFrame, the columns of data to be bound. -# Must pass column 1 as the key, and column 2 the values. -# key_on: string, default None -# Variable in the GeoJSON file to bind the data to. Must always -# start with 'feature' and be in JavaScript objection notation. -# Ex: 'feature.id' or 'feature.properties.statename'. -# threshold_scale: list, default None -# Data range for D3 threshold scale. Defaults to the following range -# of quantiles: [0, 0.5, 0.75, 0.85, 0.9], rounded to the nearest -# order-of-magnitude integer. Ex: 270 rounds to 200, 5600 to 6000. -# fill_color: string, default 'blue' -# Area fill color. Can pass a hex code, color name, or if you are -# binding data, one of the following color brewer palettes: -# 'BuGn', 'BuPu', 'GnBu', 'OrRd', 'PuBu', 'PuBuGn', 'PuRd', 'RdPu', -# 'YlGn', 'YlGnBu', 'YlOrBr', and 'YlOrRd'. -# fill_opacity: float, default 0.6 -# Area fill opacity, range 0-1. -# line_color: string, default 'black' -# GeoJSON geopath line color. -# line_weight: int, default 1 -# GeoJSON geopath line weight. -# line_opacity: float, default 1 -# GeoJSON geopath line opacity, range 0-1. -# legend_name: string, default None -# Title for data legend. If not passed, defaults to columns[1]. -# topojson: string, default None -# If using a TopoJSON, passing "objects.yourfeature" to the topojson -# keyword argument will enable conversion to GeoJSON. -# reset: boolean, default False -# Remove all current geoJSON layers, start with new layer -# -# Output -# ------ -# GeoJSON data layer in obj.template_vars -# -# Example -# ------- -# >>> m.geo_json(geo_path='us-states.json', line_color='blue', -# line_weight=3) -# >>> m.geo_json(geo_path='geo.json', data=df, -# columns=['Data 1', 'Data 2'], -# key_on='feature.properties.myvalue', fill_color='PuBu', -# threshold_scale=[0, 20, 30, 40, 50, 60]) -# >>> m.geo_json(geo_path='countries.json', topojson='objects.countries') -# """ -# -# if reset: -# reset_vars = ['json_paths', 'func_vars', 'color_scales', -# 'geo_styles', 'gjson_layers', 'map_legends', -# 'topo_convert'] -# for var in reset_vars: -# self.template_vars.update({var: []}) -# self.mark_cnt['geojson'] = 1 -# -# def json_style(style_cnt, line_color, line_weight, line_opacity, -# fill_color, fill_opacity, quant_fill): -# """Generate JSON styling function from template""" -# style_temp = self.env.get_template('geojson_style.js') -# style = style_temp.render({'style': style_cnt, -# 'line_color': line_color, -# 'line_weight': line_weight, -# 'line_opacity': line_opacity, -# 'fill_color': fill_color, -# 'fill_opacity': fill_opacity, -# 'quantize_fill': quant_fill}) -# return style -# -# # Set map type to geojson. -# self.map_type = 'geojson' -# -# # Get JSON map layer template pieces, convert TopoJSON if necessary. -# # geo_str is really a hack. -# if geo_path: -# geo_path = ".defer(d3.json, '{0}')".format(geo_path) -# elif geo_str: -# fmt = (".defer(function(callback)" -# "{{callback(null, JSON.parse('{}'))}})").format -# geo_path = fmt(geo_str) -# if topojson is None: -# map_var = '_'.join(['gjson', str(self.mark_cnt['geojson'])]) -# layer_var = map_var -# else: -# map_var = '_'.join(['tjson', str(self.mark_cnt['geojson'])]) -# topo_obj = '.'.join([map_var, topojson]) -# layer_var = '_'.join(['topo', str(self.mark_cnt['geojson'])]) -# topo_templ = self.env.get_template('topo_func.js') -# topo_func = topo_templ.render({'map_var': layer_var, -# 't_var': map_var, -# 't_var_obj': topo_obj}) -# topo_lib = self.env.get_template('topojson_ref.txt').render() -# self.template_vars.update({'topojson': topo_lib}) -# self.template_vars.setdefault('topo_convert', -# []).append(topo_func) -# -# style_count = '_'.join(['style', str(self.mark_cnt['geojson'])]) -# -# # Get Data binding pieces if available. -# if data is not None: -# -# import pandas as pd -# -# # Create DataFrame with only the relevant columns. -# if isinstance(data, pd.DataFrame): -# data = pd.concat([data[columns[0]], data[columns[1]]], axis=1) -# -# # Save data to JSON. -# self.json_data[data_out] = utilities.transform_data(data) -# -# # Add data to queue. -# d_path = ".defer(d3.json, '{0}')".format(data_out) -# self.template_vars.setdefault('json_paths', []).append(d_path) -# -# # Add data variable to makeMap function. -# data_var = '_'.join(['data', str(self.mark_cnt['geojson'])]) -# self.template_vars.setdefault('func_vars', []).append(data_var) -# -# # D3 Color scale. -# series = data[columns[1]] -# if threshold_scale and len(threshold_scale) > 6: -# raise ValueError -# domain = threshold_scale or utilities.split_six(series=series) -# if len(domain) > 253: -# raise ValueError('The threshold scale must be length <= 253') -# if not utilities.color_brewer(fill_color): -# raise ValueError('Please pass a valid color brewer code to ' -# 'fill_local. See docstring for valid codes.') -# -# palette = utilities.color_brewer(fill_color, len(domain)) -# d3range = palette[0: len(domain) + 1] -# tick_labels = utilities.legend_scaler(domain) -# -# color_temp = self.env.get_template('d3_threshold.js') -# d3scale = color_temp.render({'domain': domain, -# 'range': d3range}) -# self.template_vars.setdefault('color_scales', []).append(d3scale) -# -# # Create legend. -# name = legend_name or columns[1] -# leg_templ = self.env.get_template('d3_map_legend.js') -# legend = leg_templ.render({'lin_max': int(domain[-1]*1.1), -# 'tick_labels': tick_labels, -# 'caption': name}) -# self.template_vars.setdefault('map_legends', []).append(legend) -# -# # Style with color brewer colors. -# matchColor = 'color(matchKey({0}, {1}))'.format(key_on, data_var) -# style = json_style(style_count, line_color, line_weight, -# line_opacity, None, fill_opacity, matchColor) -# else: -# style = json_style(style_count, line_color, line_weight, -# line_opacity, fill_color, fill_opacity, None) -# -# layer = ('gJson_layer_{0} = L.geoJson({1}, {{style: {2},' -# 'onEachFeature: onEachFeature}}).addTo(map)' -# .format(self.mark_cnt['geojson'], layer_var, style_count)) -# -# self.template_vars.setdefault('json_paths', []).append(geo_path) -# self.template_vars.setdefault('func_vars', []).append(map_var) -# self.template_vars.setdefault('geo_styles', []).append(style) -# self.template_vars.setdefault('gjson_layers', []).append(layer) -# + + def geo_json(self, geo_path=None, geo_str=None, data_out='data.json', + data=None, columns=None, key_on=None, threshold_scale=None, + fill_color='blue', fill_opacity=0.6, line_color='black', + line_weight=1, line_opacity=1, legend_name=None, + topojson=None, reset=False): + """Apply a GeoJSON overlay to the map. + + Plot a GeoJSON overlay on the base map. There is no requirement + to bind data (passing just a GeoJSON plots a single-color overlay), + but there is a data binding option to map your columnar data to + different feature objects with a color scale. + + If data is passed as a Pandas dataframe, the "columns" and "key-on" + keywords must be included, the first to indicate which DataFrame + columns to use, the second to indicate the layer in the GeoJSON + on which to key the data. The 'columns' keyword does not need to be + passed for a Pandas series. + + Colors are generated from color brewer (http://colorbrewer2.org/) + sequential palettes on a D3 threshold scale. The scale defaults to the + following quantiles: [0, 0.5, 0.75, 0.85, 0.9]. A custom scale can be + passed to `threshold_scale` of length <=6, in order to match the + color brewer range. + + TopoJSONs can be passed as "geo_path", but the "topojson" keyword must + also be passed with the reference to the topojson objects to convert. + See the topojson.feature method in the TopoJSON API reference: + https://github.com/mbostock/topojson/wiki/API-Reference + + + Parameters + ---------- + geo_path: string, default None + URL or File path to your GeoJSON data + geo_str: string, default None + String of GeoJSON, alternative to geo_path + data_out: string, default 'data.json' + Path to write Pandas DataFrame/Series to JSON if binding data + data: Pandas DataFrame or Series, default None + Data to bind to the GeoJSON. + columns: dict or tuple, default None + If the data is a Pandas DataFrame, the columns of data to be bound. + Must pass column 1 as the key, and column 2 the values. + key_on: string, default None + Variable in the GeoJSON file to bind the data to. Must always + start with 'feature' and be in JavaScript objection notation. + Ex: 'feature.id' or 'feature.properties.statename'. + threshold_scale: list, default None + Data range for D3 threshold scale. Defaults to the following range + of quantiles: [0, 0.5, 0.75, 0.85, 0.9], rounded to the nearest + order-of-magnitude integer. Ex: 270 rounds to 200, 5600 to 6000. + fill_color: string, default 'blue' + Area fill color. Can pass a hex code, color name, or if you are + binding data, one of the following color brewer palettes: + 'BuGn', 'BuPu', 'GnBu', 'OrRd', 'PuBu', 'PuBuGn', 'PuRd', 'RdPu', + 'YlGn', 'YlGnBu', 'YlOrBr', and 'YlOrRd'. + fill_opacity: float, default 0.6 + Area fill opacity, range 0-1. + line_color: string, default 'black' + GeoJSON geopath line color. + line_weight: int, default 1 + GeoJSON geopath line weight. + line_opacity: float, default 1 + GeoJSON geopath line opacity, range 0-1. + legend_name: string, default None + Title for data legend. If not passed, defaults to columns[1]. + topojson: string, default None + If using a TopoJSON, passing "objects.yourfeature" to the topojson + keyword argument will enable conversion to GeoJSON. + reset: boolean, default False + Remove all current geoJSON layers, start with new layer + + Output + ------ + GeoJSON data layer in obj.template_vars + + Example + ------- + >>> m.geo_json(geo_path='us-states.json', line_color='blue', + line_weight=3) + >>> m.geo_json(geo_path='geo.json', data=df, + columns=['Data 1', 'Data 2'], + key_on='feature.properties.myvalue', fill_color='PuBu', + threshold_scale=[0, 20, 30, 40, 50, 60]) + >>> m.geo_json(geo_path='countries.json', topojson='objects.countries') + """ + warnings.warn("%s is deprecated. Use %s instead" % ("geo_json", "add_children(GeoJson)"), + FutureWarning, stacklevel=2) + + # Create GeoJson object + if geo_path: + geo_data = open(geo_path) + elif geo_str: + geo_data = geo_str + else: + geo_data = {} + + if topojson: + geo_json = TopoJson(geo_data, topojson) + else: + geo_json = GeoJson(geo_data) + + # Create color_data dict + if hasattr(data,'set_index'): + # This is a pd.DataFrame + color_data = data.set_index(columns[0])[columns[1]].to_dict() + elif hasattr(data, 'to_dict'): + # This is a pd.Series + color_data = data.to_dict() + elif data: + color_data = dict(data) + else: + color_data = None + + # Compute color_domain + if threshold_scale: + color_domain = list(threshold_scale).copy() + else: + # To avoid explicit pandas dependency ; changed default behavior. + warnings.warn("'threshold_scale' default behavior has changed." + " Now you get a linear scale between the 'min' and the 'mas'" + " of your data." + " To get former behavior, use folium.utilities.split_six.", + FutureWarning, stacklevel=2) + data_min = min(color_data.values()) + data_max = max(color_data.values()) + if data_min==data_max: + data_min = data_min if data_min<0 else 0 if data_min>0 else -1 + data_max = data_max if data_max>0 else 0 if data_max<0 else 1 + data_min, data_max = 1.01*data_min-0.01*data_max, 1.01*data_max-0.01*data_min + nb_class = 6 + color_domain = [data_min+i*(data_max-data_min)*1./nb_class for i in range(1+nb_class)] + + # Create GeoJsonStyle + geo_json_style = GeoJsonStyle(\ + color_domain, fill_color, color_data=color_data, + key_on=key_on, + weight=line_weight, opacity=line_opacity, color=line_color, + fill_opacity=fill_opacity) + + # Create ColorScale + color_scale = ColorScale(color_domain, fill_color, caption=legend_name) + + geo_json.add_children(geo_json_style) + self.add_children(geo_json) + self.add_children(color_scale) + # @iter_obj('image_overlay') # def image_overlay(self, data, opacity=0.25, min_lat=-90.0, max_lat=90.0, # min_lon=-180.0, max_lon=180.0, image_name=None, From b0fb739a419aa606ae1e0915459a9c6249065622 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Mon, 14 Sep 2015 14:26:31 +0200 Subject: [PATCH 12/36] Split test_folium.geo_json --- tests/test_folium.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/tests/test_folium.py b/tests/test_folium.py index fa204b1eb5..7767b4efde 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -341,9 +341,11 @@ def test_vega_popup(self): assert ''.join(vega_parse.render().split()) in out assert (''.join(vega_str.split()))[:-1] in out - def test_geo_json(self): + def test_geo_json_simple(self): """Test geojson method.""" + self.map = folium.Map([43, -100], zoom_start=4) + path = os.path.join(rootpath, 'us-counties.json') geo_path = ".defer(d3.json, '{0}')".format(path) @@ -370,6 +372,13 @@ def test_geo_json(self): assert templ['gjson_layers'][0] == layer assert templ['json_paths'][0] == geo_path + def test_geo_json_bad_color(self): + """Test geojson method.""" + + self.map = folium.Map([43, -100], zoom_start=4) + + path = os.path.join(rootpath, 'us-counties.json') + # Data binding incorrect color value error. data = setup_data() with pytest.raises(ValueError): @@ -377,6 +386,13 @@ def test_geo_json(self): columns=['FIPS_Code', 'Unemployed_2011'], key_on='feature.id', fill_color='blue') + def test_geo_json_bad_threshold_scale(self): + """Test geojson method.""" + + self.map = folium.Map([43, -100], zoom_start=4) + + path = os.path.join(rootpath, 'us-counties.json') + # Data binding threshold_scale too long. data = setup_data() with pytest.raises(ValueError): @@ -386,6 +402,13 @@ def test_geo_json(self): threshold_scale=[1, 2, 3, 4, 5, 6, 7], fill_color='YlGnBu') + def test_geo_json_data_binding(self): + """Test geojson method.""" + + self.map = folium.Map([43, -100], zoom_start=4) + + path = os.path.join(rootpath, 'us-counties.json') + # With DataFrame data binding, default threshold scale. self.map.geo_json(geo_path=path, data=data, columns=['FIPS_Code', 'Unemployed_2011'], @@ -424,6 +447,13 @@ def test_geo_json(self): assert templ['json_paths'] == [data_path, geo_path] assert templ['color_scales'][0] == scale + def test_topo_json(self): + """Test geojson method.""" + + self.map = folium.Map([43, -100], zoom_start=4) + + path = os.path.join(rootpath, 'us-counties.json') + # Adding TopoJSON as additional layer. path_2 = 'or_counties_topo.json' self.map.geo_json(geo_path=path_2, topojson='objects.or_counties_geo') From 6dc60f7075082942775c5841fdadcb1c607d9152 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Mon, 14 Sep 2015 17:15:27 +0200 Subject: [PATCH 13/36] Fix test_geo_json_simple --- folium/features.py | 59 ++----------------------------- folium/folium.py | 6 ++-- folium/templates/color_scale.js | 0 folium/templates/geo_json.js | 1 + folium/templates/geojson_style.js | 15 +++++--- tests/test_folium.py | 44 ++++++++++++----------- 6 files changed, 42 insertions(+), 83 deletions(-) create mode 100644 folium/templates/color_scale.js create mode 100644 folium/templates/geo_json.js diff --git a/folium/features.py b/folium/features.py index 600e2c80ee..2a135d39e8 100644 --- a/folium/features.py +++ b/folium/features.py @@ -235,7 +235,7 @@ class GeoJsonStyle(MacroElement): def __init__(self, color_domain, color_code, color_data=None, key_on='feature.properties.color', weight=1, opacity=1, color='black', - fill_opacity=0.6, dash_array='3'): + fill_opacity=0.6, dash_array=0): """TODO : docstring here. """ super(GeoJsonStyle, self).__init__() @@ -314,63 +314,8 @@ def __init__(self, color_domain, color_code, caption=""): self.caption = caption self.fill_color = color_code - self._template = Template(u""" - {% macro script(this, kwargs) %} - var {{this.get_name()}} = {}; + self._template = self._env.get_template('color_scale.js') - {%if this.color_range %} - {{this.get_name()}}.color = d3.scale.threshold() - .domain({{this.color_domain}}) - .range({{this.color_range}}); - {%else%} - {{this.get_name()}}.color = d3.scale.threshold() - .domain([{{ this.color_domain[0] }}, {{ this.color_domain[-1] }}]) - .range(['{{ this.fill_color }}', '{{ this.fill_color }}']); - {%endif%} - - {{this.get_name()}}.x = d3.scale.linear() - .domain([{{ this.color_domain[0] }}, {{ this.color_domain[-1] }}]) - .range([0, 400]); - - {{this.get_name()}}.legend = L.control({position: 'topright'}); - {{this.get_name()}}.legend.onAdd = function (map) {var div = L.DomUtil.create('div', 'legend'); return div}; - {{this.get_name()}}.legend.addTo({{this._parent.get_name()}}); - - {{this.get_name()}}.xAxis = d3.svg.axis() - .scale({{this.get_name()}}.x) - .orient("top") - .tickSize(1) - .tickValues({{ this.tick_labels }}); - - {{this.get_name()}}.svg = d3.select(".legend.leaflet-control").append("svg") - .attr("id", 'legend') - .attr("width", 450) - .attr("height", 40); - - {{this.get_name()}}.g = {{this.get_name()}}.svg.append("g") - .attr("class", "key") - .attr("transform", "translate(25,16)"); - - {{this.get_name()}}.g.selectAll("rect") - .data({{this.get_name()}}.color.range().map(function(d, i) { - return { - x0: i ? {{this.get_name()}}.x({{this.get_name()}}.color.domain()[i - 1]) : {{this.get_name()}}.x.range()[0], - x1: i < {{this.get_name()}}.color.domain().length ? {{this.get_name()}}.x({{this.get_name()}}.color.domain()[i]) : {{this.get_name()}}.x.range()[1], - z: d - }; - })) - .enter().append("rect") - .attr("height", 10) - .attr("x", function(d) { return d.x0; }) - .attr("width", function(d) { return d.x1 - d.x0; }) - .style("fill", function(d) { return d.z; }); - - {{this.get_name()}}.g.call({{this.get_name()}}.xAxis).append("text") - .attr("class", "caption") - .attr("y", 21) - .text('{{ this.caption }}'); - {% endmacro %} - """) def render(self,**kwargs): super(ColorScale,self).render(**kwargs) diff --git a/folium/folium.py b/folium/folium.py index 21ac829a4f..9b4415690c 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -877,8 +877,8 @@ def geo_json(self, geo_path=None, geo_str=None, data_out='data.json', # Compute color_domain if threshold_scale: - color_domain = list(threshold_scale).copy() - else: + color_domain = list(threshold_scale) + elif color_data: # To avoid explicit pandas dependency ; changed default behavior. warnings.warn("'threshold_scale' default behavior has changed." " Now you get a linear scale between the 'min' and the 'mas'" @@ -893,6 +893,8 @@ def geo_json(self, geo_path=None, geo_str=None, data_out='data.json', data_min, data_max = 1.01*data_min-0.01*data_max, 1.01*data_max-0.01*data_min nb_class = 6 color_domain = [data_min+i*(data_max-data_min)*1./nb_class for i in range(1+nb_class)] + else: + color_domain = [-1,1] # Create GeoJsonStyle geo_json_style = GeoJsonStyle(\ diff --git a/folium/templates/color_scale.js b/folium/templates/color_scale.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/folium/templates/geo_json.js b/folium/templates/geo_json.js new file mode 100644 index 0000000000..2a1516b2ef --- /dev/null +++ b/folium/templates/geo_json.js @@ -0,0 +1 @@ +var {{ this.get_name() }} = L.geoJson({{ this.data }}).addTo({{ this._parent.get_name() }}); \ No newline at end of file diff --git a/folium/templates/geojson_style.js b/folium/templates/geojson_style.js index c720bb2a06..58eb559942 100644 --- a/folium/templates/geojson_style.js +++ b/folium/templates/geojson_style.js @@ -1,13 +1,20 @@ -function {{ style }}(feature) { +var {{ this.get_name() }} = { + color_function : function(feature) { + return '{{ fill_color }}'; + }, + }; + +{{ this._parent.get_name() }}.setStyle(function (feature) { return { {%- if quantize_fill %} fillColor: {{ quantize_fill }}, {%- else %} - fillColor: '{{ fill_color }}', + fillColor: {{ this.get_name() }}.color_function(feature), {%- endif %} weight: {{ line_weight }}, opacity: {{ line_opacity }}, color: '{{ line_color }}', - fillOpacity: {{ fill_opacity }} + fillOpacity: {{ fill_opacity }}, + dashArray: '{{ dash_array }}' }; -} \ No newline at end of file +}); \ No newline at end of file diff --git a/tests/test_folium.py b/tests/test_folium.py index 7767b4efde..d82ebe96da 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -19,7 +19,7 @@ from folium.plugins import ScrollZoomToggler, MarkerCluster from folium.element import Html from folium.map import Popup, Marker, Icon -from folium.features import DivIcon, CircleMarker, LatLngPopup +from folium.features import DivIcon, CircleMarker, LatLngPopup, GeoJson, GeoJsonStyle, ColorScale rootpath = os.path.abspath(os.path.dirname(__file__)) @@ -344,33 +344,37 @@ def test_vega_popup(self): def test_geo_json_simple(self): """Test geojson method.""" + # No data binding. self.map = folium.Map([43, -100], zoom_start=4) - path = os.path.join(rootpath, 'us-counties.json') - geo_path = ".defer(d3.json, '{0}')".format(path) - - # No data binding. self.map.geo_json(geo_path=path) - geo_path = ".defer(d3.json, '{0}')".format(path) - map_var = 'gjson_1' - layer_var = 'gjson_1' + + geo_json = [x for x in self.map._children.values() if isinstance(x,GeoJson)][0] + color_scale = [x for x in self.map._children.values() if isinstance(x,ColorScale)][0] + geo_json_style = list(geo_json._children.values())[0] + out = ''.join(self.map._parent.render().split()) + + # Verify the geo_json object + obj_temp = self.env.get_template('geo_json.js') + obj = obj_temp.render(this = geo_json) + assert ''.join(obj.split())[:-1] in out + + # Verify the style style_temp = self.env.get_template('geojson_style.js') - style = style_temp.render({'style': 'style_1', + style = style_temp.render({'this': geo_json_style, 'line_color': 'black', 'line_weight': 1, 'line_opacity': 1, 'fill_color': 'blue', - 'fill_opacity': 0.6}) - layer = ('gJson_layer_{0} = L.geoJson({1}, {{style: {2},' - 'onEachFeature: onEachFeature}}).addTo(map)' - .format(1, layer_var, 'style_1')) - - templ = self.map.template_vars - assert self.map.map_type == 'geojson' - assert templ['func_vars'][0] == map_var - assert templ['geo_styles'][0] == style - assert templ['gjson_layers'][0] == layer - assert templ['json_paths'][0] == geo_path + 'fill_opacity': 0.6, + 'dash_array' : 0, + }) + assert ''.join(style.split())[:-1] in out + + # Verify the color_scale + colorsc_temp = self.env.get_template('color_scale.js') + colorsc = colorsc_temp.render(this=color_scale) + assert ''.join(colorsc.split())[:-1] in out def test_geo_json_bad_color(self): """Test geojson method.""" From 55fe18f046bc798be6aa85d2aac9fdff44c7cc54 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Mon, 14 Sep 2015 17:17:26 +0200 Subject: [PATCH 14/36] Un-indent color_scale template --- folium/templates/color_scale.js | 55 +++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/folium/templates/color_scale.js b/folium/templates/color_scale.js index e69de29bb2..58f01b5c3c 100644 --- a/folium/templates/color_scale.js +++ b/folium/templates/color_scale.js @@ -0,0 +1,55 @@ +{% macro script(this, kwargs) %} + var {{this.get_name()}} = {}; + + {%if this.color_range %} + {{this.get_name()}}.color = d3.scale.threshold() + .domain({{this.color_domain}}) + .range({{this.color_range}}); + {%else%} + {{this.get_name()}}.color = d3.scale.threshold() + .domain([{{ this.color_domain[0] }}, {{ this.color_domain[-1] }}]) + .range(['{{ this.fill_color }}', '{{ this.fill_color }}']); + {%endif%} + + {{this.get_name()}}.x = d3.scale.linear() + .domain([{{ this.color_domain[0] }}, {{ this.color_domain[-1] }}]) + .range([0, 400]); + + {{this.get_name()}}.legend = L.control({position: 'topright'}); + {{this.get_name()}}.legend.onAdd = function (map) {var div = L.DomUtil.create('div', 'legend'); return div}; + {{this.get_name()}}.legend.addTo({{this._parent.get_name()}}); + + {{this.get_name()}}.xAxis = d3.svg.axis() + .scale({{this.get_name()}}.x) + .orient("top") + .tickSize(1) + .tickValues({{ this.tick_labels }}); + + {{this.get_name()}}.svg = d3.select(".legend.leaflet-control").append("svg") + .attr("id", 'legend') + .attr("width", 450) + .attr("height", 40); + + {{this.get_name()}}.g = {{this.get_name()}}.svg.append("g") + .attr("class", "key") + .attr("transform", "translate(25,16)"); + + {{this.get_name()}}.g.selectAll("rect") + .data({{this.get_name()}}.color.range().map(function(d, i) { + return { + x0: i ? {{this.get_name()}}.x({{this.get_name()}}.color.domain()[i - 1]) : {{this.get_name()}}.x.range()[0], + x1: i < {{this.get_name()}}.color.domain().length ? {{this.get_name()}}.x({{this.get_name()}}.color.domain()[i]) : {{this.get_name()}}.x.range()[1], + z: d + }; + })) + .enter().append("rect") + .attr("height", 10) + .attr("x", function(d) { return d.x0; }) + .attr("width", function(d) { return d.x1 - d.x0; }) + .style("fill", function(d) { return d.z; }); + + {{this.get_name()}}.g.call({{this.get_name()}}.xAxis).append("text") + .attr("class", "caption") + .attr("y", 21) + .text('{{ this.caption }}'); +{% endmacro %} \ No newline at end of file From 6832e24444eed695d9fa2215a67fec827cb12b2a Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Mon, 14 Sep 2015 17:47:25 +0200 Subject: [PATCH 15/36] Fix test_geo_json_bad* --- folium/folium.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/folium/folium.py b/folium/folium.py index 9b4415690c..c2d158957c 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -27,7 +27,7 @@ from .map import Map, TileLayer, Icon, Marker, Popup from .features import WmsTileLayer, RegularPolygonMarker, Vega, GeoJson, GeoJsonStyle, MarkerCluster, DivIcon,\ CircleMarker, LatLngPopup, ClickForMarker, ColorScale, TopoJson - +from .utilities import color_brewer #import sys #import base64 @@ -850,6 +850,12 @@ def geo_json(self, geo_path=None, geo_str=None, data_out='data.json', warnings.warn("%s is deprecated. Use %s instead" % ("geo_json", "add_children(GeoJson)"), FutureWarning, stacklevel=2) + if threshold_scale and len(threshold_scale)>6: + raise ValueError + if data is not None and not color_brewer(fill_color): + raise ValueError('Please pass a valid color brewer code to ' + 'fill_local. See docstring for valid codes.') + # Create GeoJson object if geo_path: geo_data = open(geo_path) From ff8785d2ca3827907d8e9b8778b34b2b236f58ee Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Tue, 29 Sep 2015 14:10:31 +0200 Subject: [PATCH 16/36] Fix test_geo_json_data_binding --- folium/templates/d3_threshold.js | 2 +- tests/test_folium.py | 78 ++++++++++++++++---------------- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/folium/templates/d3_threshold.js b/folium/templates/d3_threshold.js index 2009006205..de94a24f6a 100644 --- a/folium/templates/d3_threshold.js +++ b/folium/templates/d3_threshold.js @@ -1,3 +1,3 @@ -var color = d3.scale.threshold() +{{ this.get_name() }}.color = d3.scale.threshold() .domain({{ domain }}) .range({{ range }}); \ No newline at end of file diff --git a/tests/test_folium.py b/tests/test_folium.py index d82ebe96da..9dd686186e 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -360,15 +360,13 @@ def test_geo_json_simple(self): assert ''.join(obj.split())[:-1] in out # Verify the style - style_temp = self.env.get_template('geojson_style.js') - style = style_temp.render({'this': geo_json_style, - 'line_color': 'black', - 'line_weight': 1, - 'line_opacity': 1, - 'fill_color': 'blue', - 'fill_opacity': 0.6, - 'dash_array' : 0, - }) + assert geo_json_style.color == 'black' + assert geo_json_style.weight == 1 + assert geo_json_style.opacity == 1 + assert geo_json_style.fill_color == 'blue' + assert geo_json_style.fill_opacity == 0.6 + assert geo_json_style.dash_array == 0 + style = geo_json_style._template.module.script(geo_json_style) assert ''.join(style.split())[:-1] in out # Verify the color_scale @@ -409,47 +407,51 @@ def test_geo_json_bad_threshold_scale(self): def test_geo_json_data_binding(self): """Test geojson method.""" + data = setup_data() + self.map = folium.Map([43, -100], zoom_start=4) path = os.path.join(rootpath, 'us-counties.json') # With DataFrame data binding, default threshold scale. self.map.geo_json(geo_path=path, data=data, + threshold_scale=[4.0, 1000.0, 3000.0, 5000.0, 9000.0], columns=['FIPS_Code', 'Unemployed_2011'], key_on='feature.id', fill_color='YlGnBu', reset=True) - geo_path = ".defer(d3.json, '{0}')".format(path) - data_path = ".defer(d3.json, '{0}')".format('data.json') - map_var = 'gjson_1' - layer_var = 'gjson_1' - data_var = 'data_1' + out = self.map._parent.render() + + geo_json = [x for x in self.map._children.values() if isinstance(x,GeoJson)][0] + color_scale = [x for x in self.map._children.values() if isinstance(x,ColorScale)][0] + geo_json_style = list(geo_json._children.values())[0] + + # Verify the geo_json object + obj_temp = self.env.get_template('geo_json.js') + obj = obj_temp.render(this = geo_json) + assert ''.join(obj.split())[:-1] in ''.join(out.split()) + + # Verify the style + assert geo_json_style.color == 'black' + assert geo_json_style.weight == 1 + assert geo_json_style.opacity == 1 + assert geo_json_style.fill_color == 'YlGnBu' + assert geo_json_style.fill_opacity == 0.6 + assert geo_json_style.dash_array == 0 + style = geo_json_style._template.module.script(geo_json_style) + assert ''.join(style.split())[:-1] in ''.join(out.split()) + + # Verify the colorscale domain = [4.0, 1000.0, 3000.0, 5000.0, 9000.0] palette = folium.utilities.color_brewer('YlGnBu') - d3range = palette[0: len(domain) + 1] - color_temp = self.env.get_template('d3_threshold.js') - scale = color_temp.render({'domain': domain, - 'range': d3range}) - - style_temp = self.env.get_template('geojson_style.js') - color = 'color(matchKey(feature.id, data_1))' - style = style_temp.render({'style': 'style_1', - 'line_color': 'black', - 'line_weight': 1, - 'line_opacity': 1, - 'quantize_fill': color, - 'fill_opacity': 0.6}) - - layer = ('gJson_layer_{0} = L.geoJson({1}, {{style: {2},' - 'onEachFeature: onEachFeature}}).addTo(map)' - .format(1, layer_var, 'style_1')) - - templ = self.map.template_vars - assert templ['func_vars'] == [data_var, map_var] - assert templ['geo_styles'][0] == style - assert templ['gjson_layers'][0] == layer - assert templ['json_paths'] == [data_path, geo_path] - assert templ['color_scales'][0] == scale + d3range = palette[0: len(domain) + 2] + colorscale_obj = [val for key,val in self.map._children.items() if isinstance(val, ColorScale)][0] + colorscale_temp = self.env.get_template('d3_threshold.js') + colorscale = colorscale_temp.render({ + 'this' : colorscale_obj, + 'domain': domain, + 'range': d3range}) + assert ''.join(colorscale.split())[:-1] in ''.join(out.split()) def test_topo_json(self): """Test geojson method.""" From 0154a4c49fe91a315ae753ac9e79e8b340f9ad18 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Tue, 29 Sep 2015 16:39:38 +0200 Subject: [PATCH 17/36] Fix test_topo_json --- tests/test_folium.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/tests/test_folium.py b/tests/test_folium.py index 9dd686186e..0a7057a993 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -19,7 +19,8 @@ from folium.plugins import ScrollZoomToggler, MarkerCluster from folium.element import Html from folium.map import Popup, Marker, Icon -from folium.features import DivIcon, CircleMarker, LatLngPopup, GeoJson, GeoJsonStyle, ColorScale +from folium.features import DivIcon, CircleMarker, LatLngPopup, GeoJson,\ + GeoJsonStyle, ColorScale, TopoJson rootpath = os.path.abspath(os.path.dirname(__file__)) @@ -463,20 +464,14 @@ def test_topo_json(self): # Adding TopoJSON as additional layer. path_2 = 'or_counties_topo.json' self.map.geo_json(geo_path=path_2, topojson='objects.or_counties_geo') - geo_path_2 = ".defer(d3.json, '{0}')".format(path_2) - map_var_2 = 'tjson_2' - layer_var_2 = 'topo_2' - topo_func = ('topo_2 = topojson.feature(tjson_2,' - ' tjson_2.objects.or_counties_geo);') - fmt = ('gJson_layer_{0} = L.geoJson({1}, {{style: {2},' - 'onEachFeature: onEachFeature}}).addTo(map)') - layer_2 = fmt.format(2, layer_var_2, 'style_2') - - templ = self.map.template_vars - assert templ['func_vars'] == [data_var, map_var, map_var_2] - assert templ['gjson_layers'][1] == layer_2 - assert templ['json_paths'] == [data_path, geo_path, geo_path_2] - assert templ['topo_convert'][0] == topo_func + + out = self.map._parent.render() + + # Verify TopoJson + topo_json = [val for key,val in self.map._children.items()\ + if isinstance(val,TopoJson)][0] + topojson_str = topo_json._template.module.script(topo_json) + assert ''.join(topojson_str.split())[:-1] in ''.join(out.split()) def test_map_build(self): """Test map build.""" From d401e4b7a1b892a721ac56b0005dabb56627b2c5 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Tue, 29 Sep 2015 17:52:05 +0200 Subject: [PATCH 18/36] Fix test_build_map --- folium/map.py | 1 + folium/templates/fol_template.html | 208 ++++++----------------------- tests/test_folium.py | 26 ++-- 3 files changed, 63 insertions(+), 172 deletions(-) diff --git a/folium/map.py b/folium/map.py index 9d48b9ba44..b2588050e3 100644 --- a/folium/map.py +++ b/folium/map.py @@ -110,6 +110,7 @@ def __init__(self, location=None, width='100%', height='100%', height: {{this.height[0]}}{{this.height[1]}}; left: {{this.left[0]}}{{this.left[1]}}; top: {{this.top[0]}}{{this.top[1]}}; + } {% endmacro %} {% macro html(this, kwargs) %} diff --git a/folium/templates/fol_template.html b/folium/templates/fol_template.html index 9306c2d619..3a2d0fa6b0 100644 --- a/folium/templates/fol_template.html +++ b/folium/templates/fol_template.html @@ -1,40 +1,22 @@ - - + - - - - - - - - - - - - - - {% for name, plugin in plugins.items() %} - {% set plugin_nb = 0 %} - {% for plugin_object in plugin %} - {{plugin_object.render_header(plugin_nb)}} - {% set plugin_nb = plugin_nb +1 %} - {% endfor %} - {% endfor %} - - {{ dvf_js }} - {{ d3 }} - {{ vega }} - {{ jquery }} + + + + + + + + + + + -
+
- {% for name, plugin in plugins.items() %} - {% set plugin_nb = 0 %} - {% for plugin_object in plugin %} - {{plugin_object.render_html(plugin_nb)}} - {% set plugin_nb = plugin_nb +1 %} - {% endfor %} - {% endfor %} + - + - diff --git a/tests/test_folium.py b/tests/test_folium.py index 0a7057a993..06e48c4c65 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -477,24 +477,34 @@ def test_map_build(self): """Test map build.""" # Standard map. - self.map._build_map() + self.setup() + out = self.map._parent.render() html_templ = self.env.get_template('fol_template.html') - tmpl = {'Tiles': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + tile_layers = [ + { + 'id' : 'tile_layer_'+'0'*32, + 'address' : 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 'attr': ('Map data (c) ' 'OpenStreetMap contributors'), - 'map_id': 'folium_' + '0' * 32, - 'lat': 45.5236, 'lon': -122.675, 'max_zoom': 20, - 'size': 'style="width: 900px; height: 400px"', - 'zoom_level': 4, + 'max_zoom': 20, 'min_zoom': 1, + } + ] + tmpl = { + 'map_id': 'map_' + '0' * 32, + 'lat': 45.5236, 'lon': -122.675, + 'size': 'width: 900.0px; height: 400.0px;', + 'zoom_level': 4, 'min_lat': -90, 'max_lat': 90, 'min_lon': -180, - 'max_lon': 180} + 'max_lon': 180, + 'tile_layers': tile_layers, + } HTML = html_templ.render(tmpl, plugins={}) - assert self.map.HTML == HTML + assert ''.join(out.split()) == ''.join(HTML.split()) def test_tile_attr_unicode(self): """Test tile attribution unicode From c9548ba02e7ee84f27e6b97dfcaa56739230d528 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Tue, 29 Sep 2015 18:05:23 +0200 Subject: [PATCH 19/36] Fix test_tile_attr_unicode --- folium/map.py | 2 ++ tests/test_folium.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/folium/map.py b/folium/map.py index b2588050e3..e4465e5d77 100644 --- a/folium/map.py +++ b/folium/map.py @@ -200,6 +200,8 @@ def __init__(self, tiles='OpenStreetMap', name=None, if not attr: raise ValueError('Custom tiles must' ' also be passed an attribution') + if isinstance(attr, binary_type): + attr = text_type(attr, 'utf8') self.attr = attr self._template = Template(u""" diff --git a/tests/test_folium.py b/tests/test_folium.py index 06e48c4c65..f9ef683cc2 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -516,14 +516,14 @@ def test_tile_attr_unicode(self): if not PY3: map = folium.Map(location=[45.5236, -122.6750], tiles='test', attr=b'unicode') - map._build_map() + map._parent.render() else: map = folium.Map(location=[45.5236, -122.6750], tiles='test', attr=u'юникод') - map._build_map() + map._parent.render() map = folium.Map(location=[45.5236, -122.6750], tiles='test', attr='юникод') - map._build_map() + map._parent.render() def test_create_map(self): """Test create map.""" From 2431c51863b45dbfe676b81440a06ae5b30aceb7 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Tue, 29 Sep 2015 18:07:24 +0200 Subject: [PATCH 20/36] Fix test_create_map --- tests/test_folium.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_folium.py b/tests/test_folium.py index f9ef683cc2..49a88d86a0 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -543,7 +543,7 @@ def test_create_map(self): map.polygon_marker(location=[45.5, -122.5]) # Test write. - map.create_map() + map._parent.render() def test_line(self): """Test line.""" From 80be994b999ef449259ad466547b5f71686a74a2 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Wed, 30 Sep 2015 11:23:10 +0200 Subject: [PATCH 21/36] Fix test_line --- folium/features.py | 46 +++++++++++++++++ folium/folium.py | 96 +++++++++++++++++------------------- folium/templates/polyline.js | 3 +- tests/test_folium.py | 17 +++++-- 4 files changed, 105 insertions(+), 57 deletions(-) diff --git a/folium/features.py b/folium/features.py index 2a135d39e8..315639fa7e 100644 --- a/folium/features.py +++ b/folium/features.py @@ -463,3 +463,49 @@ def __init__(self, popup=None): {{this._parent.get_name()}}.on('click', newMarker); {% endmacro %} """) + +class PolyLine(MacroElement): + def __init__(self, locations, color=None, weight=None, opacity=None, latlon=True): + """Creates a PolyLine object to append into a map with Map.add_children. + + Parameters + ---------- + locations: list of points (latitude, longitude) + Latitude and Longitude of line (Northing, Easting) + color: string, default Leaflet's default ('#03f') + weight: float, default Leaflet's default (5) + opacity: float, default Leaflet's default (0.5) + latlon: bool, default True + Whether locations are given in the form [[lat,lon]] or not ([[lon,lat]] if False). + Note that the default GeoJson format is latlon=False, + while Leaflet polyline's default is latlon=True. + + examples : + # providing file + GeoJson(open('foo.json')) + + # providing dict + GeoJson(json.load(open('foo.json'))) + + # providing string + GeoJson(open('foo.json').read()) + """ + super(PolyLine, self).__init__() + self._name = 'PolyLine' + self.data = [[x[1],x[0]] for x in locations] if not latlon else locations + self.color = color + self.weight = weight + self.opacity = opacity + + self._template = Template(u""" + {% macro script(this, kwargs) %} + var {{this.get_name()}} = L.polyline( + {{this.data}}, + { + {% if this.color != None %}color: '{{ this.color }}',{% endif %} + {% if this.weight != None %}weight: {{ this.weight }},{% endif %} + {% if this.opacity != None %}opacity: {{ this.opacity }},{% endif %} + }); + {{this._parent.get_name()}}.addLayer({{this.get_name()}}); + {% endmacro %} + """) diff --git a/folium/folium.py b/folium/folium.py index c2d158957c..b4d625b0b7 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -26,7 +26,7 @@ from .element import Element, Figure, JavascriptLink, CssLink, Div, MacroElement from .map import Map, TileLayer, Icon, Marker, Popup from .features import WmsTileLayer, RegularPolygonMarker, Vega, GeoJson, GeoJsonStyle, MarkerCluster, DivIcon,\ - CircleMarker, LatLngPopup, ClickForMarker, ColorScale, TopoJson + CircleMarker, LatLngPopup, ClickForMarker, ColorScale, TopoJson, PolyLine from .utilities import color_brewer #import sys #import base64 @@ -378,55 +378,51 @@ def div_markers(self, locations=None, popups=None, popup = Popup(popup), icon = DivIcon(width=marker_size, height=marker_size)) self.add_children(marker) -# -# @iter_obj('line') -# def line(self, locations, -# line_color=None, line_opacity=None, line_weight=None, -# popup=None, popup_width=300): -# """Add a line to the map with optional styles. -# -# Parameters -# ---------- -# locations: list of points (latitude, longitude) -# Latitude and Longitude of line (Northing, Easting) -# line_color: string, default Leaflet's default ('#03f') -# line_opacity: float, default Leaflet's default (0.5) -# line_weight: float, default Leaflet's default (5) -# popup: string or tuple, default 'Pop Text' -# Input text or visualization for object. Can pass either text, -# or a tuple of the form (Vincent object, 'vis_path.json') -# It is possible to adjust the width of text/HTML popups -# using the optional keywords `popup_width` (default is 300px). -# -# Note: If the optional styles are omitted, they will not be included -# in the HTML output and will obtain the Leaflet defaults listed above. -# -# Example -# ------- -# >>>map.line(locations=[(45.5, -122.3), (42.3, -71.0)]) -# >>>map.line(locations=[(45.5, -122.3), (42.3, -71.0)], -# line_color='red', line_opacity=1.0) -# -# """ -# count = self.mark_cnt['line'] -# -# line_temp = self.env.get_template('polyline.js') -# -# polyline_opts = {'color': line_color, 'weight': line_weight, -# 'opacity': line_opacity} -# -# varname = 'line_{}'.format(count) -# line_rendered = line_temp.render({'line': varname, -# 'locations': locations, -# 'options': polyline_opts}) -# -# popup_out = self._popup_render(popup=popup, mk_name='line_', -# count=count, width=popup_width) -# -# add_line = 'map.addLayer({});'.format(varname) -# append = (line_rendered, popup_out, add_line) -# self.template_vars.setdefault('lines', []).append((append)) -# + + def line(self, locations, + line_color=None, line_opacity=None, line_weight=None, + popup=None, popup_width=300, latlon=True): + """Add a line to the map with optional styles. + + Parameters + ---------- + locations: list of points (latitude, longitude) + Latitude and Longitude of line (Northing, Easting) + line_color: string, default Leaflet's default ('#03f') + line_opacity: float, default Leaflet's default (0.5) + line_weight: float, default Leaflet's default (5) + popup: string or tuple, default 'Pop Text' + Input text or visualization for object. Can pass either text, + or a tuple of the form (Vincent object, 'vis_path.json') + It is possible to adjust the width of text/HTML popups + using the optional keywords `popup_width` (default is 300px). + latlon: bool, default True + Whether locations are given in the form [[lat,lon]] or not ([[lon,lat]] if False). + Note that the default GeoJson format is latlon=False, + while Leaflet polyline's default is latlon=True. + + Note: If the optional styles are omitted, they will not be included + in the HTML output and will obtain the Leaflet defaults listed above. + + Example + ------- + >>>map.line(locations=[(45.5, -122.3), (42.3, -71.0)]) + >>>map.line(locations=[(45.5, -122.3), (42.3, -71.0)], + line_color='red', line_opacity=1.0) + + """ + + p = PolyLine(locations, + color=line_color, + weight=line_weight, + opacity=line_opacity, + latlon=latlon, + ) + + if popup is not None: + p.add_children(Popup(popup, max_width=popup_width)) + + self.add_children(p) # @iter_obj('multiline') # def multiline(self, locations, line_color=None, line_opacity=None, # line_weight=None): diff --git a/folium/templates/polyline.js b/folium/templates/polyline.js index 81672ee56a..dc3ded7331 100644 --- a/folium/templates/polyline.js +++ b/folium/templates/polyline.js @@ -1,5 +1,4 @@ -var latLngs = [{% for loc in locations %} [{{ loc[0] }}, {{ loc[1] }}], {% endfor %}]; -var {{ line }} = L.polyline(latLngs,{ +var {{ this.get_name() }} = L.polyline({{locations}},{ {% if options.color != None %}color: '{{ options.color }}',{% endif %} {% if options.weight != None %}weight: {{ options.weight }},{% endif %} {% if options.opacity != None %}opacity: {{ options.opacity }},{% endif %} diff --git a/tests/test_folium.py b/tests/test_folium.py index 49a88d86a0..fcb6269cc3 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -20,7 +20,7 @@ from folium.element import Html from folium.map import Popup, Marker, Icon from folium.features import DivIcon, CircleMarker, LatLngPopup, GeoJson,\ - GeoJsonStyle, ColorScale, TopoJson + GeoJsonStyle, ColorScale, TopoJson, PolyLine rootpath = os.path.abspath(os.path.dirname(__file__)) @@ -560,15 +560,22 @@ def test_line(self): [[45.5237, -122.6750], [45.5237, -122.6751]], [[45.5238, -122.6750], [45.5238, -122.6751]] ] - line_rendered = line_temp.render({'line': 'line_1', - 'locations': locations, - 'options': line_opts}) + self.setup() self.map.line(locations=locations, line_color=line_opts['color'], line_weight=line_opts['weight'], line_opacity=line_opts['opacity']) - assert self.map.template_vars['lines'][0][0] == line_rendered + polyline = [val for key,val in self.map._children.items()\ + if isinstance(val,PolyLine)][0] + out = self.map._parent.render() + + line_rendered = line_temp.render({'line': 'line_1', + 'this':polyline, + 'locations': locations, + 'options': line_opts}) + + assert ''.join(line_rendered.split()) in ''.join(out.split()) def test_multi_polyline(self): """Test multi_polyline.""" From 00a1dfe11dadc04b84ac7b093713756e43e9a618 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Wed, 30 Sep 2015 11:29:34 +0200 Subject: [PATCH 22/36] Fix path issue in test_topo_json --- tests/test_folium.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_folium.py b/tests/test_folium.py index fcb6269cc3..c868d1fece 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -459,11 +459,9 @@ def test_topo_json(self): self.map = folium.Map([43, -100], zoom_start=4) - path = os.path.join(rootpath, 'us-counties.json') - # Adding TopoJSON as additional layer. - path_2 = 'or_counties_topo.json' - self.map.geo_json(geo_path=path_2, topojson='objects.or_counties_geo') + path = os.path.join(rootpath, 'or_counties_topo.json') + self.map.geo_json(geo_path=path, topojson='objects.or_counties_geo') out = self.map._parent.render() From 0d2eece7fbbc0d5486b54d618f5aef947745d8fa Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Wed, 30 Sep 2015 12:15:35 +0200 Subject: [PATCH 23/36] Fix test_multi_polyline --- folium/features.py | 50 +++++++++++++---- folium/folium.py | 88 +++++++++++++++++++----------- folium/templates/multi_polyline.js | 12 +--- folium/utilities.py | 20 +++++++ tests/test_folium.py | 17 ++++-- 5 files changed, 127 insertions(+), 60 deletions(-) diff --git a/folium/features.py b/folium/features.py index 315639fa7e..527c1d9d51 100644 --- a/folium/features.py +++ b/folium/features.py @@ -8,7 +8,7 @@ from jinja2 import Template import json -from .utilities import color_brewer, _parse_size, legend_scaler +from .utilities import color_brewer, _parse_size, legend_scaler, _locations_mirror, _locations_tolist from .element import Element, Figure, JavascriptLink, CssLink, Div, MacroElement from .map import Map, TileLayer, Icon, Marker, Popup @@ -479,20 +479,10 @@ def __init__(self, locations, color=None, weight=None, opacity=None, latlon=True Whether locations are given in the form [[lat,lon]] or not ([[lon,lat]] if False). Note that the default GeoJson format is latlon=False, while Leaflet polyline's default is latlon=True. - - examples : - # providing file - GeoJson(open('foo.json')) - - # providing dict - GeoJson(json.load(open('foo.json'))) - - # providing string - GeoJson(open('foo.json').read()) """ super(PolyLine, self).__init__() self._name = 'PolyLine' - self.data = [[x[1],x[0]] for x in locations] if not latlon else locations + self.data = _locations_mirror(locations) if not latlon else _locations_tolist(locations) self.color = color self.weight = weight self.opacity = opacity @@ -509,3 +499,39 @@ def __init__(self, locations, color=None, weight=None, opacity=None, latlon=True {{this._parent.get_name()}}.addLayer({{this.get_name()}}); {% endmacro %} """) + +class MultiPolyLine(MacroElement): + def __init__(self, locations, color=None, weight=None, opacity=None, latlon=True): + """Creates a MultiPolyLine object to append into a map with Map.add_children. + + Parameters + ---------- + locations: list of points (latitude, longitude) + Latitude and Longitude of line (Northing, Easting) + color: string, default Leaflet's default ('#03f') + weight: float, default Leaflet's default (5) + opacity: float, default Leaflet's default (0.5) + latlon: bool, default True + Whether locations are given in the form [[lat,lon]] or not ([[lon,lat]] if False). + Note that the default GeoJson format is latlon=False, + while Leaflet polyline's default is latlon=True. + """ + super(MultiPolyLine, self).__init__() + self._name = 'MultiPolyLine' + self.data = _locations_mirror(locations) if not latlon else _locations_tolist(locations) + self.color = color + self.weight = weight + self.opacity = opacity + + self._template = Template(u""" + {% macro script(this, kwargs) %} + var {{this.get_name()}} = L.multiPolyline( + {{this.data}}, + { + {% if this.color != None %}color: '{{ this.color }}',{% endif %} + {% if this.weight != None %}weight: {{ this.weight }},{% endif %} + {% if this.opacity != None %}opacity: {{ this.opacity }},{% endif %} + }); + {{this._parent.get_name()}}.addLayer({{this.get_name()}}); + {% endmacro %} + """) diff --git a/folium/folium.py b/folium/folium.py index b4d625b0b7..c1a9fe456d 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -26,7 +26,7 @@ from .element import Element, Figure, JavascriptLink, CssLink, Div, MacroElement from .map import Map, TileLayer, Icon, Marker, Popup from .features import WmsTileLayer, RegularPolygonMarker, Vega, GeoJson, GeoJsonStyle, MarkerCluster, DivIcon,\ - CircleMarker, LatLngPopup, ClickForMarker, ColorScale, TopoJson, PolyLine + CircleMarker, LatLngPopup, ClickForMarker, ColorScale, TopoJson, PolyLine, MultiPolyLine from .utilities import color_brewer #import sys #import base64 @@ -423,38 +423,60 @@ def line(self, locations, p.add_children(Popup(popup, max_width=popup_width)) self.add_children(p) -# @iter_obj('multiline') -# def multiline(self, locations, line_color=None, line_opacity=None, -# line_weight=None): -# """Add a multiPolyline to the map with optional styles. -# -# A multiPolyline is single layer that consists of several polylines that -# share styling/popup. -# -# Parameters -# ---------- -# locations: list of lists of points (latitude, longitude) -# Latitude and Longitude of line (Northing, Easting) -# line_color: string, default Leaflet's default ('#03f') -# line_opacity: float, default Leaflet's default (0.5) -# line_weight: float, default Leaflet's default (5) -# -# Note: If the optional styles are omitted, they will not be included -# in the HTML output and will obtain the Leaflet defaults listed above. -# -# Example -# ------- -# # FIXME: Add another example. -# >>> m.multiline(locations=[[(45.5236, -122.675), (45.5236, -122.675)], -# [(45.5237, -122.675), (45.5237, -122.675)], -# [(45.5238, -122.675), (45.5238, -122.675)]]) -# >>> m.multiline(locations=[[(45.5236, -122.675), (45.5236, -122.675)], -# [(45.5237, -122.675), (45.5237, -122.675)], -# [(45.5238, -122.675), (45.5238, -122.675)]], -# line_color='red', line_weight=2, -# line_opacity=1.0) -# """ -# + + def multiline(self, locations, line_color=None, line_opacity=None, + line_weight=None, + popup=None, popup_width=300, latlon=True): + """Add a multiPolyline to the map with optional styles. + + A multiPolyline is single layer that consists of several polylines that + share styling/popup. + + Parameters + ---------- + locations: list of lists of points (latitude, longitude) + Latitude and Longitude of line (Northing, Easting) + line_color: string, default Leaflet's default ('#03f') + line_opacity: float, default Leaflet's default (0.5) + line_weight: float, default Leaflet's default (5) + popup: string or tuple, default 'Pop Text' + Input text or visualization for object. Can pass either text, + or a tuple of the form (Vincent object, 'vis_path.json') + It is possible to adjust the width of text/HTML popups + using the optional keywords `popup_width` (default is 300px). + latlon: bool, default True + Whether locations are given in the form [[lat,lon]] or not ([[lon,lat]] if False). + Note that the default GeoJson format is latlon=False, + while Leaflet polyline's default is latlon=True. + + Note: If the optional styles are omitted, they will not be included + in the HTML output and will obtain the Leaflet defaults listed above. + + Example + ------- + # FIXME: Add another example. + >>> m.multiline(locations=[[(45.5236, -122.675), (45.5236, -122.675)], + [(45.5237, -122.675), (45.5237, -122.675)], + [(45.5238, -122.675), (45.5238, -122.675)]]) + >>> m.multiline(locations=[[(45.5236, -122.675), (45.5236, -122.675)], + [(45.5237, -122.675), (45.5237, -122.675)], + [(45.5238, -122.675), (45.5238, -122.675)]], + line_color='red', line_weight=2, + line_opacity=1.0) + """ + + p = MultiPolyLine(locations, + color=line_color, + weight=line_weight, + opacity=line_opacity, + latlon=latlon, + ) + + if popup is not None: + p.add_children(Popup(popup, max_width=popup_width)) + + self.add_children(p) + # count = self.mark_cnt['multiline'] # # multiline_temp = self.env.get_template('multi_polyline.js') diff --git a/folium/templates/multi_polyline.js b/folium/templates/multi_polyline.js index 59453b03b2..dc32ec70c0 100644 --- a/folium/templates/multi_polyline.js +++ b/folium/templates/multi_polyline.js @@ -1,13 +1,5 @@ -var latLngs = [ -{% for location in locations %} - [ - {% for loc in location %} - [{{ loc[0] }}, {{ loc[1] }}], - {% endfor %} - ], -{% endfor %}]; - -var {{ multiline }} = L.multiPolyline(latLngs,{ +var {{ this.get_name() }} = L.multiPolyline({{locations}}, + { {% if options.color != None %}color: '{{ options.color }}',{% endif %} {% if options.weight != None %}weight: {{ options.weight }},{% endif %} {% if options.opacity != None %}opacity: {{ options.opacity }},{% endif %} diff --git a/folium/utilities.py b/folium/utilities.py index 957c3a525a..a05dff657a 100644 --- a/folium/utilities.py +++ b/folium/utilities.py @@ -408,3 +408,23 @@ def _parse_size(value): msg = "Cannot parse value {!r} as {!r}".format raise ValueError(msg(value, value_type)) return value, value_type + +def _locations_mirror(x): + """Mirrors the points in a list-of-list-of-...-of-list-of-points. + For example _locations_mirror([[[1,2],[3,4]],[5,6],[7,8]]) = [[[2, 1], [4, 3]], [6, 5], [8, 7]] + """ + if hasattr(x, '__iter__'): + if hasattr(x[0], '__iter__'): + return list(map(_locations_mirror,x)) + else: + return list(x[::-1]) + else: + return x + +def _locations_tolist(x): + """Transforms recusively a list of iterables into a list of list. + """ + if hasattr(x, '__iter__'): + return list(map(_locations_tolist,x)) + else: + return x \ No newline at end of file diff --git a/tests/test_folium.py b/tests/test_folium.py index c868d1fece..11249742cf 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -20,7 +20,7 @@ from folium.element import Html from folium.map import Popup, Marker, Icon from folium.features import DivIcon, CircleMarker, LatLngPopup, GeoJson,\ - GeoJsonStyle, ColorScale, TopoJson, PolyLine + GeoJsonStyle, ColorScale, TopoJson, PolyLine, MultiPolyLine rootpath = os.path.abspath(os.path.dirname(__file__)) @@ -586,15 +586,22 @@ def test_multi_polyline(self): locations = [[[45.5236, -122.6750], [45.5236, -122.6751]], [[45.5237, -122.6750], [45.5237, -122.6751]], [[45.5238, -122.6750], [45.5238, -122.6751]]] - multiline_rendered = multiline_temp.render({'multiline': 'multiline_1', - 'locations': locations, - 'options': multiline_opts}) + self.setup() self.map.multiline(locations=locations, line_color=multiline_opts['color'], line_weight=multiline_opts['weight'], line_opacity=multiline_opts['opacity']) - assert self.map.template_vars['multilines'][0][0] == multiline_rendered + multipolyline = [val for key,val in self.map._children.items()\ + if isinstance(val,MultiPolyLine)][0] + out = self.map._parent.render() + + multiline_rendered = multiline_temp.render({'multiline': 'multiline_1', + 'this' : multipolyline, + 'locations': locations, + 'options': multiline_opts}) + + assert ''.join(multiline_rendered.split()) in ''.join(out.split()) def test_fit_bounds(self): """Test fit_bounds.""" From 4c1515dac9190afc11c4f1178e772c6eee723a37 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Wed, 30 Sep 2015 16:59:09 +0200 Subject: [PATCH 24/36] Fix test_fit_bounds --- folium/folium.py | 95 ++++++++++++---------------------- folium/map.py | 48 +++++++++++++++++ folium/templates/fit_bounds.js | 2 +- tests/test_folium.py | 24 ++++++--- 4 files changed, 100 insertions(+), 69 deletions(-) diff --git a/folium/folium.py b/folium/folium.py index c1a9fe456d..c739e02d24 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -24,7 +24,7 @@ from folium.six import text_type, binary_type#, iteritems from .map import Map as _Map from .element import Element, Figure, JavascriptLink, CssLink, Div, MacroElement -from .map import Map, TileLayer, Icon, Marker, Popup +from .map import Map, TileLayer, Icon, Marker, Popup, FitBounds from .features import WmsTileLayer, RegularPolygonMarker, Vega, GeoJson, GeoJsonStyle, MarkerCluster, DivIcon,\ CircleMarker, LatLngPopup, ClickForMarker, ColorScale, TopoJson, PolyLine, MultiPolyLine from .utilities import color_brewer @@ -477,22 +477,6 @@ def multiline(self, locations, line_color=None, line_opacity=None, self.add_children(p) -# count = self.mark_cnt['multiline'] -# -# multiline_temp = self.env.get_template('multi_polyline.js') -# -# multiline_opts = {'color': line_color, 'weight': line_weight, -# 'opacity': line_opacity} -# -# varname = 'multiline_{}'.format(count) -# multiline_rendered = multiline_temp.render({'multiline': varname, -# 'locations': locations, -# 'options': multiline_opts}) -# -# add_multiline = 'map.addLayer({});'.format(varname) -# append = (multiline_rendered, add_multiline) -# self.template_vars.setdefault('multilines', []).append(append) -# # @iter_obj('circle') def circle_marker(self, location=None, radius=500, popup=None, line_color='black', fill_color='black', @@ -632,50 +616,39 @@ def click_for_marker(self, popup=None): FutureWarning, stacklevel=2) self.add_children(ClickForMarker(popup=popup)) -# def fit_bounds(self, bounds, padding_top_left=None, -# padding_bottom_right=None, padding=None, max_zoom=None): -# """Fit the map to contain a bounding box with the maximum zoom level possible. -# -# Parameters -# ---------- -# bounds: list of (latitude, longitude) points -# Bounding box specified as two points [southwest, northeast] -# padding_top_left: (x, y) point, default None -# Padding in the top left corner. Useful if some elements in -# the corner, such as controls, might obscure objects you're zooming -# to. -# padding_bottom_right: (x, y) point, default None -# Padding in the bottom right corner. -# padding: (x, y) point, default None -# Equivalent to setting both top left and bottom right padding to -# the same value. -# max_zoom: int, default None -# Maximum zoom to be used. -# -# Example -# ------- -# >>> map.fit_bounds([[52.193636, -2.221575], [52.636878, -1.139759]]) -# -# """ -# options = { -# 'paddingTopLeft': padding_top_left, -# 'paddingBottomRight': padding_bottom_right, -# 'padding': padding, -# 'maxZoom': max_zoom, -# } -# fit_bounds_options = {} -# for key, opt in options.items(): -# if opt: -# fit_bounds_options[key] = opt -# fit_bounds = self.env.get_template('fit_bounds.js') -# fit_bounds_str = fit_bounds.render({ -# 'bounds': json.dumps(bounds), -# 'fit_bounds_options': json.dumps(fit_bounds_options, -# sort_keys=True), -# }) -# -# self.template_vars.update({'fit_bounds': fit_bounds_str}) -# + def fit_bounds(self, bounds, padding_top_left=None, + padding_bottom_right=None, padding=None, max_zoom=None): + """Fit the map to contain a bounding box with the maximum zoom level possible. + + Parameters + ---------- + bounds: list of (latitude, longitude) points + Bounding box specified as two points [southwest, northeast] + padding_top_left: (x, y) point, default None + Padding in the top left corner. Useful if some elements in + the corner, such as controls, might obscure objects you're zooming + to. + padding_bottom_right: (x, y) point, default None + Padding in the bottom right corner. + padding: (x, y) point, default None + Equivalent to setting both top left and bottom right padding to + the same value. + max_zoom: int, default None + Maximum zoom to be used. + + Example + ------- + >>> map.fit_bounds([[52.193636, -2.221575], [52.636878, -1.139759]]) + + """ + self.add_children(FitBounds(bounds, + padding_top_left=padding_top_left, + padding_bottom_right=padding_bottom_right, + padding=padding, + max_zoom=max_zoom, + ) + ) + def add_plugin(self, plugin): """Adds a plugin to the map. diff --git a/folium/map.py b/folium/map.py index e4465e5d77..aada559b2f 100644 --- a/folium/map.py +++ b/folium/map.py @@ -6,6 +6,7 @@ Classes for drawing maps. """ import warnings +import json from jinja2 import Template @@ -333,3 +334,50 @@ def render(self, **kwargs): figure.script.add_children(Element(\ self._template.render(this=self, kwargs=kwargs)), name=self.get_name()) + +class FitBounds(MacroElement): + def __init__(self, bounds, padding_top_left=None, + padding_bottom_right=None, padding=None, max_zoom=None): + """Fit the map to contain a bounding box with the maximum zoom level possible. + + Parameters + ---------- + bounds: list of (latitude, longitude) points + Bounding box specified as two points [southwest, northeast] + padding_top_left: (x, y) point, default None + Padding in the top left corner. Useful if some elements in + the corner, such as controls, might obscure objects you're zooming + to. + padding_bottom_right: (x, y) point, default None + Padding in the bottom right corner. + padding: (x, y) point, default None + Equivalent to setting both top left and bottom right padding to + the same value. + max_zoom: int, default None + Maximum zoom to be used. + + """ + super(FitBounds, self).__init__() + self._name = 'FitBounds' + self.bounds = json.loads(json.dumps(bounds)) + options = { + 'maxZoom': max_zoom, + 'paddingTopLeft': padding_top_left, + 'paddingBottomRight': padding_bottom_right, + 'padding': padding, + } + self.fit_bounds_options = json.dumps({key:val for key,val in options.items() if val}, + sort_keys=True) + + self._template = Template(u""" + {% macro script(this, kwargs) %} + {% if this.autobounds %} + var autobounds = L.featureGroup({{ this.features }}).getBounds() + {% endif %} + + {{this._parent.get_name()}}.fitBounds( + {% if this.bounds %}{{ this.bounds }}{% else %}"autobounds"{% endif %}, + {{ this.fit_bounds_options }} + ); + {% endmacro %} + """) diff --git a/folium/templates/fit_bounds.js b/folium/templates/fit_bounds.js index f9d7430fe1..70429698bf 100644 --- a/folium/templates/fit_bounds.js +++ b/folium/templates/fit_bounds.js @@ -5,7 +5,7 @@ var autobounds = L.featureGroup({{ features }}).getBounds() {% endif %} {% endif %} {% if bounds %} -map.fitBounds({{ bounds }}, +{{this._parent.get_name()}}.fitBounds({{ bounds }}, {{ fit_bounds_options }} ); {% endif %} diff --git a/tests/test_folium.py b/tests/test_folium.py index 11249742cf..dcc30691d5 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -18,7 +18,7 @@ from folium.six import PY3 from folium.plugins import ScrollZoomToggler, MarkerCluster from folium.element import Html -from folium.map import Popup, Marker, Icon +from folium.map import Popup, Marker, Icon, FitBounds from folium.features import DivIcon, CircleMarker, LatLngPopup, GeoJson,\ GeoJsonStyle, ColorScale, TopoJson, PolyLine, MultiPolyLine @@ -606,13 +606,24 @@ def test_multi_polyline(self): def test_fit_bounds(self): """Test fit_bounds.""" bounds = ((52.193636, -2.221575), (52.636878, -1.139759)) + + self.setup() + self.map.fit_bounds(bounds) + fitbounds = [val for key,val in self.map._children.items() if isinstance(val,FitBounds)][0] + out = self.map._parent.render() + fit_bounds_tpl = self.env.get_template('fit_bounds.js') fit_bounds_rendered = fit_bounds_tpl.render({ 'bounds': json.dumps(bounds), + 'this' : fitbounds, 'fit_bounds_options': {}, }) - self.map.fit_bounds(bounds) - assert self.map.template_vars['fit_bounds'] == fit_bounds_rendered + assert ''.join(fit_bounds_rendered.split()) in ''.join(out.split()) + + self.setup() + self.map.fit_bounds(bounds, max_zoom=15, padding=(3, 3)) + fitbounds = [val for key,val in self.map._children.items() if isinstance(val,FitBounds)][0] + out = self.map._parent.render() fit_bounds_tpl = self.env.get_template('fit_bounds.js') fit_bounds_rendered = fit_bounds_tpl.render({ @@ -620,11 +631,10 @@ def test_fit_bounds(self): 'fit_bounds_options': json.dumps({'maxZoom': 15, 'padding': (3, 3), }, sort_keys=True), - }) - - self.map.fit_bounds(bounds, max_zoom=15, padding=(3, 3)) + 'this' : fitbounds, + }) - assert self.map.template_vars['fit_bounds'] == fit_bounds_rendered + assert ''.join(fit_bounds_rendered.split()) in ''.join(out.split()) def test_image_overlay(self): """Test image overlay.""" From c3ede82e6e8786ce01fd03fdce4816401c41964d Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Thu, 1 Oct 2015 18:42:23 +0200 Subject: [PATCH 25/36] Fix test_image_overlay --- folium/features.py | 74 +++++++++++++- folium/folium.py | 168 +++++++++++++++----------------- folium/templates/image_layer.js | 6 +- folium/utilities.py | 146 +++++++++++++++++++-------- tests/test_folium.py | 39 +++++--- 5 files changed, 283 insertions(+), 150 deletions(-) diff --git a/folium/features.py b/folium/features.py index 527c1d9d51..1304d76f2f 100644 --- a/folium/features.py +++ b/folium/features.py @@ -8,7 +8,8 @@ from jinja2 import Template import json -from .utilities import color_brewer, _parse_size, legend_scaler, _locations_mirror, _locations_tolist +from .utilities import color_brewer, _parse_size, legend_scaler, _locations_mirror, _locations_tolist, write_png,\ + mercator_transform from .element import Element, Figure, JavascriptLink, CssLink, Div, MacroElement from .map import Map, TileLayer, Icon, Marker, Popup @@ -535,3 +536,74 @@ def __init__(self, locations, color=None, weight=None, opacity=None, latlon=True {{this._parent.get_name()}}.addLayer({{this.get_name()}}); {% endmacro %} """) + +class ImageOverlay(MacroElement): + def __init__(self, image, bounds, opacity=1., attribution=None, origin='upper', colormap=None, mercator_project=False): + """Used to load and display a single image over specific bounds of the map, implements ILayer interface + + Parameters + ---------- + image: string, file or array-like object + The data you want to draw on the map. + * 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. + bounds: list + Image bounds on the map in the form [[lat_min, lon_min], [lat_max, lon_max]] + opacity: float, default Leaflet's default (1.0) + attr: string, default Leaflet's default ("") + 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, + image.read().encode('base64')) + elif 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," +write_png(data, origin=origin, colormap=colormap).encode('base64') + else: + # We got an url + self.url = json.loads(json.dumps(image)) + + self.url = self.url.replace('\n',' ') + self.bounds = json.loads(json.dumps(bounds)) + options = { + 'opacity': opacity, + 'attribution': attribution, + } + self.options = json.dumps({key:val for key,val in options.items() if val}, + sort_keys=True) + self._template = Template(u""" + {% macro script(this, kwargs) %} + var {{this.get_name()}} = L.imageOverlay( + '{{ this.url }}', + {{ this.bounds }}, + {{ this.options }} + ).addTo({{this._parent.get_name()}}); + {% endmacro %} + """) diff --git a/folium/folium.py b/folium/folium.py index c739e02d24..7d0fef8925 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -26,8 +26,8 @@ from .element import Element, Figure, JavascriptLink, CssLink, Div, MacroElement from .map import Map, TileLayer, Icon, Marker, Popup, FitBounds from .features import WmsTileLayer, RegularPolygonMarker, Vega, GeoJson, GeoJsonStyle, MarkerCluster, DivIcon,\ - CircleMarker, LatLngPopup, ClickForMarker, ColorScale, TopoJson, PolyLine, MultiPolyLine -from .utilities import color_brewer + CircleMarker, LatLngPopup, ClickForMarker, ColorScale, TopoJson, PolyLine, MultiPolyLine, ImageOverlay +from .utilities import color_brewer, write_png #import sys #import base64 @@ -907,96 +907,80 @@ def geo_json(self, geo_path=None, geo_str=None, data_out='data.json', self.add_children(geo_json) self.add_children(color_scale) -# @iter_obj('image_overlay') -# def image_overlay(self, data, opacity=0.25, min_lat=-90.0, max_lat=90.0, -# min_lon=-180.0, max_lon=180.0, image_name=None, -# filename=None): -# """ -# Simple image overlay of raster data from a numpy array. This is a -# lightweight way to overlay geospatial data on top of a map. If your -# data is high res, consider implementing a WMS server and adding a WMS -# layer. -# -# This function works by generating a PNG file from a numpy array. If -# you do not specify a filename, it will embed the image inline. -# Otherwise, it saves the file in the current directory, and then adds -# it as an image overlay layer in leaflet.js. By default, the image is -# placed and stretched using bounds that cover the entire globe. -# -# Parameters -# ---------- -# data: numpy array OR url string, required. -# if numpy array, must be a image format, -# i.e., NxM (mono), NxMx3 (rgb), or NxMx4 (rgba) -# if url, must be a valid url to a image (local or external) -# opacity: float, default 0.25 -# Image layer opacity in range 0 (transparent) to 1 (opaque) -# min_lat: float, default -90.0 -# max_lat: float, default 90.0 -# min_lon: float, default -180.0 -# max_lon: float, default 180.0 -# image_name: string, default None -# The name of the layer object in leaflet.js -# filename: string, default None -# Optional file name of output.png for image overlay. -# Use `None` for inline PNG. -# -# Output -# ------ -# Image overlay data layer in obj.template_vars -# -# Examples -# ------- -# # assumes a map object `m` has been created -# >>> import numpy as np -# >>> data = np.random.random((100,100)) -# -# # to make a rgba from a specific matplotlib colormap: -# >>> import matplotlib.cm as cm -# >>> cmapper = cm.cm.ColorMapper('jet') -# >>> data2 = cmapper.to_rgba(np.random.random((100,100))) -# >>> # Place the data over all of the globe (will be pretty pixelated!) -# >>> m.image_overlay(data) -# >>> # Put it only over a single city (Paris). -# >>> m.image_overlay(data, min_lat=48.80418, max_lat=48.90970, -# ... min_lon=2.25214, max_lon=2.44731) -# -# """ -# -# if isinstance(data, str): -# filename = data -# else: -# try: -# png_str = utilities.write_png(data) -# except Exception as e: -# raise e -# -# if filename is not None: -# with open(filename, 'wb') as fd: -# fd.write(png_str) -# else: -# png = "data:image/png;base64,{}".format -# filename = png(base64.b64encode(png_str).decode('utf-8')) -# -# if image_name not in self.added_layers: -# if image_name is None: -# image_name = "Image_Overlay" -# else: -# image_name = image_name.replace(" ", "_") -# image_url = filename -# image_bounds = [[min_lat, min_lon], [max_lat, max_lon]] -# image_opacity = opacity -# -# image_temp = self.env.get_template('image_layer.js') -# -# image = image_temp.render({'image_name': image_name, -# 'image_url': image_url, -# 'image_bounds': image_bounds, -# 'image_opacity': image_opacity}) -# -# self.template_vars['image_layers'].append(image) -# self.added_layers.append(image_name) -# + def image_overlay(self, data, opacity=0.25, min_lat=-90.0, max_lat=90.0, + min_lon=-180.0, max_lon=180.0, origin='upper', colormap=None, + image_name=None, filename=None, mercator_project=False): + """ + Simple image overlay of raster data from a numpy array. This is a + lightweight way to overlay geospatial data on top of a map. If your + data is high res, consider implementing a WMS server and adding a WMS + layer. + + This function works by generating a PNG file from a numpy array. If + you do not specify a filename, it will embed the image inline. + Otherwise, it saves the file in the current directory, and then adds + it as an image overlay layer in leaflet.js. By default, the image is + placed and stretched using bounds that cover the entire globe. + + Parameters + ---------- + data: numpy array OR url string, required. + if numpy array, must be a image format, + i.e., NxM (mono), NxMx3 (rgb), or NxMx4 (rgba) + if url, must be a valid url to a image (local or external) + opacity: float, default 0.25 + Image layer opacity in range 0 (transparent) to 1 (opaque) + min_lat: float, default -90.0 + max_lat: float, default 90.0 + min_lon: float, default -180.0 + max_lon: float, default 180.0 + image_name: string, default None + The name of the layer object in leaflet.js + filename: string, default None + Optional file name of output.png for image overlay. + Use `None` for inline PNG. + 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. + Output + ------ + Image overlay data layer in obj.template_vars + + Examples + ------- + # assumes a map object `m` has been created + >>> import numpy as np + >>> data = np.random.random((100,100)) + + # to make a rgba from a specific matplotlib colormap: + >>> import matplotlib.cm as cm + >>> cmapper = cm.cm.ColorMapper('jet') + >>> data2 = cmapper.to_rgba(np.random.random((100,100))) + >>> # Place the data over all of the globe (will be pretty pixelated!) + >>> m.image_overlay(data) + >>> # Put it only over a single city (Paris). + >>> m.image_overlay(data, min_lat=48.80418, max_lat=48.90970, + ... min_lon=2.25214, max_lon=2.44731) + + """ + if filename: + image = write_png(data, origin=origin, colormap=colormap) + open(filename,'wb').write(image) + data = filename + + self.add_children(ImageOverlay(data, [[min_lat, min_lon],[max_lat,max_lon]], + opacity=opacity, origin=origin, colormap=colormap, + mercator_project=mercator_project)) + # def _build_map(self, html_templ=None, templ_type='string'): # self._auto_bounds() # """Build HTML/JS/CSS from Templates given current map type.""" diff --git a/folium/templates/image_layer.js b/folium/templates/image_layer.js index c4082fe6f7..c8cfd64a51 100644 --- a/folium/templates/image_layer.js +++ b/folium/templates/image_layer.js @@ -1 +1,5 @@ -var {{ image_name }} = L.imageOverlay('{{ image_url }}', {{ image_bounds }}).addTo(map).setOpacity({{ image_opacity }}); +var {{ this.get_name() }} = L.imageOverlay( + '{{ image_url }}', + {{ image_bounds }} + {% if image_opacity %}, {"opacity" : {{ image_opacity }} } {% endif %} + ).addTo({{ this._parent.get_name() }}); diff --git a/folium/utilities.py b/folium/utilities.py index a05dff657a..35613627da 100644 --- a/folium/utilities.py +++ b/folium/utilities.py @@ -311,77 +311,137 @@ def base(x): arr = series.values return [base(np.percentile(arr, x)) for x in quants] +def mercator_transform(data, lat_bounds, origin='upper', height_out=None): + """Transforms an image computed in (longitude,latitude) coordinates into + the a Mercator projection image. -def write_png(array): + Parameters + ---------- + + data: numpy array or equivalent list-like object. + Must be NxM (mono), NxMx3 (RGB) or NxMx4 (RGBA) + + lat_bounds : length 2 tuple + Minimal and maximal value of the latitude of the image. + + 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. + + height_out : int, default None + The expected height of the output. + If None, the height of the input is used. """ - Format a numpy array as a PNG byte string. + if np is None: + raise ImportError("The NumPy package is required" + " for this functionality") + + mercator = lambda x: np.arcsinh(np.tan(x*np.pi/180.))*180./np.pi + + array = np.atleast_3d(data).copy() + height, width, nblayers = array.shape + + lat_min,lat_max = lat_bounds + if height_out is None: + height_out = height + + # Eventually flip the image + if origin=='upper': + array = array[::-1,:,:] + + lats = lat_min + np.linspace(0.5/height,1.-0.5/height, height)*(lat_max-lat_min) + latslats = mercator(lat_min) + np.linspace(0.5/height_out,1.-0.5/height_out, height_out)*(mercator(lat_max)-mercator(lat_min)) + + out = np.zeros((height_out,width,nblayers)) + for i in range(width): + for j in range(4): + out[:,i,j] = np.interp(latslats,mercator(lats), array[:,i,j]) + + # Eventually flip the image + if origin=='upper': + out = out[::-1,:,:] + + return out + +def write_png(data, origin='upper', colormap=None): + """ + Tranform an array of data into a PNG string. This can be writen to disk using binary I/O, or encoded using base64 for an inline png like this: >>> png_str = write_png(array) - >>> "data:image/png;base64,"+base64.b64encode(png_str) + >>> "data:image/png;base64,"+png_str.encode('base64') - Taken from + Inspired from http://stackoverflow.com/questions/902761/saving-a-numpy-array-as-an-image Parameters ---------- - array: numpy array + data: numpy array or equivalent list-like object. Must be NxM (mono), NxMx3 (RGB) or NxMx4 (RGBA) + 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`. + Returns ------- PNG formatted byte string - """ if np is None: raise ImportError("The NumPy package is required" " for this functionality") - array = np.atleast_3d(array) - if array.shape[2] not in [1, 3, 4]: - raise ValueError("Data must be NxM (mono), " - "NxMx3 (RGB), or NxMx4 (RGBA)") - - # Have to broadcast up into a full RGBA array. - array_full = np.empty((array.shape[0], array.shape[1], 4)) - # NxM -> NxMx4. - if array.shape[2] == 1: - array_full[:, :, 0] = array[:, :, 0] - array_full[:, :, 1] = array[:, :, 0] - array_full[:, :, 2] = array[:, :, 0] - array_full[:, :, 3] = 1 - # NxMx3 -> NxMx4. - elif array.shape[2] == 3: - array_full[:, :, 0] = array[:, :, 0] - array_full[:, :, 1] = array[:, :, 1] - array_full[:, :, 2] = array[:, :, 2] - array_full[:, :, 3] = 1 - # NxMx4 -> keep. - else: - array_full = array + if colormap is None: + colormap = lambda x: (x,x,x,1) + + array = np.atleast_3d(data) + height, width, nblayers = array.shape + + if nblayers not in [1, 3, 4]: + raise ValueError("Data must be NxM (mono), " + "NxMx3 (RGB), or NxMx4 (RGBA)") + assert array.shape == (height,width,nblayers) + + if nblayers==1: + array = np.array(map(colormap,array.ravel())) + nblayers = array.shape[1] + if nblayers not in [3,4]: + raise ValueError("colormap must provide colors of" + "length 3 (RGB) or 4 (RGBA)") + array = array.reshape((height,width,nblayers)) + assert array.shape == (height,width,nblayers) + + if nblayers==3: + array = np.concatenate((array, np.ones((height,width,1))), axis=2) + nblayers = 4 + assert array.shape == (height,width,nblayers) + assert nblayers == 4 # Normalize to uint8 if it isn't already. - if array_full.dtype != 'uint8': - for component in range(4): - frame = array_full[:, :, component] - array_full[:, :, component] = (frame / frame.max() * 255) - array_full = array_full.astype('uint8') - width, height = array_full.shape[:2] + if array.dtype != 'uint8': + array = (array *255./array.max(axis=(0,1)).reshape((1,1,4)))\ + .astype('uint8') - array_full = array_full.tobytes() + # Eventually flip the image + if origin=='lower': + array = array[::-1,:,:] - # Reverse the vertical line order and add null bytes at the start. - width_byte_4 = width * 4 - raw_data = b''.join(b'\x00' + array_full[span:span + width_byte_4] for - span in range((height-1) * width*4, -1, -width_byte_4)) + # Transform the array to bytes + raw_data = b''.join([b'\x00' + array[i,:,:].tobytes() for i in range(height)]) def png_pack(png_tag, data): - chunk_head = png_tag + data - return (struct.pack("!I", len(data)) + - chunk_head + - struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))) + chunk_head = png_tag + data + return (struct.pack("!I", len(data)) + + chunk_head + + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))) return b''.join([ b'\x89PNG\r\n\x1a\n', diff --git a/tests/test_folium.py b/tests/test_folium.py index dcc30691d5..f93fa091da 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -20,7 +20,7 @@ from folium.element import Html from folium.map import Popup, Marker, Icon, FitBounds from folium.features import DivIcon, CircleMarker, LatLngPopup, GeoJson,\ - GeoJsonStyle, ColorScale, TopoJson, PolyLine, MultiPolyLine + GeoJsonStyle, ColorScale, TopoJson, PolyLine, MultiPolyLine, ImageOverlay rootpath = os.path.abspath(os.path.dirname(__file__)) @@ -638,39 +638,52 @@ def test_fit_bounds(self): def test_image_overlay(self): """Test image overlay.""" - from numpy.random import random + #from numpy.random import random from folium.utilities import write_png - import base64 + #import base64 + + data = [[[1,0,0,1],[0,0,0,0],[0,0,0,0]],[[1,1,0,0.5],[0,0,1,1],[0,0,1,1]]] + min_lon, max_lon, min_lat, max_lat = -90.0, 90.0, -180.0, 180.0 + + self.setup() + image_url = 'data.png' + self.map.image_overlay(data, filename=image_url) + out = self.map._parent.render() + + imageoverlay = [val for key,val in self.map._children.items() if isinstance(val,ImageOverlay)][0] - data = random((100, 100)) png_str = write_png(data) - with open('data.png', 'wb') as f: - f.write(png_str) + #with open('data.png', 'wb') as f: + # f.write(png_str) png = "data:image/png;base64,{}".format - inline_image_url = png(base64.b64encode(png_str).decode('utf-8')) + inline_image_url = png(png_str.encode('base64').decode('utf-8')) image_tpl = self.env.get_template('image_layer.js') image_name = 'Image_Overlay' image_opacity = 0.25 - image_url = 'data.png' - min_lon, max_lon, min_lat, max_lat = -90.0, 90.0, -180.0, 180.0 image_bounds = [[min_lon, min_lat], [max_lon, max_lat]] image_rendered = image_tpl.render({'image_name': image_name, + 'this':imageoverlay, 'image_url': image_url, 'image_bounds': image_bounds, 'image_opacity': image_opacity}) - self.map.image_overlay(data, filename=image_url) - assert image_rendered in self.map.template_vars['image_layers'] + assert ''.join(image_rendered.split()) in ''.join(out.split()) + + self.setup() + self.map.image_overlay(data) + out = self.map._parent.render() + + imageoverlay = [val for key,val in self.map._children.items() if isinstance(val,ImageOverlay)][0] image_rendered = image_tpl.render({'image_name': image_name, + 'this':imageoverlay, 'image_url': inline_image_url, 'image_bounds': image_bounds, 'image_opacity': image_opacity}) - self.map.image_overlay(data) - assert image_rendered in self.map.template_vars['image_layers'] + assert ''.join(image_rendered.split()) in ''.join(out.split()) def test_scroll_zoom_toggler_plugin(self): """Test ScrollZoomToggler plugin.""" From 14b367106165d12e2fb775703f6e63f7c01601da Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Thu, 1 Oct 2015 23:35:31 +0200 Subject: [PATCH 26/36] Fix python3 stuff in test_image_overlay --- folium/features.py | 9 ++++++--- folium/utilities.py | 2 +- tests/test_folium.py | 3 ++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/folium/features.py b/folium/features.py index 1304d76f2f..830c771fb8 100644 --- a/folium/features.py +++ b/folium/features.py @@ -7,9 +7,11 @@ """ 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 +from .six import text_type, binary_type from .element import Element, Figure, JavascriptLink, CssLink, Div, MacroElement from .map import Map, TileLayer, Icon, Marker, Popup @@ -576,8 +578,8 @@ def __init__(self, image, bounds, opacity=1., attribution=None, origin='upper', else: fileformat = 'png' self.url = "data:image/{};base64,{}".format(fileformat, - image.read().encode('base64')) - elif hasattr(image,'__iter__'): + 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, @@ -585,7 +587,8 @@ def __init__(self, image, bounds, opacity=1., attribution=None, origin='upper', origin=origin) else: data = image - self.url = "data:image/png;base64," +write_png(data, origin=origin, colormap=colormap).encode('base64') + 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)) diff --git a/folium/utilities.py b/folium/utilities.py index 35613627da..fdab640b2b 100644 --- a/folium/utilities.py +++ b/folium/utilities.py @@ -411,7 +411,7 @@ def write_png(data, origin='upper', colormap=None): assert array.shape == (height,width,nblayers) if nblayers==1: - array = np.array(map(colormap,array.ravel())) + array = np.array(list(map(colormap,array.ravel()))) nblayers = array.shape[1] if nblayers not in [3,4]: raise ValueError("colormap must provide colors of" diff --git a/tests/test_folium.py b/tests/test_folium.py index f93fa091da..029b8ed48a 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -15,6 +15,7 @@ from jinja2 import Environment, PackageLoader import vincent import folium +import base64 from folium.six import PY3 from folium.plugins import ScrollZoomToggler, MarkerCluster from folium.element import Html @@ -656,7 +657,7 @@ def test_image_overlay(self): #with open('data.png', 'wb') as f: # f.write(png_str) png = "data:image/png;base64,{}".format - inline_image_url = png(png_str.encode('base64').decode('utf-8')) + inline_image_url = png(base64.b64encode(png_str).decode('utf-8')) image_tpl = self.env.get_template('image_layer.js') image_name = 'Image_Overlay' From a6a0c3c5f93aabffb5227bd04c17f2128d1c7dad Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Sat, 3 Oct 2015 16:28:01 +0200 Subject: [PATCH 27/36] Remove test_scroll_zoom_toggler and test_marker_cluster -> to plugins --- tests/test_folium.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tests/test_folium.py b/tests/test_folium.py index 029b8ed48a..59609c3c56 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -685,20 +685,3 @@ def test_image_overlay(self): 'image_opacity': image_opacity}) assert ''.join(image_rendered.split()) in ''.join(out.split()) - - def test_scroll_zoom_toggler_plugin(self): - """Test ScrollZoomToggler plugin.""" - a_map = folium.Map([45, 3], zoom_start=4) - a_map.add_plugin(ScrollZoomToggler()) - a_map._build_map() - - def test_marker_cluster_plugin(self): - """Test MarkerCluster plugin.""" - data = [(35, -12, "lower left"), - (35, 30, "lower right"), - (60, -12, "upper left"), - (60, 30, "upper right"), - ] - a_map = folium.Map([0, 0], zoom_start=0) - a_map.add_plugin(MarkerCluster(data)) - a_map._build_map() From 859d7b86d5a6b8cbb473865470915b0320aa7494 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Sat, 3 Oct 2015 16:33:31 +0200 Subject: [PATCH 28/36] Fix test_marker_cluster --- folium/plugins/marker_cluster.py | 92 +++++++++++++++++++------------- tests/test_plugins.py | 4 +- 2 files changed, 57 insertions(+), 39 deletions(-) diff --git a/folium/plugins/marker_cluster.py b/folium/plugins/marker_cluster.py index 8011dae417..0c758c1ad8 100644 --- a/folium/plugins/marker_cluster.py +++ b/folium/plugins/marker_cluster.py @@ -5,48 +5,66 @@ Creates a MarkerCluster plugin to add on a folium map. """ -import json +#import json +from jinja2 import Template -from .plugin import Plugin +from folium.element import JavascriptLink, CssLink, MacroElement, Figure +from folium.map import Popup, Icon, Marker -class MarkerCluster(Plugin): - """Adds a MarkerCluster layer on the map.""" - def __init__(self, data): +class MarkerCluster(MacroElement): + def __init__(self, locations, popups=None, icons=None): """Creates a MarkerCluster plugin to append into a map with - Map.add_plugin. + Map.add_children. Parameters ---------- - data: list of list or array of shape (n,3). - Data points of the form [[lat, lng, popup]]. + locations: list of list or array of shape (n,2). + Data points of the form [[lat, lng]]. + + popups: list of length n. + Popup for each marker. + + icons: list of length n. + Icon for each marker. """ super(MarkerCluster, self).__init__() - self.plugin_name = 'MarkerCluster' - self.data = [tuple(x) for x in data] - - def render_header(self, nb): - """Generates the HTML part of the plugin.""" - return """ - - - - """ if nb==0 else "" - - def render_js(self, nb): - """Generates the Javascript part of the plugin.""" - out = """ - var addressPoints = """+json.dumps(self.data)+"""; - - var markers = L.markerClusterGroup(); - - for (var i = 0; i < addressPoints.length; i++) { - var a = addressPoints[i]; - var title = a[2]; - var marker = L.marker(new L.LatLng(a[0], a[1]), { title: title }); - marker.bindPopup(title); - markers.addLayer(marker); - } - - map.addLayer(markers); - """ - return out + self._name = 'MarkerCluster' + + if popups is None: + popups = [None]*len(locations) + if icons is None: + icons = [None]*len(locations) + + for location, popup, icon in zip(locations,popups,icons): + if popup is None or isinstance(popup, Popup): + p = popup + else: + p = Popup(popup) + if icon is None or isinstance(icon, Icon): + i = icon + else: + i = Icon(icon) + self.add_children(Marker(location, popup=p, icon=i)) + + self._template = Template(u""" + {% macro script(this, kwargs) %} + var {{this.get_name()}} = L.markerClusterGroup(); + {{this._parent.get_name()}}.addLayer({{this.get_name()}}); + {% endmacro %} + """) + def render(self,**kwargs): + super(MarkerCluster,self).render(**kwargs) + + figure = self.get_root() + assert isinstance(figure,Figure), ("You cannot render this Element " + "if it's not in a Figure.") + + figure.header.add_children(\ + JavascriptLink("https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.4.0/leaflet.markercluster.js"), + name='markerclusterjs') + figure.header.add_children(\ + CssLink("https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.4.0/MarkerCluster.css"), + name='markerclustercss') + figure.header.add_children(\ + CssLink("https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.4.0/MarkerCluster.Default.css"), + name='markerclusterdefaultcss') diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 363d90f61f..15686998b7 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -28,8 +28,8 @@ def test_marker_cluster(self): range(N), # Popups. ]).T mapa = folium.Map([45., 3.], zoom_start=4) - mapa.add_plugin(plugins.MarkerCluster(data)) - mapa._build_map() + mapa.add_children(plugins.MarkerCluster(data)) + mapa._repr_html_() def test_terminator(self): mapa = folium.Map([45., 3.], zoom_start=1) From 121508a72cbe240005b7ddcdf4901390eaa559e8 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Sat, 3 Oct 2015 18:04:53 +0200 Subject: [PATCH 29/36] Fix test_scroll_zoom_toggler --- folium/plugins/scroll_zoom_toggler.py | 96 ++++++++++++++------------- tests/test_plugins.py | 4 +- 2 files changed, 53 insertions(+), 47 deletions(-) diff --git a/folium/plugins/scroll_zoom_toggler.py b/folium/plugins/scroll_zoom_toggler.py index 7d091baa0d..8a7c473c28 100644 --- a/folium/plugins/scroll_zoom_toggler.py +++ b/folium/plugins/scroll_zoom_toggler.py @@ -5,48 +5,54 @@ Adds a button to enable/disable zoom scrolling. """ -from .template_plugin import TemplatePlugin - -class ScrollZoomToggler(TemplatePlugin): - """Adds a button to enable/disable zoom scrolling.""" - template = """ - {% set plugin_name = "ScrollZoomToggler" %} - {% macro css(nb) %} - #ScrollZoomToggler_{{nb}} { - position:absolute; - width:35px; - bottom:10px; - height:35px; - left:10px; - background-color:#fff; - text-align:center; - line-height:35px; - vertical-align: middle; - } - {% endmacro %} - - {% macro html(nb) %} - scroll - {% endmacro %} - - {% macro js(nb) %} - {% if nb==0 %} - map.scrollEnabled = true; - - var toggleScroll = function() { - if (map.scrollEnabled) { - map.scrollEnabled = false; - map.scrollWheelZoom.disable(); - } - else { - map.scrollEnabled = true; - map.scrollWheelZoom.enable(); - } - }; - - toggleScroll(); - {% endif %} - {% endmacro %} - """ \ No newline at end of file +from jinja2 import Template + +from folium.element import MacroElement, Figure, Element + +class ScrollZoomToggler(MacroElement): + def __init__(self): + """TODO docstring here. + """ + super(ScrollZoomToggler, self).__init__() + self._name = 'ScrollZoomToggler' + + self._template = Template(""" + {% macro header(this,kwargs) %} + + {% endmacro %} + + {% macro html(this,kwargs) %} + scroll + {% endmacro %} + + {% macro script(this,kwargs) %} + {{this._parent.get_name()}}.scrollEnabled = true; + + {{this._parent.get_name()}}.toggleScroll = function() { + if (this.scrollEnabled) { + this.scrollEnabled = false; + this.scrollWheelZoom.disable(); + } + else { + this.scrollEnabled = true; + this.scrollWheelZoom.enable(); + } + }; + + {{this._parent.get_name()}}.toggleScroll(); + {% endmacro %} + """) \ No newline at end of file diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 15686998b7..861e6f6908 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -17,8 +17,8 @@ class TestPlugins(object): def test_scroll_zoom_toggler(self): mapa = folium.Map([45., 3.], zoom_start=4) - mapa.add_plugin(plugins.ScrollZoomToggler()) - mapa._build_map() + mapa.add_children(plugins.ScrollZoomToggler()) + mapa._repr_html_() def test_marker_cluster(self): N = 100 From bcf573edcdbe09a7bc799f39a2cf0e5aed5c84ee Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Sat, 3 Oct 2015 18:25:27 +0200 Subject: [PATCH 30/36] Fix python3 bug with base64 encode --- folium/element.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/folium/element.py b/folium/element.py index 1113989282..7a433c17e9 100644 --- a/folium/element.py +++ b/folium/element.py @@ -11,6 +11,7 @@ ENV = Environment(loader=PackageLoader('folium', 'templates')) from collections import OrderedDict import json +import base64 from .six import urlopen from .utilities import _camelify, _parse_size @@ -266,7 +267,7 @@ def _repr_html_(self, **kwargs): iframe = ''\ .format(\ - html = "data:text/html;base64,"+html.encode('utf8').encode('base64'), + html = b"data:text/html;base64,"+base64.b64encode(html.encode('utf8')), #html = self.HTML.replace('"','"'), width = int(60.*width), height= int(60.*height), From 75cac11ca453f7f650a1e72cca1fae653fe8a6fd Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Sat, 3 Oct 2015 19:34:29 +0200 Subject: [PATCH 31/36] Fix test_terminator --- folium/plugins/terminator.py | 36 ++++++++++++++++++------------------ tests/test_plugins.py | 6 +++--- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/folium/plugins/terminator.py b/folium/plugins/terminator.py index b9d4fe21ae..69196449fb 100644 --- a/folium/plugins/terminator.py +++ b/folium/plugins/terminator.py @@ -5,19 +5,11 @@ Leaflet.Terminator is a simple plug-in to the Leaflet library to overlay day and night regions on maps. """ -try: - from urllib.request import urlopen as _urlopen -except: - from urllib import urlopen as _urlopen +from jinja2 import Template -from .plugin import Plugin +from folium.element import JavascriptLink, MacroElement, Figure -# As LO.Terminator.js is not served on both HTTP and HTTPS, we need to embed it explicitely into the code. -_request = _urlopen("http://rawgithub.com/joergdietrich/Leaflet.Terminator/master/L.Terminator.js") -assert _request.getcode()==200, "Error while loading Leaflet.terminator.js" -_terminator_script = _request.read().decode('utf8') - -class Terminator(Plugin): +class Terminator(MacroElement): """Leaflet.Terminator is a simple plug-in to the Leaflet library to overlay day and night regions on maps.""" def __init__(self): """Creates a Terminator plugin to append into a map with @@ -27,12 +19,20 @@ def __init__(self): ---------- """ super(Terminator, self).__init__() - self.plugin_name = 'Terminator' + self._name = 'Terminator' + + self._template = Template(u""" + {% macro script(this, kwargs) %} + L.terminator().addTo({{this._parent.get_name()}}); + {% endmacro %} + """) + def render(self,**kwargs): + super(Terminator,self).render(**kwargs) - def render_header(self, nb): - """Generates the header part of the plugin.""" - return '' if nb==0 else "" + figure = self.get_root() + assert isinstance(figure,Figure), ("You cannot render this Element " + "if it's not in a Figure.") - def render_js(self, nb): - """Generates the Javascript part of the plugin.""" - return "L.terminator().addTo(map);" if nb==0 else "" + figure.header.add_children(\ + JavascriptLink("https://rawgithub.com/joergdietrich/Leaflet.Terminator/master/L.Terminator.js"), + name='markerclusterjs') diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 861e6f6908..350f257d09 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -33,9 +33,9 @@ def test_marker_cluster(self): def test_terminator(self): mapa = folium.Map([45., 3.], zoom_start=1) - mapa.add_plugin(plugins.Terminator()) - mapa.add_plugin(plugins.ScrollZoomToggler()) - mapa._build_map() + mapa.add_children(plugins.Terminator()) + mapa.add_children(plugins.ScrollZoomToggler()) + mapa._repr_html_() def test_boat_marker(self): mapa = folium.Map([30., 0.], zoom_start=3) From 8d5dd692a88eb7fb3c814245702ab8a8c7b99caf Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Sun, 4 Oct 2015 09:34:19 +0200 Subject: [PATCH 32/36] Fix test_boat_marker --- folium/plugins/boat_marker.py | 49 +++++++++++++++++------------------ tests/test_plugins.py | 6 ++--- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/folium/plugins/boat_marker.py b/folium/plugins/boat_marker.py index 53a488e5ab..537c2433b9 100644 --- a/folium/plugins/boat_marker.py +++ b/folium/plugins/boat_marker.py @@ -6,10 +6,11 @@ Creates a marker shaped like a boat. Optionally you can append a wind direction. """ import json +from jinja2 import Template -from .plugin import Plugin +from folium.element import JavascriptLink, MacroElement, Figure -class BoatMarker(Plugin): +class BoatMarker(MacroElement): """Adds a BoatMarker layer on the map.""" def __init__(self, position=None, heading=0, wind_heading=None, wind_speed=0, **kwargs): """Creates a BoatMarker plugin to append into a map with @@ -32,30 +33,28 @@ def __init__(self, position=None, heading=0, wind_heading=None, wind_speed=0, ** Speed of the wind in knots. """ super(BoatMarker, self).__init__() - self.plugin_name = 'BoatMarker' + self._name = 'BoatMarker' self.position = None if position is None else tuple(position) self.heading = heading self.wind_heading = wind_heading self.wind_speed = wind_speed - self.kwargs = kwargs.copy() - - def render_header(self, nb): - """Generates the HTML part of the plugin.""" - return """ - - """ if nb==0 else "" - - def render_js(self, nb): - """Generates the Javascript part of the plugin.""" - kwargs_str = "{%s}" % ",".join(["%s : %s" % (key,json.dumps(val)) for (key,val) in self.kwargs.items()]) - position_str = "map.getCenter()" if self.position is None else "[%.12f,%.12f]"%self.position - out = 'var boatMarker_%s = L.boatMarker(%s, %s).addTo(map);' % (nb,position_str,kwargs_str) - - if self.wind_heading is None: - out += "boatMarker_%s.setHeading(%s);" % (nb,int(self.heading)) - else: - out += "boatMarker_%s.setHeadingWind(%s, %s, %s);"%(nb,int(self.heading), - int(self.wind_speed), - int(self.wind_heading), - ) - return out + self.kwargs = json.dumps(kwargs) + + self._template = Template(u""" + {% macro script(this, kwargs) %} + var {{this.get_name()}} = L.boatMarker( + [{{this.position[0]}},{{this.position[1]}}], + {{this.kwargs}}).addTo({{this._parent.get_name()}}); + {{this.get_name()}}.setHeadingWind({{this.heading}}, {{this.wind_speed}}, {{this.wind_heading}}); + {% endmacro %} + """) + def render(self,**kwargs): + super(BoatMarker,self).render(**kwargs) + + figure = self.get_root() + assert isinstance(figure,Figure), ("You cannot render this Element " + "if it's not in a Figure.") + + figure.header.add_children(\ + JavascriptLink("https://thomasbrueggemann.github.io/leaflet.boatmarker/js/leaflet.boatmarker.min.js"), + name='markerclusterjs') diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 350f257d09..842ca05dd1 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -39,17 +39,17 @@ def test_terminator(self): def test_boat_marker(self): mapa = folium.Map([30., 0.], zoom_start=3) - mapa.add_plugin(plugins.BoatMarker((34, -43), + mapa.add_children(plugins.BoatMarker((34, -43), heading=45, wind_heading=150, wind_speed=45, color="#8f8")) - mapa.add_plugin(plugins.BoatMarker((46, -30), + mapa.add_children(plugins.BoatMarker((46, -30), heading=-20, wind_heading=46, wind_speed=25, color="#88f")) - mapa._build_map() + mapa._repr_html_() def test_layer(self): mapa = folium.Map([48., 5.], zoom_start=6) From 7dd3fb8f5c0cd6d6c83990e30808168ab108cc39 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Sun, 4 Oct 2015 16:01:30 +0200 Subject: [PATCH 33/36] Fix test_layer --- folium/map.py | 41 ++++++++++++++++++++++++++++++++++++++++- tests/test_plugins.py | 11 ++++++----- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/folium/map.py b/folium/map.py index aada559b2f..89dba89f1b 100644 --- a/folium/map.py +++ b/folium/map.py @@ -7,6 +7,7 @@ """ import warnings import json +from collections import OrderedDict from jinja2 import Template @@ -173,7 +174,7 @@ def add_tile_layer(self, tiles='OpenStreetMap', name=None, class TileLayer(MacroElement): def __init__(self, tiles='OpenStreetMap', name=None, - min_zoom=1, max_zoom=18, attr=None, API_key=None): + min_zoom=1, max_zoom=18, attr=None, API_key=None, overlay = False): """TODO docstring here Parameters ---------- @@ -185,6 +186,8 @@ def __init__(self, tiles='OpenStreetMap', name=None, self.min_zoom = min_zoom self.max_zoom = max_zoom + self.overlay = overlay + self.tiles = ''.join(tiles.lower().strip().split()) if self.tiles in ('cloudmade', 'mapbox') and not API_key: raise ValueError('You must pass an API key if using Cloudmade' @@ -219,6 +222,42 @@ def __init__(self, tiles='OpenStreetMap', name=None, {% endmacro %} """) +class LayerControl(MacroElement): + """Adds a layer control to the map.""" + def __init__(self): + """Creates a LayerControl object to be added on a folium map. + + Parameters + ---------- + """ + super(LayerControl, self).__init__() + self._name = 'LayerControl' + + self.base_layers = OrderedDict() + self.overlays = OrderedDict() + + self._template = Template(""" + {% macro script(this,kwargs) %} + var {{this.get_name()}} = { + base_layers : { {% for key,val in this.base_layers.items() %}"{{key}}" : {{val}},{% endfor %} }, + overlays : { {% for key,val in this.overlays.items() %}"{{key}}" : {{val}},{% endfor %} } + }; + L.control.layers( + {{this.get_name()}}.base_layers, + {{this.get_name()}}.overlays + ).addTo({{this._parent.get_name()}}); + {% endmacro %} + """) + + def render(self, **kwargs): + """TODO : docstring here.""" + self.base_layers = OrderedDict([(val.tile_name,val.get_name()) \ + for key,val in self._parent._children.items() if isinstance(val,TileLayer) and not val.overlay]) + self.overlays = OrderedDict([(val.tile_name,val.get_name()) \ + for key,val in self._parent._children.items() if isinstance(val,TileLayer) and val.overlay]) + + super(LayerControl, self).render() + class Icon(MacroElement): def __init__(self, color='blue', icon='info-sign', angle=0): """TODO : docstring here""" diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 842ca05dd1..07a05bee17 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -52,11 +52,12 @@ def test_boat_marker(self): mapa._repr_html_() def test_layer(self): - mapa = folium.Map([48., 5.], zoom_start=6) - layer = '//otile1.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png' - mapa.add_plugin(plugins.Layer(layer, layer_name='MapQuest')) - mapa.add_plugin(plugins.LayerControl()) - mapa._build_map() + mapa = folium.Map([48., 5.], tiles='stamentoner', zoom_start=6) + layer = 'http://otile1.mqcdn.com/tiles/1.0.0/map/{z}/{x}/{y}.png' + mapa.add_children(folium.map.TileLayer(layer, name='MapQuest', attr='attribution')) + mapa.add_children(folium.map.TileLayer(layer, name='MapQuest2', attr='attribution2', overlay=True)) + mapa.add_children(folium.map.LayerControl()) + mapa._repr_html_() def test_geo_json(self): N = 100 From f6edd35c2a97914770c979c54f4974a1cf225c84 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Sun, 4 Oct 2015 17:37:30 +0200 Subject: [PATCH 34/36] Fix test_timestamp_geo_json --- folium/plugins/timestamped_geo_json.py | 73 ++++++++++++++++++++------ tests/test_plugins.py | 4 +- 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/folium/plugins/timestamped_geo_json.py b/folium/plugins/timestamped_geo_json.py index 8c3c0c96bc..a3ad8e6a2b 100644 --- a/folium/plugins/timestamped_geo_json.py +++ b/folium/plugins/timestamped_geo_json.py @@ -13,14 +13,14 @@ Eventually, you may have Point features with a "times" property being an array of length 1. """ import json +from jinja2 import Template -from .plugin import Plugin +from folium.element import MacroElement, Figure, JavascriptLink, CssLink -class TimestampedGeoJson(Plugin): - """Adds a TimestampedGeoJson layer on the map.""" +class TimestampedGeoJson(MacroElement): def __init__(self, data, transition_time=200, loop=True, auto_play=True): """Creates a TimestampedGeoJson plugin to append into a map with - Map.add_plugin. + Map.add_children. Parameters ---------- @@ -69,8 +69,9 @@ def __init__(self, data, transition_time=200, loop=True, auto_play=True): """ super(TimestampedGeoJson, self).__init__() - self.plugin_name = 'TimestampedGeoJson' - self.template = self.env.get_template('timestamped_geo_json.tpl') + self._name = 'TimestampedGeoJson' + + #self.template = self.env.get_template('timestamped_geo_json.tpl') if 'read' in dir(data): self.data = data.read() elif type(data) is dict: @@ -81,13 +82,53 @@ def __init__(self, data, transition_time=200, loop=True, auto_play=True): self.loop = bool(loop) self.auto_play = bool(auto_play) - def render_header(self, nb): - """Generates the header part of the plugin.""" - header = self.template.module.__dict__.get('header',None) - assert header is not None, "This template must have a 'header' macro." - return header(nb) - def render_js(self, nb): - """Generates the Javascript part of the plugin.""" - js = self.template.module.__dict__.get('js',None) - assert js is not None, "This template must have a 'js' macro." - return js(nb,self) + self._template = Template(""" + {% macro script(this, kwargs) %} + {{this._parent.get_name()}}.timeDimension = L.timeDimension(); + {{this._parent.get_name()}}.timeDimensionControl = L.control.timeDimension({ + position: 'bottomleft', + autoPlay: {{'true' if this.auto_play else 'false'}}, + playerOptions: { + transitionTime: {{this.transition_time}}, + loop: {{'true' if this.loop else 'false'}}} + }); + {{this._parent.get_name()}}.addControl({{this._parent.get_name()}}.timeDimensionControl); + + var {{this.get_name()}} = L.timeDimension.layer.geoJson( + L.geoJson({{this.data}}), + {updateTimeDimension: true,addlastPoint: true} + ).addTo({{this._parent.get_name()}}); + {% endmacro %} + """) + + def render(self, **kwargs): + super(TimestampedGeoJson, self).render() + + figure = self.get_root() + assert isinstance(figure,Figure), ("You cannot render this Element " + "if it's not in a Figure.") + + figure.header.add_children(\ + JavascriptLink("https://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js"), + name='jquery2.0.0') + + figure.header.add_children(\ + JavascriptLink("https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js"), + name='jqueryui1.10.2') + + figure.header.add_children(\ + JavascriptLink("https://raw.githubusercontent.com/nezasa/iso8601-js-period/master/iso8601.min.js"), + name='iso8601') + + figure.header.add_children(\ + JavascriptLink("https://raw.githubusercontent.com/socib/Leaflet.TimeDimension/master/" + "dist/leaflet.timedimension.min.js"), + name='leaflet.timedimension') + + figure.header.add_children(\ + CssLink("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/styles/default.min.css"), + name='highlight.js_css') + + figure.header.add_children(\ + CssLink("http://apps.socib.es/Leaflet.TimeDimension/dist/leaflet.timedimension.control.min.css"), + name='leaflet.timedimension_css') diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 07a05bee17..7131d10a79 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -186,5 +186,5 @@ def test_timestamped_geo_json(self): } mape = folium.Map([47, 3], zoom_start=1) - mape.add_plugin(plugins.TimestampedGeoJson(data)) - mape._build_map() + mape.add_children(plugins.TimestampedGeoJson(data)) + mape._repr_html_() From 1831159ae3e65aeb4ef8556a28dcce08c2ce4866 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Sun, 4 Oct 2015 17:40:19 +0200 Subject: [PATCH 35/36] Removed test_plugins.test_geojson <= duplicate --- tests/test_plugins.py | 54 ------------------------------------------- 1 file changed, 54 deletions(-) diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 7131d10a79..5cfd7aeca2 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -59,60 +59,6 @@ def test_layer(self): mapa.add_children(folium.map.LayerControl()) mapa._repr_html_() - def test_geo_json(self): - N = 100 - lons = 5 - np.random.normal(size=N) - lats = 48 - np.random.normal(size=N) - coordinates = [[lon, lat] for (lat, lon) in zip(lats, lons)] - data = { - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "MultiPoint", - "coordinates": coordinates, - }, - "properties": {"prop0": "value0"} - }, - ], - } - - mapa = folium.Map([48., 5.], zoom_start=6) - mapa.add_plugin(plugins.GeoJson(data)) - mapa._build_map() - - open('geojson_plugin_test1.json', 'w').write(json.dumps(data)) - mapb = folium.Map([48., 5.], zoom_start=6) - mapb.add_plugin(plugins.GeoJson(open('geojson_plugin_test1.json'))) - mapb._build_map() - - coordinates = [[[[lon+1e-4, lat+1e-4], [lon+1e-4, lat-1e-4], - [lon-1e-4, lat-1e-4], [lon-1e-4, lat+1e-4]]] for - (lat, lon) in zip(lats, lons)] - data = { - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "MultiPolygon", - "coordinates": coordinates, - }, - "properties": {"prop0": "value0"} - }, - ], - } - - mapc = folium.Map([48., 5.], zoom_start=6) - mapc.add_plugin(plugins.GeoJson(data)) - mapc._build_map() - - open('geojson_plugin_test2.json', 'w').write(json.dumps(data)) - mapd = folium.Map([48., 5.], zoom_start=6) - mapd.add_plugin(plugins.GeoJson(open('geojson_plugin_test2.json'))) - mapd._build_map() - def test_timestamped_geo_json(self): coordinates = [[[[lon-8*np.sin(theta), -47+6*np.cos(theta)] for theta in np.linspace(0, 2*np.pi, 25)], From 5d665c553adf6dc61cea729f382923108aaf9860 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Sun, 4 Oct 2015 19:23:44 +0200 Subject: [PATCH 36/36] Cleanup useless things --- folium/folium.py | 385 ---------------------- folium/plugins/__init__.py | 2 - folium/plugins/geo_json.py | 50 --- folium/plugins/layer.py | 77 ----- folium/plugins/leaflet-dvf.markers.min.js | 6 - folium/plugins/plugin.py | 42 --- folium/plugins/template_plugin.py | 58 ---- folium/plugins/timestamped_geo_json.tpl | 31 -- 8 files changed, 651 deletions(-) delete mode 100644 folium/plugins/geo_json.py delete mode 100644 folium/plugins/layer.py delete mode 100644 folium/plugins/leaflet-dvf.markers.min.js delete mode 100644 folium/plugins/plugin.py delete mode 100644 folium/plugins/template_plugin.py delete mode 100644 folium/plugins/timestamped_geo_json.tpl diff --git a/folium/folium.py b/folium/folium.py index 7d0fef8925..f03de946e5 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -8,19 +8,10 @@ """ from __future__ import absolute_import -#from __future__ import print_function -#from __future__ import division import warnings -#import codecs -#import functools import json -#from uuid import uuid4 -#from jinja2 import Environment, PackageLoader -#from pkg_resources import resource_string - -#from folium import utilities from folium.six import text_type, binary_type#, iteritems from .map import Map as _Map from .element import Element, Figure, JavascriptLink, CssLink, Div, MacroElement @@ -28,11 +19,6 @@ from .features import WmsTileLayer, RegularPolygonMarker, Vega, GeoJson, GeoJsonStyle, MarkerCluster, DivIcon,\ CircleMarker, LatLngPopup, ClickForMarker, ColorScale, TopoJson, PolyLine, MultiPolyLine, ImageOverlay from .utilities import color_brewer, write_png -#import sys -#import base64 - -#ENV = Environment(loader=PackageLoader('folium', 'templates')) - def initialize_notebook(): """Initialize the IPython notebook display elements.""" @@ -40,214 +26,10 @@ def initialize_notebook(): FutureWarning, stacklevel=2) pass - -#def iter_obj(type): -# """Decorator to keep count of different map object types in self.mk_cnt.""" -# def decorator(func): -# @functools.wraps(func) -# def wrapper(self, *args, **kwargs): -# self.mark_cnt[type] = self.mark_cnt.get(type, 0) + 1 -# func_result = func(self, *args, **kwargs) -# return func_result -# return wrapper -# return decorator - - class Map(_Map): """This class inherits from the map.Map object in order to provide bindings to former folium API. """ -# def __init__(self, location=None, width='100%', height='100%', -# tiles='OpenStreetMap', API_key=None, max_zoom=18, min_zoom=1, -# zoom_start=10, attr=None, min_lat=-90, max_lat=90, -# min_lon=-180, max_lon=180): -# """Create a Map with Folium and Leaflet.js -# -# Generate a base map of given width and height with either default -# tilesets or a custom tileset URL. The following tilesets are built-in -# to Folium. Pass any of the following to the "tiles" keyword: -# - "OpenStreetMap" -# - "MapQuest Open" -# - "MapQuest Open Aerial" -# - "Mapbox Bright" (Limited levels of zoom for free tiles) -# - "Mapbox Control Room" (Limited levels of zoom for free tiles) -# - "Stamen" (Terrain, Toner, and Watercolor) -# - "Cloudmade" (Must pass API key) -# - "Mapbox" (Must pass API key) -# - "CartoDB" (positron and dark_matter) -# You can pass a custom tileset to Folium by passing a Leaflet-style -# URL to the tiles parameter: -# http://{s}.yourtiles.com/{z}/{x}/{y}.png -# -# Parameters -# ---------- -# location: tuple or list, default None -# Latitude and Longitude of Map (Northing, Easting). -# width: pixel int or percentage string (default: '100%') -# Width of the map. -# height: pixel int or percentage string (default: '100%') -# Height of the map. -# tiles: str, default 'OpenStreetMap' -# Map tileset to use. Can use defaults or pass a custom URL. -# API_key: str, default None -# API key for Cloudmade or Mapbox tiles. -# max_zoom: int, default 18 -# Maximum zoom depth for the map. -# zoom_start: int, default 10 -# Initial zoom level for the map. -# attr: string, default None -# Map tile attribution; only required if passing custom tile URL. -# -# Returns -# ------- -# Folium Map Object -# -# Examples -# -------- -# >>>map = folium.Map(location=[45.523, -122.675], width=750, height=500) -# >>>map = folium.Map(location=[45.523, -122.675], -# tiles='Mapbox Control Room') -# >>>map = folium.Map(location=(45.523, -122.675), max_zoom=20, -# tiles='Cloudmade', API_key='YourKey') -# >>>map = folium.Map(location=[45.523, -122.675], zoom_start=2, -# tiles=('http://{s}.tiles.mapbox.com/v3/' -# 'mapbox.control-room/{z}/{x}/{y}.png'), -# attr='Mapbox attribution') -# -# """ -# -# # Inits. -# self.map_path = None -# self.render_iframe = False -# self.map_type = 'base' -# self.map_id = '_'.join(['folium', uuid4().hex]) -# -# # Mark counter, JSON, Plugins. -# self.mark_cnt = {} -# self.json_data = {} -# self.plugins = {} -# -# # No location means we will use automatic bounds and ignore zoom -# self.location = location -# -# # If location is not passed, we center the map at 0,0 -# if not location: -# location = [0, 0] -# zoom_start = min_zoom -# -# # Map Size Parameters. -# try: -# if isinstance(width, int): -# width_type = 'px' -# assert width > 0 -# else: -# width_type = '%' -# width = int(width.strip('%')) -# assert 0 <= width <= 100 -# except: -# msg = "Cannot parse width {!r} as {!r}".format -# raise ValueError(msg(width, width_type)) -# self.width = width -# -# try: -# if isinstance(height, int): -# height_type = 'px' -# assert height > 0 -# else: -# height_type = '%' -# height = int(height.strip('%')) -# assert 0 <= height <= 100 -# except: -# msg = "Cannot parse height {!r} as {!r}".format -# raise ValueError(msg(height, height_type)) -# self.height = height -# -# self.map_size = {'width': width, 'height': height} -# self._size = ('style="width: {0}{1}; height: {2}{3}"' -# .format(width, width_type, height, height_type)) -# # Templates. -# self.env = ENV -# self.template_vars = dict(lat=location[0], -# lon=location[1], -# size=self._size, -# max_zoom=max_zoom, -# zoom_level=zoom_start, -# map_id=self.map_id, -# min_zoom=min_zoom, -# min_lat=min_lat, -# max_lat=max_lat, -# min_lon=min_lon, -# max_lon=max_lon) -# -# # Tiles. -# self.tiles = ''.join(tiles.lower().strip().split()) -# if self.tiles in ('cloudmade', 'mapbox') and not API_key: -# raise ValueError('You must pass an API key if using Cloudmade' -# ' or non-default Mapbox tiles.') -# -# self.default_tiles = ['openstreetmap', 'mapboxcontrolroom', -# 'mapquestopen', 'mapquestopenaerial', -# 'mapboxbright', 'mapbox', 'cloudmade', -# 'stamenterrain', 'stamentoner', -# 'stamenwatercolor', -# 'cartodbpositron', 'cartodbdark_matter'] -# -# self.tile_types = {} -# for tile in self.default_tiles: -# tile_path = 'tiles/%s' % tile -# self.tile_types[tile] = { -# 'templ': self.env.get_template('%s/%s' % (tile_path, -# 'tiles.txt')), -# 'attr': self.env.get_template('%s/%s' % (tile_path, -# 'attr.txt')), -# } -# -# if self.tiles in self.tile_types: -# self.template_vars['Tiles'] = (self.tile_types[self.tiles]['templ'] -# .render(API_key=API_key)) -# self.template_vars['attr'] = (self.tile_types[self.tiles]['attr'] -# .render()) -# else: -# self.template_vars['Tiles'] = tiles -# if not attr: -# raise ValueError('Custom tiles must' -# ' also be passed an attribution') -# if isinstance(attr, binary_type): -# attr = text_type(attr, 'utf8') -# self.template_vars['attr'] = attr -# self.tile_types.update({'Custom': {'template': tiles, -# 'attr': attr}}) -# -# self.added_layers = [] -# self.template_vars.setdefault('wms_layers', []) -# self.template_vars.setdefault('tile_layers', []) -# self.template_vars.setdefault('image_layers', []) -# -# @iter_obj('simple') -# def add_tile_layer(self, tile_name=None, tile_url=None, active=False): -# """Adds a simple tile layer. -# -# Parameters -# ---------- -# tile_name: string -# name of the tile layer -# tile_url: string -# url location of the tile layer -# active: boolean -# should the layer be active when added -# """ -# if tile_name not in self.added_layers: -# tile_name = tile_name.replace(" ", "_") -# tile_temp = self.env.get_template('tile_layer.js') -# -# tile = tile_temp.render({'tile_name': tile_name, -# 'tile_url': tile_url}) -# -# self.template_vars.setdefault('tile_layers', []).append((tile)) -# -# self.added_layers.append({tile_name: tile_url}) -# -# @iter_obj('simple') def add_wms_layer(self, wms_name=None, wms_url=None, wms_format=None, wms_layers=None, wms_transparent=True): """Adds a simple tile layer. @@ -266,26 +48,6 @@ def add_wms_layer(self, wms_name=None, wms_url=None, wms_format=None, transparent=wms_transparent, attribution=None) self.add_children(wms, name=wms_name) -# @iter_obj('simple') -# def add_layers_to_map(self): -# """ -# Required function to actually add the layers to the HTML packet. -# """ -# layers_temp = self.env.get_template('add_layers.js') -# -# data_string = '' -# for i, layer in enumerate(self.added_layers): -# name = list(layer.keys())[0] -# if i < len(self.added_layers)-1: -# term_string = ",\n" -# else: -# term_string += "\n" -# data_string += '\"{}\": {}'.format(name, name, term_string) -# -# data_layers = layers_temp.render({'layers': data_string}) -# self.template_vars.setdefault('data_layers', []).append((data_layers)) -# -# @iter_obj('simple') def simple_marker(self, location=None, popup=None, marker_color='blue', marker_icon='info-sign', clustered_marker=False, icon_angle=0, popup_width=300): @@ -477,7 +239,6 @@ def multiline(self, locations, line_color=None, line_opacity=None, self.add_children(p) -# @iter_obj('circle') def circle_marker(self, location=None, radius=500, popup=None, line_color='black', fill_color='black', fill_opacity=0.6, popup_width=300): @@ -684,73 +445,6 @@ def add_plugin(self, plugin): # }) # # self.template_vars.update({'fit_bounds': fit_bounds_str.strip()}) -# -# def _popup_render(self, popup=None, mk_name=None, count=None, -# width=300): -# """Popup renderer: either text or Vincent/Vega. -# -# Parameters -# ---------- -# popup: str or Vincent tuple, default None -# String for text popup, or tuple of (Vincent object, json_path) -# mk_name: str, default None -# Type of marker. Simple, Circle, etc. -# count: int, default None -# Count of marker -# """ -# if not popup: -# return '' -# else: -# if sys.version_info >= (3, 0): -# utype, stype = str, bytes -# else: -# utype, stype = unicode, str -# -# if isinstance(popup, (utype, stype)): -# popup_temp = self.env.get_template('simple_popup.js') -# if isinstance(popup, utype): -# popup_txt = popup.encode('ascii', 'xmlcharrefreplace') -# else: -# popup_txt = popup -# if sys.version_info >= (3, 0): -# popup_txt = popup_txt.decode() -# pop_txt = json.dumps(str(popup_txt)) -# return popup_temp.render({'pop_name': mk_name + str(count), -# 'pop_txt': pop_txt, 'width': width}) -# elif isinstance(popup, tuple): -# # Update template with JS libs. -# vega_temp = self.env.get_template('vega_ref.txt').render() -# jquery_temp = self.env.get_template('jquery_ref.txt').render() -# d3_temp = self.env.get_template('d3_ref.txt').render() -# vega_parse = self.env.get_template('vega_parse.js').render() -# self.template_vars.update({'vega': vega_temp, -# 'd3': d3_temp, -# 'jquery': jquery_temp, -# 'vega_parse': vega_parse}) -# -# # Parameters for Vega template. -# vega = popup[0] -# mark = ''.join([mk_name, str(count)]) -# json_out = popup[1] -# div_id = popup[1].split('.')[0] -# width = vega.width -# height = vega.height -# if isinstance(vega.padding, dict): -# width += vega.padding['left']+vega.padding['right'] -# height += vega.padding['top']+vega.padding['bottom'] -# else: -# width += 75 -# height += 50 -# max_width = max([self.map_size['width'], width]) -# vega_id = '#' + div_id -# popup_temp = self.env.get_template('vega_marker.js') -# return popup_temp.render({'mark': mark, 'div_id': div_id, -# 'width': width, 'height': height, -# 'max_width': max_width, -# 'json_out': json_out, -# 'vega_id': vega_id}) -# else: -# raise TypeError("Unrecognized popup type: {!r}".format(popup)) def geo_json(self, geo_path=None, geo_str=None, data_out='data.json', data=None, columns=None, key_on=None, threshold_scale=None, @@ -980,82 +674,3 @@ def image_overlay(self, data, opacity=0.25, min_lat=-90.0, max_lat=90.0, self.add_children(ImageOverlay(data, [[min_lat, min_lon],[max_lat,max_lon]], opacity=opacity, origin=origin, colormap=colormap, mercator_project=mercator_project)) - -# def _build_map(self, html_templ=None, templ_type='string'): -# self._auto_bounds() -# """Build HTML/JS/CSS from Templates given current map type.""" -# if html_templ is None: -# map_types = {'base': 'fol_template.html', -# 'geojson': 'geojson_template.html'} -# -# # Check current map type. -# type_temp = map_types[self.map_type] -# -# html_templ = self.env.get_template(type_temp) -# else: -# if templ_type == 'string': -# html_templ = self.env.from_string(html_templ) -# -# self.HTML = html_templ.render(self.template_vars, plugins=self.plugins) -# -# def create_map(self, path='map.html', plugin_data_out=True, template=None): -# """Write Map output to HTML and data output to JSON if available. -# -# Parameters: -# ----------- -# path: string, default 'map.html' -# Path for HTML output for map -# plugin_data_out: boolean, default True -# If using plugins such as awesome markers, write all plugin -# data such as JS/CSS/images to path -# template: string, default None -# Custom template to render -# -# """ -# self.map_path = path -# self._build_map(template) -# -# with codecs.open(path, 'w', 'utf8') as f: -# f.write(self.HTML) -# -# if self.json_data: -# for path, data in iteritems(self.json_data): -# with open(path, 'w') as g: -# json.dump(data, g) -# -# if self.plugins and plugin_data_out: -# for name, plugin in iteritems(self.plugins): -# with open(name, 'w') as f: -# if isinstance(plugin, binary_type): -# plugin = text_type(plugin, 'utf8') -# f.write(plugin) -# -# def _repr_html_(self): -# """Build the HTML representation for IPython.""" -# map_types = {'base': 'ipynb_repr.html', -# 'geojson': 'ipynb_iframe.html'} -# -# # Check current map type. -# type_temp = map_types[self.map_type] -# if self.render_iframe: -# type_temp = 'ipynb_iframe.html' -# templ = self.env.get_template(type_temp) -# self._build_map(html_templ=templ, templ_type='temp') -# if self.map_type == 'geojson' or self.render_iframe: -# if not self.map_path: -# raise ValueError('Use create_map to set the path!') -# return templ.render(path=self.map_path, width=self.width, -# height=self.height) -# return self.HTML -# -# def display(self): -# """Display the visualization inline in the IPython notebook. -# -# This is deprecated, use the following instead:: -# -# from IPython.display import display -# display(viz) -# """ -# from IPython.core.display import display, HTML -# display(HTML(self._repr_html_())) -# \ No newline at end of file diff --git a/folium/plugins/__init__.py b/folium/plugins/__init__.py index c5241f8128..5b826551a5 100644 --- a/folium/plugins/__init__.py +++ b/folium/plugins/__init__.py @@ -9,6 +9,4 @@ from .scroll_zoom_toggler import ScrollZoomToggler from .terminator import Terminator from .boat_marker import BoatMarker -from .layer import Layer, LayerControl -from .geo_json import GeoJson from .timestamped_geo_json import TimestampedGeoJson diff --git a/folium/plugins/geo_json.py b/folium/plugins/geo_json.py deleted file mode 100644 index 364f123e1a..0000000000 --- a/folium/plugins/geo_json.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- -""" -GeoJson plugin --------------- - -Add a geojson feature collection on a folium map. -""" -import json - -from .plugin import Plugin - -class GeoJson(Plugin): - """Adds a GeoJson layer on the map.""" - def __init__(self, data): - """Creates a GeoJson plugin to append into a map with - Map.add_plugin. - - Parameters - ---------- - data: file, dict or str. - The geo-json data you want to plot. - If file, then data will be read in the file and fully embeded in Leaflet's javascript. - If dict, then data will be converted to json and embeded in the javascript. - If str, then data will be passed to the javascript as-is. - - examples : - # providing file - GeoJson(open('foo.json')) - - # providing dict - GeoJson(json.load(open('foo.json'))) - - # providing string - GeoJson(open('foo.json').read()) - """ - super(GeoJson, self).__init__() - self.plugin_name = 'GeoJson' - if 'read' in dir(data): - self.data = data.read() - elif type(data) is dict: - self.data = json.dumps(data) - else: - self.data = data - - def render_js(self, nb): - """Generates the Javascript part of the plugin.""" - out = """ - var geojson_{nb} = L.geoJson({data}).addTo(map); - """.format(nb=nb, data = self.data) - return out diff --git a/folium/plugins/layer.py b/folium/plugins/layer.py deleted file mode 100644 index c2766becf9..0000000000 --- a/folium/plugins/layer.py +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Layer plugin ------------- - -Add layers and layer control to the map. -""" -from .plugin import Plugin - -class Layer(Plugin): - """Adds a layer to the map.""" - def __init__(self, url=None, layer_name = None, min_zoom=1, max_zoom=18, attribution=''): - """Crates a layer object to be added on a folium map. - - Parameters - ---------- - url : str - The url of the layer service, in the classical leaflet form. - example: url='//otile1.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png' - layer_name : str - Tha name of the layer that will be displayed in the layer control. - If None, a random hexadecimal string will be created. - min_zoom : int, default 1 - The minimal zoom allowed for this layer - max_zoom : int, default 18 - The maximal zoom allowed for this layer - attribution : str, default '' - Tha atribution string for the layer. - """ - super(Layer, self).__init__() - self.plugin_name = 'Layer' - self.tile_url = url - self.attribution = attribution - self.min_zoom = min_zoom - self.max_zoom = max_zoom - self.object_id = self.object_name - if layer_name is not None: - self.object_name = layer_name - - def render_js(self, nb): - """Generates the JS part of the plugin.""" - return """ - var layer_"""+self.object_id+""" = L.tileLayer('"""+self.tile_url+"""', { - maxZoom: """+str(self.max_zoom)+""", - minZoom: """+str(self.min_zoom)+""", - attribution: '"""+str(self.attribution)+"""' - }); - layer_"""+self.object_id+""".addTo(map); - """ - -class LayerControl(Plugin): - """Adds a layer control to the map.""" - def __init__(self, base_layer_name="Base Layer"): - """Creates a LayerControl object to be added on a folium map. - - Parameters - ---------- - base_layer_name : str, default "Base Layer" - The name of the base layer that you want to see on the control. - """ - super(LayerControl, self).__init__() - self.plugin_name = 'LayerControl' - self.base_layer_name = base_layer_name - - def render_js(self, nb): - """Generates the JS part of the plugin.""" - return """ - var baseLayer = { - "%s": base_tile,"""% self.base_layer_name+\ - ",".join(['"%s" : layer_%s ' % (x.object_name,x.object_id) for x in self.map.plugins['Layer']])+\ - """}; - - L.control.layers(baseLayer, layer_list).addTo(map); - """ - - - \ No newline at end of file diff --git a/folium/plugins/leaflet-dvf.markers.min.js b/folium/plugins/leaflet-dvf.markers.min.js deleted file mode 100644 index 628db6ef86..0000000000 --- a/folium/plugins/leaflet-dvf.markers.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/* - Leaflet Data Visualization Framework, a JavaScript library for creating thematic maps using Leaflet - (c) 2013, Scott Fairgrieve, HumanGeo -*/ -Object.keys||(Object.keys=function(){var hasOwnProperty=Object.prototype.hasOwnProperty,hasDontEnumBug=!{toString:null}.propertyIsEnumerable("toString"),dontEnums=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],dontEnumsLength=dontEnums.length;return function(obj){var result,prop,i;if("object"!=typeof obj&&"function"!=typeof obj||null===obj)throw new TypeError("Object.keys called on non-object");result=[];for(prop in obj)hasOwnProperty.call(obj,prop)&&result.push(prop);if(hasDontEnumBug)for(i=0;dontEnumsLength>i;i++)hasOwnProperty.call(obj,dontEnums[i])&&result.push(dontEnums[i]);return result}}());var L=L||{};L.Util.guid=function(){var s4=function(){return Math.floor(65536*(1+Math.random())).toString(16).substring(1)};return s4()+s4()+"-"+s4()+"-"+s4()+"-"+s4()+"-"+s4()+s4()+s4()},L.Util.getProperty=function(obj,property,defaultValue){return property in obj?obj[property]:defaultValue},L.Util.getFieldValue=function(record,fieldName){var value=null;if(fieldName){for(var part,searchParts,searchKey,searchValue,testObject,searchPart,testValue,parts=fieldName.split("."),valueField=record,bracketIndex=-1,partIndex=0;parts.length>partIndex;++partIndex)if(part=parts[partIndex],bracketIndex=part.indexOf("["),bracketIndex>-1){searchPart=part.substring(bracketIndex),part=part.substring(0,bracketIndex),searchPart=searchPart.replace("[","").replace("]",""),searchParts=searchPart.split("="),searchKey=searchParts[0],searchValue=searchParts[1],valueField=valueField[part];for(var valueIndex=0;valueField.length>valueIndex;++valueIndex)testObject=valueField[valueIndex],testValue=testObject[searchKey],testValue&&testValue===searchValue&&(valueField=testObject)}else{if(!valueField||!valueField.hasOwnProperty(part)){valueField=null;break}valueField=valueField[part]}value=valueField}else value=record;return value},L.CategoryLegend=L.Class.extend({initialize:function(options){L.Util.setOptions(this,options)},generate:function(options){options=options||{};var legend='
',$legend=$(legend),className=options.className,legendOptions=this.options;className&&$legend.addClass(className),options.title&&$legend.append('
'+options.title+"
");for(var key in legendOptions){categoryOptions=legendOptions[key];var displayName=categoryOptions.displayName||key,$legendElement=$('
'+displayName+"
"),$legendBox=$legendElement.find(".legend-box");L.StyleConverter.applySVGStyle($legendBox,categoryOptions),$legend.append($legendElement)}return $legend.wrap("
").parent().html()}}),L.LegendIcon=L.DivIcon.extend({initialize:function(fields,layerOptions,options){var field,html='
',$html=$(html),$legendBox=$html.find(".legend-box"),$legendValues=$html.find(".legend-values"),title=layerOptions.title||layerOptions.name;title&&$html.find(".title").text(title);for(var key in fields){field=fields[key];var displayName=field.name||key,displayText=field.value;$legendValues.append('
'+displayName+'
'+displayText+"
")}L.StyleConverter.applySVGStyle($legendBox,layerOptions),$legendBox.height(5),html=$html.wrap("
").parent().html(),options.html=html,options.className=options.className||"legend-icon",L.DivIcon.prototype.initialize.call(this,options)}}),L.legendIcon=function(fields,layerOptions,options){return new L.LegendIcon(fields,layerOptions,options)},L.GeometryUtils={getName:function(geoJSON){var name=null;if(geoJSON&&geoJSON.features)for(var index=0;geoJSON.features.length>index;++index){var feature=geoJSON.features[index];if(feature.properties&&feature.properties.name){name=feature.properties.name;break}}return name},getGeoJSONLocation:function(geoJSON,record,locationTextField,recordToLayer){var geoJSONLayer=new L.GeoJSON(geoJSON,{pointToLayer:function(feature,latlng){var location={location:latlng,text:locationTextField?L.Util.getFieldValue(record,locationTextField):[latlng.lat.toFixed(3),latlng.lng.toFixed(3)].join(", "),center:latlng};return recordToLayer(location,record)}}),center=null;try{center=L.GeometryUtils.loadCentroid(geoJSON)}catch(ex){console.log("Error loading centroid for "+JSON.stringify(geoJSON))}return center||(center=geoJSONLayer.getBounds().getCenter()),{location:geoJSONLayer,text:locationTextField?L.Util.getFieldValue(record,locationTextField):null,center:center}},mergeProperties:function(properties,featureCollection,mergeKey){var property,mergeValue,features=featureCollection.features,featureIndex=L.GeometryUtils.indexFeatureCollection(features,mergeKey),newFeatureCollection={type:"FeatureCollection",features:[]};for(var key in properties)if(properties.hasOwnProperty(key)&&(property=properties[key],mergeValue=property[mergeKey])){var feature=featureIndex[mergeValue];for(var prop in property)feature.properties[prop]=property[prop];newFeatureCollection.features.push(feature)}return newFeatureCollection},indexFeatureCollection:function(featureCollection,indexKey){for(var feature,properties,value,features=featureCollection.features,featureIndex={},index=0;features.length>index;++index)feature=features[index],properties=feature.properties,value=properties[indexKey],featureIndex[value]=feature;return featureIndex},arrayToMap:function(array,fromKey,toKey){for(var item,from,to,map={},index=0;array.length>index;++index)item=array[index],from=item[fromKey],to=toKey?item[toKey]:item,map[from]=to;return map},arrayToMaps:function(array,mapLinks){for(var map,item,from,to,mapLink,fromKey,toKey,maps=[],i=0;mapLinks.length>i;++i)maps.push({});for(var index=0;array.length>index;++index){item=array[index];for(var keyIndex=0;mapLinks.length>keyIndex;++keyIndex)map=maps[keyIndex],mapLink=mapLinks[keyIndex],fromKey=mapLink.from,toKey=mapLink.to,from=item[fromKey],to=toKey?item[toKey]:item,map[from]=to}return maps},loadCentroid:function(feature){var centroid,x,y,centroidLatLng=null;if(jsts){var parser=new jsts.io.GeoJSONParser,jstsFeature=parser.read(feature);if(jstsFeature.getCentroid)centroid=jstsFeature.getCentroid(),x=centroid.coordinate.x,y=centroid.coordinate.y;else if(jstsFeature.features){for(var totalCentroidX=0,totalCentroidY=0,i=0;jstsFeature.features.length>i;++i)centroid=jstsFeature.features[i].geometry.getCentroid(),totalCentroidX+=centroid.coordinate.x,totalCentroidY+=centroid.coordinate.y;x=totalCentroidX/jstsFeature.features.length,y=totalCentroidY/jstsFeature.features.length}else centroid=jstsFeature.geometry.getCentroid(),x=centroid.coordinate.x,y=centroid.coordinate.y;centroidLatLng=new L.LatLng(y,x)}return centroidLatLng},loadCentroids:function(dictionary){var feature,centroids={};for(var key in dictionary)feature=dictionary[key],centroids[key]=L.GeometryUtils.loadCentroid(feature);return centroids}},L.SVGPathBuilder=L.Class.extend({initialize:function(points,innerPoints,options){this._points=points||[],this._innerPoints=innerPoints||[],L.Util.setOptions(this,options)},_getPathString:function(points,digits){var pathString="";if(points.length>0){var point=points[0],digits=digits||2;pathString="M"+point.x.toFixed(digits)+","+point.y.toFixed(digits);for(var index=1;points.length>index;index++)point=points[index],pathString+="L"+point.x.toFixed(digits)+","+point.y.toFixed(digits);pathString+="Z"}return pathString},addPoint:function(point,inner){inner?this._innerPoints.push(point):this._points.push(point)},toString:function(digits){digits=digits||this.options.digits;var pathString=this._getPathString(this._points,digits);return this._innerPoints&&(pathString+=this._getPathString(this._innerPoints,digits)),pathString}}),L.StyleConverter={keyMap:{fillColor:{property:["background-color"],valueFunction:function(value){return value}},color:{property:["border-color"],valueFunction:function(value){return value}},weight:{property:["border-width"],valueFunction:function(value){return value+"px"}},stroke:{property:["border-style"],valueFunction:function(value){return value===!0?"solid":"none"}},dashArray:{property:["border-style"],valueFunction:function(value){var style="solid";return value&&(style="dashed"),style}},radius:{property:["height"],valueFunction:function(value){return 2*value+"px"}},fillOpacity:{property:["opacity"],valueFunction:function(value){return value}}},applySVGStyle:function($element,svgStyle,additionalKeys){var keyMap=L.StyleConverter.keyMap;additionalKeys&&(keyMap=L.Util.extend(keyMap,additionalKeys)),$element.css("border-style","solid");for(var property in svgStyle)$element=L.StyleConverter.setCSSProperty($element,property,svgStyle[property],keyMap);return $element},setCSSProperty:function($element,key,value,keyMap){var keyMap=keyMap||L.StyleConverter.keyMap,cssProperty=keyMap[key];if(cssProperty)for(var propertyKey=cssProperty.property,propertyIndex=0;propertyKey.length>propertyIndex;++propertyIndex)$element.css(propertyKey[propertyIndex],cssProperty.valueFunction(value));return $element}},L.StylesBuilder=L.Class.extend({initialize:function(categories,styleFunctionMap){this._categories=categories,this._styleFunctionMap=styleFunctionMap,this._buildStyles()},_buildStyles:function(){for(var category,styleFunction,styleValue,map={},index=0;this._categories.length>index;++index){category=this._categories[index],map[category]={};for(var property in this._styleFunctionMap)styleFunction=this._styleFunctionMap[property],styleValue=styleFunction.evaluate?styleFunction.evaluate(index):"function"==typeof styleFunction?styleFunction(index):styleFunction,map[category][property]=styleValue}this._styleMap=map},getStyles:function(){return this._styleMap}}),L.PaletteBuilder=L.Class.extend({initialize:function(styleFunctionMap){this._styleFunctionMap=styleFunctionMap},generate:function(options){options=options||{};var $paletteElement=$('
'),count=options.count||10,categories=function(count){for(var categoryArray=[],i=0;count>i;++i)categoryArray.push(i);return categoryArray}(count),styleBuilder=new L.StylesBuilder(categories,this._styleFunctionMap),styles=styleBuilder.getStyles();options.className&&$paletteElement.addClass(options.className);for(var styleKey in styles){var $i=$(''),style=styles[styleKey];L.StyleConverter.applySVGStyle($i,style),$paletteElement.append($i)}return $paletteElement.wrap("
").parent().html()}}),L.HTMLUtils={buildTable:function(obj,className,ignoreFields){className=className||"table table-condensed table-striped table-bordered";var html='
NameValue
',$html=$(html),$tbody=$html.find("tbody");ignoreFields=ignoreFields||[];for(var property in obj)obj.hasOwnProperty(property)&&-1===$.inArray(ignoreFields,property)&&($.isPlainObject(obj[property])||obj[property]instanceof Array?$tbody.append(""+property+""+L.HTMLUtils.buildTable(obj[property],ignoreFields).wrap("
").parent().html()+""):$tbody.append(""+property+""+obj[property]+""));return $html}},L.AnimationUtils={animate:function(layer,from,to,options){var delay=options.delay||0,frames=options.frames||30,duration=options.duration||500,linearFunctions={},easeFunction=options.easeFunction||function(step){return step},complete=options.complete,step=duration/frames;for(var key in from)"color"!=key&&"fillColor"!=key&&to[key]&&(linearFunctions[key]=new L.LinearFunction([0,from[key]],[frames-1,to[key]]));var layerOptions={},frame=0,updateLayer=function(){for(var key in linearFunctions)layerOptions[key]=linearFunctions[key].evaluate(frame);layer.options=$.extend(!0,{},layer.options,layerOptions),layer.redraw(),frame++,step=easeFunction(step),frames>frame?setTimeout(updateLayer,step):complete()};setTimeout(updateLayer,delay)}},L.ColorUtils={hslToRgbString:function(h,s,l){return L.ColorUtils.rgbArrayToString(L.ColorUtils.hslToRgb(h,s,l))},rgbArrayToString:function(rgbArray){for(var hexValues=[],index=0;rgbArray.length>index;++index){var hexValue=rgbArray[index].toString(16);1===hexValue.length&&(hexValue="0"+hexValue),hexValues.push(hexValue)}return"#"+hexValues.join("")},rgbToHsl:function(r,g,b){r/=255,g/=255,b/=255;var h,s,max=Math.max(r,g,b),min=Math.min(r,g,b),l=(max+min)/2;if(max==min)h=s=0;else{var d=max-min;switch(s=l>.5?d/(2-max-min):d/(max+min),max){case r:h=(g-b)/d+(b>g?6:0);break;case g:h=(b-r)/d+2;break;case b:h=(r-g)/d+4}h/=6}return[h,s,l]},hslToRgb:function(h,s,l){function hue2rgb(p,q,t){return 0>t&&(t+=1),t>1&&(t-=1),1/6>t?p+6*(q-p)*t:.5>t?q:2/3>t?p+6*(q-p)*(2/3-t):p}var r,g,b;if(0==s)r=g=b=l;else{var q=.5>l?l*(1+s):l+s-l*s,p=2*l-q;r=hue2rgb(p,q,h+1/3),g=hue2rgb(p,q,h),b=hue2rgb(p,q,h-1/3)}return[255*r,255*g,255*b]},rgbToHsv:function(r,g,b){r/=255,g/=255,b/=255;var h,s,max=Math.max(r,g,b),min=Math.min(r,g,b),v=max,d=max-min;if(s=0==max?0:d/max,max==min)h=0;else{switch(max){case r:h=(g-b)/d+(b>g?6:0);break;case g:h=(b-r)/d+2;break;case b:h=(r-g)/d+4}h/=6}return[h,s,v]},hsvToRgb:function(h,s,v){var r,g,b,i=Math.floor(6*h),f=6*h-i,p=v*(1-s),q=v*(1-f*s),t=v*(1-(1-f)*s);switch(i%6){case 0:r=v,g=t,b=p;break;case 1:r=q,g=v,b=p;break;case 2:r=p,g=v,b=t;break;case 3:r=p,g=q,b=v;break;case 4:r=t,g=p,b=v;break;case 5:r=v,g=p,b=q}return[255*r,255*g,255*b]}};var L=L||{};L.LinearFunction=L.Class.extend({initialize:function(minPoint,maxPoint,options){this.setOptions(options),this.setRange(minPoint,maxPoint)},_calculateParameters:function(minPoint,maxPoint){0===this._xRange?(this._slope=0,this._b=minPoint.y):(this._slope=(maxPoint.y-minPoint.y)/this._xRange,this._b=minPoint.y-this._slope*minPoint.x)},_arrayToPoint:function(array){return{x:array[0],y:array[1]}},setOptions:function(options){L.Util.setOptions(this,options),this._preProcess=this.options.preProcess,this._postProcess=this.options.postProcess},getBounds:function(){var minX=Math.min(this._minPoint.x,this._maxPoint.x),maxX=Math.max(this._minPoint.x,this._maxPoint.x),minY=Math.min(this._minPoint.y,this._maxPoint.y),maxY=Math.max(this._minPoint.y,this._maxPoint.y);return[new L.Point(minX,minY),new L.Point(maxX,maxY)]},setRange:function(minPoint,maxPoint){return minPoint=minPoint instanceof Array?this._arrayToPoint(minPoint):minPoint,maxPoint=maxPoint instanceof Array?this._arrayToPoint(maxPoint):maxPoint,this._minPoint=minPoint,this._maxPoint=maxPoint,this._xRange=maxPoint.x-minPoint.x,this._calculateParameters(minPoint,maxPoint),this},setMin:function(point){return this.setRange(point,this._maxPoint),this},setMax:function(point){return this.setRange(this._minPoint,point),this},setPreProcess:function(preProcess){return this._preProcess=preProcess,this},setPostProcess:function(postProcess){return this._postProcess=postProcess,this},evaluate:function(x){var y;return this._preProcess&&(x=this._preProcess(x)),y=Number((this._slope*x).toFixed(6))+Number(this._b.toFixed(6)),this._postProcess&&(y=this._postProcess(y)),y},random:function(){var randomX=Math.random()*this._xRange+this._minPoint.x;return this.evaluate(randomX)},sample:function(count){count=Math.max(count,2);for(var segmentCount=count-1,segmentSize=this._xRange/segmentCount,x=this._minPoint.x,yValues=[];this._maxPoint.x>=x;)yValues.push(this.evaluate(x)),x+=segmentSize;return yValues}}),L.ColorFunction=L.LinearFunction.extend({options:{alpha:1,includeAlpha:!1},initialize:function(minPoint,maxPoint,options){L.Util.setOptions(this,options),this._parts=[],this._dynamicPart=null,this._outputPrecision=0,this._prefix=null,this._formatOutput=function(y){return y.toFixed(this._outputPrecision)},this._mapOutput=function(parts){for(var outputParts=[],i=0;this._parts.length>i;++i){var part=this._parts[i];outputParts.push(parts[part])}return this.options.includeAlpha&&outputParts.push(this.options.alpha),outputParts},this._getColorString=function(y){y=this._formatOutput(y),this.options[this._dynamicPart]=y;var parts=this._mapOutput(this.options);return this._writeColor(this._prefix,parts)},this._writeColor=function(prefix,parts){return this.options.includeAlpha&&(prefix+="a"),prefix+"("+parts.join(",")+")"};var postProcess=function(y){return options&&options.postProcess&&(y=options.postProcess.call(this,y)),this._getColorString(y)};L.LinearFunction.prototype.initialize.call(this,minPoint,maxPoint,{preProcess:this.options.preProcess,postProcess:postProcess})}}),L.HSLColorFunction=L.ColorFunction.extend({initialize:function(minPoint,maxPoint,options){L.ColorFunction.prototype.initialize.call(this,minPoint,maxPoint,options),this._parts=["outputHue","outputSaturation","outputLuminosity"],this._prefix="hsl",this._outputPrecision=2}}),L.RGBColorFunction=L.ColorFunction.extend({initialize:function(minPoint,maxPoint,options){L.ColorFunction.prototype.initialize.call(this,minPoint,maxPoint,options),this._parts=["outputRed","outputBlue","outputGreen"],this._prefix="rgb",this._outputPrecision=0}}),L.RGBRedFunction=L.LinearFunction.extend({options:{outputGreen:0,outputBlue:0},initialize:function(minPoint,maxPoint,options){L.RGBColorFunction.prototype.initialize.call(this,minPoint,maxPoint,options),this._dynamicPart="outputRed"}}),L.RGBBlueFunction=L.LinearFunction.extend({options:{outputRed:0,outputGreen:0},initialize:function(minPoint,maxPoint,options){L.RGBColorFunction.prototype.initialize.call(this,minPoint,maxPoint,options),this._dynamicPart="outputBlue"}}),L.RGBGreenFunction=L.LinearFunction.extend({options:{outputRed:0,outputBlue:0},initialize:function(minPoint,maxPoint,options){L.RGBColorFunction.prototype.initialize.call(this,minPoint,maxPoint,options),this._dynamicPart="outputGreen"}}),L.RGBColorBlendFunction=L.LinearFunction.extend({initialize:function(minX,maxX,rgbMinColor,rgbMaxColor){var red1=rgbMinColor[0],red2=rgbMaxColor[0],green1=rgbMinColor[1],green2=rgbMaxColor[1],blue1=rgbMinColor[2],blue2=rgbMaxColor[2],postProcess=function(y){return y.toFixed(0)};this._minX=minX,this._maxX=maxX,this._redFunction=new L.LinearFunction(new L.Point(minX,red1),new L.Point(maxX,red2),{postProcess:postProcess}),this._greenFunction=new L.LinearFunction(new L.Point(minX,green1),new L.Point(maxX,green2),{postProcess:postProcess}),this._blueFunction=new L.LinearFunction(new L.Point(minX,blue1),new L.Point(maxX,blue2),{postProcess:postProcess})},getBounds:function(){var redBounds=this._redFunction.getBounds(),greenBounds=this._greenFunction.getBounds(),blueBounds=this._blueFunction.getBounds(),minY=Math.min(redBounds[0].y,greenBounds[0].y,blueBounds[0].y),maxY=Math.max(redBounds[0].y,greenBounds[0].y,blueBounds[0].y);return[new L.Point(redBounds[0].x,minY),new L.Point(redBounds[1].x,maxY)]},evaluate:function(x){return"rgb("+[this._redFunction.evaluate(x),this._greenFunction.evaluate(x),this._blueFunction.evaluate(x)].join(",")+")"}}),L.HSLHueFunction=L.HSLColorFunction.extend({options:{outputSaturation:"100%",outputLuminosity:"50%"},initialize:function(minPoint,maxPoint,options){L.HSLColorFunction.prototype.initialize.call(this,minPoint,maxPoint,options),this._dynamicPart="outputHue"}}),L.HSLSaturationFunction=L.LinearFunction.extend({options:{outputHue:0,outputLuminosity:"50%"},initialize:function(minPoint,maxPoint,options){L.HSLColorFunction.prototype.initialize.call(this,minPoint,maxPoint,options),this._formatOutput=function(y){return(100*y).toFixed(this._outputPrecision)+"%"},this._dynamicPart="outputSaturation"}}),L.HSLLuminosityFunction=L.LinearFunction.extend({options:{outputHue:0,outputSaturation:"100%"},initialize:function(minPoint,maxPoint,options){L.HSLColorFunction.prototype.initialize.call(this,minPoint,maxPoint,options),this._formatOutput=function(y){return(100*y).toFixed(this._outputPrecision)+"%"},this._dynamicPart="outputLuminosity"}}),L.PiecewiseFunction=L.LinearFunction.extend({initialize:function(functions,options){L.Util.setOptions(this,options),this._functions=functions;var startPoint,endPoint;startPoint=functions[0].getBounds()[0],endPoint=functions[functions.length-1].getBounds()[1],L.LinearFunction.prototype.initialize.call(this,startPoint,endPoint,{preProcess:this.options.preProcess,postProcess:this.options.postProcess})},_getFunction:function(x){for(var bounds,startPoint,endPoint,currentFunction,found=!1,index=0;this._functions.length>index;++index)if(currentFunction=this._functions[index],bounds=currentFunction.getBounds(),startPoint=bounds[0],endPoint=bounds[1],x>=startPoint.x&&endPoint.x>x){found=!0;break}return found?currentFunction:this._functions[this._functions.length-1]},evaluate:function(x){var currentFunction,y=null;return this._preProcess&&(x=this._preProcess(x)),currentFunction=this._getFunction(x),currentFunction&&(y=currentFunction.evaluate(x),this._postProcess&&(y=this._postProcess(y))),y}}),L.CategoryFunction=L.Class.extend({initialize:function(categoryMap,options){L.Util.setOptions(this,options),this._categoryKeys=Object.keys(categoryMap),this._categoryMap=categoryMap,this._preProcess=this.options.preProcess,this._postProcess=this.options.postProcess},evaluate:function(x){var y;return this._preProcess&&(x=this._preProcess(x)),y=this._categoryMap[x],this._postProcess&&(y=this._postProcess(y)),y},getCategories:function(){return this._categoryKeys}});var L=L||{},PathFunctions={__updateStyle:L.Path.prototype._updateStyle,_createDefs:function(){this._defs=this._createElement("defs"),this._container.appendChild(this._defs)},_createGradient:function(options){this._defs||this._createDefs();var gradient=this._createElement("linearGradient"),gradientGuid=L.Util.guid();options=options||{x1:"0%",x2:"100%",y1:"0%",y2:"100%"},options.id="grad"+gradientGuid;var stops=[{offset:"0%",style:"stop-color:rgb(255, 255, 255);stop-opacity:1"},{offset:"60%",style:"stop-color:"+(this.options.fillColor||this.options.color)+";stop-opacity:1"}];for(var key in options)gradient.setAttribute(key,options[key]);for(var i=0;stops.length>i;++i){var stop=stops[i],stopElement=this._createElement("stop");for(var key in stop)stopElement.setAttribute(key,stop[key]);gradient.appendChild(stopElement)}this._gradient=gradient,this._defs.appendChild(gradient)},_createDropShadow:function(options){this._defs||this._createDefs();var filterGuid=L.Util.guid(),filter=this._createElement("filter"),feOffset=this._createElement("feOffset"),feGaussianBlur=this._createElement("feGaussianBlur"),feBlend=this._createElement("feBlend");options=options||{width:"200%",height:"200%"},options.id="filter"+filterGuid;for(var key in options)filter.setAttribute(key,options[key]);var offsetOptions={result:"offOut","in":"SourceAlpha",dx:"2",dy:"2"},blurOptions={result:"blurOut","in":"offOut",stdDeviation:"2"},blendOptions={"in":"SourceGraphic",in2:"blurOut",mode:"lighten"};for(var key in offsetOptions)feOffset.setAttribute(key,offsetOptions[key]);for(var key in blurOptions)feGaussianBlur.setAttribute(key,blurOptions[key]);for(var key in blendOptions)feBlend.setAttribute(key,blendOptions[key]);filter.appendChild(feOffset),filter.appendChild(feGaussianBlur),filter.appendChild(feBlend),this._dropShadow=filter,this._defs.appendChild(filter)},_updateStyle:function(){this.__updateStyle.call(this),this.options.gradient&&(this._gradient||this._createGradient(),this._path.setAttribute("fill","url(#"+this._gradient.getAttribute("id")+")")),this.options.dropShadow&&(this._dropShadow||this._createDropShadow(),this._path.setAttribute("filter","url(#"+this._dropShadow.getAttribute("id")+")"))}};L.Path.include(PathFunctions),L.Polygon.include(PathFunctions),L.Polyline.include(PathFunctions),L.CircleMarker.include(PathFunctions),L.MapMarker=L.Path.extend({initialize:function(centerLatLng,options){L.Path.prototype.initialize.call(this,options),this._centerLatLng=centerLatLng},options:{fill:!0,fillOpacity:1,opacity:1,radius:15,innerRadius:5,position:{x:0,y:0},rotation:0,numberOfSides:50,color:"#000000",fillColor:"#0000FF",weight:1,gradient:!0,dropShadow:!0},setLatLng:function(latlng){return this._centerLatLng=latlng,this.redraw()},projectLatlngs:function(){this._point=this._map.latLngToLayerPoint(this._centerLatLng),this._points=this._getPoints(),this.options.innerRadius&&(this._innerPoints=this._getPoints(!0).reverse())},getBounds:function(){var map=this._map,height=3*this.options.radius,point=map.project(this._centerLatLng),swPoint=new L.Point(point.x-this.options.radius,point.y),nePoint=new L.Point(point.x+this.options.radius,point.y-height),sw=map.unproject(swPoint),ne=map.unproject(nePoint);return new L.LatLngBounds(sw,ne)},getLatLng:function(){return this._centerLatLng},getPathString:function(){return this._path.setAttribute("shape-rendering","geometricPrecision"),new L.SVGPathBuilder(this._points,this._innerPoints).toString(6)},_getPoints:function(inner){var newPoint,angleRadians,maxDegrees=inner?360:210,angleSize=inner?maxDegrees/Math.max(this.options.numberOfSides,3):maxDegrees/50,degrees=inner?maxDegrees+this.options.rotation:maxDegrees,angle=inner?this.options.rotation:-30,points=[],radius=this.options.radius,toRad=function(number){return number*Math.PI/180},startPoint=this._point;for(inner||(points.push(startPoint),points.push(new L.Point(startPoint.x+Math.sqrt(.75)*radius,startPoint.y-1.5*radius)));degrees>angle;)angleRadians=toRad(angle),newPoint=this._getPoint(angleRadians,radius,inner),points.push(newPoint),angle+=angleSize;return inner||points.push(new L.Point(startPoint.x-Math.sqrt(.75)*radius,startPoint.y-1.5*radius)),points},_getPoint:function(angle,radius,inner){var markerRadius=radius;return radius=inner?this.options.innerRadius:radius,new L.Point(this._point.x+this.options.position.x+radius*Math.cos(angle),this._point.y-2*markerRadius+this.options.position.y-radius*Math.sin(angle))}}),L.mapMarker=function(centerLatLng,options){return new L.MapMarker(centerLatLng,options)},L.RegularPolygonMarker=L.Path.extend({initialize:function(centerLatLng,options){L.Path.prototype.initialize.call(this,options),this._centerLatLng=centerLatLng,this.options.numberOfSides=Math.max(this.options.numberOfSides,3)},options:{fill:!0,radiusX:10,radiusY:10,rotation:0,numberOfSides:3,position:{x:0,y:0},maxDegrees:360,gradient:!0,dropShadow:!1},setLatLng:function(latlng){return this._centerLatLng=latlng,this.redraw()},projectLatlngs:function(){this._point=this._map.latLngToLayerPoint(this._centerLatLng),this._points=this._getPoints(),(this.options.innerRadius||this.options.innerRadiusX&&this.options.innerRadiusY)&&(this._innerPoints=this._getPoints(!0).reverse())},getBounds:function(){var map=this._map,radiusX=this.options.radius||this.options.radiusX,radiusY=this.options.radius||this.options.radiusY,deltaX=radiusX*Math.cos(Math.PI/4),deltaY=radiusY*Math.sin(Math.PI/4),point=map.project(this._centerLatLng),swPoint=new L.Point(point.x-deltaX,point.y+deltaY),nePoint=new L.Point(point.x+deltaX,point.y-deltaY),sw=map.unproject(swPoint),ne=map.unproject(nePoint);return new L.LatLngBounds(sw,ne)},getLatLng:function(){return this._centerLatLng},getPathString:function(){return this._path.setAttribute("shape-rendering","geometricPrecision"),new L.SVGPathBuilder(this._points,this._innerPoints).toString(6)},_getPoints:function(inner){for(var newPoint,angleRadians,maxDegrees=this.options.maxDegrees||360,angleSize=maxDegrees/Math.max(this.options.numberOfSides,3),degrees=maxDegrees+this.options.rotation,angle=this.options.rotation,points=[],radiusX=inner?this.options.innerRadius||this.options.innerRadiusX:this.options.radius||this.options.radiusX,radiusY=inner?this.options.innerRadius||this.options.innerRadiusY:this.options.radius||this.options.radiusY,toRad=function(number){return number*Math.PI/180};degrees>angle;)angleRadians=toRad(angle),newPoint=this._getPoint(angleRadians,radiusX,radiusY),points.push(newPoint),angle+=angleSize;return points},_getPoint:function(angle,radiusX,radiusY){return new L.Point(this._point.x+this.options.position.x+radiusX*Math.cos(angle),this._point.y+this.options.position.y+radiusY*Math.sin(angle))}}),L.regularPolygonMarker=function(centerLatLng,options){return new L.RegularPolygonMarker(centerLatLng,options)},L.StarMarker=L.RegularPolygonMarker.extend({options:{numberOfPoints:5,rotation:-15,maxDegrees:360,gradient:!0,dropShadow:!0},_getPoints:function(inner){for(var newPoint,newPointInner,angleRadians,maxDegrees=this.options.maxDegrees||360,angleSize=maxDegrees/this.options.numberOfPoints,degrees=maxDegrees+this.options.rotation,angle=this.options.rotation,points=[],radiusX=inner?this.options.innerRadius||this.options.innerRadiusX:this.options.radius||this.options.radiusX,radiusY=inner?this.options.innerRadius||this.options.innerRadiusY:this.options.radius||this.options.radiusY,toRad=function(number){return number*Math.PI/180};degrees>angle;)angleRadians=toRad(angle),newPoint=this._getPoint(angleRadians,radiusX,radiusY),newPointInner=this._getPoint(angleRadians+toRad(angleSize)/2,radiusX/2,radiusY/2),points.push(newPoint),points.push(newPointInner),angle+=angleSize;return points}}),L.starMarker=function(centerLatLng,options){return new L.StarMarker(centerLatLng,options)},L.TriangleMarker=L.RegularPolygonMarker.extend({options:{numberOfSides:3,rotation:30,radius:5}}),L.triangleMarker=function(centerLatLng,options){return new L.TriangleMarker(centerLatLng,options)},L.DiamondMarker=L.RegularPolygonMarker.extend({options:{numberOfSides:4,radiusX:5,radiusY:10}}),L.diamondMarker=function(centerLatLng,options){return new L.DiamondMarker(centerLatLng,options)},L.SquareMarker=L.RegularPolygonMarker.extend({options:{numberOfSides:4,rotation:45,radius:5}}),L.squareMarker=function(centerLatLng,options){return new L.SquareMarker(centerLatLng,options)},L.PentagonMarker=L.RegularPolygonMarker.extend({options:{numberOfSides:5,rotation:-18,radius:5}}),L.pentagonMarker=function(centerLatLng,options){return new L.PentagonMarker(centerLatLng,options)},L.HexagonMarker=L.RegularPolygonMarker.extend({options:{numberOfSides:6,rotation:30,radius:5}}),L.hexagonMarker=function(centerLatLng,options){return new L.HexagonMarker(centerLatLng,options)},L.OctagonMarker=L.RegularPolygonMarker.extend({options:{numberOfSides:8,rotation:22.5,radius:5}}),L.octagonMarker=function(centerLatLng,options){return new L.OctagonMarker(centerLatLng,options)};var L=L||{};L.BarMarker=L.Path.extend({initialize:function(centerLatLng,options){L.Path.prototype.initialize.call(this,options),this._centerLatLng=centerLatLng},options:{fill:!0,width:2,maxHeight:10,position:{x:0,y:0},weight:1,color:"#000",opacity:1,gradient:!0,dropShadow:!1},setLatLng:function(latlng){return this._centerLatLng=latlng,this.redraw()},projectLatlngs:function(){this._point=this._map.latLngToLayerPoint(this._centerLatLng),this._points=this._getPoints()},getBounds:function(){var map=this._map,point=map.project(this._centerLatLng),halfWidth=this.options.width/2,swPoint=new L.Point(point.x-halfWidth,point.y),nePoint=new L.Point(point.x+halfWidth,point.y-this.options.maxHeight),sw=map.unproject(swPoint),ne=map.unproject(nePoint);return new L.LatLngBounds(sw,ne)},getLatLng:function(){return this._centerLatlng},getPathString:function(){return this._path.setAttribute("shape-rendering","crispEdges"),""+new L.SVGPathBuilder(this._points)},_getPoints:function(){var sePoint,nePoint,nwPoint,swPoint,points=[],startX=this._point.x+this.options.position.x,startY=this._point.y+this.options.position.y,halfWidth=this.options.width/2,height=this.options.value/this.options.maxValue*this.options.maxHeight;return sePoint=new L.Point(startX+halfWidth,startY),nePoint=new L.Point(startX+halfWidth,startY-height),nwPoint=new L.Point(startX-halfWidth,startY-height),swPoint=new L.Point(startX-halfWidth,startY),points=[sePoint,nePoint,nwPoint,swPoint]}}),L.barMarker=function(centerLatLng,options){return new L.BarMarker(centerLatLng,options)},L.ChartMarker=L.FeatureGroup.extend({initialize:function(centerLatLng,options){L.Util.setOptions(this,options),this._layers={},this._centerLatLng=centerLatLng,this._loadBars()},setLatLng:function(latlng){return this._centerLatLng=latlng,this.redraw() -},getLatLng:function(){return this._centerLatlng},_loadBars:function(){},_highlight:function(options){return options.weight&&(options.weight*=2),options},_unhighlight:function(options){return options.weight&&(options.weight/=2),options},_bindMouseEvents:function(chartElement){var self=this,tooltipOptions=this.options.tooltipOptions;chartElement.on("mouseover",function(e){var newPoint,currentOptions=this.options,key=currentOptions.key,value=currentOptions.value,layerPoint=e.layerPoint,x=layerPoint.x-this._point.x,y=layerPoint.y-this._point.y,iconSize=currentOptions.iconSize,newX=x,newY=y,offset=5;newX=0>x?iconSize.x-x+offset:-x-offset,newY=0>y?iconSize.y-y+offset:-y-offset,newPoint=new L.Point(newX,newY);var legendOptions={},displayText=currentOptions.displayText?currentOptions.displayText(value):value;legendOptions[key]={name:currentOptions.displayName,value:displayText};var icon=new L.LegendIcon(legendOptions,currentOptions,{className:"leaflet-div-icon",iconSize:tooltipOptions?tooltipOptions.iconSize:iconSize,iconAnchor:newPoint});currentOptions.marker=new L.Marker(self._centerLatLng,{icon:icon}),currentOptions=self._highlight(currentOptions),this.initialize(self._centerLatLng,currentOptions),this.redraw(),this.setStyle(currentOptions),self.addLayer(currentOptions.marker)}),chartElement.on("mouseout",function(){var currentOptions=this.options;currentOptions=self._unhighlight(currentOptions),this.initialize(self._centerLatLng,currentOptions),this.redraw(),this.setStyle(currentOptions),self.removeLayer(currentOptions.marker)})},bindPopup:function(content,options){this.eachLayer(function(layer){layer.bindPopup(content,options)})}}),L.BarChartMarker=L.ChartMarker.extend({initialize:function(centerLatLng,options){L.Util.setOptions(this,options),L.ChartMarker.prototype.initialize.call(this,centerLatLng,options)},options:{weight:1,opacity:1,color:"#000",fill:!0,position:{x:0,y:0},width:10,offset:0,iconSize:new L.Point(50,40)},_loadBars:function(){var value,minValue,maxValue;this.options.rotation,this.options.maxDegrees||360;var bar,options=this.options;this.options.radiusX||this.options.radius,this.options.radiusY||this.options.radius;var x,y,chartOption,keys=Object.keys(this.options.data),count=keys.length,width=this.options.width,offset=this.options.offset||0,data=this.options.data,chartOptions=this.options.chartOptions;x=-(width*count+offset*(count-1))/2+width/2,y=0;for(var key in data)value=data[key],chartOption=chartOptions[key],minValue=chartOption.minValue||0,maxValue=chartOption.maxValue||100,options.fillColor=chartOption.fillColor||this.options.fillColor,options.value=value,options.minValue=minValue,options.maxValue=maxValue,options.position={x:x,y:y},options.width=width,options.maxHeight=chartOption.maxHeight||10,options.key=key,options.value=value,options.displayName=chartOption.displayName,options.opacity=this.options.opacity||1,options.fillOpacity=this.options.fillOpacity||.7,options.weight=this.options.weight||1,options.color=chartOption.color||this.options.color,options.displayText=chartOption.displayText,bar=new L.BarMarker(this._centerLatLng,options),this._bindMouseEvents(bar),this.addLayer(bar),x+=width+offset}}),L.RadialBarMarker=L.Path.extend({initialize:function(centerLatLng,options){L.Path.prototype.initialize.call(this,options),this._centerLatLng=centerLatLng},options:{fill:!0,radius:10,rotation:0,numberOfSides:30,position:{x:0,y:0},gradient:!0,dropShadow:!1},setLatLng:function(latlng){return this._centerLatLng=latlng,this.redraw()},projectLatlngs:function(){this._point=this._map.latLngToLayerPoint(this._centerLatLng),this._points=this._getPoints()},getBounds:function(){var map=this._map,radiusX=this.options.radiusX||this.options.radius,radiusY=this.options.radiusY||this.options.radius,deltaX=radiusX*Math.cos(Math.PI/4),deltaY=radiusY*Math.sin(Math.PI/4),point=map.project(this._centerLatLng),swPoint=new L.Point(point.x-deltaX,point.y+deltaY),nePoint=new L.Point(point.x+deltaX,point.y-deltaY),sw=map.unproject(swPoint),ne=map.unproject(nePoint);return new L.LatLngBounds(sw,ne)},getLatLng:function(){return this._centerLatlng},getPathString:function(){var angle=this.options.endAngle-this.options.startAngle,largeArc=angle>=180?"1":"0",radiusX=this.options.radiusX||this.options.radius,radiusY=this.options.radiusY||this.options.radius,path="M"+this._points[0].x.toFixed(2)+","+this._points[0].y.toFixed(2)+"A"+radiusX.toFixed(2)+","+radiusY.toFixed(2)+" 0 "+largeArc+",1 "+this._points[1].x.toFixed(2)+","+this._points[1].y.toFixed(2)+"L";return this._innerPoints?(path=path+this._innerPoints[0].x.toFixed(2)+","+this._innerPoints[0].y.toFixed(2),path=path+"A"+(radiusX-this.options.barThickness).toFixed(2)+","+(radiusY-this.options.barThickness).toFixed(2)+" 0 "+largeArc+",0 "+this._innerPoints[1].x.toFixed(2)+","+this._innerPoints[1].y.toFixed(2)+"z"):path=path+this._point.x.toFixed(2)+","+this._point.y.toFixed(2)+"z",this._path.setAttribute("shape-rendering","geometricPrecision"),path},_getPoints:function(){var angleDelta=this.options.endAngle-this.options.startAngle;angleDelta/this.options.numberOfSides;var degrees=this.options.endAngle+this.options.rotation,angle=this.options.startAngle+this.options.rotation,points=[],radiusX="radiusX"in this.options?this.options.radiusX:this.options.radius,radiusY="radiusY"in this.options?this.options.radiusY:this.options.radius,toRad=function(number){return number*Math.PI/180};360===angleDelta&&(degrees-=.1);var startRadians=toRad(angle),endRadians=toRad(degrees);return points.push(this._getPoint(startRadians,radiusX,radiusY)),points.push(this._getPoint(endRadians,radiusX,radiusY)),this.options.barThickness&&(this._innerPoints=[],radiusX-this.options.barThickness,radiusY-this.options.barThickness,this._innerPoints.push(this._getPoint(endRadians,radiusX-this.options.barThickness,radiusY-this.options.barThickness)),this._innerPoints.push(this._getPoint(startRadians,radiusX-this.options.barThickness,radiusY-this.options.barThickness))),points},_getPoint:function(angle,radiusX,radiusY){return new L.Point(this._point.x+this.options.position.x+radiusX*Math.cos(angle),this._point.y+this.options.position.y+radiusY*Math.sin(angle))}}),L.radialBarMarker=function(centerLatLng,options){return new L.RadialBarMarker(centerLatLng,options)},L.PieChartMarker=L.ChartMarker.extend({initialize:function(centerLatLng,options){L.Util.setOptions(this,options),L.ChartMarker.prototype.initialize.call(this,centerLatLng,options)},options:{weight:1,opacity:1,color:"#000",fill:!0,radius:10,rotation:0,numberOfSides:50,mouseOverExaggeration:1.2,maxDegrees:360,iconSize:new L.Point(50,40)},_highlight:function(options){var oldRadiusX=options.radiusX,oldBarThickness=options.barThickness;return options.oldBarThickness=oldBarThickness,options.radiusX*=options.mouseOverExaggeration,options.radiusY*=options.mouseOverExaggeration,options.barThickness=options.radiusX-oldRadiusX+oldBarThickness,options},_unhighlight:function(options){return options.radiusX/=options.mouseOverExaggeration,options.radiusY/=options.mouseOverExaggeration,options.barThickness=options.oldBarThickness,options},_loadBars:function(){var value,bar,chartOption,key,sum=0,angle=0,percentage=0,maxDegrees=this.options.maxDegrees||360,lastAngle=this.options.rotation,options=this.options,data=this.options.data,chartOptions=this.options.chartOptions,getValue=function(data,key){var value=0;return data[key]&&(value=parseFloat(data[key])),value};for(key in data)value=getValue(data,key),sum+=value;if(sum>0)for(key in data)value=parseFloat(data[key]),chartOption=chartOptions[key],percentage=value/sum,angle=percentage*maxDegrees,options.startAngle=lastAngle,options.endAngle=lastAngle+angle,options.fillColor=chartOption.fillColor,options.color=chartOption.color||"#000",options.radiusX=this.options.radiusX||this.options.radius,options.radiusY=this.options.radiusY||this.options.radius,options.rotation=0,options.key=key,options.value=value,options.displayName=chartOption.displayName,options.displayText=chartOption.displayText,bar=new L.RadialBarMarker(this._centerLatLng,options),this._bindMouseEvents(bar),lastAngle=options.endAngle,this.addLayer(bar)}}),L.pieChartMarker=function(centerLatLng,options){return new L.PieChartMarker(centerLatLng,options)},L.CoxcombChartMarker=L.PieChartMarker.extend({initialize:function(centerLatLng,options){L.Util.setOptions(this,options),L.PieChartMarker.prototype.initialize.call(this,centerLatLng,options)},options:{weight:1,opacity:1,color:"#000",fill:!0,radius:10,rotation:0,numberOfSides:50,mouseOverExaggeration:1.2,maxDegrees:360,iconSize:new L.Point(50,40)},_loadBars:function(){var value,minValue,maxValue,bar,chartOption,angle=0,maxDegrees=this.options.maxDegrees||360,lastAngle=this.options.rotation,options=this.options,radiusX="radiusX"in this.options?this.options.radiusX:this.options.radius,radiusY="radiusY"in this.options?this.options.radiusY:this.options.radius,keys=Object.keys(this.options.data),count=keys.length,data=this.options.data,chartOptions=this.options.chartOptions;angle=maxDegrees/count;for(var key in data){value=parseFloat(data[key]),chartOption=chartOptions[key];var minValue=chartOption.minValue||0,maxValue=chartOption.maxValue,evalFunctionX=new L.LinearFunction(new L.Point(minValue,0),new L.Point(maxValue,radiusX)),evalFunctionY=new L.LinearFunction(new L.Point(minValue,0),new L.Point(maxValue,radiusY));options.startAngle=lastAngle,options.endAngle=lastAngle+angle,options.fillColor=chartOption.fillColor,options.color=chartOption.color||"#000",options.radiusX=evalFunctionX.evaluate(value),options.radiusY=evalFunctionY.evaluate(value),options.rotation=0,options.key=key,options.value=value,options.displayName=chartOption.displayName,options.displayText=chartOption.displayText,bar=new L.RadialBarMarker(this._centerLatLng,options),this._bindMouseEvents(bar),lastAngle=options.endAngle,this.addLayer(bar)}}}),L.coxcombChartMarker=function(centerLatLng,options){return new L.CoxcombChartMarker(centerLatLng,options)},L.RadialBarChartMarker=L.ChartMarker.extend({initialize:function(centerLatLng,options){L.Util.setOptions(this,options),L.ChartMarker.prototype.initialize.call(this,centerLatLng,options)},options:{weight:1,opacity:1,color:"#000",fill:!0,radius:10,rotation:0,numberOfSides:30,offset:2,barThickness:5,maxDegrees:360,iconSize:new L.Point(50,40)},_loadBars:function(){var value,minValue,maxValue,bar,chartOption,angle=this.options.rotation,maxDegrees=this.options.maxDegrees||360,options=this.options,lastRadiusX=this.options.radiusX||this.options.radius,lastRadiusY=this.options.radiusY||this.options.radius,data=this.options.data,chartOptions=this.options.chartOptions,barThickness=this.options.barThickness||4,offset=this.options.offset||2;for(var key in data){value=parseFloat(data[key]),chartOption=chartOptions[key],minValue=chartOption.minValue||0,maxValue=chartOption.maxValue||100;var angleFunction=new L.LinearFunction(new L.Point(minValue,0),new L.Point(maxValue,maxDegrees));angle=angleFunction.evaluate(value),options.startAngle=this.options.rotation,options.endAngle=this.options.rotation+angle,options.fillColor=chartOption.fillColor,options.radiusX=lastRadiusX,options.radiusY=lastRadiusY,options.barThickness=barThickness,options.rotation=0,options.key=key,options.value=value,options.displayName=chartOption.displayName,options.displayText=chartOption.displayText,options.weight=this.options.weight||1,bar=new L.RadialBarMarker(this._centerLatLng,options),this._bindMouseEvents(bar),this.addLayer(bar),lastRadiusX+=barThickness+offset,lastRadiusY+=barThickness+offset}}}),L.radialBarChartMarker=function(centerLatLng,options){return new L.RadialBarChartMarker(centerLatLng,options)},L.StackedRegularPolygonMarker=L.ChartMarker.extend({options:{iconSize:new L.Point(50,40)},initialize:function(centerLatLng,options){L.Util.setOptions(this,options),L.ChartMarker.prototype.initialize.call(this,centerLatLng,options)},_loadBars:function(){var value;this.options.maxDegrees||360;var bar,chartOption,key,lastRadiusX=0,lastRadiusY=0,options=this.options,data=this.options.data,chartOptions=this.options.chartOptions;for(key in data){value=parseFloat(data[key]),chartOption=chartOptions[key],minValue=chartOption.minValue||0,maxValue=chartOption.maxValue||100,minRadius=chartOption.minRadius||0,maxRadius=chartOption.maxRadius||10,options.fillColor=chartOption.fillColor||this.options.fillColor,options.value=value,options.minValue=minValue,options.maxValue=maxValue;var evalFunction=new L.LinearFunction(new L.Point(minValue,minRadius),new L.Point(maxValue,maxRadius)),barThickness=evalFunction.evaluate(value);options.radiusX=lastRadiusX+barThickness,options.radiusY=lastRadiusY+barThickness,options.innerRadiusX=lastRadiusX,options.innerRadiusY=lastRadiusY,options.key=key,options.displayName=chartOption.displayName,options.opacity=this.options.opacity||1,options.fillOpacity=this.options.fillOpacity||.7,options.weight=this.options.weight||1,options.color=chartOption.color||this.options.color,options.displayText=chartOption.displayText,bar=new L.RegularPolygonMarker(this._centerLatLng,options),this._bindMouseEvents(bar),lastRadiusX=options.radiusX,lastRadiusY=options.radiusY,this.addLayer(bar)}}}),L.RadialMeterMarker=L.ChartMarker.extend({initialize:function(centerLatLng,options){L.Util.setOptions(this,options),L.ChartMarker.prototype.initialize.call(this,centerLatLng,options)},options:{weight:1,opacity:1,color:"#000",fill:!0,radius:10,rotation:180,numberOfSides:30,offset:2,barThickness:5,maxDegrees:180,iconSize:new L.Point(50,40)},_loadBars:function(){var value,minValue,maxValue,bar,chartOption,startAngle=this.options.rotation,maxDegrees=this.options.maxDegrees||360,options=this.options,radiusX=this.options.radiusX||this.options.radius,radiusY=this.options.radiusY||this.options.radius,data=this.options.data,chartOptions=this.options.chartOptions,barThickness=this.options.barThickness||4;this.options.offset||2;var displayOptions,lastAngle=startAngle,numSegments=this.options.numSegments||10,angleDelta=maxDegrees/numSegments;for(var key in data){value=parseFloat(data[key]),chartOption=chartOptions[key],displayOptions=this.options.displayOptions?this.options.displayOptions[key]:{},minValue=chartOption.minValue||0,maxValue=chartOption.maxValue||100;for(var range=maxValue-minValue,angle=maxDegrees/range*(value-minValue),endAngle=startAngle+angle,evalFunction=new L.LinearFunction(new L.Point(startAngle,minValue),new L.Point(startAngle+maxDegrees,maxValue));endAngle>lastAngle;){options.startAngle=lastAngle;var delta=Math.min(angleDelta,endAngle-lastAngle);options.endAngle=lastAngle+delta,options.fillColor=chartOption.fillColor,options.radiusX=radiusX,options.radiusY=radiusY,options.barThickness=barThickness,options.rotation=0,options.key=key,options.value=value,options.displayName=chartOption.displayName,options.displayText=chartOption.displayText;var evalValue=evalFunction.evaluate(lastAngle+delta);for(var displayKey in displayOptions)options[displayKey]=displayOptions[displayKey].evaluate?displayOptions[displayKey].evaluate(evalValue):displayOptions[displayKey];bar=new L.RadialBarMarker(this._centerLatLng,options),this._bindMouseEvents(bar),this.addLayer(bar),lastAngle+=delta}}}}); \ No newline at end of file diff --git a/folium/plugins/plugin.py b/folium/plugins/plugin.py deleted file mode 100644 index 088bce39f7..0000000000 --- a/folium/plugins/plugin.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Plugin ------- - -A generic class for creating plugins. -Basic plugin object that does nothing. -Other plugins may inherit from this one. -""" -from uuid import uuid4 - -from jinja2 import Environment, PackageLoader -ENV = Environment(loader=PackageLoader('folium', 'plugins')) - -class Plugin(object): - """Basic plugin object that does nothing. - Other plugins may inherit from this one.""" - def __init__(self): - """Creates a plugin to append into a map with Map.add_plugin. """ - self.plugin_name = 'Plugin' - self.object_name = uuid4().hex - self.env = ENV - - def add_to_map(self, map): - """Adds the plugin on a folium.map object.""" - map.plugins.setdefault(self.plugin_name,[]).append(self) - self.map = map - - def render_html(self, nb): - """Generates the HTML part of the plugin.""" - return "" - - def render_css(self, nb): - """Generates the CSS part of the plugin.""" - return "" - - def render_js(self, nb): - """Generates the Javascript part of the plugin.""" - return "" - def render_header(self, nb): - """Generates the Header part of the plugin.""" - return "" \ No newline at end of file diff --git a/folium/plugins/template_plugin.py b/folium/plugins/template_plugin.py deleted file mode 100644 index c326ae7e55..0000000000 --- a/folium/plugins/template_plugin.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Template Plugin - -A generic class to create plugins based on jinja2 templates. -""" -from .plugin import Plugin -from jinja2 import Template -from uuid import uuid4 - - -class TemplatePlugin(Plugin): - """Generates a plugin out of a jinja2 template.""" - def __init__(self): - """Creates a TemplatePlugin plugin to append into a map with - Map.add_plugin. - - Parameters - ---------- - template: jinja2.Template - The template that will be used to generate the plugin. - """ - super(TemplatePlugin, self).__init__() - assert 'template' in dir(self), 'template attibute does not exist ; you have to define one.' - self.template = self.template if self.template.__class__ is Template else Template(self.template) - self.plugin_name = self.template.module.__dict__.get('plugin_name','Unknown_'+uuid4().hex) - - def render_header(self, nb): - """Generates the header part of the plugin.""" - header = self.template.module.__dict__.get('header',None) - if header is None: - return super(TemplatePlugin, self).render_header(nb) - else: - return header(nb) - - def render_css(self, nb): - """Generates the CSS part of the plugin.""" - css = self.template.module.__dict__.get('css',None) - if css is None: - return super(TemplatePlugin, self).render_css(nb) - else: - return css(nb) - - def render_html(self, nb): - """Generates the HTML part of the plugin.""" - html = self.template.module.__dict__.get('html',None) - if html is None: - return super(TemplatePlugin, self).render_html(nb) - else: - return html(nb) - - def render_js(self, nb): - """Generates the Javascript part of the plugin.""" - js = self.template.module.__dict__.get('js',None) - if js is None: - return super(TemplatePlugin, self).render_js(nb) - else: - return js(nb) diff --git a/folium/plugins/timestamped_geo_json.tpl b/folium/plugins/timestamped_geo_json.tpl deleted file mode 100644 index e63c821963..0000000000 --- a/folium/plugins/timestamped_geo_json.tpl +++ /dev/null @@ -1,31 +0,0 @@ -{% macro header(nb) %} - {% if nb==0 %} - - - - - - - - - - - - - {% endif %} -{% endmacro %} - -{% macro js(nb,self) %} - {% if nb==0 %} - map.timeDimension = L.timeDimension(); - map.timeDimensionControl = L.control.timeDimension({ - position: 'bottomleft', - autoPlay: {{'true' if self.auto_play else 'false'}}, - playerOptions: {transitionTime: {{self.transition_time}},loop: {{'true' if self.loop else 'false'}}} - }); - map.addControl(map.timeDimensionControl); - {% endif %} - - var tsgeojson_{{nb}} = L.timeDimension.layer.geoJson(L.geoJson({{self.data}}), - {updateTimeDimension: true,addlastPoint: true}).addTo(map); -{% endmacro %} \ No newline at end of file