From d660a52e0728f02f227d091bcc9e27f7a3e7dfe3 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Mon, 21 Dec 2015 16:06:33 +0100 Subject: [PATCH 1/7] Set get_bounds method --- folium/element.py | 28 +++++++++++++++++++++++++++- folium/features.py | 25 +++++++++++++++++++++++-- folium/map.py | 17 +++++++++++------ folium/utilities.py | 16 ++++++++++++++++ 4 files changed, 77 insertions(+), 9 deletions(-) diff --git a/folium/element.py b/folium/element.py index 6954e532f8..cb6f848560 100644 --- a/folium/element.py +++ b/folium/element.py @@ -13,7 +13,7 @@ import base64 from .six import urlopen, text_type, binary_type -from .utilities import _camelify, _parse_size +from .utilities import _camelify, _parse_size, none_min, none_max ENV = Environment(loader=PackageLoader('folium', 'templates')) @@ -40,6 +40,32 @@ def __init__(self, template=None, template_name=None): def get_name(self): return _camelify(self._name) + '_' + self._id + def _get_self_bounds(self): + """Computes the bounds of the object itself (not including it's children) + in the form [[lat_min, lon_min], [lat_max, lon_max]] + """ + return [[None,None],[None,None]] + + def get_bounds(self): + """Computes the bounds of the object and all it's children + in the form [[lat_min, lon_min], [lat_max, lon_max]]. + """ + bounds = self._get_self_bounds() + + for child in self._children.values(): + child_bounds = child.get_bounds() + bounds = [ + [ + none_min(bounds[0][0], child_bounds[0][0]), + none_min(bounds[0][1], child_bounds[0][1]), + ], + [ + none_max(bounds[1][0], child_bounds[1][0]), + none_max(bounds[1][1], child_bounds[1][1]), + ], + ] + return bounds + def add_children(self, child, name=None, index=None): """Add a children.""" if name is None: diff --git a/folium/features.py b/folium/features.py index fa0aaa5dba..c025358c3f 100644 --- a/folium/features.py +++ b/folium/features.py @@ -126,7 +126,6 @@ def render(self, **kwargs): JavascriptLink("https://cdnjs.cloudflare.com/ajax/libs/leaflet-dvf/0.2/leaflet-dvf.markers.min.js"), # noqa name='dvf_js') - class Vega(Element): def __init__(self, data, width=None, height=None, left="0%", top="0%", position='relative'): @@ -277,6 +276,12 @@ def style_data(self): self.style_function(feature)) return json.dumps(self.data) + def _get_self_bounds(self): + """Computes the bounds of the object itself (not including it's children) + in the form [[lat_min, lon_min], [lat_max, lon_max]] + """ + raise NotImplementedError + class TopoJson(MacroElement): def __init__(self, data, object_path): """ @@ -315,6 +320,13 @@ def render(self, **kwargs): JavascriptLink("https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.9/topojson.min.js"), # noqa name='topojson') + def _get_self_bounds(self): + """Computes the bounds of the object itself (not including it's children) + in the form [[lat_min, lon_min], [lat_max, lon_max]] + """ + raise NotImplementedError + return [[self.location[0],self.location[1]],[self.location[0],self.location[1]]] + class ColorScale(MacroElement): def __init__(self, color_domain, color_code, caption=""): """ @@ -344,7 +356,6 @@ def render(self, **kwargs): JavascriptLink("https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"), # noqa name='d3') - class MarkerCluster(Layer): """Adds a MarkerCluster layer on the map.""" def __init__(self, overlay=True, control=True): @@ -569,6 +580,11 @@ def __init__(self, locations, color=None, weight=None, {% endmacro %} """) # noqa + def _get_self_bounds(self): + """Computes the bounds of the object itself (not including it's children) + in the form [[lat_min, lon_min], [lat_max, lon_max]] + """ + raise NotImplementedError class MultiPolyLine(MacroElement): def __init__(self, locations, color=None, weight=None, @@ -616,6 +632,11 @@ def __init__(self, locations, color=None, weight=None, {{this._parent.get_name()}}.addLayer({{this.get_name()}}); {% endmacro %} """) # noqa + def _get_self_bounds(self): + """Computes the bounds of the object itself (not including it's children) + in the form [[lat_min, lon_min], [lat_max, lon_max]] + """ + raise NotImplementedError class CustomIcon(Icon): def __init__(self, icon_image, icon_size=None, icon_anchor=None, diff --git a/folium/map.py b/folium/map.py index 1f5d62d389..8ff7c75894 100644 --- a/folium/map.py +++ b/folium/map.py @@ -16,7 +16,6 @@ from .element import Element, Figure, MacroElement, Html - class Map(MacroElement): def __init__(self, location=None, width='100%', height='100%', left="0%", top="0%", position='relative', @@ -168,6 +167,12 @@ def add_tile_layer(self, tiles='OpenStreetMap', name=None, detect_retina=detect_retina) self.add_children(tile_layer, name=tile_layer.tile_name) + def _get_self_bounds(self): + """Computes the bounds of the object itself (not including it's children) + in the form [[lat_min, lon_min], [lat_max, lon_max]] + """ + return [[self.location[0],self.location[1]],[self.location[0],self.location[1]]] + class Layer(MacroElement): """An abstract class for everything that is a Layer on the map. It will be used to define whether an object will be included in LayerControls. @@ -243,7 +248,6 @@ def __init__(self, tiles='OpenStreetMap', name=None, {% endmacro %} """) - class FeatureGroup(Layer): def __init__(self, name=None, overlay=True, control=True): """ @@ -273,7 +277,6 @@ def __init__(self, name=None, overlay=True, control=True): {% endmacro %} """) - class LayerControl(MacroElement): """Adds a layer control to the map.""" def __init__(self): @@ -374,7 +377,6 @@ def __init__(self, color='blue', icon_color='white', icon='info-sign', {% endmacro %} """) - class Marker(MacroElement): def __init__(self, location, popup=None, icon=None): """Create a simple stock Leaflet marker on the map, with optional @@ -421,6 +423,11 @@ def __init__(self, location, popup=None, icon=None): {% endmacro %} """) + def _get_self_bounds(self): + """Computes the bounds of the object itself (not including it's children) + in the form [[lat_min, lon_min], [lat_max, lon_max]] + """ + return [[self.location[0],self.location[1]],[self.location[0],self.location[1]]] class Popup(Element): def __init__(self, html=None, max_width=300): @@ -469,7 +476,6 @@ def render(self, **kwargs): 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): @@ -491,7 +497,6 @@ def __init__(self, bounds, padding_top_left=None, the same value. max_zoom: int, default None Maximum zoom to be used. - """ super(FitBounds, self).__init__() self._name = 'FitBounds' diff --git a/folium/utilities.py b/folium/utilities.py index 7a33b47364..7d267b38e1 100644 --- a/folium/utilities.py +++ b/folium/utilities.py @@ -428,3 +428,19 @@ def _locations_tolist(x): return list(map(_locations_tolist, x)) else: return x + +def none_min(x,y): + if x is None: + return y + elif y is None: + return x + else: + return min(x,y) + +def none_max(x,y): + if x is None: + return y + elif y is None: + return x + else: + return max(x,y) From 3a19e4f874c02630b8c446688ebe56daf6739fb1 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Tue, 22 Dec 2015 05:43:09 +0100 Subject: [PATCH 2/7] Implement _get_self_bounds for GeoJson, Polyline... --- folium/features.py | 57 +++++++++++++++++++++++++++++++++++++++++---- folium/map.py | 6 ----- folium/utilities.py | 18 ++++++++++++++ 3 files changed, 71 insertions(+), 10 deletions(-) diff --git a/folium/features.py b/folium/features.py index c025358c3f..b362213232 100644 --- a/folium/features.py +++ b/folium/features.py @@ -10,7 +10,9 @@ from .utilities import (color_brewer, _parse_size, legend_scaler, _locations_mirror, _locations_tolist, image_to_url, - text_type, binary_type) + text_type, binary_type, + none_min, none_max, iter_points, + ) from .element import Element, Figure, JavascriptLink, CssLink, MacroElement from .map import Layer, Icon, Marker, Popup @@ -280,7 +282,30 @@ def _get_self_bounds(self): """Computes the bounds of the object itself (not including it's children) in the form [[lat_min, lon_min], [lat_max, lon_max]] """ - raise NotImplementedError + if not self.embed: + raise ValueError('Cannot compute bounds of non-embedded GeoJSON.') + + if 'features' not in self.data.keys(): + # Catch case when GeoJSON is just a single Feature or a geometry. + if not (isinstance(self.data, dict) and 'geometry' in self.data.keys()): + # Catch case when GeoJSON is just a geometry. + self.data = {'type' : 'Feature', 'geometry' : self.data} + self.data = {'type' : 'FeatureCollection', 'features' : [self.data]} + + bounds = [[None,None],[None,None]] + for feature in self.data['features']: + for point in iter_points(feature.get('geometry',{}).get('coordinates',{})): + bounds = [ + [ + none_min(bounds[0][0], point[1]), + none_min(bounds[0][1], point[0]), + ], + [ + none_max(bounds[1][0], point[1]), + none_max(bounds[1][1], point[0]), + ], + ] + return bounds class TopoJson(MacroElement): def __init__(self, data, object_path): @@ -584,7 +609,19 @@ def _get_self_bounds(self): """Computes the bounds of the object itself (not including it's children) in the form [[lat_min, lon_min], [lat_max, lon_max]] """ - raise NotImplementedError + bounds = [[None,None],[None,None]] + for point in iter_points(self.data): + bounds = [ + [ + none_min(bounds[0][0], point[0]), + none_min(bounds[0][1], point[1]), + ], + [ + none_max(bounds[1][0], point[0]), + none_max(bounds[1][1], point[1]), + ], + ] + return bounds class MultiPolyLine(MacroElement): def __init__(self, locations, color=None, weight=None, @@ -636,7 +673,19 @@ def _get_self_bounds(self): """Computes the bounds of the object itself (not including it's children) in the form [[lat_min, lon_min], [lat_max, lon_max]] """ - raise NotImplementedError + bounds = [[None,None],[None,None]] + for point in iter_points(self.data): + bounds = [ + [ + none_min(bounds[0][0], point[0]), + none_min(bounds[0][1], point[1]), + ], + [ + none_max(bounds[1][0], point[0]), + none_max(bounds[1][1], point[1]), + ], + ] + return bounds class CustomIcon(Icon): def __init__(self, icon_image, icon_size=None, icon_anchor=None, diff --git a/folium/map.py b/folium/map.py index 8ff7c75894..b0329b364d 100644 --- a/folium/map.py +++ b/folium/map.py @@ -167,12 +167,6 @@ def add_tile_layer(self, tiles='OpenStreetMap', name=None, detect_retina=detect_retina) self.add_children(tile_layer, name=tile_layer.tile_name) - def _get_self_bounds(self): - """Computes the bounds of the object itself (not including it's children) - in the form [[lat_min, lon_min], [lat_max, lon_max]] - """ - return [[self.location[0],self.location[1]],[self.location[0],self.location[1]]] - class Layer(MacroElement): """An abstract class for everything that is a Layer on the map. It will be used to define whether an object will be included in LayerControls. diff --git a/folium/utilities.py b/folium/utilities.py index 7d267b38e1..fe4b03ada4 100644 --- a/folium/utilities.py +++ b/folium/utilities.py @@ -444,3 +444,21 @@ def none_max(x,y): return x else: return max(x,y) + +def iter_points(x): + """Iterates over a list representing a feature, and returns a list of points, + whatever the shape of the array (Point, MultiPolyline, etc). + """ + if isinstance(x,list): + if len(x): + if isinstance(x[0],list): + out = [] + for y in x: + out += iter_points(y) + return out + else: + return [x] + else: + return [] + else: + raise ValueError('List type expected {!r}'.format(x)) From 4d27f47d4114e361e82854e3888e0c4f641874b5 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Tue, 22 Dec 2015 13:34:24 +0100 Subject: [PATCH 3/7] Implement _get_self_bounds for TopoJson --- folium/features.py | 32 ++++++++++++++++++++++++++++++-- tests/test_folium.py | 4 ++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/folium/features.py b/folium/features.py index b362213232..7016066f72 100644 --- a/folium/features.py +++ b/folium/features.py @@ -316,10 +316,13 @@ def __init__(self, data, object_path): super(TopoJson, self).__init__() self._name = 'TopoJson' if 'read' in dir(data): + self.embed = True self.data = data.read() elif type(data) is dict: + self.embed = True self.data = json.dumps(data) else: + self.embed = False self.data = data self.object_path = object_path @@ -349,8 +352,33 @@ def _get_self_bounds(self): """Computes the bounds of the object itself (not including it's children) in the form [[lat_min, lon_min], [lat_max, lon_max]] """ - raise NotImplementedError - return [[self.location[0],self.location[1]],[self.location[0],self.location[1]]] + if not self.embed: + raise ValueError('Cannot compute bounds of non-embedded TopoJSON.') + + data = json.loads(self.data) + + xmin,xmax,ymin,ymax = None, None, None, None + + for arc in data['arcs']: + x,y = 0,0 + for dx, dy in arc: + x += dx + y += dy + xmin = none_min(x, xmin) + xmax = none_max(x, xmax) + ymin = none_min(y, ymin) + ymax = none_max(y, ymax) + return [ + [ + data['transform']['translate'][0] + data['transform']['scale'][0] * xmin, + data['transform']['translate'][1] + data['transform']['scale'][1] * ymin, + ], + [ + data['transform']['translate'][0] + data['transform']['scale'][0] * xmax, + data['transform']['translate'][1] + data['transform']['scale'][1] * ymax, + ] + + ] class ColorScale(MacroElement): def __init__(self, color_domain, color_code, caption=""): diff --git a/tests/test_folium.py b/tests/test_folium.py index fdb1258182..b63f51c59b 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -447,6 +447,10 @@ def test_topo_json(self): topojson_str = topo_json._template.module.script(topo_json) assert ''.join(topojson_str.split())[:-1] in ''.join(out.split()) + bounds = self.map.get_bounds() + assert bounds == [[-124.56617536999985, 41.99187135900012], + [-116.46422312599977, 46.28768217800006]], bounds + def test_map_build(self): """Test map build.""" From 22be42601b923097f940651a010d9f1d7fe7d120 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Tue, 22 Dec 2015 17:05:24 +0100 Subject: [PATCH 4/7] Add get_bounds in (almost) each test_folium --- tests/test_folium.py | 93 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 81 insertions(+), 12 deletions(-) diff --git a/tests/test_folium.py b/tests/test_folium.py index b63f51c59b..68ac9d8dce 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -102,6 +102,9 @@ def test_cloudmade(self): cloudmade = 'http://{s}.tile.cloudmade.com/###/997/256/{z}/{x}/{y}.png' assert map._children['cloudmade'].tiles == cloudmade + bounds = map.get_bounds() + assert bounds == [[None, None], [None, None]], bounds + def test_builtin_tile(self): """Test custom maptiles.""" @@ -117,6 +120,9 @@ def test_builtin_tile(self): assert map._children[tiles].tiles == url assert map._children[tiles].attr == attr + bounds = map.get_bounds() + assert bounds == [[None, None], [None, None]], bounds + def test_custom_tile(self): """Test custom tile URLs.""" @@ -130,6 +136,9 @@ def test_custom_tile(self): assert map._children[url].tiles == url assert map._children[url].attr == attr + bounds = map.get_bounds() + assert bounds == [[None, None], [None, None]], bounds + def test_wms_layer(self): """Test WMS layer URLs.""" @@ -154,6 +163,9 @@ def test_wms_layer(self): assert (''.join(wms.split())[:-1] in ''.join(map.get_root().render().split())) + bounds = map.get_bounds() + assert bounds == [[None, None], [None, None]], bounds + def test_feature_group(self): """Test FeatureGroup.""" @@ -166,6 +178,9 @@ def test_feature_group(self): map._repr_html_() + bounds = map.get_bounds() + assert bounds == [[45, -30], [45, 30]], bounds + def test_simple_marker(self): """Test simple marker addition.""" @@ -182,7 +197,9 @@ def test_simple_marker(self): '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] == "" + + bounds = self.map.get_bounds() + assert bounds == [[45.5, -122.7], [45.5, -122.7]], bounds # Test Simple marker addition. self.map.simple_marker(location=[45.60, -122.8], popup='Hi') @@ -209,6 +226,10 @@ def test_simple_marker(self): for child in list(self.map._children.values())[-1]._children.values(): assert not isinstance(child, Popup) + bounds = self.map.get_bounds() + assert bounds == [[45.5, -122.8], [45.6, -122.7]], bounds + + def test_circle_marker(self): """Test circle marker additions.""" @@ -239,6 +260,9 @@ def test_circle_marker(self): assert (''.join(circle_2.split())[:-1] in ''.join(self.map.get_root().render().split())) + bounds = self.map.get_bounds() + assert bounds == [[45.6, -122.9], [45.7, -122.8]], bounds + def test_poly_marker(self): """Test polygon marker.""" @@ -262,6 +286,9 @@ def test_poly_marker(self): assert ((''.join(polygon.split()))[-1] in ''.join(self.map.get_root().render().split())) + bounds = self.map.get_bounds() + assert bounds == [[45.5, -122.5], [45.5, -122.5]], bounds + def test_latlng_pop(self): """Test lat/lon popovers.""" @@ -273,6 +300,9 @@ def test_latlng_pop(self): assert ((''.join(pop_templ.split()))[:-1] in ''.join(self.map.get_root().render().split())) + bounds = self.map.get_bounds() + assert bounds == [[None, None], [None, None]], bounds + def test_click_for_marker(self): """Test click for marker functionality.""" @@ -294,6 +324,9 @@ def test_click_for_marker(self): assert ((''.join(click.split()))[:-1] in ''.join(self.map.get_root().render().split())) + bounds = self.map.get_bounds() + assert bounds == [[None, None], [None, None]], bounds + def test_vega_popup(self): """Test vega popups.""" @@ -320,6 +353,9 @@ def test_vega_popup(self): assert ''.join(vega_parse.render().split()) in out assert (''.join(vega_str.split()))[:-1] in out + bounds = self.map.get_bounds() + assert bounds == [[45.6, -122.8], [45.6, -122.8]], bounds + def test_geo_json_simple(self): """Test geojson method.""" @@ -332,6 +368,9 @@ def test_geo_json_simple(self): isinstance(x, GeoJson)][0] self.map._repr_html_() + bounds = self.map.get_bounds() + assert bounds == [[18.948267, -171.742517], [71.285909, -66.979601]], bounds + def test_geo_json_str(self): # No data binding. self.map = folium.Map([43, -100], zoom_start=4) @@ -364,6 +403,9 @@ def test_geo_json_str(self): obj = obj_temp.render(this=geo_json, json=json) assert ''.join(obj.split())[:-1] in out + bounds = self.map.get_bounds() + assert bounds == [[18.948267, -171.742517], [71.285909, -66.979601]], bounds + def test_geo_json_bad_color(self): """Test geojson method.""" @@ -378,6 +420,9 @@ def test_geo_json_bad_color(self): columns=['FIPS_Code', 'Unemployed_2011'], key_on='feature.id', fill_color='blue') + bounds = self.map.get_bounds() + assert bounds == [[None, None], [None, None]], bounds + def test_geo_json_bad_threshold_scale(self): """Test geojson method.""" @@ -394,6 +439,9 @@ def test_geo_json_bad_threshold_scale(self): threshold_scale=[1, 2, 3, 4, 5, 6, 7], fill_color='YlGnBu') + bounds = self.map.get_bounds() + assert bounds == [[None, None], [None, None]], bounds + def test_geo_json_data_binding(self): """Test geojson method.""" data = setup_data() @@ -430,6 +478,9 @@ def test_geo_json_data_binding(self): 'range': d3range}) assert ''.join(colorscale.split())[:-1] in ''.join(out.split()) + bounds = self.map.get_bounds() + assert bounds == [[18.948267, -171.742517], [71.285909, -66.979601]], bounds + def test_topo_json(self): """Test geojson method.""" @@ -553,6 +604,9 @@ def test_line(self): assert ''.join(line_rendered.split()) in ''.join(out.split()) + bounds = self.map.get_bounds() + assert bounds == [[45.5236, -122.6751], [45.5238, -122.675]], bounds + def test_multi_polyline(self): """Test multi_polyline.""" @@ -581,6 +635,9 @@ def test_multi_polyline(self): assert ''.join(multiline_rendered.split()) in ''.join(out.split()) + bounds = self.map.get_bounds() + assert bounds == [[45.5236, -122.6751], [45.5238, -122.675]], bounds + def test_fit_bounds(self): """Test fit_bounds.""" bounds = ((52.193636, -2.221575), (52.636878, -1.139759)) @@ -616,6 +673,9 @@ def test_fit_bounds(self): assert ''.join(fit_bounds_rendered.split()) in ''.join(out.split()) + bounds = self.map.get_bounds() + assert bounds == [[None, None], [None, None]], bounds + def test_image_overlay(self): """Test image overlay.""" # from numpy.random import random @@ -668,6 +728,9 @@ def test_image_overlay(self): assert ''.join(image_rendered.split()) in ''.join(out.split()) + bounds = self.map.get_bounds() + assert bounds == [[None, None], [None, None]], bounds + def test_custom_icon(self): """Test CustomIcon.""" self.setup() @@ -688,14 +751,20 @@ def test_custom_icon(self): self.map.add_children(mk) self.map._parent.render() -def test_tile_layer(): - 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_() + bounds = self.map.get_bounds() + assert bounds == [[45, -100], [45, -100]], bounds + + def test_tile_layer(self): + 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_() + + bounds = self.map.get_bounds() + assert bounds == [[None, None], [None, None]], bounds From 3276d8d38eeafa4467360c81ce441240569bedf1 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Tue, 22 Dec 2015 17:10:00 +0100 Subject: [PATCH 5/7] Add get_bounds in (almost) each test_features --- tests/test_features.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/test_features.py b/tests/test_features.py index 466006a216..ce501d0232 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -58,12 +58,16 @@ def test_figure_creation(): f = features.Figure() assert isinstance(f, Element) + bounds = f.get_bounds() + assert bounds == [[None, None], [None, None]], bounds def test_figure_rendering(): f = features.Figure() out = f.render() assert type(out) is text_type + bounds = f.get_bounds() + assert bounds == [[None, None], [None, None]], bounds def test_figure_html(): f = features.Figure() @@ -72,6 +76,8 @@ def test_figure_html(): print(out) assert out.strip() == tmpl.strip() + bounds = f.get_bounds() + assert bounds == [[None, None], [None, None]], bounds def test_figure_double_rendering(): f = features.Figure() @@ -79,6 +85,9 @@ def test_figure_double_rendering(): out2 = f.render() assert out == out2 + bounds = f.get_bounds() + assert bounds == [[None, None], [None, None]], bounds + def test_marker_popups(): m = Map() features.Marker([45,-180],popup='-180').add_to(m) @@ -89,6 +98,9 @@ def test_marker_popups(): features.CircleMarker([45,120],popup=Popup('120')).add_to(m) m._repr_html_() + bounds = m.get_bounds() + assert bounds == [[45, -180], [45, 120]], bounds + def test_polyline_popups(): m = Map([43,-100], zoom_start=4) features.PolyLine([[40,-80],[45,-80]], popup="PolyLine").add_to(m) @@ -97,6 +109,9 @@ def test_polyline_popups(): features.MultiPolyLine([[[40,-120],[45,-120]]], popup=Popup("MultiPolyLine")).add_to(m) m._repr_html_() + bounds = m.get_bounds() + assert bounds == [[40, -120], [45, -80]], bounds + # DivIcon. def test_divicon(): html = """ @@ -107,7 +122,6 @@ def test_divicon(): assert div.className == 'empty' assert div.html == html - # WmsTileLayer def test_wms_service(): m = Map([40, -100], zoom_start=4) @@ -120,3 +134,6 @@ def test_wms_service(): transparent=True) w.add_to(m) m._repr_html_() + + bounds = m.get_bounds() + assert bounds == [[None, None], [None, None]], bounds From 36514c228604362d13d6f450fc1b164be4ae754c Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Tue, 22 Dec 2015 18:07:24 +0100 Subject: [PATCH 6/7] Add get_bounds in (almost) each plugin test --- folium/plugins/boat_marker.py | 17 +++++------ folium/plugins/heat_map.py | 20 ++++++++++++- folium/plugins/image_overlay.py | 6 ++++ folium/plugins/timestamped_geo_json.py | 35 +++++++++++++++++++++- tests/plugins/test_boat_marker.py | 5 +++- tests/plugins/test_heat_map.py | 5 ++++ tests/plugins/test_image_overlay.py | 3 ++ tests/plugins/test_marker_cluster.py | 5 ++++ tests/plugins/test_scroll_zoom_toggler.py | 5 +++- tests/plugins/test_terminator.py | 3 ++ tests/plugins/test_timestamped_geo_json.py | 3 ++ tests/test_folium.py | 2 +- 12 files changed, 95 insertions(+), 14 deletions(-) diff --git a/folium/plugins/boat_marker.py b/folium/plugins/boat_marker.py index 4889ce9e22..ee34a8f478 100644 --- a/folium/plugins/boat_marker.py +++ b/folium/plugins/boat_marker.py @@ -10,19 +10,19 @@ import json from jinja2 import Template -from folium.element import JavascriptLink, MacroElement, Figure +from folium.element import JavascriptLink, Figure +from folium.map import Marker - -class BoatMarker(MacroElement): +class BoatMarker(Marker): """Adds a BoatMarker layer on the map.""" - def __init__(self, position=None, heading=0, - wind_heading=None, wind_speed=0, **kwargs): + def __init__(self, location, popup=None, icon=None, + heading=0, wind_heading=None, wind_speed=0, **kwargs): """Creates a BoatMarker plugin to append into a map with Map.add_plugin. Parameters ---------- - position: tuple of length 2, default None + location: tuple of length 2, default None The latitude and longitude of the marker. If None, then the middle of the map is used. @@ -36,9 +36,8 @@ def __init__(self, position=None, heading=0, wind_speed: int, default 0 Speed of the wind in knots. """ - super(BoatMarker, self).__init__() + super(BoatMarker, self).__init__(location, popup=popup, icon=icon) 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 @@ -47,7 +46,7 @@ def __init__(self, position=None, heading=0, self._template = Template(u""" {% macro script(this, kwargs) %} var {{this.get_name()}} = L.boatMarker( - [{{this.position[0]}},{{this.position[1]}}], + [{{this.location[0]}},{{this.location[1]}}], {{this.kwargs}}).addTo({{this._parent.get_name()}}); {{this.get_name()}}.setHeadingWind({{this.heading}}, {{this.wind_speed}}, {{this.wind_heading}}); {% endmacro %} diff --git a/folium/plugins/heat_map.py b/folium/plugins/heat_map.py index 60ddc9967c..52c4472486 100644 --- a/folium/plugins/heat_map.py +++ b/folium/plugins/heat_map.py @@ -11,7 +11,7 @@ from folium.element import JavascriptLink, Figure from folium.map import TileLayer - +from folium.utilities import none_min, none_max class HeatMap(TileLayer): def __init__(self, data, name=None, min_opacity=0.5, max_zoom=18, @@ -79,3 +79,21 @@ def render(self, **kwargs): figure.header.add_children( JavascriptLink("https://leaflet.github.io/Leaflet.heat/dist/leaflet-heat.js"), # noqa name='leaflet-heat.js') + + def _get_self_bounds(self): + """Computes the bounds of the object itself (not including it's children) + in the form [[lat_min, lon_min], [lat_max, lon_max]] + """ + bounds = [[None,None],[None,None]] + for point in self.data: + bounds = [ + [ + none_min(bounds[0][0], point[1]), + none_min(bounds[0][1], point[0]), + ], + [ + none_max(bounds[1][0], point[1]), + none_max(bounds[1][1], point[0]), + ], + ] + return bounds diff --git a/folium/plugins/image_overlay.py b/folium/plugins/image_overlay.py index ce470959a4..ec407811d8 100644 --- a/folium/plugins/image_overlay.py +++ b/folium/plugins/image_overlay.py @@ -136,3 +136,9 @@ def __init__(self, image, bounds, opacity=1., attr=None, ).addTo({{this._parent.get_name()}}); {% endmacro %} """) + + def _get_self_bounds(self): + """Computes the bounds of the object itself (not including it's children) + in the form [[lat_min, lon_min], [lat_max, lon_max]] + """ + return self.bounds diff --git a/folium/plugins/timestamped_geo_json.py b/folium/plugins/timestamped_geo_json.py index 844e26898e..1abdc9ab11 100644 --- a/folium/plugins/timestamped_geo_json.py +++ b/folium/plugins/timestamped_geo_json.py @@ -23,7 +23,7 @@ from jinja2 import Template from folium.element import MacroElement, Figure, JavascriptLink, CssLink - +from folium.utilities import none_min, none_max, iter_points class TimestampedGeoJson(MacroElement): def __init__(self, data, transition_time=200, loop=True, auto_play=True): @@ -87,10 +87,13 @@ def __init__(self, data, transition_time=200, loop=True, auto_play=True): # self.template = self.env.get_template('timestamped_geo_json.tpl') if 'read' in dir(data): + self.embed = True self.data = data.read() elif type(data) is dict: + self.embed = True self.data = json.dumps(data) else: + self.embed = False self.data = data self.transition_time = int(transition_time) self.loop = bool(loop) @@ -145,3 +148,33 @@ def render(self, **kwargs): figure.header.add_children( CssLink("http://apps.socib.es/Leaflet.TimeDimension/dist/leaflet.timedimension.control.min.css"), # noqa name='leaflet.timedimension_css') + + def _get_self_bounds(self): + """Computes the bounds of the object itself (not including it's children) + in the form [[lat_min, lon_min], [lat_max, lon_max]] + """ + if not self.embed: + raise ValueError('Cannot compute bounds of non-embedded GeoJSON.') + + data = json.loads(self.data) + if 'features' not in data.keys(): + # Catch case when GeoJSON is just a single Feature or a geometry. + if not (isinstance(data, dict) and 'geometry' in data.keys()): + # Catch case when GeoJSON is just a geometry. + data = {'type' : 'Feature', 'geometry' : data} + data = {'type' : 'FeatureCollection', 'features' : [data]} + + bounds = [[None,None],[None,None]] + for feature in data['features']: + for point in iter_points(feature.get('geometry',{}).get('coordinates',{})): + bounds = [ + [ + none_min(bounds[0][0], point[1]), + none_min(bounds[0][1], point[0]), + ], + [ + none_max(bounds[1][0], point[1]), + none_max(bounds[1][1], point[0]), + ], + ] + return bounds diff --git a/tests/plugins/test_boat_marker.py b/tests/plugins/test_boat_marker.py index a1ec5aff80..70e8599451 100644 --- a/tests/plugins/test_boat_marker.py +++ b/tests/plugins/test_boat_marker.py @@ -38,10 +38,13 @@ def test_boat_marker(): # We verify that the script part is correct tmpl = Template(""" var {{this.get_name()}} = L.boatMarker( - [{{this.position[0]}},{{this.position[1]}}], + [{{this.location[0]}},{{this.location[1]}}], {{this.kwargs}}).addTo({{this._parent.get_name()}}); {{this.get_name()}}.setHeadingWind({{this.heading}}, {{this.wind_speed}}, {{this.wind_heading}}); """) assert tmpl.render(this=bm1) in out assert tmpl.render(this=bm2) in out + + bounds = m.get_bounds() + assert bounds == [[34, -43], [46, -30]], bounds diff --git a/tests/plugins/test_heat_map.py b/tests/plugins/test_heat_map.py index 32164e7910..7906d82854 100644 --- a/tests/plugins/test_heat_map.py +++ b/tests/plugins/test_heat_map.py @@ -11,6 +11,7 @@ from folium import plugins def test_heat_map(): + np.random.seed(3141592) data = (np.random.normal(size=(100, 2)) * np.array([[1, 1]]) + np.array([[48, 5]])).tolist() m = folium.Map([48., 5.], tiles='stamentoner', zoom_start=6) @@ -39,3 +40,7 @@ def test_heat_map(): """) assert tmpl.render(this=hm) + + bounds = m.get_bounds() + assert bounds == [[3.0302801394447734, 46.218566840847025], + [7.132453997672826, 50.75345011431167]], bounds diff --git a/tests/plugins/test_image_overlay.py b/tests/plugins/test_image_overlay.py index d7257bacd7..5cbc6d3610 100644 --- a/tests/plugins/test_image_overlay.py +++ b/tests/plugins/test_image_overlay.py @@ -35,3 +35,6 @@ def test_image_overlay(): ).addTo({{this._parent.get_name()}}); """) assert tmpl.render(this=io) in out + + bounds = m.get_bounds() + assert bounds == [[0, -180], [90, 180]], bounds diff --git a/tests/plugins/test_marker_cluster.py b/tests/plugins/test_marker_cluster.py index 5d5fd4dc61..b86e3f4580 100644 --- a/tests/plugins/test_marker_cluster.py +++ b/tests/plugins/test_marker_cluster.py @@ -12,6 +12,7 @@ def test_marker_cluster(): N = 100 + np.random.seed(seed=26082009) data = np.array([ np.random.uniform(low=35, high=60, size=N), # Random latitudes. np.random.uniform(low=-12, high=30, size=N), # Random longitudes. @@ -49,3 +50,7 @@ def test_marker_cluster(): {% endfor %} """) assert ''.join(tmpl.render(this=mc).split()) in ''.join(out.split()) + + bounds = m.get_bounds() + assert bounds == [[35.147332572663785, -11.520684337300109], + [59.839718052359274, 29.94931046497927]], bounds diff --git a/tests/plugins/test_scroll_zoom_toggler.py b/tests/plugins/test_scroll_zoom_toggler.py index 162dd76617..fbe25fcbb3 100644 --- a/tests/plugins/test_scroll_zoom_toggler.py +++ b/tests/plugins/test_scroll_zoom_toggler.py @@ -60,4 +60,7 @@ def test_scroll_zoom_toggler(): {{this._parent.get_name()}}.toggleScroll(); """) - assert ''.join(tmpl.render(this=szt).split()) in ''.join(out.split()) \ No newline at end of file + assert ''.join(tmpl.render(this=szt).split()) in ''.join(out.split()) + + bounds = m.get_bounds() + assert bounds == [[None, None], [None, None]], bounds diff --git a/tests/plugins/test_terminator.py b/tests/plugins/test_terminator.py index f9b837fd1a..f1abdcc0e2 100644 --- a/tests/plugins/test_terminator.py +++ b/tests/plugins/test_terminator.py @@ -20,3 +20,6 @@ def test_terminator(): # Verify that the script is okay. tmpl = Template('L.terminator().addTo({{this._parent.get_name()}});') assert ''.join(tmpl.render(this=t).split()) in ''.join(out.split()) + + bounds = m.get_bounds() + assert bounds == [[None, None], [None, None]], bounds diff --git a/tests/plugins/test_timestamped_geo_json.py b/tests/plugins/test_timestamped_geo_json.py index ee8ac91618..e6e5688091 100644 --- a/tests/plugins/test_timestamped_geo_json.py +++ b/tests/plugins/test_timestamped_geo_json.py @@ -128,3 +128,6 @@ def test_timestamped_geo_json(): """) assert ''.join(tmpl.render(this=tgj).split()) in ''.join(out.split()) + + bounds = m.get_bounds() + assert bounds == [[-53.0, -158.0], [50.0, 158.0]], bounds diff --git a/tests/test_folium.py b/tests/test_folium.py index 68ac9d8dce..8040940a1b 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -729,7 +729,7 @@ def test_image_overlay(self): assert ''.join(image_rendered.split()) in ''.join(out.split()) bounds = self.map.get_bounds() - assert bounds == [[None, None], [None, None]], bounds + assert bounds == [[-90.0, -180.0], [90.0, 180.0]], bounds def test_custom_icon(self): """Test CustomIcon.""" From 457596795fb411f265c3f3b1ecf5c9a88ff7f486 Mon Sep 17 00:00:00 2001 From: Martin Journois Date: Tue, 22 Dec 2015 18:20:02 +0100 Subject: [PATCH 7/7] PEP8ing, fearing the :ghost: --- folium/features.py | 4 ++-- folium/map.py | 2 +- folium/utilities.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/folium/features.py b/folium/features.py index 7016066f72..037af8f682 100644 --- a/folium/features.py +++ b/folium/features.py @@ -637,7 +637,7 @@ def _get_self_bounds(self): """Computes the bounds of the object itself (not including it's children) in the form [[lat_min, lon_min], [lat_max, lon_max]] """ - bounds = [[None,None],[None,None]] + bounds = [[None, None], [None, None]] for point in iter_points(self.data): bounds = [ [ @@ -701,7 +701,7 @@ def _get_self_bounds(self): """Computes the bounds of the object itself (not including it's children) in the form [[lat_min, lon_min], [lat_max, lon_max]] """ - bounds = [[None,None],[None,None]] + bounds = [[None, None], [None, None]] for point in iter_points(self.data): bounds = [ [ diff --git a/folium/map.py b/folium/map.py index b0329b364d..6daa2f5c77 100644 --- a/folium/map.py +++ b/folium/map.py @@ -421,7 +421,7 @@ def _get_self_bounds(self): """Computes the bounds of the object itself (not including it's children) in the form [[lat_min, lon_min], [lat_max, lon_max]] """ - return [[self.location[0],self.location[1]],[self.location[0],self.location[1]]] + return [[self.location[0], self.location[1]], [self.location[0], self.location[1]]] class Popup(Element): def __init__(self, html=None, max_width=300): diff --git a/folium/utilities.py b/folium/utilities.py index fe4b03ada4..209356784a 100644 --- a/folium/utilities.py +++ b/folium/utilities.py @@ -449,9 +449,9 @@ def iter_points(x): """Iterates over a list representing a feature, and returns a list of points, whatever the shape of the array (Point, MultiPolyline, etc). """ - if isinstance(x,list): + if isinstance(x, list): if len(x): - if isinstance(x[0],list): + if isinstance(x[0], list): out = [] for y in x: out += iter_points(y) @@ -461,4 +461,4 @@ def iter_points(x): else: return [] else: - raise ValueError('List type expected {!r}'.format(x)) + raise ValueError('List type expected. Got {!r}.'.format(x))