4

In my project layers (QGIS 3.6.2) I use data-defined label placement. I am trying to set up the placement as follows:

  • By default, the placement chosen in Label Settings is used (in my case, "around centroid").
  • If the X and Y fields are populated (i.e., the label has been placed manually), these values are used.
  • I have a boolean T/F field called "UsePOS" in my data; if it is checked (true), and if the X and Y fields are null, then the X/Y placement uses a calculated point (in my case, a point-on-surface). If it is false, the null X and Y fields are used (i.e., the default placement).

Here's what I tried: in Label Settings, I defined the X placement with the following expression…

if( "X" is not NULL,
    "X",
    if( "UsePOS",
        x(  point_on_surface(  $geometry  )  ),
        "X"
      )
  )

…and the Y field with a like expression. This worked perfectly as expected—by default the label is placed at the centroid; if the box is checked, it jumps to the point-on-surface, and if there is data in the X/Y fields, then the label is pinned there.

However, the problem arises when I try to manually place a label. In that case, because the X/Y placement isn't directly defined by the X/Y fields, QGIS invokes auxiliary storage, which is not what I am using for label placement, and which apparently cannot be suppressed (see my comment in this thread).

So I assume that 1) I need to directly assign label placement to my X/Y fields, 2) I need to use an expression in Field Calculator to populate those fields, and 3) those fields need to be virtual so that they respond dynamically to whether I manually move the label, whether the checkbox is checked, etc. However, entering the above expression in Field Calculator causes QGIS to crash when the label is moved manually, or when I check the checkbox (which I expected, since I don't think a virtual field can reference itself).

The question is, what is the proper way to set up what I want, so that I can still manually move labels, using only data-defined labeling and not auxiliary storage?

2

The most effective workaround I've found (EDIT: not anymore; see the "default value" method for a fully functional solution) uses rule-based labeling.

First, it's necessary to populate all the X/Y fields with the labels' current (i.e., default) position, using x($geometry) and y($geometry). Then, create a rule where "X" is not null and define its override placement using the X and Y fields.

For the second rule, the ELSE case, define the override placement using the expression:

if("UsePOS",
   x(point_on_surface($geometry)),
   "X"
   )

To use this placement, you would check "UsePOS" and unpin the label (thus deleting its X and Y values). An imperfection arises if you again wish to move this label manually, as that will once again invoke auxiliary storage (which, if you allow it, will then re-define all your labels' placement to the auxiliary fields), and you cannot use the Pin/Unpin tool to re-set its X/Y values, so you'll have to manually populate those again.

1

The point is, that you can not use data defined override positioning with an expression AND use move labels feature at the same time. You can only do either of them. (Or enter your coordinates manually without using the move label feature).

I know this is not a satisfying solution, but if you want to use the move label feature, you need to first remove your expression from data defined override and use only your fields "X" or "Y". Then enable edit mode for your layer, move the labels, save the layer and enter your expression back into data defined override.

I think background is, that QGIS cannot save coordinates into expressions. Only into fields. Thats why it is trying to use auxiliary storage as it is not recognizing the expression as field.

You may want to do a feature request, but I can imagine (I am not into software developing though), implementing this could be not the easiest task.

  • Yes, that's exactly the problem: the Move Label tool and an expression-based placement override can't be used together. There are actually a couple more solutions, if one is resigned to the unsatisfying. One is just to go back to what I had been doing, and using point_on_surface() to selectively populate the X/Y fields where I want to apply it. Another imperfect solution, using rule-based labeling, I'll discuss as a separate answer. As a feature request, I could just suggest adding "point on surface" as a default placement option; though that would only address my particular task. – Nathan May 7 at 20:23
1

Once you manually place a label, QIS will ask you to select the field to use as primary key between the layer's own table and the auxiliary storage. I created a virtual field "ID" (simply calculated as "$id") and used it as a primary key.

elect primary key

Once you have done that, QGIS will make two new fields available for you:

  1. "auxiliary_storage_labeling_positionx" and
  2. "auxiliary_storage_labeling_positiony"

auxiliary_storage_labeling_position

At that point you can use them to properly place your labels. To make things easier, I created an auxiliary virtual field ("xlabel") which arbitrates which coordinate (auto, manually placed or assigned numerically) to use.

fields

Selecting "xlabel" (and "ygeom" because you need both coordinates, otherwise it will not draw labels correctly) for the placement in the Label dialog will make the trick.

relevant fields here:

  • xnum: [optional] numerical coordinate, manually input into the field
  • UsePos2: boolean-like flag [0|1] [optional], manually input into the field
  • xman: virtual field which replicates "auxiliary_storage_labeling_positionx", to make things more clear. Automatically calculated from the manually positioned label (if the label was manually moved).
  • xgeom: the coordinate of the feature. Automatically calculated.
  • X: the x-coordinate to store the arbitrated value between xman, xnum, or xgeom. This is an intermediate field and is automatically calculated.
  • xlabel: the final value used to draw the label. Use this in the "Placement" option.

After manually moving labels, xman gets updated, and depending on UsePos and xnum, also xlabel will be updated.

Fianlly, select xlabel in the Placement pane -> Data defined -> Coordinate X.

Label Placement

;)

  • I'm not sure I understand the proposed solution here—I've described auxiliary storage as the problem, but it seems you're suggesting it as the answer? I'm not opposed to using it if it works, but it doesn't seem to. Trying your setup (but with regular virtual fields, since I don't know how to make an auxiliary one) gives some problems: Manual placement using X_calc / Y_calc has no effect; the tool doesn't populate those fields. "UsePOS" has no effect, since it's assigned to blank aux fields, not to the point-on-surface function. X / Y are always null; nothing is assigned to them. – Nathan May 7 at 16:00
  • Please see updated answer, I hope the example is clearer now. – RafDouglas May 9 at 14:28
  • It's clearer, but I'm not sure I've thought it through correctly, as I still have some questions. But it does get a lot closer to seeing how virtual fields could be applied to solve the problem. I think we were on the right track with that, but it seems the key was to use the definable Default Value rather than virtual fields, because 1) for reasons I don't fully understand, a virtual field can't evaluate itself but Default Value can, and 2) my solution doesn't require auxiliary storage or any new fields (except "UsePOS"). – Nathan May 9 at 21:14
  • My remaining questions, only if you're interested in following on: -If "UsePOS" is false, you choose "xgeom"—but "xgeom" is defined as POS…? What happens if "UsePOS" is true? -What do you use "xnum" for, if manual X/Y values are entered into the aux fields? -What do you use "X" for, if X/Y override placement is defined by the aux fields? -Finally, once the value for "xlabel" is chosen, how do you get that value into the aux fields, so that QGIS will place the label there? – Nathan May 9 at 21:18
  • The trick is that xlabel is used to set the coords of the label, because you cannot override aux fields, afaik. In the example there are some intermediate fields which are ther for clarity, and which you could skip in a production case. xgeom is a calculated field with the actual feature X coordinate, is not influenced by UsePOS. xnum is used to set the numeric coordinate xman is the coordinate of the manually-placed label – RafDouglas May 13 at 14:16
