Merge pull request #7016 from m-kuhn/featureSourceEmpty

QgsFeatureSource::empty() method
This commit is contained in:
Matthias Kuhn 2018-06-26 13:01:08 +02:00 committed by GitHub
commit d7b85c69f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 254 additions and 2 deletions

View File

@ -260,6 +260,9 @@ An optional ``request`` can be used to optimise the returned
iterator, eg by restricting the returned attributes or geometry.
%End
virtual QgsFeatureSource::FeatureAvailability hasFeatures() const;
virtual QgsFeatureIterator getFeatures( const QgsFeatureRequest &request = QgsFeatureRequest() ) const;
virtual QgsCoordinateReferenceSystem sourceCrs() const;

View File

@ -23,6 +23,13 @@ An interface for objects which provide features via a getFeatures method.
%End
public:
enum FeatureAvailability
{
NoFeaturesAvailable,
FeaturesAvailable,
FeaturesMaybeAvailable
};
virtual ~QgsFeatureSource();
virtual QgsFeatureIterator getFeatures( const QgsFeatureRequest &request = QgsFeatureRequest() ) const = 0;
@ -66,6 +73,13 @@ if the feature count is unknown.
%Docstring
Returns the number of features contained in the source, or -1
if the feature count is unknown.
%End
virtual FeatureAvailability hasFeatures() const;
%Docstring
Determines if there are any features available in the source.
.. versionadded:: 3.2
%End
virtual QSet<QVariant> uniqueValues( int fieldIndex, int limit = -1 ) const;
@ -139,7 +153,6 @@ for its ownership.
.. versionadded:: 3.0
%End
};

View File

@ -122,6 +122,26 @@ Returns the geometry type which is returned by this layer
Number of features in the layer
:return: long containing number of features
%End
virtual bool empty() const;
%Docstring
Returns true if the layer contains at least one feature.
.. versionadded:: 3.4
%End
virtual QgsFeatureSource::FeatureAvailability hasFeatures() const final;
%Docstring
Will always return FeatureAvailability.FeaturesAvailable or
FeatureAvailability.NoFeaturesAvailable.
Calls empty() internally. Providers should override empty()
instead if they provide an optimized version of this call.
.. seealso:: :py:func:`empty`
.. versionadded:: 3.4
%End
virtual QgsFields fields() const = 0;

View File

@ -941,6 +941,21 @@ Number of features rendered with specified legend key. Features must be first
calculated by countSymbolFeatures()
:return: number of features rendered by symbol or -1 if failed or counts are not available
%End
virtual FeatureAvailability hasFeatures() const;
%Docstring
Determines if this vector layer has features.
.. warning::
when a layer is editable and some features
have been deleted, this will return
QgsFeatureSource.FeatureAvailability.FeaturesMayBeAvailable
to avoid a potentially expensive call to the dataprovider.
.. versionadded:: 3.4
%End
void setDataSource( const QString &dataSource, const QString &baseName, const QString &provider, bool loadDefaultStyleFlag = false ) /Deprecated/;

View File

