here.inspector.keplergl

Source code for here.inspector.keplergl

Copyright (C) 2022-2023 HERE Global B.V. and its affiliate(s).

All rights reserved.

This software and other materials contain proprietary information

controlled by HERE and are protected by applicable copyright legislation.

Any use and utilization of this software and other materials and

disclosure to any third parties is conditional upon having a separate

agreement with HERE for the access, use, utilization or disclosure of this

software. In the absence of such agreement, the use of the software is not

allowed.

"""
An inspector for the HERE PySDK based on Kepler widget.
"""
import copy
from itertools import count
from typing import Dict, List, Optional, Union

import geojson
import geopandas as gpd
from here.inspector.base import Inspector
from here.inspector.keplergl.utils import (
HEIGHT,
_geojson_with_longkey,
convert_to_2d_2,
process_dataframe,
)
from here.inspector.styles import Color, Theme
from here.inspector.utils import Features, Tiles, is_geojson, options
from keplergl import KeplerGl
from shapely.geometry import Point
from shapely.geometry.base import BaseGeometry
from xyzservices.lib import TileProvider

[docs]
class KeplerInspector(Inspector):
"""An inspector based on Kepler."""

def init(self):
"""Instantiate an inspector object."""
self._features = []
self._theme: Theme = options.default_theme
self._custom_basemap: Optional[Union[dict, TileProvider]] = None
self._center: Optional[Point] = None
self._zoom: Optional[int] = None
self._colors: List[Color] = options.default_colors
self._map: Optional[KeplerGl] = None

[docs]
def add_features(
self,
features: Optional[Features] = None,
name=None,
style: Optional[Union[Color, Dict]] = None,
hover_style: Optional[Union[Color, Dict]] = None,
point_style: Optional[Union[Color, Dict]] = None,
) -> "KeplerInspector":
"""
Add to the inspector a collection of map features (attributed geometries) as new layer.

The features layer can be named and styled.

:param features: the attributed geometries to render. Supported types:

  • gpd.GeoSeries, unattributed geometries
  • gpd.GeoDataFrame, attributes geometries
  • BaseGeometry, individual geometries
  • Iterable of BaseGeometry, it can be any container
    but also a Generator of unattributed geometries
  • Iterable of pair of BaseGeometry and Dict, it can be any container
    but also a Generator of geometries paired with attributes
  • Dict a parsed GeoJSON FeatureCollection or Feature
  • Iterable of parsed GeoJSON Feature, it can be any container
    but also a Generator of parsed GeoJSON features
    :param name: an optional name to assign to the layer
    :param style: an optional style for the features. If not present, a default one is picked.
    It can be a generic inspector style, as defined in here.inspector.styles,
    or directly an ipyleaflet style dictionary.
    :param hover_style:
    an optional hover style. If not present, a default one is picked, based on style.
    It can be a generic inspector style, as defined in here.inspector.styles,
    or directly an ipyleaflet style dictionary.
    :param point_style:
    an optional point style. If not present, a default one is picked, based on style.
    It can be a generic inspector style, as defined in here.inspector.styles,
    or directly an ipyleaflet style dictionary.
    :return: self, for further chaining
    """

fields = {"features": features, "name": str(name) if name else None}
self._features.append(fields)
return self

[docs]
def add_tiles(
self,
tiles: Optional[Tiles] = None,
name=None,
style: Optional[Union[Color, Dict]] = None,
hover_style: Optional[Union[Color, Dict]] = None,
) -> "KeplerInspector":
"""
Add to the inspector a tiling grid as new layer.

The grid can be a complete grid or contain only some tiles.
The grid layer can be named and styled.

:param tiles: the identifier of the tiles to render. Supported types:

  • pd.Series of tile identifiers

  • Iterable of tile identifiers, it can be any container but also a Generator

  • None add the complete grid of all the tiles defined by the tiling scheme
    :param name: an optional name to assign to the layer
    :param style:
    an optional style for the tiling grid. If not present, a default one is picked.
    It can be a generic inspector style, as defined in here.inspector.styles,
    or directly an ipyleaflet style dictionary.
    :param hover_style:
    an optional hover style. If not present, a default one is picked, based on style.
    It can be a generic inspector style, as defined in here.inspector.styles,
    or directly an ipyleaflet style dictionary.
    :return: self, for further chaining
    """
    if tiles is not None:
    features = [_geojson_with_longkey(tile_id) for tile_id in tiles]

    feature_collection = {"features": features, "type": "FeatureCollection"}
    self.add_features(features=feature_collection, name=name)
    return self

[docs]
def set_colors(self, colors: Optional[List[Color]] = None) -> "KeplerInspector":
"""
Set the list of colors the inspector cycles through
to render content when no color or style is specified.

The default color list can be overridden in
here.inspector.options.default_colors.

:param colors: a non-empty list of colors
:return: self, for further chaining
"""

pending implementation set color due to Kepler config limitation

return self

[docs]
def set_theme(self, theme: Optional[Theme] = None) -> "KeplerInspector":
"""
Set the inspector theme.

This determines the background map, if any, and colors.
The default theme can be overridden in here.inspector.options.default_theme.

:param theme: one of the predefined inspector themes
:return: self, for further chaining
"""

