Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Google map tiles #1963

Merged
merged 24 commits into from
May 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1178d02
Add Google map tiles
giswqs Apr 6, 2024
bd2eb91
Merge branch 'master' into gmaps
giswqs Apr 6, 2024
6a5439c
Merge branch 'master' into gmaps
giswqs Apr 8, 2024
7b49809
Remove googlemaps dependency
giswqs Apr 10, 2024
b5a2f13
Fix name and attribution
giswqs Apr 10, 2024
9c5e909
Add name variations
giswqs Apr 10, 2024
4e5f2a5
Merge branch 'master' into gmaps
giswqs Apr 10, 2024
0ad6584
Add Google map tiles to basemap list
giswqs Apr 11, 2024
e84d7f3
Merge branch 'master' into gmaps
giswqs Apr 16, 2024
ee32852
Add support for returning all map types as a dict
giswqs Apr 17, 2024
7c598e4
Use Google Roadmap by default if API Key is available
giswqs Apr 17, 2024
ad98061
Make gmap tiles available in core module
giswqs Apr 17, 2024
9ecd29d
Fix docs build error
giswqs Apr 17, 2024
a8dfafa
Remove Google Traffic and Streetview
giswqs Apr 29, 2024
88d6c86
Merge branch 'master' into gmaps
giswqs Apr 29, 2024
27ed73f
Set line wrap for docstrings
giswqs Apr 29, 2024
afa285d
Improve google_maps_api_key function
giswqs Apr 29, 2024
1238292
Merge branch 'master' into gmaps
giswqs May 7, 2024
047a0ce
Add GoogleMapsTileProvider class
giswqs May 7, 2024
9c0397e
Add backward compatibility for map alias
giswqs May 7, 2024
88e53f1
Fix add_basemap Google tiles issue
giswqs May 7, 2024
a555ac1
Reduce dependencies on hardcoded map name prefixes
naschmitz May 24, 2024
fe2689d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2024
02a3639
Allow basemap as a string
giswqs May 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -125,3 +125,4 @@ ENV/
# IDE settings
.vscode/
docs/changelog_update.py
oryx-build-commands.txt
150 changes: 148 additions & 2 deletions geemap/basemaps.py
Expand Up @@ -19,11 +19,14 @@

import collections
import os
import requests
from typing import Any, Optional

import folium
import ipyleaflet
import requests
import xyzservices
from .common import check_package, planet_tiles

from .common import check_package, get_google_maps_api_key, planet_tiles

