Skip to content

Commit

Permalink
Add QgsVectorLayer::uniqueStringsMatching() to retrieve
Browse files Browse the repository at this point in the history
a list of unique strings matching a substring for a field
  • Loading branch information
nyalldawson authored and m-kuhn committed Nov 16, 2016
1 parent 3242321 commit 4682eaf
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 1 deletion.
16 changes: 16 additions & 0 deletions python/core/qgsvectorlayer.sip
Original file line number Diff line number Diff line change
Expand Up @@ -1326,6 +1326,22 @@ class QgsVectorLayer : QgsMapLayer, QgsExpressionContextGenerator
*/
void uniqueValues( int index, QList<QVariant> &uniqueValues /Out/, int limit = -1 ) const;

/**
* Returns unique string values of an attribute which contain a specified subset string. Subset
* matching is done in a case-insensitive manner. Note that
* in some circumstances when unsaved changes are present for the layer then the returned list
* may contain outdated values (for instance when the attribute value in a saved feature has
* been changed inside the edit buffer then the previous saved value will be included in the
* returned list).
* @param index column index for attribute
* @param substring substring to match (case insensitive)
* @param limit maxmum number of the values to return, or -1 to return all unique values
* @param feedback optional feedback object for cancelling request
* @returns list of unique strings containing substring
*/
QStringList uniqueStringsMatching( int index, const QString& substring, int limit = -1,
QgsFeedback* feedback = nullptr ) const;

/** Returns the minimum value for an attribute column or an invalid variant in case of error.
* Note that in some circumstances when unsaved changes are present for the layer then the
* returned value may be outdated (for instance when the attribute value in a saved feature has
Expand Down
2 changes: 1 addition & 1 deletion src/core/qgsvectordataprovider.h
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ class CORE_EXPORT QgsVectorDataProvider : public QgsDataProvider
* @param substring substring to match (case insensitive)
* @param limit maxmum number of the values to return, or -1 to return all unique values
* @param feedback optional feedback object for cancelling request
* @returns list of unique strings containg substring
* @returns list of unique strings containing substring
*/
virtual QStringList uniqueStringsMatching( int index, const QString& substring, int limit = -1,
QgsFeedback* feedback = nullptr ) const;
Expand Down
102 changes: 102 additions & 0 deletions src/core/qgsvectorlayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
#include "qgspallabeling.h"
#include "qgssimplifymethod.h"
#include "qgsexpressioncontext.h"
#include "qgsfeedback.h"

#include "diagram/qgsdiagram.h"

Expand Down Expand Up @@ -3223,6 +3224,107 @@ void QgsVectorLayer::uniqueValues( int index, QList<QVariant> &uniqueValues, int
Q_ASSERT_X( false, "QgsVectorLayer::uniqueValues()", "Unknown source of the field!" );
}

QStringList QgsVectorLayer::uniqueStringsMatching( int index, const QString& substring, int limit, QgsFeedback* feedback ) const
{
QStringList results;
if ( !mDataProvider )
{
return results;
}

QgsFields::FieldOrigin origin = mFields.fieldOrigin( index );
switch ( origin )
{
case QgsFields::OriginUnknown:
return results;

case QgsFields::OriginProvider: //a provider field
{
results = mDataProvider->uniqueStringsMatching( index, substring, limit, feedback );

if ( mEditBuffer )
{
QgsFeatureMap added = mEditBuffer->addedFeatures();
QMapIterator< QgsFeatureId, QgsFeature > addedIt( added );
while ( addedIt.hasNext() && ( limit < 0 || results.count() < limit ) && ( !feedback || !feedback->isCancelled() ) )
{
addedIt.next();
QVariant v = addedIt.value().attribute( index );
if ( v.isValid() )
{
QString vs = v.toString();
if ( vs.contains( substring, Qt::CaseInsensitive ) && !results.contains( vs ) )
{
results << vs;
}
}
}

QMapIterator< QgsFeatureId, QgsAttributeMap > it( mEditBuffer->changedAttributeValues() );
while ( it.hasNext() && ( limit < 0 || results.count() < limit ) && ( !feedback || !feedback->isCancelled() ) )
{
it.next();
QVariant v = it.value().value( index );
if ( v.isValid() )
{
QString vs = v.toString();
if ( vs.contains( substring, Qt::CaseInsensitive ) && !results.contains( vs ) )
{
results << vs;
}
}
}
}

return results;
}

case QgsFields::OriginEdit:
// the layer is editable, but in certain cases it can still be avoided going through all features
if ( mEditBuffer->mDeletedFeatureIds.isEmpty() &&
mEditBuffer->mAddedFeatures.isEmpty() &&
!mEditBuffer->mDeletedAttributeIds.contains( index ) &&
mEditBuffer->mChangedAttributeValues.isEmpty() )
{
return mDataProvider->uniqueStringsMatching( index, substring, limit, feedback );
}
FALLTHROUGH;
//we need to go through each feature
case QgsFields::OriginJoin:
case QgsFields::OriginExpression:
{
QgsAttributeList attList;
attList << index;

QgsFeatureRequest request;
request.setSubsetOfAttributes( attList );
request.setFlags( QgsFeatureRequest::NoGeometry );
QString fieldName = mFields.at( index ).name();
request.setFilterExpression( QStringLiteral( "\"%1\" ILIKE '%%2%'" ).arg( fieldName, substring ) );
QgsFeatureIterator fit = getFeatures( request );

QgsFeature f;
QString currentValue;
while ( fit.nextFeature( f ) )
{
currentValue = f.attribute( index ).toString();
if ( !results.contains( currentValue ) )
results << currentValue;

if (( limit >= 0 && results.size() >= limit ) || ( feedback && feedback->isCancelled() ) )
{
break;
}
}

return results;
}
}

Q_ASSERT_X( false, "QgsVectorLayer::uniqueStringsMatching()", "Unknown source of the field!" );
return results;
}

