QGIS 3.x
Usually I'd do this efficiently in PostgreSQL/PostGIS (and would always recommend to have a look into SQL awesomeness), but I had a simple case to solve in pyqgis a few weeks ago.
The fairly simple script evolves around creating a dict with QgsSpatialIndex
objects for each distinct value in the JL_FIELD
(in my case, both layers had the exact same distinct values), i.e. that attribute you want to match on, from the JOIN_LYR
and execute their .nearestNeighbor()
method for each point in the BASE_LYR
with matching BL_FIELD
.
Run this from within QGIS (Python Console) and with your layers loaded into the project:
### setup
## input vars
BASE_LYR = '<base_lyr>' # layer to which you want to find NNs and add their attributes
JOIN_LYR = '<join_lyr>' # layer to find the NNs from
BL_FIELD = '<bl_field>' # field to match in BASE_LYR
JL_FIELD = '<jl_field>' # field to match in JOIN_LYR
_PREFIX = '<prefix>' # prefix to add to appended fields
## working vars
indexes = {}
attributes = {}
fields = []
### create dict of 'QgsSpatialIndex' objects for each distinct value in JL_FIELD
## process JOIN_LYR first
layer = QgsProject.instance().mapLayersByName(JOIN_LYR)[0]
## add _PREFIX to field (!not safe for provider dependent field name length!)
for field in layer.fields():
field.setName(_PREFIX + field.name())
fields.append(field)
## get names into list for faster lookups later
_names = [field.name() for field in fields]
## iterate features of JOIN_LYR...
for feature in layer.getFeatures():
_mf = feature[JL_FIELD]
## ...and if the dict key for JL_FIELD exists, add the feature to its 'QgsSpatialIndex'...
if _mf in indexes:
indexes[_mf].insertFeature(feature)
## ...or create the key and 'QgsSpatialIndex' and insert feature
else:
indexes[_mf] = QgsSpatialIndex()
indexes[_mf].insertFeature(feature)
## also, extract the attributes and combine with new field names for faster lookup later
attributes[feature.id()] = dict(zip(_names, feature.attributes()))
### get nearest neighbors and add their and attributes
## process BASE_LYR
layer = QgsProject.instance().mapLayersByName(BASE_LYR)[0]
## just to see some progress in QGIS Python Console...
_cnt = layer.featureCount()
## open layer in edit session, with auto-commit or -rollback
with edit(layer):
## add fields from JOIN_LYR to BASE_LYR
for field in fields:
layer.addAttribute(field)
## iterate over features
for i, feature in enumerate(layer.getFeatures()):
## silly way to show percentage in 10% steps
if i / _cnt * 100 % 10 <= 0.001:
print('{}%'.format(int(i / _cnt * 100)))
## find actual nearest neighbor in 'QgsSpatialIndex' object at the dict key of this features BL_FIELD value
nn = indexes[feature[BL_FIELD]].nearestNeighbor(feature.geometry().asPoint(), 1)[0]
## append the attributes
for field, value in attributes[nn].items():
feature.setAttribute(field, value)
## initialize the updates
layer.updateFeature(feature)
## done
print('100%')
I had no time for fancyness, any form of fallback or error catching, and this has some parts rather intolerant towards uncommon values and such, so there is plenty room for conceptual improvement, but this worked for me with reasonable performance. See if you can adapt to your case...
Btw., I had about 450 distinct values (and thus QgsSpatialIndex
objects) and 40k features in JL_LAYER
.
Note that QgsSpatialIndex
does only compare the bbox extent; for non-point geometries this will not fully guarantee the actual closest match. If you find some mismatches, you can e.g. implement a second step to measure the actual distance for when .nearestNeighbor()
returns more than one feature.