Skip to content

Commit 24a4ab7

Browse files
committedSep 8, 2017
Rework Select by Location algorithm
Changes: - handle different CRS transparently - don't build a spatial index on the selection layer. Instead only use feature requests to fetch features which are within the desired bounds, and rely on the presence of an appropriate spatial index at the provider's backend. Otherwise, we force every user of this algorithm to have a full iteration of the source table, regardless of how large the table is. That means that trying to select a set of addresses which fall within a specific locality from a table which contains the addresses for a whole state will FORCE every address in the state to be initially read before any calculation begins. With this change only those features within the bounding box of the selected localities will ever be fetched from the provider, resulting in huge speed improvements for the algorithm. - use prepared geometries for the spatial relation tests. This dramatically speeds up the algorithm in the case where the intersection layer features cover multiple features from the 'selection' layer. - Add a 'select within current selection' mode - Optimise feature requests for efficiency (especially with respect to the 'disjoint' selection mode)
1 parent 20d8244 commit 24a4ab7

File tree

2 files changed

+81
-63
lines changed

2 files changed

+81
-63
lines changed
 

‎python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@
134134
from .SaveSelectedFeatures import SaveSelectedFeatures
135135
from .SelectByAttribute import SelectByAttribute
136136
from .SelectByExpression import SelectByExpression
137+
from .SelectByLocation import SelectByLocation
137138
from .ServiceAreaFromLayer import ServiceAreaFromLayer
138139
from .ServiceAreaFromPoint import ServiceAreaFromPoint
139140
from .SetMValue import SetMValue
@@ -167,7 +168,6 @@
167168
from .ZonalStatistics import ZonalStatistics
168169

169170
# from .ExtractByLocation import ExtractByLocation
170-
# from .SelectByLocation import SelectByLocation
171171
# from .SpatialJoin import SpatialJoin
172172

