Skip to content

Commit f247a7c

Browse files
committed
Add c++ optimised uniqueValues method which respects processing context
Remove processing vector.uniqueValues/vector.getUniqueValues and port usage to c++ method Should be much faster than the python method, as the c++ method takes advantage of handing off the unique values calculation to the provider source whenever possible
1 parent 91679b3 commit f247a7c

File tree

10 files changed

+95
-66
lines changed

10 files changed

+95
-66
lines changed

python/core/processing/qgsprocessingutils.sip

+9
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ class QgsProcessingUtils
115115
:rtype: long
116116
%End
117117

118+
static QList< QVariant > uniqueValues( QgsVectorLayer *layer, int fieldIndex, const QgsProcessingContext &context );
119+
%Docstring
120+
Returns a list of unique values contained in a single field in a ``layer``, when
121+
the settings from the supplied ``context`` are respected. E.g. if the
122+
context is set to only use selected features, then calling this will
123+
return unique values from selected features in the layer.
124+
:rtype: list of QVariant
125+
%End
126+
118127
};
119128

120129

python/plugins/processing/algs/qgis/RandomExtractWithinSubsets.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def processAlgorithm(self, context, feedback):
9090

9191
features = QgsProcessingUtils.getFeatures(layer, context)
9292
featureCount = QgsProcessingUtils.featureCount(layer, context)
93-
unique = vector.getUniqueValues(layer, context, index)
93+
unique = QgsProcessingUtils.uniqueValues(layer, index, context)
9494
value = int(self.getParameterValue(self.NUMBER))
9595
if method == 0:
9696
if value > featureCount:

python/plugins/processing/algs/qgis/RandomSelectionWithinSubsets.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def processAlgorithm(self, context, feedback):
9090
layer.removeSelection()
9191
index = layer.fields().lookupField(field)
9292

93-
unique = vector.getUniqueValues(layer, context, index)
93+
unique = QgsProcessingUtils.uniqueValues(layer, index, context)
9494
featureCount = layer.featureCount()
9595

9696
value = int(self.getParameterValue(self.NUMBER))

python/plugins/processing/algs/qgis/UniqueValues.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@
3131

3232
from qgis.PyQt.QtGui import QIcon
3333

34+
from qgis.core import QgsProcessingUtils
35+
3436
from processing.core.GeoAlgorithm import GeoAlgorithm
3537
from processing.core.parameters import ParameterVector
3638
from processing.core.parameters import ParameterTableField
3739
from processing.core.outputs import OutputHTML
3840
from processing.core.outputs import OutputNumber
3941
from processing.core.outputs import OutputString
40-
from processing.tools import dataobjects, vector
42+
from processing.tools import dataobjects
4143

4244
pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0]
4345

