initial commit
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import branca
|
||||
from branca.colormap import ColorMap, LinearColormap, StepColormap
|
||||
from branca.element import (
|
||||
CssLink,
|
||||
Div,
|
||||
Element,
|
||||
Figure,
|
||||
Html,
|
||||
IFrame,
|
||||
JavascriptLink,
|
||||
Link,
|
||||
MacroElement,
|
||||
)
|
||||
|
||||
from folium.features import (
|
||||
Choropleth,
|
||||
ClickForLatLng,
|
||||
ClickForMarker,
|
||||
ColorLine,
|
||||
CustomIcon,
|
||||
DivIcon,
|
||||
GeoJson,
|
||||
GeoJsonPopup,
|
||||
GeoJsonTooltip,
|
||||
LatLngPopup,
|
||||
RegularPolygonMarker,
|
||||
TopoJson,
|
||||
Vega,
|
||||
VegaLite,
|
||||
)
|
||||
from folium.folium import Map
|
||||
from folium.map import (
|
||||
FeatureGroup,
|
||||
FitBounds,
|
||||
FitOverlays,
|
||||
Icon,
|
||||
LayerControl,
|
||||
Marker,
|
||||
Popup,
|
||||
Tooltip,
|
||||
)
|
||||
from folium.raster_layers import TileLayer, WmsTileLayer
|
||||
from folium.utilities import JsCode
|
||||
from folium.vector_layers import Circle, CircleMarker, Polygon, PolyLine, Rectangle
|
||||
|
||||
try:
|
||||
from ._version import __version__
|
||||
except ImportError:
|
||||
__version__ = "unknown"
|
||||
|
||||
|
||||
if branca.__version__ != "unknown" and tuple(
|
||||
int(x) for x in branca.__version__.split(".")[:2]
|
||||
) < (0, 3):
|
||||
raise ImportError(
|
||||
"branca version 0.3.0 or higher is required. "
|
||||
"Update branca with e.g. `pip install branca --upgrade`."
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Choropleth",
|
||||
"ClickForMarker",
|
||||
"ClickForLatLng",
|
||||
"ColorLine",
|
||||
"ColorMap",
|
||||
"CssLink",
|
||||
"CustomIcon",
|
||||
"Div",
|
||||
"DivIcon",
|
||||
"Element",
|
||||
"FeatureGroup",
|
||||
"Figure",
|
||||
"FitBounds",
|
||||
"FitOverlays",
|
||||
"GeoJson",
|
||||
"GeoJsonPopup",
|
||||
"GeoJsonTooltip",
|
||||
"Html",
|
||||
"IFrame",
|
||||
"Icon",
|
||||
"JavascriptLink",
|
||||
"JsCode",
|
||||
"LatLngPopup",
|
||||
"LayerControl",
|
||||
"LinearColormap",
|
||||
"Link",
|
||||
"MacroElement",
|
||||
"Map",
|
||||
"Marker",
|
||||
"Popup",
|
||||
"RegularPolygonMarker",
|
||||
"StepColormap",
|
||||
"TileLayer",
|
||||
"Tooltip",
|
||||
"TopoJson",
|
||||
"Vega",
|
||||
"VegaLite",
|
||||
"WmsTileLayer",
|
||||
# vector_layers
|
||||
"Circle",
|
||||
"CircleMarker",
|
||||
"PolyLine",
|
||||
"Polygon",
|
||||
"Rectangle",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
__version__ = "0.17.0"
|
||||
@@ -0,0 +1,139 @@
|
||||
from typing import List, Tuple
|
||||
|
||||
from branca.element import CssLink, Element, Figure, JavascriptLink, MacroElement
|
||||
from jinja2 import Template
|
||||
|
||||
from folium.utilities import JsCode
|
||||
|
||||
|
||||
class JSCSSMixin(Element):
|
||||
"""Render links to external Javascript and CSS resources."""
|
||||
|
||||
default_js: List[Tuple[str, str]] = []
|
||||
default_css: List[Tuple[str, str]] = []
|
||||
|
||||
def render(self, **kwargs) -> None:
|
||||
figure = self.get_root()
|
||||
assert isinstance(
|
||||
figure, Figure
|
||||
), "You cannot render this Element if it is not in a Figure."
|
||||
|
||||
for name, url in self.default_js:
|
||||
figure.header.add_child(JavascriptLink(url), name=name)
|
||||
|
||||
for name, url in self.default_css:
|
||||
figure.header.add_child(CssLink(url), name=name)
|
||||
|
||||
super().render(**kwargs)
|
||||
|
||||
def add_css_link(self, name: str, url: str):
|
||||
"""Add or update css resource link."""
|
||||
self._add_link(name, url, self.default_css)
|
||||
|
||||
def add_js_link(self, name: str, url: str):
|
||||
"""Add or update JS resource link."""
|
||||
self._add_link(name, url, self.default_js)
|
||||
|
||||
def _add_link(self, name: str, url: str, default_list: List[Tuple[str, str]]):
|
||||
"""Modify a css or js link.
|
||||
|
||||
If `name` does not exist, the link will be appended
|
||||
"""
|
||||
|
||||
for i, pair in enumerate(default_list):
|
||||
if pair[0] == name:
|
||||
default_list[i] = (name, url)
|
||||
break
|
||||
else:
|
||||
default_list.append((name, url))
|
||||
|
||||
|
||||
class EventHandler(MacroElement):
|
||||
'''
|
||||
Add javascript event handlers.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import folium
|
||||
>>> from folium.utilities import JsCode
|
||||
>>>
|
||||
>>> m = folium.Map()
|
||||
>>>
|
||||
>>> geo_json_data = {
|
||||
... "type": "FeatureCollection",
|
||||
... "features": [
|
||||
... {
|
||||
... "type": "Feature",
|
||||
... "geometry": {
|
||||
... "type": "Polygon",
|
||||
... "coordinates": [
|
||||
... [
|
||||
... [100.0, 0.0],
|
||||
... [101.0, 0.0],
|
||||
... [101.0, 1.0],
|
||||
... [100.0, 1.0],
|
||||
... [100.0, 0.0],
|
||||
... ]
|
||||
... ],
|
||||
... },
|
||||
... "properties": {"prop1": {"title": "Somewhere on Sumatra"}},
|
||||
... }
|
||||
... ],
|
||||
... }
|
||||
>>>
|
||||
>>> g = folium.GeoJson(geo_json_data).add_to(m)
|
||||
>>>
|
||||
>>> highlight = JsCode(
|
||||
... """
|
||||
... function highlight(e) {
|
||||
... e.target.original_color = e.layer.options.color;
|
||||
... e.target.setStyle({ color: "green" });
|
||||
... }
|
||||
... """
|
||||
... )
|
||||
>>>
|
||||
>>> reset = JsCode(
|
||||
... """
|
||||
... function reset(e) {
|
||||
... e.target.setStyle({ color: e.target.original_color });
|
||||
... }
|
||||
... """
|
||||
... )
|
||||
>>>
|
||||
>>> g.add_child(EventHandler("mouseover", highlight))
|
||||
>>> g.add_child(EventHandler("mouseout", reset))
|
||||
'''
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro script(this, kwargs) %}
|
||||
{{ this._parent.get_name()}}.on(
|
||||
{{ this.event|tojson}},
|
||||
{{ this.handler.js_code }}
|
||||
);
|
||||
{% endmacro %}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(self, event: str, handler: JsCode):
|
||||
super().__init__()
|
||||
self._name = "EventHandler"
|
||||
self.event = event
|
||||
self.handler = handler
|
||||
|
||||
|
||||
class ElementAddToElement(MacroElement):
|
||||
"""Abstract class to add an element to another element."""
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro script(this, kwargs) %}
|
||||
{{ this.element_name }}.addTo({{ this.element_parent_name }});
|
||||
{% endmacro %}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(self, element_name: str, element_parent_name: str):
|
||||
super().__init__()
|
||||
self.element_name = element_name
|
||||
self.element_parent_name = element_parent_name
|
||||
1975
flight_trackervenv/lib/python3.11/site-packages/folium/features.py
Normal file
1975
flight_trackervenv/lib/python3.11/site-packages/folium/features.py
Normal file
File diff suppressed because it is too large
Load Diff
490
flight_trackervenv/lib/python3.11/site-packages/folium/folium.py
Normal file
490
flight_trackervenv/lib/python3.11/site-packages/folium/folium.py
Normal file
@@ -0,0 +1,490 @@
|
||||
"""
|
||||
Make beautiful, interactive maps with Python and Leaflet.js
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
import webbrowser
|
||||
from typing import Any, List, Optional, Sequence, Union
|
||||
|
||||
from branca.element import Element, Figure
|
||||
from jinja2 import Template
|
||||
|
||||
from folium.elements import JSCSSMixin
|
||||
from folium.map import Evented, FitBounds, Layer
|
||||
from folium.raster_layers import TileLayer
|
||||
from folium.utilities import (
|
||||
TypeBounds,
|
||||
TypeJsonValue,
|
||||
_parse_size,
|
||||
parse_font_size,
|
||||
parse_options,
|
||||
temp_html_filepath,
|
||||
validate_location,
|
||||
)
|
||||
|
||||
_default_js = [
|
||||
("leaflet", "https://cdn.jsdelivr.net/npm/leaflet@1.9.3/dist/leaflet.js"),
|
||||
("jquery", "https://code.jquery.com/jquery-3.7.1.min.js"),
|
||||
(
|
||||
"bootstrap",
|
||||
"https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js",
|
||||
),
|
||||
(
|
||||
"awesome_markers",
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.js",
|
||||
), # noqa
|
||||
]
|
||||
|
||||
_default_css = [
|
||||
("leaflet_css", "https://cdn.jsdelivr.net/npm/leaflet@1.9.3/dist/leaflet.css"),
|
||||
(
|
||||
"bootstrap_css",
|
||||
"https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css",
|
||||
),
|
||||
# glyphicons came from Bootstrap 3 and are used for Awesome Markers
|
||||
(
|
||||
"glyphicons_css",
|
||||
"https://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css",
|
||||
),
|
||||
(
|
||||
"awesome_markers_font_css",
|
||||
"https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.2.0/css/all.min.css",
|
||||
), # noqa
|
||||
(
|
||||
"awesome_markers_css",
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css",
|
||||
), # noqa
|
||||
(
|
||||
"awesome_rotate_css",
|
||||
"https://cdn.jsdelivr.net/gh/python-visualization/folium/folium/templates/leaflet.awesome.rotate.min.css",
|
||||
), # noqa
|
||||
]
|
||||
|
||||
|
||||
class GlobalSwitches(Element):
|
||||
_template = Template(
|
||||
"""
|
||||
<script>
|
||||
L_NO_TOUCH = {{ this.no_touch |tojson}};
|
||||
L_DISABLE_3D = {{ this.disable_3d|tojson }};
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(self, no_touch=False, disable_3d=False):
|
||||
super().__init__()
|
||||
self._name = "GlobalSwitches"
|
||||
self.no_touch = no_touch
|
||||
self.disable_3d = disable_3d
|
||||
|
||||
|
||||
class Map(JSCSSMixin, Evented):
|
||||
"""Create a Map with Folium and Leaflet.js
|
||||
|
||||
Generate a base map of given width and height with either default
|
||||
tilesets or a custom tileset URL. Folium has built-in all tilesets
|
||||
available in the ``xyzservices`` package. For example, you can pass
|
||||
any of the following to the "tiles" keyword:
|
||||
|
||||
- "OpenStreetMap"
|
||||
- "CartoDB Positron"
|
||||
- "CartoDB Voyager"
|
||||
|
||||
Explore more provider names available in ``xyzservices`` here:
|
||||
https://leaflet-extras.github.io/leaflet-providers/preview/.
|
||||
|
||||
You can also pass a custom tileset by passing a
|
||||
:class:`xyzservices.TileProvider` or a Leaflet-style
|
||||
URL to the tiles parameter: ``https://{s}.yourtiles.com/{z}/{x}/{y}.png``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
location: tuple or list, default None
|
||||
Latitude and Longitude of Map (Northing, Easting).
|
||||
width: pixel int or percentage string (default: '100%')
|
||||
Width of the map.
|
||||
height: pixel int or percentage string (default: '100%')
|
||||
Height of the map.
|
||||
tiles: str or TileLayer or :class:`xyzservices.TileProvider`, default 'OpenStreetMap'
|
||||
Map tileset to use. Can choose from a list of built-in tiles,
|
||||
pass a :class:`xyzservices.TileProvider`,
|
||||
pass a custom URL, pass a TileLayer object,
|
||||
or pass `None` to create a map without tiles.
|
||||
For more advanced tile layer options, use the `TileLayer` class.
|
||||
min_zoom: int, optional, default 0
|
||||
Minimum allowed zoom level for the tile layer that is created.
|
||||
Filled by xyzservices by default.
|
||||
max_zoom: int, optional, default 18
|
||||
Maximum allowed zoom level for the tile layer that is created.
|
||||
Filled by xyzservices by default.
|
||||
zoom_start: int, default 10
|
||||
Initial zoom level for the map.
|
||||
attr: string, default None
|
||||
Map tile attribution; only required if passing custom tile URL.
|
||||
crs : str, default 'EPSG3857'
|
||||
Defines coordinate reference systems for projecting geographical points
|
||||
into pixel (screen) coordinates and back.
|
||||
You can use Leaflet's values :
|
||||
* EPSG3857 : The most common CRS for online maps, used by almost all
|
||||
free and commercial tile providers. Uses Spherical Mercator projection.
|
||||
Set in by default in Map's crs option.
|
||||
* EPSG4326 : A common CRS among GIS enthusiasts.
|
||||
Uses simple Equirectangular projection.
|
||||
* EPSG3395 : Rarely used by some commercial tile providers.
|
||||
Uses Elliptical Mercator projection.
|
||||
* Simple : A simple CRS that maps longitude and latitude into
|
||||
x and y directly. May be used for maps of flat surfaces
|
||||
(e.g. game maps). Note that the y axis should still be inverted
|
||||
(going from bottom to top).
|
||||
control_scale : bool, default False
|
||||
Whether to add a control scale on the map.
|
||||
prefer_canvas : bool, default False
|
||||
Forces Leaflet to use the Canvas back-end (if available) for
|
||||
vector layers instead of SVG. This can increase performance
|
||||
considerably in some cases (e.g. many thousands of circle
|
||||
markers on the map).
|
||||
no_touch : bool, default False
|
||||
Forces Leaflet to not use touch events even if it detects them.
|
||||
disable_3d : bool, default False
|
||||
Forces Leaflet to not use hardware-accelerated CSS 3D
|
||||
transforms for positioning (which may cause glitches in some
|
||||
rare environments) even if they're supported.
|
||||
zoom_control : bool or position string, default True
|
||||
Display zoom controls on the map. The default `True` places it in the top left corner.
|
||||
Other options are 'topleft', 'topright', 'bottomleft' or 'bottomright'.
|
||||
font_size : int or float or string (default: '1rem')
|
||||
The font size to use for Leaflet, can either be a number or a
|
||||
string ending in 'rem', 'em', or 'px'.
|
||||
**kwargs
|
||||
Additional keyword arguments are passed to Leaflets Map class:
|
||||
https://leafletjs.com/reference.html#map
|
||||
|
||||
Returns
|
||||
-------
|
||||
Folium Map Object
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> m = folium.Map(location=[45.523, -122.675], width=750, height=500)
|
||||
>>> m = folium.Map(location=[45.523, -122.675], tiles="cartodb positron")
|
||||
>>> m = folium.Map(
|
||||
... location=[45.523, -122.675],
|
||||
... zoom_start=2,
|
||||
... tiles="https://api.mapbox.com/v4/mapbox.streets/{z}/{x}/{y}.png?access_token=mytoken",
|
||||
... attr="Mapbox attribution",
|
||||
... )
|
||||
|
||||
""" # noqa
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro header(this, kwargs) %}
|
||||
<meta name="viewport" content="width=device-width,
|
||||
initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<style>
|
||||
#{{ this.get_name() }} {
|
||||
position: {{this.position}};
|
||||
width: {{this.width[0]}}{{this.width[1]}};
|
||||
height: {{this.height[0]}}{{this.height[1]}};
|
||||
left: {{this.left[0]}}{{this.left[1]}};
|
||||
top: {{this.top[0]}}{{this.top[1]}};
|
||||
}
|
||||
.leaflet-container { font-size: {{this.font_size}}; }
|
||||
</style>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro html(this, kwargs) %}
|
||||
<div class="folium-map" id={{ this.get_name()|tojson }} ></div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro script(this, kwargs) %}
|
||||
var {{ this.get_name() }} = L.map(
|
||||
{{ this.get_name()|tojson }},
|
||||
{
|
||||
center: {{ this.location|tojson }},
|
||||
crs: L.CRS.{{ this.crs }},
|
||||
{%- for key, value in this.options.items() %}
|
||||
{{ key }}: {{ value|tojson }},
|
||||
{%- endfor %}
|
||||
}
|
||||
);
|
||||
|
||||
{%- if this.control_scale %}
|
||||
L.control.scale().addTo({{ this.get_name() }});
|
||||
{%- endif %}
|
||||
|
||||
{%- if this.zoom_control_position %}
|
||||
L.control.zoom( { position: {{ this.zoom_control|tojson }} } ).addTo({{ this.get_name() }});
|
||||
{%- endif %}
|
||||
|
||||
{% if this.objects_to_stay_in_front %}
|
||||
function objects_in_front() {
|
||||
{%- for obj in this.objects_to_stay_in_front %}
|
||||
{{ obj.get_name() }}.bringToFront();
|
||||
{%- endfor %}
|
||||
};
|
||||
{{ this.get_name() }}.on("overlayadd", objects_in_front);
|
||||
$(document).ready(objects_in_front);
|
||||
{%- endif %}
|
||||
|
||||
{% endmacro %}
|
||||
"""
|
||||
)
|
||||
|
||||
# use the module variables for backwards compatibility
|
||||
default_js = _default_js
|
||||
default_css = _default_css
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
location: Optional[Sequence[float]] = None,
|
||||
width: Union[str, float] = "100%",
|
||||
height: Union[str, float] = "100%",
|
||||
left: Union[str, float] = "0%",
|
||||
top: Union[str, float] = "0%",
|
||||
position: str = "relative",
|
||||
tiles: Union[str, TileLayer, None] = "OpenStreetMap",
|
||||
attr: Optional[str] = None,
|
||||
min_zoom: Optional[int] = None,
|
||||
max_zoom: Optional[int] = None,
|
||||
zoom_start: int = 10,
|
||||
min_lat: float = -90,
|
||||
max_lat: float = 90,
|
||||
min_lon: float = -180,
|
||||
max_lon: float = 180,
|
||||
max_bounds: bool = False,
|
||||
crs: str = "EPSG3857",
|
||||
control_scale: bool = False,
|
||||
prefer_canvas: bool = False,
|
||||
no_touch: bool = False,
|
||||
disable_3d: bool = False,
|
||||
png_enabled: bool = False,
|
||||
zoom_control: Union[bool, str] = True,
|
||||
font_size: str = "1rem",
|
||||
**kwargs: TypeJsonValue,
|
||||
):
|
||||
super().__init__()
|
||||
self._name = "Map"
|
||||
|
||||
self._png_image: Optional[bytes] = None
|
||||
self.png_enabled = png_enabled
|
||||
|
||||
if location is None:
|
||||
# If location is not passed we center and zoom out.
|
||||
self.location = [0.0, 0.0]
|
||||
zoom_start = 1
|
||||
else:
|
||||
self.location = validate_location(location)
|
||||
|
||||
Figure().add_child(self)
|
||||
|
||||
# Map Size Parameters.
|
||||
self.width = _parse_size(width)
|
||||
self.height = _parse_size(height)
|
||||
self.left = _parse_size(left)
|
||||
self.top = _parse_size(top)
|
||||
self.position = position
|
||||
self.font_size = parse_font_size(font_size)
|
||||
|
||||
max_bounds_array = (
|
||||
[[min_lat, min_lon], [max_lat, max_lon]] if max_bounds else None
|
||||
)
|
||||
|
||||
self.crs = crs
|
||||
self.control_scale = control_scale
|
||||
|
||||
# Zoom control position specified ?
|
||||
if isinstance(zoom_control, str):
|
||||
self.zoom_control_position = True
|
||||
if zoom_control not in {"topleft", "topright", "bottomleft", "bottomright"}:
|
||||
raise ValueError(
|
||||
"Incorrect value for `zoom_control`, choose from 'topleft', 'topright', 'bottomleft' or 'bottomright'."
|
||||
)
|
||||
self.zoom_control = zoom_control
|
||||
else:
|
||||
self.zoom_control_position = False
|
||||
|
||||
self.options = parse_options(
|
||||
max_bounds=max_bounds_array,
|
||||
zoom=zoom_start,
|
||||
zoom_control=False if self.zoom_control_position else zoom_control,
|
||||
prefer_canvas=prefer_canvas,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.global_switches = GlobalSwitches(no_touch, disable_3d)
|
||||
|
||||
self.objects_to_stay_in_front: List[Layer] = []
|
||||
|
||||
if isinstance(tiles, TileLayer):
|
||||
self.add_child(tiles)
|
||||
elif tiles:
|
||||
tile_layer = TileLayer(
|
||||
tiles=tiles, attr=attr, min_zoom=min_zoom, max_zoom=max_zoom
|
||||
)
|
||||
self.add_child(tile_layer, name=tile_layer.tile_name)
|
||||
|
||||
def _repr_html_(self, **kwargs) -> str:
|
||||
"""Displays the HTML Map in a Jupyter notebook."""
|
||||
if self._parent is None:
|
||||
self.add_to(Figure())
|
||||
self._parent: Figure
|
||||
out = self._parent._repr_html_(**kwargs)
|
||||
self._parent = None
|
||||
else:
|
||||
out = self._parent._repr_html_(**kwargs)
|
||||
return out
|
||||
|
||||
def _to_png(self, delay: int = 3, driver: Any = None) -> bytes:
|
||||
"""Export the HTML to byte representation of a PNG image.
|
||||
|
||||
Uses selenium to render the HTML and record a PNG. You may need to
|
||||
adjust the `delay` time keyword argument if maps render without data or tiles.
|
||||
|
||||
Uses a headless Firefox webdriver by default, though you can provide your own.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> m._to_png()
|
||||
>>> m._to_png(time=10) # Wait 10 seconds between render and snapshot.
|
||||
|
||||
"""
|
||||
if self._png_image is None:
|
||||
if driver is None:
|
||||
from selenium import webdriver
|
||||
|
||||
options = webdriver.firefox.options.Options()
|
||||
options.add_argument("--headless")
|
||||
driver = webdriver.Firefox(options=options)
|
||||
|
||||
html = self.get_root().render()
|
||||
with temp_html_filepath(html) as fname:
|
||||
# We need the tempfile to avoid JS security issues.
|
||||
driver.get(f"file:///{fname}")
|
||||
driver.fullscreen_window()
|
||||
time.sleep(delay)
|
||||
div = driver.find_element("class name", "folium-map")
|
||||
png = div.screenshot_as_png
|
||||
driver.quit()
|
||||
self._png_image = png
|
||||
return self._png_image
|
||||
|
||||
def _repr_png_(self) -> Optional[bytes]:
|
||||
"""Displays the PNG Map in a Jupyter notebook."""
|
||||
# The notebook calls all _repr_*_ by default.
|
||||
# We don't want that here b/c this one is quite slow.
|
||||
if not self.png_enabled:
|
||||
return None
|
||||
return self._to_png()
|
||||
|
||||
def render(self, **kwargs) -> None:
|
||||
"""Renders the HTML representation of the element."""
|
||||
figure = self.get_root()
|
||||
assert isinstance(
|
||||
figure, Figure
|
||||
), "You cannot render this Element if it is not in a Figure."
|
||||
|
||||
# Set global switches
|
||||
figure.header.add_child(self.global_switches, name="global_switches")
|
||||
|
||||
figure.header.add_child(
|
||||
Element(
|
||||
"<style>html, body {"
|
||||
"width: 100%;"
|
||||
"height: 100%;"
|
||||
"margin: 0;"
|
||||
"padding: 0;"
|
||||
"}"
|
||||
"</style>"
|
||||
),
|
||||
name="css_style",
|
||||
)
|
||||
|
||||
figure.header.add_child(
|
||||
Element(
|
||||
"<style>#map {"
|
||||
"position:absolute;"
|
||||
"top:0;"
|
||||
"bottom:0;"
|
||||
"right:0;"
|
||||
"left:0;"
|
||||
"}"
|
||||
"</style>"
|
||||
),
|
||||
name="map_style",
|
||||
)
|
||||
|
||||
super().render(**kwargs)
|
||||
|
||||
def show_in_browser(self) -> None:
|
||||
"""Display the Map in the default web browser."""
|
||||
with temp_html_filepath(self.get_root().render()) as fname:
|
||||
webbrowser.open("file://" + fname)
|
||||
print(
|
||||
"Your map should have been opened in your browser automatically."
|
||||
"\nPress ctrl+c to return."
|
||||
)
|
||||
# Block until stopped by user, afterwards remove the temporary file
|
||||
try:
|
||||
while True:
|
||||
time.sleep(100)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
def fit_bounds(
|
||||
self,
|
||||
bounds: TypeBounds,
|
||||
padding_top_left: Optional[Sequence[float]] = None,
|
||||
padding_bottom_right: Optional[Sequence[float]] = None,
|
||||
padding: Optional[Sequence[float]] = None,
|
||||
max_zoom: Optional[int] = None,
|
||||
) -> None:
|
||||
"""Fit the map to contain a bounding box with the
|
||||
maximum zoom level possible.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bounds: list of (latitude, longitude) points
|
||||
Bounding box specified as two points [southwest, northeast]
|
||||
padding_top_left: (x, y) point, default None
|
||||
Padding in the top left corner. Useful if some elements in
|
||||
the corner, such as controls, might obscure objects you're zooming
|
||||
to.
|
||||
padding_bottom_right: (x, y) point, default None
|
||||
Padding in the bottom right corner.
|
||||
padding: (x, y) point, default None
|
||||
Equivalent to setting both top left and bottom right padding to
|
||||
the same value.
|
||||
max_zoom: int, default None
|
||||
Maximum zoom to be used.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> m.fit_bounds([[52.193636, -2.221575], [52.636878, -1.139759]])
|
||||
|
||||
"""
|
||||
self.add_child(
|
||||
FitBounds(
|
||||
bounds,
|
||||
padding_top_left=padding_top_left,
|
||||
padding_bottom_right=padding_bottom_right,
|
||||
padding=padding,
|
||||
max_zoom=max_zoom,
|
||||
)
|
||||
)
|
||||
|
||||
def keep_in_front(self, *args: Layer) -> None:
|
||||
"""Pass one or multiple layers that must stay in front.
|
||||
|
||||
The ordering matters, the last one is put on top.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*args :
|
||||
Variable length argument list. Any folium object that counts as an
|
||||
overlay. For example FeatureGroup or TileLayer.
|
||||
Does not work with markers, for those use z_index_offset.
|
||||
"""
|
||||
for obj in args:
|
||||
self.objects_to_stay_in_front.append(obj)
|
||||
735
flight_trackervenv/lib/python3.11/site-packages/folium/map.py
Normal file
735
flight_trackervenv/lib/python3.11/site-packages/folium/map.py
Normal file
@@ -0,0 +1,735 @@
|
||||
"""
|
||||
Classes for drawing maps.
|
||||
|
||||
"""
|
||||
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
from typing import Dict, List, Optional, Sequence, Tuple, Type, Union
|
||||
|
||||
from branca.element import Element, Figure, Html, MacroElement
|
||||
from jinja2 import Template
|
||||
|
||||
from folium.elements import ElementAddToElement, EventHandler
|
||||
from folium.utilities import (
|
||||
JsCode,
|
||||
TypeBounds,
|
||||
TypeJsonValue,
|
||||
camelize,
|
||||
escape_backticks,
|
||||
parse_options,
|
||||
validate_location,
|
||||
)
|
||||
|
||||
|
||||
class Evented(MacroElement):
|
||||
"""The base class for Layer and Map
|
||||
|
||||
Adds the `on` method for event handling capabilities.
|
||||
|
||||
See https://leafletjs.com/reference.html#evented for
|
||||
more in depth documentation. Please note that we have
|
||||
only added the `on(<Object> eventMap)` variant of this
|
||||
method using python keyword arguments.
|
||||
"""
|
||||
|
||||
def on(self, **event_map: JsCode):
|
||||
for event_type, handler in event_map.items():
|
||||
self.add_child(EventHandler(event_type, handler))
|
||||
|
||||
|
||||
class Layer(Evented):
|
||||
"""An abstract class for everything that is a Layer on the map.
|
||||
It will be used to define whether an object will be included in
|
||||
LayerControls.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
overlay: bool = False,
|
||||
control: bool = True,
|
||||
show: bool = True,
|
||||
):
|
||||
super().__init__()
|
||||
self.layer_name = name if name is not None else self.get_name()
|
||||
self.overlay = overlay
|
||||
self.control = control
|
||||
self.show = show
|
||||
|
||||
def render(self, **kwargs):
|
||||
if self.show:
|
||||
self.add_child(
|
||||
ElementAddToElement(
|
||||
element_name=self.get_name(),
|
||||
element_parent_name=self._parent.get_name(),
|
||||
),
|
||||
name=self.get_name() + "_add",
|
||||
)
|
||||
super().render(**kwargs)
|
||||
|
||||
|
||||
class FeatureGroup(Layer):
|
||||
"""
|
||||
Create a FeatureGroup layer ; you can put things in it and handle them
|
||||
as a single layer. For example, you can add a LayerControl to
|
||||
tick/untick the whole group.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str, default None
|
||||
The name of the featureGroup layer.
|
||||
It will be displayed in the LayerControl.
|
||||
If None get_name() will be called to get the technical (ugly) name.
|
||||
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.
|
||||
**kwargs
|
||||
Additional (possibly inherited) options. See
|
||||
https://leafletjs.com/reference.html#featuregroup
|
||||
|
||||
"""
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro script(this, kwargs) %}
|
||||
var {{ this.get_name() }} = L.featureGroup(
|
||||
{{ this.options|tojson }}
|
||||
);
|
||||
{% endmacro %}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
overlay: bool = True,
|
||||
control: bool = True,
|
||||
show: bool = True,
|
||||
**kwargs: TypeJsonValue,
|
||||
):
|
||||
super().__init__(name=name, overlay=overlay, control=control, show=show)
|
||||
self._name = "FeatureGroup"
|
||||
self.tile_name = name if name is not None else self.get_name()
|
||||
self.options = parse_options(**kwargs)
|
||||
|
||||
|
||||
class LayerControl(MacroElement):
|
||||
"""
|
||||
Creates a LayerControl object to be added on a folium map.
|
||||
|
||||
This object should be added to a Map object. Only Layer children
|
||||
of Map are included in the layer control.
|
||||
|
||||
Note
|
||||
----
|
||||
The LayerControl should be added last to the map.
|
||||
Otherwise, the LayerControl and/or the controlled layers may not appear.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
position : str
|
||||
The position of the control (one of the map corners), can be
|
||||
'topleft', 'topright', 'bottomleft' or 'bottomright'
|
||||
default: 'topright'
|
||||
collapsed : bool, default True
|
||||
If true the control will be collapsed into an icon and expanded on
|
||||
mouse hover or touch.
|
||||
autoZIndex : bool, default True
|
||||
If true the control assigns zIndexes in increasing order to all of
|
||||
its layers so that the order is preserved when switching them on/off.
|
||||
draggable: bool, default False
|
||||
By default the layer control has a fixed position. Set this argument
|
||||
to True to allow dragging the control around.
|
||||
**kwargs
|
||||
Additional (possibly inherited) options. See
|
||||
https://leafletjs.com/reference.html#control-layers
|
||||
|
||||
"""
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro script(this,kwargs) %}
|
||||
var {{ this.get_name() }}_layers = {
|
||||
base_layers : {
|
||||
{%- for key, val in this.base_layers.items() %}
|
||||
{{ key|tojson }} : {{val}},
|
||||
{%- endfor %}
|
||||
},
|
||||
overlays : {
|
||||
{%- for key, val in this.overlays.items() %}
|
||||
{{ key|tojson }} : {{val}},
|
||||
{%- endfor %}
|
||||
},
|
||||
};
|
||||
let {{ this.get_name() }} = L.control.layers(
|
||||
{{ this.get_name() }}_layers.base_layers,
|
||||
{{ this.get_name() }}_layers.overlays,
|
||||
{{ this.options|tojson }}
|
||||
).addTo({{this._parent.get_name()}});
|
||||
|
||||
{%- if this.draggable %}
|
||||
new L.Draggable({{ this.get_name() }}.getContainer()).enable();
|
||||
{%- endif %}
|
||||
|
||||
{% endmacro %}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
position: str = "topright",
|
||||
collapsed: bool = True,
|
||||
autoZIndex: bool = True,
|
||||
draggable: bool = False,
|
||||
**kwargs: TypeJsonValue,
|
||||
):
|
||||
super().__init__()
|
||||
self._name = "LayerControl"
|
||||
self.options = parse_options(
|
||||
position=position, collapsed=collapsed, autoZIndex=autoZIndex, **kwargs
|
||||
)
|
||||
self.draggable = draggable
|
||||
self.base_layers: OrderedDict[str, str] = OrderedDict()
|
||||
self.overlays: OrderedDict[str, str] = OrderedDict()
|
||||
|
||||
def reset(self) -> None:
|
||||
self.base_layers = OrderedDict()
|
||||
self.overlays = OrderedDict()
|
||||
|
||||
def render(self, **kwargs) -> None:
|
||||
"""Renders the HTML representation of the element."""
|
||||
self.reset()
|
||||
for item in self._parent._children.values():
|
||||
if not isinstance(item, Layer) or not item.control:
|
||||
continue
|
||||
key = item.layer_name
|
||||
if not item.overlay:
|
||||
self.base_layers[key] = item.get_name()
|
||||
else:
|
||||
self.overlays[key] = item.get_name()
|
||||
super().render()
|
||||
|
||||
|
||||
class Icon(MacroElement):
|
||||
"""
|
||||
Creates an Icon object that will be rendered
|
||||
using Leaflet.awesome-markers.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
color : str, default 'blue'
|
||||
The color of the marker. You can use:
|
||||
|
||||
['red', 'blue', 'green', 'purple', 'orange', 'darkred',
|
||||
'lightred', 'beige', 'darkblue', 'darkgreen', 'cadetblue',
|
||||
'darkpurple', 'white', 'pink', 'lightblue', 'lightgreen',
|
||||
'gray', 'black', 'lightgray']
|
||||
|
||||
icon_color : str, default 'white'
|
||||
The color of the drawing on the marker. You can use colors above,
|
||||
or an html color code.
|
||||
icon : str, default 'info-sign'
|
||||
The name of the marker sign.
|
||||
See Font-Awesome website to choose yours.
|
||||
Warning : depending on the icon you choose you may need to adapt
|
||||
the `prefix` as well.
|
||||
angle : int, default 0
|
||||
The icon will be rotated by this amount of degrees.
|
||||
prefix : str, default 'glyphicon'
|
||||
The prefix states the source of the icon. 'fa' for font-awesome or
|
||||
'glyphicon' for bootstrap 3.
|
||||
|
||||
https://github.com/lvoogdt/Leaflet.awesome-markers
|
||||
|
||||
"""
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro script(this, kwargs) %}
|
||||
var {{ this.get_name() }} = L.AwesomeMarkers.icon(
|
||||
{{ this.options|tojson }}
|
||||
);
|
||||
{{ this._parent.get_name() }}.setIcon({{ this.get_name() }});
|
||||
{% endmacro %}
|
||||
"""
|
||||
)
|
||||
color_options = {
|
||||
"red",
|
||||
"darkred",
|
||||
"lightred",
|
||||
"orange",
|
||||
"beige",
|
||||
"green",
|
||||
"darkgreen",
|
||||
"lightgreen",
|
||||
"blue",
|
||||
"darkblue",
|
||||
"cadetblue",
|
||||
"lightblue",
|
||||
"purple",
|
||||
"darkpurple",
|
||||
"pink",
|
||||
"white",
|
||||
"gray",
|
||||
"lightgray",
|
||||
"black",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
color: str = "blue",
|
||||
icon_color: str = "white",
|
||||
icon: str = "info-sign",
|
||||
angle: int = 0,
|
||||
prefix: str = "glyphicon",
|
||||
**kwargs: TypeJsonValue,
|
||||
):
|
||||
super().__init__()
|
||||
self._name = "Icon"
|
||||
if color not in self.color_options:
|
||||
warnings.warn(
|
||||
f"color argument of Icon should be one of: {self.color_options}.",
|
||||
stacklevel=2,
|
||||
)
|
||||
self.options = parse_options(
|
||||
marker_color=color,
|
||||
icon_color=icon_color,
|
||||
icon=icon,
|
||||
prefix=prefix,
|
||||
extra_classes=f"fa-rotate-{angle}",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class Marker(MacroElement):
|
||||
"""
|
||||
Create a simple stock Leaflet marker on the map, with optional
|
||||
popup text or Vincent visualization.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
location: tuple or list
|
||||
Latitude and Longitude of Marker (Northing, Easting)
|
||||
popup: string or folium.Popup, default None
|
||||
Label for the Marker; either an escaped HTML string to initialize
|
||||
folium.Popup or a folium.Popup instance.
|
||||
tooltip: str or folium.Tooltip, default None
|
||||
Display a text when hovering over the object.
|
||||
icon: Icon plugin
|
||||
the Icon plugin to use to render the marker.
|
||||
draggable: bool, default False
|
||||
Set to True to be able to drag the marker around the map.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Marker names and HTML in obj.template_vars
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> Marker(location=[45.5, -122.3], popup="Portland, OR")
|
||||
>>> Marker(location=[45.5, -122.3], popup=Popup("Portland, OR"))
|
||||
# If the popup label has characters that need to be escaped in HTML
|
||||
>>> Marker(
|
||||
... location=[45.5, -122.3],
|
||||
... popup=Popup("Mom & Pop Arrow Shop >>", parse_html=True),
|
||||
... )
|
||||
"""
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro script(this, kwargs) %}
|
||||
var {{ this.get_name() }} = L.marker(
|
||||
{{ this.location|tojson }},
|
||||
{{ this.options|tojson }}
|
||||
).addTo({{ this._parent.get_name() }});
|
||||
{% endmacro %}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
location: Optional[Sequence[float]] = None,
|
||||
popup: Union["Popup", str, None] = None,
|
||||
tooltip: Union["Tooltip", str, None] = None,
|
||||
icon: Optional[Icon] = None,
|
||||
draggable: bool = False,
|
||||
**kwargs: TypeJsonValue,
|
||||
):
|
||||
super().__init__()
|
||||
self._name = "Marker"
|
||||
self.location = validate_location(location) if location is not None else None
|
||||
self.options = parse_options(
|
||||
draggable=draggable or None, autoPan=draggable or None, **kwargs
|
||||
)
|
||||
if icon is not None:
|
||||
self.add_child(icon)
|
||||
self.icon = icon
|
||||
if popup is not None:
|
||||
self.add_child(popup if isinstance(popup, Popup) else Popup(str(popup)))
|
||||
if tooltip is not None:
|
||||
self.add_child(
|
||||
tooltip if isinstance(tooltip, Tooltip) else Tooltip(str(tooltip))
|
||||
)
|
||||
|
||||
def _get_self_bounds(self) -> List[List[float]]:
|
||||
"""Computes the bounds of the object itself.
|
||||
|
||||
Because a marker has only single coordinates, we repeat them.
|
||||
"""
|
||||
assert self.location is not None
|
||||
return [self.location, self.location]
|
||||
|
||||
def render(self) -> None:
|
||||
if self.location is None:
|
||||
raise ValueError(
|
||||
f"{self._name} location must be assigned when added directly to map."
|
||||
)
|
||||
super().render()
|
||||
|
||||
|
||||
class Popup(Element):
|
||||
"""Create a Popup instance that can be linked to a Layer.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
html: string or Element
|
||||
Content of the Popup.
|
||||
parse_html: bool, default False
|
||||
True if the popup is a template that needs to the rendered first.
|
||||
max_width: int for pixels or text for percentages, default '100%'
|
||||
The maximal width of the popup.
|
||||
show: bool, default False
|
||||
True renders the popup open on page load.
|
||||
sticky: bool, default False
|
||||
True prevents map and other popup clicks from closing.
|
||||
lazy: bool, default False
|
||||
True only loads the Popup content when clicking on the Marker.
|
||||
"""
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
var {{this.get_name()}} = L.popup({{ this.options|tojson }});
|
||||
|
||||
{% for name, element in this.html._children.items() %}
|
||||
{% if this.lazy %}
|
||||
{{ this._parent.get_name() }}.once('click', function() {
|
||||
{{ this.get_name() }}.setContent($(`{{ element.render(**kwargs).replace('\\n',' ') }}`)[0]);
|
||||
});
|
||||
{% else %}
|
||||
var {{ name }} = $(`{{ element.render(**kwargs).replace('\\n',' ') }}`)[0];
|
||||
{{ this.get_name() }}.setContent({{ name }});
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{{ this._parent.get_name() }}.bindPopup({{ this.get_name() }})
|
||||
{% if this.show %}.openPopup(){% endif %};
|
||||
|
||||
{% for name, element in this.script._children.items() %}
|
||||
{{element.render()}}
|
||||
{% endfor %}
|
||||
"""
|
||||
) # noqa
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
html: Union[str, Element, None] = None,
|
||||
parse_html: bool = False,
|
||||
max_width: Union[str, int] = "100%",
|
||||
show: bool = False,
|
||||
sticky: bool = False,
|
||||
lazy: bool = False,
|
||||
**kwargs: TypeJsonValue,
|
||||
):
|
||||
super().__init__()
|
||||
self._name = "Popup"
|
||||
self.header = Element()
|
||||
self.html = Element()
|
||||
self.script = Element()
|
||||
|
||||
self.header._parent = self
|
||||
self.html._parent = self
|
||||
self.script._parent = self
|
||||
|
||||
script = not parse_html
|
||||
|
||||
if isinstance(html, Element):
|
||||
self.html.add_child(html)
|
||||
elif isinstance(html, str):
|
||||
html = escape_backticks(html)
|
||||
self.html.add_child(Html(html, script=script))
|
||||
|
||||
self.show = show
|
||||
self.lazy = lazy
|
||||
self.options = parse_options(
|
||||
max_width=max_width,
|
||||
autoClose=False if show or sticky else None,
|
||||
closeOnClick=False if sticky else None,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def render(self, **kwargs) -> None:
|
||||
"""Renders the HTML representation of the element."""
|
||||
for name, child in self._children.items():
|
||||
child.render(**kwargs)
|
||||
|
||||
figure = self.get_root()
|
||||
assert isinstance(
|
||||
figure, Figure
|
||||
), "You cannot render this Element if it is not in a Figure."
|
||||
|
||||
figure.script.add_child(
|
||||
Element(self._template.render(this=self, kwargs=kwargs)),
|
||||
name=self.get_name(),
|
||||
)
|
||||
|
||||
|
||||
class Tooltip(MacroElement):
|
||||
"""
|
||||
Create a tooltip that shows text when hovering over its parent object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
text: str
|
||||
String to display as a tooltip on the object. If the argument is of a
|
||||
different type it will be converted to str.
|
||||
style: str, default None.
|
||||
HTML inline style properties like font and colors. Will be applied to
|
||||
a div with the text in it.
|
||||
sticky: bool, default True
|
||||
Whether the tooltip should follow the mouse.
|
||||
**kwargs
|
||||
These values will map directly to the Leaflet Options. More info
|
||||
available here: https://leafletjs.com/reference.html#tooltip
|
||||
|
||||
"""
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro script(this, kwargs) %}
|
||||
{{ this._parent.get_name() }}.bindTooltip(
|
||||
`<div{% if this.style %} style={{ this.style|tojson }}{% endif %}>
|
||||
{{ this.text }}
|
||||
</div>`,
|
||||
{{ this.options|tojson }}
|
||||
);
|
||||
{% endmacro %}
|
||||
"""
|
||||
)
|
||||
valid_options: Dict[str, Tuple[Type, ...]] = {
|
||||
"pane": (str,),
|
||||
"offset": (tuple,),
|
||||
"direction": (str,),
|
||||
"permanent": (bool,),
|
||||
"sticky": (bool,),
|
||||
"interactive": (bool,),
|
||||
"opacity": (float, int),
|
||||
"attribution": (str,),
|
||||
"className": (str,),
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
text: str,
|
||||
style: Optional[str] = None,
|
||||
sticky: bool = True,
|
||||
**kwargs: TypeJsonValue,
|
||||
):
|
||||
super().__init__()
|
||||
self._name = "Tooltip"
|
||||
|
||||
self.text = str(text)
|
||||
|
||||
kwargs.update({"sticky": sticky})
|
||||
self.options = self.parse_options(kwargs)
|
||||
|
||||
if style:
|
||||
assert isinstance(
|
||||
style, str
|
||||
), "Pass a valid inline HTML style property string to style."
|
||||
# noqa outside of type checking.
|
||||
self.style = style
|
||||
|
||||
def parse_options(
|
||||
self,
|
||||
kwargs: Dict[str, TypeJsonValue],
|
||||
) -> Dict[str, TypeJsonValue]:
|
||||
"""Validate the provided kwargs and return options as json string."""
|
||||
kwargs = {camelize(key): value for key, value in kwargs.items()}
|
||||
for key in kwargs.keys():
|
||||
assert (
|
||||
key in self.valid_options
|
||||
), "The option {} is not in the available options: {}.".format(
|
||||
key, ", ".join(self.valid_options)
|
||||
)
|
||||
assert isinstance(
|
||||
kwargs[key], self.valid_options[key]
|
||||
), f"The option {key} must be one of the following types: {self.valid_options[key]}."
|
||||
return kwargs
|
||||
|
||||
|
||||
class FitBounds(MacroElement):
|
||||
"""Fit the map to contain a bounding box with the
|
||||
maximum zoom level possible.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bounds: list of (latitude, longitude) points
|
||||
Bounding box specified as two points [southwest, northeast]
|
||||
padding_top_left: (x, y) point, default None
|
||||
Padding in the top left corner. Useful if some elements in
|
||||
the corner, such as controls, might obscure objects you're zooming
|
||||
to.
|
||||
padding_bottom_right: (x, y) point, default None
|
||||
Padding in the bottom right corner.
|
||||
padding: (x, y) point, default None
|
||||
Equivalent to setting both top left and bottom right padding to
|
||||
the same value.
|
||||
max_zoom: int, default None
|
||||
Maximum zoom to be used.
|
||||
"""
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro script(this, kwargs) %}
|
||||
{{ this._parent.get_name() }}.fitBounds(
|
||||
{{ this.bounds|tojson }},
|
||||
{{ this.options|tojson }}
|
||||
);
|
||||
{% endmacro %}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bounds: TypeBounds,
|
||||
padding_top_left: Optional[Sequence[float]] = None,
|
||||
padding_bottom_right: Optional[Sequence[float]] = None,
|
||||
padding: Optional[Sequence[float]] = None,
|
||||
max_zoom: Optional[int] = None,
|
||||
):
|
||||
super().__init__()
|
||||
self._name = "FitBounds"
|
||||
self.bounds = bounds
|
||||
self.options = parse_options(
|
||||
max_zoom=max_zoom,
|
||||
padding_top_left=padding_top_left,
|
||||
padding_bottom_right=padding_bottom_right,
|
||||
padding=padding,
|
||||
)
|
||||
|
||||
|
||||
class FitOverlays(MacroElement):
|
||||
"""Fit the bounds of the maps to the enabled overlays.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
padding: int, default 0
|
||||
Amount of padding in pixels applied in the corners.
|
||||
max_zoom: int, optional
|
||||
The maximum possible zoom to use when fitting to the bounds.
|
||||
fly: bool, default False
|
||||
Use a smoother, longer animation.
|
||||
fit_on_map_load: bool, default True
|
||||
Apply the fit when initially loading the map.
|
||||
"""
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro script(this, kwargs) %}
|
||||
function customFlyToBounds() {
|
||||
let bounds = L.latLngBounds([]);
|
||||
{{ this._parent.get_name() }}.eachLayer(function(layer) {
|
||||
if (typeof layer.getBounds === 'function') {
|
||||
bounds.extend(layer.getBounds());
|
||||
}
|
||||
});
|
||||
if (bounds.isValid()) {
|
||||
{{ this._parent.get_name() }}.{{ this.method }}(bounds, {{ this.options|tojson }});
|
||||
}
|
||||
}
|
||||
{{ this._parent.get_name() }}.on('overlayadd', customFlyToBounds);
|
||||
{%- if this.fit_on_map_load %}
|
||||
customFlyToBounds();
|
||||
{%- endif %}
|
||||
{% endmacro %}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
padding: int = 0,
|
||||
max_zoom: Optional[int] = None,
|
||||
fly: bool = False,
|
||||
fit_on_map_load: bool = True,
|
||||
):
|
||||
super().__init__()
|
||||
self._name = "FitOverlays"
|
||||
self.method = "flyToBounds" if fly else "fitBounds"
|
||||
self.fit_on_map_load = fit_on_map_load
|
||||
self.options = parse_options(padding=(padding, padding), max_zoom=max_zoom)
|
||||
|
||||
|
||||
class CustomPane(MacroElement):
|
||||
"""
|
||||
Creates a custom pane to hold map elements.
|
||||
|
||||
Behavior is as in https://leafletjs.com/examples/map-panes/
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name: string
|
||||
Name of the custom pane. Other map elements can be added
|
||||
to the pane by specifying the 'pane' kwarg when constructing
|
||||
them.
|
||||
z_index: int or string, default 625
|
||||
The z-index that will be associated with the pane, and will
|
||||
determine which map elements lie over/under it. The default
|
||||
(625) corresponds to between markers and tooltips. Default
|
||||
panes and z-indexes can be found at
|
||||
https://leafletjs.com/reference.html#map-pane
|
||||
pointer_events: bool, default False
|
||||
Whether or not layers in the pane should interact with the
|
||||
cursor. Setting to False will prevent interfering with
|
||||
pointer events associated with lower layers.
|
||||
"""
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro script(this, kwargs) %}
|
||||
var {{ this.get_name() }} = {{ this._parent.get_name() }}.createPane(
|
||||
{{ this.name|tojson }});
|
||||
{{ this.get_name() }}.style.zIndex = {{ this.z_index|tojson }};
|
||||
{% if not this.pointer_events %}
|
||||
{{ this.get_name() }}.style.pointerEvents = 'none';
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
z_index: Union[int, str] = 625,
|
||||
pointer_events: bool = False,
|
||||
):
|
||||
super().__init__()
|
||||
self._name = "Pane"
|
||||
self.name = name
|
||||
self.z_index = z_index
|
||||
self.pointer_events = pointer_events
|
||||
@@ -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",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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"),
|
||||
}
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
@@ -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};"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
@@ -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})
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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 = " ",
|
||||
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
|
||||
@@ -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
|
||||
@@ -0,0 +1,419 @@
|
||||
"""
|
||||
Wraps leaflet TileLayer, WmsTileLayer (TileLayer.WMS), ImageOverlay, and VideoOverlay
|
||||
|
||||
"""
|
||||
|
||||
from typing import Any, Callable, Optional, Union
|
||||
|
||||
import xyzservices
|
||||
from branca.element import Element, Figure
|
||||
from jinja2 import Template
|
||||
|
||||
from folium.map import Layer
|
||||
from folium.utilities import (
|
||||
TypeBounds,
|
||||
TypeJsonValue,
|
||||
image_to_url,
|
||||
mercator_transform,
|
||||
parse_options,
|
||||
)
|
||||
|
||||
|
||||
class TileLayer(Layer):
|
||||
"""
|
||||
Create a tile layer to append on a Map.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tiles: str or :class:`xyzservices.TileProvider`, default 'OpenStreetMap'
|
||||
Map tileset to use. Folium has built-in all tilesets
|
||||
available in the ``xyzservices`` package. For example, you can pass
|
||||
any of the following to the "tiles" keyword:
|
||||
|
||||
- "OpenStreetMap"
|
||||
- "CartoDB Positron"
|
||||
- "CartoDB Voyager"
|
||||
|
||||
Explore more provider names available in ``xyzservices`` here:
|
||||
https://leaflet-extras.github.io/leaflet-providers/preview/.
|
||||
|
||||
You can also pass a custom tileset by passing a
|
||||
:class:`xyzservices.TileProvider` or a Leaflet-style
|
||||
URL to the tiles parameter: ``https://{s}.yourtiles.com/{z}/{x}/{y}.png``.
|
||||
min_zoom: int, optional, default 0
|
||||
Minimum allowed zoom level for this tile layer. Filled by xyzservices by default.
|
||||
max_zoom: int, optional, default 18
|
||||
Maximum allowed zoom level for this tile layer. Filled by xyzservices by default.
|
||||
max_native_zoom: int, default None
|
||||
The highest zoom level at which the tile server can provide tiles.
|
||||
Filled by xyzservices by default.
|
||||
By setting max_zoom higher than max_native_zoom, you can zoom in
|
||||
past max_native_zoom, tiles will be autoscaled.
|
||||
attr: string, default None
|
||||
Map tile attribution; only required if passing custom tile URL.
|
||||
detect_retina: bool, default False
|
||||
If true and user is on a retina display, it will request four
|
||||
tiles of half the specified size and a bigger zoom level in place
|
||||
of one to utilize the high resolution.
|
||||
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.
|
||||
When adding multiple base layers, use this parameter to select which one
|
||||
should be shown when opening the map, by not showing the others.
|
||||
subdomains: list of strings, default ['abc']
|
||||
Subdomains of the tile service.
|
||||
tms: bool, default False
|
||||
If true, inverses Y axis numbering for tiles (turn this on for TMS
|
||||
services).
|
||||
opacity: float, default 1
|
||||
Sets the opacity for the layer.
|
||||
**kwargs : additional keyword arguments
|
||||
Other keyword arguments are passed as options to the Leaflet tileLayer
|
||||
object.
|
||||
"""
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro script(this, kwargs) %}
|
||||
var {{ this.get_name() }} = L.tileLayer(
|
||||
{{ this.tiles|tojson }},
|
||||
{{ this.options|tojson }}
|
||||
);
|
||||
{% endmacro %}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tiles: Union[str, xyzservices.TileProvider] = "OpenStreetMap",
|
||||
min_zoom: Optional[int] = None,
|
||||
max_zoom: Optional[int] = None,
|
||||
max_native_zoom: Optional[int] = None,
|
||||
attr: Optional[str] = None,
|
||||
detect_retina: bool = False,
|
||||
name: Optional[str] = None,
|
||||
overlay: bool = False,
|
||||
control: bool = True,
|
||||
show: bool = True,
|
||||
no_wrap: bool = False,
|
||||
subdomains: str = "abc",
|
||||
tms: bool = False,
|
||||
opacity: float = 1,
|
||||
**kwargs,
|
||||
):
|
||||
if isinstance(tiles, str):
|
||||
if tiles.lower() == "openstreetmap":
|
||||
tiles = "OpenStreetMap Mapnik"
|
||||
if name is None:
|
||||
name = "openstreetmap"
|
||||
try:
|
||||
tiles = xyzservices.providers.query_name(tiles)
|
||||
except ValueError:
|
||||
# no match, likely a custom URL
|
||||
pass
|
||||
|
||||
if isinstance(tiles, xyzservices.TileProvider):
|
||||
attr = attr if attr else tiles.html_attribution # type: ignore
|
||||
min_zoom = min_zoom or tiles.get("min_zoom")
|
||||
max_zoom = max_zoom or tiles.get("max_zoom")
|
||||
max_native_zoom = max_native_zoom or tiles.get("max_zoom")
|
||||
subdomains = tiles.get("subdomains", subdomains)
|
||||
if name is None:
|
||||
name = tiles.name.replace(".", "").lower()
|
||||
tiles = tiles.build_url(fill_subdomain=False, scale_factor="{r}") # type: ignore
|
||||
|
||||
self.tile_name = (
|
||||
name if name is not None else "".join(tiles.lower().strip().split())
|
||||
)
|
||||
super().__init__(
|
||||
name=self.tile_name, overlay=overlay, control=control, show=show
|
||||
)
|
||||
self._name = "TileLayer"
|
||||
|
||||
self.tiles = tiles
|
||||
if not attr:
|
||||
raise ValueError("Custom tiles must have an attribution.")
|
||||
|
||||
self.options = parse_options(
|
||||
min_zoom=min_zoom or 0,
|
||||
max_zoom=max_zoom or 18,
|
||||
max_native_zoom=max_native_zoom,
|
||||
no_wrap=no_wrap,
|
||||
attribution=attr,
|
||||
subdomains=subdomains,
|
||||
detect_retina=detect_retina,
|
||||
tms=tms,
|
||||
opacity=opacity,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class WmsTileLayer(Layer):
|
||||
"""
|
||||
Creates a Web Map Service (WMS) layer.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
url : str
|
||||
The url of the WMS server.
|
||||
layers : str
|
||||
Comma-separated list of WMS layers to show.
|
||||
styles : str, optional
|
||||
Comma-separated list of WMS styles.
|
||||
fmt : str, default 'image/jpeg'
|
||||
The format of the service output. Ex: 'image/png'
|
||||
transparent: bool, default False
|
||||
Whether the layer shall allow transparency.
|
||||
version : str, default '1.1.1'
|
||||
Version of the WMS service to use.
|
||||
attr : str, default ''
|
||||
The attribution of the service.
|
||||
Will be displayed in the bottom right corner.
|
||||
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.
|
||||
**kwargs : additional keyword arguments
|
||||
Passed through to the underlying tileLayer.wms object and can be used
|
||||
for setting extra tileLayer.wms parameters or as extra parameters in
|
||||
the WMS request.
|
||||
|
||||
See https://leafletjs.com/reference.html#tilelayer-wms
|
||||
"""
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro script(this, kwargs) %}
|
||||
var {{ this.get_name() }} = L.tileLayer.wms(
|
||||
{{ this.url|tojson }},
|
||||
{{ this.options|tojson }}
|
||||
);
|
||||
{% endmacro %}
|
||||
"""
|
||||
) # noqa
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url: str,
|
||||
layers: str,
|
||||
styles: str = "",
|
||||
fmt: str = "image/jpeg",
|
||||
transparent: bool = False,
|
||||
version: str = "1.1.1",
|
||||
attr: str = "",
|
||||
name: Optional[str] = None,
|
||||
overlay: bool = True,
|
||||
control: bool = True,
|
||||
show: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(name=name, overlay=overlay, control=control, show=show)
|
||||
self.url = url
|
||||
kwargs["format"] = fmt
|
||||
cql_filter = kwargs.pop("cql_filter", None)
|
||||
self.options = parse_options(
|
||||
layers=layers,
|
||||
styles=styles,
|
||||
transparent=transparent,
|
||||
version=version,
|
||||
attribution=attr,
|
||||
**kwargs,
|
||||
)
|
||||
if cql_filter:
|
||||
# special parameter that shouldn't be camelized
|
||||
self.options["cql_filter"] = cql_filter
|
||||
|
||||
|
||||
class ImageOverlay(Layer):
|
||||
"""
|
||||
Used to load and display a single image over specific bounds of
|
||||
the map, implements ILayer interface.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image: string, file or array-like object
|
||||
The data you want to draw on the map.
|
||||
* If string, it will be written directly in the output file.
|
||||
* If file, it's content will be converted as embedded in the output file.
|
||||
* If array-like, it will be converted to PNG base64 string and embedded in the output.
|
||||
bounds: list
|
||||
Image bounds on the map in the form
|
||||
[[lat_min, lon_min], [lat_max, lon_max]]
|
||||
opacity: float, default Leaflet's default (1.0)
|
||||
alt: string, default Leaflet's default ('')
|
||||
origin: ['upper' | 'lower'], optional, default 'upper'
|
||||
Place the [0,0] index of the array in the upper left or
|
||||
lower left corner of the axes.
|
||||
colormap: callable, used only for `mono` image.
|
||||
Function of the form [x -> (r,g,b)] or [x -> (r,g,b,a)]
|
||||
for transforming a mono image into RGB.
|
||||
It must output iterables of length 3 or 4,
|
||||
with values between 0 and 1.
|
||||
Hint: you can use colormaps from `matplotlib.cm`.
|
||||
mercator_project: bool, default False.
|
||||
Used only for array-like image. Transforms the data to
|
||||
project (longitude, latitude) coordinates to the Mercator projection.
|
||||
Beware that this will only work if `image` is an array-like object.
|
||||
Note that if used the image will be clipped beyond latitude -85 and 85.
|
||||
pixelated: bool, default True
|
||||
Sharp sharp/crips (True) or aliased corners (False).
|
||||
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.
|
||||
|
||||
See https://leafletjs.com/reference.html#imageoverlay for more
|
||||
options.
|
||||
|
||||
"""
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro script(this, kwargs) %}
|
||||
var {{ this.get_name() }} = L.imageOverlay(
|
||||
{{ this.url|tojson }},
|
||||
{{ this.bounds|tojson }},
|
||||
{{ this.options|tojson }}
|
||||
);
|
||||
{% endmacro %}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
image: Any,
|
||||
bounds: TypeBounds,
|
||||
origin: str = "upper",
|
||||
colormap: Optional[Callable] = None,
|
||||
mercator_project: bool = False,
|
||||
pixelated: bool = True,
|
||||
name: Optional[str] = None,
|
||||
overlay: bool = True,
|
||||
control: bool = True,
|
||||
show: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(name=name, overlay=overlay, control=control, show=show)
|
||||
self._name = "ImageOverlay"
|
||||
self.bounds = bounds
|
||||
self.options = parse_options(**kwargs)
|
||||
self.pixelated = pixelated
|
||||
if mercator_project:
|
||||
image = mercator_transform(
|
||||
image, (bounds[0][0], bounds[1][0]), origin=origin
|
||||
)
|
||||
|
||||
self.url = image_to_url(image, origin=origin, colormap=colormap)
|
||||
|
||||
def render(self, **kwargs) -> None:
|
||||
super().render()
|
||||
|
||||
figure = self.get_root()
|
||||
assert isinstance(
|
||||
figure, Figure
|
||||
), "You cannot render this Element if it is not in a Figure."
|
||||
if self.pixelated:
|
||||
pixelated = """
|
||||
<style>
|
||||
.leaflet-image-layer {
|
||||
/* old android/safari*/
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
image-rendering: crisp-edges; /* safari */
|
||||
image-rendering: pixelated; /* chrome */
|
||||
image-rendering: -moz-crisp-edges; /* firefox */
|
||||
image-rendering: -o-crisp-edges; /* opera */
|
||||
-ms-interpolation-mode: nearest-neighbor; /* ie */
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
figure.header.add_child(
|
||||
Element(pixelated), name="leaflet-image-layer"
|
||||
) # noqa
|
||||
|
||||
def _get_self_bounds(self) -> TypeBounds:
|
||||
"""
|
||||
Computes the bounds of the object itself (not including it's children)
|
||||
in the form [[lat_min, lon_min], [lat_max, lon_max]].
|
||||
|
||||
"""
|
||||
return self.bounds
|
||||
|
||||
|
||||
class VideoOverlay(Layer):
|
||||
"""
|
||||
Used to load and display a video over the map.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
video_url: str
|
||||
URL of the video
|
||||
bounds: list
|
||||
Video bounds on the map in the form
|
||||
[[lat_min, lon_min], [lat_max, lon_max]]
|
||||
autoplay: bool, default True
|
||||
loop: bool, default True
|
||||
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.
|
||||
**kwargs:
|
||||
Other valid (possibly inherited) options. See:
|
||||
https://leafletjs.com/reference.html#videooverlay
|
||||
|
||||
"""
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro script(this, kwargs) %}
|
||||
var {{ this.get_name() }} = L.videoOverlay(
|
||||
{{ this.video_url|tojson }},
|
||||
{{ this.bounds|tojson }},
|
||||
{{ this.options|tojson }}
|
||||
);
|
||||
{% endmacro %}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
video_url: str,
|
||||
bounds: TypeBounds,
|
||||
autoplay: bool = True,
|
||||
loop: bool = True,
|
||||
name: Optional[str] = None,
|
||||
overlay: bool = True,
|
||||
control: bool = True,
|
||||
show: bool = True,
|
||||
**kwargs: TypeJsonValue,
|
||||
):
|
||||
super().__init__(name=name, overlay=overlay, control=control, show=show)
|
||||
self._name = "VideoOverlay"
|
||||
self.video_url = video_url
|
||||
|
||||
self.bounds = bounds
|
||||
self.options = parse_options(autoplay=autoplay, loop=loop, **kwargs)
|
||||
|
||||
def _get_self_bounds(self) -> TypeBounds:
|
||||
"""
|
||||
Computes the bounds of the object itself (not including it's children)
|
||||
in the form [[lat_min, lon_min], [lat_max, lon_max]]
|
||||
|
||||
"""
|
||||
return self.bounds
|
||||
@@ -0,0 +1,51 @@
|
||||
import json
|
||||
from typing import Union
|
||||
|
||||
import jinja2
|
||||
from branca.element import Element
|
||||
|
||||
from folium.utilities import JsCode, TypeJsonValue, camelize
|
||||
|
||||
|
||||
def tojavascript(obj: Union[str, JsCode, dict, list, Element]) -> str:
|
||||
if isinstance(obj, JsCode):
|
||||
return obj.js_code
|
||||
elif isinstance(obj, Element):
|
||||
return obj.get_name()
|
||||
elif isinstance(obj, dict):
|
||||
out = ["{\n"]
|
||||
for key, value in obj.items():
|
||||
out.append(f' "{camelize(key)}": ')
|
||||
out.append(tojavascript(value))
|
||||
out.append(",\n")
|
||||
out.append("}")
|
||||
return "".join(out)
|
||||
elif isinstance(obj, list):
|
||||
out = ["[\n"]
|
||||
for value in obj:
|
||||
out.append(tojavascript(value))
|
||||
out.append(",\n")
|
||||
out.append("]")
|
||||
return "".join(out)
|
||||
else:
|
||||
return _to_escaped_json(obj)
|
||||
|
||||
|
||||
def _to_escaped_json(obj: TypeJsonValue) -> str:
|
||||
return (
|
||||
json.dumps(obj)
|
||||
.replace("<", "\\u003c")
|
||||
.replace(">", "\\u003e")
|
||||
.replace("&", "\\u0026")
|
||||
.replace("'", "\\u0027")
|
||||
)
|
||||
|
||||
|
||||
class Environment(jinja2.Environment):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.filters["tojavascript"] = tojavascript
|
||||
|
||||
|
||||
class Template(jinja2.Template):
|
||||
environment_class = Environment
|
||||
13
flight_trackervenv/lib/python3.11/site-packages/folium/templates/leaflet_heat.min.js
vendored
Normal file
13
flight_trackervenv/lib/python3.11/site-packages/folium/templates/leaflet_heat.min.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
Retrieved from https://leaflet.github.io/Leaflet.heat/dist/leaflet-heat.js
|
||||
Includes patch to fix weights issue (https://github.com/Leaflet/Leaflet.heat/pull/78)
|
||||
*/
|
||||
/*
|
||||
(c) 2014, Vladimir Agafonkin
|
||||
simpleheat, a tiny JavaScript library for drawing heatmaps with Canvas
|
||||
https://github.com/mourner/simpleheat
|
||||
*/!function(){"use strict";function i(t){return this instanceof i?(this._canvas=t="string"==typeof t?document.getElementById(t):t,this._ctx=t.getContext("2d"),this._width=t.width,this._height=t.height,this._max=1,void this.clear()):new i(t)}i.prototype={defaultRadius:25,defaultGradient:{.4:"blue",.6:"cyan",.7:"lime",.8:"yellow",1:"red"},data:function(t,i){return this._data=t,this},max:function(t){return this._max=t,this},add:function(t){return this._data.push(t),this},clear:function(){return this._data=[],this},radius:function(t,i){i=i||15;var a=this._circle=document.createElement("canvas"),s=a.getContext("2d"),e=this._r=t+i;return a.width=a.height=2*e,s.shadowOffsetX=s.shadowOffsetY=200,s.shadowBlur=i,s.shadowColor="black",s.beginPath(),s.arc(e-200,e-200,t,0,2*Math.PI,!0),s.closePath(),s.fill(),this},gradient:function(t){var i=document.createElement("canvas"),a=i.getContext("2d"),s=a.createLinearGradient(0,0,0,256);for(var e in i.width=1,i.height=256,t)s.addColorStop(e,t[e]);return a.fillStyle=s,a.fillRect(0,0,1,256),this._grad=a.getImageData(0,0,1,256).data,this},draw:function(t){this._circle||this.radius(this.defaultRadius),this._grad||this.gradient(this.defaultGradient);var i=this._ctx;i.clearRect(0,0,this._width,this._height);for(var a,s=0,e=this._data.length;s<e;s++)a=this._data[s],i.globalAlpha=Math.max(a[2]/this._max,t||.05),i.drawImage(this._circle,a[0]-this._r,a[1]-this._r);var n=i.getImageData(0,0,this._width,this._height);return this._colorize(n.data,this._grad),i.putImageData(n,0,0),this},_colorize:function(t,i){for(var a,s=3,e=t.length;s<e;s+=4)(a=4*t[s])&&(t[s-3]=i[a],t[s-2]=i[1+a],t[s-1]=i[2+a])}},window.simpleheat=i}(),/*
|
||||
(c) 2014, Vladimir Agafonkin
|
||||
Leaflet.heat, a tiny and fast heatmap plugin for Leaflet.
|
||||
https://github.com/Leaflet/Leaflet.heat
|
||||
*/L.HeatLayer=(L.Layer?L.Layer:L.Class).extend({initialize:function(t,i){this._latlngs=t,L.setOptions(this,i)},setLatLngs:function(t){return this._latlngs=t,this.redraw()},addLatLng:function(t){return this._latlngs.push(t),this.redraw()},setOptions:function(t){return L.setOptions(this,t),this._heat&&this._updateOptions(),this.redraw()},redraw:function(){return this._heat&&!this._frame&&this._map&&!this._map._animating&&(this._frame=L.Util.requestAnimFrame(this._redraw,this)),this},onAdd:function(t){this._map=t,this._canvas||this._initCanvas(),this.options.pane?this.getPane().appendChild(this._canvas):t._panes.overlayPane.appendChild(this._canvas),t.on("moveend",this._reset,this),t.options.zoomAnimation&&L.Browser.any3d&&t.on("zoomanim",this._animateZoom,this),this._reset()},onRemove:function(t){this.options.pane?this.getPane().removeChild(this._canvas):t.getPanes().overlayPane.removeChild(this._canvas),t.off("moveend",this._reset,this),t.options.zoomAnimation&&t.off("zoomanim",this._animateZoom,this)},addTo:function(t){return t.addLayer(this),this},_initCanvas:function(){var t=this._canvas=L.DomUtil.create("canvas","leaflet-heatmap-layer leaflet-layer"),i=L.DomUtil.testProp(["transformOrigin","WebkitTransformOrigin","msTransformOrigin"]);t.style[i]="50% 50%";var a=this._map.getSize();t.width=a.x,t.height=a.y;var s=this._map.options.zoomAnimation&&L.Browser.any3d;L.DomUtil.addClass(t,"leaflet-zoom-"+(s?"animated":"hide")),this._heat=simpleheat(t),this._updateOptions()},_updateOptions:function(){this._heat.radius(this.options.radius||this._heat.defaultRadius,this.options.blur),this.options.gradient&&this._heat.gradient(this.options.gradient)},_reset:function(){var t=this._map.containerPointToLayerPoint([0,0]);L.DomUtil.setPosition(this._canvas,t);var i=this._map.getSize();this._heat._width!==i.x&&(this._canvas.width=this._heat._width=i.x),this._heat._height!==i.y&&(this._canvas.height=this._heat._height=i.y),this._redraw()},_redraw:function(){if(this._map){var t,i,a,s,e,n,h,o,r=[],_=this._heat._r,d=this._map.getSize(),l=new L.Bounds(L.point([-_,-_]),d.add([_,_])),m=_/2,c=[],u=this._map._getMapPanePos(),f=u.x%m,g=u.y%m;for(this._max=1,t=0,i=this._latlngs.length;t<i;t++){a=this._map.latLngToContainerPoint(this._latlngs[t]),e=Math.floor((a.x-f)/m)+2,n=Math.floor((a.y-g)/m)+2;var p=void 0!==this._latlngs[t].alt?this._latlngs[t].alt:void 0!==this._latlngs[t][2]?+this._latlngs[t][2]:1;c[n]=c[n]||[],(s=c[n][e])?(s[0]=(s[0]*s[2]+a.x*p)/(s[2]+p),s[1]=(s[1]*s[2]+a.y*p)/(s[2]+p),s[2]+=p):(s=c[n][e]=[a.x,a.y,p]).p=a,s[2]>this._max&&(this._max=s[2])}for(this._heat.max(this._max),t=0,i=c.length;t<i;t++)if(c[t])for(h=0,o=c[t].length;h<o;h++)(s=c[t][h])&&l.contains(s.p)&&r.push([Math.round(s[0]),Math.round(s[1]),Math.min(s[2],this._max)]);this._heat.data(r).draw(this.options.minOpacity),this._frame=null}},_animateZoom:function(t){var i=this._map.getZoomScale(t.zoom),a=this._map._getCenterOffset(t.center)._multiplyBy(-i).subtract(this._map._getMapPanePos());L.DomUtil.setTransform?L.DomUtil.setTransform(this._canvas,a,i):this._canvas.style[L.DomUtil.TRANSFORM]=L.DomUtil.getTranslateString(a)+" scale("+i+")"}}),L.heatLayer=function(t,i){return new L.HeatLayer(t,i)};
|
||||
9
flight_trackervenv/lib/python3.11/site-packages/folium/templates/pa7_hm.min.js
vendored
Normal file
9
flight_trackervenv/lib/python3.11/site-packages/folium/templates/pa7_hm.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
26
flight_trackervenv/lib/python3.11/site-packages/folium/templates/pa7_leaflet_hm.min.js
vendored
Normal file
26
flight_trackervenv/lib/python3.11/site-packages/folium/templates/pa7_leaflet_hm.min.js
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Leaflet Heatmap Overlay
|
||||
*
|
||||
* Copyright (c) 2008-2016, Patrick Wied (https://www.patrick-wied.at)
|
||||
* Dual-licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
|
||||
* and the Beerware (http://en.wikipedia.org/wiki/Beerware) license.
|
||||
*/
|
||||
;(function(name,context,factory){if(typeof module!=="undefined"&&module.exports){module.exports=factory(require('heatmap.js'),require('leaflet'));}else if(typeof define==="function"&&define.amd){define(['heatmap.js','leaflet'],factory);}else{if(typeof window.h337==='undefined'){throw new Error('heatmap.js must be loaded before the leaflet heatmap plugin');}
|
||||
if(typeof window.L==='undefined'){throw new Error('Leaflet must be loaded before the leaflet heatmap plugin');}
|
||||
context[name]=factory(window.h337,window.L);}})("HeatmapOverlay",this,function(h337,L){'use strict';if(typeof L.Layer==='undefined'){L.Layer=L.Class;}
|
||||
var HeatmapOverlay=L.Layer.extend({initialize:function(config){this.cfg=config;this._el=L.DomUtil.create('div','leaflet-zoom-hide');this._data=[];this._max=1;this._min=0;this.cfg.container=this._el;},onAdd:function(map){var size=map.getSize();this._map=map;this._width=size.x;this._height=size.y;this._el.style.width=size.x+'px';this._el.style.height=size.y+'px';this._el.style.position='absolute';this._origin=this._map.layerPointToLatLng(new L.Point(0,0));map.getPanes().overlayPane.appendChild(this._el);if(!this._heatmap){this._heatmap=h337.create(this.cfg);}
|
||||
map.on('moveend',this._reset,this);this._draw();},addTo:function(map){map.addLayer(this);return this;},onRemove:function(map){map.getPanes().overlayPane.removeChild(this._el);map.off('moveend',this._reset,this);},_draw:function(){if(!this._map){return;}
|
||||
var mapPane=this._map.getPanes().mapPane;var point=mapPane._leaflet_pos;this._el.style[HeatmapOverlay.CSS_TRANSFORM]='translate('+
|
||||
-Math.round(point.x)+'px,'+
|
||||
-Math.round(point.y)+'px)';this._update();},_update:function(){var bounds,zoom,scale;var generatedData={max:this._max,min:this._min,data:[]};bounds=this._map.getBounds();zoom=this._map.getZoom();scale=Math.pow(2,zoom);if(this._data.length==0){if(this._heatmap){this._heatmap.setData(generatedData);}
|
||||
return;}
|
||||
var latLngPoints=[];var radiusMultiplier=this.cfg.scaleRadius?scale:1;var localMax=0;var localMin=0;var valueField=this.cfg.valueField;var len=this._data.length;while(len--){var entry=this._data[len];var value=entry[valueField];var latlng=entry.latlng;if(!bounds.contains(latlng)){continue;}
|
||||
localMax=Math.max(value,localMax);localMin=Math.min(value,localMin);var point=this._map.latLngToContainerPoint(latlng);var latlngPoint={x:Math.round(point.x),y:Math.round(point.y)};latlngPoint[valueField]=value;var radius;if(entry.radius){radius=entry.radius*radiusMultiplier;}else{radius=(this.cfg.radius||2)*radiusMultiplier;}
|
||||
latlngPoint.radius=radius;latLngPoints.push(latlngPoint);}
|
||||
if(this.cfg.useLocalExtrema){generatedData.max=localMax;generatedData.min=localMin;}
|
||||
generatedData.data=latLngPoints;this._heatmap.setData(generatedData);},setData:function(data){this._max=data.max||this._max;this._min=data.min||this._min;var latField=this.cfg.latField||'lat';var lngField=this.cfg.lngField||'lng';var valueField=this.cfg.valueField||'value';var data=data.data;var len=data.length;var d=[];while(len--){var entry=data[len];var latlng=new L.LatLng(entry[latField],entry[lngField]);var dataObj={latlng:latlng};dataObj[valueField]=entry[valueField];if(entry.radius){dataObj.radius=entry.radius;}
|
||||
d.push(dataObj);}
|
||||
this._data=d;this._draw();},addData:function(pointOrArray){if(pointOrArray.length>0){var len=pointOrArray.length;while(len--){this.addData(pointOrArray[len]);}}else{var latField=this.cfg.latField||'lat';var lngField=this.cfg.lngField||'lng';var valueField=this.cfg.valueField||'value';var entry=pointOrArray;var latlng=new L.LatLng(entry[latField],entry[lngField]);var dataObj={latlng:latlng};dataObj[valueField]=entry[valueField];this._max=Math.max(this._max,dataObj[valueField]);this._min=Math.min(this._min,dataObj[valueField]);if(entry.radius){dataObj.radius=entry.radius;}
|
||||
this._data.push(dataObj);this._draw();}},_reset:function(){this._origin=this._map.layerPointToLatLng(new L.Point(0,0));var size=this._map.getSize();if(this._width!==size.x||this._height!==size.y){this._width=size.x;this._height=size.y;this._el.style.width=this._width+'px';this._el.style.height=this._height+'px';this._heatmap._renderer.setDimensions(this._width,this._height);}
|
||||
this._draw();}});HeatmapOverlay.CSS_TRANSFORM=(function(){var div=document.createElement('div');var props=['transform','WebkitTransform','MozTransform','OTransform','msTransform'];for(var i=0;i<props.length;i++){var prop=props[i];if(div.style[prop]!==undefined){return prop;}}
|
||||
return props[0];})();return HeatmapOverlay;});
|
||||
@@ -0,0 +1,443 @@
|
||||
import base64
|
||||
import collections
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import uuid
|
||||
from contextlib import contextmanager
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
from urllib.parse import urlparse, uses_netloc, uses_params, uses_relative
|
||||
|
||||
import numpy as np
|
||||
from branca.element import Element, Figure
|
||||
|
||||
# import here for backwards compatibility
|
||||
from branca.utilities import ( # noqa F401
|
||||
_locations_mirror,
|
||||
_parse_size,
|
||||
none_max,
|
||||
none_min,
|
||||
write_png,
|
||||
)
|
||||
|
||||
try:
|
||||
import pandas as pd
|
||||
except ImportError:
|
||||
pd = None
|
||||
|
||||
|
||||
TypeLine = Iterable[Sequence[float]]
|
||||
TypeMultiLine = Union[TypeLine, Iterable[TypeLine]]
|
||||
|
||||
TypeJsonValueNoNone = Union[str, float, bool, Sequence, dict]
|
||||
TypeJsonValue = Union[TypeJsonValueNoNone, None]
|
||||
|
||||
TypePathOptions = Union[bool, str, float, None]
|
||||
|
||||
TypeBounds = Sequence[Sequence[float]]
|
||||
|
||||
|
||||
_VALID_URLS = set(uses_relative + uses_netloc + uses_params)
|
||||
_VALID_URLS.discard("")
|
||||
_VALID_URLS.add("data")
|
||||
|
||||
|
||||
def validate_location(location: Sequence[float]) -> List[float]:
|
||||
"""Validate a single lat/lon coordinate pair and convert to a list
|
||||
|
||||
Validate that location:
|
||||
* is a sized variable
|
||||
* with size 2
|
||||
* allows indexing (i.e. has an ordering)
|
||||
* where both values are floats (or convertible to float)
|
||||
* and both values are not NaN
|
||||
"""
|
||||
if isinstance(location, np.ndarray) or (
|
||||
pd is not None and isinstance(location, pd.DataFrame)
|
||||
):
|
||||
location = np.squeeze(location).tolist()
|
||||
if not hasattr(location, "__len__"):
|
||||
raise TypeError(
|
||||
"Location should be a sized variable, "
|
||||
"for example a list or a tuple, instead got "
|
||||
f"{location!r} of type {type(location)}."
|
||||
)
|
||||
if len(location) != 2:
|
||||
raise ValueError(
|
||||
"Expected two (lat, lon) values for location, "
|
||||
f"instead got: {location!r}."
|
||||
)
|
||||
try:
|
||||
coords = (location[0], location[1])
|
||||
except (TypeError, KeyError):
|
||||
raise TypeError(
|
||||
"Location should support indexing, like a list or "
|
||||
f"a tuple does, instead got {location!r} of type {type(location)}."
|
||||
)
|
||||
for coord in coords:
|
||||
try:
|
||||
float(coord)
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError(
|
||||
"Location should consist of two numerical values, "
|
||||
f"but {coord!r} of type {type(coord)} is not convertible to float."
|
||||
)
|
||||
if math.isnan(float(coord)):
|
||||
raise ValueError("Location values cannot contain NaNs.")
|
||||
return [float(x) for x in coords]
|
||||
|
||||
|
||||
def _validate_locations_basics(locations: TypeMultiLine) -> None:
|
||||
"""Helper function that does basic validation of line and multi-line types."""
|
||||
try:
|
||||
iter(locations)
|
||||
except TypeError:
|
||||
raise TypeError(
|
||||
"Locations should be an iterable with coordinate pairs,"
|
||||
f" but instead got {locations!r}."
|
||||
)
|
||||
try:
|
||||
next(iter(locations))
|
||||
except StopIteration:
|
||||
raise ValueError("Locations is empty.")
|
||||
|
||||
|
||||
def validate_locations(locations: TypeLine) -> List[List[float]]:
|
||||
"""Validate an iterable with lat/lon coordinate pairs."""
|
||||
locations = if_pandas_df_convert_to_numpy(locations)
|
||||
_validate_locations_basics(locations)
|
||||
return [validate_location(coord_pair) for coord_pair in locations]
|
||||
|
||||
|
||||
def validate_multi_locations(
|
||||
locations: TypeMultiLine,
|
||||
) -> Union[List[List[float]], List[List[List[float]]]]:
|
||||
"""Validate an iterable with possibly nested lists of coordinate pairs."""
|
||||
locations = if_pandas_df_convert_to_numpy(locations)
|
||||
_validate_locations_basics(locations)
|
||||
try:
|
||||
float(next(iter(next(iter(next(iter(locations))))))) # type: ignore
|
||||
except (TypeError, StopIteration):
|
||||
# locations is a list of coordinate pairs
|
||||
return [validate_location(coord_pair) for coord_pair in locations] # type: ignore
|
||||
else:
|
||||
# locations is a list of a list of coordinate pairs, recurse
|
||||
return [validate_locations(lst) for lst in locations] # type: ignore
|
||||
|
||||
|
||||
def if_pandas_df_convert_to_numpy(obj: Any) -> Any:
|
||||
"""Return a Numpy array from a Pandas dataframe.
|
||||
|
||||
Iterating over a DataFrame has weird side effects, such as the first
|
||||
row being the column names. Converting to Numpy is more safe.
|
||||
"""
|
||||
if pd is not None and isinstance(obj, pd.DataFrame):
|
||||
return obj.values
|
||||
else:
|
||||
return obj
|
||||
|
||||
|
||||
def image_to_url(
|
||||
image: Any,
|
||||
colormap: Optional[Callable] = None,
|
||||
origin: str = "upper",
|
||||
) -> str:
|
||||
"""
|
||||
Infers the type of an image argument and transforms it into a URL.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image: string, file or array-like object
|
||||
* If string, it will be written directly in the output file.
|
||||
* If file, it's content will be converted as embedded in the
|
||||
output file.
|
||||
* If array-like, it will be converted to PNG base64 string and
|
||||
embedded in the output.
|
||||
origin: ['upper' | 'lower'], optional, default 'upper'
|
||||
Place the [0, 0] index of the array in the upper left or
|
||||
lower left corner of the axes.
|
||||
colormap: callable, used only for `mono` image.
|
||||
Function of the form [x -> (r,g,b)] or [x -> (r,g,b,a)]
|
||||
for transforming a mono image into RGB.
|
||||
It must output iterables of length 3 or 4, with values between
|
||||
0. and 1. You can use colormaps from `matplotlib.cm`.
|
||||
|
||||
"""
|
||||
if isinstance(image, str) and not _is_url(image):
|
||||
fileformat = os.path.splitext(image)[-1][1:]
|
||||
with open(image, "rb") as f:
|
||||
img = f.read()
|
||||
b64encoded = base64.b64encode(img).decode("utf-8")
|
||||
url = f"data:image/{fileformat};base64,{b64encoded}"
|
||||
elif "ndarray" in image.__class__.__name__:
|
||||
img = write_png(image, origin=origin, colormap=colormap)
|
||||
b64encoded = base64.b64encode(img).decode("utf-8")
|
||||
url = f"data:image/png;base64,{b64encoded}"
|
||||
else:
|
||||
# Round-trip to ensure a nice formatted json.
|
||||
url = json.loads(json.dumps(image))
|
||||
return url.replace("\n", " ")
|
||||
|
||||
|
||||
def _is_url(url: str) -> bool:
|
||||
"""Check to see if `url` has a valid protocol."""
|
||||
try:
|
||||
return urlparse(url).scheme in _VALID_URLS
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def mercator_transform(
|
||||
data: Any,
|
||||
lat_bounds: Tuple[float, float],
|
||||
origin: str = "upper",
|
||||
height_out: Optional[int] = None,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Transforms an image computed in (longitude,latitude) coordinates into
|
||||
the a Mercator projection image.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
data: numpy array or equivalent list-like object.
|
||||
Must be NxM (mono), NxMx3 (RGB) or NxMx4 (RGBA)
|
||||
|
||||
lat_bounds : length 2 tuple
|
||||
Minimal and maximal value of the latitude of the image.
|
||||
Bounds must be between -85.051128779806589 and 85.051128779806589
|
||||
otherwise they will be clipped to that values.
|
||||
|
||||
origin : ['upper' | 'lower'], optional, default 'upper'
|
||||
Place the [0,0] index of the array in the upper left or lower left
|
||||
corner of the axes.
|
||||
|
||||
height_out : int, default None
|
||||
The expected height of the output.
|
||||
If None, the height of the input is used.
|
||||
|
||||
See https://en.wikipedia.org/wiki/Web_Mercator for more details.
|
||||
|
||||
"""
|
||||
|
||||
def mercator(x):
|
||||
return np.arcsinh(np.tan(x * np.pi / 180.0)) * 180.0 / np.pi
|
||||
|
||||
array = np.atleast_3d(data).copy()
|
||||
height, width, nblayers = array.shape
|
||||
|
||||
lat_min = max(lat_bounds[0], -85.051128779806589)
|
||||
lat_max = min(lat_bounds[1], 85.051128779806589)
|
||||
if height_out is None:
|
||||
height_out = height
|
||||
|
||||
# Eventually flip the image
|
||||
if origin == "upper":
|
||||
array = array[::-1, :, :]
|
||||
|
||||
lats = lat_min + np.linspace(0.5 / height, 1.0 - 0.5 / height, height) * (
|
||||
lat_max - lat_min
|
||||
)
|
||||
latslats = mercator(lat_min) + np.linspace(
|
||||
0.5 / height_out, 1.0 - 0.5 / height_out, height_out
|
||||
) * (mercator(lat_max) - mercator(lat_min))
|
||||
|
||||
out = np.zeros((height_out, width, nblayers))
|
||||
for i in range(width):
|
||||
for j in range(nblayers):
|
||||
out[:, i, j] = np.interp(latslats, mercator(lats), array[:, i, j])
|
||||
|
||||
# Eventually flip the image.
|
||||
if origin == "upper":
|
||||
out = out[::-1, :, :]
|
||||
return out
|
||||
|
||||
|
||||
def iter_coords(obj: Any) -> Iterator[Tuple[float, ...]]:
|
||||
"""
|
||||
Returns all the coordinate tuples from a geometry or feature.
|
||||
|
||||
"""
|
||||
if isinstance(obj, (tuple, list)):
|
||||
coords = obj
|
||||
elif "features" in obj:
|
||||
coords = [
|
||||
geom["geometry"]["coordinates"]
|
||||
for geom in obj["features"]
|
||||
if geom["geometry"]
|
||||
]
|
||||
elif "geometry" in obj:
|
||||
coords = obj["geometry"]["coordinates"] if obj["geometry"] else []
|
||||
elif (
|
||||
"geometries" in obj
|
||||
and obj["geometries"][0]
|
||||
and "coordinates" in obj["geometries"][0]
|
||||
):
|
||||
coords = obj["geometries"][0]["coordinates"]
|
||||
else:
|
||||
coords = obj.get("coordinates", obj)
|
||||
for coord in coords:
|
||||
if isinstance(coord, (float, int)):
|
||||
yield tuple(coords)
|
||||
break
|
||||
else:
|
||||
yield from iter_coords(coord)
|
||||
|
||||
|
||||
def get_bounds(
|
||||
locations: Any,
|
||||
lonlat: bool = False,
|
||||
) -> List[List[Optional[float]]]:
|
||||
"""
|
||||
Computes the bounds of the object in the form
|
||||
[[lat_min, lon_min], [lat_max, lon_max]]
|
||||
|
||||
"""
|
||||
bounds: List[List[Optional[float]]] = [[None, None], [None, None]]
|
||||
for point in iter_coords(locations):
|
||||
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]),
|
||||
],
|
||||
]
|
||||
if lonlat:
|
||||
bounds = _locations_mirror(bounds)
|
||||
return bounds
|
||||
|
||||
|
||||
def camelize(key: str) -> str:
|
||||
"""Convert a python_style_variable_name to lowerCamelCase.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> camelize("variable_name")
|
||||
'variableName'
|
||||
>>> camelize("variableName")
|
||||
'variableName'
|
||||
"""
|
||||
return "".join(x.capitalize() if i > 0 else x for i, x in enumerate(key.split("_")))
|
||||
|
||||
|
||||
def compare_rendered(obj1: str, obj2: str) -> bool:
|
||||
"""
|
||||
Return True/False if the normalized rendered version of
|
||||
two folium map objects are the equal or not.
|
||||
|
||||
"""
|
||||
return normalize(obj1) == normalize(obj2)
|
||||
|
||||
|
||||
def normalize(rendered: str) -> str:
|
||||
"""Return the input string without non-functional spaces or newlines."""
|
||||
out = "".join([line.strip() for line in rendered.splitlines() if line.strip()])
|
||||
out = out.replace(", ", ",")
|
||||
return out
|
||||
|
||||
|
||||
@contextmanager
|
||||
def temp_html_filepath(data: str) -> Iterator[str]:
|
||||
"""Yields the path of a temporary HTML file containing data."""
|
||||
filepath = ""
|
||||
try:
|
||||
fid, filepath = tempfile.mkstemp(suffix=".html", prefix="folium_")
|
||||
os.write(fid, data.encode("utf8") if isinstance(data, str) else data)
|
||||
os.close(fid)
|
||||
yield filepath
|
||||
finally:
|
||||
if os.path.isfile(filepath):
|
||||
os.remove(filepath)
|
||||
|
||||
|
||||
def deep_copy(item_original: Element) -> Element:
|
||||
"""Return a recursive deep-copy of item where each copy has a new ID."""
|
||||
item = copy.copy(item_original)
|
||||
item._id = uuid.uuid4().hex
|
||||
if hasattr(item, "_children") and len(item._children) > 0:
|
||||
children_new = collections.OrderedDict()
|
||||
for subitem_original in item._children.values():
|
||||
subitem = deep_copy(subitem_original)
|
||||
subitem._parent = item
|
||||
children_new[subitem.get_name()] = subitem
|
||||
item._children = children_new
|
||||
return item
|
||||
|
||||
|
||||
def get_obj_in_upper_tree(element: Element, cls: Type) -> Element:
|
||||
"""Return the first object in the parent tree of class `cls`."""
|
||||
parent = element._parent
|
||||
if parent is None:
|
||||
raise ValueError(f"The top of the tree was reached without finding a {cls}")
|
||||
if not isinstance(parent, cls):
|
||||
return get_obj_in_upper_tree(parent, cls)
|
||||
return parent
|
||||
|
||||
|
||||
def parse_options(**kwargs: TypeJsonValue) -> Dict[str, TypeJsonValueNoNone]:
|
||||
"""Return a dict with lower-camelcase keys and non-None values.."""
|
||||
return {camelize(key): value for key, value in kwargs.items() if value is not None}
|
||||
|
||||
|
||||
def escape_backticks(text: str) -> str:
|
||||
"""Escape backticks so text can be used in a JS template."""
|
||||
return re.sub(r"(?<!\\)`", r"\`", text)
|
||||
|
||||
|
||||
def escape_double_quotes(text: str) -> str:
|
||||
return text.replace('"', r"\"")
|
||||
|
||||
|
||||
def javascript_identifier_path_to_array_notation(path: str) -> str:
|
||||
"""Convert a path like obj1.obj2 to array notation: ["obj1"]["obj2"]."""
|
||||
return "".join(f'["{escape_double_quotes(x)}"]' for x in path.split("."))
|
||||
|
||||
|
||||
def get_and_assert_figure_root(obj: Element) -> Figure:
|
||||
"""Return the root element of the tree and assert it's a Figure."""
|
||||
figure = obj.get_root()
|
||||
assert isinstance(
|
||||
figure, Figure
|
||||
), "You cannot render this Element if it is not in a Figure."
|
||||
return figure
|
||||
|
||||
|
||||
class JsCode:
|
||||
"""Wrapper around Javascript code."""
|
||||
|
||||
def __init__(self, js_code: Union[str, "JsCode"]):
|
||||
if isinstance(js_code, JsCode):
|
||||
self.js_code: str = js_code.js_code
|
||||
else:
|
||||
self.js_code = js_code
|
||||
|
||||
def __str__(self):
|
||||
return self.js_code
|
||||
|
||||
|
||||
def parse_font_size(value: Union[str, int, float]) -> str:
|
||||
"""Parse a font size value, if number set as px"""
|
||||
if isinstance(value, (int, float)):
|
||||
return f"{value}px"
|
||||
|
||||
if (value[-3:] != "rem") and (value[-2:] not in ["em", "px"]):
|
||||
raise ValueError("The font size must be expressed in rem, em, or px.")
|
||||
return value
|
||||
@@ -0,0 +1,386 @@
|
||||
"""
|
||||
Wraps leaflet Polyline, Polygon, Rectangle, Circle, and CircleMarker
|
||||
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Sequence, Union
|
||||
|
||||
from branca.element import MacroElement
|
||||
from jinja2 import Template
|
||||
|
||||
from folium.map import Marker, Popup, Tooltip
|
||||
from folium.utilities import (
|
||||
TypeLine,
|
||||
TypeMultiLine,
|
||||
TypePathOptions,
|
||||
camelize,
|
||||
get_bounds,
|
||||
validate_locations,
|
||||
validate_multi_locations,
|
||||
)
|
||||
|
||||
|
||||
def path_options(
|
||||
line: bool = False, radius: Optional[float] = None, **kwargs: TypePathOptions
|
||||
):
|
||||
"""
|
||||
Contains options and constants shared between vector overlays
|
||||
(Polygon, Polyline, Circle, CircleMarker, and Rectangle).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
stroke: Bool, True
|
||||
Whether to draw stroke along the path.
|
||||
Set it to false to disable borders on polygons or circles.
|
||||
color: str, '#3388ff'
|
||||
Stroke color.
|
||||
weight: int, 3
|
||||
Stroke width in pixels.
|
||||
opacity: float, 1.0
|
||||
Stroke opacity.
|
||||
line_cap: str, 'round' (lineCap)
|
||||
A string that defines shape to be used at the end of the stroke.
|
||||
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-linecap
|
||||
line_join: str, 'round' (lineJoin)
|
||||
A string that defines shape to be used at the corners of the stroke.
|
||||
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-linejoin
|
||||
dash_array: str, None (dashArray)
|
||||
A string that defines the stroke dash pattern.
|
||||
Doesn't work on Canvas-powered layers in some old browsers.
|
||||
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray
|
||||
dash_offset:, str, None (dashOffset)
|
||||
A string that defines the distance into the dash pattern to start the dash.
|
||||
Doesn't work on Canvas-powered layers in some old browsers.
|
||||
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dashoffset
|
||||
fill: Bool, False
|
||||
Whether to fill the path with color.
|
||||
Set it to false to disable filling on polygons or circles.
|
||||
fill_color: str, default to `color` (fillColor)
|
||||
Fill color. Defaults to the value of the color option.
|
||||
fill_opacity: float, 0.2 (fillOpacity)
|
||||
Fill opacity.
|
||||
fill_rule: str, 'evenodd' (fillRule)
|
||||
A string that defines how the inside of a shape is determined.
|
||||
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule
|
||||
bubbling_mouse_events: Bool, True (bubblingMouseEvents)
|
||||
When true a mouse event on this path will trigger the same event on the
|
||||
map (unless L.DomEvent.stopPropagation is used).
|
||||
gradient: bool, default None
|
||||
When a gradient on the stroke and fill is available,
|
||||
allows turning it on or off.
|
||||
|
||||
Note that the presence of `fill_color` will override `fill=False`.
|
||||
|
||||
This function accepts both snake_case and lowerCamelCase equivalents.
|
||||
|
||||
See https://leafletjs.com/reference.html#path
|
||||
|
||||
"""
|
||||
|
||||
kwargs = {camelize(key): value for key, value in kwargs.items()}
|
||||
|
||||
extra_options = {}
|
||||
if line:
|
||||
extra_options = {
|
||||
"smoothFactor": kwargs.pop("smoothFactor", 1.0),
|
||||
"noClip": kwargs.pop("noClip", False),
|
||||
}
|
||||
if radius:
|
||||
extra_options.update({"radius": radius})
|
||||
|
||||
color = kwargs.pop("color", "#3388ff")
|
||||
fill_color = kwargs.pop("fillColor", False)
|
||||
if fill_color:
|
||||
fill = True
|
||||
elif not fill_color:
|
||||
fill_color = color
|
||||
fill = kwargs.pop("fill", False) # type: ignore
|
||||
|
||||
gradient = kwargs.pop("gradient", None)
|
||||
if gradient is not None:
|
||||
extra_options.update({"gradient": gradient})
|
||||
|
||||
if kwargs.get("tags"):
|
||||
extra_options["tags"] = kwargs.pop("tags")
|
||||
|
||||
default = {
|
||||
"stroke": kwargs.pop("stroke", True),
|
||||
"color": color,
|
||||
"weight": kwargs.pop("weight", 3),
|
||||
"opacity": kwargs.pop("opacity", 1.0),
|
||||
"lineCap": kwargs.pop("lineCap", "round"),
|
||||
"lineJoin": kwargs.pop("lineJoin", "round"),
|
||||
"dashArray": kwargs.pop("dashArray", None),
|
||||
"dashOffset": kwargs.pop("dashOffset", None),
|
||||
"fill": fill,
|
||||
"fillColor": fill_color,
|
||||
"fillOpacity": kwargs.pop("fillOpacity", 0.2),
|
||||
"fillRule": kwargs.pop("fillRule", "evenodd"),
|
||||
"bubblingMouseEvents": kwargs.pop("bubblingMouseEvents", True),
|
||||
}
|
||||
default.update(extra_options)
|
||||
return default
|
||||
|
||||
|
||||
class BaseMultiLocation(MacroElement):
|
||||
"""Base class for vector classes with multiple coordinates.
|
||||
|
||||
:meta private:
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
locations: TypeMultiLine,
|
||||
popup: Union[Popup, str, None] = None,
|
||||
tooltip: Union[Tooltip, str, None] = None,
|
||||
):
|
||||
super().__init__()
|
||||
self.locations = validate_multi_locations(locations)
|
||||
if popup is not None:
|
||||
self.add_child(popup if isinstance(popup, Popup) else Popup(str(popup)))
|
||||
if tooltip is not None:
|
||||
self.add_child(
|
||||
tooltip if isinstance(tooltip, Tooltip) else Tooltip(str(tooltip))
|
||||
)
|
||||
|
||||
def _get_self_bounds(self) -> List[List[Optional[float]]]:
|
||||
"""Compute the bounds of the object itself."""
|
||||
return get_bounds(self.locations)
|
||||
|
||||
|
||||
class PolyLine(BaseMultiLocation):
|
||||
"""Draw 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)
|
||||
Pass multiple sequences of coordinates for a multi-polyline.
|
||||
popup: str or folium.Popup, default None
|
||||
Input text or visualization for object displayed when clicking.
|
||||
tooltip: str or folium.Tooltip, default None
|
||||
Display a text when hovering over the object.
|
||||
smooth_factor: float, default 1.0
|
||||
How much to simplify the polyline on each zoom level.
|
||||
More means better performance and smoother look,
|
||||
and less means more accurate representation.
|
||||
no_clip: Bool, default False
|
||||
Disable polyline clipping.
|
||||
**kwargs
|
||||
Other valid (possibly inherited) options. See:
|
||||
https://leafletjs.com/reference.html#polyline
|
||||
|
||||
"""
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro script(this, kwargs) %}
|
||||
var {{ this.get_name() }} = L.polyline(
|
||||
{{ this.locations|tojson }},
|
||||
{{ this.options|tojson }}
|
||||
).addTo({{this._parent.get_name()}});
|
||||
{% endmacro %}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(self, locations, popup=None, tooltip=None, **kwargs):
|
||||
super().__init__(locations, popup=popup, tooltip=tooltip)
|
||||
self._name = "PolyLine"
|
||||
self.options = path_options(line=True, **kwargs)
|
||||
|
||||
|
||||
class Polygon(BaseMultiLocation):
|
||||
"""Draw polygon overlays on a map.
|
||||
|
||||
See :func:`folium.vector_layers.path_options` for the `Path` options.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
locations: list of points (latitude, longitude)
|
||||
- One list of coordinate pairs to define a polygon. You don't have to
|
||||
add a last point equal to the first point.
|
||||
- If you pass a list with multiple of those it will make a multi-
|
||||
polygon.
|
||||
popup: string or folium.Popup, default None
|
||||
Input text or visualization for object displayed when clicking.
|
||||
tooltip: str or folium.Tooltip, default None
|
||||
Display a text when hovering over the object.
|
||||
**kwargs
|
||||
Other valid (possibly inherited) options. See:
|
||||
https://leafletjs.com/reference.html#polygon
|
||||
|
||||
"""
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro script(this, kwargs) %}
|
||||
var {{ this.get_name() }} = L.polygon(
|
||||
{{ this.locations|tojson }},
|
||||
{{ this.options|tojson }}
|
||||
).addTo({{this._parent.get_name()}});
|
||||
{% endmacro %}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
locations: TypeMultiLine,
|
||||
popup: Union[Popup, str, None] = None,
|
||||
tooltip: Union[Tooltip, str, None] = None,
|
||||
**kwargs: TypePathOptions
|
||||
):
|
||||
super().__init__(locations, popup=popup, tooltip=tooltip)
|
||||
self._name = "Polygon"
|
||||
self.options = path_options(line=True, radius=None, **kwargs)
|
||||
|
||||
|
||||
class Rectangle(MacroElement):
|
||||
"""Draw rectangle overlays on a map.
|
||||
|
||||
See :func:`folium.vector_layers.path_options` for the `Path` options.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bounds: [(lat1, lon1), (lat2, lon2)]
|
||||
Two lat lon pairs marking the two corners of the rectangle.
|
||||
popup: string or folium.Popup, default None
|
||||
Input text or visualization for object displayed when clicking.
|
||||
tooltip: str or folium.Tooltip, default None
|
||||
Display a text when hovering over the object.
|
||||
**kwargs
|
||||
Other valid (possibly inherited) options. See:
|
||||
https://leafletjs.com/reference.html#rectangle
|
||||
|
||||
"""
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro script(this, kwargs) %}
|
||||
var {{this.get_name()}} = L.rectangle(
|
||||
{{ this.locations|tojson }},
|
||||
{{ this.options|tojson }}
|
||||
).addTo({{this._parent.get_name()}});
|
||||
{% endmacro %}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bounds: TypeLine,
|
||||
popup: Union[Popup, str, None] = None,
|
||||
tooltip: Union[Tooltip, str, None] = None,
|
||||
**kwargs: TypePathOptions
|
||||
):
|
||||
super().__init__()
|
||||
self._name = "rectangle"
|
||||
self.options = path_options(line=True, radius=None, **kwargs)
|
||||
self.locations = validate_locations(bounds)
|
||||
assert len(self.locations) == 2, "Need two lat/lon pairs"
|
||||
if popup is not None:
|
||||
self.add_child(popup if isinstance(popup, Popup) else Popup(str(popup)))
|
||||
if tooltip is not None:
|
||||
self.add_child(
|
||||
tooltip if isinstance(tooltip, Tooltip) else Tooltip(str(tooltip))
|
||||
)
|
||||
|
||||
def _get_self_bounds(self) -> List[List[Optional[float]]]:
|
||||
"""Compute the bounds of the object itself."""
|
||||
return get_bounds(self.locations)
|
||||
|
||||
|
||||
class Circle(Marker):
|
||||
"""
|
||||
Class for drawing circle overlays on a map.
|
||||
|
||||
It's an approximation and starts to diverge from a real circle closer to
|
||||
the poles (due to projection distortion).
|
||||
|
||||
See :func:`folium.vector_layers.path_options` for the `Path` options.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
location: tuple[float, float]
|
||||
Latitude and Longitude pair (Northing, Easting)
|
||||
popup: string or folium.Popup, default None
|
||||
Input text or visualization for object displayed when clicking.
|
||||
tooltip: str or folium.Tooltip, default None
|
||||
Display a text when hovering over the object.
|
||||
radius: float
|
||||
Radius of the circle, in meters.
|
||||
**kwargs
|
||||
Other valid (possibly inherited) options. See:
|
||||
https://leafletjs.com/reference.html#circle
|
||||
|
||||
"""
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro script(this, kwargs) %}
|
||||
var {{ this.get_name() }} = L.circle(
|
||||
{{ this.location|tojson }},
|
||||
{{ this.options|tojson }}
|
||||
).addTo({{ this._parent.get_name() }});
|
||||
{% endmacro %}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
location: Optional[Sequence[float]] = None,
|
||||
radius: float = 50,
|
||||
popup: Union[Popup, str, None] = None,
|
||||
tooltip: Union[Tooltip, str, None] = None,
|
||||
**kwargs: TypePathOptions
|
||||
):
|
||||
super().__init__(location, popup=popup, tooltip=tooltip)
|
||||
self._name = "circle"
|
||||
self.options = path_options(line=False, radius=radius, **kwargs)
|
||||
|
||||
|
||||
class CircleMarker(Marker):
|
||||
"""
|
||||
A circle of a fixed size with radius specified in pixels.
|
||||
|
||||
See :func:`folium.vector_layers.path_options` for the `Path` options.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
location: tuple[float, float]
|
||||
Latitude and Longitude pair (Northing, Easting)
|
||||
popup: string or folium.Popup, default None
|
||||
Input text or visualization for object displayed when clicking.
|
||||
tooltip: str or folium.Tooltip, default None
|
||||
Display a text when hovering over the object.
|
||||
radius: float, default 10
|
||||
Radius of the circle marker, in pixels.
|
||||
**kwargs
|
||||
Other valid (possibly inherited) options. See:
|
||||
https://leafletjs.com/reference.html#circlemarker
|
||||
|
||||
"""
|
||||
|
||||
_template = Template(
|
||||
"""
|
||||
{% macro script(this, kwargs) %}
|
||||
var {{ this.get_name() }} = L.circleMarker(
|
||||
{{ this.location|tojson }},
|
||||
{{ this.options|tojson }}
|
||||
).addTo({{ this._parent.get_name() }});
|
||||
{% endmacro %}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
location: Optional[Sequence[float]] = None,
|
||||
radius: float = 10,
|
||||
popup: Union[Popup, str, None] = None,
|
||||
tooltip: Union[Tooltip, str, None] = None,
|
||||
**kwargs: TypePathOptions
|
||||
):
|
||||
super().__init__(location, popup=popup, tooltip=tooltip)
|
||||
self._name = "CircleMarker"
|
||||
self.options = path_options(line=False, radius=radius, **kwargs)
|
||||
Reference in New Issue
Block a user