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), diff --git a/folium/features.py b/folium/features.py index 07748bfe61..830c771fb8 100644 --- a/folium/features.py +++ b/folium/features.py @@ -7,8 +7,11 @@ """ from jinja2 import Template import json +import base64 -from .utilities import color_brewer, _parse_size +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 @@ -40,8 +43,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()}}); @@ -49,7 +52,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 +60,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 +68,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) %} @@ -119,9 +123,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(""" @@ -197,8 +199,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=0): """TODO : docstring here. """ super(GeoJsonStyle, self).__init__() @@ -209,30 +249,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 %} @@ -248,6 +303,33 @@ 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 = self._env.get_template('color_scale.js') + + 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): @@ -288,3 +370,243 @@ 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 %} + """) + +class CircleMarker(MacroElement): + def __init__(self, location, radius=500, color='black', + fill_color='black', fill_opacity=0.6, popup=None): + """TODO : docstring here + """ + 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 %} + """) + +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 %} + """) + +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 %} + """) + +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. + """ + super(PolyLine, self).__init__() + self._name = 'PolyLine' + 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.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 %} + """) + +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 %} + """) + +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, + base64.b64encode(image.read()).decode('utf-8')) + elif (not (isinstance(image,text_type) or isinstance(image,binary_type))) and hasattr(image,'__iter__'): + # We got an array-like object + if mercator_project: + data = mercator_transform(image, + [bounds[0][0], bounds[1][0]], + origin=origin) + else: + data = image + self.url = "data:image/png;base64," +\ + base64.b64encode(write_png(data, origin=origin, colormap=colormap)).decode('utf-8') + else: + # We got an url + self.url = json.loads(json.dumps(image)) + + self.url = 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 83c9f0377b..f03de946e5 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -8,247 +8,28 @@ """ from __future__ import absolute_import -from __future__ import print_function -from __future__ import division -import codecs -import functools +import warnings 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 - -import sys -import base64 - -ENV = Environment(loader=PackageLoader('folium', 'templates')) +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, FitBounds +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 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 + +class Map(_Map): + """This class inherits from the map.Map object in order to provide bindings to + former folium API. + """ 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,39 +41,13 @@ 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) + 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) - 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): @@ -327,38 +82,23 @@ def simple_marker(self, location=None, popup=None, >>>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) + warnings.warn("%s is deprecated. Use %s instead" % ("simple_marker", "add_children(Marker)"), + FutureWarning, stacklevel=2) if clustered_marker: - add_mark = 'clusteredmarkers.addLayer(marker_{0})'.format(count) - name = 'cluster_markers' + 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: - 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) + popup_ = None + marker = Marker(location, + popup=popup_, + icon=Icon(color=marker_color, icon=marker_icon, angle=icon_angle)) + self.add_children(marker) - @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 @@ -393,39 +133,17 @@ def div_markers(self, locations=None, popups=None, ... '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') + 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) + def line(self, locations, line_color=None, line_opacity=None, line_weight=None, - popup=None, popup_width=300): + popup=None, popup_width=300, latlon=True): """Add a line to the map with optional styles. Parameters @@ -440,6 +158,10 @@ def line(self, locations, 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. @@ -451,28 +173,22 @@ def line(self, locations, line_color='red', line_opacity=1.0) """ - count = self.mark_cnt['line'] - line_temp = self.env.get_template('polyline.js') + p = PolyLine(locations, + color=line_color, + weight=line_weight, + opacity=line_opacity, + latlon=latlon, + ) - polyline_opts = {'color': line_color, 'weight': line_weight, - 'opacity': line_opacity} + if popup is not None: + p.add_children(Popup(popup, max_width=popup_width)) - varname = 'line_{}'.format(count) - line_rendered = line_temp.render({'line': varname, - 'locations': locations, - 'options': polyline_opts}) + self.add_children(p) - 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): + 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 @@ -485,6 +201,15 @@ def multiline(self, locations, line_color=None, line_opacity=None, 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. @@ -502,23 +227,18 @@ def multiline(self, locations, line_color=None, line_opacity=None, line_opacity=1.0) """ - count = self.mark_cnt['multiline'] - - multiline_temp = self.env.get_template('multi_polyline.js') + p = MultiPolyLine(locations, + color=line_color, + weight=line_weight, + opacity=line_opacity, + latlon=latlon, + ) - multiline_opts = {'color': line_color, 'weight': line_weight, - 'opacity': line_opacity} + if popup is not None: + p.add_children(Popup(popup, max_width=popup_width)) - varname = 'multiline_{}'.format(count) - multiline_rendered = multiline_temp.render({'multiline': varname, - 'locations': locations, - 'options': multiline_opts}) + self.add_children(p) - 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): @@ -555,27 +275,25 @@ def circle_marker(self, location=None, radius=500, popup=None, 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)) + 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, num_sides=4, rotation=0, radius=15, popup=None, @@ -614,45 +332,30 @@ def polygon_marker(self, location=None, line_color='black', line_opacity=1, 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}) + 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.""" - - latlng_temp = self.env.get_template('lat_lng_popover.js') - self.template_vars.update({'lat_lng_pop': latlng_temp.render()}) + 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 @@ -669,14 +372,10 @@ def click_for_marker(self, popup=None): >>>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}) + 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): @@ -703,24 +402,13 @@ def fit_bounds(self, bounds, padding_top_left=None, >>> 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}) + 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. @@ -731,99 +419,33 @@ 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. + 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()}) - 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', @@ -910,126 +532,78 @@ def geo_json(self, geo_path=None, geo_str=None, data_out='data.json', 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) + + 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.') - 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. + # Create GeoJson object if geo_path: - geo_path = ".defer(d3.json, '{0}')".format(geo_path) + geo_data = open(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 + geo_data = geo_str 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.') + geo_data = {} - 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) + 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: - style = json_style(style_count, line_color, line_weight, - line_opacity, fill_color, fill_opacity, None) + color_data = None + + # Compute color_domain + if threshold_scale: + 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'" + " 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)] + else: + color_domain = [-1,1] + + # 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) - layer = ('gJson_layer_{0} = L.geoJson({1}, {{style: {2},' - 'onEachFeature: onEachFeature}}).addTo(map)' - .format(self.mark_cnt['geojson'], layer_var, style_count)) + # Create ColorScale + color_scale = ColorScale(color_domain, fill_color, caption=legend_name) - 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) + 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, - filename=None): + 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 @@ -1059,7 +633,18 @@ def image_overlay(self, data, opacity=0.25, min_lat=-90.0, max_lat=90.0, 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 @@ -1081,115 +666,11 @@ def image_overlay(self, data, opacity=0.25, min_lat=-90.0, max_lat=90.0, ... 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_())) + 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)) diff --git a/folium/map.py b/folium/map.py index ff2fd017bb..89dba89f1b 100644 --- a/folium/map.py +++ b/folium/map.py @@ -6,6 +6,8 @@ Classes for drawing maps. """ import warnings +import json +from collections import OrderedDict from jinja2 import Template @@ -85,6 +87,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) @@ -108,6 +112,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) %} @@ -169,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 ---------- @@ -181,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' @@ -197,6 +204,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""" @@ -213,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""" @@ -265,6 +310,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) %} @@ -292,7 +341,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))) @@ -324,3 +373,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/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/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/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/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/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/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/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/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/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/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 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/folium/templates/color_scale.js b/folium/templates/color_scale.js new file mode 100644 index 0000000000..58f01b5c3c --- /dev/null +++ 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 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/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/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/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/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/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/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/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/folium/templates/simple_popup.js b/folium/templates/simple_popup.js index 8ee17a5f9f..c2dfc8629d 100644 --- a/folium/templates/simple_popup.js +++ b/folium/templates/simple_popup.js @@ -1,2 +1,4 @@ -{{ 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/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/folium/utilities.py b/folium/utilities.py index 957c3a525a..fdab640b2b 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. + """ + 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): """ - Format a numpy array as a PNG byte string. + 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(list(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', @@ -408,3 +468,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 f873f0a604..59609c3c56 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -15,8 +15,13 @@ 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 +from folium.map import Popup, Marker, Icon, FitBounds +from folium.features import DivIcon, CircleMarker, LatLngPopup, GeoJson,\ + GeoJsonStyle, ColorScale, TopoJson, PolyLine, MultiPolyLine, ImageOverlay rootpath = os.path.abspath(os.path.dirname(__file__)) @@ -52,7 +57,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 +66,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 +98,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 +108,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 +124,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,127 +143,139 @@ 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.""" + 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') # 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 = list(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 = 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, - '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 list(self.map._children.values())[-1]._children.values(): + assert not isinstance(child, Popup) 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 = 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) + 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): """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 = list(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 = list(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.""" + 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 = list(self.map._children.values())[-1] + polygon = poly_temp.render({'marker': marker.get_name(), 'lat': 45.5, 'lon': -122.5, 'line_color': 'black', @@ -270,75 +287,101 @@ 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.""" 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 = 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()) 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.""" + 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 - - def test_geo_json(self): - """Test geojson method.""" - path = os.path.join(rootpath, 'us-counties.json') - geo_path = ".defer(d3.json, '{0}')".format(path) + 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(), + '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_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') self.map.geo_json(geo_path=path) - geo_path = ".defer(d3.json, '{0}')".format(path) - map_var = 'gjson_1' - layer_var = 'gjson_1' - style_temp = self.env.get_template('geojson_style.js') - style = style_temp.render({'style': 'style_1', - '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 + + 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 + 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 + 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.""" + + 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() @@ -347,6 +390,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): @@ -356,84 +406,104 @@ 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.""" + + 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.""" + + self.map = folium.Map([43, -100], zoom_start=4) # 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 + 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() + + # 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.""" # 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 @@ -445,14 +515,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.""" @@ -472,7 +542,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.""" @@ -489,15 +559,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.""" @@ -510,26 +587,44 @@ 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.""" 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({ @@ -537,61 +632,56 @@ def test_fit_bounds(self): 'fit_bounds_options': json.dumps({'maxZoom': 15, 'padding': (3, 3), }, sort_keys=True), - }) + 'this' : fitbounds, + }) - self.map.fit_bounds(bounds, max_zoom=15, padding=(3, 3)) - - 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 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')) 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'] - - 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() + assert ''.join(image_rendered.split()) in ''.join(out.split()) diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 363d90f61f..5cfd7aeca2 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 @@ -28,89 +28,36 @@ 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) - 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) - 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) - 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() - - 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() + 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_timestamped_geo_json(self): coordinates = [[[[lon-8*np.sin(theta), -47+6*np.cos(theta)] for @@ -185,5 +132,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_()