Skip to content

Commit b951d5a

Browse files
committed
[FEATURE] new method QgsVectorLayer::selectByExpression(...)
Makes it simple for scripts to select by expression. The method also accepts a parameter which dictates whether matching features are added to an existing selection, removed from the selection or intersected with the current selection. The existing code from the select by expression dialog has been moved to QgsVectorLayer, and optimised for maximum possible speed. Also added unit tests.
1 parent 7187148 commit b951d5a

File tree

5 files changed

+147
-117
lines changed

5 files changed

+147
-117
lines changed

python/core/qgsvectorlayer.sip

+21
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,15 @@ class QgsVectorLayer : QgsMapLayer
147147
InvalidLayer, /**< Edit failed due to invalid layer */
148148
};
149149

150+
//! Selection behaviour
151+
enum SelectBehaviour
152+
{
153+
SetSelection, /**< Set selection, removing any existing selection */
154+
AddToSelection, /**< Add selection to current selection */
155+
IntersectSelection, /**< Modify current selection to include only select features which match */
156+
RemoveFromSelection, /**< Remove from current selection */
157+
};
158+
150159
/** Constructor - creates a vector layer
151160
*
152161
* The QgsVectorLayer is constructed by instantiating a data provider. The provider
@@ -294,9 +303,20 @@ class QgsVectorLayer : QgsMapLayer
294303
* @param addToSelection If set to true will not clear before selecting
295304
*
296305
* @see invertSelectionInRectangle(QgsRectangle & rect)
306+
* @see selectByExpression()
297307
*/
298308
void select( QgsRectangle & rect, bool addToSelection );
299309

310+
/** Select matching features using an expression.
311+
* @param expression expression to evaluate to select features
312+
* @param behaviour selection type, allows adding to current selection, removing
313+
* from selection, etc.
314+
* @note added in QGIS 2.16
315+
* @see select()
316+
* @see modifySelection()
317+
*/
318+
void selectByExpression( const QString& expression, SelectBehaviour behaviour = SetSelection );
319+
300320
/**
301321
* Modifies the current selection on this layer
302322
*
@@ -307,6 +327,7 @@ class QgsVectorLayer : QgsMapLayer
307327
* @see select(QgsFeatureId)
308328
* @see deselect(QgsFeatureIds)
309329
* @see deselect(QgsFeatureId)
330+
* @see selectByExpression()
310331
*/
311332
void modifySelection( QgsFeatureIds selectIds, QgsFeatureIds deselectIds );
312333

src/core/qgsvectorlayer.cpp

+65
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,71 @@ void QgsVectorLayer::select( QgsRectangle & rect, bool addToSelection )
465465
}
466466
}
467467

