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 1 commit
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,4 @@ ENV/
# IDE settings
.vscode/
docs/changelog_update.py
oryx-build-commands.txt
191 changes: 50 additions & 141 deletions geemap/basemaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,14 @@

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

import folium
import ipyleaflet
import requests
import xyzservices
from .common import check_package, planet_tiles, google_maps_api_key
from typing import Optional, Any, Dict, Union
from xyzservices import TileProvider

from .common import check_package, get_google_maps_api_key, planet_tiles

XYZ_TILES = {
"OpenStreetMap": {
Expand Down Expand Up @@ -241,12 +240,25 @@
custom_tiles = {"xyz": XYZ_TILES, "wms": WMS_TILES}


class GoogleMapsTileProvider(TileProvider):
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",
map_type: str = "roadmap",
language: str = "en-Us",
region: str = "US",
api_key: Optional[str] = None,
Expand All @@ -256,8 +268,8 @@ def __init__(
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 `MAPS_API_KEY`
or by passing it as an argument.
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
Expand Down Expand Up @@ -291,99 +303,50 @@ def __init__(
TileProvider object: A TileProvider object with the Google Maps tile.
"""

if api_key is None:

if "google.colab" in sys.modules:
from google.colab import userdata

api_key = userdata.get("MAPS_API_KEY")
else:
api_key = os.environ.get("MAPS_API_KEY")

if api_key is None:
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 "
"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"
)

allowed_map_types = [
"roadmap",
"satellite",
"terrain",
"hybrid",
"traffic",
"streetview",
]

# Support map type as a string with or without 'google.',
# such as 'Google Roadmap', 'Google.Roadmap', or 'Roadmap'
if isinstance(map_type, str):
map_type = (
map_type.lower().replace("google.", "").replace("google", "").strip()
)
if map_type not in self.MAP_TYPE_CONFIG:
raise ValueError(f"map_type must be one of: {self.MAP_TYPE_CONFIG.keys()}")

if map_type not in allowed_map_types:
raise ValueError(
"map_type must be one of 'roadmap', 'satellite', 'terrain', "
"'hybrid', 'traffic', 'streetview'"
)
else:
raise ValueError("map_type must be a string")

tile_args = {}

# Define the parameters for each map type
for m_type in allowed_map_types:

mapType = m_type
layerTypes = None

if m_type == "hybrid":
mapType = "satellite"
layerTypes = ["layerRoadmap"]
elif m_type == "terrain":
layerTypes = ["layerRoadmap"]
elif m_type == "traffic":
mapType = "roadmap"
layerTypes = ["layerTraffic"]
elif m_type == "streetview":
mapType = "roadmap"
layerTypes = ["layerStreetview"]

tile_args[m_type] = {
"mapType": mapType,
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,
"layerTypes": layerTypes,
**kwargs,
}

if tile_args[m_type].get("layerTypes") is None:
del tile_args[m_type]["layerTypes"]

args = tile_args[map_type]
response = requests.post(
f"https://tile.googleapis.com/v1/createSession?key={api_key}",
headers={"Content-Type": "application/json"},
json=args,
},
timeout=3,
)

if response.status_code == 200:
res = response.json()
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={res['session']}&key={{accessToken}}",
"attribution": f"© Google {map_type.capitalize()}",
"accessToken": api_key,
"name": f"Google.{map_type.capitalize()}",
"ext": res["imageFormat"],
"tileSize": res["tileWidth"],
"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 google_map_tiles(
def get_google_map_tile_providers(
language: str = "en-Us",
region: str = "US",
api_key: Optional[str] = None,
Expand All @@ -410,70 +373,16 @@ def google_map_tiles(
('roadmap', 'satellite', 'terrain', 'hybrid')
and the values are the corresponding GoogleMapsTileProvider objects.
"""
allowed_map_types = [
"roadmap",
"satellite",
"terrain",
"hybrid",
# "traffic",
# "streetview",
]

gmap_providers = {}

for m_type in allowed_map_types:
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 check_basemap_alias(
basemap: str,
) -> str:
"""
Checks if the provided basemap is an alias and returns the corresponding basemap name.

Args:
basemap (str): The basemap to check.

Returns:
str: The corresponding basemap name if the input is an alias, otherwise the input itself.
"""

allowed_aliases = {
"DEFAULT": {
"Google": "Google.Roadmap",
"Esri": "OpenStreetMap.Mapnik",
},
"ROADMAP": {
"Google": "Google.Roadmap",
"Esri": "Esri.WorldStreetMap",
},
"SATELLITE": {
"Google": "Google.Satellite",
"Esri": "Esri.WorldImagery",
},
"TERRAIN": {
"Google": "Google.Terrain",
"Esri": "Esri.WorldTopoMap",
},
"HYBRID": {
"Google": "Google.Hybrid",
"Esri": "Esri.WorldImagery",
},
}

if isinstance(basemap, str) and basemap.upper() in allowed_aliases:
if google_maps_api_key() is not None:
return allowed_aliases[basemap.upper()]["Google"]
else:
return allowed_aliases[basemap.upper()]["Esri"]
else:
return basemap


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

Expand Down
17 changes: 8 additions & 9 deletions geemap/common.py
giswqs marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -16169,23 +16169,22 @@ def is_on_aws():
return on_aws


def google_maps_api_key(lookup_key: str = "MAPS_API_KEY") -> Optional[str]:
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:
lookup_key (str, optional): The name of the environment variable or
Colab user data key where the API key is stored.
Defaults to 'MAPS_API_KEY'.
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

env_value = userdata.get(lookup_key)
else:
env_value = os.environ.get(lookup_key, None)

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

return os.environ.get(key, None)
71 changes: 32 additions & 39 deletions geemap/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,23 +402,13 @@ class Map(ipyleaflet.Map, MapInterface):
"scroll_wheel_zoom": True,
}

if common.google_maps_api_key() is not None:
_BASEMAP_ALIASES: Dict[str, str] = {
"OpenStreetMap": "OpenStreetMap.Mapnik",
"Google.Roadmap": "Google.Roadmap",
"Google.Satellite": "Google.Satellite",
"Google.Terrain": "Google.Terrain",
"Google.Hybrid": "Google.Hybrid",
}

else:
_BASEMAP_ALIASES: Dict[str, str] = {
"OpenStreetMap": "OpenStreetMap.Mapnik",
"Esri.WorldStreetMap": "Esri.WorldStreetMap",
"Esri.WorldImagery": "Esri.WorldImagery",
"Esri.WorldTopoMap": "Esri.WorldTopoMap",
"Esri.WorldImagery": "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 @@ -468,14 +458,9 @@ def _basemap_selector(self) -> Optional[map_widgets.Basemap]:
def __init__(self, **kwargs):
self._available_basemaps = self._get_available_basemaps()

if "basemap" not in kwargs and "Google.Roadmap" in self._available_basemaps:
kwargs["basemap"] = self._available_basemaps["Google.Roadmap"]
elif "basemap" in kwargs and isinstance(kwargs["basemap"], str):
kwargs["basemap"] = basemaps.check_basemap_alias(kwargs["basemap"])
if kwargs["basemap"] in self._available_basemaps:
kwargs["basemap"] = self._available_basemaps.get(kwargs["basemap"])
giswqs marked this conversation as resolved.
Show resolved Hide resolved
else:
raise ValueError(f"Basemap {kwargs['basemap']} not found.")
# Use the first basemap in the list of available basemaps.
if "basemap" not in kwargs:
kwargs["basemap"] = next(iter(self._available_basemaps.values()))

if "width" in kwargs:
self.width: str = kwargs.pop("width", "100%")
Expand Down Expand Up @@ -865,27 +850,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

if common.google_maps_api_key() is not None:
for tile_info in basemaps.google_map_tiles().values():
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
Loading
Loading