Source code for tracktable.render.backends.folium_backend

#
# Copyright (c) 2014-2021 National Technology and Engineering
# Solutions of Sandia, LLC. Under the terms of Contract DE-NA0003525
# with National Technology and Engineering Solutions of Sandia, LLC,
# the U.S. Government retains certain rights in this software.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""
tracktable.render.folium - render trajectories in using the folium backend
"""

import itertools
from datetime import datetime

import folium as fol
import matplotlib
from folium.plugins import HeatMap
from matplotlib.colors import ListedColormap, hsv_to_rgb, rgb2hex
from tracktable.core.geomath import compute_bounding_box
from tracktable.render.map_decoration import coloring
from tracktable.render.map_processing import common_processing


# later can add multiple layers and switch between with:
# folium.TileLayer('cartodbdark_matter', attr='.').add_to(map)
# folium.TileLayer('CartoDBPositron', attr='.').add_to(map)
# folium.LayerControl().add_to(map)
# TODO add region specifications
# TODO could have opacity, and point size  genertor?
# TODO color_scale, can we remove min,max?
# TODO what if color map but no generator or vice versa
# TODO add point_color_map?
# TODO could customize choice of mapping hues to trajs
[docs]def render_trajectories(trajectories, #common arguments map_canvas = None, obj_ids = [], map_bbox = None, show_lines = True, gradient_hue = None, color_map = '', line_color = '', linewidth = 2.4, show_points = False, point_size = 0.6, point_color = '', show_dot = True, dot_size = 0.7, dot_color = 'white', trajectory_scalar_generator = common_processing.path_length_fraction_generator, trajectory_linewidth_generator = None, color_scale = matplotlib.colors.Normalize(vmin=0, vmax=1), show = False, save = False, filename = '', # folium specific args tiles = 'cartodbdark_matter', attr = ".", crs = "EPSG3857", point_popup_properties = [], show_distance_geometry = False, distance_geometry_depth = 4, zoom_frac = [0,1], #undocumented feature, for now show_scale = True, max_zoom = 22, fast = False, **kwargs): """Render a list of trajectories using the folium backend For documentation on the parameters, please see render_trajectories """ if not fast: trajectories, line_color, color_map, gradient_hue \ = common_processing.common_processing(trajectories, obj_ids, line_color, color_map, gradient_hue) if not trajectories: return if map_canvas == None: map_canvas = fol.Map(tiles=tiles, attr=attr, crs=crs, control_scale = show_scale, max_zoom=max_zoom) for i, trajectory in enumerate(trajectories): coordinates = [(point[1], point[0]) for point in trajectory] if not fast: # set up generators if trajectory_scalar_generator: scalars = trajectory_scalar_generator(trajectory) if trajectory_linewidth_generator: # NOTE: lines invisible below about .37 widths = trajectory_linewidth_generator(trajectory) current_color_map, current_point_cmap, mapper, point_mapper = \ coloring.setup_colors(line_color, color_map, gradient_hue, point_color, color_scale, trajectory[0].object_id, i, trajectory_linewidth_generator) else: rgb = hsv_to_rgb([common_processing.hash_short_md5(trajectory[0].object_id), 1.0, 1.0]) current_color_map = ListedColormap([rgb2hex(rgb)]) if show_lines: popup_str = str(trajectory[0].object_id)+'<br>'+ \ trajectory[0].timestamp.strftime('%Y-%m-%d %H:%M:%S')+ \ '<br> to <br>'+ \ trajectory[-1].timestamp.strftime('%Y-%m-%d %H:%M:%S') tooltip_str = str(trajectory[0].object_id) if fast or (type(current_color_map) is ListedColormap \ and len(current_color_map.colors) == 1 \ and trajectory_linewidth_generator == None): # Polyline ok fol.PolyLine(coordinates, color=current_color_map.colors[0], weight=linewidth, opacity=1, tooltip=tooltip_str, popup=popup_str).add_to(map_canvas) else: # mapped color (not solid) last_pos = coordinates[0] for i, pos in enumerate(coordinates[1:]): weight = linewidth if trajectory_linewidth_generator: weight = widths[i] segment_color = rgb2hex(mapper.to_rgba(scalars[i])) fol.PolyLine([last_pos,pos], color=segment_color, weight=weight, opacity=1, tooltip=tooltip_str, popup=popup_str).add_to(map_canvas) last_pos = pos if show_points: for i, c in enumerate(coordinates[:-1]): # all but last (dot) point_radius = point_size if type(current_point_cmap) is ListedColormap \ and len(current_point_cmap.colors) == 1: # one color current_point_color = current_point_cmap.colors[0] else: current_point_color = \ rgb2hex(point_mapper.to_rgba(scalars[i])) render_point(trajectory[i], point_popup_properties, c, point_radius, current_point_color, map_canvas) if show_dot: render_point(trajectory[-1], point_popup_properties, coordinates[-1], dot_size, dot_color, map_canvas) if show_distance_geometry: common_processing.render_distance_geometry('folium', distance_geometry_depth, trajectory, map_canvas) if map_bbox: map_canvas.fit_bounds([(map_bbox[1], map_bbox[0]), (map_bbox[3], map_bbox[2])]) else: if zoom_frac != [0,1]: sub_trajs = common_processing.sub_trajs_from_frac(trajectories, zoom_frac) map_canvas.fit_bounds(bounding_box_for_folium(sub_trajs)) else: map_canvas.fit_bounds(bounding_box_for_folium(trajectories)) if save: # saves as .html document if not filename: datetime_str = datetime.now().strftime("%Y-%m-%dT%H%M%S-%f") filename = "trajs-"+datetime_str+'.html' map_canvas.save(filename) if show: display(map_canvas) return map_canvas
# ----------------------------------------------------------------------
[docs]def render_heatmap(points, trajectories=None, weights=None, color_map='viridis', tiles='cartodbdark_matter', attr='.', crs="EPSG3857", show = False, save = False, filename = ''): """Creates an interactive heatmap visualization Args: points (list): list of points Keyword Arguments: trajectories: (Trajectoies) list of trajectories corresponding to the points, render trajectories if provided (Default: None) weights: (list) list of weights associated with each point (Default: None) color_map: (str) name of matplotlib colormap to use for the heatmap (Default: 'viridis') tiles (str): name of map tiling to use (Default: 'cartodbdark_matter') attr (str): folium specific parameter (Default: '.') crs (str): folium specific parameter (Default: "EPSG3857") show (bool): whether or not to show the result (if possible) (default True) if saving to a file, might not want to view. save (bool): whether or not to save the result to a file. For folium the results can be saved to an html file. For cartopy the results can be saved as an image. If no filename is given, a default filename including the timestamp is used. (default False) filename (str): Path and filename to save the results to, if save is set to True. If no filename is given, a default filename including the timestamp is used. Returns: an interactive heatmap """ # lat, long, (optional weight) of points to render if weights is None: display_points = [[point[1], point[0]] for point in points] else: display_points = [[point[1], point[0], weight] for point, weight in zip(points, weights)] # create the heat map heat_map = fol.Map(tiles=tiles, zoom_start=4) gradient = coloring.matplotlib_cmap_to_dict(color_map) if trajectories is not None: heat_map = render_trajectories(trajectories, map=heat_map, line_color='grey', linewidth=0.5, tiles=tiles, attr=attr, crs=crs) HeatMap(display_points, gradient=gradient).add_to(heat_map) if save: # saves as .html document if not filename: datetime_str = datetime.now().strftime("%Y-%m-%dT%H%M%S-%f") filename = "heatmap-"+datetime_str+'.html' heat_map.save(filename) if show: display(heat_map) return heat_map
# ----------------------------------------------------------------------
[docs]def render_point(current_point, point_popup_properties, coord, point_radius, point_color, map_canvas): """Renders a point to the folium map Args: current_point (point): Current point of the trajectory point_popup_properties (list): Point properties coord (tuple): Coordinates to render point point_radius (int): Size of the point to render point_color (str): Color of the point to render map (Basemap): Folium map Returns: No return value """ tooltip_str_point = common_processing.point_tooltip(current_point) popup_point = fol.Popup(common_processing.point_popup(current_point, point_popup_properties), max_width=450) fol.CircleMarker(coord, radius=point_radius, fill=True, fill_opacity=1.0, fill_color=point_color, color=point_color, tooltip=tooltip_str_point, popup=popup_point).add_to(map_canvas)
# ----------------------------------------------------------------------
[docs]def bounding_box_for_folium(trajectories): """Translates a computed bounding box to the format needed by folium """ raw_bbox = compute_bounding_box(itertools.chain(*trajectories)) # folium needs two corner points [sw, ne], in (lat,lon) order box_for_folium = [(raw_bbox.min_corner[1], raw_bbox.min_corner[0]), (raw_bbox.max_corner[1], raw_bbox.max_corner[0])] return box_for_folium