468+
void QgsVectorLayer::selectByExpression( const QString& expression, QgsVectorLayer::SelectBehaviour behaviour )
469+
{
470+
QgsFeatureIds newSelection;
471+
472+
QgsExpressionContext context;
473+
context << QgsExpressionContextUtils::globalScope()
474+
<< QgsExpressionContextUtils::projectScope()
475+
<< QgsExpressionContextUtils::layerScope( this );
476+
477+
if ( behaviour == SetSelection || behaviour == AddToSelection )
478+
{
479+
QgsFeatureRequest request = QgsFeatureRequest().setFilterExpression( expression )
480+
.setExpressionContext( context )
481+
.setFlags( QgsFeatureRequest::NoGeometry );
482+
//TODO - investigate whether removing all attributes is possible
483+
//for now, just set the first attribute
484+
request.setSubsetOfAttributes( QgsAttributeList() << 0 );
485+
486+
QgsFeatureIterator features = getFeatures( request );
487+
488+
if ( behaviour == AddToSelection )
489+
{
490+
newSelection = selectedFeaturesIds();
491+
}
492+
QgsFeature feat;
493+
while ( features.nextFeature( feat ) )
494+
{
495+
newSelection << feat.id();
496+
}
497+
features.close();
498+
}
499+
else if ( behaviour == IntersectSelection || behaviour == RemoveFromSelection )
500+
{
501+
QgsExpression exp( expression );
502+
exp.prepare( &context );
503+
504+
QgsFeatureIds oldSelection = selectedFeaturesIds();
505+
QgsFeatureRequest request = QgsFeatureRequest().setFilterFids( oldSelection );
506+
507+
//refine request
508+
if ( !exp.needsGeometry() )
509+
request.setFlags( QgsFeatureRequest::NoGeometry );
510+
request.setSubsetOfAttributes( exp.referencedColumns(), fields() );
511+
512+
QgsFeatureIterator features = getFeatures( request );
513+
QgsFeature feat;
514+
while ( features.nextFeature( feat ) )
515+
{
516+
context.setFeature( feat );
517+
bool matches = exp.evaluate( &context ).toBool();
518+
519+
if ( matches && behaviour == IntersectSelection )
520+
{
521+
newSelection << feat.id();
522+
}
523+
else if ( !matches && behaviour == RemoveFromSelection )
524+
{
525+
newSelection << feat.id();
526+
}
527+
}
528+
}
529+
530+
setSelectedFeatures( newSelection );
531+
}
532+
468533
void QgsVectorLayer::modifySelection( QgsFeatureIds selectIds, QgsFeatureIds deselectIds )
469534
{
470535
QgsFeatureIds intersectingIds = selectIds & deselectIds;

src/core/qgsvectorlayer.h

+21
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,15 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer
504504
InvalidLayer = 4, /**< Edit failed due to invalid layer */
505505
};
506506

507+
//! Selection behaviour
508+
enum SelectBehaviour
509+
{
510+
SetSelection, /**< Set selection, removing any existing selection */
511+
AddToSelection, /**< Add selection to current selection */
512+
IntersectSelection, /**< Modify current selection to include only select features which match */
513+
RemoveFromSelection, /**< Remove from current selection */
514+
};
515+
507516
/** Constructor - creates a vector layer
508517
*
509518
* The QgsVectorLayer is constructed by instantiating a data provider. The provider
@@ -657,9 +666,20 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer
657666
* @param addToSelection If set to true will not clear before selecting
658667
*
659668
* @see invertSelectionInRectangle(QgsRectangle & rect)
669+
* @see selectByExpression()
660670
*/
661671
void select( QgsRectangle & rect, bool addToSelection );
662672

673+
/** Select matching features using an expression.
674+
* @param expression expression to evaluate to select features
675+
* @param behaviour selection type, allows adding to current selection, removing
676+
* from selection, etc.
677+
* @note added in QGIS 2.16
678+
* @see select()
679+
* @see modifySelection()
680+
*/
681+
void selectByExpression( const QString& expression, SelectBehaviour behaviour = SetSelection );
682+
663683
/**
664684
* Modifies the current selection on this layer
665685
*
@@ -670,6 +690,7 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer
670690
* @see select(QgsFeatureId)
671691
* @see deselect(QgsFeatureIds)
672692
* @see deselect(QgsFeatureId)
693+
* @see selectByExpression()
673694
*/
674695
void modifySelection( QgsFeatureIds selectIds, QgsFeatureIds deselectIds );
675696

src/gui/qgsexpressionselectiondialog.cpp

