Dynamic elevation profile lines as QGIS geometry generator

Load a raster layer in QGIS.

Add a new Scratch Layer of Polygon type (in any CRS). Set its Symbology to Inverted Polygons. Use a Geometry Generator as symbol layer type. Set it to LineString/MultiLineString. Enter the expression below and adjust the layer ID (best enter the expression editor and find it in the “layers” tree). Then adjust the scaling factor at the very bottom.

-- UPPER CASE comments below are where you can change things

with_variable(
	'raster_layer',
	'long_and_complicated_layer_id',  -- RASTER LAYER to sample from
	-- this collects all the linestrings generated below into one multilinestring
	collect_geometries(
		-- a loop for each y value of the grid
		array_foreach(
			-- array_foreach loops over all elements of the series generated below
			-- which is a range of numbers from the bottom to the top of y values
			-- of the map canvas extent coordinates.
			-- the result will be an array of linestrings
			generate_series(
				y(@map_extent_center)-(@map_extent_height/2), -- bottom y
				y(@map_extent_center)+(@map_extent_height/2),  -- top y
				@map_extent_height/50  -- stepsize -> HOW MANY LINES
			),
			
			-- we want to enter another loop so we assign the name 'y' to
			-- the current element of the array_foreach loop
			with_variable(
				'y',
				@element,
				
				-- now we are ready to generate the line for this y value
				make_line(
					-- another loop, this time for the x values. same logic as before
					-- the result will be an array of points
					array_foreach(
						generate_series(
							x(@map_extent_center)-(@map_extent_width/2), -- left x
							x(@map_extent_center)+(@map_extent_width/2),  -- right x
							@map_extent_width/50  -- stepsize -> HOW MANY POINTS PER LINE
						),
						-- and here we create each point of the line
						make_point(
							@element,  -- the current value from the loop over the x value range
							@y  -- the y value from the outer loop
							+   -- will get an additional offset to generate the effect
							-- we look for values at _this point_ in the raster, and since
							-- the raster might not have any value here, we must use coalesce
							-- to use a replacement value in those cases
							coalesce(  -- coalesce to catch raster null values
								raster_value(
									@raster_layer,
									1,  -- band 1, *snore*
									-- to look up the raster value we need to look in the right position
									-- so we make a sampling point in the same CRS as the raster layer
									transform( 
										make_point(@element, @y),
										@map_crs,
										layer_property(@raster_layer,'crs')
									)
								),
								0  -- coalesce 0 if raster_value gave null
							-- here is where we set the scaling factor for the raster -> y values
							-- if things are weird, set it to 0 and try small multiplications or divisions
							-- to see what happens.
							-- for metric systems you will want to multiply
							-- for geographic coordinates you will want to divide
							)*10  -- user-defined factor for VERTICAL EXAGGERATION
						)
					)
				)
			)
		)
	)
)  -- wee

If you don’t have your raster data on a SSD this can be a bit slow.

Yes, this works if you change your CRS!

19 thoughts on “Dynamic elevation profile lines as QGIS geometry generator

  1. Pingback: Dynamic elevation profile lines as @QGIS geometry generator – GeoNe.ws

  2. Toni

    Amazing work!
    I would like to hear more explanation since for some reason, the code does not work for me, and input “collect_geometries” does not exist in qgis 3.4.4

    Reply
    1. Hannes

      See the note at the very top. This only works in the current development version until the next version is released.

      Reply
  3. Kevin

    I used QGIS 3.8.3, I tried to put the “expression” in the expression inside the scratch layer properties, but it does not do anything, and it says the expression is wrong. Can you help me by explaining a little bit more on how to use the expression

    Reply
    1. Hannes Post author

      “Development version” means one that you compile yourself from the latest source code. ;)

      You will have to wait until 3.10. Which will be released in a week!

      Reply
  4. Owen

    Hi,
    First off this is really great! Second, i have the code to work but it is just giving back flat lines and not taking into account the raster layer. I have the raster_layer as the file path for the raster layer and then the layer id as the polygon scratch layer, could this be why?

    Reply
        1. Hannes Post author

          Only the raster layer needs to be explicitly referenced in the expression if I remember correctly, so make sure you did not mix up that with the scratch layer.
          If that is all fine and the problem persists, play with the “VERTICAL EXAGGERATION” value.

          Reply
  5. Pingback: #30DayMapChallenge: Day 3 – Polygons OR Lego® style brick raster in QGIS using Geometry Generator expressions | Hannes ihm sein Blog

  6. Luca Bellani

    Hello, and thanks for the tutorial, very interesting!
    In my case, there is a displacement of the curves towards the north, of an approximate 1 kilometers. But I can’t understand what I can modify to match .. thank you very much!

    Reply
    1. Hannes Post author

      Hm, not sure. You can try lowering the exaggeration to 1 (= no exaggeration) at the bottom of the script. Are the lines still offset then?

      Reply
        1. Pat

          Out of curiosity did you find a solution to this? I’m experiencing the same issue.

          Reply
  7. Pingback: Johannes Kröger to GeoHipster: “$existing_free_software can do that already.” | GeoHipster

  8. Pingback: Johannes Kröger to GeoHipster: “$existing_free_software can do that already.” – GeoHipster

  9. Pingback: Joy Plots with Geo Data – COOL BLUE DATA

  10. Pingback: Joy Plot on the Map in Tableau – COOL BLUE DATA

Leave a Reply to Owen Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.