initial commit
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
"""
|
||||
Utilities
|
||||
-------
|
||||
|
||||
Utility module for Folium helper functions.
|
||||
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import typing
|
||||
import zlib
|
||||
from typing import Any, Callable, Union
|
||||
|
||||
from jinja2 import Environment, PackageLoader
|
||||
|
||||
try:
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
np = None
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from branca.colormap import ColorMap
|
||||
|
||||
|
||||
rootpath = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
def get_templates():
|
||||
"""Get Jinja templates."""
|
||||
return Environment(loader=PackageLoader("branca", "templates"))
|
||||
|
||||
|
||||
def legend_scaler(legend_values, max_labels=10.0):
|
||||
"""
|
||||
Downsamples the number of legend values so that there isn't a collision
|
||||
of text on the legend colorbar (within reason). The colorbar seems to
|
||||
support ~10 entries as a maximum.
|
||||
|
||||
"""
|
||||
if len(legend_values) < max_labels:
|
||||
legend_ticks = legend_values
|
||||
else:
|
||||
spacer = int(math.ceil(len(legend_values) / max_labels))
|
||||
legend_ticks = []
|
||||
for i in legend_values[::spacer]:
|
||||
legend_ticks += [i]
|
||||
legend_ticks += [""] * (spacer - 1)
|
||||
return legend_ticks
|
||||
|
||||
|
||||
def linear_gradient(hexList, nColors):
|
||||
"""
|
||||
Given a list of hexcode values, will return a list of length
|
||||
nColors where the colors are linearly interpolated between the
|
||||
(r, g, b) tuples that are given.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> linear_gradient([(0, 0, 0), (255, 0, 0), (255, 255, 0)], 100)
|
||||
|
||||
"""
|
||||
|
||||
def _scale(start, finish, length, i):
|
||||
"""
|
||||
Return the value correct value of a number that is in between start
|
||||
and finish, for use in a loop of length *length*.
|
||||
|
||||
"""
|
||||
base = 16
|
||||
|
||||
fraction = float(i) / (length - 1)
|
||||
raynge = int(finish, base) - int(start, base)
|
||||
thex = hex(int(int(start, base) + fraction * raynge)).split("x")[-1]
|
||||
if len(thex) != 2:
|
||||
thex = "0" + thex
|
||||
return thex
|
||||
|
||||
allColors = []
|
||||
# Separate (R, G, B) pairs.
|
||||
for start, end in zip(hexList[:-1], hexList[1:]):
|
||||
# Linearly interpolate between pair of hex ###### values and
|
||||
# add to list.
|
||||
nInterpolate = 765
|
||||
for index in range(nInterpolate):
|
||||
r = _scale(start[1:3], end[1:3], nInterpolate, index)
|
||||
g = _scale(start[3:5], end[3:5], nInterpolate, index)
|
||||
b = _scale(start[5:7], end[5:7], nInterpolate, index)
|
||||
allColors.append("".join(["#", r, g, b]))
|
||||
|
||||
# Pick only nColors colors from the total list.
|
||||
result = []
|
||||
for counter in range(nColors):
|
||||
fraction = float(counter) / (nColors - 1)
|
||||
index = int(fraction * (len(allColors) - 1))
|
||||
result.append(allColors[index])
|
||||
return result
|
||||
|
||||
|
||||
def color_brewer(color_code, n=6):
|
||||
"""
|
||||
Generate a colorbrewer color scheme of length 'len', type 'scheme.
|
||||
Live examples can be seen at http://colorbrewer2.org/
|
||||
|
||||
"""
|
||||
maximum_n = 253
|
||||
minimum_n = 3
|
||||
|
||||
if not isinstance(n, int):
|
||||
raise TypeError("n has to be an int, not a %s" % type(n))
|
||||
|
||||
# Raise an error if the n requested is greater than the maximum.
|
||||
if n > maximum_n:
|
||||
raise ValueError(
|
||||
"The maximum number of colors in a"
|
||||
" ColorBrewer sequential color series is 253",
|
||||
)
|
||||
if n < minimum_n:
|
||||
raise ValueError(
|
||||
"The minimum number of colors in a"
|
||||
" ColorBrewer sequential color series is 3",
|
||||
)
|
||||
|
||||
if not isinstance(color_code, str):
|
||||
raise ValueError(f"color should be a string, not a {type(color_code)}.")
|
||||
if color_code[-2:] == "_r":
|
||||
base_code = color_code[:-2]
|
||||
core_color_code = base_code + "_" + str(n).zfill(2)
|
||||
color_reverse = True
|
||||
else:
|
||||
base_code = color_code
|
||||
core_color_code = base_code + "_" + str(n).zfill(2)
|
||||
color_reverse = False
|
||||
|
||||
with open(os.path.join(rootpath, "_schemes.json")) as f:
|
||||
schemes = json.loads(f.read())
|
||||
|
||||
with open(os.path.join(rootpath, "scheme_info.json")) as f:
|
||||
scheme_info = json.loads(f.read())
|
||||
|
||||
with open(os.path.join(rootpath, "scheme_base_codes.json")) as f:
|
||||
core_schemes = json.loads(f.read())["codes"]
|
||||
|
||||
if base_code not in core_schemes:
|
||||
raise ValueError(base_code + " is not a valid ColorBrewer code")
|
||||
|
||||
explicit_scheme = True
|
||||
if schemes.get(core_color_code) is None:
|
||||
explicit_scheme = False
|
||||
|
||||
# Only if n is greater than the scheme length do we interpolate values.
|
||||
if not explicit_scheme:
|
||||
# Check to make sure that it is not a qualitative scheme.
|
||||
if scheme_info[base_code] == "Qualitative":
|
||||
matching_quals = []
|
||||
for key in schemes:
|
||||
if base_code + "_" in key:
|
||||
matching_quals.append(int(key.split("_")[1]))
|
||||
|
||||
raise ValueError(
|
||||
"Expanded color support is not available"
|
||||
" for Qualitative schemes; restrict the"
|
||||
" number of colors for the "
|
||||
+ base_code
|
||||
+ " code to between "
|
||||
+ str(min(matching_quals))
|
||||
+ " and "
|
||||
+ str(max(matching_quals)),
|
||||
)
|
||||
else:
|
||||
longest_scheme_name = base_code
|
||||
longest_scheme_n = 0
|
||||
for sn_name in schemes.keys():
|
||||
if "_" not in sn_name:
|
||||
continue
|
||||
if sn_name.split("_")[0] != base_code:
|
||||
continue
|
||||
if int(sn_name.split("_")[1]) > longest_scheme_n:
|
||||
longest_scheme_name = sn_name
|
||||
longest_scheme_n = int(sn_name.split("_")[1])
|
||||
|
||||
if not color_reverse:
|
||||
color_scheme = linear_gradient(schemes.get(longest_scheme_name), n)
|
||||
else:
|
||||
color_scheme = linear_gradient(
|
||||
schemes.get(longest_scheme_name)[::-1],
|
||||
n,
|
||||
)
|
||||
else:
|
||||
if not color_reverse:
|
||||
color_scheme = schemes.get(core_color_code, None)
|
||||
else:
|
||||
color_scheme = schemes.get(core_color_code, None)[::-1]
|
||||
return color_scheme
|
||||
|
||||
|
||||
def image_to_url(image, colormap=None, origin="upper"):
|
||||
"""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. Hint : you can use colormaps from `matplotlib.cm`.
|
||||
"""
|
||||
if hasattr(image, "read"):
|
||||
# We got an image file.
|
||||
if hasattr(image, "name"):
|
||||
# We try to get the image format from the file name.
|
||||
fileformat = image.name.lower().split(".")[-1]
|
||||
else:
|
||||
fileformat = "png"
|
||||
url = "data:image/{};base64,{}".format(
|
||||
fileformat,
|
||||
base64.b64encode(image.read()).decode("utf-8"),
|
||||
)
|
||||
elif (not (isinstance(image, str) or isinstance(image, bytes))) and hasattr(
|
||||
image,
|
||||
"__iter__",
|
||||
):
|
||||
# We got an array-like object.
|
||||
png = write_png(image, origin=origin, colormap=colormap)
|
||||
url = "data:image/png;base64," + base64.b64encode(png).decode("utf-8")
|
||||
else:
|
||||
# We got an URL.
|
||||
url = json.loads(json.dumps(image))
|
||||
|
||||
return url.replace("\n", " ")
|
||||
|
||||
|
||||
def write_png(
|
||||
data: Any,
|
||||
origin: str = "upper",
|
||||
colormap: Union["ColorMap", Callable, None] = None,
|
||||
) -> bytes:
|
||||
"""
|
||||
Transform an array of data into a PNG string.
|
||||
This can be written to disk using binary I/O, or encoded using base64
|
||||
for an inline PNG like this:
|
||||
|
||||
>>> png_str = write_png(array)
|
||||
>>> "data:image/png;base64," + png_str.encode("base64")
|
||||
|
||||
Inspired from
|
||||
http://stackoverflow.com/questions/902761/saving-a-numpy-array-as-an-image
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data: numpy array or equivalent list-like object.
|
||||
Must be NxM (mono), NxMx3 (RGB) or NxMx4 (RGBA)
|
||||
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 : ColorMap subclass or callable, optional
|
||||
Only needed to transform mono images into RGB. You have three options:
|
||||
- use a subclass of `ColorMap` like `LinearColorMap`
|
||||
- use a colormap from `matplotlib.cm`
|
||||
- use a custom function of the form [x -> (r,g,b)] or [x -> (r,g,b,a)].
|
||||
It must output iterables of length 3 or 4 with values between 0 and 1.
|
||||
|
||||
Returns
|
||||
-------
|
||||
PNG formatted byte string
|
||||
"""
|
||||
from branca.colormap import ColorMap
|
||||
|
||||
if np is None:
|
||||
raise ImportError("The NumPy package is required" " for this functionality")
|
||||
|
||||
if isinstance(colormap, ColorMap):
|
||||
colormap_callable = colormap.rgba_floats_tuple
|
||||
elif callable(colormap):
|
||||
colormap_callable = colormap
|
||||
else:
|
||||
colormap_callable = lambda x: (x, x, x, 1) # noqa E731
|
||||
|
||||
array = np.atleast_3d(data)
|
||||
height, width, nblayers = array.shape
|
||||
|
||||
if nblayers not in [1, 3, 4]:
|
||||
raise ValueError("Data must be NxM (mono), " "NxMx3 (RGB), or NxMx4 (RGBA)")
|
||||
assert array.shape == (height, width, nblayers)
|
||||
|
||||
if nblayers == 1:
|
||||
array = np.array(list(map(colormap_callable, array.ravel())))
|
||||
nblayers = array.shape[1]
|
||||
if nblayers not in [3, 4]:
|
||||
raise ValueError(
|
||||
"colormap must provide colors of" "length 3 (RGB) or 4 (RGBA)",
|
||||
)
|
||||
array = array.reshape((height, width, nblayers))
|
||||
assert array.shape == (height, width, nblayers)
|
||||
|
||||
if nblayers == 3:
|
||||
array = np.concatenate((array, np.ones((height, width, 1))), axis=2)
|
||||
nblayers = 4
|
||||
assert array.shape == (height, width, nblayers)
|
||||
assert nblayers == 4
|
||||
|
||||
# Normalize to uint8 if it isn't already.
|
||||
if array.dtype != "uint8":
|
||||
with np.errstate(divide="ignore", invalid="ignore"):
|
||||
array = array * 255.0 / array.max(axis=(0, 1)).reshape((1, 1, 4))
|
||||
array[~np.isfinite(array)] = 0
|
||||
array = array.astype("uint8")
|
||||
|
||||
# Eventually flip the image.
|
||||
if origin == "lower":
|
||||
array = array[::-1, :, :]
|
||||
|
||||
# Transform the array to bytes.
|
||||
raw_data = b"".join([b"\x00" + array[i, :, :].tobytes() for i in range(height)])
|
||||
|
||||
def png_pack(png_tag, data):
|
||||
chunk_head = png_tag + data
|
||||
return (
|
||||
struct.pack("!I", len(data))
|
||||
+ chunk_head
|
||||
+ struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))
|
||||
)
|
||||
|
||||
return b"".join(
|
||||
[
|
||||
b"\x89PNG\r\n\x1a\n",
|
||||
png_pack(b"IHDR", struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
|
||||
png_pack(b"IDAT", zlib.compress(raw_data, 9)),
|
||||
png_pack(b"IEND", b""),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _camelify(out):
|
||||
return (
|
||||
(
|
||||
"".join(
|
||||
[
|
||||
(
|
||||
"_" + x.lower()
|
||||
if i < len(out) - 1
|
||||
and x.isupper()
|
||||
and out[i + 1].islower() # noqa
|
||||
else (
|
||||
x.lower() + "_"
|
||||
if i < len(out) - 1
|
||||
and x.islower()
|
||||
and out[i + 1].isupper() # noqa
|
||||
else x.lower()
|
||||
)
|
||||
)
|
||||
for i, x in enumerate(list(out))
|
||||
],
|
||||
)
|
||||
)
|
||||
.lstrip("_")
|
||||
.replace("__", "_")
|
||||
) # noqa
|
||||
|
||||
|
||||
def _parse_size(value):
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value), "px"
|
||||
elif isinstance(value, str):
|
||||
# match digits or a point, possibly followed by a space,
|
||||
# followed by a unit: either 1 to 5 letters or a percent sign
|
||||
match = re.fullmatch(r"([\d.]+)\s?(\w{1,5}|%)", value.strip())
|
||||
if match:
|
||||
return float(match.group(1)), match.group(2)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Cannot parse {value!r}, it should be a number followed by a unit.",
|
||||
)
|
||||
elif (
|
||||
isinstance(value, tuple)
|
||||
and isinstance(value[0], (int, float))
|
||||
and isinstance(value[1], str)
|
||||
):
|
||||
# value had been already parsed
|
||||
return (float(value[0]), value[1])
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Cannot parse {value!r}, it should be a number or a string containing a number and a unit.",
|
||||
)
|
||||
|
||||
|
||||
def _locations_mirror(x):
|
||||
"""Mirrors the points in a list-of-list-of-...-of-list-of-points.
|
||||
For example:
|
||||
>>> _locations_mirror([[[1, 2], [3, 4]], [5, 6], [7, 8]])
|
||||
[[[2, 1], [4, 3]], [6, 5], [8, 7]]
|
||||
|
||||
"""
|
||||
if hasattr(x, "__iter__"):
|
||||
if hasattr(x[0], "__iter__"):
|
||||
return list(map(_locations_mirror, x))
|
||||
else:
|
||||
return list(x[::-1])
|
||||
else:
|
||||
return x
|
||||
|
||||
|
||||
def _locations_tolist(x):
|
||||
"""Transforms recursively a list of iterables into a list of list."""
|
||||
if hasattr(x, "__iter__"):
|
||||
return list(map(_locations_tolist, x))
|
||||
else:
|
||||
return x
|
||||
|
||||
|
||||
def none_min(x, y):
|
||||
if x is None:
|
||||
return y
|
||||
elif y is None:
|
||||
return x
|
||||
else:
|
||||
return min(x, y)
|
||||
|
||||
|
||||
def none_max(x, y):
|
||||
if x is None:
|
||||
return y
|
||||
elif y is None:
|
||||
return x
|
||||
else:
|
||||
return max(x, y)
|
||||
|
||||
|
||||
def iter_points(x):
|
||||
"""Iterates over a list representing a feature, and returns a list of points,
|
||||
whatever the shape of the array (Point, MultiPolyline, etc).
|
||||
"""
|
||||
if isinstance(x, (list, tuple)):
|
||||
if len(x):
|
||||
if isinstance(x[0], (list, tuple)):
|
||||
out = []
|
||||
for y in x:
|
||||
out += iter_points(y)
|
||||
return out
|
||||
else:
|
||||
return [x]
|
||||
else:
|
||||
return []
|
||||
else:
|
||||
raise ValueError(f"List/tuple type expected. Got {x!r}.")
|
||||
Reference in New Issue
Block a user