initial commit

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

View File

@@ -0,0 +1,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