ICRW 2023
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.
- Leafmap: https://leafmap.org
- WhiteboxTools: https://www.whiteboxgeo.com
- WhiteboxTools User Manual: https://www.whiteboxgeo.com/manual/wbt_book
Installation¶
Uncomment and run the following cell to install necessary packages for this notebook, including leafmap, geopandas, localtileserver, rio-cogeo, pynhd, py3dep.
# %pip install leafmap[raster] geopandas
Import libraries¶
import os
import leafmap
Create interactive maps¶
Specify the map center, zoom level, and height.
m = leafmap.Map(center=[40, -100], zoom=4, height="600px")
m
Add basemaps¶
Add OpenTopoMap, USGS 3DEP Elevation, and USGS Hydrography basemaps.
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.
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.
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.
gdf = leafmap.get_nhd_basins(feature_ids=23763529, fsource="comid", simplified=False)
Plot the watershed boundary on the map.
m = leafmap.Map()
m.add_gdf(gdf, layer_name="Catchment", info_mode=None)
m
Save the watershed boundary to a GeoJSON or shapefile.
gdf.to_file("basin.geojson", driver="GeoJSON")
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).
leafmap.get_3dep_dem(
gdf, resolution=30, output="dem.tif", dst_crs="EPSG:3857", to_cog=True
)
Display the DEM on the map.
m.add_raster("dem.tif", palette="terrain", layer_name="DEM")
m
Get DEM metadata¶
metadata = leafmap.image_metadata("dem.tif")
metadata
Get a summary statistics of the DEM.
metadata["bands"]
Add colorbar¶
m.add_colormap(cmap="terrain", vmin="60", vmax=1500, label="Elevation (m)")
m
Initialize WhiteboxTools¶
Initialize the WhiteboxTools class.
wbt = leafmap.WhiteboxTools()
Check the WhiteboxTools version.
wbt.version()
Display the WhiteboxTools interface.
leafmap.whiteboxgui()
Set working directory¶
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.
wbt.feature_preserving_smoothing("dem.tif", "smoothed.tif", filter=9)
Display the smoothed DEM and watershed boundary on the map.
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¶
wbt.hillshade("smoothed.tif", "hillshade.tif", azimuth=315, altitude=35)
Overlay the hillshade on the smoothed DEM with transparency.
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
wbt.find_no_flow_cells("smoothed.tif", "noflow.tif")
Display the no-flow cells on the map.
m.add_raster("noflow.tif", layer_name="No Flow Cells")
m
Fill depressions¶
wbt.fill_depressions("smoothed.tif", "filled.tif")
Alternatively, you can use depression breaching to fill the depressions.
wbt.breach_depressions("smoothed.tif", "breached.tif")
wbt.find_no_flow_cells("breached.tif", "noflow2.tif")
m.add_raster("noflow2.tif", layer_name="No Flow Cells after Breaching")
m
Delineate flow direction¶
wbt.d8_pointer("breached.tif", "flow_direction.tif")
Calculate flow accumulation¶
wbt.d8_flow_accumulation("breached.tif", "flow_accum.tif")
m.add_raster("flow_accum.tif", layer_name="Flow Accumulation")
m
Extract streams¶
wbt.extract_streams("flow_accum.tif", "streams.tif", threshold=5000)
m.add_raster("streams.tif", layer_name="Streams")
Calculate distance to outlet¶
wbt.distance_to_outlet(
"flow_direction.tif", streams="streams.tif", output="distance_to_outlet.tif"
)
m.add_raster("distance_to_outlet.tif", layer_name="Distance to Outlet")
Vectorize streams¶
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.
leafmap.vector_set_crs(source="streams.shp", output="streams.shp", crs="EPSG:3857")
m.add_shp(
"streams.shp", layer_name="Streams Vector", style={"color": "#ff0000", "weight": 3}
)
m
Delineate basins¶
wbt.basins("flow_direction.tif", "basins.tif")
m.add_raster("basins.tif", layer_name="Basins")
Delineate the longest flow path¶
wbt.longest_flowpath(
dem="breached.tif", basins="basins.tif", output="longest_flowpath.shp"
)
Select only the longest flow path.
leafmap.select_largest(
"longest_flowpath.shp", column="LENGTH", output="longest_flowpath.shp"
)
m.add_shp(
"longest_flowpath.shp",
layer_name="Longest Flowpath",
style={"color": "#ff0000", "weight": 3},
)
m
Generate a pour point¶
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¶
wbt.snap_pour_points(
"pour_point.shp", "flow_accum.tif", "pour_point_snapped.shp", snap_dist=300
)
m.add_shp("pour_point_snapped.shp", layer_name="Pour Point")
Delineate watershed¶
wbt.watershed("flow_direction.tif", "pour_point_snapped.shp", "watershed.tif")
m.add_raster("watershed.tif", layer_name="Watershed")
m
Convert watershed raster to vector¶
wbt.raster_to_vector_polygons("watershed.tif", "watershed.shp")
m.add_shp(
"watershed.shp",
layer_name="Watershed Vector",
style={"color": "#ffff00", "weight": 3},
)