Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
363 changes: 363 additions & 0 deletions docs/notebooks/117_urban_heat_island.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,363 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Urban Heat Island Mapping with MODIS Land Surface Temperature\n",
"\n",
"[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/leafmap/blob/master/docs/notebooks/117_urban_heat_island.ipynb)\n",
"\n",
"This notebook demonstrates how to visualize and analyze **Urban Heat Island (UHI)** effects using MODIS MOD11A2 Land Surface Temperature (LST) data and [leafmap](https://leafmap.org).\n",
"\n",
"The Urban Heat Island effect describes how urban areas exhibit significantly higher temperatures than surrounding rural land due to human activity, impervious surfaces, and reduced vegetation. LST derived from thermal infrared satellite sensors is one of the most widely used proxies for quantifying UHI intensity.\n",
"\n",
"**Outline:**\n",
"- Install and import dependencies\n",
"- Download MODIS MOD11A2 LST data via `earthaccess`\n",
"- Load and preprocess the LST GeoTIFF\n",
"- Clip to the study area, apply the scale factor, and mask fill/low-quality pixels\n",
"- Visualize LST on an interactive leafmap\n",
"- Compute a split map comparing urban vs. rural thermal patterns\n",
"- Export results"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Install dependencies\n",
"\n",
"Uncomment if running in Google Colab or a fresh environment."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# %pip install leafmap earthaccess rioxarray matplotlib"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Repository files matching notebook:\n'
fd -a '117_urban_heat_island.ipynb' . || true

printf '\nOutline/stat for notebook:\n'
if [ -f docs/notebooks/117_urban_heat_island.ipynb ]; then
  wc -l docs/notebooks/117_urban_heat_island.ipynb
  python3 - <<'PY'
import json, pathlib
p=pathlib.Path('docs/notebooks/117_urban_heat_island.ipynb')
nb=json.loads(p.read_text())
for i, cell in enumerate(nb['cells'],1):
    source=''.join(cell.get('source',[]))[:2000]
    if any(k in source for k in ['%pip install','add_raster','split_map','open_rasterio','earthaccess','leafmap','leafmap']):
        print(f'\nCell {i} type={cell["cell_type"]}')
        lines=source.splitlines()
        for j,l in enumerate(lines):
            if any(s in l for s in ['%pip install','add_raster','split_map','open_rasterio','rxr.open_rasterio','earthaccess','leafmap']):
                print(f'{j+1}: {l}')
PY
fi

printf '\nSearch relevant symbols in docs/notebooks:\n'
rg -n --glob '*.ipynb' 'add_raster|split_map|open_rasterio|rxr\.open_rasterio|%pip install|leafmap\[raster\]|localtileserver|earthaccess|rioxarray' docs/notebooks || true

printf '\nCheck installed package metadata availability if present:\n'
python3 - <<'PY'
import importlib.util, importlib.metadata, sys
for name in ['leafmap','localtileserver','rasterio','rioxarray']:
    spec=importlib.util.find_spec(name)
    print(f"{name}: spec={bool(spec)}", end='')
    if spec:
        print(f" origin={spec.origin}")
        try:
            md=importlib.metadata.metadata(name)
        except importlib.metadata.PackageNotFoundError as e:
            md=None
            print(f" metadata_missing={e}")
        else:
            print(f" requires_dist={md.get('Requires-Dist','')}")
    else:
        print()
PY

Repository: opengeos/leafmap

Length of output: 18671


🌐 Web query:

leafmap raster extra localtileserver rioxarray GDAL HDF4 driver pip wheels

💡 Result:

