""" Utilities ------- Utility module for Folium helper functions. """ import base64 import json import math import os import struct import zlib from jinja2 import Environment, PackageLoader try: import pandas as pd except ImportError: pd = None try: import numpy as np except ImportError: np = None 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 intepolate 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('color should be a string, not a {}.' .format(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 split_six(series=None): """ Given a Pandas Series, get a domain of values from zero to the 90% quantile rounded to the nearest order-of-magnitude integer. For example, 2100 is rounded to 2000, 2790 to 3000. Parameters ---------- series: Pandas series, default None Returns ------- list """ if pd is None: raise ImportError('The Pandas package is required' ' for this functionality') if np is None: raise ImportError('The NumPy package is required' ' for this functionality') def base(x): if x > 0: base = pow(10, math.floor(math.log10(x))) return round(x/base)*base else: return 0 quants = [0, 50, 75, 85, 90] # Some weirdness in series quantiles a la 0.13. arr = series.values return [base(np.percentile(arr, x)) for x in quants] 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, origin='upper', colormap=None): """ 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 : 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`. Returns ------- PNG formatted byte string """ if np is None: raise ImportError('The NumPy package is required' ' for this functionality') if colormap is None: def colormap(x): return (x, x, x, 1) 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, 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': array = array * 255./array.max(axis=(0, 1)).reshape((1, 1, 4)) 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): try: if isinstance(value, int) or isinstance(value, float): value_type = 'px' value = float(value) assert value > 0 else: value_type = '%' value = float(value.strip('%')) assert 0 <= value <= 100 except Exception: msg = 'Cannot parse value {!r} as {!r}'.format raise ValueError(msg(value, value_type)) return value, value_type 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('List/tuple type expected. Got {!r}.'.format(x))