38

Is there a way to interrupt a contour line beneath an elevation label using QGIS?

enter image description here

2
  • Would you accept an answer that requires PostGIS? – Scro Mar 5 '13 at 15:01
  • 3
    unfortunatly: no :) But, is there a way to solve the problem with PostGIS? – MAP Mar 5 '13 at 18:53
24
+100

Yes, doable. Normally I'd suggest a partially transparent buffer, but I see why you want to do this cartographically.

This could be slow, and you need to manually decide where you want the labels to go - but cartographically speaking, that's not a bad thing!

Here's a screenshot...

As you can see, no buffers. The raster underneath is unaffected. I've included thinner intermediate contour lines, and styled them so they're only shown when ELEV % 50 <>0

example of interrupted contour lines

I've done this in QGIS 2.12 ... your mileage may vary with earlier versions.

I assume here you have an "ELEV" field on each contour line.

Segmentize the contour lines

  1. Use processing and the GRASS algorithm v.split.length to split your contours into segments of equal length. You need to choose a length which will be close to the size of your label in map units, assuming you're using meters. Here I used 200m.

    Be careful with this as it will make your file much, much larger (note the feature counts in the screenshot).

    To get around this, you might want to generate only those contours lines you wish to style (e.g. every 50 or 100 meters) to avoid processing all the intermediate contour lines.

  2. To this layer, add a 1 digit integer field called showLabel. Default to 0 or NULL.

  3. Change the label to only show on a segment where this field is set to 1. Use this for the label text expression...

    if ( "showlabel" is not null, "ELEV", "")
    

    I think if(expression,true-value,false-value) is fairly new; if using an older version, you can use CASE-ELSE

  4. Change the line style so the fixed length segments are all drawn, except those segments where the label is displayed. So use Rule-Based rendering with two rules

    Rule 1: "showLabel" is null
    Black, 0% transparent
    
    Rule 2: "showLabel" is not null
    Any colour, 100% transparent
    

    Now, all contours will show by default, but no labels.

    Manually edit segments where you want to show labels

    Go into edit mode and manually select the segments where you want the contour values to display, and set the value of showLabel to 1 for the selected features. You can use Ctrl + select (on Ubuntu/Win, Cmd + Ctrl + Click / on Mac?) to multi-select segments to speed things up.

    This should now 'clip' the contours where you want the labels to show, and the labels will show in the gaps.

In this case, my label settings were:

CRS: EPSG 27700 (Local UTM for UK, in meters)
Text size: 50 map units
Placement: Parallel, On Line

Hope that helps!

2
  • 5
    This is the only one fully working solution I can imagine. Very painful though if there are many labels, I can't imagine doing all my groundwater heads maps (thousands per year) this way. Would be great if in the future this is achievable through style - as the best would be custom line pattern and label repeat + offset. – Miro Nov 19 '15 at 0:02
  • 3
    Just some ideas to ease the task: To select the multiple segments to draw the label, Select by Polygon or Select by Freehand can be handy. Also, another aproach would be creating a scratch line layer to draw lines that intercept the contours, and then do a Select by location. – Alexandre Neto Feb 3 '16 at 23:22
9

I use the "Buffer" option on the "Label setting" tab. (Using the labels button, not the old labels option on the layer properties dialog.) This does not wipe out the contour line, as I imagine you are wanting to do, but it does make the label legible.

8
  • 4
    I never thought of it before, but it would be convenient if instead of assigning a color for the buffer, you could choose to apply it as a 'knockout' to selected layers. – Scro Feb 25 '13 at 22:01
  • 5
    The latest version of QGIS has transparent buffers so you can reduce the impact on other parts of the map. – Nathan W Feb 25 '13 at 22:59
  • 1
    @MAP A knockout erases the pixels beneath it. If that were an option, in this case you would choose to knockout the contour layer. – Scro Feb 26 '13 at 11:41
  • 1
    knockout - the esri term is "masking" resources.arcgis.com/en/help/main/10.1/index.html#//… – mike Feb 26 '13 at 19:47
  • 1
    @MAP - Sponsor a developer, or submit a feature request and wait on the benevolence of others. :) – Scro Feb 27 '13 at 23:02