Leafmap, localtileserver, and rioxarray are common tools in the Python geospatial stack. Their relationship and requirements regarding raster data and GDAL drivers are as follows: Leafmap and Optional Dependencies: Leafmap provides extra install targets to manage its dependencies based on use case [1][2]. For raster data support, you can use: pip install "leafmap[raster]" This installs essential raster-handling libraries, including localtileserver, rioxarray, and relevant NetCDF/HDF5 support packages (e.g., netcdf4, h5netcdf, h5py) [2][3]. localtileserver and rioxarray: localtileserver uses rioxarray to enable visualization of multi-dimensional datasets like NetCDF, Zarr, and HDF5 [4][5]. You can explicitly install these dependencies for localtileserver via: pip install localtileserver[xarray] This ensures that rioxarray and xarray are available, allowing the server to handle data formats that rioxarray supports [4][5]. GDAL HDF4 Driver and Pip Wheels: The HDF4 format is a legacy data format [6][7]. The standard GDAL Python bindings available on PyPI (the gdal package) do not ship as pre-compiled "manylinux" or "macosx" wheels that bundle all native libraries; instead, they are source distributions that require a system-level installation of libgdal and its development headers (e.g., libgdal-dev) [8][9][10]. Because of this, whether the HDF4 driver is available depends on how your local system's GDAL library was compiled [6][10]. If you face difficulties with GDAL dependencies or specific driver support (like HDF4) via pip, the recommended approach is to use conda/mamba, which provides pre-built binaries with comprehensive driver support through the conda-forge channel [1][11]: mamba create -n geo leafmap geopandas localtileserver python -c conda-forge Alternatively, some newer community-driven packages (e.g., pyramids-gis) offer self-contained wheels that bundle GDAL/HDF4 native libraries on specific platforms to simplify installation, though these are independent of the standard GDAL pip package [7].

Citations:


Install the raster dependencies before using local rasters.

Uncommenting this command installs bare leafmap, missing localtileserver; later cells call m.add_raster and m.split_map with local GeoTIFFs. Use leafmap[raster], e.g. "leafmap[raster]", since that extra includes the required raster backend dependencies. Also ensure GDAL has the HDF4 driver available for rxr.open_rasterio, because standard pip GDAL/Rasterio wheels do not guarantee all native drivers.

