12

In QGIS 3.18, I have two point layers: layer_1 with attribute id_1 and layer_2 with id_2. I want to use the overlay_nearest function: overlay_nearest(layer[,expression][,filter][,limit=1][,max_distance][,cache=false])

Applied on layer_1, I want to get for each point on layer_1 all points on layer_2 where id_2 has a different value from id_1 (to connect them by a line). However, I can't find out how the syntax of the filter-argument works, how to refer to attribute values of two different layers for comparison.

The basic expression is: overlay_nearest( 'layer_2', $geometry, [filter])

How is the syntax of the [filter] part? filter:="id1" <> "id2" obviously doesn't work. $id <> "id2" and $id <> "id1" don't work either.


Edit: see this example here to make the idea clearer.

  • Red dots = layer1, labeled with id1
  • White dots = layer2, labeled with id2

Each red point should be connected to it's nearest white point, except when their id is the same. However, on the upper left (red arrows), you see that 12 is connected to 12, 17 to 17:

enter image description here

The expression used in geometry generator on layer1 for this is as follows (the filter condition is on lines 6 to 18):

collect_geometries (
    array_foreach (
        overlay_nearest( 
            'layer2', 
            $geometry, 
            filter:=attribute (
                get_feature_by_id (
                    'layer1', 
                    $id
                ), 
                'id1'
            ) <> attribute (
                get_feature_by_id (
                    'layer2', 
                    $id
                ), 
                'id2'
            ) , 
            limit:=1
        ),
        make_line (
            $geometry, 
            @element
        )
    )
)

*Edit

See more here in the responses to the feature request, especially:

The purpose of the filter parameter is to reduce the dataset (layer2) to use for research, so it's applied before it proceeds to any overlay test

https://github.com/qgis/QGIS/issues/43146

