initial commit

This commit is contained in:
klein panic
2024-09-29 01:46:07 -04:00
commit ba6e6914d1
7576 changed files with 1356825 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
"""Wrap some of the most popular leaflet external plugins."""
from folium.plugins.antpath import AntPath
from folium.plugins.beautify_icon import BeautifyIcon
from folium.plugins.boat_marker import BoatMarker
from folium.plugins.draw import Draw
from folium.plugins.dual_map import DualMap
from folium.plugins.encoded import PolygonFromEncoded, PolyLineFromEncoded
from folium.plugins.fast_marker_cluster import FastMarkerCluster
from folium.plugins.feature_group_sub_group import FeatureGroupSubGroup
from folium.plugins.float_image import FloatImage
from folium.plugins.fullscreen import Fullscreen
from folium.plugins.geocoder import Geocoder
from folium.plugins.groupedlayercontrol import GroupedLayerControl
from folium.plugins.heat_map import HeatMap
from folium.plugins.heat_map_withtime import HeatMapWithTime
from folium.plugins.locate_control import LocateControl
from folium.plugins.marker_cluster import MarkerCluster
from folium.plugins.measure_control import MeasureControl
from folium.plugins.minimap import MiniMap
from folium.plugins.mouse_position import MousePosition
from folium.plugins.pattern import CirclePattern, StripePattern
from folium.plugins.polyline_offset import PolyLineOffset
from folium.plugins.polyline_text_path import PolyLineTextPath
from folium.plugins.realtime import Realtime
from folium.plugins.scroll_zoom_toggler import ScrollZoomToggler
from folium.plugins.search import Search
from folium.plugins.semicircle import SemiCircle
from folium.plugins.side_by_side import SideBySideLayers
from folium.plugins.tag_filter_button import TagFilterButton
from folium.plugins.terminator import Terminator
from folium.plugins.time_slider_choropleth import TimeSliderChoropleth
from folium.plugins.timeline import Timeline, TimelineSlider
from folium.plugins.timestamped_geo_json import TimestampedGeoJson
from folium.plugins.timestamped_wmstilelayer import TimestampedWmsTileLayers
from folium.plugins.treelayercontrol import TreeLayerControl
from folium.plugins.vectorgrid_protobuf import VectorGridProtobuf
__all__ = [
"AntPath",
"BeautifyIcon",
"BoatMarker",
"CirclePattern",
"Draw",
"DualMap",
"FastMarkerCluster",
"FeatureGroupSubGroup",
"FloatImage",
"Fullscreen",
"Geocoder",
"GroupedLayerControl",
"HeatMap",
"HeatMapWithTime",
"LocateControl",
"MarkerCluster",
"MeasureControl",
"MiniMap",
"MousePosition",
"PolygonFromEncoded",
"PolyLineFromEncoded",
"PolyLineTextPath",
"PolyLineOffset",
"Realtime",
"ScrollZoomToggler",
"Search",
"SemiCircle",
"SideBySideLayers",
"StripePattern",
"TagFilterButton",
"Terminator",
"TimeSliderChoropleth",
"Timeline",
"TimelineSlider",
"TimestampedGeoJson",
"TimestampedWmsTileLayers",
"TreeLayerControl",
"VectorGridProtobuf",
]

View File

@@ -0,0 +1,69 @@
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.vector_layers import BaseMultiLocation, path_options
class AntPath(JSCSSMixin, BaseMultiLocation):
"""
Class for drawing AntPath polyline overlays on a map.
See :func:`folium.vector_layers.path_options` for the `Path` options.
Parameters
----------
locations: list of points (latitude, longitude)
Latitude and Longitude of line (Northing, Easting)
popup: str or folium.Popup, default None
Input text or visualization for object displayed when clicking.
tooltip: str or folium.Tooltip, optional
Display a text when hovering over the object.
**kwargs:
Polyline and AntPath options. See their Github page for the
available parameters.
https://github.com/rubenspgcavalcante/leaflet-ant-path/
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
{{ this.get_name() }} = L.polyline.antPath(
{{ this.locations|tojson }},
{{ this.options|tojson }}
).addTo({{this._parent.get_name()}});
{% endmacro %}
"""
)
default_js = [
(
"antpath",
"https://cdn.jsdelivr.net/npm/leaflet-ant-path@1.1.2/dist/leaflet-ant-path.min.js",
)
]
def __init__(self, locations, popup=None, tooltip=None, **kwargs):
super().__init__(
locations,
popup=popup,
tooltip=tooltip,
)
self._name = "AntPath"
# Polyline + AntPath defaults.
self.options = path_options(line=True, **kwargs)
self.options.update(
{
"paused": kwargs.pop("paused", False),
"reverse": kwargs.pop("reverse", False),
"hardwareAcceleration": kwargs.pop("hardware_acceleration", False),
"delay": kwargs.pop("delay", 400),
"dashArray": kwargs.pop("dash_array", [10, 20]),
"weight": kwargs.pop("weight", 5),
"opacity": kwargs.pop("opacity", 0.5),
"color": kwargs.pop("color", "#0000FF"),
"pulseColor": kwargs.pop("pulse_color", "#FFFFFF"),
}
)

View File

@@ -0,0 +1,115 @@
from branca.element import MacroElement
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.utilities import parse_options
class BeautifyIcon(JSCSSMixin, MacroElement):
"""
Create a BeautifyIcon that can be added to a Marker
Parameters
----------
icon: string, default None
the Font-Awesome icon name to use to render the marker.
icon_shape: string, default None
the icon shape
border_width: integer, default 3
the border width of the icon
border_color: string with hexadecimal RGB, default '#000'
the border color of the icon
text_color: string with hexadecimal RGB, default '#000'
the text color of the icon
background_color: string with hexadecimal RGB, default '#FFF'
the background color of the icon
inner_icon_style: string with css styles for the icon, default ''
the css styles of the icon
spin: boolean, default False
allow the icon to be spinning.
number: integer, default None
the number of the icon.
Examples
--------
Plugin Website: https://github.com/masajid390/BeautifyMarker
>>> BeautifyIcon(
... text_color="#000", border_color="transparent", background_color="#FFF"
... ).add_to(marker)
>>> number_icon = BeautifyIcon(
... text_color="#000",
... border_color="transparent",
... background_color="#FFF",
... number=10,
... inner_icon_style="font-size:12px;padding-top:-5px;",
... )
>>> Marker(
... location=[45.5, -122.3],
... popup=folium.Popup("Portland, OR"),
... icon=number_icon,
... )
>>> BeautifyIcon(icon="arrow-down", icon_shape="marker").add_to(marker)
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
var {{ this.get_name() }} = new L.BeautifyIcon.icon(
{{ this.options|tojson }}
)
{{ this._parent.get_name() }}.setIcon({{ this.get_name() }});
{% endmacro %}
"""
)
ICON_SHAPE_TYPES = [
"circle",
"circle-dot",
"doughnut",
"rectangle-dot",
"marker",
None,
]
default_js = [
(
"beautify_icon_js",
"https://cdn.jsdelivr.net/gh/marslan390/BeautifyMarker/leaflet-beautify-marker-icon.min.js",
)
]
default_css = [
(
"beautify_icon_css",
"https://cdn.jsdelivr.net/gh/marslan390/BeautifyMarker/leaflet-beautify-marker-icon.min.css",
)
]
def __init__(
self,
icon=None,
icon_shape=None,
border_width=3,
border_color="#000",
text_color="#000",
background_color="#FFF",
inner_icon_style="",
spin=False,
number=None,
**kwargs
):
super().__init__()
self._name = "BeautifyIcon"
self.options = parse_options(
icon=icon,
icon_shape=icon_shape,
border_width=border_width,
border_color=border_color,
text_color=text_color,
background_color=background_color,
inner_icon_style=inner_icon_style,
spin=spin,
isAlphaNumericIcon=number is not None,
text=number,
**kwargs
)

View File

@@ -0,0 +1,70 @@
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.map import Marker
from folium.utilities import parse_options
class BoatMarker(JSCSSMixin, Marker):
"""Add a Marker in the shape of a boat.
Parameters
----------
location: tuple of length 2, default None
The latitude and longitude of the marker.
If None, then the middle of the map is used.
heading: int, default 0
Heading of the boat to an angle value between 0 and 360 degrees
wind_heading: int, default None
Heading of the wind to an angle value between 0 and 360 degrees
If None, then no wind is represented.
wind_speed: int, default 0
Speed of the wind in knots.
https://github.com/thomasbrueggemann/leaflet.boatmarker
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
var {{ this.get_name() }} = L.boatMarker(
{{ this.location|tojson }},
{{ this.options|tojson }}
).addTo({{ this._parent.get_name() }});
{% if this.wind_heading is not none -%}
{{ this.get_name() }}.setHeadingWind(
{{ this.heading }},
{{ this.wind_speed }},
{{ this.wind_heading }}
);
{% else -%}
{{this.get_name()}}.setHeading({{this.heading}});
{% endif -%}
{% endmacro %}
"""
)
default_js = [
(
"markerclusterjs",
"https://unpkg.com/leaflet.boatmarker/leaflet.boatmarker.min.js",
),
]
def __init__(
self,
location,
popup=None,
icon=None,
heading=0,
wind_heading=None,
wind_speed=0,
**kwargs
):
super().__init__(location, popup=popup, icon=icon)
self._name = "BoatMarker"
self.heading = heading
self.wind_heading = wind_heading
self.wind_speed = wind_speed
self.options = parse_options(**kwargs)

View File

@@ -0,0 +1,154 @@
from branca.element import Element, Figure, MacroElement
from jinja2 import Template
from folium.elements import JSCSSMixin
class Draw(JSCSSMixin, MacroElement):
"""
Vector drawing and editing plugin for Leaflet.
Parameters
----------
export : bool, default False
Add a small button that exports the drawn shapes as a geojson file.
filename : string, default 'data.geojson'
Name of geojson file
position : {'topleft', 'toprigth', 'bottomleft', 'bottomright'}
Position of control.
See https://leafletjs.com/reference.html#control
show_geometry_on_click : bool, default True
When True, opens an alert with the geometry description on click.
draw_options : dict, optional
The options used to configure the draw toolbar. See
http://leaflet.github.io/Leaflet.draw/docs/leaflet-draw-latest.html#drawoptions
edit_options : dict, optional
The options used to configure the edit toolbar. See
https://leaflet.github.io/Leaflet.draw/docs/leaflet-draw-latest.html#editpolyoptions
Examples
--------
>>> m = folium.Map()
>>> Draw(
... export=True,
... filename="my_data.geojson",
... position="topleft",
... draw_options={"polyline": {"allowIntersection": False}},
... edit_options={"poly": {"allowIntersection": False}},
... ).add_to(m)
For more info please check
https://leaflet.github.io/Leaflet.draw/docs/leaflet-draw-latest.html
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
var options = {
position: {{ this.position|tojson }},
draw: {{ this.draw_options|tojson }},
edit: {{ this.edit_options|tojson }},
}
// FeatureGroup is to store editable layers.
var drawnItems_{{ this.get_name() }} = new L.featureGroup().addTo(
{{ this._parent.get_name() }}
);
options.edit.featureGroup = drawnItems_{{ this.get_name() }};
var {{ this.get_name() }} = new L.Control.Draw(
options
).addTo( {{this._parent.get_name()}} );
{{ this._parent.get_name() }}.on(L.Draw.Event.CREATED, function(e) {
var layer = e.layer,
type = e.layerType;
var coords = JSON.stringify(layer.toGeoJSON());
{%- if this.show_geometry_on_click %}
layer.on('click', function() {
alert(coords);
console.log(coords);
});
{%- endif %}
drawnItems_{{ this.get_name() }}.addLayer(layer);
});
{{ this._parent.get_name() }}.on('draw:created', function(e) {
drawnItems_{{ this.get_name() }}.addLayer(e.layer);
});
{% if this.export %}
document.getElementById('export').onclick = function(e) {
var data = drawnItems_{{ this.get_name() }}.toGeoJSON();
var convertedData = 'text/json;charset=utf-8,'
+ encodeURIComponent(JSON.stringify(data));
document.getElementById('export').setAttribute(
'href', 'data:' + convertedData
);
document.getElementById('export').setAttribute(
'download', {{ this.filename|tojson }}
);
}
{% endif %}
{% endmacro %}
"""
)
default_js = [
(
"leaflet_draw_js",
"https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.2/leaflet.draw.js",
)
]
default_css = [
(
"leaflet_draw_css",
"https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.2/leaflet.draw.css",
)
]
def __init__(
self,
export=False,
filename="data.geojson",
position="topleft",
show_geometry_on_click=True,
draw_options=None,
edit_options=None,
):
super().__init__()
self._name = "DrawControl"
self.export = export
self.filename = filename
self.position = position
self.show_geometry_on_click = show_geometry_on_click
self.draw_options = draw_options or {}
self.edit_options = edit_options or {}
def render(self, **kwargs):
super().render(**kwargs)
figure = self.get_root()
assert isinstance(
figure, Figure
), "You cannot render this Element if it is not in a Figure."
export_style = """
<style>
#export {
position: absolute;
top: 5px;
right: 10px;
z-index: 999;
background: white;
color: black;
padding: 6px;
border-radius: 4px;
font-family: 'Helvetica Neue';
cursor: pointer;
font-size: 12px;
text-decoration: none;
top: 90px;
}
</style>
"""
export_button = """<a href='#' id='export'>Export</a>"""
if self.export:
figure.header.add_child(Element(export_style), name="export")
figure.html.add_child(Element(export_button), name="export_button")

View File

