|
25 | 25 | none_min,
|
26 | 26 | get_obj_in_upper_tree,
|
27 | 27 | parse_options,
|
| 28 | + camelize |
28 | 29 | )
|
29 | 30 | from folium.vector_layers import PolyLine, path_options
|
30 | 31 |
|
@@ -437,7 +438,7 @@ class GeoJson(Layer):
|
437 | 438 |
|
438 | 439 | def __init__(self, data, style_function=None, highlight_function=None, # noqa
|
439 | 440 | name=None, overlay=True, control=True, show=True,
|
440 |
| - smooth_factor=None, tooltip=None, embed=True): |
| 441 | + smooth_factor=None, tooltip=None, embed=True, popup=None): |
441 | 442 | super(GeoJson, self).__init__(name=name, overlay=overlay,
|
442 | 443 | control=control, show=show)
|
443 | 444 | self._name = 'GeoJson'
|
@@ -467,6 +468,8 @@ def __init__(self, data, style_function=None, highlight_function=None, # noqa
|
467 | 468 | self.add_child(tooltip)
|
468 | 469 | elif tooltip is not None:
|
469 | 470 | self.add_child(Tooltip(tooltip))
|
| 471 | + if isinstance(popup, (GeoJsonPopup)): |
| 472 | + self.add_child(popup) |
470 | 473 |
|
471 | 474 | def process_data(self, data):
|
472 | 475 | """Convert an unknown data input into a geojson dictionary."""
|
@@ -796,7 +799,112 @@ def get_bounds(self):
|
796 | 799 | ]
|
797 | 800 |
|
798 | 801 |
|
799 |
| -class GeoJsonTooltip(Tooltip): |
| 802 | +class GeoJsonDetail(MacroElement): |
| 803 | + |
| 804 | + """ |
| 805 | + Base class for GeoJsonTooltip and GeoJsonPopup to inherit methods and |
| 806 | + template structure from. Not for direct usage. |
| 807 | +
|
| 808 | + """ |
| 809 | + base_template = u""" |
| 810 | + function(layer){ |
| 811 | + let div = L.DomUtil.create('div'); |
| 812 | + {% if this.fields %} |
| 813 | + let handleObject = feature=>typeof(feature)=='object' ? JSON.stringify(feature) : feature; |
| 814 | + let fields = {{ this.fields | tojson | safe }}; |
| 815 | + let aliases = {{ this.aliases | tojson | safe }}; |
| 816 | + let table = '<table>' + |
| 817 | + String( |
| 818 | + fields.map( |
| 819 | + (v,i)=> |
| 820 | + `<tr>{% if this.labels %} |
| 821 | + <th>${aliases[i]{% if this.localize %}.toLocaleString(){% endif %}}</th> |
| 822 | + {% endif %} |
| 823 | + <td>${handleObject(layer.feature.properties[v]){% if this.localize %}.toLocaleString(){% endif %}}</td> |
| 824 | + </tr>`).join('')) |
| 825 | + +'</table>'; |
| 826 | + div.innerHTML=table; |
| 827 | + {% endif %} |
| 828 | + return div |
| 829 | + } |
| 830 | + """ |
| 831 | + |
| 832 | + def __init__(self, fields, aliases=None, labels=True, localize=False, style=None, |
| 833 | + class_name="geojsondetail"): |
| 834 | + super(GeoJsonDetail, self).__init__() |
| 835 | + assert isinstance(fields, (list, tuple)), 'Please pass a list or ' \ |
| 836 | + 'tuple to fields.' |
| 837 | + if aliases is not None: |
| 838 | + assert isinstance(aliases, (list, tuple)) |
| 839 | + assert len(fields) == len(aliases), 'fields and aliases must have' \ |
| 840 | + ' the same length.' |
| 841 | + assert isinstance(labels, bool), 'labels requires a boolean value.' |
| 842 | + assert isinstance(localize, bool), 'localize must be bool.' |
| 843 | + self._name = "GeoJsonDetail" |
| 844 | + self.fields = fields |
| 845 | + self.aliases = aliases if aliases is not None else fields |
| 846 | + self.labels = labels |
| 847 | + self.localize = localize |
| 848 | + self.class_name = class_name |
| 849 | + if style: |
| 850 | + assert isinstance(style, str), \ |
| 851 | + 'Pass a valid inline HTML style property string to style.' |
| 852 | + # noqa outside of type checking. |
| 853 | + self.style = style |
| 854 | + |
| 855 | + def warn_for_geometry_collections(self): |
| 856 | + """Checks for GeoJson GeometryCollection features to warn user about incompatibility.""" |
| 857 | + geom_collections = [ |
| 858 | + feature.get('properties') if feature.get('properties') is not None else key |
| 859 | + for key, feature in enumerate(self._parent.data['features']) |
| 860 | + if feature['geometry']['type'] == 'GeometryCollection' |
| 861 | + ] |
| 862 | + if any(geom_collections): |
| 863 | + warnings.warn( |
| 864 | + "{} is not configured to render for GeoJson GeometryCollection geometries. " |
| 865 | + "Please consider reworking these features: {} to MultiPolygon for full functionality.\n" |
| 866 | + "https://tools.ietf.org/html/rfc7946#page-9".format(self._name, geom_collections), UserWarning) |
| 867 | + |
| 868 | + def render(self, **kwargs): |
| 869 | + """Renders the HTML representation of the element.""" |
| 870 | + figure = self.get_root() |
| 871 | + if isinstance(self._parent, GeoJson): |
| 872 | + keys = tuple(self._parent.data['features'][0]['properties'].keys()) |
| 873 | + self.warn_for_geometry_collections() |
| 874 | + elif isinstance(self._parent, TopoJson): |
| 875 | + obj_name = self._parent.object_path.split('.')[-1] |
| 876 | + keys = tuple(self._parent.data['objects'][obj_name][ |
| 877 | + 'geometries'][0]['properties'].keys()) |
| 878 | + else: |
| 879 | + raise TypeError('You cannot add a {} to anything other than a ' |
| 880 | + 'GeoJson or TopoJson object.'.format(self._name)) |
| 881 | + keys = tuple(x for x in keys if x not in ('style', 'highlight')) |
| 882 | + for value in self.fields: |
| 883 | + assert value in keys, ('The field {} is not available in the data. ' |
| 884 | + 'Choose from: {}.'.format(value, keys)) |
| 885 | + figure.header.add_child(Element( |
| 886 | + Template(u""" |
| 887 | + <style> |
| 888 | + .{{ this.class_name }} { |
| 889 | + {{ this.style }} |
| 890 | + } |
| 891 | + .{{ this.class_name }} table{ |
| 892 | + margin: auto; |
| 893 | + } |
| 894 | + .{{ this.class_name }} tr{ |
| 895 | + text-align: left; |
| 896 | + } |
| 897 | + .{{ this.class_name }} th{ |
| 898 | + padding: 2px; padding-right: 8px; |
| 899 | + } |
| 900 | + </style> |
| 901 | + """).render(this=self)), name=self.get_name() + "tablestyle" |
| 902 | + ) |
| 903 | + |
| 904 | + super(GeoJsonDetail, self).render() |
| 905 | + |
| 906 | + |
| 907 | +class GeoJsonTooltip(GeoJsonDetail): |
800 | 908 | """
|
801 | 909 | Create a tooltip that uses data from either geojson or topojson.
|
802 | 910 |
|
@@ -839,93 +947,76 @@ class GeoJsonTooltip(Tooltip):
|
839 | 947 | >>> GeoJsonTooltip(fields=('CNTY_NM',), labels=False, sticky=False)
|
840 | 948 | """
|
841 | 949 | _template = Template(u"""
|
842 |
| - {% macro script(this, kwargs) %} |
843 |
| - {{ this._parent.get_name() }}.bindTooltip( |
844 |
| - function(layer){ |
845 |
| - // Convert non-primitive to String. |
846 |
| - let handleObject = (feature)=>typeof(feature)=='object' ? JSON.stringify(feature) : feature; |
847 |
| - let fields = {{ this.fields|tojson }}; |
848 |
| - {%- if this.aliases %} |
849 |
| - let aliases = {{ this.aliases|tojson }}; |
850 |
| - {%- endif %} |
851 |
| - return '<table{% if this.style %} style={{ this.style|tojson }}{% endif%}>' + |
852 |
| - String( |
853 |
| - fields.map( |
854 |
| - columnname=> |
855 |
| - `<tr style="text-align: left;">{% if this.labels %} |
856 |
| - <th style="padding: 4px; padding-right: 10px;">{% if this.aliases %} |
857 |
| - ${aliases[fields.indexOf(columnname)] |
858 |
| - {% if this.localize %}.toLocaleString(){% endif %}} |
859 |
| - {% else %} |
860 |
| - ${ columnname{% if this.localize %}.toLocaleString(){% endif %}} |
861 |
| - {% endif %}</th> |
862 |
| - {% endif %} |
863 |
| - <td style="padding: 4px;">${handleObject(layer.feature.properties[columnname]) |
864 |
| - {% if this.localize %}.toLocaleString(){% endif %}}</td></tr>` |
865 |
| - ).join('')) |
866 |
| - +'</table>' |
867 |
| - }, {{ this.options|tojson }}); |
868 |
| - {% endmacro %} |
869 |
| - """) |
870 |
| - |
871 |
| - def __init__(self, fields, aliases=None, labels=True, |
872 |
| - localize=False, style=None, sticky=True, **kwargs): |
| 950 | + {% macro script(this, kwargs) %} |
| 951 | + {{ this._parent.get_name() }}.bindTooltip(""" + GeoJsonDetail.base_template + |
| 952 | + u""",{{ this.tooltip_options | tojson | safe }}); |
| 953 | + {% endmacro %} |
| 954 | + """) |
| 955 | + |
| 956 | + def __init__(self, fields, aliases=None, labels=True, localize=False, |
| 957 | + style=None, class_name='foliumtooltip', sticky=True, **kwargs): |
873 | 958 | super(GeoJsonTooltip, self).__init__(
|
874 |
| - text='', style=style, sticky=sticky, **kwargs |
| 959 | + fields=fields, aliases=aliases, labels=labels, localize=localize, |
| 960 | + style=style, class_name=class_name |
875 | 961 | )
|
876 | 962 | self._name = 'GeoJsonTooltip'
|
| 963 | + kwargs.update({'sticky': sticky, 'class_name': class_name}) |
| 964 | + self.tooltip_options = { |
| 965 | + camelize(key): kwargs[key] for key in kwargs.keys()} |
877 | 966 |
|
878 |
| - assert isinstance(fields, (list, tuple)), 'Please pass a list or ' \ |
879 |
| - 'tuple to fields.' |
880 |
| - if aliases is not None: |
881 |
| - assert isinstance(aliases, (list, tuple)) |
882 |
| - assert len(fields) == len(aliases), 'fields and aliases must have' \ |
883 |
| - ' the same length.' |
884 |
| - assert isinstance(labels, bool), 'labels requires a boolean value.' |
885 |
| - assert isinstance(localize, bool), 'localize must be bool.' |
886 |
| - assert 'permanent' not in kwargs, 'The `permanent` option does not ' \ |
887 |
| - 'work with GeoJsonTooltip.' |
888 | 967 |
|
889 |
| - self.fields = fields |
890 |
| - self.aliases = aliases |
891 |
| - self.labels = labels |
892 |
| - self.localize = localize |
893 |
| - if style: |
894 |
| - assert isinstance(style, str), \ |
895 |
| - 'Pass a valid inline HTML style property string to style.' |
896 |
| - # noqa outside of type checking. |
897 |
| - self.style = style |
| 968 | +class GeoJsonPopup(GeoJsonDetail): |
| 969 | + """ |
| 970 | + Create a popup feature to bind to each element of a GeoJson layer based on |
| 971 | + its attributes. |
898 | 972 |
|
899 |
| - def warn_for_geometry_collections(self): |
900 |
| - """Checks for GeoJson GeometryCollection features to warn user about incompatibility.""" |
901 |
| - geom_collections = [ |
902 |
| - feature.get('properties') if feature.get('properties') is not None else key |
903 |
| - for key, feature in enumerate(self._parent.data['features']) |
904 |
| - if feature['geometry']['type'] == 'GeometryCollection' |
905 |
| - ] |
906 |
| - if any(geom_collections): |
907 |
| - warnings.warn( |
908 |
| - "GeoJsonTooltip is not configured to render tooltips for GeoJson GeometryCollection geometries. " |
909 |
| - "Please consider reworking these features: {} to MultiPolygon for full functionality.\n" |
910 |
| - "https://tools.ietf.org/html/rfc7946#page-9".format(geom_collections), UserWarning) |
| 973 | + Parameters |
| 974 | + ---------- |
| 975 | + fields: list or tuple. |
| 976 | + Labels of GeoJson/TopoJson 'properties' or GeoPandas GeoDataFrame |
| 977 | + columns you'd like to display. |
| 978 | + aliases: list/tuple of strings, same length/order as fields, default None. |
| 979 | + Optional aliases you'd like to display in the tooltip as field name |
| 980 | + instead of the keys of `fields`. |
| 981 | + labels: bool, default True. |
| 982 | + Set to False to disable displaying the field names or aliases. |
| 983 | + localize: bool, default False. |
| 984 | + This will use JavaScript's .toLocaleString() to format 'clean' values |
| 985 | + as strings for the user's location; i.e. 1,000,000.00 comma separators, |
| 986 | + float truncation, etc. |
| 987 | + *Available for most of JavaScript's primitive types (any data you'll |
| 988 | + serve into the template). |
| 989 | + style: str, default None. |
| 990 | + HTML inline style properties like font and colors. Will be applied to |
| 991 | + a div with the text in it. |
911 | 992 |
|
912 |
| - def render(self, **kwargs): |
913 |
| - """Renders the HTML representation of the element.""" |
914 |
| - if isinstance(self._parent, GeoJson): |
915 |
| - keys = tuple(self._parent.data['features'][0]['properties'].keys()) |
916 |
| - self.warn_for_geometry_collections() |
917 |
| - elif isinstance(self._parent, TopoJson): |
918 |
| - obj_name = self._parent.object_path.split('.')[-1] |
919 |
| - keys = tuple(self._parent.data['objects'][obj_name][ |
920 |
| - 'geometries'][0]['properties'].keys()) |
921 |
| - else: |
922 |
| - raise TypeError('You cannot add a GeoJsonTooltip to anything else ' |
923 |
| - 'than a GeoJson or TopoJson object.') |
924 |
| - keys = tuple(x for x in keys if x not in ('style', 'highlight')) |
925 |
| - for value in self.fields: |
926 |
| - assert value in keys, ('The field {} is not available in the data. ' |
927 |
| - 'Choose from: {}.'.format(value, keys)) |
928 |
| - super(GeoJsonTooltip, self).render(**kwargs) |
| 993 | + Examples |
| 994 | + --- |
| 995 | + gjson = folium.GeoJson(gdf).add_to(m) |
| 996 | +
|
| 997 | + folium.features.GeoJsonPopup(fields=['NAME'], |
| 998 | + labels=False |
| 999 | + ).add_to(gjson) |
| 1000 | + """ |
| 1001 | + |
| 1002 | + _template = Template(u""" |
| 1003 | + {% macro script(this, kwargs) %} |
| 1004 | + let name_getter = '{{this._parent.get_name()}}'; |
| 1005 | + {{ this._parent.get_name() }}.bindPopup(""" + GeoJsonDetail.base_template + |
| 1006 | + u""",{{ this.popup_options | tojson | safe }}); |
| 1007 | + {% endmacro %} |
| 1008 | + """) |
| 1009 | + |
| 1010 | + def __init__(self, fields=None, aliases=None, labels=True, |
| 1011 | + style="margin: auto;", class_name='foliumpopup', localize=True, |
| 1012 | + **kwargs): |
| 1013 | + super(GeoJsonPopup, self).__init__( |
| 1014 | + fields=fields, aliases=aliases, labels=labels, localize=localize, |
| 1015 | + class_name=class_name, style=style) |
| 1016 | + self._name = "GeoJsonPopup" |
| 1017 | + kwargs.update({'class_name': self.class_name}) |
| 1018 | + self.popup_options = { |
| 1019 | + camelize(key): value for key, value in kwargs.items()} |
929 | 1020 |
|
930 | 1021 |
|
931 | 1022 | class Choropleth(FeatureGroup):
|
|
0 commit comments