diff --git a/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py b/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py
index 084a27c2085..6923764d923 100644
--- a/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py
+++ b/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py
@@ -150,6 +150,7 @@ from .Smooth import Smooth
from .SnapGeometries import SnapGeometriesToLayer
from .SpatialiteExecuteSQL import SpatialiteExecuteSQL
from .SpatialIndex import SpatialIndex
+from .SpatialJoin import SpatialJoin
from .SplitWithLines import SplitWithLines
from .StatisticsByCategories import StatisticsByCategories
from .SumLines import SumLines
@@ -166,7 +167,6 @@ from .VectorSplit import VectorSplit
from .VoronoiPolygons import VoronoiPolygons
from .ZonalStatistics import ZonalStatistics
-# from .SpatialJoin import SpatialJoin
pluginPath = os.path.normpath(os.path.join(
os.path.split(os.path.dirname(__file__))[0], os.pardir))
@@ -180,9 +180,6 @@ class QGISAlgorithmProvider(QgsProcessingProvider):
self.externalAlgs = []
def getAlgs(self):
- # algs = [
- # SpatialJoin(),
- # ]
algs = [AddTableField(),
Aggregate(),
Aspect(),
@@ -293,6 +290,7 @@ class QGISAlgorithmProvider(QgsProcessingProvider):
SnapGeometriesToLayer(),
SpatialiteExecuteSQL(),
SpatialIndex(),
+ SpatialJoin(),
SplitWithLines(),
StatisticsByCategories(),
SumLines(),
diff --git a/python/plugins/processing/algs/qgis/SpatialJoin.py b/python/plugins/processing/algs/qgis/SpatialJoin.py
index 5fe3cc2ce30..aae392a8be6 100644
--- a/python/plugins/processing/algs/qgis/SpatialJoin.py
+++ b/python/plugins/processing/algs/qgis/SpatialJoin.py
@@ -31,28 +31,31 @@ __revision__ = '$Format:%H$'
import os
from qgis.PyQt.QtGui import QIcon
-from qgis.PyQt.QtCore import QVariant
-from qgis.core import QgsFields, QgsField, QgsFeatureSink, QgsFeature, QgsGeometry, NULL, QgsWkbTypes, QgsProcessingUtils
+from qgis.core import (QgsFields,
+ QgsFeatureSink,
+ QgsFeatureRequest,
+ QgsGeometry,
+ QgsProcessing,
+ QgsProcessingParameterBoolean,
+ QgsProcessingParameterFeatureSource,
+ QgsProcessingParameterEnum,
+ QgsProcessingParameterField,
+ QgsProcessingParameterFeatureSink)
from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
-from processing.core.parameters import ParameterVector
-from processing.core.parameters import ParameterNumber
-from processing.core.parameters import ParameterSelection
-from processing.core.parameters import ParameterString
-from processing.core.outputs import OutputVector
from processing.tools import vector
pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0]
class SpatialJoin(QgisAlgorithm):
- TARGET = "TARGET"
+ INPUT = "INPUT"
JOIN = "JOIN"
PREDICATE = "PREDICATE"
- SUMMARY = "SUMMARY"
- STATS = "STATS"
- KEEP = "KEEP"
+ JOIN_FIELDS = "JOIN_FIELDS"
+ METHOD = "METHOD"
+ DISCARD_NONMATCHING = "DISCARD_NONMATCHING"
OUTPUT = "OUTPUT"
def icon(self):
@@ -74,32 +77,40 @@ class SpatialJoin(QgisAlgorithm):
('within', self.tr('within')),
('crosses', self.tr('crosses')))
- self.summarys = [
- self.tr('Take attributes of the first located feature'),
- self.tr('Take summary of intersecting features')
+ self.reversed_predicates = {'intersects': 'intersects',
+ 'contains': 'within',
+ 'isEqual': 'isEqual',
+ 'touches': 'touches',
+ 'overlaps': 'overlaps',
+ 'within': 'contains',
+ 'crosses': 'crosses'}
+
+ self.methods = [
+ self.tr('Create separate feature for each located feature'),
+ self.tr('Take attributes of the first located feature only')
]
- self.keeps = [
- self.tr('Only keep matching records'),
- self.tr('Keep all records (including non-matching target records)')
- ]
-
- self.addParameter(ParameterVector(self.TARGET,
- self.tr('Target vector layer')))
- self.addParameter(ParameterVector(self.JOIN,
- self.tr('Join vector layer')))
- self.addParameter(ParameterSelection(self.PREDICATE,
- self.tr('Geometric predicate'),
- self.predicates,
- multiple=True))
- self.addParameter(ParameterSelection(self.SUMMARY,
- self.tr('Attribute summary'), self.summarys))
- self.addParameter(ParameterString(self.STATS,
- self.tr('Statistics for summary (comma separated)'),
- 'sum,mean,min,max,median', optional=True))
- self.addParameter(ParameterSelection(self.KEEP,
- self.tr('Joined table'), self.keeps))
- self.addOutput(OutputVector(self.OUTPUT, self.tr('Joined layer')))
+ self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT,
+ self.tr('Input layer'),
+ [QgsProcessing.TypeVectorAnyGeometry]))
+ self.addParameter(QgsProcessingParameterFeatureSource(self.JOIN,
+ self.tr('Join layer'),
+ [QgsProcessing.TypeVectorAnyGeometry]))
+ self.addParameter(QgsProcessingParameterEnum(self.PREDICATE,
+ self.tr('Geometric predicate'),
+ options=[p[1] for p in self.predicates],
+ allowMultiple=True, defaultValue=[0]))
+ self.addParameter(QgsProcessingParameterField(self.JOIN_FIELDS,
+ self.tr('Fields to add (leave empty to use all fields)'),
+ parentLayerParameterName=self.JOIN,
+ allowMultiple=True, optional=True))
+ self.addParameter(QgsProcessingParameterEnum(self.METHOD,
+ self.tr('Join type'), self.methods))
+ self.addParameter(QgsProcessingParameterBoolean(self.DISCARD_NONMATCHING,
+ self.tr('Discard records which could not be joined'),
+ defaultValue=False))
+ self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT,
+ self.tr('Joined layer')))
def name(self):
return 'joinattributesbylocation'
@@ -107,155 +118,97 @@ class SpatialJoin(QgisAlgorithm):
def displayName(self):
return self.tr('Join attributes by location')
+ def tags(self):
+ return self.tr("join,intersects,intersecting,touching,within,contains,overlaps,relation,spatial").split(',')
+
def processAlgorithm(self, parameters, context, feedback):
- target = QgsProcessingUtils.mapLayerFromString(self.getParameterValue(self.TARGET), context)
- join = QgsProcessingUtils.mapLayerFromString(self.getParameterValue(self.JOIN), context)
- predicates = self.getParameterValue(self.PREDICATE)
+ source = self.parameterAsSource(parameters, self.INPUT, context)
+ join_source = self.parameterAsSource(parameters, self.JOIN, context)
+ join_fields = self.parameterAsFields(parameters, self.JOIN_FIELDS, context)
+ method = self.parameterAsEnum(parameters, self.METHOD, context)
+ discard_nomatch = self.parameterAsBool(parameters, self.DISCARD_NONMATCHING, context)
- summary = self.getParameterValue(self.SUMMARY) == 1
- keep = self.getParameterValue(self.KEEP) == 1
-
- sumList = self.getParameterValue(self.STATS).lower().split(',')
-
- targetFields = target.fields()
- joinFields = join.fields()
-
- fieldList = QgsFields()
-
- if not summary:
- joinFields = vector.testForUniqueness(targetFields, joinFields)
- seq = list(range(len(targetFields) + len(joinFields)))
- targetFields.extend(joinFields)
- targetFields = dict(list(zip(seq, targetFields)))
+ source_fields = source.fields()
+ fields_to_join = QgsFields()
+ join_field_indexes = []
+ if not join_fields:
+ fields_to_join = join_source.fields()
+ join_field_indexes = [i for i in range(len(fields_to_join))]
else:
- numFields = {}
- for j in range(len(joinFields)):
- if joinFields[j].type() in [QVariant.Int, QVariant.Double, QVariant.LongLong, QVariant.UInt, QVariant.ULongLong]:
- numFields[j] = []
- for i in sumList:
- field = QgsField(i + str(joinFields[j].name()), QVariant.Double, '', 24, 16)
- fieldList.append(field)
- field = QgsField('count', QVariant.Double, '', 24, 16)
- fieldList.append(field)
- joinFields = vector.testForUniqueness(targetFields, fieldList)
- targetFields.extend(fieldList)
- seq = list(range(len(targetFields)))
- targetFields = dict(list(zip(seq, targetFields)))
+ for f in join_fields:
+ idx = join_source.fields().lookupField(f)
+ join_field_indexes.append(idx)
+ if idx >= 0:
+ fields_to_join.append(join_source.fields().at(idx))
- fields = QgsFields()
- for f in list(targetFields.values()):
- fields.append(f)
+ out_fields = vector.combineFields(source_fields, fields_to_join)
- writer = self.getOutputFromName(self.OUTPUT).getVectorWriter(fields, target.wkbType(), target.crs(), context)
+ (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
+ out_fields, source.wkbType(), source.sourceCrs())
- outFeat = QgsFeature()
- inFeatB = QgsFeature()
- inGeom = QgsGeometry()
+ # do the join
- index = QgsProcessingUtils.createSpatialIndex(join, context)
+ # build a list of 'reversed' predicates, because in this function
+ # we actually test the reverse of what the user wants (allowing us
+ # to prepare geometries and optimise the algorithm)
+ predicates = [self.reversed_predicates[self.predicates[i][0]] for i in
+ self.parameterAsEnums(parameters, self.PREDICATE, context)]
- mapP2 = dict()
- features = QgsProcessingUtils.getFeatures(join, context)
- for f in features:
- mapP2[f.id()] = QgsFeature(f)
+ remaining = set()
+ if not discard_nomatch:
+ remaining = set(source.allFeatureIds())
- features = QgsProcessingUtils.getFeatures(target, context)
- total = 100.0 / target.featureCount() if target.featureCount() else 0
- for c, f in enumerate(features):
- atMap1 = f.attributes()
- outFeat.setGeometry(f.geometry())
- inGeom = f.geometry()
- none = True
- joinList = []
- if inGeom.type() == QgsWkbTypes.PointGeometry:
- bbox = inGeom.buffer(10, 2).boundingBox()
- else:
- bbox = inGeom.boundingBox()
- joinList = index.intersects(bbox)
- if len(joinList) > 0:
- count = 0
- for i in joinList:
- inFeatB = mapP2[i]
- inGeomB = inFeatB.geometry()
+ added_set = set()
- res = False
- for predicate in predicates:
- res = getattr(inGeom, predicate)(inGeomB)
- if res:
- break
+ request = QgsFeatureRequest().setSubsetOfAttributes(join_field_indexes).setDestinationCrs(source.sourceCrs())
+ features = join_source.getFeatures(request)
+ total = 100.0 / join_source.featureCount() if join_source.featureCount() else 0
- if res:
- count = count + 1
- none = False
- atMap2 = inFeatB.attributes()
- if not summary:
- atMap = atMap1
- atMap2 = atMap2
- atMap.extend(atMap2)
- atMap = dict(list(zip(seq, atMap)))
- break
- else:
- for j in list(numFields.keys()):
- numFields[j].append(atMap2[j])
+ for current, f in enumerate(features):
+ if feedback.isCanceled():
+ break
- if summary and not none:
- atMap = atMap1
- for j in list(numFields.keys()):
- for k in sumList:
- if k == 'sum':
- atMap.append(sum(self._filterNull(numFields[j])))
- elif k == 'mean':
- try:
- nn_count = sum(1 for _ in self._filterNull(numFields[j]))
- atMap.append(sum(self._filterNull(numFields[j])) / nn_count)
- except ZeroDivisionError:
- atMap.append(NULL)
- elif k == 'min':
- try:
- atMap.append(min(self._filterNull(numFields[j])))
- except ValueError:
- atMap.append(NULL)
- elif k == 'median':
- atMap.append(self._median(numFields[j]))
- else:
- try:
- atMap.append(max(self._filterNull(numFields[j])))
- except ValueError:
- atMap.append(NULL)
+ if not f.hasGeometry():
+ continue
- numFields[j] = []
- atMap.append(count)
- atMap = dict(list(zip(seq, atMap)))
- if none:
- outFeat.setAttributes(atMap1)
- else:
- outFeat.setAttributes(list(atMap.values()))
+ bbox = f.geometry().boundingBox()
+ engine = None
- if keep:
- writer.addFeature(outFeat, QgsFeatureSink.FastInsert)
- else:
- if not none:
- writer.addFeature(outFeat, QgsFeatureSink.FastInsert)
+ request = QgsFeatureRequest().setFilterRect(bbox)
+ for test_feat in source.getFeatures(request):
+ if feedback.isCanceled():
+ break
+ if method == 1 and test_feat.id() in added_set:
+ # already added this feature, and user has opted to only output first match
+ continue
- feedback.setProgress(int(c * total))
- del writer
+ join_attributes = []
+ for a in join_field_indexes:
+ join_attributes.append(f.attributes()[a])
- def _filterNull(self, values):
- """Takes an iterator of values and returns a new iterator
- returning the same values but skipping any NULL values"""
- return (v for v in values if v != NULL)
+ if engine is None:
+ engine = QgsGeometry.createGeometryEngine(f.geometry().geometry())
+ engine.prepareGeometry()
- def _median(self, data):
- count = len(data)
- if count == 1:
- return data[0]
- data.sort()
+ for predicate in predicates:
+ if getattr(engine, predicate)(test_feat.geometry().geometry()):
+ added_set.add(test_feat.id())
- median = 0
- if count > 1:
- if (count % 2) == 0:
- median = 0.5 * ((data[count / 2 - 1]) + (data[count / 2]))
- else:
- median = data[(count + 1) / 2 - 1]
+ # join attributes and add
+ attributes = test_feat.attributes()
+ attributes.extend(join_attributes)
+ output_feature = test_feat
+ output_feature.setAttributes(attributes)
+ sink.addFeature(output_feature, QgsFeatureSink.FastInsert)
+ break
- return median
+ feedback.setProgress(int(current * total))
+
+ if not discard_nomatch:
+ remaining = remaining.difference(added_set)
+ for f in source.getFeatures(QgsFeatureRequest().setFilterFids(list(remaining))):
+ if feedback.isCanceled():
+ break
+ sink.addFeature(f, QgsFeatureSink.FastInsert)
+
+ return {self.OUTPUT: dest_id}
diff --git a/python/plugins/processing/tests/testdata/expected/join_by_location_intersect.gfs b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect.gfs
new file mode 100644
index 00000000000..091f58e110e
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect.gfs
@@ -0,0 +1,48 @@
+
+
+ join_by_location_intersect
+ join_by_location_intersect
+
+ 3
+ EPSG:4326
+
+ 10
+ -1.00000
+ 10.00000
+ -3.00000
+ 6.00000
+
+
+ name
+ name
+ String
+ 5
+
+
+ intval
+ intval
+ Integer
+
+
+ floatval
+ floatval
+ Real
+
+
+ fid_2
+ fid_2
+ String
+ 8
+
+
+ id
+ id
+ Integer
+
+
+ id2
+ id2
+ Integer
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/join_by_location_intersect.gml b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect.gml
new file mode 100644
index 00000000000..5e980296d19
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect.gml
@@ -0,0 +1,111 @@
+
+
+
+
+ -1-3
+ 106
+
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1
+ aaaaa
+ 33
+ 44.123456
+ points.0
+ 1
+ 2
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1
+ aaaaa
+ 33
+ 44.123456
+ points.1
+ 2
+ 1
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1
+ aaaaa
+ 33
+ 44.123456
+ points.2
+ 3
+ 0
+
+
+
+
+ 3,2 6,1 6,-3 2,-1 2,2 3,2
+ elim
+ 2
+ 3.33
+ points.2
+ 3
+ 0
+
+
+
+
+ 3,2 6,1 6,-3 2,-1 2,2 3,2
+ elim
+ 2
+ 3.33
+ points.4
+ 5
+ 1
+
+
+
+
+ 6,1 10,1 10,-3 6,-3 6,17,0 7,-2 9,-2 9,0 7,0
+ ASDF
+ 0
+ points.7
+ 8
+ 0
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1
+ aaaaa
+ 33
+ 44.123456
+ points.8
+ 9
+ 0
+
+
+
+
+ 2,5 2,6 3,6 3,5 2,5
+ bbaaa
+ 0.123
+
+
+
+
+ 5,5 6,4 4,4 5,5
+ Aaaaa
+ -33
+ 0
+
+
+
+
+ 120
+ -100291.43213
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_discardnomatch.gfs b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_discardnomatch.gfs
new file mode 100644
index 00000000000..c153c85fb0b
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_discardnomatch.gfs
@@ -0,0 +1,48 @@
+
+
+ join_by_location_intersect_discardnomatch
+ join_by_location_intersect_discardnomatch
+
+ 3
+ EPSG:4326
+
+ 7
+ -1.00000
+ 10.00000
+ -3.00000
+ 3.00000
+
+
+ name
+ name
+ String
+ 5
+
+
+ intval
+ intval
+ Integer
+
+
+ floatval
+ floatval
+ Real
+
+
+ fid_2
+ fid_2
+ String
+ 8
+
+
+ id
+ id
+ Integer
+
+
+ id2
+ id2
+ Integer
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_discardnomatch.gml b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_discardnomatch.gml
new file mode 100644
index 00000000000..bba44e2bcbd
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_discardnomatch.gml
@@ -0,0 +1,90 @@
+
+
+
+
+ -1-3
+ 103
+
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1
+ aaaaa
+ 33
+ 44.123456
+ points.0
+ 1
+ 2
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1
+ aaaaa
+ 33
+ 44.123456
+ points.1
+ 2
+ 1
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1
+ aaaaa
+ 33
+ 44.123456
+ points.2
+ 3
+ 0
+
+
+
+
+ 3,2 6,1 6,-3 2,-1 2,2 3,2
+ elim
+ 2
+ 3.33
+ points.2
+ 3
+ 0
+
+
+
+
+ 3,2 6,1 6,-3 2,-1 2,2 3,2
+ elim
+ 2
+ 3.33
+ points.4
+ 5
+ 1
+
+
+
+
+ 6,1 10,1 10,-3 6,-3 6,17,0 7,-2 9,-2 9,0 7,0
+ ASDF
+ 0
+ points.7
+ 8
+ 0
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1
+ aaaaa
+ 33
+ 44.123456
+ points.8
+ 9
+ 0
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_first_only.gfs b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_first_only.gfs
new file mode 100644
index 00000000000..1aaca3a3a85
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_first_only.gfs
@@ -0,0 +1,48 @@
+
+
+ join_by_location_intersect_first_only
+ join_by_location_intersect_first_only
+
+ 3
+ EPSG:4326
+
+ 6
+ -1.00000
+ 10.00000
+ -3.00000
+ 6.00000
+
+
+ name
+ name
+ String
+ 5
+
+
+ intval
+ intval
+ Integer
+
+
+ floatval
+ floatval
+ Real
+
+
+ fid_2
+ fid_2
+ String
+ 8
+
+
+ id
+ id
+ Integer
+
+
+ id2
+ id2
+ Integer
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_first_only.gml b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_first_only.gml
new file mode 100644
index 00000000000..ea6a3491cd6
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_first_only.gml
@@ -0,0 +1,67 @@
+
+
+
+
+ -1-3
+ 106
+
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1
+ aaaaa
+ 33
+ 44.123456
+ points.0
+ 1
+ 2
+
+
+
+
+ 3,2 6,1 6,-3 2,-1 2,2 3,2
+ elim
+ 2
+ 3.33
+ points.2
+ 3
+ 0
+
+
+
+
+ 6,1 10,1 10,-3 6,-3 6,17,0 7,-2 9,-2 9,0 7,0
+ ASDF
+ 0
+ points.7
+ 8
+ 0
+
+
+
+
+ 5,5 6,4 4,4 5,5
+ Aaaaa
+ -33
+ 0
+
+
+
+
+ 2,5 2,6 3,6 3,5 2,5
+ bbaaa
+ 0.123
+
+
+
+
+ 120
+ -100291.43213
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_first_only_discardnomatch.gfs b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_first_only_discardnomatch.gfs
new file mode 100644
index 00000000000..0148d194dff
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_first_only_discardnomatch.gfs
@@ -0,0 +1,48 @@
+
+
+ join_by_location_intersect_first_only_discardnomatch
+ join_by_location_intersect_first_only_discardnomatch
+
+ 3
+ EPSG:4326
+
+ 3
+ -1.00000
+ 10.00000
+ -3.00000
+ 3.00000
+
+
+ name
+ name
+ String
+ 5
+
+
+ intval
+ intval
+ Integer
+
+
+ floatval
+ floatval
+ Real
+
+
+ fid_2
+ fid_2
+ String
+ 8
+
+
+ id
+ id
+ Integer
+
+
+ id2
+ id2
+ Integer
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_first_only_discardnomatch.gml b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_first_only_discardnomatch.gml
new file mode 100644
index 00000000000..3414e9b3a42
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_first_only_discardnomatch.gml
@@ -0,0 +1,46 @@
+
+
+
+
+ -1-3
+ 103
+
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1
+ aaaaa
+ 33
+ 44.123456
+ points.0
+ 1
+ 2
+
+
+
+
+ 3,2 6,1 6,-3 2,-1 2,2 3,2
+ elim
+ 2
+ 3.33
+ points.2
+ 3
+ 0
+
+
+
+
+ 6,1 10,1 10,-3 6,-3 6,17,0 7,-2 9,-2 9,0 7,0
+ ASDF
+ 0
+ points.7
+ 8
+ 0
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_subset_fields.gfs b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_subset_fields.gfs
new file mode 100644
index 00000000000..3f87c7c78e5
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_subset_fields.gfs
@@ -0,0 +1,37 @@
+
+
+ join_by_location_intersect_subset_fields
+ join_by_location_intersect_subset_fields
+
+ 3
+ EPSG:4326
+
+ 10
+ -1.00000
+ 10.00000
+ -3.00000
+ 6.00000
+
+
+ name
+ name
+ String
+ 5
+
+
+ intval
+ intval
+ Integer
+
+
+ floatval
+ floatval
+ Real
+
+
+ id
+ id
+ Integer
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_subset_fields.gml b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_subset_fields.gml
new file mode 100644
index 00000000000..406222d911f
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/join_by_location_intersect_subset_fields.gml
@@ -0,0 +1,97 @@
+
+
+
+
+ -1-3
+ 106
+
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1
+ aaaaa
+ 33
+ 44.123456
+ 1
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1
+ aaaaa
+ 33
+ 44.123456
+ 2
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1
+ aaaaa
+ 33
+ 44.123456
+ 3
+
+
+
+
+ 3,2 6,1 6,-3 2,-1 2,2 3,2
+ elim
+ 2
+ 3.33
+ 3
+
+
+
+
+ 3,2 6,1 6,-3 2,-1 2,2 3,2
+ elim
+ 2
+ 3.33
+ 5
+
+
+
+
+ 6,1 10,1 10,-3 6,-3 6,17,0 7,-2 9,-2 9,0 7,0
+ ASDF
+ 0
+ 8
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1
+ aaaaa
+ 33
+ 44.123456
+ 9
+
+
+
+
+ 5,5 6,4 4,4 5,5
+ Aaaaa
+ -33
+ 0
+
+
+
+
+ 2,5 2,6 3,6 3,5 2,5
+ bbaaa
+ 0.123
+
+
+
+
+ 120
+ -100291.43213
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/join_by_location_touches.gfs b/python/plugins/processing/tests/testdata/expected/join_by_location_touches.gfs
new file mode 100644
index 00000000000..cdaf8027817
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/join_by_location_touches.gfs
@@ -0,0 +1,48 @@
+
+
+ join_by_location_touches
+ join_by_location_touches
+
+ 3
+ EPSG:4326
+
+ 5
+ -1.00000
+ 10.00000
+ -3.00000
+ 3.00000
+
+
+ name
+ name
+ String
+ 5
+
+
+ intval
+ intval
+ Integer
+
+
+ floatval
+ floatval
+ Real
+
+
+ fid_2
+ fid_2
+ String
+ 8
+
+
+ id
+ id
+ Integer
+
+
+ id2
+ id2
+ Integer
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/join_by_location_touches.gml b/python/plugins/processing/tests/testdata/expected/join_by_location_touches.gml
new file mode 100644
index 00000000000..48cee4d031e
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/join_by_location_touches.gml
@@ -0,0 +1,68 @@
+
+
+
+
+ -1-3
+ 103
+
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1
+ aaaaa
+ 33
+ 44.123456
+ points.1
+ 2
+ 1
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1
+ aaaaa
+ 33
+ 44.123456
+ points.2
+ 3
+ 0
+
+
+
+
+ 3,2 6,1 6,-3 2,-1 2,2 3,2
+ elim
+ 2
+ 3.33
+ points.2
+ 3
+ 0
+
+
+
+
+ 6,1 10,1 10,-3 6,-3 6,17,0 7,-2 9,-2 9,0 7,0
+ ASDF
+ 0
+ points.7
+ 8
+ 0
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1
+ aaaaa
+ 33
+ 44.123456
+ points.8
+ 9
+ 0
+
+
+
diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml
index bf5f5ee36a6..828d5080fa2 100644
--- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml
+++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml
@@ -3623,4 +3623,161 @@ tests:
pk: date
compare:
fields:
- fid: skip
\ No newline at end of file
+ fid: skip
+
+ - algorithm: qgis:joinattributesbylocation
+ name: Join by location (intersects)
+ params:
+ DISCARD_NONMATCHING: false
+ INPUT:
+ name: polys.gml
+ type: vector
+ JOIN:
+ name: custom/points.shp
+ type: vector
+ METHOD: 0
+ PREDICATE:
+ - 0
+ results:
+ OUTPUT:
+ name: expected/join_by_location_intersect.gml
+ type: vector
+ pk:
+ - name
+ - id
+ - id2
+ compare:
+ fields:
+ fid: skip
+ fid_2: skip
+
+ - algorithm: qgis:joinattributesbylocation
+ name: Join by location (intersects), discard no match
+ params:
+ DISCARD_NONMATCHING: true
+ INPUT:
+ name: polys.gml
+ type: vector
+ JOIN:
+ name: custom/points.shp
+ type: vector
+ METHOD: 0
+ PREDICATE:
+ - 0
+ results:
+ OUTPUT:
+ name: expected/join_by_location_intersect_discardnomatch.gml
+ type: vector
+ pk:
+ - name
+ - id
+ - id2
+ compare:
+ fields:
+ fid: skip
+ fid_2: skip
+
+ - algorithm: qgis:joinattributesbylocation
+ name: Join by location (intersects), first match only
+ params:
+ DISCARD_NONMATCHING: false
+ INPUT:
+ name: polys.gml
+ type: vector
+ JOIN:
+ name: custom/points.shp
+ type: vector
+ METHOD: 1
+ PREDICATE:
+ - 0
+ results:
+ OUTPUT:
+ name: expected/join_by_location_intersect_first_only.gml
+ type: vector
+ pk:
+ - name
+ compare:
+ fields:
+ fid: skip
+ fid_2: skip
+ id: skip # cant check these - order of match is not predictable
+ id2: skip
+
+ - algorithm: qgis:joinattributesbylocation
+ name: Join by location (intersects), first match only, discard no match
+ params:
+ DISCARD_NONMATCHING: true
+ INPUT:
+ name: expected/join_by_location_intersect_first_only.gml
+ type: vector
+ JOIN:
+ name: custom/points.shp
+ type: vector
+ METHOD: 1
+ PREDICATE:
+ - 0
+ results:
+ OUTPUT:
+ name: expected/join_by_location_intersect_first_only_discardnomatch.gml
+ type: vector
+ pk:
+ - name
+ compare:
+ fields:
+ fid: skip
+ fid_2: skip
+ id: skip # cant check these - order of match is not predictable
+ id2: skip
+
+ - algorithm: qgis:joinattributesbylocation
+ name: Join by location (intersects), subset of fields
+ params:
+ DISCARD_NONMATCHING: false
+ INPUT:
+ name: polys.gml
+ type: vector
+ JOIN:
+ name: custom/points.shp
+ type: vector
+ JOIN_FIELDS:
+ - id
+ METHOD: 0
+ PREDICATE:
+ - 0
+ results:
+ OUTPUT:
+ name: expected/join_by_location_intersect_subset_fields.gml
+ type: vector
+ pk:
+ - name
+ - id
+ compare:
+ fields:
+ fid: skip
+ fid_2: skip
+
+ - algorithm: qgis:joinattributesbylocation
+ name: Join by location (touches)
+ params:
+ DISCARD_NONMATCHING: true
+ INPUT:
+ name: polys.gml
+ type: vector
+ JOIN:
+ name: custom/points.shp
+ type: vector
+ METHOD: 0
+ PREDICATE:
+ - 3
+ results:
+ OUTPUT:
+ name: expected/join_by_location_touches.gml
+ type: vector
+ pk:
+ - name
+ - id
+ - id2
+ compare:
+ fields:
+ fid: skip
+ fid_2: skip
\ No newline at end of file