@@ -0,0 +1,132 @@
from branca.element import Figure, MacroElement
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.folium import Map
from folium.map import LayerControl
from folium.utilities import deep_copy
class DualMap(JSCSSMixin, MacroElement):
"""Create two maps in the same window.
Adding children to this objects adds them to both maps. You can access
the individual maps with `DualMap.m1` and `DualMap.m2`.
Uses the Leaflet plugin Sync: https://github.com/jieter/Leaflet.Sync
Parameters
----------
location: tuple or list, optional
Latitude and longitude of center point of the maps.
layout : {'horizontal', 'vertical'}
Select how the two maps should be positioned. Either horizontal (left
and right) or vertical (top and bottom).
**kwargs
Keyword arguments are passed to the two Map objects.
Examples
--------
>>> # DualMap accepts the same arguments as Map:
>>> m = DualMap(location=(0, 0), tiles="cartodbpositron", zoom_start=5)
>>> # Add the same marker to both maps:
>>> Marker((0, 0)).add_to(m)
>>> # The individual maps are attributes called `m1` and `m2`:
>>> Marker((0, 1)).add_to(m.m1)
>>> LayerControl().add_to(m)
>>> m.save("map.html")
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
{{ this.m1.get_name() }}.sync({{ this.m2.get_name() }});
{{ this.m2.get_name() }}.sync({{ this.m1.get_name() }});
{% endmacro %}
"""
)
default_js = [
(
"Leaflet.Sync",
"https://cdn.jsdelivr.net/gh/jieter/Leaflet.Sync/L.Map.Sync.min.js",
)
]
def __init__(self, location=None, layout="horizontal", **kwargs):
super().__init__()
for key in ("width", "height", "left", "top", "position"):
assert key not in kwargs, f"Argument {key} cannot be used with DualMap."
if layout not in ("horizontal", "vertical"):
raise ValueError(
f"Undefined option for argument `layout`: {layout}. "
"Use either 'horizontal' or 'vertical'."
)
width = "50%" if layout == "horizontal" else "100%"
height = "100%" if layout == "horizontal" else "50%"
self.m1 = Map(
location=location,
width=width,
height=height,
left="0%",
top="0%",
position="absolute",
**kwargs,
)
self.m2 = Map(
location=location,
width=width,
height=height,
left="50%" if layout == "horizontal" else "0%",
top="0%" if layout == "horizontal" else "50%",
position="absolute",
**kwargs,
)
figure = Figure()
figure.add_child(self.m1)
figure.add_child(self.m2)
# Important: add self to Figure last.
figure.add_child(self)
self.children_for_m2 = []
self.children_for_m2_copied = [] # list with ids
def _repr_html_(self, **kwargs):
"""Displays the HTML Map in a Jupyter notebook."""
if self._parent is None:
self.add_to(Figure())
out = self._parent._repr_html_(**kwargs)
self._parent = None
else:
out = self._parent._repr_html_(**kwargs)
return out
def add_child(self, child, name=None, index=None):
"""Add object `child` to the first map and store it for the second."""
self.m1.add_child(child, name, index)
if index is None:
index = len(self.m2._children)
self.children_for_m2.append((child, name, index))
def render(self, **kwargs):
super().render(**kwargs)
for child, name, index in self.children_for_m2:
if child._id in self.children_for_m2_copied:
# This map has been rendered before, child was copied already.
continue
child_copy = deep_copy(child)
if isinstance(child_copy, LayerControl):
child_copy.reset()
self.m2.add_child(child_copy, name, index)
# m2 has already been rendered, so render the child here:
child_copy.render()
self.children_for_m2_copied.append(child._id)
def fit_bounds(self, *args, **kwargs):
for m in (self.m1, self.m2):
m.fit_bounds(*args, **kwargs)
def keep_in_front(self, *args):
for m in (self.m1, self.m2):
m.keep_in_front(*args)

View File

@@ -0,0 +1,121 @@
from abc import ABC, abstractmethod
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.features import MacroElement
from folium.vector_layers import path_options
class _BaseFromEncoded(JSCSSMixin, MacroElement, ABC):
"""Base Interface to create folium objects from encoded strings.
Derived classes must define `_encoding_type` property which returns the string
representation of the folium object to create from the encoded string.
Parameters
----------
encoded: str
The raw encoded string from the Polyline Encoding Algorithm. See:
https://developers.google.com/maps/documentation/utilities/polylinealgorithm
**kwargs:
Object options as accepted by leaflet.
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
var {{ this.get_name() }} = L.{{ this._encoding_type }}.fromEncoded(
{{ this.encoded|tojson }},
{{ this.options|tojson }}
).addTo({{ this._parent.get_name() }});
{% endmacro %}
"""
)
default_js = [
(
"polyline-encoded",
"https://cdn.jsdelivr.net/npm/polyline-encoded@0.0.9/Polyline.encoded.js",
)
]
def __init__(self, encoded: str):
super().__init__()
self.encoded = encoded
@property
@abstractmethod
def _encoding_type(self) -> str:
"""An abstract getter to return the type of folium object to create."""
raise NotImplementedError
class PolyLineFromEncoded(_BaseFromEncoded):
"""Create PolyLines directly from the encoded string.
Parameters
----------
encoded: str
The raw encoded string from the Polyline Encoding Algorithm. See:
https://developers.google.com/maps/documentation/utilities/polylinealgorithm
**kwargs:
Polyline options as accepted by leaflet. See:
https://leafletjs.com/reference.html#polyline
Adapted from https://github.com/jieter/Leaflet.encoded
Examples
--------
>>> from folium import Map
>>> from folium.plugins import PolyLineFromEncoded
>>> m = Map()
>>> encoded = r"_p~iF~cn~U_ulLn{vA_mqNvxq`@"
>>> PolyLineFromEncoded(encoded=encoded, color="green").add_to(m)
"""
def __init__(self, encoded: str, **kwargs):
self._name = "PolyLineFromEncoded"
super().__init__(encoded=encoded)
self.options = path_options(line=True, **kwargs)
@property
def _encoding_type(self) -> str:
"""Return the name of folium object created from the encoded."""
return "Polyline"
class PolygonFromEncoded(_BaseFromEncoded):
"""Create Polygons directly from the encoded string.
Parameters
----------
encoded: str
The raw encoded string from the Polyline Encoding Algorithm. See:
https://developers.google.com/maps/documentation/utilities/polylinealgorithm
**kwargs:
Polygon options as accepted by leaflet. See:
https://leafletjs.com/reference.html#polygon
Adapted from https://github.com/jieter/Leaflet.encoded
Examples
--------
>>> from folium import Map
>>> from folium.plugins import PolygonFromEncoded
>>> m = Map()
>>> encoded = r"w`j~FpxivO}jz@qnnCd}~Bsa{@~f`C`lkH"
>>> PolygonFromEncoded(encoded=encoded).add_to(m)
"""
def __init__(self, encoded: str, **kwargs):
self._name = "PolygonFromEncoded"
super().__init__(encoded)
self.options = path_options(line=True, radius=None, **kwargs)
@property
def _encoding_type(self) -> str:
"""Return the name of folium object created from the encoded."""
return "Polygon"

View File

@@ -0,0 +1,108 @@
from jinja2 import Template
from folium.plugins.marker_cluster import MarkerCluster
from folium.utilities import if_pandas_df_convert_to_numpy, validate_location
class FastMarkerCluster(MarkerCluster):
"""
Add marker clusters to a map using in-browser rendering.
Using FastMarkerCluster it is possible to render 000's of
points far quicker than the MarkerCluster class.
Be aware that the FastMarkerCluster class passes an empty
list to the parent class' __init__ method during initialisation.
This means that the add_child method is never called, and
no reference to any marker data are retained. Methods such
as get_bounds() are therefore not available when using it.
Parameters
----------
data: list of list with values
List of list of shape [[lat, lon], [lat, lon], etc.]
When you use a custom callback you could add more values after the
lat and lon. E.g. [[lat, lon, 'red'], [lat, lon, 'blue']]
callback: string, optional
A string representation of a valid Javascript function
that will be passed each row in data. See the
FasterMarkerCluster for an example of a custom callback.
name : string, optional
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.
icon_create_function : string, default None
Override the default behaviour, making possible to customize
markers colors and sizes.
**kwargs
Additional arguments are passed to Leaflet.markercluster options. See
https://github.com/Leaflet/Leaflet.markercluster
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
var {{ this.get_name() }} = (function(){
{{ this.callback }}
var data = {{ this.data|tojson }};
var cluster = L.markerClusterGroup({{ this.options|tojson }});
{%- if this.icon_create_function is not none %}
cluster.options.iconCreateFunction =
{{ this.icon_create_function.strip() }};
{%- endif %}
for (var i = 0; i < data.length; i++) {
var row = data[i];
var marker = callback(row);
marker.addTo(cluster);
}
cluster.addTo({{ this._parent.get_name() }});
return cluster;
})();
{% endmacro %}"""
)
def __init__(
self,
data,
callback=None,
options=None,
name=None,
overlay=True,
control=True,
show=True,
icon_create_function=None,
**kwargs,
):
if options is not None:
kwargs.update(options) # options argument is legacy
super().__init__(
name=name,
overlay=overlay,
control=control,
show=show,
icon_create_function=icon_create_function,
**kwargs,
)
self._name = "FastMarkerCluster"
data = if_pandas_df_convert_to_numpy(data)
self.data = [
[*validate_location(row[:2]), *row[2:]] for row in data # noqa: E999
]
if callback is None:
self.callback = """
var callback = function (row) {
var icon = L.AwesomeMarkers.icon();
var marker = L.marker(new L.LatLng(row[0], row[1]));
marker.setIcon(icon);
return marker;
};"""
else:
self.callback = f"var callback = {callback};"

View File

@@ -0,0 +1,79 @@
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.map import Layer
class FeatureGroupSubGroup(JSCSSMixin, Layer):
"""
Creates a Feature Group that adds its child layers into a parent group when
added to a map (e.g. through LayerControl). Useful to create nested groups,
or cluster markers from multiple overlays. From [0].
[0] https://github.com/ghybs/Leaflet.FeatureGroup.SubGroup
Parameters
----------
group : Layer
The MarkerCluster or FeatureGroup containing this subgroup.
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.
Examples
-------
Nested groups
=============
>>> fg = folium.FeatureGroup() # Main group
>>> g1 = folium.plugins.FeatureGroupSubGroup(fg, "g1") # First subgroup of fg
>>> g2 = folium.plugins.FeatureGroupSubGroup(fg, "g2") # Second subgroup of fg
>>> m.add_child(fg)
>>> m.add_child(g1)
>>> m.add_child(g2)
>>> g1.add_child(folium.Marker([0, 0]))
>>> g2.add_child(folium.Marker([0, 1]))
>>> folium.LayerControl().add_to(m)
Multiple overlays part of the same cluster group
=====================================================
>>> mcg = folium.plugins.MarkerCluster(
... control=False
... ) # Marker Cluster, hidden in controls
>>> g1 = folium.plugins.FeatureGroupSubGroup(mcg, "g1") # First group, in mcg
>>> g2 = folium.plugins.FeatureGroupSubGroup(mcg, "g2") # Second group, in mcg
>>> m.add_child(mcg)
>>> m.add_child(g1)
>>> m.add_child(g2)
>>> g1.add_child(folium.Marker([0, 0]))
>>> g2.add_child(folium.Marker([0, 1]))
>>> folium.LayerControl().add_to(m)
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
var {{ this.get_name() }} = L.featureGroup.subGroup(
{{ this._group.get_name() }}
);
{% endmacro %}
"""
)
default_js = [
(
"featuregroupsubgroupjs",
"https://unpkg.com/leaflet.featuregroup.subgroup@1.0.2/dist/leaflet.featuregroup.subgroup.js",
),
]
def __init__(self, group, name=None, overlay=True, control=True, show=True):
super().__init__(name=name, overlay=overlay, control=control, show=show)
self._group = group
self._name = "FeatureGroupSubGroup"

View File

@@ -0,0 +1,53 @@
from branca.element import MacroElement
from jinja2 import Template
class FloatImage(MacroElement):
"""Adds a floating image in HTML canvas on top of the map.
Parameters
----------
image: str
Url to image location. Can also be an inline image using a data URI
or a local file using `file://`.
bottom: int, default 75
Vertical position from the bottom, as a percentage of screen height.
left: int, default 75
Horizontal position from the left, as a percentage of screen width.
**kwargs
Additional keyword arguments are applied as CSS properties.
For example: `width='300px'`.
"""
_template = Template(
"""
{% macro header(this,kwargs) %}
<style>
#{{this.get_name()}} {
position: absolute;
bottom: {{this.bottom}}%;
left: {{this.left}}%;
{%- for property, value in this.css.items() %}
{{ property }}: {{ value }};
{%- endfor %}
}
</style>
{% endmacro %}
{% macro html(this,kwargs) %}
<img id="{{this.get_name()}}" alt="float_image"
src="{{ this.image }}"
style="z-index: 999999">
</img>
{% endmacro %}
"""
)
def __init__(self, image, bottom=75, left=75, **kwargs):
super().__init__()
self._name = "FloatImage"
self.image = image
self.bottom = bottom
self.left = left
self.css = kwargs

View File

@@ -0,0 +1,69 @@
from branca.element import MacroElement
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.utilities import parse_options
class Fullscreen(JSCSSMixin, MacroElement):
"""
Adds a fullscreen button to your map.
Parameters
----------
position : str
change the position of the button can be:
'topleft', 'topright', 'bottomright' or 'bottomleft'
default: 'topleft'
title : str
change the title of the button,
default: 'Full Screen'
title_cancel : str
change the title of the button when fullscreen is on,
default: 'Exit Full Screen'
force_separate_button : bool, default False
force separate button to detach from zoom buttons,
See https://github.com/brunob/leaflet.fullscreen for more information.
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
L.control.fullscreen(
{{ this.options|tojson }}
).addTo({{this._parent.get_name()}});
{% endmacro %}
"""
) # noqa
default_js = [
(
"Control.Fullscreen.js",
"https://cdn.jsdelivr.net/npm/leaflet.fullscreen@3.0.0/Control.FullScreen.min.js",
)
]
default_css = [
(
"Control.FullScreen.css",
"https://cdn.jsdelivr.net/npm/leaflet.fullscreen@3.0.0/Control.FullScreen.css",
)
]
def __init__(
self,
position="topleft",
title="Full Screen",
title_cancel="Exit Full Screen",
force_separate_button=False,
**kwargs
):
super().__init__()
self._name = "Fullscreen"
self.options = parse_options(
position=position,
title=title,
title_cancel=title_cancel,
force_separate_button=force_separate_button,
**kwargs
)

