How to make a non-rectangular map layout in QGIS

Rectangles are the most common shape of maps in GIS software, but why stop there?

In GIS software like ArcGIS Pro, it is possible to create map layouts in non-rectangular shapes without too much hassle. But that is not the case in QGIS (yet!), so let’s look at ways to make creative map shapes in QGIS.

In this post, I explain how to download a Sentinel 2 image from the Copernicus Open Access Hub. I want to use this image to make some unique map-layouts.

I want to make an NDVI map, so I take the NIR and Red bands from the image (band 8 and 4), add them to QGIS, and use the Raster Calculator to make an NDVI image

I use the latest country-border polygon from GADM to clip the raster layer

I want to show the NDVI data of the area near the city of Aalborg, in some different shapes.

I start by creating two polygon layers, and create a big polygon in one layer and a small polygon (near Aalborg) in the second layer . The small polygon is the one that will define the layout, so get creative!

I use the Difference tool in QGIS to cut out the shape of the small polygon in the large polygon.

The outcome looks something like this:

Depending on your creative skills, you can make some cool shapes. You can also import a pre-made polygon and use that instead.

Here are the 3 versions I ended up with:

You can also add some of the classical map elements

How to download a satellite image from Copernicus Open Access Hub

Personally I don’t find the Copernicus Open Access Hub very intuitive to use, so I wanted to provide an example of how I download data from the website.

For this example I want to download a single Sentinel 2 (A or B) image from the northern part of Jutland in Denmark. I want it to be cloud free and it should be as recent as possible.

Here’s how I would do that:

1. Login to Copernicus Open Access Hub by clicking the confused looking person in the top right corner

2. Click on “Switch to Navigation Mode” in order to select the area that you are interested in

3. Select the area

4. Click on the filter-button left of the search bar, and make the following selections:

  • Select Sort by : Sensing date
  • Select the time period you are interested in (choose Sensing period)
  • Select the Sentinel satellite mission you want data from
    Tip: If e.g. you just want any image from Sentinel 2, you don’t have to select anything in the Satellite platform dropdown, just leave it empty.
  • Select cloud cover % if that is relevant. For Sentinel 2, I usually set the cloud cover very low, e.g. “[0 to 10]”.

5. Click on the search symbol in the top, and you should see a list of available images from your area.
If you don’t see any images, try to change the cloud cover or expand the selected time period.

6. Select the image that suits your needs and download it.
The download might take a while. The most recent image I downloaded took ~3 min to download and the size was ~0.6GB.

How a filter could look like

The image I ended up selecting

Once the download is finished, the next challenge is to find the raster files in the labyrinth of subfolders. I found mine here:

C:\Users\username\Downloads\S2B_MSIL2A_20220618T102559_N0400_R108_T32VNJ_20220618T121737\S2B_MSIL2A_20220618T102559_N0400_R108_T32VNJ_20220618T121737.SAFE\GRANULE\L2A_T32VNJ_A027590_20220618T102602\IMG_DATA

Here you can find three resolutions folders (R10m, R20m, R60m). In my case I chose R10m.

The data is in the JP2-format, which a program like QGIS should not have any problem reading.

That’s it — good luck with downloading data on your own!

If you want to see what I used the sentinel image for, you can read about it in this post.

Calculate the percentage (%) cropland for each polygon in a shapefile [Google Earth Engine]

In a previous post, I use a similar script to calculate NDVI per polygon. However, this script includes a small difference which I have not yet myself understood why it was necessary.


As before, my shapefile consists of the administrative borders of Tanzania. I got this shapefile from the GADM database (version 3.6).

// Import administrative borders and select the gadm level 
// (0=country, 1=region, 2=district, 3=ward)
var gadm_level = '2';
var fc_oldID = ee.FeatureCollection('users/username/gadm36_TZA_'+gadm_level);
Map.addLayer(fc, {}, 'gadm TZA');
Map.centerObject(fc, 6);

Now I change the system:index of the FeatureCollection.

Why do this at all? I was struggling to produce a mean value of the COPERNICUS landcover data, after having created a successful script for the MODIS NDVI dataset. When I ran the script, no mean column was created. However, I was able to calculate mean values from a couple of polygons that I created myself within Google Earth Engine, so I knew the problem had to be in the shapefile somehow. So I started to investigate the gadm asset (i.e. the shapefile) and saw that Google Earth Engine automatically assigns some odd index names to each feature (e.g. 000000000001 or 0000000000a). I used the following steps described by the user Kuik on StackExchange to change the system:index:

// Change the system:index ("ID")

var idList = ee.List.sequence(0,fc_oldID.size().subtract(1));

var list = fc_oldID.toList(fc_oldID.size());

var fc = ee.FeatureCollection(idList.map(function(newSysIndex){
  var feat = ee.Feature(list.get(newSysIndex));
  var indexString = ee.Number(newSysIndex).format('%01d');
  return feat.set('system:index', indexString, 'ID', indexString);
}));

print('new system:index', fc);

Now you can see that the system:index has been changed:

Now I load in the landcover data. Here I use the 100m resolution, yearly Copernicus Global Land Cover dataset. Here we can find an image per year from 2015-2019. This dataset has a long list of interesting bands, e.g. a land cover classification with 23 categories, percentage of urban areas and percentage of cropland. In this script, I will look at the percentage of cropland (crops-coverfraction) for different administrative regions in Tanzania. Read more about the COPERNICUS dataset here.

// Load data

var year = '2015'
var lc = ee.Image("COPERNICUS/Landcover/100m/Proba-V-C3/Global/"+year)
      .select('crops-coverfraction')
      .clip(fc)

print('Image from '+year, lc)

print('Projection, crs, and crs_transform:', lc.projection());
print('Scale in meters:', lc.projection().nominalScale());

The last two lines is a good way to get a bit more information about your data if you, like I was, are looking for a reason why your script is misbehaving. It will give you the following output:

Now we can use the reducer reduceRegions. Again, remember that the scale should reflect the spatial resolution of the data. In the image above, we can see that the scale is 100 m.

// Reduce regions
var MeansOfFeatures= lc.reduceRegions({
  reducer: ee.Reducer.mean(),
  collection: fc,
  scale: 100,
})

print('gadm mean', ee.Feature(mean.first())) // Test if mean is produced

// Set visualization parameters    
var lcVis = {
  min: 0.0,
  max: 100.0,
  palette: [
    'ffffff', 'fcd163', '99b718', '66a000', '3e8601', '207401', '056201',
    '004c00', '011301'
  ],
};

Map.addLayer(lc, lcVis, 'crop cover (%) '+year)

// Export the featureCollection (table) as a CSV file
Export.table.toDrive({
  collection: MeansOfFeatures,
  description: 'proba_V_C3_TZA_adm'+gadm_level+'_'+year,
  folder: 'GEE output',
  fileFormat: 'CSV'
});

This should give you the following output:


You can find the full script here.