4

Using QGIS 3.30, I have a (line) layer of streams, each with a name attribute.

enter image description here

enter image description here

I am looking for a QGIS expression to find the distance to the nearest stream with the same name, or NULL if there is only the one stream with that name in the layer. For some names, there may be up to 11 streams with the same name.

It seems to me that this should be possible using overlay_nearest, using a filter of same name and different id, but have had no luck coming up with the right expression.

I'd thought something along these lines, but this returns nothing. In any case, it also needs to exclude the item with the same ID.

with_variable('parent_name',"name",
    with_variable('parent_id',@id,
    overlay_nearest(@layer,
        filter:=@parent_name="name",
        expression:=distance(
            @geometry,geometry(get_feature_by_id(@layer,@parent_id))
        )
    )
))

This question: Find nearest point with the same attribute appears similar, but the answers didn't seem to help, probably because the items with the same name are not necessarily nearby.

A copy of the dataset can be found at https://ozultimate.com/downloads/dcs_streams.gpkg

5
  • 1
    I believe the filter should be something like attribute(@parent, 'name') = "name".
    – Matt
    Jun 17, 2023 at 6:37
  • It looks that some aggregates have an unexpected behavior under some conditions, so try it in a different way, use aggregate.
    – Mayo
    Jun 17, 2023 at 17:03
  • You didn't post any data to reproduce the problem and test the solution, so I hope it works. Otherwise, provide sample data.
    – Babel
    Jun 17, 2023 at 17:19
  • 1
    @Matt to my knowledge, @parent doesn't work inside an overlay function. It works for aggregate functions only - see the answers here: gis.stackexchange.com/q/391120/88814
    – Babel
    Jun 17, 2023 at 17:23
  • @Babel, good to know. Thanks for the reference to the other thread. There is a lot of information there!
    – Matt
    Jun 18, 2023 at 7:40

2 Answers 2

4

In the overlay function's filter condition, you can't directly refer to a variable to insert the current value stored in this variable. You have to create the whole overlay function as a text string and convert it to a function with eval(), whereas the variables are outside the text an should be concatenated with the pipes ||.

In addition, the value stored in the @parent_name variable has be be passed as a string to be concatenated, so you have to use three single quotes ''' (two single quotes are used inside a string to introduce a quote and thus prevent the string being ended; one single quote simply ends the string).

I also added an array_sort() function to sort the distances in increasing order and got the second element of the array (index operator \[1\]; first element is always the current feature with distance=0) to get the distance to only the nearest of the features with the same name.

A last thing to be aware of: due to a bug, when using the expression with the layer you're working on as target layer for the overlay_nearest() function (using variable @layer), results will be incorrect for the first one or two features (see here for details). For this, duplicate the layer and then apply the expression on the initial layer, but refering to the duplicated layer inside the overlay_nearest() function (line 6).

So the whole expression looks like this:

array_sort(
with_variable ('parent_name',"name",
with_variable ('parent_id',@id,
    eval('
        overlay_nearest(
            ''duplicated_layer'',  -- replace with name of your duplicated layer
            expression:= round (distance(
                @geometry,
                geometry (get_feature_by_id (@layer,'  ||  @parent_id || '))
            ),0),
            limit:=20,  -- use a convenient number (max. number of features with same name)
            filter:= name =''' || @parent_name || ''' and @id <> '''  ||  @parent_id  ||  '''
        )
    ')
)))[0]
6
  • I tried running this on the sample dataset now attached, and got NULL for all entries. I wasn't able to debug it. Jun 18, 2023 at 8:41
  • Now tested with your dataset, see the revised expression in the answer. Two changes: add a limit argument (by default, you get just one feature) and instead of singe quotes ' before/after concatenating the variable @parent_name, use three single quotes: '''
    – Babel
    Jun 18, 2023 at 9:31
  • 1
    Seems to work correctly for all but Features with ID 1 and 2, which come up with a NULL distance, even though they have the same name as each other (ALLENS CREEK in the sample data set). For some reason, the expression returns no element with distance 0 for these features, so taking index [1] removes the only element, being the actual distance. I tried creating new layers with other features in ID 1, and ID 1 & 2, and the issue seems to only occur with the features with the first 2 IDs. Not sure what the root cause is though. Strange. Jun 18, 2023 at 12:45
  • 1
    You're right. I modfied the expression with an additional filter condition. Now, ID 2 will generate a correct value, only ID 1 is empty. Seems to be an issue with overaly function for the very first feature, not quite sure what it is...
    – Babel
    Jun 18, 2023 at 13:40
  • 1
    Posted this as a separate question: gis.stackexchange.com/q/461886/88814
    – Babel
    Jun 18, 2023 at 14:04
2

For those simply trying to solve the problem in any way, the following approach will work.

To summarise, for each feature, the expression:

  1. Builds a string of ids of features with the same name, using aggregate/concatenate
  2. Converts this to an array (string_to_array)
  3. Gets the distance of the original feature to each feature in this array, using array_foreach and distance - this is returned as an array
  4. Removes the 0 from the array using array_filter (as the distance of the feature to itself will be 0 - we want the distance to another feature)
  5. Finally, gets the minimum value in the array, which will be the minimum distance (or NULL)

I've found that the approach of building arrays of feature ids using aggregate/concatenate, and iterating using array_foreach is a useful solution to a variety of complex QGIS expression problems. However, the resulting expressions tend to be quite involved, and not generally that efficient.

I would be keen to see a simpler approach if one exists.

array_min(
    array_filter(
        array_foreach(
            with_variable('parent_name',"name",(
                string_to_array(
                    aggregate(@layer,
                              aggregate:='concatenate',
                              expression:=to_string(@id),
                              filter:="name"=@parent_name,
                              concatenator:=','),
                delimiter:=',')
                )
            ),
        distance(@geometry,geometry(get_feature_by_id(@layer,@element)))
        ),
    @element>0)
)

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.