From 7a813f1e6af51c0619a98277df92cf1057f61f97 Mon Sep 17 00:00:00 2001 From: Qiusheng Wu Date: Fri, 5 Jul 2024 23:39:09 -0400 Subject: [PATCH] Add maptiler 3d style function (#818) * Add maptiler 3d style function * Add 3d style and ocean notebook examples --- README.md | 4 +- docs/index.md | 4 +- docs/maplibre/3d_style.ipynb | 136 +++++++++++++++++++++++ docs/maplibre/ocean_bathymetry.ipynb | 93 ++++++++++++++++ docs/maplibre/overview.md | 10 ++ leafmap/maplibregl.py | 160 +++++++++++++++++++++++++-- mkdocs.yml | 2 + 7 files changed, 398 insertions(+), 11 deletions(-) create mode 100644 docs/maplibre/3d_style.ipynb create mode 100644 docs/maplibre/ocean_bathymetry.ipynb diff --git a/README.md b/README.md index 9a603fe0f6..9f062e17c3 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,6 @@ If you find **leafmap** useful in your research, please consider citing the foll ## YouTube Channel -I have created a [YouTube Channel](https://youtube.com/@giswqs) for sharing geospatial tutorials. You can subscribe to my channel for regular updates. If there is any specific tutorial you would like to see, please submit a feature request [here](https://github.com/opengeos/leafmap/issues). +I have created a [YouTube Channel](https://youtube.com/@giswqs) for sharing geospatial tutorials. You can subscribe to my channel for regular updates. Check out the following videos for 3D mapping with MapLibre and Leafmap. -[![Earth Engine Tutorials on YouTube](https://wetlands.io/file/images/youtube2024.jpeg)](https://youtube.com/@giswqs) +[![MapLibre tutorials](https://assets.gishub.org/images/maplibre-tutorials.png)](https://bit.ly/maplibre) diff --git a/docs/index.md b/docs/index.md index 58154e2ee8..1215a5f894 100644 --- a/docs/index.md +++ b/docs/index.md @@ -104,6 +104,6 @@ If you find **leafmap** useful in your research, please consider citing the foll ## YouTube Channel -I have created a [YouTube Channel](https://youtube.com/@giswqs) for sharing geospatial tutorials. You can subscribe to my channel for regular updates. If there is any specific tutorial you would like to see, please submit a feature request [here](https://github.com/opengeos/leafmap/issues). +I have created a [YouTube Channel](https://youtube.com/@giswqs) for sharing geospatial tutorials. You can subscribe to my channel for regular updates. Check out the following videos for 3D mapping with MapLibre and Leafmap. -[![Leafmap Tutorials on YouTube](https://wetlands.io/file/images/youtube2024.jpeg)](https://youtube.com/@giswqs) +[![MapLibre tutorials](https://assets.gishub.org/images/maplibre-tutorials.png)](https://bit.ly/maplibre) diff --git a/docs/maplibre/3d_style.ipynb b/docs/maplibre/3d_style.ipynb new file mode 100644 index 0000000000..c646faf38a --- /dev/null +++ b/docs/maplibre/3d_style.ipynb @@ -0,0 +1,136 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[![image](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://demo.leafmap.org/lab/index.html?path=maplibre/3d_style.ipynb)\n", + "[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/leafmap/blob/master/docs/maplibre/3d_style.ipynb)\n", + "[![image](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/opengeos/leafmap/HEAD)\n", + "\n", + "**Construct 3D style maps with MapTiler**\n", + "\n", + "Uncomment the following line to install [leafmap](https://leafmap.org) if needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install \"leafmap[maplibre]\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import leafmap.maplibregl as leafmap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To run this notebook, you will need an [API key](https://docs.maptiler.com/cloud/api/authentication-key/) from [MapTiler](https://www.maptiler.com/cloud/). Once you have the API key, you can uncomment the following code block and replace `YOUR_API_KEY` with your actual API key. Then, run the code block code to set the API key as an environment variable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# import os\n", + "# os.environ[\"MAPTILER_KEY\"] = \"YOUR_API_KEY\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "MapTiler provides a variety of basemaps and styles that can be used to create 3D maps. You can use any styles from the MapTiler basemap gallery and prefix the style name with `3d-`. For example, `3d-hybrid`, `3d-satellite`, or `3d-topo`. To use the hillshade only, you can use the `3d-hillshade` style.\n", + "\n", + "![](https://i.imgur.com/dp2HxR2.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m = leafmap.Map(style=\"3d-hybrid\")\n", + "m.add_layer_control(bg_layers=True)\n", + "m" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/3Q2Q3CG.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m = leafmap.Map(style=\"3d-satellite\")\n", + "m.add_layer_control(bg_layers=True)\n", + "m" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/5PNMbAv.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m = leafmap.Map(style=\"3d-topo\", exaggeration=1.5, hillshade=False)\n", + "m.add_layer_control(bg_layers=True)\n", + "m" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/y33leIj.png)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/maplibre/ocean_bathymetry.ipynb b/docs/maplibre/ocean_bathymetry.ipynb new file mode 100644 index 0000000000..7183035d53 --- /dev/null +++ b/docs/maplibre/ocean_bathymetry.ipynb @@ -0,0 +1,93 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[![image](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://demo.leafmap.org/lab/index.html?path=maplibre/ocean_bathymetry.ipynb)\n", + "[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/leafmap/blob/master/docs/maplibre/ocean_bathymetry.ipynb)\n", + "[![image](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/opengeos/leafmap/HEAD)\n", + "\n", + "**Ocean bathymetry 3D**\n", + "\n", + "This source code of this example is adapted from the MapTiler SDK JS example - [Ocean bathymetry 3D](https://docs.maptiler.com/sdk-js/examples/ocean-bathymetry).\n", + "\n", + "Uncomment the following line to install [leafmap](https://leafmap.org) if needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install \"leafmap[maplibre]\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import leafmap.maplibregl as leafmap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To run this notebook, you will need an [API key](https://docs.maptiler.com/cloud/api/authentication-key/) from [MapTiler](https://www.maptiler.com/cloud/). Once you have the API key, you can uncomment the following code block and replace `YOUR_API_KEY` with your actual API key. Then, run the code block code to set the API key as an environment variable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# import os\n", + "# os.environ[\"MAPTILER_KEY\"] = \"YOUR_API_KEY\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m = leafmap.Map(style=\"3d-ocean\", exaggeration=1.5, hillshade=True)\n", + "m.add_layer_control(bg_layers=True)\n", + "m" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/m6NwSWG.png)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/maplibre/overview.md b/docs/maplibre/overview.md index 8e2637bc3b..6992dfdb09 100644 --- a/docs/maplibre/overview.md +++ b/docs/maplibre/overview.md @@ -22,6 +22,10 @@ Create a 3D indoor map with the fill-extrude-height paint property. [![](https://i.imgur.com/eYhSWaT.png)](https://leafmap.org/maplibre/3d_indoor_mapping) +## Construct 3D map styles with MapTiler + +[![](https://i.imgur.com/3Q2Q3CG.png)](https://leafmap.org/maplibre/3d_style) + ## 3D Terrain Go beyond hillshade and show elevation in actual 3D. @@ -382,6 +386,12 @@ Add zoom and rotation controls to the map. [![](https://i.imgur.com/0A2o0oI.png)](https://leafmap.org/maplibre/navigation) +## Ocean Bathymetry 3D + +Visualize ocean bathymetry in 3D. + +[![](https://i.imgur.com/m6NwSWG.png)](https://leafmap.org/maplibre/ocean_bathymetry) + ## PMTiles source and protocol Uses the PMTiles plugin and protocol to present a map. diff --git a/leafmap/maplibregl.py b/leafmap/maplibregl.py index 71ff468261..7c12540237 100644 --- a/leafmap/maplibregl.py +++ b/leafmap/maplibregl.py @@ -93,14 +93,14 @@ def __init__( "The provided style URL is invalid. Falling back to 'dark-matter'." ) style = "dark-matter" - elif style == "3d-terrain": - style = self._get_3d_terrain_style( - exaggeration=kwargs.pop("exaggeration", 1.0) - ) - elif style == "3d-terrain-background": - style = self._get_3d_terrain_style( - satellite=False, exaggeration=kwargs.pop("exaggeration", 1.0) + elif style.startswith("3d-"): + style = maptiler_3d_style( + style=style.replace("3d-", "").lower(), + exaggeration=kwargs.pop("exaggeration", 1), + tile_size=kwargs.pop("tile_size", 512), + hillshade=kwargs.pop("hillshade", True), ) + elif style.lower() in carto_basemaps: style = construct_carto_basemap_url(style.lower()) elif style == "demotiles": @@ -2873,3 +2873,149 @@ def construct_maptiler_style(style: str, api_key: Optional[str] = None) -> str: url = "dark-matter" return url + + +def maptiler_3d_style( + style="satellite", + exaggeration: float = 1, + tile_size: int = 512, + tile_type: str = None, + max_zoom: int = 24, + hillshade: bool = True, + token: str = "MAPTILER_KEY", + api_key: Optional[str] = None, +) -> Dict[str, Any]: + """ + Get the 3D terrain style for the map. + + This function generates a style dictionary for the map that includes 3D terrain features. + The terrain exaggeration and API key can be specified. If the API key is not provided, + it will be retrieved using the specified token. + + Args: + style (str): The name of the MapTiler style to be accessed. It can be one of the following: + aquarelle, backdrop, basic, bright, dataviz, hillshade, landscape, ocean, openstreetmap, outdoor, + satellite, streets, toner, topo, winter, etc. + exaggeration (float, optional): The terrain exaggeration. Defaults to 1. + tile_size (int, optional): The size of the tiles. Defaults to 512. + tile_type (str, optional): The type of the tiles. It can be one of the following: + webp, png, jpg. Defaults to None. + max_zoom (int, optional): The maximum zoom level. Defaults to 24. + hillshade (bool, optional): Whether to include hillshade. Defaults to True. + token (str, optional): The token to use to retrieve the API key. Defaults to "MAPTILER_KEY". + api_key (Optional[str], optional): The API key. If not provided, it will be retrieved using the token. + + Returns: + Dict[str, Any]: The style dictionary for the map. + + Raises: + ValueError: If the API key is not provided and cannot be retrieved using the token. + """ + + if api_key is None: + api_key = get_api_key(token) + + if api_key is None: + print("An API key is required to use the 3D terrain feature.") + return "dark-matter" + + if style == "terrain": + style = "satellite" + elif style == "hillshade": + style = None + + if tile_type is None: + + image_types = { + "aquarelle": "webp", + "backdrop": "png", + "basic": "png", + "basic-v2": "png", + "bright": "png", + "bright-v2": "png", + "dataviz": "png", + "hybrid": "jpg", + "landscape": "png", + "ocean": "png", + "openstreetmap": "jpg", + "outdoor": "png", + "outdoor-v2": "png", + "satellite": "jpg", + "toner": "png", + "toner-v2": "png", + "topo": "png", + "topo-v2": "png", + "winter": "png", + "winter-v2": "png", + } + if style in image_types: + tile_type = image_types[style] + else: + tile_type = "png" + + layers = [] + + if isinstance(style, str): + layers.append({"id": style, "type": "raster", "source": style}) + + if hillshade: + layers.append( + { + "id": "hillshade", + "type": "hillshade", + "source": "hillshadeSource", + "layout": {"visibility": "visible"}, + "paint": {"hillshade-shadow-color": "#473B24"}, + } + ) + + if style == "ocean": + sources = { + "terrainSource": { + "type": "raster-dem", + "url": f"https://api.maptiler.com/tiles/ocean-rgb/tiles.json?key={api_key}", + "tileSize": tile_size, + }, + "hillshadeSource": { + "type": "raster-dem", + "url": f"https://api.maptiler.com/tiles/ocean-rgb/tiles.json?key={api_key}", + "tileSize": tile_size, + }, + } + else: + sources = { + "terrainSource": { + "type": "raster-dem", + "url": f"https://api.maptiler.com/tiles/terrain-rgb-v2/tiles.json?key={api_key}", + "tileSize": tile_size, + }, + "hillshadeSource": { + "type": "raster-dem", + "url": f"https://api.maptiler.com/tiles/terrain-rgb-v2/tiles.json?key={api_key}", + "tileSize": tile_size, + }, + } + if isinstance(style, str): + sources[style] = { + "type": "raster", + "tiles": [ + "https://api.maptiler.com/maps/" + + style + + "/{z}/{x}/{y}." + + tile_type + + "?key=" + + api_key + ], + "tileSize": tile_size, + "attribution": "© MapTiler", + "maxzoom": max_zoom, + } + + style = { + "version": 8, + "sources": sources, + "layers": layers, + "terrain": {"source": "terrainSource", "exaggeration": exaggeration}, + } + + return style diff --git a/mkdocs.yml b/mkdocs.yml index 18f3d8af8c..812d637b98 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -142,6 +142,7 @@ nav: - maplibre/overview.md - maplibre/3d_buildings.ipynb - maplibre/3d_indoor_mapping.ipynb + - maplibre/3d_style.ipynb - maplibre/3d_terrain.ipynb - maplibre/add_3d_buildings.ipynb - maplibre/add_a_marker.ipynb @@ -202,6 +203,7 @@ nav: - maplibre/mouse_position.ipynb - maplibre/multiple_geometries.ipynb - maplibre/navigation.ipynb + - maplibre/ocean_bathymetry.ipynb - maplibre/pmtiles.ipynb - maplibre/restrict_bounds.ipynb - maplibre/satellite_map.ipynb