Proposed setup fix
-    "# %pip install leafmap earthaccess rioxarray matplotlib"
+    "# %pip install \"leafmap[raster]\" earthaccess rioxarray matplotlib"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"# %pip install leafmap earthaccess rioxarray matplotlib"
"# %pip install \"leafmap[raster]\" earthaccess rioxarray matplotlib"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/notebooks/117_urban_heat_island.ipynb` at line 40, Update the notebook’s
commented dependency-install command to use the raster-enabled leafmap extra
(`leafmap[raster]`) instead of bare `leafmap`, and ensure the setup documents or
provisions GDAL with HDF4 support for `rxr.open_rasterio` before the local
raster cells use `m.add_raster` and `m.split_map`.

]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Import libraries"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"import rioxarray as rxr\n",
"import earthaccess\n",
"import leafmap"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Authenticate with NASA Earthdata\n",
"\n",
"A free NASA Earthdata account is required. Register at [urs.earthdata.nasa.gov](https://urs.earthdata.nasa.gov). Credentials are stored in `~/.netrc` after first login."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"earthaccess.login(persist=True)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Search for MODIS MOD11A2 LST data\n",
"\n",
"MOD11A2 provides 8-day composite Land Surface Temperature at 1 km resolution. We search for a summer granule over Lahore, Pakistan — one of South Asia's most prominent Urban Heat Island cities."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": "# Lahore, Pakistan bounding box [west, south, east, north]\nbbox = (73.8, 31.3, 74.6, 31.7)\n\nresults = earthaccess.search_data(\n short_name=\"MOD11A2\",\n version=\"061\",\n bounding_box=bbox,\n temporal=(\"2023-06-01\", \"2023-06-30\"),\n)\n\nprint(f\"Found {len(results)} granule(s)\")\nif not results:\n raise ValueError(\n \"No granules found — check your Earthdata credentials and the search parameters.\"\n )\nresults[0]"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Download the LST granule"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"files = earthaccess.download(results[:1], local_path=\"modis_lst\")\n",
"# earthaccess may return sidecar files (e.g. .hdf.xml) alongside the data file,\n",
"# and the ordering is not guaranteed, so select the HDF explicitly.\n",
"hdf_path = next((f for f in files if str(f).endswith(\".hdf\")), None)\n",
"if hdf_path is None:\n",
" raise ValueError(\"Download failed — no HDF file returned by earthaccess.\")\n",
"print(f\"Downloaded: {hdf_path}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Load and preprocess LST\n",
"\n",
"A MOD11A2 granule covers an entire MODIS tile (~1200 km across), so we first clip it to the Lahore bounding box — otherwise the scene statistics below would describe the whole tile rather than the city.\n",
"\n",
"MOD11A2 stores LST as scaled integers. The LST Day layer (`LST_Day_1km`) must be multiplied by **0.02** to convert to Kelvin, then subtract 273.15 for Celsius. Fill value **0** is masked out, and the `QC_Day` layer is used to drop cloud-contaminated and other low-quality retrievals."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"grid = f'HDF4_EOS:EOS_GRID:\"{hdf_path}\":MODIS_Grid_8Day_1km_LST:'\n",
"\n",
"# Open the daytime LST subdataset and its quality layer\n",
"lst_raw = rxr.open_rasterio(grid + \"LST_Day_1km\", masked=True).squeeze()\n",
"qc_day = rxr.open_rasterio(grid + \"QC_Day\").squeeze()\n",
"\n",
"# Clip the granule to the Lahore bounding box before computing any statistics\n",
"lst_raw = lst_raw.rio.clip_box(*bbox, crs=\"EPSG:4326\")\n",
"qc_day = qc_day.rio.clip_box(*bbox, crs=\"EPSG:4326\")\n",
"\n",
"# QC_Day bits 0-1 are the mandatory QA flags; 00 means good-quality LST was\n",
"# produced. Anything else is cloud-contaminated or otherwise unreliable.\n",
"good_quality = (qc_day & 0b11) == 0\n",
"\n",
"# Apply scale factor and convert to Celsius\n",
"lst_kelvin = lst_raw.where((lst_raw != 0) & good_quality) * 0.02\n",
"lst_celsius = lst_kelvin - 273.15\n",
"\n",
"print(\n",
" f\"LST range: {float(lst_celsius.min()):.1f} °C to {float(lst_celsius.max()):.1f} °C\"\n",
")\n",
"lst_celsius"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Quick static plot\n",
"\n",
"A preliminary view before loading into leafmap."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig, ax = plt.subplots(figsize=(10, 7))\n",
"lst_celsius.plot(\n",
" ax=ax,\n",
" cmap=\"RdYlBu_r\",\n",
" robust=True,\n",
" cbar_kwargs={\"label\": \"Land Surface Temperature (°C)\"},\n",
")\n",
"ax.set_title(\"MODIS MOD11A2 — Daytime LST, Lahore, June 2023\", fontsize=13)\n",
"ax.set_xlabel(\"Longitude\")\n",
"ax.set_ylabel(\"Latitude\")\n",
"plt.tight_layout()\n",
"plt.savefig(\"lahore_lst.png\", dpi=150)\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Export preprocessed LST as GeoTIFF"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"lst_output = \"lahore_lst_celsius.tif\"\n",
"lst_celsius.rio.to_raster(lst_output)\n",
"print(f\"Saved: {lst_output}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Visualize on an interactive leafmap\n",
"\n",
"Load the LST raster with a diverging colormap. Warmer colours (red/orange) indicate urban heat pockets; cooler blues indicate vegetated or rural land."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"m = leafmap.Map(center=[31.5, 74.2], zoom=10)\n",
"m.add_basemap(\"OpenStreetMap\")\n",
"\n",
"m.add_raster(\n",
" lst_output,\n",
" colormap=\"RdYlBu_r\",\n",
" vmin=20,\n",
" vmax=50,\n",
" layer_name=\"LST Day (°C)\",\n",
" opacity=0.75,\n",
")\n",
"\n",
"# RdYlBu_r: blue (cool) → yellow → red (hot)\n",
"lst_colors = [\"313695\", \"74ADD1\", \"E0F3F8\", \"FEE090\", \"F46D43\", \"D73027\"]\n",
"m.add_colorbar(colors=lst_colors, vmin=20, vmax=50)\n",
"\n",
"m"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Split map — LST vs. satellite\n",
"\n",
"Swipe between the LST raster and the satellite basemap to correlate high-temperature zones with built-up surfaces."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"m2 = leafmap.Map(center=[31.5, 74.2], zoom=10)\n",
"m2.split_map(\n",
" left_layer=lst_output,\n",
" right_layer=\"SATELLITE\",\n",
" left_args={\"colormap\": \"RdYlBu_r\", \"vmin\": 20, \"vmax\": 50, \"opacity\": 0.9},\n",
" left_label=\"LST Day (°C)\",\n",
" right_label=\"Satellite\",\n",
")\n",
"m2"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Compute UHI intensity\n",
"\n",
"A simple UHI index: difference between each pixel and the mean LST of the study area. Positive values = warmer than average (urban heat zones)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"scene_mean = float(lst_celsius.mean())\n",
"uhi_index = lst_celsius - scene_mean\n",
"\n",
"uhi_output = \"lahore_uhi_index.tif\"\n",
"uhi_index.rio.to_raster(uhi_output)\n",
"\n",
"print(f\"Scene mean LST: {scene_mean:.2f} °C\")\n",
"print(f\"Max UHI intensity: +{float(uhi_index.max()):.2f} °C\")\n",
"print(f\"Min UHI intensity: {float(uhi_index.min()):.2f} °C\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Visualize UHI index"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"m3 = leafmap.Map(center=[31.5, 74.2], zoom=10)\n",
"m3.add_basemap(\"SATELLITE\")\n",
"\n",
"m3.add_raster(\n",
" uhi_output,\n",
" colormap=\"seismic\",\n",
" vmin=-5,\n",
" vmax=5,\n",
" layer_name=\"UHI Index (°C above mean)\",\n",
" opacity=0.75,\n",
")\n",
"\n",
"# seismic: blue (below mean) → white → red (above mean / urban heat)\n",
"uhi_colors = [\"3333CC\", \"AAAAFF\", \"FFFFFF\", \"FFAAAA\", \"CC3333\"]\n",
"m3.add_colorbar(colors=uhi_colors, vmin=-5, vmax=5)\n",
"\n",
"m3"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Summary\n",
"\n",
"This notebook demonstrated how to:\n",
"\n",
"1. Search and download MODIS MOD11A2 LST data using `earthaccess`\n",
"2. Apply scale factors and unit conversions with `rioxarray`\n",
"3. Visualize LST interactively with `leafmap` and a diverging colormap\n",
"4. Build a split map to compare LST against basemap layers\n",
"5. Compute a simple Urban Heat Island intensity index\n",
"\n",
"The workflow is applicable to any city worldwide — change `bbox` and `temporal` to explore different regions and seasons.\n",
"\n",
"## References\n",
"\n",
"- [MODIS MOD11A2 Product Page](https://lpdaac.usgs.gov/products/mod11a2v061/) — LP DAAC\n",
"- [earthaccess](https://earthaccess.readthedocs.io/) — NASA Earthdata Python library\n",
"- [leafmap](https://leafmap.org/) — Wu, 2021\n",
"- [rioxarray](https://corteva.github.io/rioxarray/)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.10.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
1 change: 1 addition & 0 deletions zensical.toml
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ nav = [
{ "114_nasa_fire" = "notebooks/114_nasa_fire.md" },
{ "115_terrascope" = "notebooks/115_terrascope.md" },
{ "116_hls_nasa_earthdata" = "notebooks/116_hls_nasa_earthdata.md" },
{ "117_urban_heat_island" = "notebooks/117_urban_heat_island.md" },
] },
]

Expand Down
Loading