6

I think the closest it can get with current QGIS abilities is to use halo (or background) effect with color sourced from table which will be based on the elevation value and color scheme same as used for underlying grid. Of course this would not take into account hillshade and everything else below the halo in the map. Random color example: random color for halo effect of labels With some bit of code this could be rewritten as function to reflect grid color.

In theory it should be possible to use custom line pattern and label repeat + offset. Unfortunately there is no label offset setting.

  • after some testing it is impossible to force QGIS to be strict with placing the labels in exact interval and nowhere else (+ missing starting offset anyway)
  • it is impossible to create custom line pattern with zero mm for space to have starting offset like 20 line - 10 space - 70 line - 0 space - so the label would be placed every 100mm with 30mm offset at the beginning - meaning label would be in the middle of every 10mm hole.

enter image description here

5

During my research to achieve a mostly dynamic labeling of contour lines in QGIS 3.10, I put everything together in a small cooking recipe and changed my original answer to share the findings with the community.

The solution result: enter image description here

And here are the ingredients:

  1. a "linestring" layer called "contours": the file format doesn't matter in that case, but I prefer "GeoPackage" to store the data together with the QGIS project. Additionally the layer needs a primary key field "fid", a text field called "elev" and should contain a spatial index to speed up processing
  2. a "linestring" layer called "scratch_lines" without any additional fields, but spatially indexed. We use this layer to create "scratch" lines that intersect the contour lines to define all label positions.
  3. a virtual layer called "contours_with_labels" defined in DB Manager, using the two project layers "contours" and "scratch_lines"
  4. a QGIS project variable called "contourlabel_size" containing the global text size of all contour labels in Map Units.
  5. a QGIS project Python macro to define an Expression Function for retrieving the current canvas extents, and some code to refresh the virtual layer during project reload. Without that code, we have to refresh the virtual layer manually (i.e. using the wonderful plugin "changeDataSource": right click on layer name => Change vector datasource => OK) what we have to do anyway if we want to change the label text size.
from qgis.core import qgsfunction,QgsProject,QgsGeometry
from qgis.utils import iface

@qgsfunction(args='auto', group='Custom')
def currentExtent(feature, parent):
    return QgsGeometry.fromRect(iface.mapCanvas().extent())

def openProject():
    layer = QgsProject.instance().mapLayersByName('contours_with_labels')[0]
    layer.reload()
    layer.triggerRepaint()

The dynamic creation of all contour labels is driven by making use of QGIS's (GDAL's) SpatialLite functionality inside the SQL query of the virtual layer:

------------------------------------------------------------------------
-- select all contour lines that do not intersect any scratch lines
------------------------------------------------------------------------
SELECT c.geometry,c.elev,0 as label 
FROM contours c,
         (SELECT ST_UNION(geometry) AS geom FROM scratch_lines 
          WHERE _search_frame_ =  currentExtent()) AS scr 
WHERE c._search_frame_ =  currentExtent()
AND NOT ST_INTERSECTS(c.geometry,scr.geom)

UNION

--------------------------------------------------------------------------------------------------------
-- create buffers around all intersection points (bufferwidth = length(elevation_text) * txtsize/2.5),
-- get st_difference between contour lines and buffers
-- and set attribute "label" to 0
--------------------------------------------------------------------------------------------------------
SELECT ST_DIFFERENCE(c.geometry,buf.geom) AS geom,c.elev,0 AS label 
FROM contours c,
    (SELECT c.fid,
                 ST_UNION(ST_BUFFER(ST_INTERSECTION(c.geometry,scr.geometry),
                 LENGTH(c.elev) * var('contourlabel_size') / 2.5)) AS geom
    FROM contours c, scratch_lines scr
    WHERE c._search_frame_ =  currentExtent()
    AND scr._search_frame_ =  currentExtent()
    AND st_intersects(c.geometry,scr.geometry)
    GROUP BY c.fid) AS buf
WHERE c._search_frame_ =  currentExtent()
AND c.fid = buf.fid 

UNION