QVariant QgsVectorLayer::minimumValue( int index ) const
{
if ( !mDataProvider )
Expand Down
17 changes: 17 additions & 0 deletions src/core/qgsvectorlayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class QgsVectorLayerEditBuffer;
class QgsVectorLayerJoinBuffer;
class QgsAbstractVectorLayerLabeling;
class QgsPointV2;
class QgsFeedback;

typedef QList<int> QgsAttributeList;
typedef QSet<int> QgsAttributeIds;
Expand Down Expand Up @@ -1468,6 +1469,22 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte
*/
void uniqueValues( int index, QList<QVariant> &uniqueValues, int limit = -1 ) const;

/**
* Returns unique string values of an attribute which contain a specified subset string. Subset
* matching is done in a case-insensitive manner. Note that
* in some circumstances when unsaved changes are present for the layer then the returned list
* may contain outdated values (for instance when the attribute value in a saved feature has
* been changed inside the edit buffer then the previous saved value will be included in the
* returned list).
* @param index column index for attribute
* @param substring substring to match (case insensitive)
* @param limit maxmum number of the values to return, or -1 to return all unique values
* @param feedback optional feedback object for cancelling request
* @returns list of unique strings containing substring
*/
QStringList uniqueStringsMatching( int index, const QString& substring, int limit = -1,
QgsFeedback* feedback = nullptr ) const;

/** Returns the minimum value for an attribute column or an invalid variant in case of error.
* Note that in some circumstances when unsaved changes are present for the layer then the
* returned value may be outdated (for instance when the attribute value in a saved feature has
Expand Down
47 changes: 47 additions & 0 deletions tests/src/python/test_qgsvectorlayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,53 @@ def testUniqueValue(self):
# note - this isn't 100% accurate, since 123 no longer exists - but it avoids looping through all features
self.assertEqual(set(layer.uniqueValues(1)), set([123, 457, 888, -1, 0, 999, 9999, 481523]))

def testUniqueStringsMatching(self):
""" test retrieving unique strings matching subset """
layer = QgsVectorLayer("Point?field=fldtxt:string", "addfeat", "memory")
pr = layer.dataProvider()
f = QgsFeature()
f.setAttributes(["apple"])
f2 = QgsFeature()
f2.setAttributes(["orange"])
f3 = QgsFeature()
f3.setAttributes(["pear"])
f4 = QgsFeature()
f4.setAttributes(["BanaNa"])
f5 = QgsFeature()
f5.setAttributes(["ApriCot"])
assert pr.addFeatures([f, f2, f3, f4, f5])
assert layer.featureCount() == 5

# test layer with just provider features
self.assertEqual(set(layer.uniqueStringsMatching(0, 'N')), set(['orange', 'BanaNa']))

# add feature with new value
layer.startEditing()
f1 = QgsFeature()
f1.setAttributes(["waterMelon"])
self.assertTrue(layer.addFeature(f1))

# should be included in unique values
self.assertEqual(set(layer.uniqueStringsMatching(0, 'N')), set(['orange', 'BanaNa', 'waterMelon']))
# add it again, should be no change
f2 = QgsFeature()
f2.setAttributes(["waterMelon"])
self.assertTrue(layer.addFeature(f1))
self.assertEqual(set(layer.uniqueStringsMatching(0, 'N')), set(['orange', 'BanaNa', 'waterMelon']))
self.assertEqual(set(layer.uniqueStringsMatching(0, 'aN')), set(['orange', 'BanaNa']))
# add another feature
f3 = QgsFeature()
f3.setAttributes(["pineapple"])
self.assertTrue(layer.addFeature(f3))
self.assertEqual(set(layer.uniqueStringsMatching(0, 'n')), set(['orange', 'BanaNa', 'waterMelon', 'pineapple']))

# change an attribute value to a new unique value
f = QgsFeature()
f1_id = next(layer.getFeatures()).id()
self.assertTrue(layer.changeAttributeValue(f1_id, 0, 'coconut'))
# note - this isn't 100% accurate, since orange no longer exists - but it avoids looping through all features
self.assertEqual(set(layer.uniqueStringsMatching(0, 'n')), set(['orange', 'BanaNa', 'waterMelon', 'pineapple', 'coconut']))

def testMinValue(self):
""" test retrieving minimum values """
layer = createLayerWithFivePoints()
Expand Down

0 comments on commit 4682eaf

Please sign in to comment.