From 74dadcbea75b8144acf7e338e4164d95241c1dcd Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sun, 6 Apr 2025 19:21:03 +0200 Subject: [PATCH 1/4] Add leaflet control --- folium/__init__.py | 2 ++ folium/features.py | 63 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/folium/__init__.py b/folium/__init__.py index 01a0b66077..c6fa376e4c 100644 --- a/folium/__init__.py +++ b/folium/__init__.py @@ -17,6 +17,7 @@ ClickForLatLng, ClickForMarker, ColorLine, + Control, CustomIcon, DivIcon, GeoJson, @@ -64,6 +65,7 @@ "ClickForLatLng", "ColorLine", "ColorMap", + "Control", "CssLink", "CustomIcon", "Div", diff --git a/folium/features.py b/folium/features.py index 1c18f426db..7e0498efe0 100644 --- a/folium/features.py +++ b/folium/features.py @@ -7,7 +7,17 @@ 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, + Optional, + Sequence, + Tuple, + Union, +) import numpy as np import requests @@ -1992,3 +2002,54 @@ 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(JSCSSMixin, 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() }} = new L.Control.{{this._name}}( + {% for arg in this.args %} + {{ arg | tojavascript }}, + {% endfor %} + {{ this.options|tojavascript }} + ).addTo({{ this._parent.get_name() }}); + {% endmacro %} + """ + ) + + def __init__( + self, + control: Optional[str] = None, + *args, + **kwargs, + ): + super().__init__() + if control: + self._name = control + + self.args = args + self.options = remove_empty(**kwargs) From cf0e8817998cd668a004847e556344c26e2c695a Mon Sep 17 00:00:00 2001 From: Hans Then Date: Tue, 22 Apr 2025 14:36:54 +0200 Subject: [PATCH 2/4] Updated after review comments --- folium/features.py | 9 +++++++++ folium/utilities.py | 2 ++ 2 files changed, 11 insertions(+) diff --git a/folium/features.py b/folium/features.py index 7e0498efe0..ec4ef99172 100644 --- a/folium/features.py +++ b/folium/features.py @@ -17,6 +17,7 @@ Sequence, Tuple, Union, + get_args, ) import numpy as np @@ -43,6 +44,7 @@ TypeJsonValue, TypeLine, TypePathOptions, + TypePosition, _parse_size, escape_backticks, get_bounds, @@ -2045,11 +2047,18 @@ def __init__( self, control: Optional[str] = None, *args, + position: Optional[TypePosition] = None, **kwargs, ): super().__init__() if control: self._name = control + if position: + position = position.lower() + if position not in (args := get_args(TypePosition)): + raise TypeError(f"position must be one of {args}") + kwargs["position"] = position + self.args = args self.options = remove_empty(**kwargs) diff --git a/folium/utilities.py b/folium/utilities.py index 131492e453..eaf1bc4df8 100644 --- a/folium/utilities.py +++ b/folium/utilities.py @@ -16,6 +16,7 @@ Iterable, Iterator, List, + Literal, Optional, Sequence, Tuple, @@ -57,6 +58,7 @@ TypeBoundsReturn = List[List[Optional[float]]] TypeContainer = Union[Figure, Div, "Popup"] +TypePosition = Literal["bottomright", "bottomleft", "topright", "topleft"] _VALID_URLS = set(uses_relative + uses_netloc + uses_params) From 4a1e41920be72ae637459b1b3cba020063e51a24 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Tue, 22 Apr 2025 15:02:25 +0200 Subject: [PATCH 3/4] Fix some type errors --- folium/features.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/folium/features.py b/folium/features.py index ec4ef99172..2b90967155 100644 --- a/folium/features.py +++ b/folium/features.py @@ -1825,7 +1825,7 @@ def __init__(self, popup: Union[IFrame, Html, str, None] = None): if isinstance(popup, Element): popup = popup.render() if popup: - self.popup = "`" + escape_backticks(popup) + "`" + self.popup = "`" + escape_backticks(popup) + "`" # type: ignore else: self.popup = '"Latitude: " + lat + "
Longitude: " + lng ' @@ -2054,8 +2054,8 @@ def __init__( if control: self._name = control - if position: - position = position.lower() + if position is not None: + position = position.lower() # type: ignore if position not in (args := get_args(TypePosition)): raise TypeError(f"position must be one of {args}") kwargs["position"] = position From 0bb71dcf6b92c034a698013a6b8d8d6665501302 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Tue, 22 Apr 2025 17:41:41 +0200 Subject: [PATCH 4/4] Added test for Control typechecking --- tests/test_folium.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_folium.py b/tests/test_folium.py index 58ad28c978..2a945b056c 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -501,3 +501,29 @@ def test_json_request(self): np.testing.assert_allclose( bounds, [[18.948267, -178.123152], [71.351633, 173.304726]] ) + + def test_control_typecheck(self): + m = folium.Map( + location=[39.949610, -75.150282], zoom_start=5, zoom_control=False + ) + tiles = TileLayer( + tiles="OpenStreetMap", + show=False, + control=False, + ) + tiles.add_to(m) + + with pytest.raises(TypeError) as excinfo: + minimap = folium.Control("MiniMap", tiles, position="downunder") + minimap.add_js_link( + "minimap_js", + "https://cdnjs.cloudflare.com/ajax/libs/leaflet-minimap/3.6.1/Control.MiniMap.min.js", + ) + minimap.add_css_link( + "minimap_css", + "https://cdnjs.cloudflare.com/ajax/libs/leaflet-minimap/3.6.1/Control.MiniMap.css", + ) + minimap.add_to(m) + assert "position must be one of ('bottomright', 'bottomleft'" in str( + excinfo.value + )