View File

@@ -0,0 +1,93 @@
from typing import Optional
from branca.element import MacroElement
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.utilities import parse_options
class Geocoder(JSCSSMixin, MacroElement):
"""A simple geocoder for Leaflet that by default uses OSM/Nominatim.
Please respect the Nominatim usage policy:
https://operations.osmfoundation.org/policies/nominatim/
Parameters
----------
collapsed: bool, default False
If True, collapses the search box unless hovered/clicked.
position: str, default 'topright'
Choose from 'topleft', 'topright', 'bottomleft' or 'bottomright'.
add_marker: bool, default True
If True, adds a marker on the found location.
zoom: int, default 11, optional
Set zoom level used for displaying the geocode result, note that this only has an effect when add_marker is set to False. Set this to None to preserve the current map zoom level.
provider: str, default 'nominatim'
Defaults to "nominatim", see https://github.com/perliedman/leaflet-control-geocoder/tree/2.4.0/src/geocoders for other built-in providers.
provider_options: dict, default {}
For use with specific providers that may require api keys or other parameters.
For all options see https://github.com/perliedman/leaflet-control-geocoder
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
var geocoderOpts_{{ this.get_name() }} = {{ this.options|tojson }};
// note: geocoder name should start with lowercase
var geocoderName_{{ this.get_name() }} = geocoderOpts_{{ this.get_name() }}["provider"];
var customGeocoder_{{ this.get_name() }} = L.Control.Geocoder[ geocoderName_{{ this.get_name() }} ](
geocoderOpts_{{ this.get_name() }}['providerOptions']
);
geocoderOpts_{{ this.get_name() }}["geocoder"] = customGeocoder_{{ this.get_name() }};
L.Control.geocoder(
geocoderOpts_{{ this.get_name() }}
).on('markgeocode', function(e) {
var zoom = geocoderOpts_{{ this.get_name() }}['zoom'] || {{ this._parent.get_name() }}.getZoom();
{{ this._parent.get_name() }}.setView(e.geocode.center, zoom);
}).addTo({{ this._parent.get_name() }});
{% endmacro %}
"""
)
default_js = [
(
"Control.Geocoder.js",
"https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js",
)
]
default_css = [
(
"Control.Geocoder.css",
"https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css",
)
]
def __init__(
self,
collapsed: bool = False,
position: str = "topright",
add_marker: bool = True,
zoom: Optional[int] = 11,
provider: str = "nominatim",
provider_options: dict = {},
**kwargs
):
super().__init__()
self._name = "Geocoder"
self.options = parse_options(
collapsed=collapsed,
position=position,
default_mark_geocode=add_marker,
zoom=zoom,
provider=provider,
provider_options=provider_options,
**kwargs
)

View File

@@ -0,0 +1,91 @@
from branca.element import MacroElement
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.utilities import parse_options
class GroupedLayerControl(JSCSSMixin, MacroElement):
"""
Create a Layer Control with groups of overlays.
Parameters
----------
groups : dict
A dictionary where the keys are group names and the values are lists
of layer objects.
e.g. {
"Group 1": [layer1, layer2],
"Group 2": [layer3, layer4]
}
exclusive_groups: bool, default True
Whether to use radio buttons (default) or checkboxes.
If you want to use both, use two separate instances of this class.
**kwargs
Additional (possibly inherited) options. See
https://leafletjs.com/reference.html#control-layers
"""
default_js = [
(
"leaflet.groupedlayercontrol.min.js",
"https://cdnjs.cloudflare.com/ajax/libs/leaflet-groupedlayercontrol/0.6.1/leaflet.groupedlayercontrol.min.js", # noqa
),
]
default_css = [
(
"leaflet.groupedlayercontrol.min.css",
"https://cdnjs.cloudflare.com/ajax/libs/leaflet-groupedlayercontrol/0.6.1/leaflet.groupedlayercontrol.min.css", # noqa
)
]
_template = Template(
"""
{% macro script(this,kwargs) %}
L.control.groupedLayers(
null,
{
{%- for group_name, overlays in this.grouped_overlays.items() %}
{{ group_name|tojson }} : {
{%- for overlaykey, val in overlays.items() %}
{{ overlaykey|tojson }} : {{val}},
{%- endfor %}
},
{%- endfor %}
},
{{ this.options|tojson }},
).addTo({{this._parent.get_name()}});
{%- for val in this.layers_untoggle %}
{{ val }}.remove();
{%- endfor %}
{% endmacro %}
"""
)
def __init__(self, groups, exclusive_groups=True, **kwargs):
super().__init__()
self._name = "GroupedLayerControl"
self.options = parse_options(**kwargs)
if exclusive_groups:
self.options["exclusiveGroups"] = list(groups.keys())
self.layers_untoggle = set()
self.grouped_overlays = {}
for group_name, sublist in groups.items():
self.grouped_overlays[group_name] = {}
for element in sublist:
self.grouped_overlays[group_name][
element.layer_name
] = element.get_name()
if not element.show:
self.layers_untoggle.add(element.get_name())
# make sure the elements used in GroupedLayerControl
# don't show up in the regular LayerControl.
element.control = False
if exclusive_groups:
# only enable the first radio button
for element in sublist[1:]:
self.layers_untoggle.add(element.get_name())

View File

@@ -0,0 +1,121 @@
import warnings
import numpy as np
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.map import Layer
from folium.utilities import (
if_pandas_df_convert_to_numpy,
none_max,
none_min,
parse_options,
validate_location,
)
class HeatMap(JSCSSMixin, Layer):
"""
Create a Heatmap layer
Parameters
----------
data : list of points of the form [lat, lng] or [lat, lng, weight]
The points you want to plot.
You can also provide a numpy.array of shape (n,2) or (n,3).
name : string, default None
The name of the Layer, as it will appear in LayerControls.
min_opacity : default 1.
The minimum opacity the heat will start at.
max_zoom : default 18
Zoom level where the points reach maximum intensity (as intensity
scales with zoom), equals maxZoom of the map by default
radius : int, default 25
Radius of each "point" of the heatmap
blur : int, default 15
Amount of blur
gradient : dict, default None
Color gradient config. e.g. {0.4: 'blue', 0.65: 'lime', 1: 'red'}
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.
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
var {{ this.get_name() }} = L.heatLayer(
{{ this.data|tojson }},
{{ this.options|tojson }}
);
{% endmacro %}
"""
)
default_js = [
(
"leaflet-heat.js",
"https://cdn.jsdelivr.net/gh/python-visualization/folium@main/folium/templates/leaflet_heat.min.js",
),
]
def __init__(
self,
data,
name=None,
min_opacity=0.5,
max_zoom=18,
radius=25,
blur=15,
gradient=None,
overlay=True,
control=True,
show=True,
**kwargs
):
super().__init__(name=name, overlay=overlay, control=control, show=show)
self._name = "HeatMap"
data = if_pandas_df_convert_to_numpy(data)
self.data = [
[*validate_location(line[:2]), *line[2:]] for line in data # noqa: E999
]
if np.any(np.isnan(self.data)):
raise ValueError("data may not contain NaNs.")
if kwargs.pop("max_val", None):
warnings.warn(
"The `max_val` parameter is no longer necessary. "
"The largest intensity is calculated automatically.",
stacklevel=2,
)
self.options = parse_options(
min_opacity=min_opacity,
max_zoom=max_zoom,
radius=radius,
blur=blur,
gradient=gradient,
**kwargs
)
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]].
"""
bounds = [[None, None], [None, None]]
for point in self.data:
bounds = [
[
none_min(bounds[0][0], point[0]),
none_min(bounds[0][1], point[1]),
],
[
none_max(bounds[1][0], point[0]),
none_max(bounds[1][1], point[1]),
],
]
return bounds

View File

@@ -0,0 +1,318 @@
from branca.element import Element, Figure
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.map import Layer
from folium.utilities import none_max, none_min
class HeatMapWithTime(JSCSSMixin, Layer):
"""
Create a HeatMapWithTime layer
Parameters
----------
data: list of list of points of the form [lat, lng] or [lat, lng, weight]
The points you want to plot. The outer list corresponds to the various time
steps in sequential order. (weight is in (0, 1] range and defaults to 1 if
not specified for a point)
index: Index giving the label (or timestamp) of the elements of data. Should have
the same length as data, or is replaced by a simple count if not specified.
name : string, default None
The name of the Layer, as it will appear in LayerControls.
radius: default 15.
The radius used around points for the heatmap.
blur: default 0.8.
Blur strength used for the heatmap. Must be between 0 and 1.
min_opacity: default 0
The minimum opacity for the heatmap.
max_opacity: default 0.6
The maximum opacity for the heatmap.
scale_radius: default False
Scale the radius of the points based on the zoom level.
gradient: dict, default None
Match point density values to colors. Color can be a name ('red'),
RGB values ('rgb(255,0,0)') or a hex number ('#FF0000').
use_local_extrema: default False
Defines whether the heatmap uses a global extrema set found from the input data
OR a local extrema (the maximum and minimum of the currently displayed view).
auto_play: default False
Automatically play the animation across time.
display_index: default True
Display the index (usually time) in the time control.
index_steps: default 1
Steps to take in the index dimension between animation steps.
min_speed: default 0.1
Minimum fps speed for animation.
max_speed: default 10
Maximum fps speed for animation.
speed_step: default 0.1
Step between different fps speeds on the speed slider.
position: default 'bottomleft'
Position string for the time slider. Format: 'bottom/top'+'left/right'.
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.
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
var times = {{this.times}};
{{this._parent.get_name()}}.timeDimension = L.timeDimension(
{times : times, currentTime: new Date(1)}
);
var {{this._control_name}} = new L.Control.TimeDimensionCustom({{this.index}}, {
autoPlay: {{this.auto_play}},
backwardButton: {{this.backward_button}},
displayDate: {{this.display_index}},
forwardButton: {{this.forward_button}},
limitMinimumRange: {{this.limit_minimum_range}},
limitSliders: {{this.limit_sliders}},
loopButton: {{this.loop_button}},
maxSpeed: {{this.max_speed}},
minSpeed: {{this.min_speed}},
playButton: {{this.play_button}},
playReverseButton: {{this.play_reverse_button}},
position: "{{this.position}}",
speedSlider: {{this.speed_slider}},
speedStep: {{this.speed_step}},
styleNS: "{{this.style_NS}}",
timeSlider: {{this.time_slider}},
timeSliderDrapUpdate: {{this.time_slider_drap_update}},
timeSteps: {{this.index_steps}}
})
.addTo({{this._parent.get_name()}});
var {{this.get_name()}} = new TDHeatmap({{this.data}},
{heatmapOptions: {
radius: {{this.radius}},
blur: {{this.blur}},
minOpacity: {{this.min_opacity}},
maxOpacity: {{this.max_opacity}},
scaleRadius: {{this.scale_radius}},
useLocalExtrema: {{this.use_local_extrema}},
defaultWeight: 1,
{% if this.gradient %}gradient: {{ this.gradient }}{% endif %}
}
});
{% endmacro %}
"""
)
default_js = [
(
"iso8601",
"https://cdn.jsdelivr.net/npm/iso8601-js-period@0.2.1/iso8601.min.js",
),
(
"leaflet.timedimension.min.js",
"https://cdn.jsdelivr.net/npm/leaflet-timedimension@1.1.1/dist/leaflet.timedimension.min.js",
),
(
"heatmap.min.js",
"https://cdn.jsdelivr.net/gh/python-visualization/folium/folium/templates/pa7_hm.min.js",
),
(
"leaflet-heatmap.js",
"https://cdn.jsdelivr.net/gh/python-visualization/folium/folium/templates/pa7_leaflet_hm.min.js",
),
]
default_css = [
(
"leaflet.timedimension.control.min.css",
"https://cdn.jsdelivr.net/npm/leaflet-timedimension@1.1.1/dist/leaflet.timedimension.control.css",
)
]
def __init__(
self,
data,
index=None,
name=None,
radius=15,
blur=0.8,
min_opacity=0,
max_opacity=0.6,
scale_radius=False,
gradient=None,
use_local_extrema=False,
auto_play=False,
display_index=True,
index_steps=1,
min_speed=0.1,
max_speed=10,
speed_step=0.1,
position="bottomleft",
overlay=True,
control=True,
show=True,
):
super().__init__(name=name, overlay=overlay, control=control, show=show)
self._name = "HeatMap"
self._control_name = self.get_name() + "Control"
# Input data.
self.data = data
self.index = (
index if index is not None else [str(i) for i in range(1, len(data) + 1)]
)
if len(self.data) != len(self.index):
raise ValueError(
"Input data and index are not of compatible lengths."
) # noqa
self.times = list(range(1, len(data) + 1))
# Heatmap settings.
self.radius = radius
self.blur = blur
self.min_opacity = min_opacity
self.max_opacity = max_opacity
self.scale_radius = "true" if scale_radius else "false"
self.use_local_extrema = "true" if use_local_extrema else "false"
self.gradient = gradient
# Time dimension settings.
self.auto_play = "true" if auto_play else "false"
self.display_index = "true" if display_index else "false"
self.min_speed = min_speed
self.max_speed = max_speed
self.position = position
self.speed_step = speed_step
self.index_steps = index_steps
# Hard coded defaults for simplicity.
self.backward_button = "true"
self.forward_button = "true"
self.limit_sliders = "true"
self.limit_minimum_range = 5
self.loop_button = "true"
self.speed_slider = "true"
self.time_slider = "true"
self.play_button = "true"
self.play_reverse_button = "true"
self.time_slider_drap_update = "false"
self.style_NS = "leaflet-control-timecontrol"
def render(self, **kwargs):
super().render(**kwargs)
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(
"""
<script>
var TDHeatmap = L.TimeDimension.Layer.extend({
initialize: function(data, options) {
var heatmapCfg = {
radius: 15,
blur: 0.8,
maxOpacity: 1.,
scaleRadius: false,
useLocalExtrema: false,
latField: 'lat',
lngField: 'lng',
valueField: 'count',
defaultWeight : 1,
};
heatmapCfg = $.extend({}, heatmapCfg, options.heatmapOptions || {});
var layer = new HeatmapOverlay(heatmapCfg);
L.TimeDimension.Layer.prototype.initialize.call(this, layer, options);
this._currentLoadedTime = 0;
this._currentTimeData = {
data: []
};
this.data= data;
this.defaultWeight = heatmapCfg.defaultWeight || 1;
},
onAdd: function(map) {
L.TimeDimension.Layer.prototype.onAdd.call(this, map);
map.addLayer(this._baseLayer);
if (this._timeDimension) {
this._getDataForTime(this._timeDimension.getCurrentTime());
}
},
_onNewTimeLoading: function(ev) {
this._getDataForTime(ev.time);
return;
},
isReady: function(time) {
return (this._currentLoadedTime == time);
},
_update: function() {
this._baseLayer.setData(this._currentTimeData);
return true;
},
_getDataForTime: function(time) {
delete this._currentTimeData.data;
this._currentTimeData.data = [];
var data = this.data[time-1];
for (var i = 0; i < data.length; i++) {
this._currentTimeData.data.push({
lat: data[i][0],
lng: data[i][1],
count: data[i].length>2 ? data[i][2] : this.defaultWeight
});
}
this._currentLoadedTime = time;
if (this._timeDimension && time == this._timeDimension.getCurrentTime() && !this._timeDimension.isLoading()) {
this._update();
}
this.fire('timeload', {
time: time
});
}
});
L.Control.TimeDimensionCustom = L.Control.TimeDimension.extend({
initialize: function(index, options) {
var playerOptions = {
buffer: 1,
minBufferReady: -1
};
options.playerOptions = $.extend({}, playerOptions, options.playerOptions || {});
L.Control.TimeDimension.prototype.initialize.call(this, options);
this.index = index;
},
_getDisplayDateFormat: function(date){
return this.index[date.getTime()-1];
}
});
</script>
""", # noqa
template_name="timeControlScript",
)
)
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]].
"""
bounds = [[None, None], [None, None]]
for point in self.data:
bounds = [
[
none_min(bounds[0][0], point[0]),
none_min(bounds[0][1], point[1]),
],
[
none_max(bounds[1][0], point[0]),
none_max(bounds[1][1], point[1]),
],
]
return bounds

