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 13 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
85 changes: 55 additions & 30 deletions geemap/basemaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,36 +23,61 @@
import folium
import ipyleaflet
import xyzservices
from .common import check_package, planet_tiles

XYZ_TILES = {
"OpenStreetMap": {
"url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
"attribution": "OpenStreetMap",
"name": "OpenStreetMap",
},
"ROADMAP": {
"url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}",
"attribution": "Esri",
"name": "Esri.WorldStreetMap",
},
"SATELLITE": {
"url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
"attribution": "Esri",
"name": "Esri.WorldImagery",
},
"TERRAIN": {
"url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}",
"attribution": "Esri",
"name": "Esri.WorldTopoMap",
},
"HYBRID": {
"url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
"attribution": "Esri",
"name": "Esri.WorldImagery",
},
}

from .common import (
check_package,
planet_tiles,
google_map_tiles,
google_maps_api_key,
)

MAPS_API_KEY = google_maps_api_key()
giswqs marked this conversation as resolved.
Show resolved Hide resolved

if MAPS_API_KEY is None:

XYZ_TILES = {
"OpenStreetMap": {
"url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
"attribution": "OpenStreetMap",
"name": "OpenStreetMap",
},
"ROADMAP": {
"url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}",
"attribution": "Esri",
"name": "Esri.WorldStreetMap",
},
"SATELLITE": {
"url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
"attribution": "Esri",
"name": "Esri.WorldImagery",
},
"TERRAIN": {
"url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}",
"attribution": "Esri",
"name": "Esri.WorldTopoMap",
},
"HYBRID": {
"url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
"attribution": "Esri",
"name": "Esri.WorldImagery",
},
}

else:
XYZ_TILES = {
"OpenStreetMap": {
"url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
"attribution": "OpenStreetMap",
"name": "OpenStreetMap",
},
}

gmap_providers = google_map_tiles(api_key=MAPS_API_KEY)
for key, provider in gmap_providers.items():
XYZ_TILES[provider["name"]] = {
"url": provider.build_url(),
"attribution": provider["attribution"],
"name": provider["name"],
}

# Custom WMS tile services.
WMS_TILES = {
Expand Down
159 changes: 158 additions & 1 deletion 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 @@ -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,160 @@ def is_on_aws():
if item.endswith(".aws") or "ec2-user" in item:
on_aws = True
return on_aws


def google_map_tiles(
jdbcode marked this conversation as resolved.
Show resolved Hide resolved
giswqs marked this conversation as resolved.
Show resolved Hide resolved
map_type: str = None,
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 `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 None, which generates all map types, and returns a dictionary of TileProvider objects.
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. Defaults to None.
**kwargs: Additional parameters to pass to the map generation. For more info, visit https://developers.google.com/maps/documentation/tile/session_tokens#optional_fields

giswqs marked this conversation as resolved.
Show resolved Hide resolved
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:
>>> import geemap
>>> m = geemap.Map()
>>> basemap = geemap.google_map_tiles(map_type='roadmap', language="en-Us", region="US", scale="scaleFactor2x", highDpi=True)
>>> m.add_basemap(basemap)

Returns:
TileProvider | dict: A TileProvider object with the generated map, or a dictionary of TileProvider objects for all map types,
or None if the map could not be generated.
"""

from xyzservices import TileProvider

if api_key is None:
giswqs marked this conversation as resolved.
Show resolved Hide resolved

if in_colab_shell():
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:
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"
)

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 allowed_map_types:
raise ValueError(
"mapType must be one of 'roadmap', 'satellite', 'terrain', 'hybrid', 'traffic', 'streetview'"
)

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,
"language": language,
"region": region,
"layerTypes": layerTypes,
**kwargs,
}

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

gmap_providers = {}

for m_type in allowed_map_types:

# If map_type is provided, only generate the specified map
if map_type is not None and m_type != map_type:
continue

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

if response.status_code == 200:
res = response.json()
gmap_provider = TileProvider(
{
"url": f"https://tile.googleapis.com/v1/2dtiles/{{z}}/{{x}}/{{y}}?session={res['session']}&key={{accessToken}}",
"attribution": f"© Google {m_type.capitalize()}",
"accessToken": api_key,
"name": f"Google.{m_type.capitalize()}",
"ext": res["imageFormat"],
"tileSize": res["tileWidth"],
}
)
gmap_providers[m_type] = gmap_provider
else:
display(response.text)
gmap_provider = None

if map_type is None: # Return all map types
return gmap_providers
else: # Return the specified map type
return gmap_providers.get(map_type)


def google_maps_api_key(token_name="MAPS_API_KEY"):
giswqs marked this conversation as resolved.
Show resolved Hide resolved
giswqs marked this conversation as resolved.
Show resolved Hide resolved
"""
Retrieves the Google Maps API key from the environment or Colab user data.

Args:
token_name (str, optional): The name of the environment variable or Colab user data key where the API key is stored. Defaults to '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

MAPS_API_KEY = userdata.get(token_name)
else:
MAPS_API_KEY = os.environ.get(token_name, None)

return MAPS_API_KEY
40 changes: 33 additions & 7 deletions geemap/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,13 +402,25 @@ 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",
}
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",
"Google.Traffic": "Google.Traffic",
"Google.Streetview": "Google.Streetview",
giswqs marked this conversation as resolved.
Show resolved Hide resolved
giswqs marked this conversation as resolved.
Show resolved Hide resolved
}

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",
}

_USER_AGENT_PREFIX = "geemap-core"

Expand Down Expand Up @@ -458,6 +470,15 @@ 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)
and 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 @@ -850,6 +871,11 @@ def _get_available_basemaps(self) -> Dict[str, Any]:
for tile_info in basemaps.get_xyz_dict().values():
tile_info["url"] = tile_info.build_url()
ret_dict[tile_info["name"]] = tile_info

if "Google.Roadmap" in basemaps.XYZ_TILES:
for key in basemaps.XYZ_TILES:
if key.startswith("Google"):
ret_dict[key] = basemaps.XYZ_TILES[key]
extra_dict = {k: ret_dict[v] for k, v in self._BASEMAP_ALIASES.items()}
return {**extra_dict, **ret_dict}

Expand Down
3 changes: 2 additions & 1 deletion geemap/geemap.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ def __init__(self, **kwargs):
kwargs["add_google_map"] = False
else:
kwargs.pop("basemap")

elif "Google.Roadmap" in basemaps.keys():
kwargs["basemap"] = get_basemap("Google.Roadmap")
self._xyz_dict = get_xyz_dict()

self.baseclass = "ipyleaflet"
Expand Down