Watershed Analysis

Jade Dormoy-Boulanger

February 2026

By the end of this workshop, you will be able to delineate a watershed using QGIS and extract land use data from it using R.

You will need this land use map (from Agriculture and Agri-Food Canada’s crop inventory) -> https://ouvert.canada.ca/data/fr/dataset/32546f7b-55c2-481e-b300-83fc16054b95

You will also need to create a free account and obtain an API key from OpenTopography -> https://opentopography.org/blog/introducing-api-keys-access-opentopography-global-datasets

To make sure you get the most out of this workshop, let’s start with a few theoretical concepts:

Theoretical Background

In this workshop, we will be working with two types of GIS (Geographic Information System) objects: vectors and rasters.

A vector is a geometric object defined by points and lines, based on mathematical coordinates. It is generally used to represent linear or bounded features, such as roads or boundaries. In our case, the watershed we will generate will be in vector format.

A raster, on the other hand, is essentially an image: a grid of pixels, where each pixel contains a piece of information. We will be using a land use map from Agriculture and Agri-Food Canada’s crop inventory, in which each pixel colour corresponds to a particular type of activity.

By overlaying these different GIS layers, it becomes possible to reconstruct a landscape and analyze the interactions between them, for example, between the boundaries of a watershed and its land use.

Representation of the different types of spatial layers by ESRI (https://resources.arcgis.com/fr/help/getting-started/articles/026n0000000p000000.htm)

Additionally, every GIS object is associated with a georeferencing system. In practice, each geographic coordinate (latitude, longitude) is defined relative to a Spatial Reference System (SRS), which makes it possible to locate objects on a representation of the Earth, whether that’s a flat grid like a map, or an ellipsoid like a globe.

SRS differ notably in their unit of measurement: some express coordinates in meters (like the UTM system), others in degrees (like WGS84, also known as EPSG 4326). Since we won’t be calculating distances in this workshop, we will use the EPSG 4326 system.

Two types of projections, Mercator (top) and Robinson (bottom). (https://futuremaps.com/blogs/news/top-10-world-map-projections)

QGIS

1 - Loading a Basemap

We will work with a Google Maps basemap. To do so, you need to install the QuickMapServices plugin:

Then, display the Google Maps basemap:

Web tab -> QuickMapServices -> Google -> Google Satellite

2 - Setting the SRS

We will change the SRS to EPSG 4326

3 - Downloading Elevation Data (DEM)

To calculate a watershed, you need elevation data for the area of interest. This data is available through SRTM satellites:

Install the SRTM-Downloader plugin

Plugins tab -> SRTM-Downloader

Click on Set canvas extent to define the dimensions of the elevation data, and enter the API key obtained from OpenTopography at the beginning of the workshop.

Click Download and then Close

4 - Installing SAGA Tools

In the search bar of the Plugins tab, search for SAGA.

Install Processing Saga NextGen Provider

5 - Watershed Delineation

We will use the Fill sinks (Wang & Liu) algorithm to identify and fill depression areas in our elevation model. Make sure to select the SAGA version!

In the DEM field, enter the DEM [EPSG:4326], then click Run

We will then compute the hydrological characteristics of the watersheds in the study area (flow direction, flow accumulation, stream networks, etc.) using the r.watershed tool from GRASS

In the Elevation menu, select Filled DEM [EPSG:4326]

In the Minimum size of exterior watershed basin field, enter 1000

Check only Enable Single Flow, Use positive flow, and Beautify flat areas

Click Run then Close

In the Layers panel at the bottom left, uncheck everything except Stream segments and Google Satellite

Find and open the r.water.outlet tool from GRASS

In the Drainage direction raster menu, select Drainage direction [EPSG:4326]

Click the three dots to the right of the Coordinates of outlet point field and click on the map at the location of your station.

Click Run and then Close

Your watershed should now appear on your map

6 - Exporting the Watershed

To export the watershed, we first need to convert it to a vector format.

Raster tab -> Conversion -> Polygonize (raster to vector)

Then, right-click on the newly created vector layer -> Export -> Save Features As…

Select the ESRI Shapefile format

In the File name field, enter the desired name and click on the three dots to choose where the file will be saved

Heads up! The export will create 6 files. These 6 files make up your shapefile and must always be kept together!

7 - Extracting Land Use Data

Now that we have a watershed boundary in shapefile format, we can import it into R and use it to extract land use data. We will also need a land use raster. For this workshop, I used the 2020 Annual Crop Inventory from Agriculture and Agri-Food Canada, available here -> https://ouvert.canada.ca/data/fr/dataset/32546f7b-55c2-481e-b300-83fc16054b95

library(sf)
library(tmap)
library(terra)
library(tidyverse)

#Shapefile importation
essai <- st_read("essai.shp")
Reading layer `essai' from data source 
  `/Users/charlesmartin/My Drive/UQTR_Pro/Numerilab/Ateliers/BassinsVersantsJade/essai.shp' 
  using driver `ESRI Shapefile'
Simple feature collection with 2 features and 2 fields
Geometry type: POLYGON
Dimension:     XY
Bounding box:  xmin: -73.63875 ymin: 46.29458 xmax: -72.89042 ymax: 46.83875
Geodetic CRS:  WGS 84
#Everything worked!
tmap_mode("view")
tm_shape(essai) + tm_polygons()
#Land use raster importation
landuse = rast("aci_2020_qc_v4.tif")

#Everything worked!
str(landuse)
S4 class 'SpatRaster' [package "terra"]
plot(landuse)

#Swiping numerical categories with their definition, as per Agriculture and #Agrifood Canada's guidelines 

landuse_classes <-data.frame(lu_code = c(10,20,30,34,35,50,80,85,110,120,122,130:143,145:158, 160:163, 167,168,174:183, 185,188:200, 210,220,230),
                             
landuse_class = c("Cloud", "Water","Exposed_Land_Barren", "Urban_Developed",
                                          "Greenhouses","Shrubland","Wetland","Peatland","Grassland",
                                          "Agriculture","Pasture_Forages","Too_Wet_to_be_Seeded",
                                          "Fallow","Cereals","Barley","Other_Grains","Millet","Oats","Rye",
                                          "Spelt","Triticale","Wheat","Switchgrass","Sorghun","Quinoa","Winter_Wheat",
                                          "Spring_Wheat","Corn","Tobacco","Ginseng","Oilseeds","Borage","Camelia",
                                          "Canola_Rapeseed","Flaxseed","Mustard","Safflower","Sunflower","Soybeans",
                                          "Pulses","Other_Pulses","Peas","Chickpeas","Beans","Fababeans","Lentils",
                                          "Vegetables","Tomatoes","Potatoes","Sugarbeets","Other Vegetables","Fruits",
                                          "Berries","Blueberry","Cranberry","Other Berry","Orchards","Other Fruits",
                                          "Vineyard","Hops","Sod","Herbs","Nursery","Buckwheat","Canaryseed","Hemp",
                                          "Vetch","Other_Crops","Forest","Coniferous","Broadleaf","Mixedwood"))

levels(landuse)<-landuse_classes

The next step is to convert our boundary shapefile into a SpatVector format and to reproject it under the same coordinate system as the land use map. Once compatible, we will be able to extract information on land use.

#Shapefile conversion into a compatible format
spoly = vect(essai) %>% 
  terra::project(landuse)

#Land use data extraction
landuse.essai = landuse %>% 
  terra::extract(spoly) %>% 
  count(ID, landuse_class)

One last thing that might be interesting is to create a dataframe containing land use percentage. This way we could determine what is the predominant land use in our watershed.

#Pixels' number conversion into percentage, according to land use
landuse.essai<-mutate(landuse.essai,percent= (n*100)/sum(n))

#Quick visualisation graph
graph <- ggplot(landuse.essai) +
  geom_col(aes(x = landuse_class, y = percent, fill = landuse_class, width = 5))+
  theme_classic()+
  theme(axis.text.x = element_text(angle=45, hjust = 1))+
   xlab("Landuse Categories") +ylab("Percentage (%)")+
  ggtitle("Watershed landuses")

graph