35

Is there a way to select an attribute from a polygon layer and insert the value into a Virtual field of a point layer using "within" in the Field Calculator?

CASE
    WHEN within($geometry, geometry_polygon) THEN attribute_polygon
END

enter image description here

Taras
  • 32,823
  • 4
  • 66
  • 137
Lunar Sea
  • 1,986
  • 3
  • 24
  • 39

3 Answers3

29

For QGIS versions QGIS 3.16 and higher

Since QGIS 3.16 the functionality of the "refFunctions plugin" was emended into core. The current corresponding function is the overlay_within().

For QGIS versions lower than QGIS 3.16

Spatial joins are available in the Field Calculator after installing the "refFunctions" plugin.

geomwithin(targetLayer, targetField)

Keep in mind that:

This plugin is deprecated!

Taras
  • 32,823
  • 4
  • 66
  • 137
Lunar Sea
  • 1,986
  • 3
  • 24
  • 39
23

Out of the box, field calculator does not support spatial joins across feature layers. But, if you have a look at NathanW's post on the function editor for qgis expressions you will be able to make out that we can script our own data interaction.

The following script will allow you to express what you're after. It works by iterating through all features on the polygon layer and if there is a spatial join, then reference tabular data from the specified column:

from qgis.core import *
from qgis.gui import *
from qgis.utils import iface

allfeatures = None
index = QgsSpatialIndex()
indexMade = 0
refLayer = None

@qgsfunction(args="auto", group='Custom')
def spatialJoinLookup(layerName, refColumn, defaultValue, geom, feature, parent):

    if geom is None:
        return defaultValue

    # globals so we don't create the index, refLayer more than once
    global allfeatures
    global index
    global indexMade
    global refLayer

    # Get the reference layer
    if refLayer is None:
        for layer in iface.mapCanvas().layers():
            if layerName == layer.name():
                refLayer = layer
                break
    if refLayer is None:
        raise Exception("Layer [" + layerName + "] not found")

    # Create the index if not exists
    if indexMade == 0:
        index = QgsSpatialIndex()
        allAttrs = layer.pendingAllAttributesList()
        layer.select(allAttrs)
        allfeatures = {feature.id(): feature for (feature) in refLayer.getFeatures()}
        for f in allfeatures.values():
            index.insertFeature(f)
        indexMade = 1

    # Use spatail index to find intersect 
    fid = None
    ids = index.intersects(geom.boundingBox())
    for id in ids:
        fid = id
        break # Only get the first match.
    if fid is not None:
        return allfeatures[fid].attribute(refColumn)

    # Default
    return defaultValue

Polygon Layer Example

Below is an example of a polygon layer that you might have. I've also created a corresponding point layer that you will see in the final image.

enter image description here

Expression Usage

Note, if you want to use a separate column you must change the second argument to match the column name in the polygon dataset. Example, you could use the 'AreaNumber' column, but would have to match the column type in the field calculator settings.

enter image description here

Result

You can see that the default column value has been applied where there is no spatial join, and the other's have matched the correct data. Note the script I've given will only join on the first match. You would need to create some other business logic if your polygons were overlapping.

enter image description here

nagytech
  • 3,741
  • 18
  • 37
  • Many thanks, your script is working fine using 'geom' instead of 'geometry' in the first 'if' statement. Joining geometries this way may be quite useful e.g. for creating multiple labels on polygons. – Lunar Sea Jun 29 '15 at 10:01
  • Sorry, I don't know how I missed that. Hopefully there's no performance issues - I only tried it with a very small subset of records. – nagytech Jun 29 '15 at 10:08
  • Having only 100+ point features QGIS struggles with performance issues. Adding a new point feature is really a pain even there is no point feature displayed in the acutal canvas. Otherwise when zooming in QGIS speeds up. I've tried `CASE WHEN $scale < 10000 THEN spatialJoinLookupI('Polygons', 'AreaName', 'None', $geometry) END' but it's not working. Is there anything I can do to improve the performance? – Lunar Sea Jul 04 '15 at 19:15
  • @LunarSea I've updated the function to use a spatial index. It should be reasonably faster. – nagytech Jul 06 '15 at 10:00
  • Thanks for your help. The spatial join is much faster now but unfortunaetely something isn't working properly. I'm getting differnt results for points within one and the same polygon. – Lunar Sea Jul 06 '15 at 11:10
  • I'm not sure what the issue could be. You could try following Nathan W's post http://nathanw.net/2013/01/04/using-a-qgis-spatial-index-to-speed-up-your-code/. Could be that you have some residual intersections and may need to do another loop to filter them out. – nagytech Jul 09 '15 at 14:26
  • I've created a new PostGIS table containing four polygon features. Intersection only works for one of the polygon features. Please take a look at the attached screenshot. I will need some Python knowledge to understand this. – Lunar Sea Jul 09 '15 at 19:26
  • Hi, the script works GREAT to use on a pointshape which is within a polygon shape. Is there a way to tweak it so it also works for polygons within polygons? When I run it for this sake I get a lot of false matches. refFunctions does work for polygon within polygon but takes ages to do what your script does in 10 seconds. Impressive! – Hannes Ledegen Oct 12 '17 at 14:39
22

It can be done in Field Calculator with function aggregate(). In point layer create new field with field calculator expression like this:

aggregate(
layer:= 'polygon_layer_name',
aggregate:='concatenate',
expression:=joining_field_name,
concatenator:=', ',
filter:=intersects($geometry, geometry(@parent))
)

Where layer is polygon layer name written like string, aggreagate is aggregate function (can be used also sum etc.), expression is field from values will be taken,concatenator is joining character string (have to be set, even in this case) and filter is filtering features based on expression (in this case interesects layer geometry with geometry of parent layer).

For more info check Aggregates QGIS documentation.

For automatic updates can be used virtual fields or you can set the expression as Default value in Attributes Form settings in Layer Properties (Attribute form setting documentation).

enter image description here

Oto Kaláb
  • 6,895
  • 3
  • 29
  • 50
  • 4
    It should be noted that spatial functions (with geometry(@parent)) are supported only from QGIS 3 onwards. Just in case anyone reading this is still using 2.18... – she_weeds Mar 24 '19 at 08:02