View File

@@ -0,0 +1,77 @@
"""Add Locate control to folium Map.
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.utilities import parse_options
class LocateControl(JSCSSMixin, MacroElement):
"""Control plugin to geolocate the user.
This plugins adds a button to the map, and when it's clicked shows the current
user device location.
To work properly in production, the connection needs to be encrypted, otherwise browser will not
allow users to share their location.
Parameters
----------
auto-start : bool, default False
When set to True, plugin will be activated on map loading and search for user position.
Once user location is founded, the map will automatically centered in using user coordinates.
**kwargs
For possible options, see https://github.com/domoritz/leaflet-locatecontrol
Examples
--------
>>> m = folium.Map()
# With default settings
>>> LocateControl().add_to(m)
# With some custom options
>>> LocateControl(
... position="bottomright",
... strings={"title": "See you current location", "popup": "Your position"},
... ).add_to(m)
For more info check:
https://github.com/domoritz/leaflet-locatecontrol
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
var {{this.get_name()}} = L.control.locate(
{{this.options | tojson}}
).addTo({{this._parent.get_name()}});
{% if this.auto_start %}
{{this.get_name()}}.start();
{% endif %}
{% endmacro %}
"""
)
default_js = [
(
"Control_locate_min_js",
"https://cdnjs.cloudflare.com/ajax/libs/leaflet-locatecontrol/0.66.2/L.Control.Locate.min.js",
)
]
default_css = [
(
"Control_locate_min_css",
"https://cdnjs.cloudflare.com/ajax/libs/leaflet-locatecontrol/0.66.2/L.Control.Locate.min.css",
)
]
def __init__(self, auto_start=False, **kwargs):
super().__init__()
self._name = "LocateControl"
self.auto_start = auto_start
self.options = parse_options(**kwargs)

View File

@@ -0,0 +1,109 @@
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.map import Layer, Marker
from folium.utilities import parse_options, validate_locations
class MarkerCluster(JSCSSMixin, Layer):
"""
Provides Beautiful Animated Marker Clustering functionality for maps.
Parameters
----------
locations: list of list or array of shape (n, 2).
Data points of the form [[lat, lng]].
popups: list of length n, default None
Popup for each marker, either a Popup object or a string or None.
icons: list of length n, default None
Icon for each marker, either an Icon object or a string or None.
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.
icon_create_function : string, default None
Override the default behaviour, making possible to customize
markers colors and sizes.
options : dict, default None
A dictionary with options for Leaflet.markercluster. See
https://github.com/Leaflet/Leaflet.markercluster for options.
Example
-------
>>> icon_create_function = '''
... function(cluster) {
... return L.divIcon({html: '<b>' + cluster.getChildCount() + '</b>',
... className: 'marker-cluster marker-cluster-small',
... iconSize: new L.Point(20, 20)});
... }
... '''
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
var {{ this.get_name() }} = L.markerClusterGroup(
{{ this.options|tojson }}
);
{%- if this.icon_create_function is not none %}
{{ this.get_name() }}.options.iconCreateFunction =
{{ this.icon_create_function.strip() }};
{%- endif %}
{% endmacro %}
"""
)
default_js = [
(
"markerclusterjs",
"https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.1.0/leaflet.markercluster.js",
)
]
default_css = [
(
"markerclustercss",
"https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.1.0/MarkerCluster.css",
),
(
"markerclusterdefaultcss",
"https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.1.0/MarkerCluster.Default.css",
),
]
def __init__(
self,
locations=None,
popups=None,
icons=None,
name=None,
overlay=True,
control=True,
show=True,
icon_create_function=None,
options=None,
**kwargs
):
if options is not None:
kwargs.update(options) # options argument is legacy
super().__init__(name=name, overlay=overlay, control=control, show=show)
self._name = "MarkerCluster"
if locations is not None:
locations = validate_locations(locations)
for i, location in enumerate(locations):
self.add_child(
Marker(
location, popup=popups and popups[i], icon=icons and icons[i]
)
)
self.options = parse_options(**kwargs)
if icon_create_function is not None:
assert isinstance(icon_create_function, str)
self.icon_create_function = icon_create_function

View File

@@ -0,0 +1,83 @@
from branca.element import MacroElement
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.utilities import parse_options
class MeasureControl(JSCSSMixin, MacroElement):
"""Add a measurement widget on the map.
Parameters
----------
position: str, default 'topright'
Location of the widget.
primary_length_unit: str, default 'meters'
secondary_length_unit: str, default 'miles'
primary_area_unit: str, default 'sqmeters'
secondary_area_unit: str, default 'acres'
See https://github.com/ljagis/leaflet-measure for more information.
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
var {{ this.get_name() }} = new L.Control.Measure(
{{ this.options|tojson }});
{{this._parent.get_name()}}.addControl({{this.get_name()}});
// Workaround for using this plugin with Leaflet>=1.8.0
// https://github.com/ljagis/leaflet-measure/issues/171
L.Control.Measure.include({
_setCaptureMarkerIcon: function () {
// disable autopan
this._captureMarker.options.autoPanOnFocus = false;
// default function
this._captureMarker.setIcon(
L.divIcon({
iconSize: this._map.getSize().multiplyBy(2)
})
);
},
});
{% endmacro %}
"""
) # noqa
default_js = [
(
"leaflet_measure_js",
"https://cdn.jsdelivr.net/gh/ljagis/leaflet-measure@2.1.7/dist/leaflet-measure.min.js",
)
]
default_css = [
(
"leaflet_measure_css",
"https://cdn.jsdelivr.net/gh/ljagis/leaflet-measure@2.1.7/dist/leaflet-measure.min.css",
)
]
def __init__(
self,
position="topright",
primary_length_unit="meters",
secondary_length_unit="miles",
primary_area_unit="sqmeters",
secondary_area_unit="acres",
**kwargs
):
super().__init__()
self._name = "MeasureControl"
self.options = parse_options(
position=position,
primary_length_unit=primary_length_unit,
secondary_length_unit=secondary_length_unit,
primary_area_unit=primary_area_unit,
secondary_area_unit=secondary_area_unit,
**kwargs
)

View File

@@ -0,0 +1,132 @@
from branca.element import MacroElement
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.raster_layers import TileLayer
from folium.utilities import parse_options
class MiniMap(JSCSSMixin, MacroElement):
"""Add a minimap (locator) to an existing map.
Uses the Leaflet plugin by Norkart under BSD 2-Clause "Simplified" License.
https://github.com/Norkart/Leaflet-MiniMap
Parameters
----------
tile_layer : folium TileLayer object or str, default None
Provide a folium TileLayer object or the wanted tiles as string.
If not provided it will use the default of 'TileLayer', currently
OpenStreetMap.
position : str, default 'bottomright'
The standard Control position parameter for the widget.
width : int, default 150
The width of the minimap in pixels.
height : int, default 150
The height of the minimap in pixels.
collapsed_width : int, default 25
The width of the toggle marker and the minimap when collapsed in pixels.
collapsed_height : int, default 25
The height of the toggle marker and the minimap when collapsed
zoom_level_offset : int, default -5
The offset applied to the zoom in the minimap compared to the zoom of
the main map. Can be positive or negative.
zoom_level_fixed : int, default None
Overrides the offset to apply a fixed zoom level to the minimap
regardless of the main map zoom.
Set it to any valid zoom level, if unset zoom_level_offset is used
instead.
center_fixed : bool, default False
Applies a fixed position to the minimap regardless of the main map's
view / position. Prevents panning the minimap, but does allow zooming
(both in the minimap and the main map).
If the minimap is zoomed, it will always zoom around the centerFixed
point. You can pass in a LatLng-equivalent object.
zoom_animation : bool, default False
Sets whether the minimap should have an animated zoom.
(Will cause it to lag a bit after the movement of the main map.)
toggle_display : bool, default False
Sets whether the minimap should have a button to minimise it.
auto_toggle_display : bool, default False
Sets whether the minimap should hide automatically
if the parent map bounds does not fit within the minimap bounds.
Especially useful when 'zoomLevelFixed' is set.
minimized : bool, default False
Sets whether the minimap should start in a minimized position.
Examples
--------
>>> MiniMap(position="bottomleft")
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
var {{ this.tile_layer.get_name() }} = L.tileLayer(
{{ this.tile_layer.tiles|tojson }},
{{ this.tile_layer.options|tojson }}
);
var {{ this.get_name() }} = new L.Control.MiniMap(
{{ this.tile_layer.get_name() }},
{{ this.options|tojson }}
);
{{ this._parent.get_name() }}.addControl({{ this.get_name() }});
{% endmacro %}
"""
) # noqa
default_js = [
(
"Control_MiniMap_js",
"https://cdnjs.cloudflare.com/ajax/libs/leaflet-minimap/3.6.1/Control.MiniMap.js",
)
]
default_css = [
(
"Control_MiniMap_css",
"https://cdnjs.cloudflare.com/ajax/libs/leaflet-minimap/3.6.1/Control.MiniMap.css",
),
]
def __init__(
self,
tile_layer=None,
position="bottomright",
width=150,
height=150,
collapsed_width=25,
collapsed_height=25,
zoom_level_offset=-5,
zoom_level_fixed=None,
center_fixed=False,
zoom_animation=False,
toggle_display=False,
auto_toggle_display=False,
minimized=False,
**kwargs
):
super().__init__()
self._name = "MiniMap"
if tile_layer is None:
self.tile_layer = TileLayer()
elif isinstance(tile_layer, TileLayer):
self.tile_layer = tile_layer
else:
self.tile_layer = TileLayer(tile_layer)
self.options = parse_options(
position=position,
width=width,
height=height,
collapsed_width=collapsed_width,
collapsed_height=collapsed_height,
zoom_level_offset=zoom_level_offset,
zoom_level_fixed=zoom_level_fixed,
center_fixed=center_fixed,
zoom_animation=zoom_animation,
toggle_display=toggle_display,
auto_toggle_display=auto_toggle_display,
minimized=minimized,
**kwargs
)

View File

@@ -0,0 +1,101 @@
from branca.element import MacroElement
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.utilities import parse_options
class MousePosition(JSCSSMixin, MacroElement):
"""Add a field that shows the coordinates of the mouse position.
Uses the Leaflet plugin by Ardhi Lukianto under MIT license.
https://github.com/ardhi/Leaflet.MousePosition
Parameters
----------
position : str, default 'bottomright'
The standard Control position parameter for the widget.
separator : str, default ' : '
Character used to separate latitude and longitude values.
empty_string : str, default 'Unavailable'
Initial text to display.
lng_first : bool, default False
Whether to put the longitude first or not.
Set as True to display longitude before latitude.
num_digits : int, default '5'
Number of decimal places included in the displayed
longitude and latitude decimal degree values.
prefix : str, default ''
A string to be prepended to the coordinates.
lat_formatter : str, default None
Custom Javascript function to format the latitude value.
lng_formatter : str, default None
Custom Javascript function to format the longitude value.
Examples
--------
>>> fmtr = "function(num) {return L.Util.formatNum(num, 3) + ' º ';};"
>>> MousePosition(
... position="topright",
... separator=" | ",
... prefix="Mouse:",
... lat_formatter=fmtr,
... lng_formatter=fmtr,
... )
"""
_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",
separator=" : ",
empty_string="Unavailable",
lng_first=False,
num_digits=5,
prefix="",
lat_formatter=None,
lng_formatter=None,
**kwargs
):
super().__init__()
self._name = "MousePosition"
self.options = parse_options(
position=position,
separator=separator,
empty_string=empty_string,
lng_first=lng_first,
num_digits=num_digits,
prefix=prefix,
**kwargs
)
self.lat_formatter = lat_formatter or "undefined"
self.lng_formatter = lng_formatter or "undefined"

View File

@@ -0,0 +1,157 @@
from branca.element import MacroElement
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.folium import Map
from folium.utilities import get_obj_in_upper_tree, parse_options
class StripePattern(JSCSSMixin, MacroElement):
"""Fill Pattern for polygon composed of alternating lines.
Add these to the 'fillPattern' field in GeoJson style functions.
Parameters
----------
angle: float, default 0.5
Angle of the line pattern (degrees). Should be between -360 and 360.
weight: float, default 4
Width of the main lines (pixels).
space_weight: float
Width of the alternate lines (pixels).
color: string with hexadecimal, RGB, or named color, default "#000000"
Color of the main lines.
space_color: string with hexadecimal, RGB, or named color, default "#ffffff"
Color of the alternate lines.
opacity: float, default 0.75
Opacity of the main lines. Should be between 0 and 1.
space_opacity: float, default 0.0
Opacity of the alternate lines. Should be between 0 and 1.
See https://github.com/teastman/Leaflet.pattern for more information.
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
var {{ this.get_name() }} = new L.StripePattern(
{{ this.options|tojson }}
);
{{ this.get_name() }}.addTo({{ this.parent_map.get_name() }});
{% endmacro %}
"""
)
default_js = [
("pattern", "https://teastman.github.io/Leaflet.pattern/leaflet.pattern.js")
]
def __init__(
self,
angle=0.5,
weight=4,
space_weight=4,
color="#000000",
space_color="#ffffff",
opacity=0.75,
space_opacity=0.0,
**kwargs
):
super().__init__()
self._name = "StripePattern"
self.options = parse_options(
angle=angle,
weight=weight,
space_weight=space_weight,
color=color,
space_color=space_color,
opacity=opacity,
space_opacity=space_opacity,
**kwargs
)
self.parent_map = None
def render(self, **kwargs):
self.parent_map = get_obj_in_upper_tree(self, Map)
super().render(**kwargs)
class CirclePattern(JSCSSMixin, MacroElement):
"""Fill Pattern for polygon composed of repeating circles.
Add these to the 'fillPattern' field in GeoJson style functions.
Parameters
----------
width: int, default 20
Horizontal distance between circles (pixels).
height: int, default 20
Vertical distance between circles (pixels).
radius: int, default 12
Radius of each circle (pixels).
weight: float, default 2.0
Width of outline around each circle (pixels).
color: string with hexadecimal, RGB, or named color, default "#3388ff"
Color of the circle outline.
fill_color: string with hexadecimal, RGB, or named color, default "#3388ff"
Color of the circle interior.
opacity: float, default 0.75
Opacity of the circle outline. Should be between 0 and 1.
fill_opacity: float, default 0.5
Opacity of the circle interior. Should be between 0 and 1.
See https://github.com/teastman/Leaflet.pattern for more information.
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
var {{ this.get_name() }}_shape = new L.PatternCircle(
{{ this.options_pattern_circle|tojson }}
);
var {{ this.get_name() }} = new L.Pattern(
{{ this.options_pattern|tojson }}
);
{{ this.get_name() }}.addShape({{ this.get_name() }}_shape);
{{ this.get_name() }}.addTo({{ this.parent_map }});
{% endmacro %}
"""
)
default_js = [
("pattern", "https://teastman.github.io/Leaflet.pattern/leaflet.pattern.js")
]
def __init__(
self,
width=20,
height=20,
radius=12,
weight=2.0,
color="#3388ff",
fill_color="#3388ff",
opacity=0.75,
fill_opacity=0.5,
):
super().__init__()
self._name = "CirclePattern"
self.options_pattern_circle = parse_options(
x=radius + 2 * weight,
y=radius + 2 * weight,
weight=weight,
radius=radius,
color=color,
fill_color=fill_color,
opacity=opacity,
fill_opacity=fill_opacity,
fill=True,
)
self.options_pattern = parse_options(
width=width,
height=height,
)
self.parent_map = None
def render(self, **kwargs):
self.parent_map = get_obj_in_upper_tree(self, Map).get_name()
super().render(**kwargs)

View File

@@ -0,0 +1,55 @@
from folium.elements import JSCSSMixin
from folium.vector_layers import PolyLine
class PolyLineOffset(JSCSSMixin, PolyLine):
"""
Add offset capabilities to the PolyLine class.
This plugin adds to folium Polylines the ability to be drawn with a
relative pixel offset, without modifying their actual coordinates. The offset
value can be either negative or positive, for left- or right-side offset,
and remains constant across zoom levels.
See :func:`folium.vector_layers.path_options` for the `Path` options.
Parameters
----------
locations: list of points (latitude, longitude)
Latitude and Longitude of line (Northing, Easting)
popup: str or folium.Popup, default None
Input text or visualization for object displayed when clicking.
tooltip: str or folium.Tooltip, optional
Display a text when hovering over the object.
offset: int, default 0
Relative pixel offset to draw a line parallel to an existent one,
at a fixed distance.
**kwargs:
Polyline options. See their Github page for the
available parameters.
See https://github.com/bbecquet/Leaflet.PolylineOffset
Examples
--------
>>> plugins.PolyLineOffset(
... [[58, -28], [53, -23]], color="#f00", opacity=1, offset=-5
... ).add_to(m)
>>> plugins.PolyLineOffset(
... [[58, -28], [53, -23]], color="#080", opacity=1, offset=10
... ).add_to(m)
"""
default_js = [
(
"polylineoffset",
"https://cdn.jsdelivr.net/npm/leaflet-polylineoffset@1.1.1/leaflet.polylineoffset.min.js",
)
]
def __init__(self, locations, popup=None, tooltip=None, offset=0, **kwargs):
super().__init__(locations=locations, popup=popup, tooltip=tooltip, **kwargs)
self._name = "PolyLineOffset"
# Add PolyLineOffset offset.
self.options.update({"offset": offset})

View File

@@ -0,0 +1,80 @@
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.features import MacroElement
from folium.utilities import parse_options
class PolyLineTextPath(JSCSSMixin, MacroElement):
"""
Shows a text along a PolyLine.
Parameters
----------
polyline: folium.features.PolyLine object
The folium.features.PolyLine object to attach the text to.
text: string
The string to be attached to the polyline.
repeat: bool, default False
Specifies if the text should be repeated along the polyline.
center: bool, default False
Centers the text according to the polyline's bounding box
below: bool, default False
Show text below the path
offset: int, default 0
Set an offset to position text relative to the polyline.
orientation: int, default 0
Rotate text to a specified angle.
attributes: dict
Object containing the attributes applied to the text tag.
Check valid attributes here:
https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text#attributes
Example: {'fill': '#007DEF', 'font-weight': 'bold', 'font-size': '24'}
See https://github.com/makinacorpus/Leaflet.TextPath for more information.
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
{{ this.polyline.get_name() }}.setText(
{{ this.text|tojson }},
{{ this.options|tojson }}
);
{% endmacro %}
"""
)
default_js = [
(
"polylinetextpath",
"https://cdn.jsdelivr.net/npm/leaflet-textpath@1.2.3/leaflet.textpath.min.js",
)
]
def __init__(
self,
polyline,
text,
repeat=False,
center=False,
below=False,
offset=0,
orientation=0,
attributes=None,
**kwargs
):
super().__init__()
self._name = "PolyLineTextPath"
self.polyline = polyline
self.text = text
self.options = parse_options(
repeat=repeat,
center=center,
below=below,
offset=offset,
orientation=orientation,
attributes=attributes,
**kwargs
)

View File

@@ -0,0 +1,135 @@
from typing import Optional, Union
from branca.element import MacroElement
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.map import Layer
from folium.utilities import JsCode, camelize, parse_options
class Realtime(JSCSSMixin, MacroElement):
"""Put realtime data on a Leaflet map: live tracking GPS units,
sensor data or just about anything.
Based on: https://github.com/perliedman/leaflet-realtime
Parameters
----------
source: str, dict, JsCode
The source can be one of:
* a string with the URL to get data from
* a dict that is passed to javascript's `fetch` function
for fetching the data
* a `folium.JsCode` object in case you need more freedom.
start: bool, default True
Should automatic updates be enabled when layer is added
on the map and stopped when layer is removed from the map
interval: int, default 60000
Automatic update interval, in milliseconds
get_feature_id: str or JsCode, optional
A JS function with a geojson `feature` as parameter
default returns `feature.properties.id`
Function to get an identifier to uniquely identify a feature over time
update_feature: str or JsCode, optional
A JS function with a geojson `feature` as parameter
Used to update an existing feature's layer;
by default, points (markers) are updated, other layers are discarded
and replaced with a new, updated layer.
Allows to create more complex transitions,
for example, when a feature is updated
remove_missing: bool, default False
Should missing features between updates been automatically
removed from the layer
container: Layer, 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
`addLayer` and `removeLayer`.
Other keyword arguments are passed to the GeoJson layer, so you can pass
`style`, `point_to_layer` and/or `on_each_feature`. Make sure to wrap
Javascript functions in the JsCode class.
Examples
--------
>>> from folium import JsCode
>>> m = folium.Map(location=[40.73, -73.94], zoom_start=12)
>>> rt = Realtime(
... "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/subway_stations.geojson",
... get_feature_id=JsCode("(f) => { return f.properties.objectid; }"),
... point_to_layer=JsCode(
... "(f, latlng) => { return L.circleMarker(latlng, {radius: 8, fillOpacity: 0.2})}"
... ),
... interval=10000,
... )
>>> rt.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 %}
{% if this.container -%}
{{ this.get_name() }}_options["container"]
= {{ this.container.get_name() }};
{% endif -%}
var {{ this.get_name() }} = L.realtime(
{% if this.src is string or this.src is mapping -%}
{{ this.src|tojson }},
{% else -%}
{{ this.src.js_code }},
{% endif -%}
{{ this.get_name() }}_options
);
{{ this._parent.get_name() }}.addLayer(
{{ this.get_name() }}._container);
{% endmacro %}
"""
)
default_js = [
(
"Leaflet_Realtime_js",
"https://cdnjs.cloudflare.com/ajax/libs/leaflet-realtime/2.2.0/leaflet-realtime.js",
)
]
def __init__(
self,
source: Union[str, dict, JsCode],
start: bool = True,
interval: int = 60000,
get_feature_id: Union[JsCode, str, None] = None,
update_feature: Union[JsCode, str, None] = None,
remove_missing: bool = False,
container: Optional[Layer] = None,
**kwargs
):
super().__init__()
self._name = "Realtime"
self.src = source
self.container = container
kwargs["start"] = start
kwargs["interval"] = interval
if get_feature_id is not None:
kwargs["get_feature_id"] = JsCode(get_feature_id)
if update_feature is not None:
kwargs["update_feature"] = JsCode(update_feature)
kwargs["remove_missing"] = remove_missing
# 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)

View File

@@ -0,0 +1,54 @@
from branca.element import MacroElement
from jinja2 import Template
class ScrollZoomToggler(MacroElement):
"""Creates a button for enabling/disabling scroll on the Map."""
_template = Template(
"""
{% macro header(this,kwargs) %}
<style>
#{{ this.get_name() }} {
position:absolute;
width:35px;
bottom:10px;
height:35px;
left:10px;
background-color:#fff;
text-align:center;
line-height:35px;
vertical-align: middle;
}
</style>
{% endmacro %}
{% macro html(this,kwargs) %}
<img id="{{ this.get_name() }}"
alt="scroll"
src="https://cdnjs.cloudflare.com/ajax/libs/ionicons/2.0.1/png/512/arrow-move.png"
style="z-index: 999999"
onclick="{{ this._parent.get_name() }}.toggleScroll()">
</img>
{% endmacro %}
{% macro script(this,kwargs) %}
{{ this._parent.get_name() }}.scrollEnabled = true;
{{ this._parent.get_name() }}.toggleScroll = function() {
if (this.scrollEnabled) {
this.scrollEnabled = false;
this.scrollWheelZoom.disable();
} else {
this.scrollEnabled = true;
this.scrollWheelZoom.enable();
}
};
{{ this._parent.get_name() }}.toggleScroll();
{% endmacro %}
"""
)
def __init__(self):
super().__init__()
self._name = "ScrollZoomToggler"

View File

@@ -0,0 +1,150 @@
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.plugins import MarkerCluster
from folium.utilities import parse_options
class Search(JSCSSMixin, MacroElement):
"""
Adds a search tool to your map.
Parameters
----------
layer: GeoJson, TopoJson, FeatureGroup, MarkerCluster class object.
The map layer to index in the Search view.
search_label: str, optional
'properties' key in layer to index Search, if layer is GeoJson/TopoJson.
search_zoom: int, optional
Zoom level to set the map to on match.
By default zooms to Polygon/Line bounds and points
on their natural extent.
geom_type: str, default 'Point'
Feature geometry type. "Point", "Line" or "Polygon"
position: str, default 'topleft'
Change the position of the search bar, can be:
'topleft', 'topright', 'bottomright' or 'bottomleft',
placeholder: str, default 'Search'
Placeholder text inside the Search box if nothing is entered.
collapsed: boolean, default False
Whether the Search box should be collapsed or not.
**kwargs.
Assorted style options to change feature styling on match.
Use the same way as vector layer arguments.
See https://github.com/stefanocudini/leaflet-search for more information.
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
var {{this.layer.get_name()}}searchControl = new L.Control.Search({
layer: {{this.layer.get_name()}},
{% if this.search_label %}
propertyName: '{{this.search_label}}',
{% endif %}
collapsed: {{this.collapsed|tojson|safe}},
textPlaceholder: '{{this.placeholder}}',
position:'{{this.position}}',
{% if this.geom_type == 'Point' %}
initial: false,
{% if this.search_zoom %}
zoom: {{this.search_zoom}},
{% endif %}
hideMarkerOnCollapse: true
{% else %}
marker: false,
moveToLocation: function(latlng, title, map) {
var zoom = {% if this.search_zoom %} {{ this.search_zoom }} {% else %} map.getBoundsZoom(latlng.layer.getBounds()) {% endif %}
map.flyTo(latlng, zoom); // access the zoom
}
{% endif %}
});
{{this.layer.get_name()}}searchControl.on('search:locationfound', function(e) {
{{this.layer.get_name()}}.setStyle(function(feature){
return feature.properties.style
})
{% if this.options %}
e.layer.setStyle({{ this.options|tojson }});
{% endif %}
if(e.layer._popup)
e.layer.openPopup();
})
{{this.layer.get_name()}}searchControl.on('search:collapsed', function(e) {
{{this.layer.get_name()}}.setStyle(function(feature){
return feature.properties.style
});
});
{{this._parent.get_name()}}.addControl( {{this.layer.get_name()}}searchControl );
{% endmacro %}
""" # noqa
)
default_js = [
(
"Leaflet.Search.js",
"https://cdn.jsdelivr.net/npm/leaflet-search@2.9.7/dist/leaflet-search.min.js",
)
]
default_css = [
(
"Leaflet.Search.css",
"https://cdn.jsdelivr.net/npm/leaflet-search@2.9.7/dist/leaflet-search.min.css",
)
]
def __init__(
self,
layer,
search_label=None,
search_zoom=None,
geom_type="Point",
position="topleft",
placeholder="Search",
collapsed=False,
**kwargs,
):
super().__init__()
assert isinstance(layer, (GeoJson, MarkerCluster, FeatureGroup, TopoJson)), (
"Search can only index FeatureGroup, "
"MarkerCluster, GeoJson, and TopoJson layers at "
"this time."
)
self.layer = layer
self.search_label = search_label
self.search_zoom = search_zoom
self.geom_type = geom_type
self.position = position
self.placeholder = placeholder
self.collapsed = collapsed
self.options = parse_options(**kwargs)
def test_params(self, keys):
if keys is not None and self.search_label is not None:
assert self.search_label in keys, (
f"The label '{self.search_label}' was not " f"available in {keys}" ""
)
assert isinstance(
self._parent, Map
), "Search can only be added to folium Map objects."
def render(self, **kwargs):
if isinstance(self.layer, GeoJson):
keys = tuple(self.layer.data["features"][0]["properties"].keys())
elif isinstance(self.layer, TopoJson):
obj_name = self.layer.object_path.split(".")[-1]
keys = tuple(
self.layer.data["objects"][obj_name]["geometries"][0][
"properties"
].keys()
) # noqa
else:
keys = None
self.test_params(keys=keys)
super().render(**kwargs)

View File

@@ -0,0 +1,94 @@
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.map import Marker
from folium.utilities import parse_options
from folium.vector_layers import path_options
class SemiCircle(JSCSSMixin, Marker):
"""Add a marker in the shape of a semicircle, similar to the Circle class.
Use (direction and arc) or (start_angle and stop_angle), not both.
Parameters
----------
location: tuple[float, float]
Latitude and Longitude pair (Northing, Easting)
radius: float
Radius of the circle, in meters.
direction: int, default None
Direction angle in degrees
arc: int, default None
Arc angle in degrees.
start_angle: int, default None
Start angle in degrees
stop_angle: int, default None
Stop angle in degrees.
popup: str or folium.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
For additional arguments see :func:`folium.vector_layers.path_options`
Uses Leaflet plugin https://github.com/jieter/Leaflet-semicircle
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
var {{ this.get_name() }} = L.semiCircle(
{{ this.location|tojson }},
{{ this.options|tojson }}
)
{%- if this.direction %}
.setDirection({{ this.direction[0] }}, {{ this.direction[1] }})
{%- endif %}
.addTo({{ this._parent.get_name() }});
{% endmacro %}
"""
)
default_js = [
(
"semicirclejs",
"https://cdn.jsdelivr.net/npm/leaflet-semicircle@2.0.4/Semicircle.min.js",
)
]
def __init__(
self,
location,
radius,
direction=None,
arc=None,
start_angle=None,
stop_angle=None,
popup=None,
tooltip=None,
**kwargs
):
super().__init__(location, popup=popup, tooltip=tooltip)
self._name = "SemiCircle"
self.direction = (
(direction, arc) if direction is not None and arc is not None else None
)
self.options = path_options(line=False, radius=radius, **kwargs)
self.options.update(
parse_options(
start_angle=start_angle,
stop_angle=stop_angle,
)
)
if not (
(direction is None and arc is None)
and (start_angle is not None and stop_angle is not None)
or (direction is not None and arc is not None)
and (start_angle is None and stop_angle is None)
):
raise ValueError(
"Invalid arguments. Either provide direction and arc OR start_angle and stop_angle"
)

View File

@@ -0,0 +1,50 @@
from branca.element import MacroElement
from jinja2 import Template
from folium.elements import JSCSSMixin
class SideBySideLayers(JSCSSMixin, MacroElement):
"""
Creates a SideBySideLayers that takes two Layers and adds a sliding
control with the leaflet-side-by-side plugin.
Uses the Leaflet leaflet-side-by-side plugin https://github.com/digidem/leaflet-side-by-side
Parameters
----------
layer_left: Layer.
The left Layer within the side by side control.
Must be created and added to the map before being passed to this class.
layer_right: Layer.
The right Layer within the side by side control.
Must be created and added to the map before being passed to this class.
Examples
--------
>>> sidebyside = SideBySideLayers(layer_left, layer_right)
>>> sidebyside.add_to(m)
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
var {{ this.get_name() }} = L.control.sideBySide(
{{ this.layer_left.get_name() }}, {{ this.layer_right.get_name() }}
).addTo({{ this._parent.get_name() }});
{% endmacro %}
"""
)
default_js = [
(
"leaflet.sidebyside",
"https://cdn.jsdelivr.net/gh/digidem/leaflet-side-by-side@2.0.0/leaflet-side-by-side.min.js",
),
]
def __init__(self, layer_left, layer_right):
super().__init__()
self._name = "SideBySideLayers"
self.layer_left = layer_left
self.layer_right = layer_right

View File

@@ -0,0 +1,96 @@
from branca.element import MacroElement
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.utilities import parse_options
class TagFilterButton(JSCSSMixin, MacroElement):
"""
Creates a Tag Filter Button to filter elements based on criteria
(https://github.com/maydemirx/leaflet-tag-filter-button)
This plugin works for multiple element types like Marker, GeoJson
and vector layers like PolyLine.
Parameters
----------
data: list, of strings.
The tags to filter for this filter button.
icon: string, default 'fa-filter'
The icon for the filter button
clear_text: string, default 'clear'
Text of the clear button
filter_on_every_click: bool, default True
if True, the plugin will filter on every click event on checkbox.
open_popup_on_hover: bool, default False
if True, popup that contains tags will be open at mouse hover time
"""
_template = Template(
"""
{% macro header(this,kwargs) %}
<style>
.easy-button-button {
display: block !important;
}
.tag-filter-tags-container {
left: 30px;
}
</style>
{% endmacro %}
{% macro script(this, kwargs) %}
var {{ this.get_name() }} = L.control.tagFilterButton(
{{ this.options|tojson }}
).addTo({{ this._parent.get_name() }});
{% endmacro %}
"""
)
default_js = [
(
"tag-filter-button.js",
"https://cdn.jsdelivr.net/npm/leaflet-tag-filter-button/src/leaflet-tag-filter-button.js",
),
(
"easy-button.js",
"https://cdn.jsdelivr.net/npm/leaflet-easybutton@2/src/easy-button.js",
),
]
default_css = [
(
"tag-filter-button.css",
"https://cdn.jsdelivr.net/npm/leaflet-tag-filter-button/src/leaflet-tag-filter-button.css",
),
(
"easy-button.css",
"https://cdn.jsdelivr.net/npm/leaflet-easybutton@2/src/easy-button.css",
),
(
"ripples.min.css",
"https://cdn.jsdelivr.net/npm/css-ripple-effect@1.0.5/dist/ripple.min.css",
),
]
def __init__(
self,
data,
icon="fa-filter",
clear_text="clear",
filter_on_every_click=True,
open_popup_on_hover=False,
**kwargs
):
super().__init__()
self._name = "TagFilterButton"
self.options = parse_options(
data=data,
icon=icon,
clear_text=clear_text,
filter_on_every_click=filter_on_every_click,
open_popup_on_hover=open_popup_on_hover,
**kwargs
)

View File

@@ -0,0 +1,26 @@
from branca.element import MacroElement
from jinja2 import Template
from folium.elements import JSCSSMixin
class Terminator(JSCSSMixin, MacroElement):
"""
Leaflet.Terminator is a simple plug-in to the Leaflet library to
overlay day and night regions on maps.
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
L.terminator().addTo({{this._parent.get_name()}});
{% endmacro %}
"""
)
default_js = [("terminator", "https://unpkg.com/@joergdietrich/leaflet.terminator")]
def __init__(self):
super().__init__()
self._name = "Terminator"

View File

@@ -0,0 +1,208 @@
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.features import GeoJson
from folium.map import Layer
class TimeSliderChoropleth(JSCSSMixin, Layer):
"""
Create a choropleth with a timeslider for timestamped data.
Visualize timestamped data, allowing users to view the choropleth at
different timestamps using a slider.
Parameters
----------
data: str
geojson string
styledict: dict
A dictionary where the keys are the geojson feature ids and the values are
dicts of `{time: style_options_dict}`
highlight: bool, default False
Whether to show a visual effect on mouse hover and click.
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.
init_timestamp: int, default 0
Initial time-stamp index on the slider. Must be in the range
`[-L, L-1]`, where `L` is the maximum number of time stamps in
`styledict`. For example, use `-1` to initialize the slider to the
latest timestamp.
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
{
let timestamps = {{ this.timestamps|tojson }};
let styledict = {{ this.styledict|tojson }};
let current_timestamp = timestamps[{{ this.init_timestamp }}];
let slider_body = d3.select("body").insert("div", "div.folium-map")
.attr("id", "slider_{{ this.get_name() }}");
$("#slider_{{ this.get_name() }}").hide();
// insert time slider label
slider_body.append("output")
.attr("width", "100")
.style('font-size', '18px')
.style('text-align', 'center')
.style('font-weight', '500%')
.style('margin', '5px');
// insert time slider
slider_body.append("input")
.attr("type", "range")
.attr("width", "100px")
.attr("min", 0)
.attr("max", timestamps.length - 1)
.attr("value", {{ this.init_timestamp }})
.attr("step", "1")
.style('align', 'center');
let datestring = new Date(parseInt(current_timestamp)*1000).toDateString();
d3.select("#slider_{{ this.get_name() }} > output").text(datestring);
let fill_map = function(){
for (var feature_id in styledict){
let style = styledict[feature_id]//[current_timestamp];
var fillColor = 'white';
var opacity = 0;
if (current_timestamp in style){
fillColor = style[current_timestamp]['color'];
opacity = style[current_timestamp]['opacity'];
d3.selectAll('#{{ this.get_name() }}-feature-'+feature_id
).attr('fill', fillColor)
.style('fill-opacity', opacity);
}
}
}
d3.select("#slider_{{ this.get_name() }} > input").on("input", function() {
current_timestamp = timestamps[this.value];
var datestring = new Date(parseInt(current_timestamp)*1000).toDateString();
d3.select("#slider_{{ this.get_name() }} > output").text(datestring);
fill_map();
});
let onEachFeature;
{% if this.highlight %}
onEachFeature = function(feature, layer) {
layer.on({
mouseout: function(e) {
if (current_timestamp in styledict[e.target.feature.id]){
var opacity = styledict[e.target.feature.id][current_timestamp]['opacity'];
d3.selectAll('#{{ this.get_name() }}-feature-'+e.target.feature.id).style('fill-opacity', opacity);
}
},
mouseover: function(e) {
if (current_timestamp in styledict[e.target.feature.id]){
d3.selectAll('#{{ this.get_name() }}-feature-'+e.target.feature.id).style('fill-opacity', 1);
}
},
click: function(e) {
{{this._parent.get_name()}}.fitBounds(e.target.getBounds());
}
});
};
{% endif %}
var {{ this.get_name() }} = L.geoJson(
{{ this.data|tojson }},
{onEachFeature: onEachFeature}
);
{{ this.get_name() }}.setStyle(function(feature) {
if (feature.properties.style !== undefined){
return feature.properties.style;
}
else{
return "";
}
});
let onOverlayAdd = function(e) {
{{ this.get_name() }}.eachLayer(function (layer) {
layer._path.id = '{{ this.get_name() }}-feature-' + layer.feature.id;
});
$("#slider_{{ this.get_name() }}").show();
d3.selectAll('path')
.attr('stroke', '{{ this.stroke_color }}')
.attr('stroke-width', {{ this.stroke_width }})
.attr('stroke-dasharray', '5,5')
.attr('stroke-opacity', {{ this.stroke_opacity }})
.attr('fill-opacity', 0);
fill_map();
}
{{ this.get_name() }}.on('add', onOverlayAdd);
{{ this.get_name() }}.on('remove', function() {
$("#slider_{{ this.get_name() }}").hide();
})
{%- if this.show %}
{{ this.get_name() }}.addTo({{ this._parent.get_name() }});
$("#slider_{{ this.get_name() }}").show();
{%- endif %}
}
{% endmacro %}
"""
)
default_js = [("d3v4", "https://d3js.org/d3.v4.min.js")]
def __init__(
self,
data,
styledict,
highlight: bool = False,
name=None,
overlay=True,
control=True,
show=True,
init_timestamp=0,
stroke_opacity=1,
stroke_width=0.8,
stroke_color="#FFFFFF",
):
super().__init__(name=name, overlay=overlay, control=control, show=show)
self.data = GeoJson.process_data(GeoJson({}), data)
self.highlight = highlight
self.stroke_opacity = stroke_opacity
self.stroke_width = stroke_width
self.stroke_color = stroke_color
if not isinstance(styledict, dict):
raise ValueError(
f"styledict must be a dictionary, got {styledict!r}"
) # noqa
for val in styledict.values():
if not isinstance(val, dict):
raise ValueError(
f"Each item in styledict must be a dictionary, got {val!r}"
) # noqa
# Make set of timestamps.
timestamps_set = set()
for feature in styledict.values():
timestamps_set.update(set(feature.keys()))
try:
timestamps = sorted(timestamps_set, key=int)
except (TypeError, ValueError):
timestamps = sorted(timestamps_set)
self.timestamps = timestamps
self.styledict = styledict
assert (
-len(timestamps) <= init_timestamp < len(timestamps)
), f"init_timestamp must be in the range [-{len(timestamps)}, {len(timestamps)}) but got {init_timestamp}"
if init_timestamp < 0:
init_timestamp = len(timestamps) + init_timestamp
self.init_timestamp = init_timestamp

View File

@@ -0,0 +1,279 @@
from typing import List, Optional, TextIO, Union
from branca.element import MacroElement
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.features import GeoJson
from folium.folium import Map
from folium.utilities import JsCode, camelize, get_bounds, parse_options
class Timeline(GeoJson):
"""
Create a layer from GeoJSON with time data to add to a map.
To add time data, you need to do one of the following:
* Add a 'start' and 'end' property to each feature. The start and end
can be any comparable item.
Alternatively, you can provide a `get_interval` function.
* This function should be a JsCode object and take as parameter
a GeoJson feature and return a dict containing values for
'start', 'end', 'startExclusive' and 'endExcusive' (or false if no
data could be extracted from the feature).
* 'start' and 'end' can be any comparable items
* 'startExclusive' and 'endExclusive' should be boolean values.
Parameters
----------
data: file, dict or str.
The geojson data you want to plot.
get_interval: JsCode, optional
Called for each feature, and should return either a time range for the
feature or `false`, indicating that it should not be included in the
timeline. The time range object should have 'start' and 'end' properties.
Optionally, the boolean keys 'startExclusive' and 'endExclusive' allow the
interval to be considered exclusive.
If `get_interval` is not provided, 'start' and 'end' properties are
assumed to be present on each feature.
Examples
--------
>>> from folium.plugins import Timeline, TimelineSlider
>>> m = folium.Map()
>>> data = requests.get(
... "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson"
... ).json()
>>> timeline = Timeline(
... data,
... get_interval=JsCode(
... '''
... function (quake) {
... // earthquake data only has a time, so we\'ll use that as a "start"
... // and the "end" will be that + some value based on magnitude
... // 18000000 = 30 minutes, so a quake of magnitude 5 would show on the
... // map for 150 minutes or 2.5 hours
... return {
... start: quake.properties.time,
... end: quake.properties.time + quake.properties.mag * 1800000,
... };
... };
... '''
... ),
... ).add_to(m)
>>> TimelineSlider(
... auto_play=False,
... show_ticks=True,
... enable_keyboard_controls=True,
... playback_duration=30000,
... ).add_timelines(timeline).add_to(m)
Other keyword arguments are passed to the GeoJson layer, so you can pass
`style`, `point_to_layer` and/or `on_each_feature`.
"""
_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() }} = L.timeline(
{{ this.data|tojson }},
{{ this.get_name() }}_options
);
{{ this.get_name() }}.addTo({{ this._parent.get_name() }});
{% endmacro %}
"""
)
default_js = [
(
"timeline",
"https://cdn.jsdelivr.net/npm/leaflet.timeline@1.6.0/dist/leaflet.timeline.min.js",
),
(
"moment",
"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.min.js",
),
]
def __init__(
self,
data: Union[dict, str, TextIO],
get_interval: Optional[JsCode] = None,
**kwargs
):
super().__init__(data)
self._name = "Timeline"
if get_interval is not None:
kwargs["get_interval"] = get_interval
# 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)
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)
class TimelineSlider(JSCSSMixin, MacroElement):
"""
Creates a timeline slider for timeline layers.
Parameters
----------
auto_play: bool, default True
Whether the animation shall start automatically at startup.
start: str, int or float, default earliest 'start' in GeoJson
The beginning/minimum value of the timeline.
end: str, int or float, default latest 'end' in GeoJSON
The end/maximum value of the timeline.
date_options: str, default "YYYY-MM-DD HH:mm:ss"
A format string to render the currently active time in the control.
enable_playback: bool, default True
Show playback controls (i.e. prev/play/pause/next).
enable_keyboard_controls: bool, default False
Allow playback to be controlled using the spacebar (play/pause) and
right/left arrow keys (next/previous).
show_ticks: bool, default True
Show tick marks on the slider
steps: int, default 1000
How many steps to break the timeline into.
Each step will then be (end-start) / steps. Only affects playback.
playback_duration: int, default 10000
Minimum time, in ms, for the playback to take. Will almost certainly
actually take at least a bit longer -- after each frame, the next
one displays in playback_duration/steps ms, so each frame really
takes frame processing time PLUS step time.
Examples
--------
See the documentation for Timeline
"""
_template = Template(
"""
{% macro header(this,kwargs) %}
<style>
.leaflet-bottom.leaflet-left {
width: 100%;
}
.leaflet-control-container .leaflet-timeline-controls {
box-sizing: border-box;
width: 100%;
margin: 0;
margin-bottom: 15px;
}
</style>
{% endmacro %}
{% 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() }} = L.timelineSliderControl(
{{ this.get_name() }}_options
);
{{ this.get_name() }}.addTo({{ this._parent.get_name() }});
{% for timeline in this.timelines %}
{{ this.get_name() }}.addTimelines({{ timeline.get_name() }});
{% endfor %}
{% endmacro %}
"""
)
default_js = [
(
"timeline",
"https://cdn.jsdelivr.net/npm/leaflet.timeline@1.6.0/dist/leaflet.timeline.min.js",
),
(
"moment",
"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.min.js",
),
]
def __init__(
self,
# arguments relevant to both interval and timestamp mode
auto_play: bool = True,
date_options: str = "YYYY-MM-DD HH:mm:ss",
start: Optional[Union[str, int, float]] = None,
end: Optional[Union[str, int, float]] = None,
enable_playback: bool = True,
enable_keyboard_controls: bool = False,
show_ticks: bool = True,
steps: int = 1000,
playback_duration: int = 10000,
**kwargs
):
super().__init__()
self._name = "TimelineSlider"
kwargs["auto_play"] = auto_play
kwargs["start"] = start
kwargs["end"] = end
kwargs["enable_playback"] = enable_playback
kwargs["enable_keyboard_controls"] = enable_keyboard_controls
kwargs["show_ticks"] = show_ticks
kwargs["steps"] = steps
kwargs["duration"] = playback_duration
kwargs["format_output"] = JsCode(
"""
function(date) {
var newdate = new moment(date);
return newdate.format(\""""
+ date_options
+ """\");
}
"""
)
# 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.timelines: List[Timeline] = []
self.options = parse_options(**kwargs)
def add_timelines(self, *args):
"""Add timelines to the control"""
self.timelines += args # we do not check for duplicates
return self
def render(self, **kwargs):
assert isinstance(
self._parent, Map
), "TimelineSlider can only be added to a Map object."
super().render(**kwargs)

