6

I am using the following expression to visualize with a buffer, the nearest neighbors at a given distance:

case when

distance(
  $geometry,
  array_get(
    overlay_nearest(
      layer:=@layer,
      expression:=$geometry,
      limit:=2
    ),
    1
  )
)<0.9

then

with_variable(
    'dista',
        distance(
          $geometry,
          array_get(
            overlay_nearest(
              layer:=@layer,
              expression:=$geometry,
              limit:=2
            ),
            1
          )
),
    collect_geometries (
        array_foreach (
                overlay_nearest(
                    @layer,
                    $geometry,
                    limit:=1
                ),
            buffer(@element,@dista/1)
        )
    )
)

end

Screenshot:

enter image description here

I have developed this expression from the solutions presented by user Babel in the questions Clustering vectorpoints and Dissolve buffers if the intersection/overlap is greater than 50%.

My goal is to label count points within dissolved buffers.

Here is a handmade screenshot of the expected idea of counting points within dissolved buffers:

enter image description here

Implementing the solution proposed by user Babel:

For the label, I have adapted the constant buffer radius to a variable buffer radius according to distance:

case when 

distance(
  $geometry,
  array_get(
    overlay_nearest(
      layer:=@layer,
      expression:=$geometry,
      limit:=2
    ),
    1
  )
)<0.9

then 

with_variable(
    'radius',
    0.45,  -- change buffer size here
    array_length( 
        array_agg( 
            $id,
            group_by:= array_foreach (
                generate_series (
                    0,
                    array_length(
                        geometries_to_array( 
                            buffer (
                                collect(
                                    buffer ($geometry, @radius)
                                ),
                                0
                            )
                        )
                    )-1
                ),
                intersects (
                    $geometry,  
                    geometries_to_array( 
                        buffer (
                            collect(
                                buffer ($geometry, @radius)
                            ),
                            0
                        )
                    )[@element]
                )
            )
        )
    )
)
end

For the label placement properties (avoid text repetitions), I do something messy (copy/paste the same expression and injecting the centroid function):

with_variable(
    'radius',
    0.45,  -- change buffer size here
centroid (
        collect(
            $geometry,
            group_by:= array_foreach (
                generate_series (
                    0,
                    array_length(
                        geometries_to_array(
                            buffer (
                                collect(
                                    buffer ($geometry, @radius)
                                ),
                                0
                            )
                        )
                    )-1
                ),
                intersects (
                    $geometry,  
                    geometries_to_array(
                        buffer (
                            collect(
                                buffer ($geometry, @radius)
                            ),
                            0
                        )
                    )[@element]
                )
            )
        )
        )  
)

enter image description here

At the beginning the objective is achieved, but now the problem is the expression performance. For a layer with 57 points geometry, the labeling and the placement expressions needs 3 minutes to finish with an i7 processor (32GB RAM). I think it is too long for a layer with little information.

I would like, if possible, to ask for optimize a little more the efficiency of the first solution.

5
  • 1
    Regarding your update, this approach is always going to be time consuming with Geometry Generators, as Babel mentions it's iterated for every single feature and for each feature you are re-calculating entire expression (clustering, buffering, joining) on the fly... The way I see it you at least need to separately generate and store a cluster ID to each point first, based on nearest neighbour, then generate the geometry based on that (and also filtering the symbol so that it only generates a geometry for the first feature in each cluster ID group).
    – she_weeds
    Nov 21, 2023 at 9:19
  • Yes, I agree with you on what produces the bottleneck funnel in the solution approach. According to the requirements of my question, the solution proposed by Babel is totally adequate. Perhaps the problem resides in my initial approach to the problem. Perhaps the only solution is to pre-generate the geometries or perhaps there is a totally different approach to solving the problem. I will continue to investigating Nov 21, 2023 at 9:37
  • 2
    It might help if your original expression was cleaned up a bit. It seems really convoluted - I'm struggling to follow some of it. Why use make_line() and then distance() when you can just use distance() directly on two points? And why use array_foreach() - twice - when the overlay_nearest() is limited to 1 result? And the if() test seems unnecessary, of course the distance to the closest feature will be less than that of the second-closest feature (i.e. @dista variable - index 1 of array). But from your first screenshot it appears your buffers are generated lines, not polygons...
    – she_weeds
    Nov 21, 2023 at 10:51
  • Agreed also. I've cleaned up the if test for now. You can see the updated code in my question. At the moment I can't remove one of the array_foreach() nor the make_line(). However, the expression that draws the buffers is fast. The efficiency problem is focused more on the labeling expression Nov 21, 2023 at 12:07
  • 2
    Yeah, for labelling I think your best bet is to either use virtual layer or processing tools to generate some kind of id column to differentiate each "cluster" (merged group), to speed up counting and label positioning
    – she_weeds
    Nov 21, 2023 at 12:13

3 Answers 3

7

This isn't a complete answer but might provide you or others some ideas to combine with other suggestions.