+8-117
Original file line numberDiff line numberDiff line change
@@ -76,138 +76,29 @@ void QgsExpressionSelectionDialog::setGeomCalculator( const QgsDistanceArea & da
7676

7777
void QgsExpressionSelectionDialog::on_mActionSelect_triggered()
7878
{
79-
QgsFeatureIds newSelection;
80-
QgsExpression* expression = new QgsExpression( mExpressionBuilder->expressionText() );
81-
82-
QgsExpressionContext context;
83-
context << QgsExpressionContextUtils::globalScope()
84-
<< QgsExpressionContextUtils::projectScope()
85-
<< QgsExpressionContextUtils::layerScope( mLayer );
86-
87-
QgsFeatureRequest request = QgsFeatureRequest().setFilterExpression( mExpressionBuilder->expressionText() ).setExpressionContext( context );
88-
QgsFeatureIterator features = mLayer->getFeatures( request );
89-
90-
QgsFeature feat;
91-
while ( features.nextFeature( feat ) )
92-
{
93-
newSelection << feat.id();
94-
}
95-
96-
features.close();
97-
98-
mLayer->setSelectedFeatures( newSelection );
99-
100-
delete expression;
79+
mLayer->selectByExpression( mExpressionBuilder->expressionText(),
80+
QgsVectorLayer::SetSelection );
10181
saveRecent();
10282
}
10383

10484
void QgsExpressionSelectionDialog::on_mActionAddToSelection_triggered()
10585
{
106-
QgsFeatureIds newSelection = mLayer->selectedFeaturesIds();
107-
QgsExpression* expression = new QgsExpression( mExpressionBuilder->expressionText() );
108-
109-
QgsExpressionContext context;
110-
context << QgsExpressionContextUtils::globalScope()
111-
<< QgsExpressionContextUtils::projectScope()
112-
<< QgsExpressionContextUtils::layerScope( mLayer );
113-
114-
QgsFeatureRequest request = QgsFeatureRequest().setFilterExpression( mExpressionBuilder->expressionText() ).setExpressionContext( context );
115-
QgsFeatureIterator features = mLayer->getFeatures( request );
116-
117-
QgsFeature feat;
118-
while ( features.nextFeature( feat ) )
119-
{
120-
newSelection << feat.id();
121-
}
122-
123-
features.close();
124-
125-
mLayer->setSelectedFeatures( newSelection );
126-
127-
delete expression;
86+
mLayer->selectByExpression( mExpressionBuilder->expressionText(),
87+
QgsVectorLayer::AddToSelection );
12888
saveRecent();
12989
}
13090

13191
void QgsExpressionSelectionDialog::on_mActionSelectIntersect_triggered()
13292
{
133-
const QgsFeatureIds &oldSelection = mLayer->selectedFeaturesIds();
134-
QgsFeatureIds newSelection;
135-
136-
QgsExpression* expression = new QgsExpression( mExpressionBuilder->expressionText() );
137-
138-
QgsExpressionContext context;
139-
context << QgsExpressionContextUtils::globalScope()
140-
<< QgsExpressionContextUtils::projectScope()
141-
<< QgsExpressionContextUtils::layerScope( mLayer );
142-
143-
expression->prepare( &context );
144-
145-
QgsFeature feat;
146-
Q_FOREACH ( const QgsFeatureId fid, oldSelection )
147-
{
148-
QgsFeatureIterator features = mLayer->getFeatures( QgsFeatureRequest().setFilterFid( fid ) );
149-
150-
if ( features.nextFeature( feat ) )
151-
{
152-
context.setFeature( feat );
153-
if ( expression->evaluate( &context ).toBool() )
154-
{
155-
newSelection << feat.id();
156-
}
157-
}
158-
else
159-
{
160-
Q_ASSERT( false );
161-
}
162-
163-
features.close();
164-
}
165-
166-
mLayer->setSelectedFeatures( newSelection );
167-
168-
delete expression;
93+
mLayer->selectByExpression( mExpressionBuilder->expressionText(),
94+
QgsVectorLayer::IntersectSelection );
16995
saveRecent();
17096
}
17197

17298
void QgsExpressionSelectionDialog::on_mActionRemoveFromSelection_triggered()
17399
{
174-
const QgsFeatureIds &oldSelection = mLayer->selectedFeaturesIds();
175-
QgsFeatureIds newSelection = mLayer->selectedFeaturesIds();
176-
177-
QgsExpression* expression = new QgsExpression( mExpressionBuilder->expressionText() );
178-
179-
QgsExpressionContext context;
180-
context << QgsExpressionContextUtils::globalScope()
181-
<< QgsExpressionContextUtils::projectScope()
182-
<< QgsExpressionContextUtils::layerScope( mLayer );
183-
184-
expression->prepare( &context );
185-
186-
QgsFeature feat;
187-
Q_FOREACH ( const QgsFeatureId fid, oldSelection )
188-
{
189-
QgsFeatureIterator features = mLayer->getFeatures( QgsFeatureRequest().setFilterFid( fid ) );
190-
191-
if ( features.nextFeature( feat ) )
192-
{
193-
context.setFeature( feat );
194-
if ( expression->evaluate( &context ).toBool() )
195-
{
196-
newSelection.remove( feat.id() );
197-
}
198-
}
199-
else
200-
{
201-
Q_ASSERT( false );
202-
}
203-
204-
features.close();
205-
}
206-
207-
mLayer->setSelectedFeatures( newSelection );
208-
209-
delete expression;
210-
100+
mLayer->selectByExpression( mExpressionBuilder->expressionText(),
101+
QgsVectorLayer::RemoveFromSelection );
211102
saveRecent();
212103
}
213104

tests/src/python/test_qgsvectorlayer.py

+32
Original file line numberDiff line numberDiff line change
@@ -1063,6 +1063,38 @@ def test_ExpressionFilter(self):
10631063

10641064
assert(len(list(features)) == 1)
10651065

1066+
def testSelectByExpression(self):
1067+
""" Test selecting by expression """
1068+
layer = QgsVectorLayer(os.path.join(unitTestDataPath(), 'points.shp'), 'Points', 'ogr')
1069+
1070+
# SetSelection
1071+
layer.selectByExpression('"Class"=\'B52\' and "Heading" > 10 and "Heading" <70', QgsVectorLayer.SetSelection)
1072+
self.assertEqual(set(layer.selectedFeaturesIds()), set([10, 11]))
1073+
# check that existing selection is cleared
1074+
layer.selectByExpression('"Class"=\'Biplane\'', QgsVectorLayer.SetSelection)
1075+
self.assertEqual(set(layer.selectedFeaturesIds()), set([1, 5, 6, 7, 8]))
1076+
# SelSelection no matching
1077+
layer.selectByExpression('"Class"=\'A380\'', QgsVectorLayer.SetSelection)
1078+
self.assertEqual(set(layer.selectedFeaturesIds()), set([]))
1079+
1080+
# AddToSelection
1081+
layer.selectByExpression('"Importance"=3', QgsVectorLayer.AddToSelection)
1082+
self.assertEqual(set(layer.selectedFeaturesIds()), set([0, 2, 3, 4, 14]))
1083+
layer.selectByExpression('"Importance"=4', QgsVectorLayer.AddToSelection)
1084+
self.assertEqual(set(layer.selectedFeaturesIds()), set([0, 2, 3, 4, 13, 14]))
1085+
1086+
# IntersectSelection
1087+
layer.selectByExpression('"Heading"<100', QgsVectorLayer.IntersectSelection)
1088+
self.assertEqual(set(layer.selectedFeaturesIds()), set([0, 2, 3, 4]))
1089+
layer.selectByExpression('"Cabin Crew"=1', QgsVectorLayer.IntersectSelection)
1090+
self.assertEqual(set(layer.selectedFeaturesIds()), set([2, 3]))
1091+
1092+
# RemoveFromSelection
1093+
layer.selectByExpression('"Heading"=85', QgsVectorLayer.RemoveFromSelection)
1094+
self.assertEqual(set(layer.selectedFeaturesIds()), set([3]))
1095+
layer.selectByExpression('"Heading"=95', QgsVectorLayer.RemoveFromSelection)
1096+
self.assertEqual(set(layer.selectedFeaturesIds()), set([]))
1097+
10661098
def testAggregate(self):
10671099
""" Test aggregate calculation """
10681100
layer = QgsVectorLayer("Point?field=fldint:integer", "layer", "memory")

0 commit comments

Comments
 (0)