--------------------------------------------------------------------------------------------------------
-- create buffers around all intersection points (bufferwidth = length(elevation_text) * txtsize/2.5),
-- get st_intersection between contour lines and buffers
-- and set attribute "label" to 1
--------------------------------------------------------------------------------------------------------
SELECT ST_INTERSECTION(c.geometry,ST_BUFFER(ST_INTERSECTION(c.geometry,scr.geometry),
            LENGTH(c.elev) * var('contourlabel_size') / 2.5)) AS geom,
            c.elev,
            1 AS label 
FROM contours c, 
         scratch_lines scr 
WHERE c._search_frame_ =  currentExtent()
AND scr._search_frame_ =  currentExtent()
AND ST_INTERSECTS(c.geometry,scr.geometry)

The styling of the virtual layer is based on 2 rules:

  • Rule 1 ("Label" - Simple line,width=0,color=full transparent):

    contains( @map_extent ,start_point($geometry))
    AND
    contains( @map_extent ,end_point($geometry))
    AND "label" = 1

  • Rule 2 ("Contours" - Geometry generator: smooth(simplify($geometry,1),3) > Simple line:width=0.5,color=black):

    ELSE

And the label configuration of the virtual layer (Single labels):

Value:

CASE WHEN label = 1 THEN 
  CASE WHEN 
    contains( @map_extent ,start_point($geometry)) 
    AND 
    contains( @map_extent ,end_point($geometry))
    THEN elev ELSE '' 
  END
END

Size:

Variable "contourlabel_size"

Placement: (Parallel, On line) Linestring Geometry generator

make_line(start_point( $geometry ), end_point( $geometry ))

That's it!

Hope it's useful for anyone, like it was useful for me.


UPDATE:

With this small update of the SQL query, we can force an uphill orientation of all contour labels by drawing the scratch lines in uphill direction:

------------------------------------------------------------------------
-- select all contour lines that do not intersect any scratch lines
------------------------------------------------------------------------
SELECT c.geometry,c.elev,0 AS label /*:int*/
FROM contours c,
         (SELECT ST_UNION(geometry) AS geom FROM scratch_lines 
          WHERE _search_frame_ =  currentExtent()) AS scr 
WHERE c._search_frame_ =  currentExtent()
AND NOT ST_INTERSECTS(c.geometry,scr.geom)

UNION

--------------------------------------------------------------------------------------------------------
-- create buffers around all intersection points (bufferwidth = length(elevation_text) * txtsize/2.5),
-- get st_difference between contour lines and buffers
-- and set attribute "label" to 0
--------------------------------------------------------------------------------------------------------
SELECT ST_DIFFERENCE(c.geometry,buf.geom) AS geom,c.elev,0 AS label 
FROM contours c,
    (SELECT c.fid,
                 ST_UNION(ST_BUFFER(ST_INTERSECTION(c.geometry,scr.geometry),
                 LENGTH(c.elev) * var('contourlabel_size') / 2.5)) AS geom
    FROM contours c, scratch_lines scr
    WHERE c._search_frame_ =  currentExtent()
    AND scr._search_frame_ =  currentExtent()
    AND st_intersects(c.geometry,scr.geometry)
    GROUP BY c.fid) AS buf
WHERE c._search_frame_ =  currentExtent()
AND c.fid = buf.fid 

UNION

--------------------------------------------------------------------------------------------------------
-- create buffers around all intersection points (bufferwidth = length(elevation_text) * txtsize/2.5),
-- get st_intersection between contour lines and buffers
-- create line from left intersection point to right intersection point (as seen from scratch line!)
-- and set attribute "label" to 1
--------------------------------------------------------------------------------------------------------
SELECT
    CASE WHEN ST_Distance(start,OffsetCurve(scr_geom,1)) < ST_Distance(start,OffsetCurve(scr_geom,-1))
    THEN MakeLine(start,end) ELSE MakeLine(end,start) END AS geom,
    elev,1 AS label
FROM    
(SELECT ST_STARTPOINT(geom) as start,
       ST_ENDPOINT(geom) as end,
       elev,
       scr_geom
FROM
(SELECT ST_INTERSECTION(c.geometry,ST_BUFFER(ST_INTERSECTION(c.geometry,scr.geometry),
            LENGTH(c.elev) * var('contourlabel_size') / 2.5)) AS geom,
        c.elev,
        scr.geometry AS scr_geom
FROM contours c, scratch_lines scr 
WHERE c._search_frame_ =  currentExtent()
AND scr._search_frame_ =  currentExtent()
AND ST_INTERSECTS(c.geometry,scr.geometry)) AS temp) AS temp2

