diff --git a/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py b/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py index bf8950e6ffae..53e719623ff3 100644 --- a/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py +++ b/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py @@ -47,7 +47,6 @@ from .RandomPointsAlongLines import RandomPointsAlongLines from .RandomPointsLayer import RandomPointsLayer from .RandomPointsPolygons import RandomPointsPolygons -from .RandomSelection import RandomSelection from .RandomSelectionWithinSubsets import RandomSelectionWithinSubsets from .RasterCalculator import RasterCalculator from .RasterLayerHistogram import RasterLayerHistogram @@ -103,7 +102,6 @@ def getAlgs(self): RandomPointsAlongLines(), RandomPointsLayer(), RandomPointsPolygons(), - RandomSelection(), RandomSelectionWithinSubsets(), RasterCalculator(), RasterLayerHistogram(), diff --git a/python/plugins/processing/algs/qgis/RandomSelection.py b/python/plugins/processing/algs/qgis/RandomSelection.py deleted file mode 100644 index b19e6c1f1b4e..000000000000 --- a/python/plugins/processing/algs/qgis/RandomSelection.py +++ /dev/null @@ -1,145 +0,0 @@ -""" -*************************************************************************** - RandomSelection.py - --------------------- - Date : August 2012 - Copyright : (C) 2012 by Victor Olaya - Email : volayaf at gmail dot com -*************************************************************************** -* * -* This program is free software; you can redistribute it and/or modify * -* it under the terms of the GNU General Public License as published by * -* the Free Software Foundation; either version 2 of the License, or * -* (at your option) any later version. * -* * -*************************************************************************** -""" - -__author__ = "Victor Olaya" -__date__ = "August 2012" -__copyright__ = "(C) 2012, Victor Olaya" - -import os -import random - -from qgis.PyQt.QtGui import QIcon -from qgis.core import ( - QgsApplication, - QgsFeatureSink, - QgsProcessingException, - QgsProcessingUtils, - QgsProcessingAlgorithm, - QgsProcessingParameterVectorLayer, - QgsProcessingParameterEnum, - QgsProcessingParameterNumber, - QgsProcessingParameterFeatureSink, - QgsProcessingOutputVectorLayer, -) - -from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm - -pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0] - - -class RandomSelection(QgisAlgorithm): - INPUT = "INPUT" - OUTPUT = "OUTPUT" - METHOD = "METHOD" - NUMBER = "NUMBER" - - def icon(self): - return QgsApplication.getThemeIcon("/algorithms/mAlgorithmSelectRandom.svg") - - def svgIconPath(self): - return QgsApplication.iconPath("/algorithms/mAlgorithmSelectRandom.svg") - - def group(self): - return self.tr("Vector selection") - - def groupId(self): - return "vectorselection" - - def flags(self): - return ( - super().flags() - | QgsProcessingAlgorithm.Flag.FlagNoThreading - | QgsProcessingAlgorithm.Flag.FlagNotAvailableInStandaloneTool - ) - - def __init__(self): - super().__init__() - - def initAlgorithm(self, config=None): - self.methods = [ - self.tr("Number of selected features"), - self.tr("Percentage of selected features"), - ] - - self.addParameter( - QgsProcessingParameterVectorLayer(self.INPUT, self.tr("Input layer")) - ) - self.addParameter( - QgsProcessingParameterEnum( - self.METHOD, self.tr("Method"), self.methods, False, 0 - ) - ) - self.addParameter( - QgsProcessingParameterNumber( - self.NUMBER, - self.tr("Number/percentage of selected features"), - QgsProcessingParameterNumber.Type.Integer, - 10, - False, - 0.0, - ) - ) - self.addOutput( - QgsProcessingOutputVectorLayer(self.OUTPUT, self.tr("Selected (random)")) - ) - - def name(self): - return "randomselection" - - def displayName(self): - return self.tr("Random selection") - - def shortDescription(self): - return self.tr("Randomly selects features from a vector layer.") - - def shortHelpString(self): - return self.tr( - "This algorithm takes a vector layer and selects a subset of its features. " - "No new layer is generated by this algorithm.\n" - "The subset is defined randomly, using a percentage or count value to define " - "the total number of features in the subset." - ) - - def processAlgorithm(self, parameters, context, feedback): - layer = self.parameterAsVectorLayer(parameters, self.INPUT, context) - method = self.parameterAsEnum(parameters, self.METHOD, context) - - ids = layer.allFeatureIds() - value = self.parameterAsInt(parameters, self.NUMBER, context) - - if method == 0: - if value > len(ids): - raise QgsProcessingException( - self.tr( - "Selected number is greater than feature count. " - "Choose a lower value and try again." - ) - ) - else: - if value > 100: - raise QgsProcessingException( - self.tr( - "Percentage can't be greater than 100. Set a " - "different value and try again." - ) - ) - value = int(round(value / 100.0, 4) * len(ids)) - - selran = random.sample(ids, value) - - layer.selectByIds(selran) - return {self.OUTPUT: parameters[self.INPUT]} diff --git a/src/analysis/processing/qgsalgorithmrandomextract.cpp b/src/analysis/processing/qgsalgorithmrandomextract.cpp index eb2e1df35861..09fe4be2776c 100644 --- a/src/analysis/processing/qgsalgorithmrandomextract.cpp +++ b/src/analysis/processing/qgsalgorithmrandomextract.cpp @@ -17,34 +17,99 @@ #include "qgsalgorithmrandomextract.h" -#include #include +#include "qgsvectorlayer.h" + ///@cond PRIVATE -QString QgsRandomExtractAlgorithm::name() const +void QgsRandomExtractSelectAlgorithmBase::sampleFeatureIds( QgsFeatureSource *source, const long long count, QgsProcessingFeedback *feedback ) { - return u"randomextract"_s; + // Build a list of all feature ids + QgsFeatureIterator fit = source->getFeatures( QgsFeatureRequest() + .setFlags( Qgis::FeatureRequestFlag::NoGeometry ) + .setNoAttributes() ); + std::vector allFeats; + allFeats.reserve( count ); + QgsFeature f; + feedback->pushInfo( QObject::tr( "Building list of all features..." ) ); + while ( fit.nextFeature( f ) ) + { + if ( feedback->isCanceled() ) + return; + allFeats.push_back( f.id() ); + } + feedback->pushInfo( QObject::tr( "Done." ) ); + + // initialize random engine + std::random_device randomDevice; + std::mt19937 mersenneTwister( randomDevice() ); + std::uniform_int_distribution fidsDistribution; + + // If the number of features to select is greater than half the total number of features + // we will instead randomly select features to *exclude* from the output layer + const std::size_t actualFeatureCount = allFeats.size(); + std::size_t shuffledFeatureCount = count; + bool invertSelection = static_cast( count ) > actualFeatureCount / 2; + if ( invertSelection ) + shuffledFeatureCount = actualFeatureCount - count; + + std::size_t nb = actualFeatureCount; + + // Shuffle features at the start of the iterator + feedback->pushInfo( QObject::tr( "Randomly selecting %1 features" ).arg( count ) ); + auto cursor = allFeats.begin(); + using difference_type = std::vector::difference_type; + while ( shuffledFeatureCount-- ) + { + if ( feedback->isCanceled() ) + return; + + // Update the distribution to match the number of unshuffled features + fidsDistribution.param( std::uniform_int_distribution::param_type( 0, nb - 1 ) ); + // Swap the current feature with a random one + std::swap( *cursor, *( cursor + static_cast( fidsDistribution( mersenneTwister ) ) ) ); + // Move the cursor to the next feature + ++cursor; + + // Decrement the number of unshuffled features + --nb; + } + + // Insert the selected features into a QgsFeatureIds set + if ( invertSelection ) + for ( auto it = cursor; it != allFeats.end(); ++it ) + mSelectedFeatureIds.insert( *it ); + else + for ( auto it = allFeats.begin(); it != cursor; ++it ) + mSelectedFeatureIds.insert( *it ); } -QString QgsRandomExtractAlgorithm::displayName() const +QString QgsRandomExtractSelectAlgorithmBase::group() const { - return QObject::tr( "Random extract" ); + return QObject::tr( "Vector selection" ); } -QStringList QgsRandomExtractAlgorithm::tags() const +QString QgsRandomExtractSelectAlgorithmBase::groupId() const { - return QObject::tr( "extract,filter,random,number,percentage" ).split( ',' ); + return u"vectorselection"_s; } -QString QgsRandomExtractAlgorithm::group() const +// Random extract algorithm + +QString QgsRandomExtractAlgorithm::name() const { - return QObject::tr( "Vector selection" ); + return u"randomextract"_s; } -QString QgsRandomExtractAlgorithm::groupId() const +QString QgsRandomExtractAlgorithm::displayName() const { - return u"vectorselection"_s; + return QObject::tr( "Random extract" ); +} + +QStringList QgsRandomExtractAlgorithm::tags() const +{ + return QObject::tr( "extract,filter,random,number,percentage" ).split( ',' ); } QString QgsRandomExtractAlgorithm::shortDescription() const @@ -91,8 +156,8 @@ QVariantMap QgsRandomExtractAlgorithm::processAlgorithm( const QVariantMap ¶ throw QgsProcessingException( invalidSinkError( parameters, u"OUTPUT"_s ) ); const int method = parameterAsEnum( parameters, u"METHOD"_s, context ); - int number = parameterAsInt( parameters, u"NUMBER"_s, context ); - const long count = source->featureCount(); + long long number = parameterAsInt( parameters, u"NUMBER"_s, context ); + const long long count = source->featureCount(); if ( method == 0 ) { @@ -106,84 +171,113 @@ QVariantMap QgsRandomExtractAlgorithm::processAlgorithm( const QVariantMap ¶ if ( number > 100 ) throw QgsProcessingException( QObject::tr( "Percentage can't be greater than 100. Choose a lower value and try again." ) ); - number = static_cast( std::ceil( number * count / 100 ) ); + number = static_cast( std::ceil( number * count / 100 ) ); } - // Build a list of all feature ids - QgsFeatureIterator fit = source->getFeatures( QgsFeatureRequest() - .setFlags( Qgis::FeatureRequestFlag::NoGeometry ) - .setNoAttributes() ); - std::vector allFeats; - allFeats.reserve( count ); + sampleFeatureIds( source.get(), number, feedback ); + + feedback->pushInfo( QObject::tr( "Adding selected features" ) ); QgsFeature f; - feedback->pushInfo( QObject::tr( "Building list of all features..." ) ); + QgsFeatureIterator fit = source->getFeatures( QgsFeatureRequest().setFilterFids( mSelectedFeatureIds ), Qgis::ProcessingFeatureSourceFlag::SkipGeometryValidityChecks ); while ( fit.nextFeature( f ) ) { if ( feedback->isCanceled() ) return QVariantMap(); - allFeats.push_back( f.id() ); + + if ( !sink->addFeature( f, QgsFeatureSink::FastInsert ) ) + throw QgsProcessingException( writeFeatureError( sink.get(), parameters, u"OUTPUT"_s ) ); } - feedback->pushInfo( QObject::tr( "Done." ) ); - // initialize random engine - std::random_device randomDevice; - std::mt19937 mersenneTwister( randomDevice() ); - std::uniform_int_distribution fidsDistribution; + sink->finalize(); - // If the number of features to select is greater than half the total number of features - // we will instead randomly select features to *exclude* from the output layer - size_t actualFeatureCount = allFeats.size(); - size_t shuffledFeatureCount = number; - bool invertSelection = static_cast( number ) > actualFeatureCount / 2; - if ( invertSelection ) - shuffledFeatureCount = actualFeatureCount - number; + QVariantMap outputs; + outputs.insert( u"OUTPUT"_s, dest ); + return outputs; +} - size_t nb = actualFeatureCount; +// Random selection algorithm - // Shuffle features at the start of the iterator - feedback->pushInfo( QObject::tr( "Randomly select %1 features" ).arg( number ) ); - auto cursor = allFeats.begin(); - using difference_type = std::vector::difference_type; - while ( shuffledFeatureCount-- ) - { - if ( feedback->isCanceled() ) - return QVariantMap(); +QString QgsRandomSelectionAlgorithm::name() const +{ + return u"randomselection"_s; +} - // Update the distribution to match the number of unshuffled features - fidsDistribution.param( std::uniform_int_distribution::param_type( 0, nb - 1 ) ); - // Swap the current feature with a random one - std::swap( *cursor, *( cursor + static_cast( fidsDistribution( mersenneTwister ) ) ) ); - // Move the cursor to the next feature - ++cursor; +QString QgsRandomSelectionAlgorithm::displayName() const +{ + return QObject::tr( "Random selection" ); +} - // Decrement the number of unshuffled features - --nb; - } +QStringList QgsRandomSelectionAlgorithm::tags() const +{ + return QObject::tr( "select,random,number,percentage" ).split( ',' ); +} - // Insert the selected features into a QgsFeatureIds set - QgsFeatureIds selected; - if ( invertSelection ) - for ( auto it = cursor; it != allFeats.end(); ++it ) - selected.insert( *it ); - else - for ( auto it = allFeats.begin(); it != cursor; ++it ) - selected.insert( *it ); +QString QgsRandomSelectionAlgorithm::shortDescription() const +{ + return QObject::tr( "Randomly selects features from a vector layer." ); +} - feedback->pushInfo( QObject::tr( "Adding selected features" ) ); - fit = source->getFeatures( QgsFeatureRequest().setFilterFids( selected ), Qgis::ProcessingFeatureSourceFlag::SkipGeometryValidityChecks ); - while ( fit.nextFeature( f ) ) +QString QgsRandomSelectionAlgorithm::shortHelpString() const +{ + return QObject::tr( "This algorithm takes a vector layer and selects a subset of its features. " + "No new layer is generated by this algorithm.\n\n" + "The subset is defined randomly, using a percentage or count value to define " + "the total number of features in the subset." ); +} + +QgsRandomSelectionAlgorithm *QgsRandomSelectionAlgorithm::createInstance() const +{ + return new QgsRandomSelectionAlgorithm(); +} + +void QgsRandomSelectionAlgorithm::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterVectorLayer( u"INPUT"_s, QObject::tr( "Input layer" ), QList() << static_cast( Qgis::ProcessingSourceType::Vector ) ) ); + addParameter( new QgsProcessingParameterEnum( u"METHOD"_s, QObject::tr( "Method" ), QStringList() << QObject::tr( "Number of features" ) << QObject::tr( "Percentage of features" ), false, 0 ) ); + addParameter( new QgsProcessingParameterNumber( u"NUMBER"_s, QObject::tr( "Number/percentage of features" ), Qgis::ProcessingNumberParameterType::Integer, 10, false, 0 ) ); + + addOutput( new QgsProcessingOutputVectorLayer( u"OUTPUT"_s, QObject::tr( "Selected (random)" ) ) ); +} + +QVariantMap QgsRandomSelectionAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + mInput = parameters.value( u"INPUT"_s ); + mTargetLayer = parameterAsVectorLayer( parameters, u"INPUT"_s, context ); + + if ( !mTargetLayer ) + throw QgsProcessingException( QObject::tr( "Could not load source layer for INPUT." ) ); + + const int method = parameterAsEnum( parameters, u"METHOD"_s, context ); + long long number = parameterAsInt( parameters, u"NUMBER"_s, context ); + const long long count = mTargetLayer->featureCount(); + + if ( method == 0 ) { - if ( feedback->isCanceled() ) - return QVariantMap(); + // number of features + if ( number > count ) + throw QgsProcessingException( QObject::tr( "Selected number is greater than feature count. Choose a lower value and try again." ) ); + } + else + { + // percentage of features + if ( number > 100 ) + throw QgsProcessingException( QObject::tr( "Percentage can't be greater than 100. Choose a lower value and try again." ) ); - if ( !sink->addFeature( f, QgsFeatureSink::FastInsert ) ) - throw QgsProcessingException( writeFeatureError( sink.get(), parameters, u"OUTPUT"_s ) ); + number = static_cast( std::ceil( number * count / 100 ) ); } - sink->finalize(); + // Insert the selected features into a QgsFeatureIds set + sampleFeatureIds( mTargetLayer, number, feedback ); + + return QVariantMap(); +} + +QVariantMap QgsRandomSelectionAlgorithm::postProcessAlgorithm( QgsProcessingContext &, QgsProcessingFeedback * ) +{ + mTargetLayer->selectByIds( mSelectedFeatureIds ); QVariantMap outputs; - outputs.insert( u"OUTPUT"_s, dest ); + outputs.insert( u"OUTPUT"_s, mInput ); return outputs; } diff --git a/src/analysis/processing/qgsalgorithmrandomextract.h b/src/analysis/processing/qgsalgorithmrandomextract.h index d27c9e5dee1d..d15dc78307ef 100644 --- a/src/analysis/processing/qgsalgorithmrandomextract.h +++ b/src/analysis/processing/qgsalgorithmrandomextract.h @@ -21,22 +21,39 @@ #define SIP_NO_FILE #include "qgis_sip.h" +#include "qgsapplication.h" #include "qgsprocessingalgorithm.h" ///@cond PRIVATE +/** + * Base class for random extraction/selection algorithms. + */ +class QgsRandomExtractSelectAlgorithmBase : public QgsProcessingAlgorithm +{ + public: + QString group() const override; + QString groupId() const override; + + protected: + /** + * Selectes \a count random feature IDs from the \a source. + */ + void sampleFeatureIds( QgsFeatureSource *source, const long long count, QgsProcessingFeedback *feedback ); + + QgsFeatureIds mSelectedFeatureIds; +}; + /** * Native random extract algorithm. */ -class QgsRandomExtractAlgorithm : public QgsProcessingAlgorithm +class QgsRandomExtractAlgorithm : public QgsRandomExtractSelectAlgorithmBase { public: QgsRandomExtractAlgorithm() = default; QString name() const override; QString displayName() const override; QStringList tags() const override; - QString group() const override; - QString groupId() const override; QString shortHelpString() const override; QString shortDescription() const override; Qgis::ProcessingAlgorithmDocumentationFlags documentationFlags() const override; @@ -47,6 +64,32 @@ class QgsRandomExtractAlgorithm : public QgsProcessingAlgorithm QVariantMap processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; }; +/** + * Native random selection algorithm. + */ +class QgsRandomSelectionAlgorithm : public QgsRandomExtractSelectAlgorithmBase +{ + public: + QgsRandomSelectionAlgorithm() = default; + QString name() const override; + QString displayName() const override; + QStringList tags() const override; + QString shortHelpString() const override; + QString shortDescription() const override; + QIcon icon() const override { return QgsApplication::getThemeIcon( u"/algorithms/mAlgorithmSelectRandom.svg"_s ); } + QString svgIconPath() const override { return QgsApplication::iconPath( u"/algorithms/mAlgorithmSelectRandom.svg"_s ); } + void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override; + QgsRandomSelectionAlgorithm *createInstance() const override SIP_FACTORY; + + protected: + QVariantMap processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + QVariantMap postProcessAlgorithm( QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + + private: + QVariant mInput; + QgsVectorLayer *mTargetLayer = nullptr; +}; + ///@endcond PRIVATE #endif // QGSALGORITHMRANDOMEXTRACT_H diff --git a/src/analysis/processing/qgsnativealgorithms.cpp b/src/analysis/processing/qgsnativealgorithms.cpp index 2c2c542de066..48e699ff8853 100644 --- a/src/analysis/processing/qgsnativealgorithms.cpp +++ b/src/analysis/processing/qgsnativealgorithms.cpp @@ -560,6 +560,7 @@ void QgsNativeAlgorithms::loadAlgorithms() addAlgorithm( new QgsRandomPointsInPolygonsAlgorithm() ); addAlgorithm( new QgsRandomPointsOnLinesAlgorithm() ); addAlgorithm( new QgsRandomPoissonRasterAlgorithm() ); + addAlgorithm( new QgsRandomSelectionAlgorithm() ); addAlgorithm( new QgsRandomUniformRasterAlgorithm() ); addAlgorithm( new QgsRasterCalculatorAlgorithm() ); addAlgorithm( new QgsRasterCalculatorModelerAlgorithm() );