Skip to content

ICRW 2023

image image

An Introduction to Watershed Analysis with Leafmap and WhiteboxTools

This notebook provides an introduction to watershed analysis with Leafmap and WhiteboxTools. It is designed for The Interagency Conference on Research in the Watersheds (ICRW) 2023 workshop - Working with Geospatial Hydrologic Data for Watershed Analyses in R and Python Using Web Services.

Installation

Uncomment and run the following cell to install necessary packages for this notebook, including leafmap, geopandas, localtileserver, rio-cogeo, pynhd, py3dep.

1
# %pip install leafmap[raster] geopandas

Import libraries

1
2
import os
import leafmap

Create interactive maps

Specify the map center, zoom level, and height.

1
2
m = leafmap.Map(center=[40, -100], zoom=4, height="600px")
m

Add basemaps

Add OpenTopoMap, USGS 3DEP Elevation, and USGS Hydrography basemaps.

1
2
3
4
5
m = leafmap.Map()
m.add_basemap("OpenTopoMap")
m.add_basemap("USGS 3DEP Elevation")
m.add_basemap("USGS Hydrography")
m

Add NLCD land cover map and legend.

1
2
3
4
5
m = leafmap.Map(center=[40, -100], zoom=4)
m.add_basemap("HYBRID")
m.add_basemap("NLCD 2019 CONUS Land Cover")
m.add_legend(builtin_legend="NLCD", title="NLCD Land Cover Type")
m

Add WMS layers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
m = leafmap.Map(center=[40, -100], zoom=4)
m.add_basemap("Esri.WorldImagery")
url = "https://www.mrlc.gov/geoserver/mrlc_display/NLCD_2019_Land_Cover_L48/wms?"
m.add_wms_layer(
    url,
    layers="NLCD_2019_Land_Cover_L48",
    name="NLCD 2019 CONUS Land Cover",
    format="image/png",
    transparent=True,
)
m.add_legend(builtin_legend="NLCD", title="NLCD Land Cover Type")
m

Get watershed data

Let's download watershed data for the Calapooia River basin in Oregon.

1
gdf = leafmap.get_nhd_basins(feature_ids=23763529, fsource="comid", simplified=False)

Plot the watershed boundary on the map.

1
2
3
m = leafmap.Map()
m.add_gdf(gdf, layer_name="Catchment", info_mode=None)
m

Save the watershed boundary to a GeoJSON or shapefile.

1
gdf.to_file("basin.geojson", driver="GeoJSON")
1
gdf.to_file("basin.shp")

Download DEM

Download a digital elevation model (DEM) for the watershed from the USGS 3DEP Elevation service. Convert the DEM to a Cloud Optimized GeoTIFF (COG).

1
2
3
leafmap.get_3dep_dem(
    gdf, resolution=30, output="dem.tif", dst_crs="EPSG:3857", to_cog=True
)

Display the DEM on the map.

1
2
m.add_raster("dem.tif", palette="terrain", layer_name="DEM")
m

Get DEM metadata

1
2
metadata = leafmap.image_metadata("dem.tif")
metadata

Get a summary statistics of the DEM.

1
metadata["bands"]

Add colorbar

1
2
m.add_colormap(cmap="terrain", vmin="60", vmax=1500, label="Elevation (m)")
m

Initialize WhiteboxTools

Initialize the WhiteboxTools class.

1
wbt = leafmap.WhiteboxTools()

Check the WhiteboxTools version.

1
wbt.version()

Display the WhiteboxTools interface.

1
leafmap.whiteboxgui()

Set working directory

1
2
wbt.set_working_dir(os.getcwd())
wbt.verbose = False

Smooth DEM

All WhiteboxTools functions will return 0 if they are successful, and 1 if they are not.

1
wbt.feature_preserving_smoothing("dem.tif", "smoothed.tif", filter=9)

Display the smoothed DEM and watershed boundary on the map.

1
2
3
4
m = leafmap.Map()
m.add_raster("smoothed.tif", palette="terrain", layer_name="Smoothed DEM")
m.add_geojson("basin.geojson", layer_name="Watershed", info_mode=None)
m