enter image description here

9
  • I must say, I'm impressed with the result. Until we get a built-in option (if ever), this is definitely the cleanest way. Virtual layers to the rescue, again. – Gabriel C. Nov 29 '19 at 19:37
  • I was indeed impressed by myself. But, can it stand really large contour layers? – christoph Nov 29 '19 at 19:43
  • And I'm very glad, after testing it, that it also works with data in GeoPackages! I wanted to verify since it's based on Spatialite. Wish I could vote up more than once... – Gabriel C. Nov 30 '19 at 14:34
  • IMHO, it’s independent from any file format, because the query is running inside QGIS. So it should even work with CSV. – christoph Nov 30 '19 at 14:41
  • 1
    That's very interesting. I'd suggest, when all is said and done, to edit your original answer to include all your updates. – Gabriel C. Dec 2 '19 at 18:55
3

Let me share my home-brewed method for those like me who are not skilled enough for GRASS or Python scripts:

  1. duplicate the contour layer, you will use one layer for the unlabeled intermediate contours (eg. 50m, 150m, 250m and so on) which you will call "intermediate contours", and the other, new layer for the most important lines that you would like to label (eg. 100m, 200m, 300m and so on), which you will call "main contours".
  2. symbolize the intermediate contours with a thinner line (typically a brown line). of course you should use an expression to set to transparent the main contours in the intermediate layer.
  3. in case your contour file is very large make the "main contours" layer lighter by cancelling all of the intermediate line features, this will speed up the next steps.
  4. create a new line file for guidance lines and draw vertical lines (ie orthogonal to contours) where you want to show labels.
  5. use the buffer function to create polygons that you will use to cut the main contours file.
  6. save all the contour sections that are INSIDE the polygons into a new file that you will call "contour labels"
  7. save all the contour sections that are OUTSIDE the polygons into a new file called "cut main contours"
  8. symbolize the "contour labels" file with the elevation label, with transparent line
  9. symbolize the "cut main contours" with a fat brown line
  10. et voilà you have elevation labels interrupting the main contour lines. please note that according to the font you are using you might want to use two different buffers (larger buffer to cut away the contour lines, narrower buffer for the labels file) in order to have labels properly centered in the line gaps.
2

After running into the same problem recently I've put together a QGIS Python script to carry out the heavy lifting. The script including some (UK) test data, Readme (Guide) and style sheets used can be found at https://github.com/pjgeng/Contour-Labels

In short the script uses two vector layers as input - the annotated contour layer and a "guides" layer. The latter consists of polylines intersecting the contours at the desired label locations.

The script then works out based on distance between contours and the index contour interval which labels to apply, adds a rotation value to the label points and eventually clips the original contour layer to produce the gaps.

Closeup of the final result.

The approach works particularly well should the user need to produce contour maps at differing intervals in the same area (i.e. the guides don't change). A drawback is the inability to change the label position once the script has finished. For this the user would have to adjust the guide lines and rerun the script against the original input. I previously worked with buffers around labels a lot to create the interrupted effect, but this turned out to be aesthetically unpleasant on vector data driven maps.

Unfortunately I can't add any more pictures at this time to document or illustrate the process further.

PS: If using the style layers provided in the repository users may need to "activate" the custom fields for "Rotation", "Show Label" and "Always Show" in the labelling menu. On some installations of QGIS these are applied automatically from the stylesheet - I haven't found out what causes this yet.

1

This seems to be related to Placing elevation numbers on contours with uphill orientation and position in QGIS?

The only way I can think of to get near to a solution to your problem would be to overlay your contour layer with a clipped contour layer, use this for labelling and change the line colour to something neutral that would mask the contours under the labels, one hopes without being too intrusive.

It might be worth looking at the second answer to Halo use background color too. Perhaps breaking the contour lines might be an answer, perhaps using the buffer layer used to clip the contours.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.