@ -721,6 +721,18 @@ QgsFeatureIterator QgsProcessingFeatureSource::getFeatures( const QgsFeatureRequ
return mSource->getFeatures( req );
}
QgsFeatureSource::FeatureAvailability QgsProcessingFeatureSource::hasFeatures() const
{
FeatureAvailability sourceAvailability = mSource->hasFeatures();
if ( sourceAvailability == NoFeaturesAvailable )
return NoFeaturesAvailable; // never going to be features if underlying source has no features
else if ( mInvalidGeometryCheck == QgsFeatureRequest::GeometryNoCheck )
return sourceAvailability;
else
// we don't know... source has features, but these may be filtered out by invalid geometry check
return FeaturesMaybeAvailable;
}
QgsFeatureIterator QgsProcessingFeatureSource::getFeatures( const QgsFeatureRequest &request ) const
{
QgsFeatureRequest req( request );

View File

@ -317,6 +317,8 @@ class CORE_EXPORT QgsProcessingFeatureSource : public QgsFeatureSource
*/
QgsFeatureIterator getFeatures( const QgsFeatureRequest &request, Flags flags ) const;
QgsFeatureSource::FeatureAvailability hasFeatures() const override;
QgsFeatureIterator getFeatures( const QgsFeatureRequest &request = QgsFeatureRequest() ) const override;
QgsCoordinateReferenceSystem sourceCrs() const override;
QgsFields fields() const override;

View File

@ -23,6 +23,11 @@
#include "qgsvectorlayer.h"
#include "qgsvectordataprovider.h"
QgsFeatureSource::FeatureAvailability QgsFeatureSource::hasFeatures() const
{
return FeaturesMaybeAvailable;
}
QSet<QVariant> QgsFeatureSource::uniqueValues( int fieldIndex, int limit ) const
{
if ( fieldIndex < 0 || fieldIndex >= fields().count() )

View File

@ -38,6 +38,22 @@ class CORE_EXPORT QgsFeatureSource
{
public:
/**
* Possible return value for hasFeatures() to determine if a source is empty.
* It is implemented as a three-value logic, so it can return if
* there are features available for sure, if there are no features
* available for sure or if there might be features available but
* there is no guarantee for this.
*
* \since QGIS 3.4
*/
enum FeatureAvailability
{
NoFeaturesAvailable, //!< There are certainly no features available in this source
FeaturesAvailable, //!< There is at least one feature available in this source
FeaturesMaybeAvailable //!< There may be features available in this source
};
virtual ~QgsFeatureSource() = default;
/**
@ -85,6 +101,13 @@ class CORE_EXPORT QgsFeatureSource
*/
virtual long featureCount() const = 0;
/**
* Determines if there are any features available in the source.
*
* \since QGIS 3.2
*/
virtual FeatureAvailability hasFeatures() const;
/**
* Returns the set of unique values contained within the specified \a fieldIndex from this source.
* If specified, the \a limit option can be used to limit the number of returned values.
@ -150,7 +173,6 @@ class CORE_EXPORT QgsFeatureSource
*/
QgsVectorLayer *materialize( const QgsFeatureRequest &request,
QgsFeedback *feedback = nullptr ) SIP_FACTORY;
};
Q_DECLARE_METATYPE( QgsFeatureSource * )

View File

@ -46,6 +46,27 @@ QString QgsVectorDataProvider::storageType() const
return QStringLiteral( "Generic vector file" );
}
bool QgsVectorDataProvider::empty() const
{
QgsFeature f;
QgsFeatureRequest request;
request.setSubsetOfAttributes( QgsAttributeList() );
request.setFlags( QgsFeatureRequest::NoGeometry );
request.setLimit( 1 );
if ( getFeatures( request ).nextFeature( f ) )
return false;
else
return true;
}
QgsFeatureSource::FeatureAvailability QgsVectorDataProvider::hasFeatures() const
{
if ( empty() )
return QgsFeatureSource::FeatureAvailability::NoFeaturesAvailable;
else
return QgsFeatureSource::FeatureAvailability::FeaturesAvailable;
}
QgsCoordinateReferenceSystem QgsVectorDataProvider::sourceCrs() const
{
return crs();

View File

@ -163,6 +163,25 @@ class CORE_EXPORT QgsVectorDataProvider : public QgsDataProvider, public QgsFeat
*/
long featureCount() const override = 0;
/**
* Returns true if the layer contains at least one feature.
*
* \since QGIS 3.4
*/
virtual bool empty() const;
/**
* Will always return FeatureAvailability::FeaturesAvailable or
* FeatureAvailability::NoFeaturesAvailable.
*
* Calls empty() internally. Providers should override empty()
* instead if they provide an optimized version of this call.
*
* \see empty()
* \since QGIS 3.4
*/
virtual QgsFeatureSource::FeatureAvailability hasFeatures() const final;
/**
* Returns the fields associated with this data provider.
*/

View File

@ -704,6 +704,8 @@ long QgsVectorLayer::featureCount( const QString &legendKey ) const
return mSymbolFeatureCountMap.value( legendKey );
}
QgsVectorLayerFeatureCounter *QgsVectorLayer::countSymbolFeatures()
{
if ( mSymbolFeatureCounted || mFeatureCounter )
@ -2766,6 +2768,25 @@ long QgsVectorLayer::featureCount() const
( mEditBuffer ? mEditBuffer->mAddedFeatures.size() - mEditBuffer->mDeletedFeatureIds.size() : 0 );
}
QgsFeatureSource::FeatureAvailability QgsVectorLayer::hasFeatures() const
{
const QgsFeatureIds deletedFeatures( mEditBuffer ? mEditBuffer->deletedFeatureIds() : QgsFeatureIds() );
const QgsFeatureMap addedFeatures( mEditBuffer ? mEditBuffer->addedFeatures() : QgsFeatureMap() );
if ( mEditBuffer && !deletedFeatures.empty() )
{
if ( addedFeatures.size() > deletedFeatures.size() )
return QgsFeatureSource::FeatureAvailability::FeaturesAvailable;
else
return QgsFeatureSource::FeatureAvailability::FeaturesMaybeAvailable;
}
if ( ( !mEditBuffer || addedFeatures.empty() ) && mDataProvider->empty() )
return QgsFeatureSource::FeatureAvailability::NoFeaturesAvailable;
else
return QgsFeatureSource::FeatureAvailability::FeaturesAvailable;
}
bool QgsVectorLayer::commitChanges()
{
mCommitErrors.clear();

View File

@ -930,6 +930,18 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte
*/
long featureCount( const QString &legendKey ) const;
/**
* Determines if this vector layer has features.
*
* \warning when a layer is editable and some features
* have been deleted, this will return
* QgsFeatureSource::FeatureAvailability::FeaturesMayBeAvailable
* to avoid a potentially expensive call to the dataprovider.
*
* \since QGIS 3.4
*/
FeatureAvailability hasFeatures() const override;
/**
* Update the data source of the layer. The layer's renderer and legend will be preserved only
* if the geometry type of the new data source matches the current geometry type of the layer.

View File

@ -3190,6 +3190,19 @@ long QgsPostgresProvider::featureCount() const
return num;
}
bool QgsPostgresProvider::empty() const
{
QString sql = QStringLiteral( "SELECT EXISTS (SELECT * FROM %1%2 LIMIT 1)" ).arg( mQuery, filterWhereClause() );
QgsPostgresResult res( connectionRO()->PQexec( sql ) );
if ( res.PQresultStatus() != PGRES_TUPLES_OK )
{
pushError( res.PQresultErrorMessage() );
return false;
}
return res.PQgetvalue( 0, 0 ) != 't';
}
QgsRectangle QgsPostgresProvider::extent() const
{
if ( mGeometryColumn.isNull() )

View File

@ -109,6 +109,16 @@ class QgsPostgresProvider : public QgsVectorDataProvider
long featureCount() const override;
/**
* Determines if there is at least one feature available on this table.
*
* \note In contrast to the featureCount() method, this method is not
* affected by estimated metadata.
*
* \since QGIS 3.4
*/
bool empty() const override;
/**
* Returns a string representation of the endian-ness for the layer
*/

View File

@ -1206,6 +1206,27 @@ QString QgsWFSProvider::translateMetadataValue( const QString &mdKey, const QVar
{
return value.toString();
}
}
bool QgsWFSProvider::empty() const
{
QgsFeature f;
QgsFeatureRequest request;
request.setSubsetOfAttributes( QgsAttributeList() );
request.setFlags( QgsFeatureRequest::NoGeometry );
// Whoops, the WFS provider returns an empty iterator when we are using
// a setLimit call in combination with a subsetString.
// Remove this method (and default to the QgsVectorDataProvider one)
// once this is fixed
#if 0
request.setLimit( 1 );
#endif
if ( getFeatures( request ).nextFeature( f ) )
return false;
else
return true;
};
bool QgsWFSProvider::describeFeatureType( QString &geometryAttribute, QgsFields &fields, QgsWkbTypes::Type &geomType )

View File

@ -111,6 +111,8 @@ class QgsWFSProvider : public QgsVectorDataProvider
QString translateMetadataKey( const QString &mdKey ) const override;
QString translateMetadataValue( const QString &mdKey, const QVariant &value ) const override;
bool empty() const override;
public slots:
void reloadData() override;

View File

@ -28,6 +28,7 @@ from qgis.core import (
QgsVectorLayerFeatureSource,
QgsFeatureSink,
QgsTestUtils,
QgsFeatureSource,
NULL
)
from qgis.PyQt.QtTest import QSignalSpy
@ -422,6 +423,46 @@ class ProviderTestCase(FeatureSourceTestCase):
self.assertEqual(count, 0)
self.assertEqual(self.source.featureCount(), 5)
def testEmpty(self):
self.assertFalse(self.source.empty())
self.assertEqual(self.source.hasFeatures(), QgsFeatureSource.FeaturesAvailable)
if self.source.supportsSubsetString():
try:
backup = self.source.subsetString()
# Add a subset string and test feature count
subset = self.getSubsetString()
self.source.setSubsetString(subset)
self.assertFalse(self.source.empty())
self.assertEqual(self.source.hasFeatures(), QgsFeatureSource.FeaturesAvailable)
subsetNoMatching = self.getSubsetStringNoMatching()
self.source.setSubsetString(subsetNoMatching)
self.assertTrue(self.source.empty())
self.assertEqual(self.source.hasFeatures(), QgsFeatureSource.NoFeaturesAvailable)
finally:
self.source.setSubsetString(None)
self.assertFalse(self.source.empty())
# If the provider supports tests on editable layers
if getattr(self, 'getEditableLayer', None):
l = self.getEditableLayer()
self.assertTrue(l.isValid())
self.assertEqual(l.hasFeatures(), QgsFeatureSource.FeaturesAvailable)
# Test that deleting some features in the edit buffer does not
# return empty, we accept FeaturesAvailable as well as
# MaybeAvailable
l.startEditing()
l.deleteFeature(next(l.getFeatures()).id())
self.assertNotEqual(l.hasFeatures(), QgsFeatureSource.NoFeaturesAvailable)
l.rollBack()
# Call truncate(), we need an empty set now
l.dataProvider().truncate()
self.assertTrue(l.dataProvider().empty())
self.assertEqual(l.dataProvider().hasFeatures(), QgsFeatureSource.NoFeaturesAvailable)
def testGetFeaturesNoGeometry(self):
""" Test that no geometry is present when fetching features without geometry"""