""" Leaflet GeoJson and miscellaneous features. """ import functools import json import operator import warnings import numpy as np import requests from branca.colormap import LinearColormap, StepColormap from branca.element import Element, Figure, JavascriptLink, MacroElement from branca.utilities import color_brewer from jinja2 import Template from folium.elements import JSCSSMixin from folium.folium import Map from folium.map import FeatureGroup, Icon, Layer, Marker, Popup, Tooltip from folium.utilities import ( _parse_size, camelize, escape_backticks, get_bounds, get_obj_in_upper_tree, image_to_url, javascript_identifier_path_to_array_notation, none_max, none_min, parse_options, validate_locations, ) from folium.vector_layers import Circle, CircleMarker, PolyLine, path_options class RegularPolygonMarker(JSCSSMixin, Marker): """ Custom markers using the Leaflet Data Vis Framework. Parameters ---------- location: tuple or list Latitude and Longitude of Marker (Northing, Easting) number_of_sides: int, default 4 Number of polygon sides rotation: int, default 0 Rotation angle in degrees radius: int, default 15 Marker radius, in pixels popup: string or Popup, optional Input text or visualization for object displayed when clicking. tooltip: str or folium.Tooltip, optional Display a text when hovering over the object. **kwargs: See vector layers path_options for additional arguments. https://humangeo.github.io/leaflet-dvf/ """ _template = Template( """ {% macro script(this, kwargs) %} var {{ this.get_name() }} = new L.RegularPolygonMarker( {{ this.location|tojson }}, {{ this.options|tojson }} ).addTo({{ this._parent.get_name() }}); {% endmacro %} """ ) default_js = [ ( "dvf_js", "https://cdnjs.cloudflare.com/ajax/libs/leaflet-dvf/0.3.0/leaflet-dvf.markers.min.js", ), ] def __init__( self, location, number_of_sides=4, rotation=0, radius=15, popup=None, tooltip=None, **kwargs, ): super().__init__(location, popup=popup, tooltip=tooltip) self._name = "RegularPolygonMarker" self.options = path_options(**kwargs) self.options.update( parse_options( number_of_sides=number_of_sides, rotation=rotation, radius=radius, ) ) class Vega(JSCSSMixin, Element): """ Creates a Vega chart element. Parameters ---------- data: JSON-like str or object The Vega description of the chart. It can also be any object that has a method `to_json`, so that you can (for instance) provide a `vincent` chart. width: int or str, default None The width of the output element. If None, either data['width'] (if available) or '100%' will be used. Ex: 120, '120px', '80%' height: int or str, default None The height of the output element. If None, either data['width'] (if available) or '100%' will be used. Ex: 120, '120px', '80%' left: int or str, default '0%' The horizontal distance of the output with respect to the parent HTML object. Ex: 120, '120px', '80%' top: int or str, default '0%' The vertical distance of the output with respect to the parent HTML object. Ex: 120, '120px', '80%' position: str, default 'relative' The `position` argument that the CSS shall contain. Ex: 'relative', 'absolute' """ _template = Template("") default_js = [ ("d3", "https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"), ("vega", "https://cdnjs.cloudflare.com/ajax/libs/vega/1.4.3/vega.min.js"), ("jquery", "https://code.jquery.com/jquery-2.1.0.min.js"), ] def __init__( self, data, width=None, height=None, left="0%", top="0%", position="relative" ): super().__init__() self._name = "Vega" self.data = data.to_json() if hasattr(data, "to_json") else data if isinstance(self.data, str): self.data = json.loads(self.data) # Size Parameters. self.width = _parse_size( self.data.get("width", "100%") if width is None else width ) self.height = _parse_size( self.data.get("height", "100%") if height is None else height ) self.left = _parse_size(left) self.top = _parse_size(top) self.position = position def render(self, **kwargs): """Renders the HTML representation of the element.""" super().render(**kwargs) self.json = json.dumps(self.data) self._parent.html.add_child( Element( Template( """
""" ).render(this=self, kwargs=kwargs) ), name=self.get_name(), ) self._parent.script.add_child( Element( Template( """ vega_parse({{this.json}},{{this.get_name()}}); """ ).render(this=self) ), name=self.get_name(), ) figure = self.get_root() assert isinstance( figure, Figure ), "You cannot render this Element if it is not in a Figure." figure.header.add_child( Element( Template( """ """ ).render(this=self, **kwargs) ), name=self.get_name(), ) figure.script.add_child( Template( """function vega_parse(spec, div) { vg.parse.spec(spec, function(chart) { chart({el:div}).update(); });}""" ), # noqa name="vega_parse", ) class VegaLite(Element): """ Creates a Vega-Lite chart element. Parameters ---------- data: JSON-like str or object The Vega-Lite description of the chart. It can also be any object that has a method `to_json`, so that you can (for instance) provide an `Altair` chart. width: int or str, default None The width of the output element. If None, either data['width'] (if available) or '100%' will be used. Ex: 120, '120px', '80%' height: int or str, default None The height of the output element. If None, either data['width'] (if available) or '100%' will be used. Ex: 120, '120px', '80%' left: int or str, default '0%' The horizontal distance of the output with respect to the parent HTML object. Ex: 120, '120px', '80%' top: int or str, default '0%' The vertical distance of the output with respect to the parent HTML object. Ex: 120, '120px', '80%' position: str, default 'relative' The `position` argument that the CSS shall contain. Ex: 'relative', 'absolute' """ _template = Template("") def __init__( self, data, width=None, height=None, left="0%", top="0%", position="relative" ): super(self.__class__, self).__init__() self._name = "VegaLite" self.data = data.to_json() if hasattr(data, "to_json") else data if isinstance(self.data, str): self.data = json.loads(self.data) self.json = json.dumps(self.data) # Size Parameters. self.width = _parse_size( self.data.get("width", "100%") if width is None else width ) self.height = _parse_size( self.data.get("height", "100%") if height is None else height ) self.left = _parse_size(left) self.top = _parse_size(top) self.position = position def render(self, **kwargs): """Renders the HTML representation of the element.""" self._parent.html.add_child( Element( Template( """ """ ).render(this=self, kwargs=kwargs) ), name=self.get_name(), ) figure = self.get_root() assert isinstance( figure, Figure ), "You cannot render this Element if it is not in a Figure." figure.header.add_child( Element( Template( """ """ ).render(this=self, **kwargs) ), name=self.get_name(), ) embed_mapping = { 1: self._embed_vegalite_v1, 2: self._embed_vegalite_v2, 3: self._embed_vegalite_v3, 4: self._embed_vegalite_v4, 5: self._embed_vegalite_v5, } # Version 2 is assumed as the default, if no version is given in the schema. embed_vegalite = embed_mapping.get( self.vegalite_major_version, self._embed_vegalite_v2 ) embed_vegalite(figure) @property def vegalite_major_version(self) -> int: if "$schema" not in self.data: return None schema = self.data["$schema"] return int(schema.split("/")[-1].split(".")[0].lstrip("v")) def _embed_vegalite_v5(self, figure): self._vega_embed() figure.header.add_child( JavascriptLink("https://cdn.jsdelivr.net/npm//vega@5"), name="vega" ) figure.header.add_child( JavascriptLink("https://cdn.jsdelivr.net/npm/vega-lite@5"), name="vega-lite" ) figure.header.add_child( JavascriptLink("https://cdn.jsdelivr.net/npm/vega-embed@6"), name="vega-embed", ) def _embed_vegalite_v4(self, figure): self._vega_embed() figure.header.add_child( JavascriptLink("https://cdn.jsdelivr.net/npm//vega@5"), name="vega" ) figure.header.add_child( JavascriptLink("https://cdn.jsdelivr.net/npm/vega-lite@4"), name="vega-lite" ) figure.header.add_child( JavascriptLink("https://cdn.jsdelivr.net/npm/vega-embed@6"), name="vega-embed", ) def _embed_vegalite_v3(self, figure): self._vega_embed() figure.header.add_child( JavascriptLink("https://cdn.jsdelivr.net/npm/vega@4"), name="vega" ) figure.header.add_child( JavascriptLink("https://cdn.jsdelivr.net/npm/vega-lite@3"), name="vega-lite" ) figure.header.add_child( JavascriptLink("https://cdn.jsdelivr.net/npm/vega-embed@3"), name="vega-embed", ) def _embed_vegalite_v2(self, figure): self._vega_embed() figure.header.add_child( JavascriptLink("https://cdn.jsdelivr.net/npm/vega@3"), name="vega" ) figure.header.add_child( JavascriptLink("https://cdn.jsdelivr.net/npm/vega-lite@2"), name="vega-lite" ) figure.header.add_child( JavascriptLink("https://cdn.jsdelivr.net/npm/vega-embed@3"), name="vega-embed", ) def _vega_embed(self): self._parent.script.add_child( Element( Template( """ vegaEmbed({{this.get_name()}}, {{this.json}}) .then(function(result) {}) .catch(console.error); """ ).render(this=self) ), name=self.get_name(), ) def _embed_vegalite_v1(self, figure): self._parent.script.add_child( Element( Template( """ var embedSpec = { mode: "vega-lite", spec: {{this.json}} }; vg.embed( {{this.get_name()}}, embedSpec, function(error, result) {} ); """ ).render(this=self) ), name=self.get_name(), ) figure.header.add_child( JavascriptLink("https://d3js.org/d3.v3.min.js"), name="d3" ) figure.header.add_child( JavascriptLink("https://cdnjs.cloudflare.com/ajax/libs/vega/2.6.5/vega.js"), name="vega", ) # noqa figure.header.add_child( JavascriptLink( "https://cdnjs.cloudflare.com/ajax/libs/vega-lite/1.3.1/vega-lite.js" ), name="vega-lite", ) # noqa figure.header.add_child( JavascriptLink( "https://cdnjs.cloudflare.com/ajax/libs/vega-embed/2.2.0/vega-embed.js" ), name="vega-embed", ) # noqa class GeoJson(Layer): """ Creates a GeoJson object for plotting into a Map. Parameters ---------- data: file, dict or str. The GeoJSON data you want to plot. * If file, then data will be read in the file and fully embedded in Leaflet's JavaScript. * If dict, then data will be converted to JSON and embedded in the JavaScript. * If str, then data will be passed to the JavaScript as-is. * If `__geo_interface__` is available, the `__geo_interface__` dictionary will be serialized to JSON and reprojected if `to_crs` is available. style_function: function, default None Function mapping a GeoJson Feature to a style dict. highlight_function: function, default None Function mapping a GeoJson Feature to a style dict for mouse events. name : string, default None The name of the Layer, as it will appear in LayerControls overlay : bool, default True Adds the layer as an optional overlay (True) or the base layer (False). control : bool, default True Whether the Layer will be included in LayerControls show: bool, default True Whether the layer will be shown on opening (only for overlays). smooth_factor: float, default None How much to simplify the polyline on each zoom level. More means better performance and smoother look, and less means more accurate representation. Leaflet defaults to 1.0. tooltip: GeoJsonTooltip, Tooltip or str, default None Display a text when hovering over the object. Can utilize the data, see folium.GeoJsonTooltip for info on how to do that. popup: GeoJsonPopup, optional Show a different popup for each feature by passing a GeoJsonPopup object. marker: Circle, CircleMarker or Marker, optional If your data contains Point geometry, you can format the markers by passing a Circle, CircleMarker or Marker object with your wanted options. The `style_function` and `highlight_function` will also target the marker object you passed. embed: bool, default True Whether to embed the data in the html file or not. Note that disabling embedding is only supported if you provide a file link or URL. zoom_on_click: bool, default False Set to True to enable zooming in on a geometry when clicking on it. Examples -------- >>> # Providing filename that shall be embedded. >>> GeoJson("foo.json") >>> # Providing filename that shall not be embedded. >>> GeoJson("foo.json", embed=False) >>> # Providing dict. >>> GeoJson(json.load(open("foo.json"))) >>> # Providing string. >>> GeoJson(open("foo.json").read()) >>> # Provide a style_function that color all states green but Alabama. >>> style_function = lambda x: { ... "fillColor": "#0000ff" ... if x["properties"]["name"] == "Alabama" ... else "#00ff00" ... } >>> GeoJson(geojson, style_function=style_function) """ _template = Template( """ {% macro script(this, kwargs) %} {%- if this.style %} function {{ this.get_name() }}_styler(feature) { switch({{ this.feature_identifier }}) { {%- for style, ids_list in this.style_map.items() if not style == 'default' %} {% for id_val in ids_list %}case {{ id_val|tojson }}: {% endfor %} return {{ style }}; {%- endfor %} default: return {{ this.style_map['default'] }}; } } {%- endif %} {%- if this.highlight %} function {{ this.get_name() }}_highlighter(feature) { switch({{ this.feature_identifier }}) { {%- for style, ids_list in this.highlight_map.items() if not style == 'default' %} {% for id_val in ids_list %}case {{ id_val|tojson }}: {% endfor %} return {{ style }}; {%- endfor %} default: return {{ this.highlight_map['default'] }}; } } {%- endif %} {%- if this.marker %} function {{ this.get_name() }}_pointToLayer(feature, latlng) { var opts = {{ this.marker.options | tojson | safe }}; {% if this.marker._name == 'Marker' and this.marker.icon %} const iconOptions = {{ this.marker.icon.options | tojson | safe }} const iconRootAlias = L{%- if this.marker.icon._name == "Icon" %}.AwesomeMarkers{%- endif %} opts.icon = new iconRootAlias.{{ this.marker.icon._name }}(iconOptions) {% endif %} {%- if this.style_function %} let style = {{ this.get_name()}}_styler(feature) Object.assign({%- if this.marker.icon -%}opts.icon.options{%- else -%} opts {%- endif -%}, style) {% endif %} return new L.{{this.marker._name}}(latlng, opts) } {%- endif %} function {{this.get_name()}}_onEachFeature(feature, layer) { layer.on({ {%- if this.highlight %} mouseout: function(e) { if(typeof e.target.setStyle === "function"){ {{ this.get_name() }}.resetStyle(e.target); } }, mouseover: function(e) { if(typeof e.target.setStyle === "function"){ const highlightStyle = {{ this.get_name() }}_highlighter(e.target.feature) e.target.setStyle(highlightStyle); } }, {%- endif %} {%- if this.zoom_on_click %} click: function(e) { if (typeof e.target.getBounds === 'function') { {{ this.parent_map.get_name() }}.fitBounds(e.target.getBounds()); } else if (typeof e.target.getLatLng === 'function'){ let zoom = {{ this.parent_map.get_name() }}.getZoom() zoom = zoom > 12 ? zoom : zoom + 1 {{ this.parent_map.get_name() }}.flyTo(e.target.getLatLng(), zoom) } } {%- endif %} }); }; var {{ this.get_name() }} = L.geoJson(null, { {%- if this.smooth_factor is not none %} smoothFactor: {{ this.smooth_factor|tojson }}, {%- endif %} onEachFeature: {{ this.get_name() }}_onEachFeature, {% if this.style %} style: {{ this.get_name() }}_styler, {%- endif %} {%- if this.marker %} pointToLayer: {{ this.get_name() }}_pointToLayer {%- endif %} }); function {{ this.get_name() }}_add (data) { {{ this.get_name() }} .addData(data) .addTo({{ this._parent.get_name() }}); } {%- if this.embed %} {{ this.get_name() }}_add({{ this.data|tojson }}); {%- else %} $.ajax({{ this.embed_link|tojson }}, {dataType: 'json', async: false}) .done({{ this.get_name() }}_add); {%- endif %} {% endmacro %} """ ) # noqa def __init__( self, data, style_function=None, highlight_function=None, # noqa name=None, overlay=True, control=True, show=True, smooth_factor=None, tooltip=None, embed=True, popup=None, zoom_on_click=False, marker=None, ): super().__init__(name=name, overlay=overlay, control=control, show=show) self._name = "GeoJson" self.embed = embed self.embed_link = None self.json = None self.parent_map = None self.smooth_factor = smooth_factor self.style = style_function is not None self.highlight = highlight_function is not None self.zoom_on_click = zoom_on_click if marker: if not isinstance(marker, (Circle, CircleMarker, Marker)): raise TypeError( "Only Marker, Circle, and CircleMarker are supported as GeoJson marker types." ) self.marker = marker self.data = self.process_data(data) if self.style or self.highlight: self.convert_to_feature_collection() if self.style: self._validate_function(style_function, "style_function") self.style_function = style_function self.style_map = {} if self.highlight: self._validate_function(highlight_function, "highlight_function") self.highlight_function = highlight_function self.highlight_map = {} self.feature_identifier = self.find_identifier() if isinstance(tooltip, (GeoJsonTooltip, Tooltip)): self.add_child(tooltip) elif tooltip is not None: self.add_child(Tooltip(tooltip)) if isinstance(popup, (GeoJsonPopup, Popup)): self.add_child(popup) def process_data(self, data): """Convert an unknown data input into a geojson dictionary.""" if isinstance(data, dict): self.embed = True return data elif isinstance(data, str): if data.lower().startswith(("http:", "ftp:", "https:")): if not self.embed: self.embed_link = data return self.get_geojson_from_web(data) elif data.lstrip()[0] in "[{": # This is a GeoJSON inline string self.embed = True return json.loads(data) else: # This is a filename if not self.embed: self.embed_link = data with open(data) as f: return json.loads(f.read()) elif hasattr(data, "__geo_interface__"): self.embed = True if hasattr(data, "to_crs"): data = data.to_crs("EPSG:4326") return json.loads(json.dumps(data.__geo_interface__)) else: raise ValueError( "Cannot render objects with any missing geometries" ": {!r}".format(data) ) def get_geojson_from_web(self, url): return requests.get(url).json() def convert_to_feature_collection(self): """Convert data into a FeatureCollection if it is not already.""" if self.data["type"] == "FeatureCollection": return if not self.embed: raise ValueError( "Data is not a FeatureCollection, but it should be to apply " "style or highlight. Because `embed=False` it cannot be " "converted into one.\nEither change your geojson data to a " "FeatureCollection, set `embed=True` or disable styling." ) # Catch case when GeoJSON is just a single Feature or a geometry. if "geometry" not 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]} def _validate_function(self, func, name): """ Tests `self.style_function` and `self.highlight_function` to ensure they are functions returning dictionaries. """ # If for some reason there are no features (e.g., empty API response) # don't attempt validation if not self.data["features"]: return test_feature = self.data["features"][0] if not callable(func) or not isinstance(func(test_feature), dict): raise ValueError( "{} should be a function that accepts items from " "data['features'] and returns a dictionary.".format(name) ) def find_identifier(self): """Find a unique identifier for each feature, create it if needed. According to the GeoJSON specs a feature: - MAY have an 'id' field with a string or numerical value. - MUST have a 'properties' field. The content can be any json object or even null. """ feats = self.data["features"] # Each feature has an 'id' field with a unique value. unique_ids = {feat.get("id", None) for feat in feats} if None not in unique_ids and len(unique_ids) == len(feats): return "feature.id" # Each feature has a unique string or int property. if all(isinstance(feat.get("properties", None), dict) for feat in feats): for key in feats[0]["properties"]: unique_values = { feat["properties"].get(key, None) for feat in feats if isinstance(feat["properties"].get(key, None), (str, int)) } if len(unique_values) == len(feats): return f"feature.properties.{key}" # We add an 'id' field with a unique value to the data. if self.embed: for i, feature in enumerate(feats): feature["id"] = str(i) return "feature.id" raise ValueError( "There is no unique identifier for each feature and because " "`embed=False` it cannot be added. Consider adding an `id` " "field to your geojson data or set `embed=True`. " ) 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 get_bounds(self.data, lonlat=True) def render(self, **kwargs): self.parent_map = get_obj_in_upper_tree(self, Map) # Need at least one feature, otherwise style mapping fails if (self.style or self.highlight) and self.data["features"]: mapper = GeoJsonStyleMapper(self.data, self.feature_identifier, self) if self.style: self.style_map = mapper.get_style_map(self.style_function) if self.highlight: self.highlight_map = mapper.get_highlight_map(self.highlight_function) super().render() class GeoJsonStyleMapper: """Create dicts that map styling to GeoJson features. Used in the GeoJson class. Users don't have to call this class directly. """ def __init__(self, data, feature_identifier, geojson_obj): self.data = data self.feature_identifier = feature_identifier self.geojson_obj = geojson_obj def get_style_map(self, style_function): """Return a dict that maps style parameters to features.""" return self._create_mapping(style_function, "style") def get_highlight_map(self, highlight_function): """Return a dict that maps highlight parameters to features.""" return self._create_mapping(highlight_function, "highlight") def _create_mapping(self, func, switch): """Internal function to create the mapping.""" mapping = {} for feature in self.data["features"]: content = func(feature) if switch == "style": for key, value in content.items(): if isinstance(value, MacroElement): # Make sure objects are rendered: if value._parent is None: value._parent = self.geojson_obj value.render() # Replace objects with their Javascript var names: content[key] = "{{'" + value.get_name() + "'}}" key = self._to_key(content) mapping.setdefault(key, []).append(self.get_feature_id(feature)) self._set_default_key(mapping) return mapping def get_feature_id(self, feature): """Return a value identifying the feature.""" fields = self.feature_identifier.split(".")[1:] return functools.reduce(operator.getitem, fields, feature) @staticmethod def _to_key(d): """Convert dict to str and enable Jinja2 template syntax.""" as_str = json.dumps(d, sort_keys=True) return as_str.replace('"{{', "{{").replace('}}"', "}}") @staticmethod def _set_default_key(mapping): """Replace the field with the most features with a 'default' field.""" key_longest = sorted([(len(v), k) for k, v in mapping.items()], reverse=True)[ 0 ][1] mapping["default"] = key_longest del mapping[key_longest] class TopoJson(JSCSSMixin, Layer): """ Creates a TopoJson object for plotting into a Map. Parameters ---------- data: file, dict or str. The TopoJSON data you want to plot. * If file, then data will be read in the file and fully embedded in Leaflet's JavaScript. * If dict, then data will be converted to JSON and embedded in the JavaScript. * If str, then data will be passed to the JavaScript as-is. object_path: str The path of the desired object into the TopoJson structure. Ex: 'objects.myobject'. style_function: function, default None A function mapping a TopoJson geometry to a style dict. name : string, default None The name of the Layer, as it will appear in LayerControls overlay : bool, default False Adds the layer as an optional overlay (True) or the base layer (False). control : bool, default True Whether the Layer will be included in LayerControls. show: bool, default True Whether the layer will be shown on opening (only for overlays). smooth_factor: float, default None How much to simplify the polyline on each zoom level. More means better performance and smoother look, and less means more accurate representation. Leaflet defaults to 1.0. tooltip: GeoJsonTooltip, Tooltip or str, default None Display a text when hovering over the object. Can utilize the data, see folium.GeoJsonTooltip for info on how to do that. Examples -------- >>> # Providing file that shall be embedded. >>> TopoJson(open("foo.json"), "object.myobject") >>> # Providing filename that shall not be embedded. >>> TopoJson("foo.json", "object.myobject") >>> # Providing dict. >>> TopoJson(json.load(open("foo.json")), "object.myobject") >>> # Providing string. >>> TopoJson(open("foo.json").read(), "object.myobject") >>> # Provide a style_function that color all states green but Alabama. >>> style_function = lambda x: { ... "fillColor": "#0000ff" ... if x["properties"]["name"] == "Alabama" ... else "#00ff00" ... } >>> TopoJson(topo_json, "object.myobject", style_function=style_function) """ _template = Template( """ {% macro script(this, kwargs) %} var {{ this.get_name() }}_data = {{ this.data|tojson }}; var {{ this.get_name() }} = L.geoJson( topojson.feature( {{ this.get_name() }}_data, {{ this.get_name() }}_data{{ this._safe_object_path }} ), { {%- if this.smooth_factor is not none %} smoothFactor: {{ this.smooth_factor|tojson }}, {%- endif %} } ).addTo({{ this._parent.get_name() }}); {{ this.get_name() }}.setStyle(function(feature) { return feature.properties.style; }); {% endmacro %} """ ) # noqa default_js = [ ( "topojson", "https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.9/topojson.min.js", ), ] def __init__( self, data, object_path, style_function=None, name=None, overlay=True, control=True, show=True, smooth_factor=None, tooltip=None, ): super().__init__(name=name, overlay=overlay, control=control, show=show) self._name = "TopoJson" if "read" in dir(data): self.embed = True self.data = json.load(data) elif type(data) is dict: self.embed = True self.data = data else: self.embed = False self.data = data self.object_path = object_path self._safe_object_path = javascript_identifier_path_to_array_notation( object_path ) if style_function is None: def style_function(x): return {} self.style_function = style_function self.smooth_factor = smooth_factor if isinstance(tooltip, (GeoJsonTooltip, Tooltip)): self.add_child(tooltip) elif tooltip is not None: self.add_child(Tooltip(tooltip)) def style_data(self): """Applies self.style_function to each feature of self.data.""" def recursive_get(data, keys): if len(keys): return recursive_get(data.get(keys[0]), keys[1:]) else: return data geometries = recursive_get(self.data, self.object_path.split("."))[ "geometries" ] # noqa for feature in geometries: feature.setdefault("properties", {}).setdefault("style", {}).update( self.style_function(feature) ) # noqa def render(self, **kwargs): """Renders the HTML representation of the element.""" self.style_data() super().render(**kwargs) def get_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 TopoJSON.") xmin, xmax, ymin, ymax = None, None, None, None for arc in self.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 [ [ self.data["transform"]["translate"][1] + self.data["transform"]["scale"][1] * ymin, # noqa self.data["transform"]["translate"][0] + self.data["transform"]["scale"][0] * xmin, # noqa ], [ self.data["transform"]["translate"][1] + self.data["transform"]["scale"][1] * ymax, # noqa self.data["transform"]["translate"][0] + self.data["transform"]["scale"][0] * xmax, # noqa ], ] class GeoJsonDetail(MacroElement): """ Base class for GeoJsonTooltip and GeoJsonPopup to inherit methods and template structure from. Not for direct usage. """ base_template = """ function(layer){ let div = L.DomUtil.create('div'); {% if this.fields %} let handleObject = feature=>typeof(feature)=='object' ? JSON.stringify(feature) : feature; let fields = {{ this.fields | tojson | safe }}; let aliases = {{ this.aliases | tojson | safe }}; let table = '${aliases[i]{% if this.localize %}.toLocaleString(){% endif %}} | {% endif %}${handleObject(layer.feature.properties[v]){% if this.localize %}.toLocaleString(){% endif %}} |
---|