1

I have found an effective solution, using expression-defined default values for the X and Y fields.

In Layer Properties, from the Attributes Form pane, I have set the default value for X using the expression:

if( "UsePOS" ,
x( point_on_surface( $geometry ) ),
"X" )

…and likewise for Y. I have also set both to "apply default on update"; this way, QGIS always checks whether "UsePOS" is selected, and if so, calculates X and Y according to the point-on-surface. If "UsePOS" is not selected, QGIS uses the value already entered in X or Y, if any, allowing me to manually adjust the label placement.

There is one subtle difference in this solution from the question as I originally posed it. I had intended for manual placement to override either the POS or the centroid option—in fact, I now realize, rather than using an expression-based data-defined override placement, what I really wanted was an expression-based data-defined default placement (which QGIS doesn't offer). But in this solution, the POS option overrides manual placement, meaning that it's necessary to un-check "UsePOS" before manually moving the label.

EDIT: That difference can easily be overcome by changing the expression. For example, the following expression not only allows manual placement to override the selection of "UsePOS", but also introduces a further override called "HCen" and "VCen", which locks the X or Y coordinate, or both, to the geometric center of the feature. I use this as a quick way to "snap" the label to the feature's mid-point, and as a way to move the label up and down but not side to side (or vice versa) by locking just the X or Y coordinate. (NOTE: This is the expression for the X field; for the Y field, just swap each "x" for "y" in the expression, and transpose "HCen" and "VCen".)

if( "HCen",
    ( x_min( $geometry ) + x_max( $geometry ) ) / 2,
    if( "X" is not NULL AND "X" != x( point_on_surface( $geometry ) ),
        "X",
        if( "UsePOS",
            x( point_on_surface( $geometry ) ),
            if( "VCen",
                x( $geometry ),
                NULL
            )
        )
    )
)

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.