@@ -76,7 +78,7 @@ def processAlgorithm(self, context, feedback):
7678
layer = dataobjects.getLayerFromString(self.getParameterValue(self.INPUT_LAYER))
7779
fieldName = self.getParameterValue(self.FIELD_NAME)
7880
outputFile = self.getOutputValue(self.OUTPUT)
79-
values = vector.getUniqueValues(layer, context, layer.fields().lookupField(fieldName))
81+
values = QgsProcessingUtils.uniqueValues(layer, layer.fields().lookupField(fieldName), context)
8082
self.createHTML(outputFile, values)
8183
self.setOutputValue(self.TOTAL_VALUES, len(values))
8284
self.setOutputValue(self.UNIQUE_VALUES, ';'.join([str(v) for v in

python/plugins/processing/algs/qgis/VectorSplit.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def processAlgorithm(self, context, feedback):
7575
mkdir(directory)
7676

7777
fieldIndex = layer.fields().lookupField(fieldName)
78-
uniqueValues = vector.uniqueValues(layer, context, fieldIndex)
78+
uniqueValues = QgsProcessingUtils.uniqueValues(layer, fieldIndex, context)
7979
baseName = os.path.join(directory, '{0}_{1}'.format(layer.name(), fieldName))
8080

8181
fields = layer.fields()

python/plugins/processing/tests/ToolsTest.py

-26
Original file line numberDiff line numberDiff line change
@@ -91,32 +91,6 @@ def testValues(self):
9191
res = vector.values(test_layer, context, 1)
9292
self.assertEqual(set(res[1]), set([5, 7, 3]))
9393

94-
def testUniqueValues(self):
95-
96-
context = QgsProcessingContext()
97-
# disable check for geometry validity
98-
context.setFlags(QgsProcessingContext.Flags(0))
99-
100-
test_data = points()
101-
test_layer = QgsVectorLayer(test_data, 'test', 'ogr')
102-
103-
# field by index
104-
v = vector.uniqueValues(test_layer, context, 2)
105-
self.assertEqual(len(v), len(set(v)))
106-
self.assertEqual(set(v), set([2, 1, 0]))
107-
108-
# field by name
109-
v = vector.uniqueValues(test_layer, context, 'id2')
110-
self.assertEqual(len(v), len(set(v)))
111-
self.assertEqual(set(v), set([2, 1, 0]))
112-
113-
# test with selected features
114-
context.setFlags(QgsProcessingContext.UseSelectionIfPresent)
115-
test_layer.selectByIds([2, 4, 6])
116-
v = vector.uniqueValues(test_layer, context, 'id')
117-
self.assertEqual(len(v), len(set(v)))
118-
self.assertEqual(set(v), set([5, 7, 3]))
119-
12094
def testOgrLayerNameExtraction(self):
12195
outdir = tempfile.mkdtemp()
12296
self.cleanup_paths.append(outdir)

python/plugins/processing/tools/vector.py

-35
Original file line numberDiff line numberDiff line change
@@ -85,32 +85,6 @@
8585
}
8686

8787

88-
def uniqueValues(layer, context, attribute):
89-
"""Returns a list of unique values for a given attribute.
90-
91-
Attribute can be defined using a field names or a zero-based
92-
field index. It considers the existing selection.
93-
:param context:
94-
"""
95-
96-
fieldIndex = resolveFieldIndex(layer, attribute)
97-
if context.flags() & QgsProcessingContext.UseSelectionIfPresent \
98-
and layer.selectedFeatureCount() > 0:
99-
100-
# iterate through selected features
101-
values = []
102-
request = QgsFeatureRequest().setSubsetOfAttributes([fieldIndex]).setFlags(QgsFeatureRequest.NoGeometry)
103-
feats = QgsProcessingUtils.getFeatures(layer, context, request)
104-
for feat in feats:
105-
if feat.attributes()[fieldIndex] not in values:
106-
values.append(feat.attributes()[fieldIndex])
107-
return values
108-
else:
109-
# no selection, or not considering selecting
110-
# so we can take advantage of provider side unique value optimisations
111-
return layer.uniqueValues(fieldIndex)
112-
113-
11488
def resolveFieldIndex(layer, attr):
11589
"""This method takes an object and returns the index field it
11690
refers to in a layer. If the passed object is an integer, it
@@ -295,15 +269,6 @@ def simpleMeasure(geom, method=0, ellips=None, crs=None):
295269
return (attr1, attr2)
296270

297271

298-
def getUniqueValues(layer, context, fieldIndex):
299-
values = []
300-
feats = QgsProcessingUtils.getFeatures(layer, context)
301-
for feat in feats:
302-
if feat.attributes()[fieldIndex] not in values:
303-
values.append(feat.attributes()[fieldIndex])
304-
return values
305-
306-
307272
def combineVectorFields(layerA, layerB):
308273
"""Create single field map from two input field maps.
309274
"""

src/core/processing/qgsprocessingutils.cpp

+30
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,33 @@ long QgsProcessingUtils::featureCount( QgsVectorLayer *layer, const QgsProcessin
199199
return layer->featureCount();
200200
}
201201

202+
QList<QVariant> QgsProcessingUtils::uniqueValues( QgsVectorLayer *layer, int fieldIndex, const QgsProcessingContext &context )
203+
{
204+
if ( !layer )
205+
return QList<QVariant>();
206+
207+
if ( fieldIndex < 0 || fieldIndex >= layer->fields().count() )
208+
return QList<QVariant>();
209+
210+
bool useSelection = context.flags() & QgsProcessingContext::UseSelectionIfPresent && layer->selectedFeatureCount() > 0;
211+
if ( !useSelection )
212+
{
213+
// not using selection, so use provider optimised version
214+
QList<QVariant> values;
215+
layer->uniqueValues( fieldIndex, values );
216+
return values;
217+
}
218+
else
219+
{
220+
// using selection, so we have to iterate through selected features
221+
QSet<QVariant> values;
222+
QgsFeature f;
223+
QgsFeatureIterator it = layer->selectedFeaturesIterator( QgsFeatureRequest().setSubsetOfAttributes( QgsAttributeList() << fieldIndex ).setFlags( QgsFeatureRequest::NoGeometry ) );
224+
while ( it.nextFeature( f ) )
225+
{
226+
values.insert( f.attribute( fieldIndex ) );
227+
}
228+
return values.toList();
229+
}
230+
}
231+

src/core/processing/qgsprocessingutils.h

+8
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,14 @@ class CORE_EXPORT QgsProcessingUtils
122122
*/
123123
static long featureCount( QgsVectorLayer *layer, const QgsProcessingContext &context );
124124

125+
/**
126+
* Returns a list of unique values contained in a single field in a \a layer, when
127+
* the settings from the supplied \a context are respected. E.g. if the
128+
* context is set to only use selected features, then calling this will
129+
* return unique values from selected features in the layer.
130+
*/
131+
static QList< QVariant > uniqueValues( QgsVectorLayer *layer, int fieldIndex, const QgsProcessingContext &context );
132+
125133
private:
126134

127135
static bool canUseLayer( const QgsRasterLayer *layer );

tests/src/core/testqgsprocessing.cpp

+41
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ class TestQgsProcessing: public QObject
106106
void mapLayerFromString();
107107
void algorithm();
108108
void features();
109+
void uniqueValues();
109110

110111
private:
111112

@@ -528,5 +529,45 @@ void TestQgsProcessing::features()
528529
delete polyLayer;
529530
}
530531

532+
void TestQgsProcessing::uniqueValues()
533+
{
534+
QgsVectorLayer *layer = new QgsVectorLayer( "Point?field=a:integer&field=b:string", "v1", "memory" );
535+
for ( int i = 0; i < 6; ++i )
536+
{
537+
QgsFeature f( i );
538+
f.setAttributes( QgsAttributes() << i % 3 + 1 << QString( QChar( ( i % 3 ) + 65 ) ) );
539+
layer->dataProvider()->addFeatures( QgsFeatureList() << f );
540+
}
541+
542+
QgsProcessingContext context;
543+
context.setFlags( QgsProcessingContext::Flags( 0 ) );
544+
545+
// some bad checks
546+
QVERIFY( QgsProcessingUtils::uniqueValues( nullptr, 0, context ).isEmpty() );
547+
QVERIFY( QgsProcessingUtils::uniqueValues( nullptr, -1, context ).isEmpty() );
548+
QVERIFY( QgsProcessingUtils::uniqueValues( nullptr, 10001, context ).isEmpty() );
549+
QVERIFY( QgsProcessingUtils::uniqueValues( layer, -1, context ).isEmpty() );
550+
QVERIFY( QgsProcessingUtils::uniqueValues( layer, 10001, context ).isEmpty() );
551+
552+
// good checks
553+
QCOMPARE( QgsProcessingUtils::uniqueValues( layer, 0, context ), QList< QVariant >() << 1 << 2 << 3 );
554+
QCOMPARE( QgsProcessingUtils::uniqueValues( layer, 1, context ), QList< QVariant >() << QString( "A" ) << QString( "B" ) << QString( "C" ) );
555+
556+
//using only selected features
557+
layer->selectByIds( QgsFeatureIds() << 1 << 2 << 4 );
558+
// but not using selection yet...
559+
QCOMPARE( QgsProcessingUtils::uniqueValues( layer, 0, context ), QList< QVariant >() << 1 << 2 << 3 );
560+
QCOMPARE( QgsProcessingUtils::uniqueValues( layer, 1, context ), QList< QVariant >() << QString( "A" ) << QString( "B" ) << QString( "C" ) );
561+
562+
// selection and using selection
563+
context.setFlags( QgsProcessingContext::UseSelectionIfPresent );
564+
QVERIFY( QgsProcessingUtils::uniqueValues( layer, -1, context ).isEmpty() );
565+
QVERIFY( QgsProcessingUtils::uniqueValues( layer, 10001, context ).isEmpty() );
566+
QCOMPARE( QgsProcessingUtils::uniqueValues( layer, 0, context ), QList< QVariant >() << 1 << 2 );
567+
QCOMPARE( QgsProcessingUtils::uniqueValues( layer, 1, context ), QList< QVariant >() << QString( "A" ) << QString( "B" ) );
568+
569+
delete layer;
570+
}
571+
531572
QGSTEST_MAIN( TestQgsProcessing )
532573
#include "testqgsprocessing.moc"

0 commit comments

Comments
 (0)