Appendix
NDVI Computation
The code chunk that we used to compute the NDVI from Google Earth Engine is shown below. It actually computes the NDVI as well as two other indices (NDREI and NDWI) that we ended up not using for our analysis. You can easily compute other indices with the library that is used, by changing the variable called indices
. Furthermore, it computes an average on the last five years, roughly for each season, and exports the data to Google Earth Engine. You might have to change the output path to adapt it to your situation in the call to the function Export.image.toDrive
. Finally, this algorithm requires an extent (a vector layer) as an input that defines the area of interest. This can be changed in the variable called extent
.
var extent = ee.FeatureCollection("projects/arfw0501/assets/Spatial_units_extent");
var spectral = require("users/dmlmont/spectral:spectral");
// Indices to compute
var indices = ["NDVI", "NDREI", "NDWI"];
// Get the bounding box of the shapefile and buffer it
var bbox = extent.geometry().bounds();
var bbox_utm = bbox.transform('EPSG:25833', 0.1);
var bbox_wgs84_buffered = bbox_utm.transform('EPSG:4326', 0.1).buffer(3000);
// Center the map
Map.centerObject(bbox_wgs84_buffered, 12);
// Define seasons and their date patterns
var seasons = [
name: 'Winter', start: '-12-01', end: '-02-28', lastStartYear: 2024},
{name: 'Spring', start: '-03-01', end: '-05-31', lastStartYear: 2024},
{name: 'Summer', start: '-06-01', end: '-08-31', lastStartYear: 2024},
{name: 'Autumn', start: '-09-01', end: '-11-30', lastStartYear: 2024},
{;
]
// Loop over each season
.forEach(function(season) {
seasons// Create an object to store images for each index
var indexImages = {};
// Initialize empty lists per index
.forEach(function(index) {
indices= [];
indexImages[index] ;
})
// Loop over the past 5 years
for (var y = 0; y < 5; y++) {
var year = season.lastStartYear - y;
var startYear = year;
var endYear = (season.name === 'Winter') ? year + 1 : year;
var startDate = startYear + season.start;
var endDate = endYear + season.end;
// Load and filter Sentinel-2 imagery
var collection = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
.filterBounds(bbox_wgs84_buffered)
.filterDate(startDate, endDate)
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 10))
.median();
// Compute all requested indices
var parameters = {
"S2": collection.select("B12"),
"S1": collection.select("B11"),
"N2": collection.select("B8A"),
"N": collection.select("B8"),
"RE3": collection.select("B7"),
"RE2": collection.select("B6"),
"RE1": collection.select("B5"),
"R": collection.select("B4"),
"G": collection.select("B3"),
"B": collection.select("B2"),
"L": 0.5
;
}
var computedIndices = spectral.computeIndex(collection, indices, parameters);
= computedIndices.clip(bbox_wgs84_buffered);
computedIndices
// Add each computed index image to the relevant list
.forEach(function(index) {
indices.push(computedIndices.select(index));
indexImages[index];
})
}
// Compute and display/export average per index for the season
.forEach(function(index) {
indicesvar meanImage = ee.ImageCollection(indexImages[index]).mean();
// Display
Map.addLayer(meanImage, {min: -1, max: 1, palette: ['red', 'white', 'green']},
+ '_' + season.name + '_mean');
index
// Export
.image.toDrive({
Exportimage: meanImage,
description: index + '-' + season.name + '-mean_5yr',
folder: 'EarthEngineExports',
fileNamePrefix: index + '-' + season.name + '-mean_5yr',
region: bbox_utm,
scale: 10,
crs: 'EPSG:25833',
maxPixels: 1e13
;
});
}); })
Building Geometry
The process of fixing the geometries of the clipped building layer can be done through this text Since the building layer had several geometry errors, it went through some geometry validation and fixing processes (i.e. valid geometry, fix geometries, simplify and re-project). For a smoother experience, you can read through this process to fix your geometries:
- “Use “Check Validity” Go to: Vector → Geometry Tools → Check Validity
- Choose “GEOS” as the engine.
- Review the reported issues — “valid” in QGIS may not be sufficient for overlay ops if robustness is compromised.
- Fix Geometries (Again) Run: Vector → Geometry Tools → Fix Geometries
If already done, try exporting the layer as a new file (Right-click layer → Export → Save Features As…) to reset underlying geometry representations.
If you encountered errors for holes or self-intersections, Simplify or Snap Geometries Minor overlaps/gaps or very close vertices can trigger GEOS precision issues.
Try:
- Vector → Geometry Tools → Snap geometries to layer
- or Vector → Geometry Tools → Simplify Geometry
Use “Invalid Geometry Handling” in Processing If using model/processing tools, go to the settings (Processing → Options) → “Invalid geometry handling” → set to “Ignore features with invalid geometries” (temporary workaround). - Use Buffer(0) as a Cleanup Trick. Use the expression:
buffer($geometry, 0)
in the Geometry by Expression tool.
It often resolves subtle robustness problems.
CRS Conflict
Your raster is in EPSG:25833, but you’re trying to extract it using a mask layer in EPSG:32633. These CRS look similar (both UTM zone 33N), but they’re not the same datum:
- EPSG:25833 → ETRS89 (used in Europe, based on stable tectonic plate)
- EPSG:32633 → WGS84 (global standard)
GEOS and GDAL can throw robustness errors if vector and raster CRS differ even slightly during masking.
Option 1: Reproject the Mask Layer to Match the Raster Right-click the mask layer → Export → Save Features As…
- Set CRS to EPSG:25833
- Use the new projected mask layer for extraction.
- Raster → Extraction → Clip Raster by Mask Layer
- Use this EPSG:25833 version of the mask
Option 2: Reproject the Raster Instead If the mask needs to stay in 32633, reproject the raster:
- Raster → Projections → Warp (Reproject)
- Target CRS: EPSG:32633
Resampling: Nearest neighbour (for categorical) or Bilinear (for continuous)
- Save as new GeoTIFF
- Then run the extract using the reprojected raster.
Always ensure both raster and mask have the exact same CRS (code and datum), not just similar projection zones. Otherwise, operations like masking can fail due to subtle misalignments or geometry issues.
Nested Holes Error
Furthermore, if you received such error:
Warning 1: Holes are nested at or near point 403128.48007987819 5659925.564998446ERROR 1: Cutline polygon is invalid. This means your cutline (mask) layer dissolved_fixed_reprojected.shp has self-intersecting or nested holes. GDAL (gdalwarp) cannot use this geometry until it is fully valid and simplified.
Final Fix — Repair Deep Geometry Issues Do the following in exact order:
Explode MultiParts Vector → Geometry Tools → Multipart to Singleparts → Run it on dissolved_fixed_reprojected.shp → Save result as e.g. dissolved_singleparts.shp
- Use “Buffer(0)” Trick Open Processing Toolbox
- Use Geometry by Expression on the singlepart layer
- Expression:
buffer($geometry, 0)
- Save as: dissolved_cleaned_buffer0.shp
This rewrites the geometry and usually removes nested holes and invalid rings.
Final Geometry Check:
- Run Check Validity tool on dissolved_cleaned_buffer0.shp
- GEOS engine
All features must be valid
Reproject This Layer (if needed) If your raster is in EPSG:25833, make sure this cleaned mask is also in EPSG:25833.
Few Invalid Geometries
If you found a few invalid geometries after running Check Validity, do this:
Fix Them Properly (Step-by-Step)
- Isolate Invalid Features Open the Check Validity output layer (or attribute table):
- Filter by “valid” = ‘0’
- Select all invalid features
- Fix Individually or in Batch Use either:
A. Manual Fix:
- Enable editing
- Use vertex tool to correct overlapping rings, holes, etc. Useful for <10 features
B. Automated Fix:
- Select invalid features
- Run: Vector → Geometry Tools → Fix Geometries
- Only apply to selected features
- Save as fixed_invalids.shp
- Replace Invalids in Main Layer Use “Merge Vector Layers”: Input: your original cleaned valid layer + fixed_invalids.shp
- Output: final_cleaned_mask.shp
If you faced such error:
Warning 1: Too few points in geometry component at or near point 7143.291…ERROR 1: Cutline polygon is invalid.
It means you have “degenerate geometries” in your “valid” features — like a polygon made from just 1 or 2 vertices, which isn’t a valid polygon for GDAL.
Solution: Remove Features with Too Few Vertices
- Open the attribute table of valid_only_mask_readyformask.shp
- Use this filter in the expression dialog:
num_points(\$geometry) \>= 4
- Export the selected features:
- Right-click → Export → Save Selected Features As…
- Name it valid_clean_mask.shp
- Optional: Run Buffer(0) Again Just to be sure:
- Geometry by Expression Use: buffer($geometry, 0)
- Save result as final_cutline_mask.shp
- Retry Clipping Now use final_cutline_mask.shp as your cutline.
This should fully eliminate:
- Nested holes
- Invalid rings
- Too-few-points geometries
Self-intersection, Small rings or Overlapping vertices error
If you faced this error:
Warning 1: Self-intersection at or near point … ERROR 1: Cutline polygon is invalid.
means even after filtering and buffering, some geometries still have self-intersections, often caused by very small rings or overlapping vertices introduced during buffering or fixing.
Final Clean Repair: Force Rebuild of Valid Topology Do this:
- Use Delete Holes Vector → Geometry Tools → Delete holes
- Input: final_cutline_mask_buffered.shp
- Keep minimum area very small (e.g. 0.0001)
- Save as final_noholes.shp
- Simplify + Buffer(0) Combo Now:
- Run Simplify Geometry:
- Tolerance: 0.01 to 0.1 depending on your unit (meters)
- Save as final_simplified.shp
- Then apply buffer($geometry, 0) again on that layer
- Re-check Validity One Last Time Use Check Validity (GEOS) on the result.
- Ensure all features pass.
- Use That as Cutline Use the output from the above steps (final_simplified_buffered.shp) as your mask layer.”
Heavy Raster File (1m Resolution)
After processing the mask layer, it was used to clip the raster layer and patched with the DSM layer to go through the shadow analysis process. Since the raster layer was very large, there’s a need to reduce the resolution or just clipping it to a smaller area, unless a strong machine with minimum 32GB RAM would run the analysis. Otherwise, the process cannot be done due to lack of memory in your machine.
To resample the raster resolution:
If you’re using 0.5m or 1m resolution:
- Use Raster → Projections → Warp (Reproject)
- Set a coarser pixel size (e.g. 2m or 5m)
- Resampling: Average or Nearest Neighbour”.
The patched layer was resampled to 3m resolution which made the process feasible.
Recap & Summary
- Always match CRS between raster and mask.
- Run Check Validity, Delete Holes, Simplify, and buffer($geometry, 0) on complex masks.
- Filter out degenerate geometries (num_points($geometry) < 4).
- Use batch or models to automate cleanup when needed.
- Resample the raster to smaller resolution (from 1m to 3m or 5m)
Maps Not Included in Final Results
We did an accessibility analysis to identify areas within walking distance of the streams, with steps as follows:
- Interpolated points were generated every 500m along the stream center line and river outline.
- Using the service area tool, we identified which areas within walking distance of interpolated points using three ranges 500m, 750, and 1000m
- The output layers were aggregated to our the spatial units with assigned scores: 1 for areas within 500m, 0.5 for 750, and 0 for 1000m
We excluded these maps from further analysis because some spatial units had no value after aggregation.