diff --git a/docs/get-started.md b/docs/get-started.md
index 6531b2692b..240026d35b 100644
--- a/docs/get-started.md
+++ b/docs/get-started.md
@@ -38,6 +38,32 @@ Map = geemap.Map(center=(40, -100), zoom=4)
Map
```
+## Use basemaps
+
+Basemaps can be added to the map using the `add_basemap()` function. The default basemap is `OpenStreetMap`.
+
+```python
+Map = geemap.Map()
+Map.add_basemap("Esri.WorldImagery")
+Map.add_basemap("OpenTopoMap")
+Map
+```
+
+All Google basemaps have been removed from the geemap since [v0.26.0](https://geemap.org/changelog/#v0270-sep-21-2023) to comply with Google Maps' terms of service. Users can choose to add Google basemaps at their own risks by setting environment variables as follows. If no env variables are detected, Esri basemaps will be used.
+
+```python
+import os
+
+os.environ["ROADMAP"] = 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}'
+os.environ["SATELLITE"] = 'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}'
+os.environ["TERRAIN"] = 'https://mt1.google.com/vt/lyrs=p&x={x}&y={y}&z={z}'
+os.environ["HYBRID"] = 'https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}'
+
+Map = geemap.Map()
+Map.add_basemap("HYBRID")
+Map
+```
+
## Add Earth Engine data
```python
diff --git a/docs/notebooks/114_dynamic_world.ipynb b/docs/notebooks/114_dynamic_world.ipynb
index 91b713f8b4..8e4b760db7 100644
--- a/docs/notebooks/114_dynamic_world.ipynb
+++ b/docs/notebooks/114_dynamic_world.ipynb
@@ -1,119 +1,143 @@
{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "\n",
- "\n",
- "**Creating near real-time global 10-m land cover maps with geemap and Dynamic World**\n",
- "\n",
- "- App: https://www.dynamicworld.app\n",
- "- App2: https://earthoutreach.users.earthengine.app/view/dynamicworld\n",
- "- Paper: https://doi.org/10.1038/s41597-022-01307-4\n",
- "- Model: https://github.com/google/dynamicworld\n",
- "- Training data: https://doi.pangaea.de/10.1594/PANGAEA.933475\n",
- "- Data: https://developers.google.com/earth-engine/datasets/catalog/GOOGLE_DYNAMICWORLD_V1\n",
- "- JavaScript tutorial: https://developers.google.com/earth-engine/tutorials/community/introduction-to-dynamic-world-pt-1"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import ee\n",
- "import geemap"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "Map = geemap.Map()\n",
- "Map.add_basemap('HYBRID')\n",
- "Map"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Set the region of interest by simply drawing a polygon on the map\n",
- "region = Map.user_roi\n",
- "if region is None:\n",
- " region = ee.Geometry.BBox(-89.7088, 42.9006, -89.0647, 43.2167)\n",
- "\n",
- "Map.centerObject(region)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Set the date range\n",
- "start_date = '2021-01-01'\n",
- "end_date = '2022-01-01'"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Create a Sentinel-2 image composite\n",
- "image = geemap.dynamic_world_s2(region, start_date, end_date)\n",
- "vis_params = {'bands': ['B4', 'B3', 'B2'], 'min': 0, 'max': 3000}\n",
- "Map.addLayer(image, vis_params, 'Sentinel-2 image')"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Create Dynamic World land cover composite\n",
- "landcover = geemap.dynamic_world(region, start_date, end_date, return_type='hillshade')\n",
- "Map.addLayer(landcover, {}, 'Land Cover')"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": []
- },
- "outputs": [],
- "source": [
- "# Add legend to the map\n",
- "Map.add_legend(title=\"Dynamic World Land Cover\", builtin_legend='Dynamic_World')\n",
- "Map"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "![](https://i.imgur.com/GEzsSii.png)"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3",
- "language": "python",
- "name": "python3"
- }
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ },
+ "source": [
+ "\n",
+ "\n",
+ "**Creating near real-time global 10-m land cover maps with geemap and Dynamic World**\n",
+ "\n",
+ "- App: \n",
+ "- App2: \n",
+ "- Paper: \n",
+ "- Model: \n",
+ "- Training data: \n",
+ "- Data: \n",
+ "- JavaScript tutorial: "
+ ]
},
- "nbformat": 4,
- "nbformat_minor": 5
-}
\ No newline at end of file
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ },
+ "outputs": [],
+ "source": [
+ "import ee\n",
+ "import geemap"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ },
+ "outputs": [],
+ "source": [
+ "Map = geemap.Map()\n",
+ "Map.add_basemap('HYBRID')\n",
+ "Map"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ },
+ "outputs": [],
+ "source": [
+ "# Set the region of interest by simply drawing a polygon on the map\n",
+ "region = Map.user_roi\n",
+ "if region is None:\n",
+ " region = ee.Geometry.BBox(-89.7088, 42.9006, -89.0647, 43.2167)\n",
+ "\n",
+ "Map.centerObject(region)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ },
+ "outputs": [],
+ "source": [
+ "# Set the date range\n",
+ "start_date = '2021-01-01'\n",
+ "end_date = '2022-01-01'"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ },
+ "outputs": [],
+ "source": [
+ "# Create a Sentinel-2 image composite\n",
+ "image = geemap.dynamic_world_s2(region, start_date, end_date)\n",
+ "vis_params = {'bands': ['B4', 'B3', 'B2'], 'min': 0, 'max': 3000}\n",
+ "Map.addLayer(image, vis_params, 'Sentinel-2 image')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ },
+ "outputs": [],
+ "source": [
+ "# Create Dynamic World land cover composite\n",
+ "landcover = geemap.dynamic_world(region, start_date, end_date, return_type='hillshade')\n",
+ "Map.addLayer(landcover, {}, 'Land Cover')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Add legend to the map\n",
+ "Map.add_legend(title=\"Dynamic World Land Cover\", builtin_legend='Dynamic_World')\n",
+ "Map"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ },
+ "source": [
+ "![](https://i.imgur.com/GEzsSii.png)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ },
+ "outputs": [],
+ "source": [
+ "# Save Dynamic World class data in GeoTIFF format\n",
+ "output_path = 'landcover.tif'\n",
+ "landcover = geemap.dynamic_world(region, start_date, end_date, return_type='class')\n",
+ "geemap.ee_export_image(landcover, filename=output_path, scale=10, region=region, file_per_band=False)"
+ ]
+ }
+ ],
+ "metadata": {
+ "colab": {
+ "provenance": []
+ },
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/notebooks/141_image_array_viz.ipynb b/docs/notebooks/141_image_array_viz.ipynb
new file mode 100644
index 0000000000..b9e6d1e337
--- /dev/null
+++ b/docs/notebooks/141_image_array_viz.ipynb
@@ -0,0 +1,202 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "\n",
+ "**Visualizing in-memory raster datasets and image arrays**"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# !pip install -U geemap"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import ee\n",
+ "import geemap"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Use a Landsat image covering San Francisco Bay Area."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "m = geemap.Map()\n",
+ "image = ee.Image(\"LANDSAT/LC08/C02/T1_TOA/LC08_044034_20140318\").select(\n",
+ " ['B5', 'B4', 'B3']\n",
+ ")\n",
+ "vis_params = {\n",
+ " 'min': 0.0,\n",
+ " 'max': 0.4,\n",
+ "}\n",
+ "m.add_layer(image, vis_params, 'False Color (543)')\n",
+ "m.centerObject(image)\n",
+ "m"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Retrieve the projection information from the image."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "projection = image.select(0).projection().getInfo()\n",
+ "crs = projection['crs']\n",
+ "crs"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Read the Earth Engine image into an Xarray DataArray."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "ds = geemap.ee_to_xarray(\n",
+ " image, \n",
+ " crs=\"EPSG:32610\", \n",
+ " scale=300,\n",
+ " geometry=image.geometry(),\n",
+ " ee_mask_value=0\n",
+ ")\n",
+ "ds"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Save the DataArray as a Cloud Optimized GeoTIFF."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "geemap.xee_to_image(ds, filenames=['landsat.tif'])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Calculate the NDVI and save it to the DataArray."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "ndvi = (ds['B5'] - ds['B4']) / (ds['B5'] + ds['B4'])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Create an in-memory raster dataset from the DataArray."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "ndvi_image = geemap.array_to_image(ndvi, source=\"landsat.tif\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Visualize the in-memory raster dataset."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "m.add_raster('landsat.tif', band=[1, 2, 3], nodata=-1, layer_name=\"Landsat 7\")\n",
+ "m.add_raster(ndvi_image, cmap=\"Greens\", layer_name=\"NDVI\")\n",
+ "m"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Save the in-memory raster dataset to a GeoTIFF file."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "geemap.array_to_image(ndvi, output=\"ndvi.tif\", source=\"landsat.tif\")"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "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.5"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/notebooks/83_local_tile.ipynb b/docs/notebooks/83_local_tile.ipynb
index a20fd8b04a..15a1960f75 100644
--- a/docs/notebooks/83_local_tile.ipynb
+++ b/docs/notebooks/83_local_tile.ipynb
@@ -1,166 +1,166 @@
{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "[![image](https://mybinder.org/badge_logo.svg)](https://gishub.org/geemap-binder)\n",
- "\n",
- "**Using local raster datasets or remote Cloud Optimized GeoTIFFs (COG) with geemap**\n",
- "\n",
- "Uncomment the following line to install [geemap](https://geemap.org) and [localtileserver](https://github.com/banesullivan/localtileserver) if needed."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# !pip install geemap localtileserver"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import os\n",
- "import geemap"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Specify input raster datasets"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "out_dir = os.path.expanduser('~/Downloads')\n",
- "\n",
- "if not os.path.exists(out_dir):\n",
- " os.makedirs(out_dir)\n",
- "\n",
- "dem = os.path.join(out_dir, 'dem.tif')"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Download samples raster datasets."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "if not os.path.exists(dem):\n",
- " dem_url = 'https://drive.google.com/file/d/1vRkAWQYsLWCi6vcTMk8vLxoXMFbdMFn8/view?usp=sharing'\n",
- " geemap.download_file(dem_url, dem, unzip=False)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Create an interactive map."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m = geemap.Map()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Add local raster datasets to the map. The available palettes can be found at https://jiffyclub.github.io/palettable/"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.add_local_tile(dem, palette='terrain', layer_name=\"DEM\")"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Add a remote Cloud Optimized GeoTIFF(COG) to the map."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m = geemap.Map()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "url = 'https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2018-02-16/pine-gulch-fire20/1030010076004E00.tif'"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.add_remote_tile(url, layer_name=\"CA Fire\")"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3",
- "language": "python",
- "name": "python3"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 5
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "[![image](https://mybinder.org/badge_logo.svg)](https://gishub.org/geemap-binder)\n",
+ "\n",
+ "**Using local raster datasets or remote Cloud Optimized GeoTIFFs (COG) with geemap**\n",
+ "\n",
+ "Uncomment the following line to install [geemap](https://geemap.org) and [localtileserver](https://github.com/banesullivan/localtileserver) if needed."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# !pip install geemap localtileserver"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import os\n",
+ "import geemap"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Specify input raster datasets"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "out_dir = os.path.expanduser('~/Downloads')\n",
+ "\n",
+ "if not os.path.exists(out_dir):\n",
+ " os.makedirs(out_dir)\n",
+ "\n",
+ "dem = os.path.join(out_dir, 'dem.tif')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Download samples raster datasets."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "if not os.path.exists(dem):\n",
+ " dem_url = 'https://drive.google.com/file/d/1vRkAWQYsLWCi6vcTMk8vLxoXMFbdMFn8/view?usp=sharing'\n",
+ " geemap.download_file(dem_url, dem, unzip=False)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Create an interactive map."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "m = geemap.Map()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Add local raster datasets to the map. The available palettes can be found at https://jiffyclub.github.io/palettable/"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "m.add_raster(dem, palette='terrain', layer_name=\"DEM\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "m"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Add a remote Cloud Optimized GeoTIFF(COG) to the map."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "m = geemap.Map()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "url = 'https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2018-02-16/pine-gulch-fire20/1030010076004E00.tif'"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "m.add_remote_tile(url, layer_name=\"CA Fire\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "m"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
}
diff --git a/docs/notebooks/95_create_cog.ipynb b/docs/notebooks/95_create_cog.ipynb
index a090ea84cd..92ef456704 100644
--- a/docs/notebooks/95_create_cog.ipynb
+++ b/docs/notebooks/95_create_cog.ipynb
@@ -141,7 +141,7 @@
"outputs": [],
"source": [
"m = geemap.Map()\n",
- "m.add_local_tile(out_cog, palette=\"dem\", layer_name=\"Local COG\")\n",
+ "m.add_raster(out_cog, palette=\"dem\", layer_name=\"Local COG\")\n",
"m.add_cog_layer(url, palette=\"gist_earth\", name=\"Remote COG\")\n",
"m"
]
@@ -156,4 +156,4 @@
},
"nbformat": 4,
"nbformat_minor": 5
-}
\ No newline at end of file
+}
diff --git a/docs/tutorials.md b/docs/tutorials.md
index db3b5ef581..5ad4a18aca 100644
--- a/docs/tutorials.md
+++ b/docs/tutorials.md
@@ -152,3 +152,4 @@ More video tutorials for geemap and Earth Engine are available on my [YouTube ch
138. Clipping Earth Engine images interactively with the Draw Control ([notebook](https://geemap.org/notebooks/138_draw_control))
139. Converting an Earth Engine to an image ([notebook](https://geemap.org/notebooks/139_layer_to_image))
140. Converting Earth Engine images to an Xarray Dataset ([notebook](https://geemap.org/notebooks/140_ee_to_xarray))
+141. Visualizing in-memory raster datasets and image arrays ([notebook](https://geemap.org/notebooks/141_image_array_viz))
diff --git a/examples/README.md b/examples/README.md
index db28d90fe3..5561b0b7e1 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -157,6 +157,7 @@ More video tutorials for geemap and Earth Engine are available on my [YouTube ch
138. Clipping Earth Engine images interactively with the Draw Control ([notebook](https://geemap.org/notebooks/138_draw_control))
139. Converting an Earth Engine to an image ([notebook](https://geemap.org/notebooks/139_layer_to_image))
140. Converting Earth Engine images to an Xarray Dataset ([notebook](https://geemap.org/notebooks/140_ee_to_xarray))
+141. Visualizing in-memory raster datasets and image arrays ([notebook](https://geemap.org/notebooks/141_image_array_viz))
### 1. Introducing the geemap Python package for interactive mapping with Google Earth Engine
diff --git a/examples/notebooks/114_dynamic_world.ipynb b/examples/notebooks/114_dynamic_world.ipynb
index e7043a2313..8e4b760db7 100644
--- a/examples/notebooks/114_dynamic_world.ipynb
+++ b/examples/notebooks/114_dynamic_world.ipynb
@@ -3,55 +3,48 @@
{
"cell_type": "markdown",
"metadata": {
- "id": "oz4mAK4uuu16"
},
"source": [
"\n",
"\n",
"**Creating near real-time global 10-m land cover maps with geemap and Dynamic World**\n",
"\n",
- "- App: https://www.dynamicworld.app\n",
- "- App2: https://earthoutreach.users.earthengine.app/view/dynamicworld\n",
- "- Paper: https://doi.org/10.1038/s41597-022-01307-4\n",
- "- Model: https://github.com/google/dynamicworld\n",
- "- Training data: https://doi.pangaea.de/10.1594/PANGAEA.933475\n",
- "- Data: https://developers.google.com/earth-engine/datasets/catalog/GOOGLE_DYNAMICWORLD_V1\n",
- "- JavaScript tutorial: https://developers.google.com/earth-engine/tutorials/community/introduction-to-dynamic-world-pt-1"
- ],
- "id": "oz4mAK4uuu16"
+ "- App: \n",
+ "- App2: \n",
+ "- Paper: \n",
+ "- Model: \n",
+ "- Training data: \n",
+ "- Data: \n",
+ "- JavaScript tutorial: "
+ ]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
- "id": "7Cf5PSGNuu19"
},
"outputs": [],
"source": [
"import ee\n",
"import geemap"
- ],
- "id": "7Cf5PSGNuu19"
+ ]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
- "id": "z-bJd8Reuu1-"
},
"outputs": [],
"source": [
"Map = geemap.Map()\n",
"Map.add_basemap('HYBRID')\n",
"Map"
- ],
- "id": "z-bJd8Reuu1-"
+ ]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
- "id": "oQkJa0Xzuu1-"
},
"outputs": [],
"source": [
@@ -61,28 +54,24 @@
" region = ee.Geometry.BBox(-89.7088, 42.9006, -89.0647, 43.2167)\n",
"\n",
"Map.centerObject(region)"
- ],
- "id": "oQkJa0Xzuu1-"
+ ]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
- "id": "gkQcggxRuu1_"
},
"outputs": [],
"source": [
"# Set the date range\n",
"start_date = '2021-01-01'\n",
"end_date = '2022-01-01'"
- ],
- "id": "gkQcggxRuu1_"
+ ]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
- "id": "7yBvgunJuu1_"
},
"outputs": [],
"source": [
@@ -90,74 +79,65 @@
"image = geemap.dynamic_world_s2(region, start_date, end_date)\n",
"vis_params = {'bands': ['B4', 'B3', 'B2'], 'min': 0, 'max': 3000}\n",
"Map.addLayer(image, vis_params, 'Sentinel-2 image')"
- ],
- "id": "7yBvgunJuu1_"
+ ]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
- "id": "RRnATqO8uu1_"
},
"outputs": [],
"source": [
"# Create Dynamic World land cover composite\n",
"landcover = geemap.dynamic_world(region, start_date, end_date, return_type='hillshade')\n",
"Map.addLayer(landcover, {}, 'Land Cover')"
- ],
- "id": "RRnATqO8uu1_"
+ ]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
- "tags": [],
- "id": "ft0fqSCKuu1_"
+ "tags": []
},
"outputs": [],
"source": [
"# Add legend to the map\n",
"Map.add_legend(title=\"Dynamic World Land Cover\", builtin_legend='Dynamic_World')\n",
"Map"
- ],
- "id": "ft0fqSCKuu1_"
+ ]
},
{
"cell_type": "markdown",
"metadata": {
- "id": "6rvNY7fWuu2A"
},
"source": [
"![](https://i.imgur.com/GEzsSii.png)"
- ],
- "id": "6rvNY7fWuu2A"
+ ]
},
{
"cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ },
+ "outputs": [],
"source": [
"# Save Dynamic World class data in GeoTIFF format\n",
"output_path = 'landcover.tif'\n",
"landcover = geemap.dynamic_world(region, start_date, end_date, return_type='class')\n",
"geemap.ee_export_image(landcover, filename=output_path, scale=10, region=region, file_per_band=False)"
- ],
- "metadata": {
- "id": "OxkTIXHluwRO"
- },
- "id": "OxkTIXHluwRO",
- "execution_count": null,
- "outputs": []
+ ]
}
],
"metadata": {
+ "colab": {
+ "provenance": []
+ },
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
- },
- "colab": {
- "provenance": []
}
},
"nbformat": 4,
"nbformat_minor": 5
-}
\ No newline at end of file
+}
diff --git a/examples/notebooks/141_image_array_viz.ipynb b/examples/notebooks/141_image_array_viz.ipynb
new file mode 100644
index 0000000000..b9e6d1e337
--- /dev/null
+++ b/examples/notebooks/141_image_array_viz.ipynb
@@ -0,0 +1,202 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "\n",
+ "**Visualizing in-memory raster datasets and image arrays**"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# !pip install -U geemap"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import ee\n",
+ "import geemap"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Use a Landsat image covering San Francisco Bay Area."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "m = geemap.Map()\n",
+ "image = ee.Image(\"LANDSAT/LC08/C02/T1_TOA/LC08_044034_20140318\").select(\n",
+ " ['B5', 'B4', 'B3']\n",
+ ")\n",
+ "vis_params = {\n",
+ " 'min': 0.0,\n",
+ " 'max': 0.4,\n",
+ "}\n",
+ "m.add_layer(image, vis_params, 'False Color (543)')\n",
+ "m.centerObject(image)\n",
+ "m"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Retrieve the projection information from the image."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "projection = image.select(0).projection().getInfo()\n",
+ "crs = projection['crs']\n",
+ "crs"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Read the Earth Engine image into an Xarray DataArray."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "ds = geemap.ee_to_xarray(\n",
+ " image, \n",
+ " crs=\"EPSG:32610\", \n",
+ " scale=300,\n",
+ " geometry=image.geometry(),\n",
+ " ee_mask_value=0\n",
+ ")\n",
+ "ds"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Save the DataArray as a Cloud Optimized GeoTIFF."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "geemap.xee_to_image(ds, filenames=['landsat.tif'])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Calculate the NDVI and save it to the DataArray."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "ndvi = (ds['B5'] - ds['B4']) / (ds['B5'] + ds['B4'])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Create an in-memory raster dataset from the DataArray."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "ndvi_image = geemap.array_to_image(ndvi, source=\"landsat.tif\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Visualize the in-memory raster dataset."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "m.add_raster('landsat.tif', band=[1, 2, 3], nodata=-1, layer_name=\"Landsat 7\")\n",
+ "m.add_raster(ndvi_image, cmap=\"Greens\", layer_name=\"NDVI\")\n",
+ "m"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Save the in-memory raster dataset to a GeoTIFF file."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "geemap.array_to_image(ndvi, output=\"ndvi.tif\", source=\"landsat.tif\")"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "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.5"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/examples/notebooks/83_local_tile.ipynb b/examples/notebooks/83_local_tile.ipynb
index a20fd8b04a..30dc5e95df 100644
--- a/examples/notebooks/83_local_tile.ipynb
+++ b/examples/notebooks/83_local_tile.ipynb
@@ -98,7 +98,7 @@
"metadata": {},
"outputs": [],
"source": [
- "m.add_local_tile(dem, palette='terrain', layer_name=\"DEM\")"
+ "m.add_raster(dem, palette='terrain', layer_name=\"DEM\")"
]
},
{
diff --git a/examples/notebooks/95_create_cog.ipynb b/examples/notebooks/95_create_cog.ipynb
index a090ea84cd..92ef456704 100644
--- a/examples/notebooks/95_create_cog.ipynb
+++ b/examples/notebooks/95_create_cog.ipynb
@@ -141,7 +141,7 @@
"outputs": [],
"source": [
"m = geemap.Map()\n",
- "m.add_local_tile(out_cog, palette=\"dem\", layer_name=\"Local COG\")\n",
+ "m.add_raster(out_cog, palette=\"dem\", layer_name=\"Local COG\")\n",
"m.add_cog_layer(url, palette=\"gist_earth\", name=\"Remote COG\")\n",
"m"
]
@@ -156,4 +156,4 @@
},
"nbformat": 4,
"nbformat_minor": 5
-}
\ No newline at end of file
+}
diff --git a/geemap/common.py b/geemap/common.py
index baaf9188ef..37dc320009 100644
--- a/geemap/common.py
+++ b/geemap/common.py
@@ -24,6 +24,7 @@
import ee
import ipywidgets as widgets
from ipytree import Node, Tree
+from typing import Union, List, Dict, Optional, Tuple
try:
from IPython.display import display, IFrame, Javascript
@@ -6932,8 +6933,7 @@ def zonal_stats(
out_file_path = os.path.join(os.getcwd(), "zonal_stats.csv")
if "statistics_type" in kwargs:
- stat_type = kwargs["statistics_type"]
- kwargs.pop("statistics_type")
+ stat_type = kwargs.pop("statistics_type")
allowed_formats = ["csv", "geojson", "kml", "kmz", "shp"]
filename = os.path.abspath(out_file_path)
@@ -7097,8 +7097,7 @@ def zonal_stats_by_group(
out_file_path = os.path.join(os.getcwd(), "zonal_stats_by_group.csv")
if "statistics_type" in kwargs:
- stat_type = kwargs["statistics_type"]
- kwargs.pop("statistics_type")
+ stat_type = kwargs.pop("statistics_type")
band_count = in_value_raster.bandNames().size().getInfo()
@@ -10758,14 +10757,15 @@ def get_local_tile_layer(
tile_format="ipyleaflet",
layer_name="Local COG",
return_client=False,
+ quiet=False,
**kwargs,
):
"""Generate an ipyleaflet/folium TileLayer from a local raster dataset or remote Cloud Optimized GeoTIFF (COG).
- If you are using this function in JupyterHub on a remote server (e.g., Binder, Microsoft Planetary Computer),
- try adding to following two lines to the beginning of the notebook if the raster does not render properly.
+ If you are using this function in JupyterHub on a remote server and the raster does not render properly, try
+ running the following two lines before calling this function:
import os
- os.environ['LOCALTILESERVER_CLIENT_PREFIX'] = f'{os.environ['JUPYTERHUB_SERVICE_PREFIX'].lstrip('/')}/proxy/{{port}}'
+ os.environ['LOCALTILESERVER_CLIENT_PREFIX'] = 'proxy/{port}'
Args:
source (str): The path to the GeoTIFF file or the URL of the Cloud Optimized GeoTIFF.
@@ -10781,31 +10781,52 @@ def get_local_tile_layer(
tile_format (str, optional): The tile layer format. Can be either ipyleaflet or folium. Defaults to "ipyleaflet".
layer_name (str, optional): The layer name to use. Defaults to None.
return_client (bool, optional): If True, the tile client will be returned. Defaults to False.
+ quiet (bool, optional): If True, the error messages will be suppressed. Defaults to False.
Returns:
ipyleaflet.TileLayer | folium.TileLayer: An ipyleaflet.TileLayer or folium.TileLayer.
"""
- warnings.filterwarnings("ignore")
+ from osgeo import gdal
+ import rasterio
- output = widgets.Output()
+ # ... and suppress errors
+ gdal.PushErrorHandler("CPLQuietErrorHandler")
check_package(
"localtileserver", URL="https://github.com/banesullivan/localtileserver"
)
+ if "max_zoom" not in kwargs:
+ kwargs["max_zoom"] = 100
+ if "max_native_zoom" not in kwargs:
+ kwargs["max_native_zoom"] = 100
+
# Make it compatible with binder and JupyterHub
if os.environ.get("JUPYTERHUB_SERVICE_PREFIX") is not None:
os.environ[
"LOCALTILESERVER_CLIENT_PREFIX"
] = f"{os.environ['JUPYTERHUB_SERVICE_PREFIX'].lstrip('/')}/proxy/{{port}}"
+ if is_studio_lab():
+ os.environ[
+ "LOCALTILESERVER_CLIENT_PREFIX"
+ ] = f"studiolab/default/jupyter/proxy/{{port}}"
+ elif is_on_aws():
+ os.environ["LOCALTILESERVER_CLIENT_PREFIX"] = "proxy/{port}"
+ elif "prefix" in kwargs:
+ os.environ["LOCALTILESERVER_CLIENT_PREFIX"] = kwargs["prefix"]
+ kwargs.pop("prefix")
+
from localtileserver import (
get_leaflet_tile_layer,
get_folium_tile_layer,
TileClient,
)
+ if "show_loading" not in kwargs:
+ kwargs["show_loading"] = False
+
if isinstance(source, str):
if not source.startswith("http"):
if source.startswith("~"):
@@ -10816,6 +10837,11 @@ def get_local_tile_layer(
raise ValueError("The source path does not exist.")
else:
source = github_raw_url(source)
+ elif isinstance(source, TileClient) or isinstance(
+ source, rasterio.io.DatasetReader
+ ):
+ pass
+
else:
raise ValueError("The source must either be a string or TileClient")
@@ -10831,15 +10857,48 @@ def get_local_tile_layer(
else:
layer_name = "LocalTile_" + random_string(3)
- if "cmap" not in kwargs:
- kwargs["cmap"] = palette
+ if isinstance(source, str) or isinstance(source, rasterio.io.DatasetReader):
+ tile_client = TileClient(source, port=port, debug=debug)
- if "alpha" in kwargs:
- kwargs["opacity"] = float(kwargs["alpha"])
+ else:
+ tile_client = source
- with output:
- tile_client = TileClient(source, port=port, debug=debug)
+ if "cmap" not in kwargs:
+ kwargs["cmap"] = palette
+ if quiet:
+ output = widgets.Output()
+ with output:
+ if tile_format == "ipyleaflet":
+ tile_layer = get_leaflet_tile_layer(
+ tile_client,
+ port=port,
+ debug=debug,
+ projection=projection,
+ band=band,
+ vmin=vmin,
+ vmax=vmax,
+ nodata=nodata,
+ attribution=attribution,
+ name=layer_name,
+ **kwargs,
+ )
+ else:
+ tile_layer = get_folium_tile_layer(
+ tile_client,
+ port=port,
+ debug=debug,
+ projection=projection,
+ band=band,
+ vmin=vmin,
+ vmax=vmax,
+ nodata=nodata,
+ attr=attribution,
+ overlay=True,
+ name=layer_name,
+ **kwargs,
+ )
+ else:
if tile_format == "ipyleaflet":
tile_layer = get_leaflet_tile_layer(
tile_client,
@@ -10852,8 +10911,6 @@ def get_local_tile_layer(
nodata=nodata,
attribution=attribution,
name=layer_name,
- max_zoom=30,
- max_native_zoom=30,
**kwargs,
)
else:
@@ -10869,8 +10926,6 @@ def get_local_tile_layer(
attr=attribution,
overlay=True,
name=layer_name,
- max_zoom=30,
- max_native_zoom=30,
**kwargs,
)
@@ -15681,3 +15736,398 @@ def geotiff_to_image(image: str, output: str) -> None:
# Save the image as a JPEG file
image.save(output)
+
+
+def xee_to_image(
+ xds,
+ filenames: Optional[Union[str, List[str]]] = None,
+ out_dir: Optional[str] = None,
+ crs: Optional[str] = None,
+ nodata: Optional[float] = None,
+ driver: str = "COG",
+ time_unit: str = "D",
+ quiet: bool = False,
+ **kwargs,
+) -> None:
+ """
+ Convert xarray Dataset to georeferenced images.
+
+ Args:
+ xds (xr.Dataset): The xarray Dataset to convert to images.
+ filenames (Union[str, List[str]], optional): Output filenames for the images.
+ If a single string is provided, it will be used as the filename for all images.
+ If a list of strings is provided, the filenames will be used in order. Defaults to None.
+ out_dir (str, optional): Output directory for the images. Defaults to current working directory.
+ crs (str, optional): Coordinate reference system (CRS) of the output images.
+ If not provided, the CRS is inferred from the Dataset's attributes ('crs' attribute) or set to 'EPSG:4326'.
+ nodata (float, optional): The nodata value used for the output images. Defaults to None.
+ driver (str, optional): Driver used for writing the output images, such as 'GTiff'. Defaults to "COG".
+ time_unit (str, optional): Time unit used for generating default filenames. Defaults to 'D'.
+ quiet (bool, optional): If True, suppresses progress messages. Defaults to False.
+ **kwargs: Additional keyword arguments passed to rioxarray's `rio.to_raster()` function.
+
+ Returns:
+ None
+
+ Raises:
+ ValueError: If the number of filenames doesn't match the number of time steps in the Dataset.
+
+ """
+ import numpy as np
+
+ try:
+ import rioxarray
+ except ImportError:
+ install_package("rioxarray")
+ import rioxarray
+
+ if crs is None and "crs" in xds.attrs:
+ crs = xds.attrs["crs"]
+ if crs is None:
+ crs = "EPSG:4326"
+
+ if out_dir is None:
+ out_dir = os.getcwd()
+
+ if not os.path.exists(out_dir):
+ os.makedirs(out_dir)
+
+ if isinstance(filenames, str):
+ filenames = [filenames]
+ if isinstance(filenames, list):
+ if len(filenames) != len(xds.time):
+ raise ValueError(
+ "The number of filenames must match the number of time steps"
+ )
+
+ coords = [coord for coord in xds.coords]
+ x_dim = coords[1]
+ y_dim = coords[2]
+
+ for index, time in enumerate(xds.time.values):
+ if nodata is not None:
+ # Create a Boolean mask where all three variables are zero (nodata)
+ mask = (xds == nodata).all(dim="time")
+ # Set nodata values based on the mask for all variables
+ xds = xds.where(~mask, other=np.nan)
+
+ if not quiet:
+ print(f"Processing {index + 1}/{len(xds.time.values)}: {time}")
+ image = xds.sel(time=time)
+ # transform the image to suit rioxarray format
+ image = (
+ image.rename({y_dim: "y", x_dim: "x"})
+ .transpose("y", "x")
+ .rio.write_crs(crs)
+ )
+
+ if filenames is None:
+ date = np.datetime_as_string(time, unit=time_unit)
+ filename = f"{date}.tif"
+ else:
+ filename = filenames.pop()
+
+ output_path = os.path.join(out_dir, filename)
+ image.rio.to_raster(output_path, driver=driver, **kwargs)
+
+
+def array_to_memory_file(
+ array,
+ source: str = None,
+ dtype: str = None,
+ compress: str = "deflate",
+ transpose: bool = True,
+ cellsize: float = None,
+ crs: str = None,
+ transform: tuple = None,
+ driver="COG",
+ **kwargs,
+):
+ """Convert a NumPy array to a memory file.
+
+ Args:
+ array (numpy.ndarray): The input NumPy array.
+ source (str, optional): Path to the source file to extract metadata from. Defaults to None.
+ dtype (str, optional): The desired data type of the array. Defaults to None.
+ compress (str, optional): The compression method for the output file. Defaults to "deflate".
+ transpose (bool, optional): Whether to transpose the array from (bands, rows, columns) to (rows, columns, bands). Defaults to True.
+ cellsize (float, optional): The cell size of the array if source is not provided. Defaults to None.
+ crs (str, optional): The coordinate reference system of the array if source is not provided. Defaults to None.
+ transform (tuple, optional): The affine transformation matrix if source is not provided. Defaults to None.
+ driver (str, optional): The driver to use for creating the output file, such as 'GTiff'. Defaults to "COG".
+ **kwargs: Additional keyword arguments to be passed to the rasterio.open() function.
+
+ Returns:
+ rasterio.DatasetReader: The rasterio dataset reader object for the converted array.
+ """
+ import rasterio
+ import numpy as np
+ import xarray as xr
+
+ if isinstance(array, xr.DataArray):
+ coords = [coord for coord in array.coords]
+ if coords[0] == "time":
+ x_dim = coords[1]
+ y_dim = coords[2]
+ array = (
+ array.isel(time=0).rename({y_dim: "y", x_dim: "x"}).transpose("y", "x")
+ )
+ array = array.values
+
+ if array.ndim == 3 and transpose:
+ array = np.transpose(array, (1, 2, 0))
+
+ if source is not None:
+ with rasterio.open(source) as src:
+ crs = src.crs
+ transform = src.transform
+ if compress is None:
+ compress = src.compression
+ else:
+ if cellsize is None:
+ raise ValueError("cellsize must be provided if source is not provided")
+ if crs is None:
+ raise ValueError(
+ "crs must be provided if source is not provided, such as EPSG:3857"
+ )
+
+ if "transform" not in kwargs:
+ # Define the geotransformation parameters
+ xmin, ymin, xmax, ymax = (
+ 0,
+ 0,
+ cellsize * array.shape[1],
+ cellsize * array.shape[0],
+ )
+ # (west, south, east, north, width, height)
+ transform = rasterio.transform.from_bounds(
+ xmin, ymin, xmax, ymax, array.shape[1], array.shape[0]
+ )
+ else:
+ transform = kwargs["transform"]
+
+ if dtype is None:
+ # Determine the minimum and maximum values in the array
+ min_value = np.min(array)
+ max_value = np.max(array)
+ # Determine the best dtype for the array
+ if min_value >= 0 and max_value <= 1:
+ dtype = np.float32
+ elif min_value >= 0 and max_value <= 255:
+ dtype = np.uint8
+ elif min_value >= -128 and max_value <= 127:
+ dtype = np.int8
+ elif min_value >= 0 and max_value <= 65535:
+ dtype = np.uint16
+ elif min_value >= -32768 and max_value <= 32767:
+ dtype = np.int16
+ else:
+ dtype = np.float64
+
+ # Convert the array to the best dtype
+ array = array.astype(dtype)
+
+ # Define the GeoTIFF metadata
+ metadata = {
+ "driver": driver,
+ "height": array.shape[0],
+ "width": array.shape[1],
+ "dtype": array.dtype,
+ "crs": crs,
+ "transform": transform,
+ }
+
+ if array.ndim == 2:
+ metadata["count"] = 1
+ elif array.ndim == 3:
+ metadata["count"] = array.shape[2]
+ if compress is not None:
+ metadata["compress"] = compress
+
+ metadata.update(**kwargs)
+
+ # Create a new memory file and write the array to it
+ memory_file = rasterio.MemoryFile()
+ dst = memory_file.open(**metadata)
+
+ if array.ndim == 2:
+ dst.write(array, 1)
+ elif array.ndim == 3:
+ for i in range(array.shape[2]):
+ dst.write(array[:, :, i], i + 1)
+
+ dst.close()
+
+ # Read the dataset from memory
+ dataset_reader = rasterio.open(dst.name, mode="r")
+
+ return dataset_reader
+
+
+def array_to_image(
+ array,
+ output: str = None,
+ source: str = None,
+ dtype: str = None,
+ compress: str = "deflate",
+ transpose: bool = True,
+ cellsize: float = None,
+ crs: str = None,
+ driver: str = "COG",
+ **kwargs,
+) -> str:
+ """Save a NumPy array as a GeoTIFF using the projection information from an existing GeoTIFF file.
+
+ Args:
+ array (np.ndarray): The NumPy array to be saved as a GeoTIFF.
+ output (str): The path to the output image. If None, a temporary file will be created. Defaults to None.
+ source (str, optional): The path to an existing GeoTIFF file with map projection information. Defaults to None.
+ dtype (np.dtype, optional): The data type of the output array. Defaults to None.
+ compress (str, optional): The compression method. Can be one of the following: "deflate", "lzw", "packbits", "jpeg". Defaults to "deflate".
+ transpose (bool, optional): Whether to transpose the array from (bands, rows, columns) to (rows, columns, bands). Defaults to True.
+ cellsize (float, optional): The resolution of the output image in meters. Defaults to None.
+ crs (str, optional): The CRS of the output image. Defaults to None.
+ driver (str, optional): The driver to use for creating the output file, such as 'GTiff'. Defaults to "COG".
+ **kwargs: Additional keyword arguments to be passed to the rasterio.open() function.
+ """
+
+ import numpy as np
+ import rasterio
+ import xarray as xr
+
+ if output is None:
+ return array_to_memory_file(
+ array, source, dtype, compress, transpose, cellsize, crs, driver, **kwargs
+ )
+
+ if isinstance(array, xr.DataArray):
+ coords = [coord for coord in array.coords]
+ if coords[0] == "time":
+ x_dim = coords[1]
+ y_dim = coords[2]
+ array = (
+ array.isel(time=0).rename({y_dim: "y", x_dim: "x"}).transpose("y", "x")
+ )
+ array = array.values
+
+ if array.ndim == 3 and transpose:
+ array = np.transpose(array, (1, 2, 0))
+
+ out_dir = os.path.dirname(os.path.abspath(output))
+ if not os.path.exists(out_dir):
+ os.makedirs(out_dir)
+
+ if not output.endswith(".tif"):
+ output += ".tif"
+
+ if source is not None:
+ with rasterio.open(source) as src:
+ crs = src.crs
+ transform = src.transform
+ if compress is None:
+ compress = src.compression
+ else:
+ if cellsize is None:
+ raise ValueError("resolution must be provided if source is not provided")
+ if crs is None:
+ raise ValueError(
+ "crs must be provided if source is not provided, such as EPSG:3857"
+ )
+
+ if "transform" not in kwargs:
+ # Define the geotransformation parameters
+ xmin, ymin, xmax, ymax = (
+ 0,
+ 0,
+ cellsize * array.shape[1],
+ cellsize * array.shape[0],
+ )
+ transform = rasterio.transform.from_bounds(
+ xmin, ymin, xmax, ymax, array.shape[1], array.shape[0]
+ )
+ else:
+ transform = kwargs["transform"]
+
+ if dtype is None:
+ # Determine the minimum and maximum values in the array
+ min_value = np.min(array)
+ max_value = np.max(array)
+ # Determine the best dtype for the array
+ if min_value >= 0 and max_value <= 1:
+ dtype = np.float32
+ elif min_value >= 0 and max_value <= 255:
+ dtype = np.uint8
+ elif min_value >= -128 and max_value <= 127:
+ dtype = np.int8
+ elif min_value >= 0 and max_value <= 65535:
+ dtype = np.uint16
+ elif min_value >= -32768 and max_value <= 32767:
+ dtype = np.int16
+ else:
+ dtype = np.float64
+
+ # Convert the array to the best dtype
+ array = array.astype(dtype)
+
+ # Define the GeoTIFF metadata
+ metadata = {
+ "driver": driver,
+ "height": array.shape[0],
+ "width": array.shape[1],
+ "dtype": array.dtype,
+ "crs": crs,
+ "transform": transform,
+ }
+
+ if array.ndim == 2:
+ metadata["count"] = 1
+ elif array.ndim == 3:
+ metadata["count"] = array.shape[2]
+ if compress is not None:
+ metadata["compress"] = compress
+
+ metadata.update(**kwargs)
+
+ # Create a new GeoTIFF file and write the array to it
+ with rasterio.open(output, "w", **metadata) as dst:
+ if array.ndim == 2:
+ dst.write(array, 1)
+ elif array.ndim == 3:
+ for i in range(array.shape[2]):
+ dst.write(array[:, :, i], i + 1)
+
+
+def is_studio_lab():
+ """Check if the current notebook is running on Studio Lab.
+
+ Returns:
+ bool: True if the notebook is running on Studio Lab.
+ """
+
+ import psutil
+
+ output = psutil.Process().parent().cmdline()
+
+ on_studio_lab = False
+ for item in output:
+ if "studiolab/bin" in item:
+ on_studio_lab = True
+ return on_studio_lab
+
+
+def is_on_aws():
+ """Check if the current notebook is running on AWS.
+
+ Returns:
+ bool: True if the notebook is running on AWS.
+ """
+
+ import psutil
+
+ output = psutil.Process().parent().cmdline()
+
+ on_aws = False
+ for item in output:
+ if item.endswith(".aws") or "ec2-user" in item:
+ on_aws = True
+ return on_aws
diff --git a/geemap/foliumap.py b/geemap/foliumap.py
index 0c235ea11c..b01cb5d935 100644
--- a/geemap/foliumap.py
+++ b/geemap/foliumap.py
@@ -614,7 +614,6 @@ def add_raster(
arc_add_layer(tile_layer.tiles, layer_name, True, 1.0)
arc_zoom_to_extent(bounds[0], bounds[1], bounds[2], bounds[3])
- add_local_tile = add_raster
def add_remote_tile(
self,
@@ -2409,7 +2408,7 @@ def add_netcdf(
else:
band_idx = [vars.index(v) + 1 for v in variables]
- self.add_local_tile(
+ self.add_raster(
tif,
band=band_idx,
palette=palette,
diff --git a/geemap/geemap.py b/geemap/geemap.py
index b1d5782009..0aefe299ce 100644
--- a/geemap/geemap.py
+++ b/geemap/geemap.py
@@ -2369,6 +2369,7 @@ def add_raster(
attribution=None,
layer_name="Local COG",
zoom_to_layer=True,
+ visible=True,
**kwargs,
):
"""Add a local raster dataset to the map.
@@ -2389,6 +2390,7 @@ def add_raster(
attribution (str, optional): Attribution for the source raster. This defaults to a message about it being a local file.. Defaults to None.
layer_name (str, optional): The layer name to use. Defaults to 'Local COG'.
zoom_to_layer (bool, optional): Whether to zoom to the extent of the layer. Defaults to True.
+ visible (bool, optional): Whether the layer is visible. Defaults to True.
"""
tile_layer, tile_client = get_local_tile_layer(
@@ -2403,6 +2405,7 @@ def add_raster(
return_client=True,
**kwargs,
)
+ tile_layer.visible = visible
self.add(tile_layer)
@@ -2433,8 +2436,6 @@ def add_raster(
}
self.cog_layer_dict[layer_name] = params
- add_local_tile = add_raster
-
def add_remote_tile(
self,
source,
diff --git a/geemap/toolbar.py b/geemap/toolbar.py
index 888489237b..f37b7dbdd1 100644
--- a/geemap/toolbar.py
+++ b/geemap/toolbar.py
@@ -1746,7 +1746,7 @@ def ok_cancel_clicked(change):
except Exception as _:
pass
- m.add_local_tile(
+ m.add_raster(
file_path,
layer_name=layer_name.value,
band=band,
@@ -4358,49 +4358,49 @@ def cleanup_and_toggle_off():
@_cleanup_toolbar_item
def _inspector_tool_callback(map, selected, item):
- del selected, item # Unused.
+ del selected, item # Unused.
map.add_inspector()
return map._inspector
@_cleanup_toolbar_item
def _plotting_tool_callback(map, selected, item):
- del selected, item # Unused.
+ del selected, item # Unused.
ee_plot_gui(map)
return map._plot_dropdown_control
@_cleanup_toolbar_item
def _timelapse_tool_callback(map, selected, item):
- del selected, item # Unused.
+ del selected, item # Unused.
timelapse_gui(map)
return map.tool_control
@_cleanup_toolbar_item
def _convert_js_tool_callback(map, selected, item):
- del selected, item # Unused.
+ del selected, item # Unused.
convert_js2py(map)
return map._convert_ctrl
@_cleanup_toolbar_item
def _basemap_tool_callback(map, selected, item):
- del selected, item # Unused.
+ del selected, item # Unused.
map.add_basemap_widget()
return map._basemap_selector
@_cleanup_toolbar_item
def _open_data_tool_callback(map, selected, item):
- del selected, item # Unused.
+ del selected, item # Unused.
open_data_widget(map)
return map._tool_output_ctrl
@_cleanup_toolbar_item
def _whitebox_tool_callback(map, selected, item):
- del selected, item # Unused.
+ del selected, item # Unused.
import whiteboxgui.whiteboxgui as wbt
tools_dict = wbt.get_wbt_dict()
@@ -4419,7 +4419,7 @@ def _whitebox_tool_callback(map, selected, item):
@_cleanup_toolbar_item
def _gee_toolbox_tool_callback(map, selected, item):
- del selected, item # Unused.
+ del selected, item # Unused.
tools_dict = get_tools_dict()
gee_toolbox = build_toolbox(tools_dict, max_width="800px", max_height="500px")
geetoolbox_control = ipyleaflet.WidgetControl(
@@ -4432,35 +4432,35 @@ def _gee_toolbox_tool_callback(map, selected, item):
@_cleanup_toolbar_item
def _time_slider_tool_callback(map, selected, item):
- del selected, item # Unused.
+ del selected, item # Unused.
time_slider(map)
return map.tool_control
@_cleanup_toolbar_item
def _collect_samples_tool_callback(map, selected, item):
- del selected, item # Unused.
+ del selected, item # Unused.
collect_samples(map)
return map.training_ctrl
@_cleanup_toolbar_item
def _plot_transect_tool_callback(map, selected, item):
- del selected, item # Unused.
+ del selected, item # Unused.
plot_transect(map)
return map.tool_control
@_cleanup_toolbar_item
def _sankee_tool_callback(map, selected, item):
- del selected, item # Unused.
+ del selected, item # Unused.
sankee_gui(map)
return map.tool_control
@_cleanup_toolbar_item
def _cog_stac_inspector_callback(map, selected, item):
- del selected, item # Unused.
+ del selected, item # Unused.
inspector_gui(map)
return map.tool_control
diff --git a/mkdocs.yml b/mkdocs.yml
index 4967327571..0cdffa4d66 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -284,6 +284,7 @@ nav:
- notebooks/138_draw_control.ipynb
- notebooks/139_layer_to_image.ipynb
- notebooks/140_ee_to_xarray.ipynb
+ - notebooks/141_image_array_viz.ipynb
# - miscellaneous:
# - notebooks/cartoee_colab.ipynb
# - notebooks/cartoee_colorbar.ipynb