""" Leaflet GeoJson and miscellaneous features. """ import json import warnings import functools import operator from branca.colormap import LinearColormap, StepColormap from branca.element import (Element, Figure, JavascriptLink, MacroElement) from branca.utilities import color_brewer from folium.elements import JSCSSMixin from folium.folium import Map from folium.map import (FeatureGroup, Icon, Layer, Marker, Tooltip, Popup) from folium.utilities import ( validate_locations, _parse_size, get_bounds, image_to_url, none_max, none_min, get_obj_in_upper_tree, parse_options, camelize ) from folium.vector_layers import Circle, CircleMarker, PolyLine, path_options from jinja2 import Template import numpy as np import requests 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(u""" {% 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(RegularPolygonMarker, self).__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(u'') 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(Vega, self).__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(u'') 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.""" vegalite_major_version = self._get_vegalite_major_versions(self.data) 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()) if vegalite_major_version == '1': self._embed_vegalite_v1(figure) elif vegalite_major_version == '2': self._embed_vegalite_v2(figure) elif vegalite_major_version == '3': self._embed_vegalite_v3(figure) else: # Version 2 is assumed as the default, if no version is given in the schema. self._embed_vegalite_v2(figure) def _get_vegalite_major_versions(self, spec): try: schema = spec['$schema'] except KeyError: major_version = None else: major_version = schema.split('/')[-1].split('.')[0].lstrip('v') return major_version 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(u""" {% 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(GeoJson, self).__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. """ 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 = set(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 = set( 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 'feature.properties.{}'.format(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) if self.style or self.highlight: 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(GeoJson, self).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(u""" {% 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.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(TopoJson, self).__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 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(TopoJson, self).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 = u""" 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 %}} |
---|