import folium
from nrel.routee.compass.utils.type_alias import (
Result as QueryResult,
Results as QueryResults,
)
from typing import Any, Callable, Optional, Sequence, Tuple, Union
from nrel.routee.compass.plot.plot_utils import ColormapCircularIterator, rgba_to_hex
from nrel.routee.compass.utils.geometry import ROUTE_KEY, geometry_from_route
DEFAULT_LINE_KWARGS = {
"color": "blue",
"weight": 10,
"opacity": 0.8,
}
[docs]
def result_dict_to_coords(result_dict: QueryResult) -> Sequence[Tuple[float, float]]:
"""
Converts the CompassApp results to coords to be sent to the folium map.
Args:
result_dict (Dict[str, Any]): A result dictionary from a CompassApp query
Returns:
Sequence[(float, float)]: A sequence of latitude and longitude tuples.
Example:
>>> from nrel.routee.compass import CompassApp
>>> from nrel.routee.compass.plot import result_dict_to_coords
>>> app = CompassApp.from_config_file("config.toml")
>>> query = {origin_x: -105.1710052, origin_y: 39.7402804, destination_x: -104.9009913, destination_y: 39.6757025}
>>> result = app.run(query)
>>> coords = result_dict_to_coords(result)
"""
try:
import shapely
except ImportError:
raise ImportError(
"You need to install the shapely package to use this function"
)
if not isinstance(result_dict, dict):
raise ValueError(f"Expected to get a dictionary but got a {type(result_dict)}")
route = result_dict.get(ROUTE_KEY)
if route is None:
raise KeyError(
f"Could not find '{ROUTE_KEY}' in result. "
"Make sure the geometry output plugin is activated"
)
linestring = geometry_from_route(route)
if isinstance(linestring, shapely.geometry.MultiLineString):
coords = []
for line in linestring.geoms:
coords.extend([(lat, lon) for lon, lat in line.coords])
else:
coords = [(lat, lon) for lon, lat in linestring.coords]
return coords
def _calculate_folium_args(fit_coords: Sequence[Tuple[float, float]]) -> dict[str, Any]:
"""
Calculates where the center of the map and the bounds that the map
should fit.
Args:
fit_coords (Sequence[Tuple[float, float]): The list of coords that needs to fit in the map
Returns:
dict: A dict with two keys. "location" contains the center of the map and "fit_bounds"
represents a rectangle that covers all the coords.
Example:
>>> from nrel.routee.compass import CompassApp
>>> from nrel.routee.compass.plot import _create_empty_folium_map
>>> app = CompassApp.from_config_file("config.toml")
>>> query = {origin_x: -105.1710052, origin_y: 39.7402804, destination_x: -104.9009913, destination_y: 39.6757025}
>>> result = app.run(query)
>>> coords = result_dict_to_coords(result)
>>> folium_args = _calculate_folium_args(coords)
"""
max_x = max(coord[0] for coord in fit_coords)
min_x = min(coord[0] for coord in fit_coords)
max_y = max(coord[1] for coord in fit_coords)
min_y = min(coord[1] for coord in fit_coords)
return {
"location": ((max_x + min_x) / 2, (max_y + min_y) / 2),
"fit_bounds": ([min_x, min_y], [max_x, max_y]),
}
def _create_empty_folium_map(fit_coords: Sequence[Tuple[float, float]]) -> folium.Map:
"""
Creates an empty folium.Map calculating the center and the fit_bounds
using _calculate_folium_args.
Args:
fit_coords (Sequence[Tuple[float, float]): The list of coords that needs to fit in the map
Returns:
folium.Map: An empty folium map centered to fit all the coordinates
Example:
>>> from nrel.routee.compass import CompassApp
>>> from nrel.routee.compass.plot import _create_empty_folium_map
>>> app = CompassApp.from_config_file("config.toml")
>>> query = {origin_x: -105.1710052, origin_y: 39.7402804, destination_x: -104.9009913, destination_y: 39.6757025}
>>> result = app.run(query)
>>> coords = result_dict_to_coords(result)
>>> empty_folium_map = _create_empty_folium_map(coords)
"""
try:
import folium
except ImportError:
raise ImportError("You need to install the folium package to use this function")
folium_args = _calculate_folium_args(fit_coords)
folium_map = folium.Map(location=folium_args["location"], zoom_start=12)
folium_map.fit_bounds(folium_args["fit_bounds"], max_zoom=12)
return folium_map
[docs]
def plot_route_folium(
result_dict: QueryResult,
line_kwargs: Optional[QueryResult] = None,
folium_map: Optional[folium.Map] = None,
) -> folium.Map:
"""
Plots a single route from a compass query on a folium map.
Args:
result_dict: A result dictionary from a CompassApp query
line_kwargs: A dictionary of keyword arguments to pass to the folium Polyline
folium_map: A existing folium map to plot the route on.
Returns:
folium_map: A folium map with the route plotted on it
Example:
>>> from nrel.routee.compass import CompassApp
>>> from nrel.routee.compass.plot import plot_route_folium
>>> app = CompassApp.from_config_file("config.toml")
>>> query = {origin_x: -105.1710052, origin_y: 39.7402804, destination_x: -104.9009913, destination_y: 39.6757025}
>>> result = app.run(query)
>>> m = plot_route_folium(result)
"""
coords = result_dict_to_coords(result_dict)
return plot_coords_folium(coords, line_kwargs, folium_map)
[docs]
def plot_coords_folium(
coords: Sequence[Tuple[float, float]],
line_kwargs: Optional[dict[str, Any]] = None,
folium_map: Optional[folium.Map] = None,
) -> folium.Map:
"""
Plots a sequence of pairs of latitude and longitude on a folium map as a route.
Args:
coords (Sequence[Tuple[float, float]]): A sequence of pairs of latitude and longitude
line_kwargs (Optional[Dict[str, Any]], optional): A dictionary of keyword
arguments to pass to the folium Polyline
folium_map (folium.Map, optional): A existing folium map to plot the route on.
Defaults to None.
Returns:
folium.Map: A folium map with the route plotted on it
Example:
>>> from nrel.routee.compass import CompassApp
>>> from nrel.routee.compass.plot import plot_route_folium
>>> app = CompassApp.from_config_file("config.toml")
>>> query = {origin_x: -105.1710052, origin_y: 39.7402804, destination_x: -104.9009913, destination_y: 39.6757025}
>>> result = app.run(query)
>>> coords = result_dict_to_coords(result[0])
>>> m = plot_coords_folium(coords)
"""
try:
import folium
except ImportError:
raise ImportError("You need to install the folium package to use this function")
if folium_map is None:
folium_map = _create_empty_folium_map(coords)
kwargs = {**DEFAULT_LINE_KWARGS, **(line_kwargs or {})}
folium.PolyLine(
locations=coords,
**kwargs,
).add_to(folium_map)
start_icon = folium.Icon(color="green", icon="circle", prefix="fa")
folium.Marker(
location=coords[0],
icon=start_icon,
tooltip="Origin",
).add_to(folium_map)
end_icon = folium.Icon(color="red", icon="circle", prefix="fa")
folium.Marker(
location=coords[-1],
icon=end_icon,
tooltip="Destination",
).add_to(folium_map)
return folium_map
[docs]
def plot_routes_folium(
results: Union[QueryResult, QueryResults],
value_fn: Callable[[QueryResult], Any] = lambda r: r["request"].get("name"),
color_map: str = "viridis",
folium_map: Optional[folium.Map] = None,
) -> folium.Map:
"""
Plot multiple routes from a CompassApp query on a folium map
Args:
results: A result dictionary or list of result dictionaries from a CompassApp query
value_fn: A function that takes a result dictionary and returns a value to use for coloring the routes.
Defaults to lambda r: r["request"].get("name").
color_map (str, optional): The name of the matplotlib colormap to use
for coloring the routes. Defaults to "viridis".
folium_map (folium.Map, optional): A existing folium map to plot the routes on.
Defaults to None.
Returns:
folium_map: A folium map with the routes plotted on it
Example:
>>> from nrel.routee.compass import CompassApp
>>> from nrel.routee.compass.plot import plot_results_folium
>>> app = CompassApp.from_config_file("config.toml")
>>> query = {origin_x: -105.1710052, origin_y: 39.7402804, destination_x: -104.9009913, destination_y: 39.6757025}
>>> result = app.run(query)
>>> m = plot_results_folium(result)
"""
try:
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
except ImportError:
raise ImportError(
"You need to install the matplotlib package to use this function"
)
try:
import numpy as np
except ImportError:
raise ImportError("requires numpy to be installed. ")
if isinstance(results, dict):
results = [results]
values = [value_fn(result) for result in results]
cmap = plt.get_cmap(color_map)
if all(isinstance(v, float) or isinstance(v, int) for v in values):
norm = mcolors.Normalize(vmin=min(values), vmax=max(values))
colors = [rgba_to_hex(cmap(norm(v))) for v in values]
else:
cmap_iter = ColormapCircularIterator(cmap, len(values))
colors = [next(cmap_iter) for _ in values]
results_coords = [result_dict_to_coords(result_dict) for result_dict in results]
if folium_map is None:
folium_map = _create_empty_folium_map(
fit_coords=list(np.concatenate(results_coords))
)
for coords, value, route_color in zip(results_coords, values, colors):
line_kwargs = {"color": route_color, "tooltip": f"{value}"}
folium_map = plot_coords_folium(coords, line_kwargs, folium_map=folium_map)
return folium_map