XYZ_TILES = {
"OpenStreetMap": {
Expand Down Expand Up @@ -237,6 +240,149 @@
custom_tiles = {"xyz": XYZ_TILES, "wms": WMS_TILES}


class GoogleMapsTileProvider(xyzservices.TileProvider):
"""Google Maps TileProvider."""

MAP_TYPE_CONFIG = {
"roadmap": {"mapType": "roadmap"},
"satellite": {"mapType": "satellite"},
"terrain": {
"mapType": "terrain",
"layerTypes": ["layerRoadmap"],
},
"hybrid": {
"mapType": "satellite",
"layerTypes": ["layerRoadmap"],
},
}

def __init__(
self,
map_type: str = "roadmap",
language: str = "en-Us",
region: str = "US",
api_key: Optional[str] = None,
**kwargs: Any,
):
"""
Generates Google Map tiles using the provided parameters. To get an API key
and enable Map Tiles API, visit
https://developers.google.com/maps/get-started#create-project.
You can set the API key using the environment variable
`GOOGLE_MAPS_API_KEY` or by passing it as an argument.

Args:
map_type (str, optional): The type of map to generate. Options are
'roadmap', 'satellite', 'terrain', 'hybrid', 'traffic', 'streetview'.
Defaults to 'roadmap'.
language (str, optional): An IETF language tag that specifies the
language used to display information on the tiles, such as 'zh-Cn'.
Defaults to 'en-Us'.
region (str, optional): A Common Locale Data Repository region
identifier (two uppercase letters) that represents the physical
location of the user. Defaults to 'US'.
api_key (str, optional): The API key to use for the Google Maps API.
If not provided, it will try to get it from the environment or
Colab user data with the key 'MAPS_API_KEY'. Defaults to None.
**kwargs: Additional parameters to pass to the map generation. For more
info, visit https://bit.ly/3UhbZKU

Raises:
ValueError: If the API key is not provided and cannot be found in the
environment or Colab user data.
ValueError: If the map_type is not one of the allowed types.

Example:
>>> from geemap.basemaps import GoogleMapsTileProvider
>>> m = geemap.Map()
>>> basemap = GoogleMapsTileProvider(map_type='roadmap',
language="en-Us", region="US", scale="scaleFactor2x", highDpi=True)
>>> m.add_basemap(basemap)

Returns:
TileProvider object: A TileProvider object with the Google Maps tile.
"""

key = api_key or get_google_maps_api_key()
if key is None:
raise ValueError(
"API key is required to access Google Maps API. To get an API "
"key and enable Map Tiles API, visit "
"https://developers.google.com/maps/get-started#create-project"
)

if map_type not in self.MAP_TYPE_CONFIG:
raise ValueError(f"map_type must be one of: {self.MAP_TYPE_CONFIG.keys()}")

request_url = f"https://tile.googleapis.com/v1/createSession?key={key}"
response = requests.post(
url=request_url,
headers={"Content-Type": "application/json"},
json={
**self.MAP_TYPE_CONFIG[map_type],
"language": language,
"region": region,
**kwargs,
},
timeout=3,
)

if response.status_code == requests.codes.ok:
json = response.json()
map_name = map_type.capitalize()
super().__init__(
{
"url": f"https://tile.googleapis.com/v1/2dtiles/{{z}}/{{x}}/{{y}}?session={json['session']}&key={{accessToken}}",
"attribution": f"© Google {map_name}",
"accessToken": key,
"name": f"Google.{map_name}",
"ext": json["imageFormat"],
"tileSize": json["tileWidth"],
}
)
else:
raise RuntimeError(
f"Error creating a Maps API session:\n{response.json()}."
)


def get_google_map_tile_providers(
language: str = "en-Us",
region: str = "US",
api_key: Optional[str] = None,
**kwargs: Any,
):
"""
Generates a dictionary of Google Map tile providers for different map types.

Args:
language (str, optional): An IETF language tag that specifies the
language used to display information on the tiles, such as 'zh-Cn'.
Defaults to 'en-Us'.
region (str, optional): A Common Locale Data Repository region
identifier (two uppercase letters) that represents the physical
location of the user. Defaults to 'US'.
api_key (str, optional): The API key to use for the Google Maps API.
If not provided, it will try to get it from the environment or
Colab user data with the key 'MAPS_API_KEY'. Defaults to None.
**kwargs: Additional parameters to pass to the map generation. For more
info, visit https://bit.ly/3UhbZKU

Returns:
dict: A dictionary where the keys are the map types
('roadmap', 'satellite', 'terrain', 'hybrid')
and the values are the corresponding GoogleMapsTileProvider objects.
"""
gmap_providers = {}

for m_type in GoogleMapsTileProvider.MAP_TYPE_CONFIG:
gmap_providers[m_type] = GoogleMapsTileProvider(
map_type=m_type, language=language, region=region, api_key=api_key, **kwargs
)

return gmap_providers


def get_xyz_dict(free_only=True, france=False):
"""Returns a dictionary of xyz services.

Expand Down
23 changes: 22 additions & 1 deletion geemap/common.py
giswqs marked this conversation as resolved.
Show resolved Hide resolved
Expand Up @@ -24,7 +24,7 @@
import ee
import ipywidgets as widgets
from ipytree import Node, Tree
from typing import Union, List, Dict, Optional, Tuple
from typing import Union, List, Dict, Optional, Tuple, Any

try:
from IPython.display import display, IFrame, Javascript
Expand Down Expand Up @@ -16167,3 +16167,24 @@ def is_on_aws():
if item.endswith(".aws") or "ec2-user" in item:
on_aws = True
return on_aws


def get_google_maps_api_key(key: str = "GOOGLE_MAPS_API_KEY") -> Optional[str]:
"""
Retrieves the Google Maps API key from the environment or Colab user data.

Args:
key (str, optional): The name of the environment variable or Colab user
data key where the API key is stored. Defaults to
'GOOGLE_MAPS_API_KEY'.

Returns:
str: The API key, or None if it could not be found.
"""
if in_colab_shell():
giswqs marked this conversation as resolved.
Show resolved Hide resolved
from google.colab import userdata

if api_key := userdata.get(key):
return api_key

return os.environ.get(key, None)
51 changes: 36 additions & 15 deletions geemap/core.py
Expand Up @@ -402,12 +402,12 @@ class Map(ipyleaflet.Map, MapInterface):
"scroll_wheel_zoom": True,
}

_BASEMAP_ALIASES: Dict[str, str] = {
"DEFAULT": "OpenStreetMap.Mapnik",
"ROADMAP": "Esri.WorldStreetMap",
"SATELLITE": "Esri.WorldImagery",
"TERRAIN": "Esri.WorldTopoMap",
"HYBRID": "Esri.WorldImagery",
_BASEMAP_ALIASES: Dict[str, List[str]] = {
"DEFAULT": ["Google.Roadmap", "OpenStreetMap.Mapnik"],
"ROADMAP": ["Google.Roadmap", "Esri.WorldStreetMap"],
"SATELLITE": ["Google.Satellite", "Esri.WorldImagery"],
"TERRAIN": ["Google.Terrain", "Esri.WorldTopoMap"],
"HYBRID": ["Google.Hybrid", "Esri.WorldImagery"],
}

_USER_AGENT_PREFIX = "geemap-core"
Expand Down Expand Up @@ -458,6 +458,13 @@ def _basemap_selector(self) -> Optional[map_widgets.Basemap]:
def __init__(self, **kwargs):
self._available_basemaps = self._get_available_basemaps()

# Use the first basemap in the list of available basemaps.
if "basemap" not in kwargs:
kwargs["basemap"] = next(iter(self._available_basemaps.values()))
elif "basemap" in kwargs and isinstance(kwargs["basemap"], str):
if kwargs["basemap"] in self._available_basemaps:
kwargs["basemap"] = self._available_basemaps.get(kwargs["basemap"])

if "width" in kwargs:
self.width: str = kwargs.pop("width", "100%")
self.height: str = kwargs.pop("height", "600px")
Expand Down Expand Up @@ -846,21 +853,35 @@ def _replace_basemap(self, basemap_name: str) -> None:

def _get_available_basemaps(self) -> Dict[str, Any]:
"""Convert xyz tile services to a dictionary of basemaps."""
tile_providers = list(basemaps.get_xyz_dict().values())
if common.get_google_maps_api_key():
tile_providers = tile_providers + list(
basemaps.get_google_map_tile_providers().values()
)

ret_dict = {}
for tile_info in basemaps.get_xyz_dict().values():
for tile_info in tile_providers:
tile_info["url"] = tile_info.build_url()
ret_dict[tile_info["name"]] = tile_info
extra_dict = {k: ret_dict[v] for k, v in self._BASEMAP_ALIASES.items()}
return {**extra_dict, **ret_dict}

# Each alias needs to point to a single map. For each alias, pick the
# first aliased map in `self._BASEMAP_ALIASES`.
aliased_maps = {}
for alias, maps in self._BASEMAP_ALIASES.items():
for map_name in maps:
if provider := ret_dict.get(map_name):
aliased_maps[alias] = provider
break
return {**aliased_maps, **ret_dict}

def _get_preferred_basemap_name(self, basemap_name: str) -> str:
"""Returns the aliased basemap name."""
try:
return list(self._BASEMAP_ALIASES.keys())[
list(self._BASEMAP_ALIASES.values()).index(basemap_name)
]
except ValueError:
return basemap_name
reverse_aliases = {}
for alias, maps in self._BASEMAP_ALIASES.items():
for map_name in maps:
if map_name not in reverse_aliases:
reverse_aliases[map_name] = alias
return reverse_aliases.get(basemap_name, basemap_name)

def _on_layers_change(self, change) -> None:
del change # Unused.
Expand Down
42 changes: 10 additions & 32 deletions geemap/geemap.py
Expand Up @@ -113,15 +113,6 @@ def __init__(self, **kwargs):
if "max_zoom" not in kwargs:
kwargs["max_zoom"] = 24

# Use any basemap available through the basemap module, such as 'ROADMAP', 'OpenTopoMap'
if "basemap" in kwargs:
kwargs["basemap"] = check_basemap(kwargs["basemap"])
if kwargs["basemap"] in basemaps.keys():
kwargs["basemap"] = get_basemap(kwargs["basemap"])
kwargs["add_google_map"] = False
else:
kwargs.pop("basemap")

self._xyz_dict = get_xyz_dict()

self.baseclass = "ipyleaflet"
Expand Down Expand Up @@ -411,34 +402,26 @@ def get_scale(self):

getScale = get_scale

def add_basemap(self, basemap="ROADMAP", show=True, **kwargs):
def add_basemap(
self, basemap: Optional[str] = "ROADMAP", show: Optional[bool] = True, **kwargs
) -> None:
"""Adds a basemap to the map.

Args:
basemap (str, optional): Can be one of string from basemaps. Defaults to 'ROADMAP'.
visible (bool, optional): Whether the basemap is visible or not. Defaults to True.
show (bool, optional): Whether the basemap is visible or not. Defaults to True.
**kwargs: Keyword arguments for the TileLayer.
"""
import xyzservices

try:
layer_names = self.get_layer_names()

map_dict = {
"ROADMAP": "Esri.WorldStreetMap",
"SATELLITE": "Esri.WorldImagery",
"TERRAIN": "Esri.WorldTopoMap",
"HYBRID": "Esri.WorldImagery",
}

if isinstance(basemap, str):
if basemap.upper() in map_dict:
if basemap in os.environ:
if "name" in kwargs:
kwargs["name"] = basemap
basemap = os.environ[basemap]
else:
basemap = map_dict[basemap.upper()]
for map_name, tile_provider in self._available_basemaps.items():
if basemap.upper() == map_name.upper():
basemap = tile_provider
break

if isinstance(basemap, xyzservices.TileProvider):
name = basemap.name
Expand Down Expand Up @@ -946,18 +929,13 @@ def _on_basemap_changed(self, basemap_name):
bounds = [bounds[0][1], bounds[0][0], bounds[1][1], bounds[1][0]]
self.zoom_to_bounds(bounds)

def add_basemap_widget(self, value="OpenStreetMap", position="topright"):
def add_basemap_widget(self, position="topright"):
"""Add the Basemap GUI to the map.

Args:
value (str): The default value from basemaps to select. Defaults to "OpenStreetMap".
position (str, optional): The position of the Inspector GUI. Defaults to "topright".
"""
super()._add_basemap_selector(
position, basemaps=list(basemaps.keys()), value=value
)
if basemap_selector := self._basemap_selector:
basemap_selector.on_basemap_changed = self._on_basemap_changed
super()._add_basemap_selector(position=position)

def add_draw_control(self, position="topleft"):
"""Add a draw control to the map
Expand Down