7
  • 1
    Came accross the same issue recently. Not sure if its a bug or just me not understanding how this works...
    – MrXsquared
    Commented Mar 25, 2021 at 22:41
  • 1
    Somehow consoling to see others with the same problem :-) So let's hope we get cleared the mystery here...
    – Babel
    Commented Mar 25, 2021 at 22:43
  • and something like filter:=eval('"id2" <>' || "id1"') (just an idea, not tested) Commented Mar 29, 2021 at 13:36
  • @J. Monticolo: unfortunately, neither filter:=eval("id_2" || '<>' || "id_1") nor if both terms in the brackets are in single quotes ' does work. At least the syntax doesn't produce an error, but returns an empty result, even if there are corresponding features. I guess the problem is somehow to tell QGIS which attribute comes from which layer.
    – Babel
    Commented Mar 29, 2021 at 14:01
  • filter:=attribute (get_feature_by_id ('layer_1', $id), 'id_1') <> "id_2" seems to work - have to check it, will come back later.
    – Babel
    Commented Mar 29, 2021 at 14:13

3 Answers 3

14
+50

I tested some configurations, tried to understand how it works. So ! I figure it out.

The scope of the filter parameter in the expression function of overlay_nearest is purely the layer set in the layer argument, as if you put this expression in the layer select by expression.

To achieve your goal, I came up with another solution. Instead of select the nearest $geometry of the other layer, I selected the nearest $currentfeature. And in the end, instead of building a line directly, I put an if condition to return a NULL geometry if the nearest layer_2 feature id2 equals the current id1.

collect_geometries(
    array_foreach(
        overlay_nearest(
            'layer_2',
            $currentfeature,
            limit:=1
        ),
        if(
            condition:=attribute(@element, 'id2') = "id1",
            result_when_true:=NULL,
            result_when_false:=make_line(geometry(@element), $geometry)
        )
    )
)
EDIT : display more connections / increase the overlay_nearest limit

With the QGIS expression above, if you increase the overlay_nearest limit, say 2 instead of 1, if one of the collected geometries is NULL because it meets the condition, it nullifies all the geometries and it's not what we want.

Just replace in this case the NULL condition result with a zero length line :

collect_geometries(
    array_foreach(
        overlay_nearest(
            'layer_2',
            $currentfeature,
            limit:=2 -- update here
        ),
        if(
            condition:=attribute(@element, 'id2') = "id1",
            result_when_true:=make_line($geometry, $geometry), -- update here
            result_when_false:=make_line(geometry(@element), $geometry)
        )
    )
)
EDIT : each layer_1 point have the same number of lines

Hey ! Yes it works but when layer_1.id1 = layer_2.id2 I got only one line for this point ...

Okay ... I heard you, you are right, GIS is a demanding discipline so I'm reviewing my copy !

To achieve this, I defined a variable, placed on top for easy access, lines_per_point. You can set the number of lines per point, here 2.

With the comment of @MrXsquared, I used array_filter instead an if condition in the array_for_each, it drops all NULL values.

But ! I increased by 1 the overlay_nearest limit of features returned (I considered that layer_2 identifiants are unique, so filter can drop for one layer_1 point only one line maximum (when layer_1.id1 = layer_2.id2)).

Ok, it returns arrays of lenght 2 or 3. Here I used array_slice to get only the two first features (beware, the slice doesn't work as a Python list, it includes the 2 bounds), so [0, 1], so [0, lines_per_point - 1].

We have now an array of length 2 with layer_2 features. So, I applied an array_foreach to create the line with make_line(geometry(@element), $geometry).

(Obviously, you'll get different number of lines for each layer_1 points if the lines per point equals the number of features of layer_2)

The QGIS expression below :

with_variable('lines_per_point', 2,  -- adjust here
collect_geometries(
    array_foreach(
        array_slice(
            array_filter(
                overlay_nearest(
                    'layer_2',
                    $currentfeature,
                    limit:=@lines_per_point + 1
                ),
                attribute(@element,'id2') <> "id1"
            ),
            0, @lines_per_point - 1
        ),
        make_line(geometry(@element), $geometry)
    )
))
9
  • Thanks a lot for your effort! Indeed, the filter of overlay_nearest works on the layer set in the layer argument - but it seems strictly limited to it. Not even addressin explicitely another layer with get_feature_by_id does work and I'm still not sure if it is by purpose, a but or if I miss something.
    – Babel
    Commented Apr 2, 2021 at 21:55
  • Your solution is great as a workaround for the missing abilitiy of the overlay_nearest filter to compare attributes from different layers. It works great! However I was wondering now to increase the number of connections, let's say to 2. See the screenshot: normally 2 lines are drawn. But if nearest point has the same id as the current point, no line at all is drawn: i.sstatic.net/AclCk.png - shuldn't just one line be drawn? See yellow area: there is correctly no connection from 2 to 2 and 1/1. But 1 and 2 should each be connected to 7 as 2nd nearest. Have to figure out why not.
    – Babel
    Commented Apr 2, 2021 at 22:04
  • 2
    @Babel: see my edit. Commented Apr 3, 2021 at 7:43
  • 2
    great! may I suggest to use array_filter() instead of array_foreach(if()) to prevent the array from containing NULL values at all, like array_filter(overlay_nearest('layer_2',$currentfeature,limit:=2),attribute(@element,'id2') <> "id1"). Only disadvantage is, that you will only get 1 result at the end if the other one is filtered out.
    – MrXsquared
    Commented Apr 3, 2021 at 12:31
  • 1
    @J. Monticolo: great job, indeed! And replacing the second argument of the with_variable from 2 to aggregate( 'layer2', 'count',$id), one can draw a line from every point of layer 1 to every point of layer 2, except when their id has the same value.
    – Babel
    Commented Apr 4, 2021 at 14:49
7

With QGIS 3.22.1 and 3.16.15 and newer versions, it will be possible to use a much simpler and more efficient expression to achieve the expected result:

make_line(
    eval(
        'overlay_nearest(\'layer2\',
        $geometry,
        filter:=id2<>' || "id1" || ' )' 
        )[0],
    $geometry
    )

This expression also works with the current QGIS 3.23.0-master (since commit 768ccc6)

7
  • 1
    Thanks so much for the patch and sharing
    – pigreco
    Commented Oct 31, 2021 at 17:39
  • So if I understood correctly, with QGIS 3.22.0 it does not yet work, right? Just asking because I thought point releases do not add functionality, only bug fixes, but this seems to be wrong at least in this case. Anyway, this is indeed great, thanks a lot!
    – Babel
    Commented Oct 31, 2021 at 20:06
  • 2
    @Babel it was a bug (an incorrect caching) that prevented the above expression to work. Commented Oct 31, 2021 at 20:26
  • 2
    The above expression is a valid expression in QGIS < 3.22.1, but it gives incorrect results. In QGIS 3.22.1 it will give the correct results. Commented Oct 31, 2021 at 20:33
  • 1
    @bugmenot123 unfortunately it is necessary for now. This is a "workaround" made possible by PR github.com/qgis/QGIS/pull/45744. See the feature request github.com/qgis/QGIS/issues/43146. Commented Feb 1, 2022 at 18:59
3

Starting from this dataset [1] taken from here [0], this expression works for me: (QGIS 3.22.0)

collect_geometries(
         make_line(
             $geometry,
             geometry(get_feature('layer2','id2',
                         attribute( 
                         array_filter( 
                         overlay_nearest('layer2',$currentfeature,limit:=-1), 
                attribute(@element,'id2' ) != "id1")[0], 'id2' ))))
                  )

enter image description here

[0] https://github.com/qgis/QGIS/issues/43146#issuecomment-836472827

[1] https://drive.switch.ch/index.php/s/af5cHue6P3NA9xM

1
  • 1
    Indeed great to see how many good answers this question gets. A pity I can accept only one, so I'll leave the initial accept.
    – Babel
    Commented Oct 31, 2021 at 20:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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