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