173173
pluginPath = os.path.normpath(os.path.join(
@@ -183,7 +183,6 @@ def __init__(self):
183183

184184
def getAlgs(self):
185185
# algs = [
186-
# SelectByLocation(),
187186
# ExtractByLocation(),
188187
# SpatialJoin(),
189188
# ]
@@ -281,6 +280,7 @@ def getAlgs(self):
281280
SaveSelectedFeatures(),
282281
SelectByAttribute(),
283282
SelectByExpression(),
283+
SelectByLocation(),
284284
ServiceAreaFromLayer(),
285285
ServiceAreaFromPoint(),
286286
SetMValue(),

‎python/plugins/processing/algs/qgis/SelectByLocation.py

Lines changed: 79 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,19 @@
2929

3030
from qgis.PyQt.QtGui import QIcon
3131

32-
from qgis.core import QgsGeometry, QgsFeatureRequest, QgsProcessingUtils
32+
from qgis.core import (QgsGeometry,
33+
QgsFeatureRequest,
34+
QgsProcessingUtils,
35+
QgsProcessing,
36+
QgsProcessingParameterVectorLayer,
37+
QgsProcessingParameterFeatureSource,
38+
QgsProcessingParameterEnum,
39+
QgsProcessingParameterNumber,
40+
QgsProcessingOutputVectorLayer,
41+
QgsVectorLayer)
42+
3343

3444
from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
35-
from processing.core.parameters import ParameterSelection
36-
from processing.core.parameters import ParameterVector
37-
from processing.core.parameters import ParameterNumber
38-
from processing.core.outputs import OutputVector
3945
from processing.tools import vector
4046

4147
pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0]
@@ -63,32 +69,43 @@ def initAlgorithm(self, config=None):
6369
self.predicates = (
6470
('intersects', self.tr('intersects')),
6571
('contains', self.tr('contains')),
66-
('disjoint', self.tr('disjoint')),
67-
('equals', self.tr('equals')),
72+
('disjoint', self.tr('is disjoint')),
73+
('isEqual', self.tr('equals')),
6874
('touches', self.tr('touches')),
6975
('overlaps', self.tr('overlaps')),
7076
('within', self.tr('within')),
7177
('crosses', self.tr('crosses')))
7278

79+
self.reversed_predicates = {'intersects': 'intersects',
80+
'contains': 'within',
81+
'disjoint': 'disjoint',
82+
'isEqual': 'isEqual',
83+
'touches': 'touches',
84+
'overlaps': 'overlaps',
85+
'within': 'contains',
86+
'crosses': 'crosses'}
87+
7388
self.methods = [self.tr('creating new selection'),
7489
self.tr('adding to current selection'),
90+
self.tr('select within current selection'),
7591
self.tr('removing from current selection')]
7692

77-
self.addParameter(ParameterVector(self.INPUT,
78-
self.tr('Layer to select from')))
79-
self.addParameter(ParameterVector(self.INTERSECT,
80-
self.tr('Additional layer (intersection layer)')))
81-
self.addParameter(ParameterSelection(self.PREDICATE,
82-
self.tr('Geometric predicate'),
83-
self.predicates,
84-
multiple=True))
85-
self.addParameter(ParameterNumber(self.PRECISION,
86-
self.tr('Precision'),
87-
0.0, None, 0.0))
88-
self.addParameter(ParameterSelection(self.METHOD,
89-
self.tr('Modify current selection by'),
90-
self.methods, 0))
91-
self.addOutput(OutputVector(self.OUTPUT, self.tr('Selected (location)'), True))
93+
self.addParameter(QgsProcessingParameterVectorLayer(self.INPUT,
94+
self.tr('Select features from'), types=[QgsProcessing.TypeVectorAnyGeometry]))
95+
self.addParameter(QgsProcessingParameterEnum(self.PREDICATE,
96+
self.tr('Where the features are (geometric predicate)'),
97+
options=[p[1] for p in self.predicates],
98+
allowMultiple=True, defaultValue=[0]))
99+
self.addParameter(QgsProcessingParameterFeatureSource(self.INTERSECT,
100+
self.tr('By comparing to the features from'), types=[QgsProcessing.TypeVectorAnyGeometry]))
101+
self.addParameter(QgsProcessingParameterNumber(self.PRECISION,
102+
self.tr('Precision'), type=QgsProcessingParameterNumber.Double,
103+
minValue=0.0, defaultValue=0.0))
104+
self.addParameter(QgsProcessingParameterEnum(self.METHOD,
105+
self.tr('Modify current selection by'),
106+
options=self.methods, defaultValue=0))
107+
108+
self.addOutput(QgsProcessingOutputVectorLayer(self.OUTPUT, self.tr('Selected (by location)')))
92109

93110
def name(self):
94111
return 'selectbylocation'
@@ -97,60 +114,61 @@ def displayName(self):
97114
return self.tr('Select by location')
98115

99116
def processAlgorithm(self, parameters, context, feedback):
100-
filename = self.getParameterValue(self.INPUT)
101-
inputLayer = QgsProcessingUtils.mapLayerFromString(filename, context)
102-
method = self.getParameterValue(self.METHOD)
103-
filename2 = self.getParameterValue(self.INTERSECT)
104-
selectLayer = QgsProcessingUtils.mapLayerFromString(filename2, context)
105-
predicates = self.getParameterValue(self.PREDICATE)
106-
precision = self.getParameterValue(self.PRECISION)
107-
108-
oldSelection = set(inputLayer.selectedFeatureIds())
109-
inputLayer.removeSelection()
110-
index = QgsProcessingUtils.createSpatialIndex(inputLayer, context)
117+
select_layer = self.parameterAsVectorLayer(parameters, self.INPUT, context)
118+
method = QgsVectorLayer.SelectBehavior(self.parameterAsEnum(parameters, self.METHOD, context))
119+
intersect_source = self.parameterAsSource(parameters, self.INTERSECT, context)
120+
# build a list of 'reversed' predicates, because in this function
121+
# we actually test the reverse of what the user wants (allowing us
122+
# to prepare geometries and optimise the algorithm)
123+
predicates = [self.reversed_predicates[self.predicates[i][0]] for i in self.parameterAsEnums(parameters, self.PREDICATE, context)]
124+
precision = self.parameterAsDouble(parameters, self.PRECISION, context)
111125

112126
if 'disjoint' in predicates:
113-
disjoinSet = []
114-
for feat in QgsProcessingUtils.getFeatures(inputLayer, context):
115-
disjoinSet.append(feat.id())
116-
117-
geom = QgsGeometry()
118-
selectedSet = []
119-
features = QgsProcessingUtils.getFeatures(selectLayer, context)
120-
total = 100.0 / selectLayer.featureCount() if selectLayer.featureCount() else 0
127+
disjoint_set = select_layer.allFeatureIds()
128+
else:
129+
disjoint_set = None
130+
131+
selected_set = set()
132+
request = QgsFeatureRequest().setSubsetOfAttributes([]).setDestinationCrs(select_layer.crs())
133+
features = intersect_source.getFeatures(request)
134+
total = 100.0 / intersect_source.featureCount() if intersect_source.featureCount() else 0
121135
for current, f in enumerate(features):
122-
geom = vector.snapToPrecision(f.geometry(), precision)
123-
bbox = geom.boundingBox()
136+
if feedback.isCanceled():
137+
break
138+
139+
if not f.hasGeometry():
140+
continue
141+
142+
engine = QgsGeometry.createGeometryEngine(f.geometry().geometry())
143+
engine.prepareGeometry()
144+
bbox = f.geometry().boundingBox()
124145
bbox.grow(0.51 * precision)
125-
intersects = index.intersects(bbox)
126146

127-
request = QgsFeatureRequest().setFilterFids(intersects).setSubsetOfAttributes([])
128-
for feat in inputLayer.getFeatures(request):
129-
tmpGeom = vector.snapToPrecision(feat.geometry(), precision)
147+
request = QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry).setFilterRect(bbox).setSubsetOfAttributes([])
148+
for test_feat in select_layer.getFeatures(request):
149+
if feedback.isCanceled():
150+
break
151+
152+
if test_feat in selected_set:
153+
# already added this one, no need for further tests
154+
continue
130155

131-
res = False
132156
for predicate in predicates:
133157
if predicate == 'disjoint':
134-
if tmpGeom.intersects(geom):
158+
if test_feat.geometry().intersects(f.geometry()):
135159
try:
136-
disjoinSet.remove(feat.id())
160+
disjoint_set.remove(test_feat.id())
137161
except:
138162
pass # already removed
139163
else:
140-
res = getattr(tmpGeom, predicate)(geom)
141-
if res:
142-
selectedSet.append(feat.id())
164+
if getattr(engine, predicate)(test_feat.geometry().geometry()):
165+
selected_set.add(test_feat.id())
143166
break
144167

145168
feedback.setProgress(int(current * total))
146169

147170
if 'disjoint' in predicates:
148-
selectedSet = selectedSet + disjoinSet
149-
150-
if method == 1:
151-
selectedSet = list(oldSelection.union(selectedSet))
152-
elif method == 2:
153-
selectedSet = list(oldSelection.difference(selectedSet))
171+
selected_set = list(selected_set) + disjoint_set
154172

155-
inputLayer.selectByIds(selectedSet)
156-
self.setOutputValue(self.OUTPUT, filename)
173+
select_layer.selectByIds(list(selected_set), method)
174+
return {self.OUTPUT: parameters[self.INPUT]}

0 commit comments

Comments
 (0)
Please sign in to comment.