python-package/lets_plot/plot/geom_livemap_.py (171 lines of code) (raw):

# # Copyright (c) 2019. JetBrains s.r.o. # Use of this source code is governed by the MIT license that can be found in the LICENSE file. # from enum import Enum from typing import Union, Optional, List from lets_plot._global_settings import MAPTILES_KIND, MAPTILES_URL, MAPTILES_THEME, MAPTILES_ATTRIBUTION, \ GEOCODING_PROVIDER_URL, GEOCODING_ROUTE, \ TILES_RASTER_ZXY, TILES_VECTOR_LETS_PLOT, MAPTILES_MIN_ZOOM, MAPTILES_MAX_ZOOM, TILES_SOLID, \ MAPTILES_SOLID_FILL_COLOR, TILES_CHESSBOARD from lets_plot._global_settings import has_global_value, get_global_val from .geom import _geom try: import pandas except ImportError: pandas = None # from ..geo_data.livemap_helper import _prepare_location # from ..geo_data.livemap_helper import _prepare_parent # from ..geo_data.livemap_helper import _prepare_tiles __all__ = ['geom_livemap'] def geom_livemap(*, location=None, zoom=None, projection=None, tiles=None, show_coord_pick_tools=None, data_size_zoomin=None, const_size_zoomin=None, **other_args): """ Display an interactive map. Parameters ---------- location : list Initial position of the map. If not set, display the United States. There are [lon1, lat1, lon2, lat2,..., lonN, latN]: lon1, lon2,..., lonN are longitudes in degrees (positive in the Eastern hemisphere); lat1, lat2,..., latN are latitudes in degrees (positive in the Northern hemisphere). zoom : int Zoom of the map in the range 1 - 15. projection : str, default='epsg3857' The map projection. There are: 'epsg3857' for Mercator projection; 'epsg4326' for Equirectangular projection. ``projection`` only works with vector map tiles (i.e. Lets-Plot map tiles). tiles : str Tile provider: - pass a predefined constant from the ``tilesets`` module (Lets-Plot's vector tiles, e.g. `LETS_PLOT_COLOR <https://lets-plot.org/python/pages/api/lets_plot.tilesets.LETS_PLOT_COLOR.html>`__, or external raster tiles, e.g. `OPEN_TOPO_MAP <https://lets-plot.org/python/pages/api/lets_plot.tilesets.OPEN_TOPO_MAP.html>`__); - pass a URL for a standard raster ZXY tile provider with {z}, {x} and {y} wildcards (e.g. 'http://my.tile.com/{z}/{x}/{y}.png') if the required tileset not present in the module; - pass the result of a call to a `maptiles_zxy() <https://lets-plot.org/python/pages/api/lets_plot.maptiles_zxy.html>`__ function if further customisation is required (e.g. attribution or zoom). More information about tiles can be found here: https://lets-plot.org/python/pages/basemap_tiles.html show_coord_pick_tools : bool, default=False Show buttons "copy location" and "draw geometry". data_size_zoomin : int, default=0 Control how zooming-in of the map widget increases size of geometry objects (circles, lines etc.) on map when the size is set by means of mapping between the data and the ``size`` aesthetic. 0 - size never increases; -1 - size will be increasing without limits; n - a number of zooming-in steps (counting from the initial state of the map widget) when size of objects will be increasing. Farther zooming will no longer affect the size. const_size_zoomin : int, default=-1 Control how zooming-in of the map widget increases size of geometry objects (circles, lines etc.) on map when the size is not linked to a data (i.e. constant size). 0 - size never increases; -1 - size will be increasing without limits; n - a number of zooming-in steps (counting from the initial state of the map widget) when size of objects will be increasing. Farther zooming will no longer affect the size. other_args Other arguments passed on to the layer. Returns ------- ``LayerSpec`` Geom object specification. Notes ----- ``geom_livemap()`` draws a map, which can be dragged and zoomed. ---- By default the livemap area has a non-zero inset. You can get rid of this with the theme: ``theme(plot_inset=0)``. --- When drawing a path with two points, the shortest route is taken. To create a longer arc, add intermediate points. Examples -------- .. jupyter-execute:: :linenos: :emphasize-lines: 3 from lets_plot import * LetsPlot.setup_html() ggplot() + geom_livemap() | .. jupyter-execute:: :linenos: :emphasize-lines: 10 from lets_plot import * from lets_plot import tilesets LetsPlot.setup_html() data = { 'city': ['New York City', 'Prague'], 'lon': [-73.7997, 14.418540], 'lat': [40.6408, 50.073658], } ggplot(data, aes(x='lon', y='lat')) + \\ geom_livemap(projection='epsg4326', tiles=tilesets.LETS_PLOT_DARK) + \\ geom_path(color='white', geodesic=True) + \\ geom_point(color='white', tooltips=layer_tooltips().line('@city')) + \\ ggtitle("The shortest path between New York and Prague") | .. jupyter-execute:: :linenos: :emphasize-lines: 9 from lets_plot import * LetsPlot.setup_html() data = { 'x': [-170, 170, -170, 0, 170], 'y': [10, 10, -10, -10, -10], 'route': ['A', 'A', 'B', 'B', 'B'], } ggplot(data) + \\ geom_livemap(zoom=1, location=[180, 0]) + \\ geom_path(aes('x', 'y', color='route'), size=1) + \\ scale_color_manual(values=['red', 'green'], labels={'A': "'x': [-170, 170]", 'B': "'x': [-170, 0, 170]"}) + \\ ggtitle("A path that crosses the antimeridian") """ if 'symbol' in other_args: print("WARN: The parameter 'symbol' is no longer supported. " "Use separate geom_point() or geom_pie() geometry layers to display markers on the map.") other_args.pop('symbol') deprecated_params = set.intersection( {'data', 'mapping', 'map', 'map_join', 'ontop', 'stat', 'position', 'show_legend', 'sampling', 'tooltips'}, other_args ) if len(deprecated_params) > 0: print(f"WARN: These parameters are not supported and will be ignored: {str(deprecated_params):s}. " "Specify a separate geometry layer to display data on the livemap.") for param in deprecated_params: other_args.pop(param) if location is not None: location = _prepare_location(location) tiles = _prepare_tiles(tiles) geocoding = _prepare_geocoding() return _geom('livemap', mapping=None, data=None, stat=None, position=None, show_legend=None, sampling=None, tooltips=None, map=None, map_join=None, location=location, zoom=zoom, projection=projection, tiles=tiles, geocoding=geocoding, show_coord_pick_tools=show_coord_pick_tools, data_size_zoomin=data_size_zoomin, const_size_zoomin=const_size_zoomin, **other_args ) LOCATION_COORDINATE_COLUMNS = {'lon', 'lat'} LOCATION_RECTANGLE_COLUMNS = {'lonmin', 'latmin', 'lonmax', 'latmax'} LOCATION_LIST_ERROR_MESSAGE = "Expected: location = [double lon1, double lat1, ... , double lonN, double latN]" LOCATION_DATAFRAME_ERROR_MESSAGE = "Expected: location = DataFrame with [{}] or [{}] columns" \ .format(', '.join(LOCATION_COORDINATE_COLUMNS), ', '.join(LOCATION_RECTANGLE_COLUMNS)) OPTIONS_MAPTILES_KIND = 'kind' OPTIONS_MAPTILES_URL = 'url' OPTIONS_MAPTILES_THEME = 'theme' OPTIONS_MAPTILES_ATTRIBUTION = 'attribution' OPTIONS_MAPTILES_MIN_ZOOM = 'min_zoom' OPTIONS_MAPTILES_MAX_ZOOM = 'max_zoom' OPTIONS_MAPTILES_FILL_COLOR = 'fill_color' OPTIONS_GEOCODING_PROVIDER_URL = 'url' class RegionKind(Enum): region_ids = 'region_ids' region_name = 'region_name' coordinates = 'coordinates' data_frame = 'data_frame' def _prepare_geocoding(): if has_global_value(GEOCODING_PROVIDER_URL): return { OPTIONS_GEOCODING_PROVIDER_URL: get_global_val(GEOCODING_PROVIDER_URL) + GEOCODING_ROUTE } return {} def _prepare_tiles(tiles: Optional[Union[str, dict]]) -> Optional[dict]: if isinstance(tiles, str): return { OPTIONS_MAPTILES_KIND: TILES_RASTER_ZXY, OPTIONS_MAPTILES_URL: tiles } if isinstance(tiles, dict): if tiles.get(MAPTILES_KIND) == TILES_RASTER_ZXY: _warn_deprecated_tiles(tiles) return { OPTIONS_MAPTILES_KIND: TILES_RASTER_ZXY, OPTIONS_MAPTILES_URL: tiles[MAPTILES_URL], OPTIONS_MAPTILES_ATTRIBUTION: tiles[MAPTILES_ATTRIBUTION], OPTIONS_MAPTILES_MIN_ZOOM: tiles[MAPTILES_MIN_ZOOM], OPTIONS_MAPTILES_MAX_ZOOM: tiles[MAPTILES_MAX_ZOOM], } elif tiles.get(MAPTILES_KIND) == TILES_VECTOR_LETS_PLOT: return { OPTIONS_MAPTILES_KIND: TILES_VECTOR_LETS_PLOT, OPTIONS_MAPTILES_URL: tiles[MAPTILES_URL], OPTIONS_MAPTILES_THEME: tiles[MAPTILES_THEME], OPTIONS_MAPTILES_ATTRIBUTION: tiles[MAPTILES_ATTRIBUTION], } elif tiles.get(MAPTILES_KIND) == TILES_SOLID: return { OPTIONS_MAPTILES_KIND: TILES_SOLID, OPTIONS_MAPTILES_FILL_COLOR: tiles[MAPTILES_SOLID_FILL_COLOR] } elif tiles.get(MAPTILES_KIND) == TILES_CHESSBOARD: return { OPTIONS_MAPTILES_KIND: TILES_CHESSBOARD } else: raise ValueError("Unsupported 'tiles' kind: " + tiles.get(MAPTILES_KIND)) if tiles is not None: raise ValueError("Unsupported 'tiles' parameter type: " + type(tiles)) # tiles are not set for this livemap - try to get global tiles config if has_global_value(MAPTILES_KIND): if not has_global_value(MAPTILES_URL): raise ValueError('URL for tiles service is not set') if get_global_val(MAPTILES_KIND) == TILES_RASTER_ZXY: _warn_deprecated_tiles(None) return { OPTIONS_MAPTILES_KIND: TILES_RASTER_ZXY, OPTIONS_MAPTILES_URL: get_global_val(MAPTILES_URL), OPTIONS_MAPTILES_ATTRIBUTION: get_global_val(MAPTILES_ATTRIBUTION) if has_global_value( MAPTILES_ATTRIBUTION) else None, OPTIONS_MAPTILES_MIN_ZOOM: get_global_val(MAPTILES_MIN_ZOOM) if has_global_value( MAPTILES_MIN_ZOOM) else None, OPTIONS_MAPTILES_MAX_ZOOM: get_global_val(MAPTILES_MAX_ZOOM) if has_global_value( MAPTILES_MAX_ZOOM) else None, } if get_global_val(MAPTILES_KIND) == TILES_VECTOR_LETS_PLOT: return { OPTIONS_MAPTILES_KIND: TILES_VECTOR_LETS_PLOT, OPTIONS_MAPTILES_URL: get_global_val(MAPTILES_URL), OPTIONS_MAPTILES_THEME: get_global_val(MAPTILES_THEME) if has_global_value(MAPTILES_THEME) else None, OPTIONS_MAPTILES_ATTRIBUTION: get_global_val(MAPTILES_ATTRIBUTION) if has_global_value( MAPTILES_ATTRIBUTION) else None, } if get_global_val(MAPTILES_KIND) == TILES_SOLID: return { OPTIONS_MAPTILES_KIND: TILES_SOLID, OPTIONS_MAPTILES_FILL_COLOR: get_global_val(MAPTILES_SOLID_FILL_COLOR), } raise ValueError('Tile provider is not set.') def _warn_deprecated_tiles(tiles: Union[dict, None]): if tiles is None: maptiles_url = get_global_val(MAPTILES_URL) else: maptiles_url = tiles[MAPTILES_URL] # Check if the current tiles should be deprecated and print a deprecation message. Otherwise, return. return def _prepare_location(location: Union[str, List[float]]) -> Optional[dict]: if location is None: return None value = location # if isinstance(location, Geocoder): # kind = RegionKind.region_ids # value = location.unique_ids() if isinstance(location, str): kind = RegionKind.region_name elif isinstance(location, list): if len(location) == 0 or len(location) % 2 != 0: raise ValueError(LOCATION_LIST_ERROR_MESSAGE) kind = RegionKind.coordinates elif pandas and isinstance(location, pandas.DataFrame): if not LOCATION_COORDINATE_COLUMNS.issubset(location.columns) and not LOCATION_RECTANGLE_COLUMNS.issubset( location.columns): raise ValueError(LOCATION_DATAFRAME_ERROR_MESSAGE) kind = RegionKind.data_frame else: raise ValueError('Wrong location type: ' + location.__str__()) return {'type': kind.value, 'data': value}