pending implementation set theme due to Kepler config limitation

return self

[docs]
def set_basemap(self, basemap: Optional[Union[dict, TileProvider]] = None) -> "Inspector":
"""Set a custom basemap to use as a background.

This overrides the default base map of the theme, if any is defined.
Setting it to None restores the default base map.

:param basemap: Either a dictionary or an object of class:xyzservices.lib.TileProvider.
:return: ``self`, for further chaining
"""

pending implementation set basemap due to Kepler config limitation

return self

[docs]
def set_center(self, center: Point) -> "KeplerInspector":
"""
Configure the location to show in the inspector when opened.

If not set, an optimal starting location
is calculated from the features and tiling grids.

:param center: the center point of the map
:return: self, for further chaining
"""

pending implementation set center due to Kepler config limitation

return self

[docs]
def set_zoom(self, zoom: int) -> "KeplerInspector":
"""
Configure the starting zoom level of the inspector.

If not set, an optimal starting zoom level
is calculated from the geodata and tiling grids.

:param zoom: the zoom level, from 0 to 31
:return: self, for further chaining
"""

pending implementation set zoom due to Kepler config limitation

return self

[docs]
def show(self) -> KeplerGl:
"""
Show the inspector in a Jupyter notebook with the features and tiles loaded so far.

:return: The ipyleaflet.Map object created by the inspector
"""

in this specific case, we have nothing special to do to render

the map apart from returning the map object.

return self.backend()

[docs]
def backend(self) -> KeplerGl:
"""Return the inspector's backend, in this case its Map object.

The backend is populated with the features, tiling grids and settings
specified so far. Users can further configure the Map, but also UI
components, according to the ipyleaflet documentation.

:return: The Kepler.Map object created by the inspector
:raises ValueError: in case the backend can't be configured with the data provided
"""
if not self._map:
self._map = KeplerGl(height=HEIGHT, show_docs=False)

def is_shape(x):
return isinstance(x, BaseGeometry)

def is_geojson_feature(x):
return type(x) == geojson.feature.Feature or (type(x) == dict and is_geojson(x))

unnamed_features = count(1)
names = ["features", "name"]
for entry in self._features:
params = {n: entry[n] for n in names if not entry[n] is None}

params["name"] = params.get("name") or f"Unnamed {next(unnamed_features)}"

typ = type(params["features"])
feats = params["features"]
if isinstance(
params["features"],
(dict, geojson.feature.Feature, geojson.feature.FeatureCollection),
):
params["data"] = feats
new_df = copy.copy(params["data"])
convert_to_2d_2(new_df)
del params["features"]
new_df_feature = copy.deepcopy(new_df)
new_json_string = process_dataframe(new_df_feature)
self._map.add_data(data=new_json_string, name=params["name"])
elif (
isinstance(params["features"], list)
and feats
and all(is_geojson_feature(x) for x in feats)
):
params["data"] = geojson.feature.FeatureCollection(feats)
new_df = copy.copy(params["data"])
convert_to_2d_2(new_df)
del params["features"]
new_df_feature = copy.deepcopy(new_df)
new_json_string = process_dataframe(new_df_feature)
self._map.add_data(data=new_json_string, name=params["name"])
elif isinstance(params["features"], gpd.GeoDataFrame):
params["geo_dataframe"] = feats
new_df = copy.copy(params["geo_dataframe"])
convert_to_2d_2(new_df)
del params["features"]
new_df_feature = copy.deepcopy(new_df)
self._map.add_data(data=new_df_feature, name=params["name"])
elif isinstance(params["features"], gpd.GeoSeries):
params["geo_dataframe"] = gpd.GeoDataFrame(geometry=feats)
new_df = copy.copy(params["geo_dataframe"])
convert_to_2d_2(new_df)
del params["features"]
new_df_feature = copy.deepcopy(new_df)
self._map.add_data(data=new_df_feature, name=params["name"])
elif (
isinstance(params["features"], list)
and feats
and all(is_shape(x) for x in feats)
):
params["geo_dataframe"] = gpd.GeoDataFrame(geometry=feats)
new_df = params["geo_dataframe"].copy()
convert_to_2d_2(new_df)
del params["features"]
new_df_feature = new_df.copy(deep=True)
self._map.add_data(data=new_df_feature, name=params["name"])
elif issubclass(typ, BaseGeometry):
params["geo_dataframe"] = gpd.GeoDataFrame(geometry=[feats])
new_df = copy.copy(params["geo_dataframe"])
convert_to_2d_2(new_df)
del params["features"]
new_df_feature = new_df.copy(deep=True)
self._map.add_data(data=new_df_feature, name=params["name"])
elif isinstance(params["features"], list) and not feats:
params["geo_dataframe"] = gpd.GeoDataFrame(geometry=[])
del params["features"]
dict_geojson_feature = copy.copy(params)
self._map.add_data(
data=dict_geojson_feature["geo_dataframe"],
name=dict_geojson_feature["name"],
)
else:
raise ValueError(
f'Unsupported type {typ} and/or content for layer {params["name"]}'
)
return self._map