diff --git a/folium/features.py b/folium/features.py index bca5c6d70..085c45a2f 100644 --- a/folium/features.py +++ b/folium/features.py @@ -7,7 +7,18 @@ import json import operator import warnings -from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Literal, + Optional, + Sequence, + Tuple, + Union, +) import numpy as np import requests @@ -20,6 +31,7 @@ from folium.folium import Map from folium.map import FeatureGroup, Icon, Layer, Marker, Popup, Tooltip from folium.utilities import ( + JsCode, TypeJsonValue, TypeLine, TypePathOptions, @@ -1973,3 +1985,65 @@ def __init__( out.setdefault(cm(color), []).append([[lat1, lng1], [lat2, lng2]]) for key, val in out.items(): self.add_child(PolyLine(val, color=key, weight=weight, opacity=opacity)) + + +class Control(MacroElement): + """ + Add a Leaflet Control object to the map + + Parameters + ---------- + control: str + The javascript class name of the control to be rendered. + position: str + One of "bottomright", "bottomleft", "topright", "topleft" + + Examples + -------- + + >>> import folium + >>> from folium.features import Control, Marker + >>> from folium.plugins import Geocoder + + >>> m = folium.Map( + ... location=[46.603354, 1.8883335], attr=None, zoom_control=False, zoom_start=5 + ... ) + >>> Control("Zoom", position="topleft").add_to(m) + """ + + _template = Template( + """ + {% macro script(this, kwargs) %} + var {{ this.get_name() }}_options = {{ this.options|tojson }}; + {% for key, value in this.functions.items() %} + {{ this.get_name() }}_options["{{key}}"] = {{ value }}; + {% endfor %} + + var {{ this.get_name() }} = new L.Control.{{this._name}}( + {{ this.get_name() }}_options + ).addTo({{ this._parent.get_name() }}); + {% endmacro %} + """ + ) + + def __init__( + self, + control: Optional[str] = None, + position: Literal[ + "bottomright", "bottomleft", "topright", "topleft" + ] = "bottomleft", + **kwargs, + ): + super().__init__() + if control: + self._name = control + kwargs["position"] = position + + # extract JsCode objects + self.functions = {} + for key, value in list(kwargs.items()): + if isinstance(value, JsCode): + self.functions[camelize(key)] = value.js_code + kwargs.pop(key) + + self.options = parse_options(**kwargs) diff --git a/folium/plugins/beautify_icon.py b/folium/plugins/beautify_icon.py index d45e9eb58..99a8e1e6c 100644 --- a/folium/plugins/beautify_icon.py +++ b/folium/plugins/beautify_icon.py @@ -95,7 +95,7 @@ def __init__( inner_icon_style="", spin=False, number=None, - **kwargs + **kwargs, ): super().__init__() self._name = "BeautifyIcon" @@ -111,5 +111,5 @@ def __init__( spin=spin, isAlphaNumericIcon=number is not None, text=number, - **kwargs + **kwargs, ) diff --git a/folium/plugins/draw.py b/folium/plugins/draw.py index 95155f8e1..2e8faff05 100644 --- a/folium/plugins/draw.py +++ b/folium/plugins/draw.py @@ -1,10 +1,11 @@ -from branca.element import Element, Figure, MacroElement +from branca.element import Element, Figure from jinja2 import Template from folium.elements import JSCSSMixin +from folium.features import Control -class Draw(JSCSSMixin, MacroElement): +class Draw(JSCSSMixin, Control): """ Vector drawing and editing plugin for Leaflet. diff --git a/folium/plugins/fullscreen.py b/folium/plugins/fullscreen.py index 84ab7e730..ce2812254 100644 --- a/folium/plugins/fullscreen.py +++ b/folium/plugins/fullscreen.py @@ -1,11 +1,11 @@ -from branca.element import MacroElement from jinja2 import Template from folium.elements import JSCSSMixin +from folium.features import Control from folium.utilities import parse_options -class Fullscreen(JSCSSMixin, MacroElement): +class Fullscreen(JSCSSMixin, Control): """ Adds a fullscreen button to your map. @@ -56,7 +56,7 @@ def __init__( title="Full Screen", title_cancel="Exit Full Screen", force_separate_button=False, - **kwargs + **kwargs, ): super().__init__() self._name = "Fullscreen" @@ -65,5 +65,5 @@ def __init__( title=title, title_cancel=title_cancel, force_separate_button=force_separate_button, - **kwargs + **kwargs, ) diff --git a/folium/plugins/geocoder.py b/folium/plugins/geocoder.py index 3d267a05d..b950f24f2 100644 --- a/folium/plugins/geocoder.py +++ b/folium/plugins/geocoder.py @@ -1,13 +1,13 @@ from typing import Optional -from branca.element import MacroElement from jinja2 import Template from folium.elements import JSCSSMixin +from folium.features import Control from folium.utilities import parse_options -class Geocoder(JSCSSMixin, MacroElement): +class Geocoder(JSCSSMixin, Control): """A simple geocoder for Leaflet that by default uses OSM/Nominatim. Please respect the Nominatim usage policy: @@ -78,7 +78,7 @@ def __init__( zoom: Optional[int] = 11, provider: str = "nominatim", provider_options: dict = {}, - **kwargs + **kwargs, ): super().__init__() self._name = "Geocoder" @@ -89,5 +89,5 @@ def __init__( zoom=zoom, provider=provider, provider_options=provider_options, - **kwargs + **kwargs, ) diff --git a/folium/plugins/groupedlayercontrol.py b/folium/plugins/groupedlayercontrol.py index 6745f8aa4..848ce9238 100644 --- a/folium/plugins/groupedlayercontrol.py +++ b/folium/plugins/groupedlayercontrol.py @@ -1,11 +1,11 @@ -from branca.element import MacroElement from jinja2 import Template from folium.elements import JSCSSMixin +from folium.features import Control from folium.utilities import parse_options -class GroupedLayerControl(JSCSSMixin, MacroElement): +class GroupedLayerControl(JSCSSMixin, Control): """ Create a Layer Control with groups of overlays. diff --git a/folium/plugins/heat_map.py b/folium/plugins/heat_map.py index cc6a981d5..5d022db30 100644 --- a/folium/plugins/heat_map.py +++ b/folium/plugins/heat_map.py @@ -74,7 +74,7 @@ def __init__( overlay=True, control=True, show=True, - **kwargs + **kwargs, ): super().__init__(name=name, overlay=overlay, control=control, show=show) self._name = "HeatMap" @@ -96,7 +96,7 @@ def __init__( radius=radius, blur=blur, gradient=gradient, - **kwargs + **kwargs, ) def _get_self_bounds(self): diff --git a/folium/plugins/locate_control.py b/folium/plugins/locate_control.py index fb1c825d1..aa02ced53 100644 --- a/folium/plugins/locate_control.py +++ b/folium/plugins/locate_control.py @@ -3,14 +3,14 @@ Based on leaflet plugin: https://github.com/domoritz/leaflet-locatecontrol """ -from branca.element import MacroElement from jinja2 import Template from folium.elements import JSCSSMixin +from folium.features import Control from folium.utilities import parse_options -class LocateControl(JSCSSMixin, MacroElement): +class LocateControl(JSCSSMixin, Control): """Control plugin to geolocate the user. This plugins adds a button to the map, and when it's clicked shows the current diff --git a/folium/plugins/marker_cluster.py b/folium/plugins/marker_cluster.py index b6460e088..a1dcbfe74 100644 --- a/folium/plugins/marker_cluster.py +++ b/folium/plugins/marker_cluster.py @@ -87,7 +87,7 @@ def __init__( show=True, icon_create_function=None, options=None, - **kwargs + **kwargs, ): if options is not None: kwargs.update(options) # options argument is legacy diff --git a/folium/plugins/measure_control.py b/folium/plugins/measure_control.py index 4b543dc2f..c28546fa2 100644 --- a/folium/plugins/measure_control.py +++ b/folium/plugins/measure_control.py @@ -1,11 +1,11 @@ -from branca.element import MacroElement from jinja2 import Template from folium.elements import JSCSSMixin +from folium.features import Control from folium.utilities import parse_options -class MeasureControl(JSCSSMixin, MacroElement): +class MeasureControl(JSCSSMixin, Control): """Add a measurement widget on the map. Parameters @@ -68,7 +68,7 @@ def __init__( secondary_length_unit="miles", primary_area_unit="sqmeters", secondary_area_unit="acres", - **kwargs + **kwargs, ): super().__init__() self._name = "MeasureControl" @@ -79,5 +79,5 @@ def __init__( secondary_length_unit=secondary_length_unit, primary_area_unit=primary_area_unit, secondary_area_unit=secondary_area_unit, - **kwargs + **kwargs, ) diff --git a/folium/plugins/minimap.py b/folium/plugins/minimap.py index 999cabd25..a00410af5 100644 --- a/folium/plugins/minimap.py +++ b/folium/plugins/minimap.py @@ -1,12 +1,12 @@ -from branca.element import MacroElement from jinja2 import Template from folium.elements import JSCSSMixin +from folium.features import Control from folium.raster_layers import TileLayer from folium.utilities import parse_options -class MiniMap(JSCSSMixin, MacroElement): +class MiniMap(JSCSSMixin, Control): """Add a minimap (locator) to an existing map. Uses the Leaflet plugin by Norkart under BSD 2-Clause "Simplified" License. @@ -103,7 +103,7 @@ def __init__( toggle_display=False, auto_toggle_display=False, minimized=False, - **kwargs + **kwargs, ): super().__init__() self._name = "MiniMap" @@ -128,5 +128,5 @@ def __init__( toggle_display=toggle_display, auto_toggle_display=auto_toggle_display, minimized=minimized, - **kwargs + **kwargs, ) diff --git a/folium/plugins/mouse_position.py b/folium/plugins/mouse_position.py index 2c1b1b6f3..ef8b8b895 100644 --- a/folium/plugins/mouse_position.py +++ b/folium/plugins/mouse_position.py @@ -1,11 +1,9 @@ -from branca.element import MacroElement -from jinja2 import Template - from folium.elements import JSCSSMixin -from folium.utilities import parse_options +from folium.features import Control +from folium.utilities import JsCode -class MousePosition(JSCSSMixin, MacroElement): +class MousePosition(JSCSSMixin, Control): """Add a field that shows the coordinates of the mouse position. Uses the Leaflet plugin by Ardhi Lukianto under MIT license. @@ -45,34 +43,6 @@ class MousePosition(JSCSSMixin, MacroElement): """ - _template = Template( - """ - {% macro script(this, kwargs) %} - var {{ this.get_name() }} = new L.Control.MousePosition( - {{ this.options|tojson }} - ); - {{ this.get_name() }}.options["latFormatter"] = - {{ this.lat_formatter }}; - {{ this.get_name() }}.options["lngFormatter"] = - {{ this.lng_formatter }}; - {{ this._parent.get_name() }}.addControl({{ this.get_name() }}); - {% endmacro %} - """ - ) - - default_js = [ - ( - "Control_MousePosition_js", - "https://cdn.jsdelivr.net/gh/ardhi/Leaflet.MousePosition/src/L.Control.MousePosition.min.js", - ) - ] - default_css = [ - ( - "Control_MousePosition_css", - "https://cdn.jsdelivr.net/gh/ardhi/Leaflet.MousePosition/src/L.Control.MousePosition.min.css", - ) - ] - def __init__( self, position="bottomright", @@ -83,19 +53,25 @@ def __init__( prefix="", lat_formatter=None, lng_formatter=None, - **kwargs + **kwargs, ): - super().__init__() - self._name = "MousePosition" - - self.options = parse_options( + super().__init__( + control="MousePosition", position=position, separator=separator, empty_string=empty_string, lng_first=lng_first, num_digits=num_digits, prefix=prefix, - **kwargs + lat_formatter=JsCode(lat_formatter) if lat_formatter else None, + lng_formatter=JsCode(lng_formatter) if lng_formatter else None, + **kwargs, + ) + self.add_js_link( + "Control_MousePosition_js", + "https://cdn.jsdelivr.net/gh/ardhi/Leaflet.MousePosition/src/L.Control.MousePosition.min.js", + ) + self.add_css_link( + "Control_MousePosition_css", + "https://cdn.jsdelivr.net/gh/ardhi/Leaflet.MousePosition/src/L.Control.MousePosition.min.css", ) - self.lat_formatter = lat_formatter or "undefined" - self.lng_formatter = lng_formatter or "undefined" diff --git a/folium/plugins/pattern.py b/folium/plugins/pattern.py index 2f90d32df..d5d139b33 100644 --- a/folium/plugins/pattern.py +++ b/folium/plugins/pattern.py @@ -55,7 +55,7 @@ def __init__( space_color="#ffffff", opacity=0.75, space_opacity=0.0, - **kwargs + **kwargs, ): super().__init__() self._name = "StripePattern" @@ -67,7 +67,7 @@ def __init__( space_color=space_color, opacity=opacity, space_opacity=space_opacity, - **kwargs + **kwargs, ) self.parent_map = None diff --git a/folium/plugins/polyline_text_path.py b/folium/plugins/polyline_text_path.py index 660d15909..826193be2 100644 --- a/folium/plugins/polyline_text_path.py +++ b/folium/plugins/polyline_text_path.py @@ -63,7 +63,7 @@ def __init__( offset=0, orientation=0, attributes=None, - **kwargs + **kwargs, ): super().__init__() self._name = "PolyLineTextPath" @@ -76,5 +76,5 @@ def __init__( offset=offset, orientation=orientation, attributes=attributes, - **kwargs + **kwargs, ) diff --git a/folium/plugins/realtime.py b/folium/plugins/realtime.py index d7f99594d..69af3c217 100644 --- a/folium/plugins/realtime.py +++ b/folium/plugins/realtime.py @@ -1,14 +1,14 @@ from typing import Optional, Union -from branca.element import MacroElement from jinja2 import Template from folium.elements import JSCSSMixin +from folium.features import GeoJson from folium.map import Layer from folium.utilities import JsCode, camelize, parse_options -class Realtime(JSCSSMixin, MacroElement): +class Realtime(JSCSSMixin, Layer): """Put realtime data on a Leaflet map: live tracking GPS units, sensor data or just about anything. @@ -42,7 +42,7 @@ class Realtime(JSCSSMixin, MacroElement): remove_missing: bool, default False Should missing features between updates been automatically removed from the layer - container: Layer, default GeoJson + container: FeatureGroup, default GeoJson The container will typically be a `FeatureGroup`, `MarkerCluster` or `GeoJson`, but it can be anything that generates a javascript L.LayerGroup object, i.e. something that has the methods @@ -109,8 +109,8 @@ def __init__( get_feature_id: Union[JsCode, str, None] = None, update_feature: Union[JsCode, str, None] = None, remove_missing: bool = False, - container: Optional[Layer] = None, - **kwargs + container: Optional[GeoJson] = None, + **kwargs, ): super().__init__() self._name = "Realtime" diff --git a/folium/plugins/search.py b/folium/plugins/search.py index b477456b5..618a74ff4 100644 --- a/folium/plugins/search.py +++ b/folium/plugins/search.py @@ -1,14 +1,13 @@ -from branca.element import MacroElement from jinja2 import Template from folium import Map from folium.elements import JSCSSMixin -from folium.features import FeatureGroup, GeoJson, TopoJson +from folium.features import Control, FeatureGroup, GeoJson, TopoJson from folium.plugins import MarkerCluster from folium.utilities import parse_options -class Search(JSCSSMixin, MacroElement): +class Search(JSCSSMixin, Control): """ Adds a search tool to your map. diff --git a/folium/plugins/semicircle.py b/folium/plugins/semicircle.py index 6e85b1b1b..9b7edcdd3 100644 --- a/folium/plugins/semicircle.py +++ b/folium/plugins/semicircle.py @@ -68,7 +68,7 @@ def __init__( stop_angle=None, popup=None, tooltip=None, - **kwargs + **kwargs, ): super().__init__(location, popup=popup, tooltip=tooltip) self._name = "SemiCircle" diff --git a/folium/plugins/tag_filter_button.py b/folium/plugins/tag_filter_button.py index 4fe342a89..dbd7fcd37 100644 --- a/folium/plugins/tag_filter_button.py +++ b/folium/plugins/tag_filter_button.py @@ -1,11 +1,11 @@ -from branca.element import MacroElement from jinja2 import Template from folium.elements import JSCSSMixin +from folium.features import Control from folium.utilities import parse_options -class TagFilterButton(JSCSSMixin, MacroElement): +class TagFilterButton(JSCSSMixin, Control): """ Creates a Tag Filter Button to filter elements based on criteria (https://github.com/maydemirx/leaflet-tag-filter-button) @@ -82,7 +82,7 @@ def __init__( clear_text="clear", filter_on_every_click=True, open_popup_on_hover=False, - **kwargs + **kwargs, ): super().__init__() self._name = "TagFilterButton" @@ -92,5 +92,5 @@ def __init__( clear_text=clear_text, filter_on_every_click=filter_on_every_click, open_popup_on_hover=open_popup_on_hover, - **kwargs + **kwargs, ) diff --git a/folium/plugins/timeline.py b/folium/plugins/timeline.py index 77ba81cb2..d9d2fbccd 100644 --- a/folium/plugins/timeline.py +++ b/folium/plugins/timeline.py @@ -112,7 +112,7 @@ def __init__( self, data: Union[dict, str, TextIO], get_interval: Optional[JsCode] = None, - **kwargs + **kwargs, ): super().__init__(data) self._name = "Timeline" @@ -232,7 +232,7 @@ def __init__( show_ticks: bool = True, steps: int = 1000, playback_duration: int = 10000, - **kwargs + **kwargs, ): super().__init__() self._name = "TimelineSlider" diff --git a/folium/plugins/timestamped_geo_json.py b/folium/plugins/timestamped_geo_json.py index 3fe66ac04..d99d657fc 100644 --- a/folium/plugins/timestamped_geo_json.py +++ b/folium/plugins/timestamped_geo_json.py @@ -1,14 +1,14 @@ import json -from branca.element import MacroElement from jinja2 import Template from folium.elements import JSCSSMixin +from folium.features import Control from folium.folium import Map from folium.utilities import get_bounds, parse_options -class TimestampedGeoJson(JSCSSMixin, MacroElement): +class TimestampedGeoJson(JSCSSMixin, Control): """ Creates a TimestampedGeoJson plugin from timestamped GeoJSONs to append into a map with Map.add_child. diff --git a/folium/plugins/timestamped_wmstilelayer.py b/folium/plugins/timestamped_wmstilelayer.py index 6f1147638..375b7490b 100644 --- a/folium/plugins/timestamped_wmstilelayer.py +++ b/folium/plugins/timestamped_wmstilelayer.py @@ -1,12 +1,12 @@ -from branca.element import MacroElement from jinja2 import Template from folium.elements import JSCSSMixin +from folium.features import Control from folium.raster_layers import WmsTileLayer from folium.utilities import parse_options -class TimestampedWmsTileLayers(JSCSSMixin, MacroElement): +class TimestampedWmsTileLayers(JSCSSMixin, Control): """ Creates a TimestampedWmsTileLayer that takes a WmsTileLayer and adds time control with the Leaflet.TimeDimension plugin. diff --git a/folium/plugins/treelayercontrol.py b/folium/plugins/treelayercontrol.py index ff1af6994..59d3c7756 100644 --- a/folium/plugins/treelayercontrol.py +++ b/folium/plugins/treelayercontrol.py @@ -146,7 +146,7 @@ def __init__( collapse_all: str = "", expand_all: str = "", label_is_selector: str = "both", - **kwargs + **kwargs, ): super().__init__() self._name = "TreeLayerControl" diff --git a/folium/vector_layers.py b/folium/vector_layers.py index 9f535467a..be7a1decb 100644 --- a/folium/vector_layers.py +++ b/folium/vector_layers.py @@ -230,7 +230,7 @@ def __init__( locations: TypeMultiLine, popup: Union[Popup, str, None] = None, tooltip: Union[Tooltip, str, None] = None, - **kwargs: TypePathOptions + **kwargs: TypePathOptions, ): super().__init__(locations, popup=popup, tooltip=tooltip) self._name = "Polygon" @@ -272,7 +272,7 @@ def __init__( bounds: TypeLine, popup: Union[Popup, str, None] = None, tooltip: Union[Tooltip, str, None] = None, - **kwargs: TypePathOptions + **kwargs: TypePathOptions, ): super().__init__() self._name = "rectangle" @@ -333,7 +333,7 @@ def __init__( radius: float = 50, popup: Union[Popup, str, None] = None, tooltip: Union[Tooltip, str, None] = None, - **kwargs: TypePathOptions + **kwargs: TypePathOptions, ): super().__init__(location, popup=popup, tooltip=tooltip) self._name = "circle" @@ -379,7 +379,7 @@ def __init__( radius: float = 10, popup: Union[Popup, str, None] = None, tooltip: Union[Tooltip, str, None] = None, - **kwargs: TypePathOptions + **kwargs: TypePathOptions, ): super().__init__(location, popup=popup, tooltip=tooltip) self._name = "CircleMarker"