Create hillshade

1
wbt.hillshade("smoothed.tif", "hillshade.tif", azimuth=315, altitude=35)

Overlay the hillshade on the smoothed DEM with transparency.

1
2
m.add_raster("hillshade.tif", layer_name="Hillshade")
m.layers[-1].opacity = 0.6

Find no-flow cells

Find cells with undefined flow, i.e. no valid flow direction, based on the D8 flow direction algorithm

1
wbt.find_no_flow_cells("smoothed.tif", "noflow.tif")

Display the no-flow cells on the map.

1
2
m.add_raster("noflow.tif", layer_name="No Flow Cells")
m

Fill depressions

1
wbt.fill_depressions("smoothed.tif", "filled.tif")

Alternatively, you can use depression breaching to fill the depressions.

1
wbt.breach_depressions("smoothed.tif", "breached.tif")
1
wbt.find_no_flow_cells("breached.tif", "noflow2.tif")
1
2
m.add_raster("noflow2.tif", layer_name="No Flow Cells after Breaching")
m

Delineate flow direction

1
wbt.d8_pointer("breached.tif", "flow_direction.tif")

Calculate flow accumulation

1
wbt.d8_flow_accumulation("breached.tif", "flow_accum.tif")
1
2
m.add_raster("flow_accum.tif", layer_name="Flow Accumulation")
m

Extract streams

1
wbt.extract_streams("flow_accum.tif", "streams.tif", threshold=5000)
1
m.add_raster("streams.tif", layer_name="Streams")

Calculate distance to outlet

1
2
3
wbt.distance_to_outlet(
    "flow_direction.tif", streams="streams.tif", output="distance_to_outlet.tif"
)
1
m.add_raster("distance_to_outlet.tif", layer_name="Distance to Outlet")

Vectorize streams

1
2
3
wbt.raster_streams_to_vector(
    "streams.tif", d8_pntr="flow_direction.tif", output="streams.shp"
)

The raster_streams_to_vector tool has a bug. The output vector file is missing the coordinate system. Use leafmap.vector_set_crs() to set the coordinate system.

1
leafmap.vector_set_crs(source="streams.shp", output="streams.shp", crs="EPSG:3857")
1
2
3
4
m.add_shp(
    "streams.shp", layer_name="Streams Vector", style={"color": "#ff0000", "weight": 3}
)
m

Delineate basins

1
wbt.basins("flow_direction.tif", "basins.tif")
1
m.add_raster("basins.tif", layer_name="Basins")

Delineate the longest flow path

1
2
3
wbt.longest_flowpath(
    dem="breached.tif", basins="basins.tif", output="longest_flowpath.shp"
)

Select only the longest flow path.

1
2
3
leafmap.select_largest(
    "longest_flowpath.shp", column="LENGTH", output="longest_flowpath.shp"
)
1
2
3
4
5
6
m.add_shp(
    "longest_flowpath.shp",
    layer_name="Longest Flowpath",
    style={"color": "#ff0000", "weight": 3},
)
m

Generate a pour point

1
2
3
4
5
if m.user_roi is not None:
    m.save_draw_features("pour_point.shp", crs="EPSG:3857")
else:
    coords = [-122.613559, 44.284383]
    leafmap.coords_to_vector(coords, output="pour_point.shp", crs="EPSG:3857")

Snap pour point to stream

1
2
3
wbt.snap_pour_points(
    "pour_point.shp", "flow_accum.tif", "pour_point_snapped.shp", snap_dist=300
)
1
m.add_shp("pour_point_snapped.shp", layer_name="Pour Point")

Delineate watershed

1
wbt.watershed("flow_direction.tif", "pour_point_snapped.shp", "watershed.tif")
1
2
m.add_raster("watershed.tif", layer_name="Watershed")
m

Convert watershed raster to vector

1
wbt.raster_to_vector_polygons("watershed.tif", "watershed.shp")
1
2
3
4
5
m.add_shp(
    "watershed.shp",
    layer_name="Watershed Vector",
    style={"color": "#ffff00", "weight": 3},
)