Considerations:

  • Assumes you are OK with clustering algorithm approach (not necessarily same as nearest neighbour)

  • You need to forego a dynamic expression for clustering (which will significantly increase complexity and processing speed - I'm not aware of any way to do it in QGIS other than importing your data to PostGIS and using the ST_ClusterDBScan() function)

  • This is an alternative to Babel's dissolved buffer tool approach, still uses a processing tool with a static result but lets you change buffer size on the fly


  1. Use the DBSCAN clustering tool in the Processing Toolbox and set your maximum distance (0.9 metres?) and minimum cluster size (2). Your result is the same layer but with new columns CLUSTER_ID and CLUSTER_SIZE. If you want, use the Categorised symbology by CLUSTER_ID to check that the clusters are defined as expected.

  2. Create a geometry generator symbol with Polygon geometry type and generate your buffer with the appropriate expression:

  • static 100m buffer, merged by CLUSTER_ID: buffer(collect($geometry,"cluster_id"),100)
  • dynamic buffer based on distance to closest feature, merged by CLUSTER_ID: buffer(collect(buffer($geometry,distance($geometry,overlay_nearest(@layer,$geometry)[0])),"cluster_id"),0)
  1. To limit symbol generation to only one feature per cluster, and excluding points not in a cluster at all, use a data-defined override for Enable symbol layer with the following expression: "cluster_size" is not null and $id = array_agg($id,"cluster_id")[0]

click to enlarge

  1. Set CLUSTER_SIZE as your label expression, and under Placement > Geometry generator, use the same expression in step 2 and select Geometry type as Polygon. You can configure your label placement like it's in a polygon (e.g. placement mode Outside polygons, Offset from centroid, etc.)

  2. To limit label generation to one feature per cluster, go to label Rendering > Show label and use the following expression: $id = array_agg($id,"cluster_id")[0]


Result (clusters generated with max 10m distance):

5m buffer: enter image description here

Dynamic buffer: enter image description here

1
  • 3
    Excellent and completely new approach that gives an out-of-the-box solution. I really like your proposal of using DBSCAN clustering, and I am absolutely surprised by the exact similarity of the result with the Nearest neighbor based solution and DBSCAN solution. This confirms to me that the results of the project I am working on show a consistent pattern. I consider both solutions, Babel's and She_Weeds', to be excellent Nov 21, 2023 at 12:38
6

What is a bit tricky: expressions work on a per-feature basis, so there is no direct way to only label the dissolved buffer (and any indirect way would be too complicated for the task, an overkill). You can, however, label each point with the number of points inside the same buffer using the following expression. For other options, see my other solution.

Points labeled with the expression - red dissolved buffers just for visualisation purpose: enter image description here

with_variable(
    'radius',
    50,  -- change buffer size here
    array_length( 
        array_agg( 
            $id,
            group_by:= array_foreach (
                generate_series (
                    0,
                    array_length(
                        geometries_to_array( 
                            buffer (
                                collect(
                                    buffer ($geometry, @radius)
                                ),
                                0
                            )
                        )
                    )-1
                ),
                intersects (
                    $geometry,  
                    geometries_to_array( 
                        buffer (
                            collect(
                                buffer ($geometry, @radius)
                            ),
                            0
                        )
                    )[@element]
                )
            )
        )
    )
)
4
  • 2
    @ Babel, labelling per dissolved buffer can be achieved reasonably easily by using the Geometry Generator placement option (and specifying Around Point or Offset from point) and using your expression with a couple of adaptations - replace the first array_length with collect_geometries, replace the first $id with @geometry and wrap the collect_geometries in the centroid function. Admittedly, perhaps centroid is not the desired placement - also, as discussed, this will be even less performant than the counting expression alone.. but it's proof of concept :)
    – Matt
    Nov 21, 2023 at 9:53
  • @Matt indeed - it's possible, but not very straightforward and, at least to me, a bit too complex for what finally can be achieved much easier - that's what I meant with "indirect way". But yeah, in principle it's possible - feel free to add it as a further solution, will be happy to upvote it :-)
    – Babel
    Nov 21, 2023 at 9:58
  • 1
    It's not really worth an extra answer as it handles only the placement, not the core of the question that is the counting of the points. Besides, I just recycled your hard work. If someone comments and specifically asks for it then I will make an answer out of it.
    – Matt
    Nov 21, 2023 at 10:04
  • 2
    The best solution in my view would be creating the dissolved buffers as virtual layer and label this - like this it would still be dynamic and update when points are changed. To create dissolved buffers in virtual layers is easy with the query select st_union( st_buffer(p.geometry, 50)) from points as p, but it creates one multipart feature and I don't know of an easy way to keep disjoint features separate as you can do when using the buffer tool from Menu Vector in newer QGIS versions.
    – Babel
    Nov 21, 2023 at 10:12
4

If you want indeed to create one label per buffer, it is easier to indeed create dissolved buffers with the box Keep disjoint features separate checked to get one separate feature per "cluster". Then use this expression on the buffer layer to count points inside each buffer:

array_length(overlay_contains('points',$id))

enter image description here

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.