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

Babel
  • 71,072
  • 14
  • 78
  • 208
  • 1
    Came accross the same issue recently. Not sure if its a bug or just me not understanding how this works... – MrXsquared Mar 25 '21 at 22:41
  • 1
    Somehow consoling to see others with the same problem :-) So let's hope we get cleared the mystery here... – Babel Mar 25 '21 at 22:43
  • and something like filter:=eval('"id2" <>' || "id1"') (just an idea, not tested) – J. Monticolo Mar 29 '21 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 Mar 29 '21 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 Mar 29 '21 at 14:13
  • Unfortunately not, see edited question – Babel Apr 01 '21 at 10:44
  • Let's hope your bounty (thanks, by the way!), which is active for some days to come, brings further attention to this problem and maybe even a solution - otherwise indeed it would be good to report it. – Babel Apr 01 '21 at 19:36

3 Answers3

13

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)
    )
))
Matt
  • 16,843
  • 3
  • 21
  • 52
J. Monticolo
  • 15,695
  • 1
  • 29
  • 64
  • 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 Apr 02 '21 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: https://i.stack.imgur.com/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 Apr 02 '21 at 22:04
  • If I just use the overlay_nearest part of the expression with limit:=2 on layer1 (red), it clearly shows in the preview for feature 2 on layer 1 two matching features on layer2 (white): feature 2 and feature 7: https://i.stack.imgur.com/bQijs.png - applying the same to the geometry generator, no line at all is drawn: line from 2 to 2 is filtered out (correct), but line from 2 to 7 does not appear, either. – Babel Apr 02 '21 at 22:12
  • 2
    @Babel: see my edit. – J. Monticolo Apr 03 '21 at 07: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 Apr 03 '21 at 12:31
  • @J. Monticolo: Thanks, that is great and works perfect! I thus upvoted the question, but will wait to accept it. I still hope to get an "authoritative" answer regarding the filter-condition of overlay_nearest - if there is definitely no way to use in the way I intended to and we have to use some workaround for this. – Babel Apr 03 '21 at 12:50
  • @MrXsquared: great comment, would be worth posting it as a separate answer. In the comments, it gets overlooked – Babel Apr 03 '21 at 12:51
  • New edit, new solution ! – J. Monticolo Apr 04 '21 at 09: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 Apr 04 '21 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)

Andrea Giudiceandrea
  • 1,319
  • 10
  • 17
  • 1
    Thanks so much for the patch and sharing – pigreco Oct 31 '21 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 Oct 31 '21 at 20:06
  • 2
    @Babel it was a bug (an incorrect caching) that prevented the above expression to work. – Andrea Giudiceandrea Oct 31 '21 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. – Andrea Giudiceandrea Oct 31 '21 at 20:33
  • The fix was back-ported to 3.16, so it will be also available since QGIS 3.16.15 onward. – Andrea Giudiceandrea Nov 07 '21 at 19:42
  • Is it really necessary to eval() here (which is not very elegant imo) or was that just your approach in the example? – bugmenot123 Feb 01 '22 at 13:43
  • 1
    @bugmenot123 unfortunately it is necessary for now. This is a "workaround" made possible by PR https://github.com/qgis/QGIS/pull/45744. See the feature request https://github.com/qgis/QGIS/issues/43146. – Andrea Giudiceandrea Feb 01 '22 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

Andrea Giudiceandrea
  • 1,319
  • 10
  • 17
pigreco
  • 4,460
  • 1
  • 15
  • 32
  • 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 Oct 31 '21 at 20:13