View File

@@ -0,0 +1,252 @@
import json
from branca.element import MacroElement
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.folium import Map
from folium.utilities import get_bounds, parse_options
class TimestampedGeoJson(JSCSSMixin, MacroElement):
"""
Creates a TimestampedGeoJson plugin from timestamped GeoJSONs to append
into a map with Map.add_child.
A geo-json is timestamped if:
* it contains only features of types LineString, MultiPoint, MultiLineString,
Polygon and MultiPolygon.
* each feature has a 'times' property with the same length as the
coordinates array.
* each element of each 'times' property is a timestamp in ms since epoch,
or in ISO string.
Eventually, you may have Point features with a 'times' property being an
array of length 1.
Parameters
----------
data: file, dict or str.
The timestamped geo-json 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.
transition_time: int, default 200.
The duration in ms of a transition from between timestamps.
loop: bool, default True
Whether the animation shall loop.
auto_play: bool, default True
Whether the animation shall start automatically at startup.
add_last_point: bool, default True
Whether a point is added at the last valid coordinate of a LineString.
period: str, default "P1D"
Used to construct the array of available times starting
from the first available time. Format: ISO8601 Duration
ex: 'P1M' 1/month, 'P1D' 1/day, 'PT1H' 1/hour, and 'PT1M' 1/minute
duration: str, default None
Period of time which the features will be shown on the map after their
time has passed. If None, all previous times will be shown.
Format: ISO8601 Duration
ex: 'P1M' 1/month, 'P1D' 1/day, 'PT1H' 1/hour, and 'PT1M' 1/minute
Examples
--------
>>> TimestampedGeoJson(
... {
... "type": "FeatureCollection",
... "features": [
... {
... "type": "Feature",
... "geometry": {
... "type": "LineString",
... "coordinates": [[-70, -25], [-70, 35], [70, 35]],
... },
... "properties": {
... "times": [1435708800000, 1435795200000, 1435881600000],
... "tooltip": "my tooltip text",
... },
... }
... ],
... }
... )
See https://github.com/socib/Leaflet.TimeDimension for more information.
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
L.Control.TimeDimensionCustom = L.Control.TimeDimension.extend({
_getDisplayDateFormat: function(date){
var newdate = new moment(date);
console.log(newdate)
return newdate.format("{{this.date_options}}");
}
});
{{this._parent.get_name()}}.timeDimension = L.timeDimension(
{
period: {{ this.period|tojson }},
}
);
var timeDimensionControl = new L.Control.TimeDimensionCustom(
{{ this.options|tojson }}
);
{{this._parent.get_name()}}.addControl(this.timeDimensionControl);
var geoJsonLayer = L.geoJson({{this.data}}, {
pointToLayer: function (feature, latLng) {
if (feature.properties.icon == 'marker') {
if(feature.properties.iconstyle){
return new L.Marker(latLng, {
icon: L.icon(feature.properties.iconstyle)});
}
//else
return new L.Marker(latLng);
}
if (feature.properties.icon == 'circle') {
if (feature.properties.iconstyle) {
return new L.circleMarker(latLng, feature.properties.iconstyle)
};
//else
return new L.circleMarker(latLng);
}
//else
return new L.Marker(latLng);
},
style: function (feature) {
return feature.properties.style;
},
onEachFeature: function(feature, layer) {
if (feature.properties.popup) {
layer.bindPopup(feature.properties.popup);
}
if (feature.properties.tooltip) {
layer.bindTooltip(feature.properties.tooltip);
}
}
})
var {{this.get_name()}} = L.timeDimension.layer.geoJson(
geoJsonLayer,
{
updateTimeDimension: true,
addlastPoint: {{ this.add_last_point|tojson }},
duration: {{ this.duration }},
}
).addTo({{this._parent.get_name()}});
{% endmacro %}
"""
) # noqa
default_js = [
(
"jquery3.7.1",
"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js",
),
(
"jqueryui1.10.2",
"https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js",
),
(
"iso8601",
"https://cdn.jsdelivr.net/npm/iso8601-js-period@0.2.1/iso8601.min.js",
),
(
"leaflet.timedimension",
"https://cdn.jsdelivr.net/npm/leaflet-timedimension@1.1.1/dist/leaflet.timedimension.min.js",
),
# noqa
(
"moment",
"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.min.js",
),
]
default_css = [
(
"highlight.js_css",
"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/styles/default.min.css",
),
(
"leaflet.timedimension_css",
"https://cdn.jsdelivr.net/npm/leaflet-timedimension@1.1.1/dist/leaflet.timedimension.control.css",
),
]
def __init__(
self,
data,
transition_time=200,
loop=True,
auto_play=True,
add_last_point=True,
period="P1D",
min_speed=0.1,
max_speed=10,
loop_button=False,
date_options="YYYY-MM-DD HH:mm:ss",
time_slider_drag_update=False,
duration=None,
speed_slider=True,
):
super().__init__()
self._name = "TimestampedGeoJson"
if "read" in dir(data):
self.embed = True
self.data = data.read()
elif type(data) is dict:
self.embed = True
self.data = json.dumps(data)
else:
self.embed = False
self.data = data
self.add_last_point = bool(add_last_point)
self.period = period
self.date_options = date_options
self.duration = "undefined" if duration is None else '"' + duration + '"'
self.options = parse_options(
position="bottomleft",
min_speed=min_speed,
max_speed=max_speed,
auto_play=auto_play,
loop_button=loop_button,
time_slider_drag_update=time_slider_drag_update,
speed_slider=speed_slider,
player_options={
"transitionTime": int(transition_time),
"loop": loop,
"startOver": True,
},
)
def render(self, **kwargs):
assert isinstance(
self._parent, Map
), "TimestampedGeoJson can only be added to a Map object."
super().render(**kwargs)
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]].
"""
if not self.embed:
raise ValueError("Cannot compute bounds of non-embedded GeoJSON.")
data = json.loads(self.data)
if "features" not in data.keys():
# Catch case when GeoJSON is just a single Feature or a geometry.
if not (isinstance(data, dict) and "geometry" in data.keys()):
# Catch case when GeoJSON is just a geometry.
data = {"type": "Feature", "geometry": data}
data = {"type": "FeatureCollection", "features": [data]}
return get_bounds(data, lonlat=True)

View File

@@ -0,0 +1,147 @@
from branca.element import MacroElement
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.raster_layers import WmsTileLayer
from folium.utilities import parse_options
class TimestampedWmsTileLayers(JSCSSMixin, MacroElement):
"""
Creates a TimestampedWmsTileLayer that takes a WmsTileLayer and adds time
control with the Leaflet.TimeDimension plugin.
Parameters
----------
data: WmsTileLayer.
The WmsTileLayer that you want to add time support to.
Must be created like a typical WmsTileLayer and added to the map
before being passed to this class.
transition_time: int, default 200.
The duration in ms of a transition from between timestamps.
loop: bool, default False
Whether the animation shall loop, default is to reduce load on WMS
services.
auto_play: bool, default False
Whether the animation shall start automatically at startup, default
is to reduce load on WMS services.
period: str, default 'P1D'
Used to construct the array of available times starting
from the first available time. Format: ISO8601 Duration
ex: 'P1M' -> 1/month, 'P1D' -> 1/day, 'PT1H' -> 1/hour, and 'PT1M' -> 1/minute
Note: this seems to be overridden by the WMS Tile Layer GetCapabilities.
Examples
--------
>>> w0 = WmsTileLayer(
... "http://this.wms.server/ncWMS/wms",
... name="Test WMS Data",
... styles="",
... fmt="image/png",
... transparent=True,
... layers="test_data",
... COLORSCALERANGE="0,10",
... )
>>> w0.add_to(m)
>>> w1 = WmsTileLayer(
... "http://this.wms.server/ncWMS/wms",
... name="Test WMS Data",
... styles="",
... fmt="image/png",
... transparent=True,
... layers="test_data_2",
... COLORSCALERANGE="0,5",
... )
>>> w1.add_to(m)
>>> # Add WmsTileLayers to time control.
>>> time = TimestampedWmsTileLayers([w0, w1])
>>> time.add_to(m)
See https://github.com/socib/Leaflet.TimeDimension for more information.
"""
_template = Template(
"""
{% macro script(this, kwargs) %}
{{ this._parent.get_name() }}.timeDimension = L.timeDimension(
{{ this.options|tojson }}
);
{{ this._parent.get_name() }}.timeDimensionControl =
L.control.timeDimension(
{{ this.options_control|tojson }}
);
{{ this._parent.get_name() }}.addControl(
{{ this._parent.get_name() }}.timeDimensionControl
);
{% for layer in this.layers %}
var {{ layer.get_name() }} = L.timeDimension.layer.wms(
{{ layer.get_name() }},
{
updateTimeDimension: false,
wmsVersion: {{ layer.options['version']|tojson }},
}
).addTo({{ this._parent.get_name() }});
{% endfor %}
{% endmacro %}
"""
)
default_js = [
(
"jquery3.7.1",
"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js",
),
(
"jqueryui1.10.2",
"https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js",
),
(
"iso8601",
"https://cdn.jsdelivr.net/npm/iso8601-js-period@0.2.1/iso8601.min.js",
),
(
"leaflet.timedimension",
"https://cdn.jsdelivr.net/npm/leaflet-timedimension@1.1.1/dist/leaflet.timedimension.min.js",
),
]
default_css = [
(
"highlight.js_css",
"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/styles/default.min.css",
),
(
"leaflet.timedimension_css",
"https://cdn.jsdelivr.net/npm/leaflet-timedimension@1.1.1/dist/leaflet.timedimension.control.css",
),
]
def __init__(
self,
data,
transition_time=200,
loop=False,
auto_play=False,
period="P1D",
time_interval=False,
):
super().__init__()
self._name = "TimestampedWmsTileLayers"
self.options = parse_options(
period=period,
time_interval=time_interval,
)
self.options_control = parse_options(
position="bottomleft",
auto_play=auto_play,
player_options={
"transitionTime": int(transition_time),
"loop": loop,
},
)
if isinstance(data, WmsTileLayer):
self.layers = [data]
else:
self.layers = data # Assume iterable

View File

@@ -0,0 +1,163 @@
from typing import Union
from branca.element import MacroElement
from folium.elements import JSCSSMixin
from folium.template import Template
from folium.utilities import parse_options
class TreeLayerControl(JSCSSMixin, MacroElement):
"""
Create a Layer Control allowing a tree structure for the layers.
See https://github.com/jjimenezshaw/Leaflet.Control.Layers.Tree for more
information.
Parameters
----------
base_tree : dict
A dictionary defining the base layers.
Valid elements are
children: list
Array of child nodes for this node. Each node is a dict that has the same valid elements as base_tree.
label: str
Text displayed in the tree for this node. It may contain HTML code.
layer: Layer
The layer itself. This needs to be added to the map.
name: str
Text displayed in the toggle when control is minimized.
If not present, label is used. It makes sense only when
namedToggle is true, and with base layers.
radioGroup: str, default ''
Text to identify different radio button groups.
It is used in the name attribute in the radio button.
It is used only in the overlays layers (ignored in the base
layers), allowing you to have radio buttons instead of checkboxes.
See that radio groups cannot be unselected, so create a 'fake'
layer (like L.layersGroup([])) if you want to disable it.
Default '' (that means checkbox).
collapsed: bool, default False
Indicate whether this tree node should be collapsed initially,
useful for opening large trees partially based on user input or
context.
selectAllCheckbox: bool or str
Displays a checkbox to select/unselect all overlays in the
sub-tree. In case of being a <str>, that text will be the title
(tooltip). When any overlay in the sub-tree is clicked, the
checkbox goes into indeterminate state (a dash in the box).
overlay_tree: dict
Similar to baseTree, but for overlays.
closed_symbol: str, default '+',
Symbol displayed on a closed node (that you can click to open).
opened_symbol: str, default '-',
Symbol displayed on an opened node (that you can click to close).
space_symbol: str, default ' ',
Symbol between the closed or opened symbol, and the text.
selector_back: bool, default False,
Flag to indicate if the selector (+ or ) is after the text.
named_toggle: bool, default False,
Flag to replace the toggle image (box with the layers image) with the
'name' of the selected base layer. If the name field is not present in
the tree for this layer, label is used. See that you can show a
different name when control is collapsed than the one that appears
in the tree when it is expanded.
collapse_all: str, default '',
Text for an entry in control that collapses the tree (baselayers or
overlays). If empty, no entry is created.
expand_all: str, default '',
Text for an entry in control that expands the tree. If empty, no entry
is created
label_is_selector: str, default 'both',
Controls if a label or only the checkbox/radiobutton can toggle layers.
If set to `both`, `overlay` or `base` those labels can be clicked
on to toggle the layer.
**kwargs
Additional (possibly inherited) options. See
https://leafletjs.com/reference.html#control-layers
Examples
--------
>>> import folium
>>> from folium.plugins.treelayercontrol import TreeLayerControl
>>> from folium.features import Marker
>>> m = folium.Map(location=[46.603354, 1.8883335], zoom_start=5)
>>> marker = Marker([48.8582441, 2.2944775]).add_to(m)
>>> overlay_tree = {
... "label": "Points of Interest",
... "selectAllCheckbox": "Un/select all",
... "children": [
... {
... "label": "Europe",
... "selectAllCheckbox": True,
... "children": [
... {
... "label": "France",
... "selectAllCheckbox": True,
... "children": [
... {"label": "Tour Eiffel", "layer": marker},
... ],
... }
... ],
... }
... ],
... }
>>> control = TreeLayerControl(overlay_tree=overlay_tree).add_to(m)
"""
default_js = [
(
"L.Control.Layers.Tree.min.js",
"https://cdn.jsdelivr.net/npm/leaflet.control.layers.tree@1.1.0/L.Control.Layers.Tree.min.js", # noqa
),
]
default_css = [
(
"L.Control.Layers.Tree.min.css",
"https://cdn.jsdelivr.net/npm/leaflet.control.layers.tree@1.1.0/L.Control.Layers.Tree.min.css", # noqa
)
]
_template = Template(
"""
{% macro script(this,kwargs) %}
L.control.layers.tree(
{{this.base_tree|tojavascript}},
{{this.overlay_tree|tojavascript}},
{{this.options|tojson}}
).addTo({{this._parent.get_name()}});
{% endmacro %}
"""
)
def __init__(
self,
base_tree: Union[dict, list, None] = None,
overlay_tree: Union[dict, list, None] = None,
closed_symbol: str = "+",
opened_symbol: str = "-",
space_symbol: str = "&nbsp;",
selector_back: bool = False,
named_toggle: bool = False,
collapse_all: str = "",
expand_all: str = "",
label_is_selector: str = "both",
**kwargs
):
super().__init__()
self._name = "TreeLayerControl"
kwargs["closed_symbol"] = closed_symbol
kwargs["openened_symbol"] = opened_symbol
kwargs["space_symbol"] = space_symbol
kwargs["selector_back"] = selector_back
kwargs["named_toggle"] = named_toggle
kwargs["collapse_all"] = collapse_all
kwargs["expand_all"] = expand_all
kwargs["label_is_selector"] = label_is_selector
self.options = parse_options(**kwargs)
self.base_tree = base_tree
self.overlay_tree = overlay_tree

View File

@@ -0,0 +1,140 @@
from typing import Optional, Union
from jinja2 import Template
from folium.elements import JSCSSMixin
from folium.map import Layer
class VectorGridProtobuf(JSCSSMixin, Layer):
"""
Add vector tile layers based on https://github.com/Leaflet/Leaflet.VectorGrid.
Parameters
----------
url: str
url to tile provider
e.g. https://free-{s}.tilehosting.com/data/v3/{z}/{x}/{y}.pbf?token={token}
name: str, optional
Name of the layer that will be displayed in LayerControl
options: dict or str, optional
VectorGrid.protobuf options, which you can pass as python dictionary or string.
Strings allow plain JavaScript to be passed, therefore allow for conditional styling (see examples).
Additionally the url might contain any string literals like {token}, or {key}
that can be passed as attribute to the options dict and will be substituted.
Every layer inside the tile layer has to be styled separately.
overlay : bool, default True
Whether your layer will be an overlay (ticked with a check box in
LayerControls) or a base layer (ticked with a radio button).
control: bool, default True
Whether the layer will be included in LayerControls.
show: bool, default True
Whether the layer will be shown on opening.
Examples
--------
Options as dict:
>>> m = folium.Map()
>>> url = "https://free-{s}.tilehosting.com/data/v3/{z}/{x}/{y}.pbf?token={token}"
>>> options = {
... "subdomain": "tilehosting",
... "token": "af6P2G9dztAt1F75x7KYt0Hx2DJR052G",
... "vectorTileLayerStyles": {
... "layer_name_one": {
... "fill": True,
... "weight": 1,
... "fillColor": "green",
... "color": "black",
... "fillOpacity": 0.6,
... "opacity": 0.6,
... },
... "layer_name_two": {
... "fill": True,
... "weight": 1,
... "fillColor": "red",
... "color": "black",
... "fillOpacity": 0.6,
... "opacity": 0.6,
... },
... },
... }
>>> VectorGridProtobuf(url, "layer_name", options).add_to(m)
Options as string allows to pass functions
>>> m = folium.Map()
>>> url = "https://free-{s}.tilehosting.com/data/v3/{z}/{x}/{y}.pbf?token={token}"
>>> options = '''{
... "subdomain": "tilehosting",
... "token": "af6P2G9dztAt1F75x7KYt0Hx2DJR052G",
... "vectorTileLayerStyles": {
... all: function(f) {
... if (f.type === 'parks') {
... return {
... "fill": true,
... "weight": 1,
... "fillColor": 'green',
... "color": 'black',
... "fillOpacity":0.6,
... "opacity":0.6
... };
... }
... if (f.type === 'water') {
... return {
... "fill": true,
... "weight": 1,
... "fillColor": 'purple',
... "color": 'black',
... "fillOpacity":0.6,
... "opacity":0.6
... };
... }
... }
... }
... }'''
>>> VectorGridProtobuf(url, "layer_name", options).add_to(m)
For more info, see: https://leaflet.github.io/Leaflet.VectorGrid/vectorgrid-api-docs.html#styling-vectorgrids.
"""
_template = Template(
"""
{% macro script(this, kwargs) -%}
var {{ this.get_name() }} = L.vectorGrid.protobuf(
'{{ this.url }}',
{%- if this.options is defined %}
{{ this.options if this.options is string else this.options|tojson }}
{%- endif %}
);
{%- endmacro %}
"""
)
default_js = [
(
"vectorGrid",
"https://unpkg.com/leaflet.vectorgrid@latest/dist/Leaflet.VectorGrid.bundled.js",
)
]
def __init__(
self,
url: str,
name: Optional[str] = None,
options: Union[str, dict, None] = None,
overlay: bool = True,
control: bool = True,
show: bool = True,
):
super().__init__(name=name, overlay=overlay, control=control, show=show)
self._name = "VectorGridProtobuf"
self.url = url
if options is not None:
self.options = options