11

I'm trying to create a map to print out and the size of my polygons varies greatly. As this will be a reference map I need to label every polygon, but some of the names will not fit in the polygon. In those cases I'd like to use the feature id instead and then have an attribute table link the id to the name.

What I would like to do is have some sort of expression that I can use to select which label to use on the map and which rows to include in the attribute table. Something like isLabelled that would be able to take the map scale, label length, font size etc into account.

7
  • 2
    Keep in mind, that it might be confusing for the user of the map, when polygons of the same feature class are labeled in different ways and additionally only some of them appear in an attribute table. Commented Jan 15, 2017 at 13:03
  • I'm thinking of a county map of GB
    – Ian Turton
    Commented Jan 15, 2017 at 13:04
  • What about dynamically changing the font to fit? You could take the longest of the features dimensions, divide by how many characters in the label string, and either do some math with the scale or just use that to alter your already set font size... Alternatively you could use that measure (longest feature dimension / len of string) and if it's too long label with the id.
    – user52245
    Commented Jan 15, 2017 at 17:01
  • mostly I'm concerned with readability so I don't want to mess with the font size
    – Ian Turton
    Commented Jan 15, 2017 at 17:09
  • 1
    You could use a Python custom expression taking the label, font size, and scale and returning true or false if it will fit. Then in expression engine use an if clause to switch labels. I'll work up an answer for you later with examples.
    – user52245
    Commented Jan 15, 2017 at 18:48

3 Answers 3

9

Here's a somewhat approximate (but hopefully effective) way to do it.

First some math. We need to figure out how many characters at a certain font-size a feature can contain. Here things / assumptions to know:

  • assuming metric units, (added slight change below which may make this work for us foot crs.)
  • font size is a measure of font height. Most font's are about half as wide as they are high.
  • using pt for font height, 1pt is roughtly 0.035CM
  • assuming your parcels are roughly rectangular / regular

To find how long an item is on screen / print we can use it's longest dimension (length or hight, or just length if your labels are horizontal only) / scale * 100 (to go from M to CM)

Then we can see if our label would fit.

You could do it all in the expression engine, but a custom python expression would be more efficient. Here is a tutorial on how to create one.

My custom expression:

from qgis.core import *
from qgis.gui import *

@qgsfunction(args='auto', group='Custom')
def labelFits(labelStr, fontPt, scaleM, feature, parent):
    # returns true if a label will fit in the feature at a given
    # font size and scale

    bbox = feature.geometry().boundingBox()

    # for CRS in us foot
    # fontWidth = fontPt * 0.0875

    # for CRS in metric
    # 1pt = 0.035CM
    fontWidth = fontPt * 0.035 * 0.5

    # length of maximum dimension on-screen/print in CM
    labelRoom =  max(bbox.width(),bbox.height()) / scaleM * 100

    # approx length of our label in CM based on font height * 0.5
    labelLen = len(labelStr) * fontWidth

    if labelRoom > labelLen:
        return True
    else:
        return False

Then just use that expression in the label:

if(labelFits( "yourAttribute" , 8 , @map_scale ), "yourAttribute",$id)

You'll need to plug in whatever attribute you're actually using of course, and change the font size from 8 to whatever you're using. Also, since fonts vary quite a bit you may need to tweak it a little bit till it looks right.

You could also use that expression to do things like change the color based on if the label fits or not.


To use this expression outside the map canvas (i.e. in a composer label or attribute table) we need a way to programmatically get the scale of a given map item. I've posted code to accomplish that in another answer here The method there gives us an expression getScale('composername','mapname') so to incorporate it into our expression:

if(
    labelFits( "yourAttribute" , 8 , getScale('composername','mapname')),
    "yourAttribute",$id)
6
  • 1
    I was thinking of something similar too. Especially if using map units and a projection in meters. The new(ish) geometry operators could also help here, assuming the labels are centered on the centroid. This will work better with a fixed-width (monospaced) font than a proportional font (Where a 'W' is much wider than an 'i', for example). You're on to something here, but I suspect proportional fonts and kerning will add complexity.
    – Steven Kay
    Commented Jan 15, 2017 at 22:54
  • You could use pil's imagefont.getsize(). That would give you an exact width of a string rendered in your chosen font, but I think that would have a major performance hit. The rough math above is far simpler, especially if you have hundreds of labels on screen
    – user52245
    Commented Jan 16, 2017 at 0:43
  • The only minor issue is that the function doesn't seem to work as a filter in attribute tables in print composer.
    – Ian Turton
    Commented Jan 26, 2017 at 9:36
  • Does your map scale vary from page to page? Try manually putting in the scale instead of the @map_scale variable. my guess would be the attribute table has no way of knowing your map scale. (Because for instance you could have multiple map items in composer at different scales)
    – user52245
    Commented Jan 26, 2017 at 9:40
  • 1
    @iant I posted a link to another custom expression that will get the scale of your composer map programmatically.
    – user52245
    Commented Jan 31, 2017 at 3:27
3

Duplicate the layer, one with no symbology, and set different zoom levels/obstacle settings for the labels for each.

1

I had a similar situation with the obligation to label every road in a road layer. My solution was to create the canvas at a bigger size, export to PDF and then print the PDF to the size that was required.

For example, the finished product was required to be A3. On the canvas at A3 I could not fit all the labels. So I made the canvas A2 and that allowed all the labels to be seen. Create a PDF of the A2 size map and print it as A3.

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