diff --git a/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py b/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py index 39c90a0b0df..5b4051fb5a1 100644 --- a/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py +++ b/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py @@ -63,7 +63,6 @@ from .StatisticsByCategories import StatisticsByCategories from .TextToFloat import TextToFloat from .TinInterpolation import TinInterpolation from .TopoColors import TopoColor -from .UniqueValues import UniqueValues from .VariableDistanceBuffer import VariableDistanceBuffer from .VectorLayerHistogram import VectorLayerHistogram from .VectorLayerScatterplot import VectorLayerScatterplot @@ -121,7 +120,6 @@ class QgisAlgorithmProvider(QgsProcessingProvider): TextToFloat(), TinInterpolation(), TopoColor(), - UniqueValues(), VariableDistanceBuffer(), VectorLayerHistogram(), VectorLayerScatterplot(), diff --git a/python/plugins/processing/algs/qgis/UniqueValues.py b/python/plugins/processing/algs/qgis/UniqueValues.py deleted file mode 100644 index 87201c92d4a..00000000000 --- a/python/plugins/processing/algs/qgis/UniqueValues.py +++ /dev/null @@ -1,211 +0,0 @@ -""" -*************************************************************************** - UniqueValues.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 codecs - -from qgis.PyQt.QtGui import QIcon - -from qgis.core import ( - QgsApplication, - QgsCoordinateReferenceSystem, - QgsWkbTypes, - QgsFeature, - QgsFeatureSink, - QgsFeatureRequest, - QgsFields, - QgsProcessing, - QgsProcessingException, - QgsProcessingParameterField, - QgsProcessingParameterFeatureSource, - QgsProcessingParameterFeatureSink, - QgsProcessingOutputNumber, - QgsProcessingOutputString, - QgsProcessingFeatureSource, - QgsProcessingParameterFileDestination, -) - -from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm - -pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0] - - -class UniqueValues(QgisAlgorithm): - INPUT = "INPUT" - FIELDS = "FIELDS" - TOTAL_VALUES = "TOTAL_VALUES" - UNIQUE_VALUES = "UNIQUE_VALUES" - OUTPUT = "OUTPUT" - OUTPUT_HTML_FILE = "OUTPUT_HTML_FILE" - - def icon(self): - return QgsApplication.getThemeIcon("/algorithms/mAlgorithmUniqueValues.svg") - - def svgIconPath(self): - return QgsApplication.iconPath("/algorithms/mAlgorithmUniqueValues.svg") - - def group(self): - return self.tr("Vector analysis") - - def groupId(self): - return "vectoranalysis" - - def __init__(self): - super().__init__() - - def initAlgorithm(self, config=None): - self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT, - self.tr("Input layer"), - types=[QgsProcessing.SourceType.TypeVector], - ) - ) - self.addParameter( - QgsProcessingParameterField( - self.FIELDS, - self.tr("Target field(s)"), - parentLayerParameterName=self.INPUT, - type=QgsProcessingParameterField.DataType.Any, - allowMultiple=True, - ) - ) - - self.addParameter( - QgsProcessingParameterFeatureSink( - self.OUTPUT, self.tr("Unique values"), optional=True, defaultValue=None - ) - ) - - self.addParameter( - QgsProcessingParameterFileDestination( - self.OUTPUT_HTML_FILE, - self.tr("HTML report"), - self.tr("HTML files (*.html)"), - None, - True, - ) - ) - self.addOutput( - QgsProcessingOutputNumber(self.TOTAL_VALUES, self.tr("Total unique values")) - ) - self.addOutput( - QgsProcessingOutputString(self.UNIQUE_VALUES, self.tr("Unique values")) - ) - - def name(self): - return "listuniquevalues" - - def displayName(self): - return self.tr("List unique values") - - def processAlgorithm(self, parameters, context, feedback): - source = self.parameterAsSource(parameters, self.INPUT, context) - if source is None: - raise QgsProcessingException( - self.invalidSourceError(parameters, self.INPUT) - ) - - field_names = self.parameterAsFields(parameters, self.FIELDS, context) - - fields = QgsFields() - field_indices = [] - for field_name in field_names: - field_index = source.fields().lookupField(field_name) - if field_index < 0: - feedback.reportError( - self.tr("Invalid field name {}").format(field_name) - ) - continue - field = source.fields()[field_index] - fields.append(field) - field_indices.append(field_index) - (sink, dest_id) = self.parameterAsSink( - parameters, - self.OUTPUT, - context, - fields, - QgsWkbTypes.Type.NoGeometry, - QgsCoordinateReferenceSystem(), - ) - - results = {} - values = set() - if len(field_indices) == 1: - # one field, can use provider optimised method - values = tuple([v] for v in source.uniqueValues(field_indices[0])) - else: - # have to scan whole table - # TODO - add this support to QgsVectorDataProvider so we can run it on - # the backend - request = QgsFeatureRequest().setFlags(QgsFeatureRequest.Flag.NoGeometry) - request.setSubsetOfAttributes(field_indices) - total = 100.0 / source.featureCount() if source.featureCount() else 0 - for current, f in enumerate( - source.getFeatures( - request, - QgsProcessingFeatureSource.Flag.FlagSkipGeometryValidityChecks, - ) - ): - if feedback.isCanceled(): - break - - value = tuple(f.attribute(i) for i in field_indices) - values.add(value) - feedback.setProgress(int(current * total)) - - if sink: - for value in values: - if feedback.isCanceled(): - break - - f = QgsFeature() - f.setAttributes([attr for attr in value]) - sink.addFeature(f, QgsFeatureSink.Flag.FastInsert) - sink.finalize() - results[self.OUTPUT] = dest_id - - output_file = self.parameterAsFileOutput( - parameters, self.OUTPUT_HTML_FILE, context - ) - if output_file: - self.createHTML(output_file, values) - results[self.OUTPUT_HTML_FILE] = output_file - - results[self.TOTAL_VALUES] = len(values) - results[self.UNIQUE_VALUES] = ";".join( - ",".join(str(attr) for attr in v) for v in values - ) - return results - - def createHTML(self, outputFile, algData): - with codecs.open(outputFile, "w", encoding="utf-8") as f: - f.write("") - f.write( - '' - ) - f.write(self.tr("

Total unique values: ") + str(len(algData)) + "

") - f.write(self.tr("

Unique values:

")) - f.write("") diff --git a/python/plugins/processing/gui/menus.py b/python/plugins/processing/gui/menus.py index aaf0cf35d13..5188e0b03c5 100644 --- a/python/plugins/processing/gui/menus.py +++ b/python/plugins/processing/gui/menus.py @@ -58,7 +58,7 @@ def initMenusAndToolbars(): "qgis:distancematrix": analysisToolsMenu, "native:sumlinelengths": analysisToolsMenu, "native:countpointsinpolygon": analysisToolsMenu, - "qgis:listuniquevalues": analysisToolsMenu, + "native:listuniquevalues": analysisToolsMenu, "native:basicstatisticsforfields": analysisToolsMenu, "native:nearestneighbouranalysis": analysisToolsMenu, "native:meancoordinates": analysisToolsMenu, diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests1.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests1.yaml index d06e40e5106..8f264f5cdf7 100644 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests1.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests1.yaml @@ -556,7 +556,7 @@ tests: - 'Mean length: 3.0' - 'NULL \(missing\) values: 1' - - algorithm: qgis:listuniquevalues + - algorithm: native:listuniquevalues name: Unique values params: INPUT: diff --git a/src/analysis/CMakeLists.txt b/src/analysis/CMakeLists.txt index b346b38d1db..2cf8e627d5c 100644 --- a/src/analysis/CMakeLists.txt +++ b/src/analysis/CMakeLists.txt @@ -289,6 +289,7 @@ set(QGIS_ANALYSIS_SRCS processing/qgsalgorithmtranslate.cpp processing/qgsalgorithmtruncatetable.cpp processing/qgsalgorithmunion.cpp + processing/qgsalgorithmuniquevalues.cpp processing/qgsalgorithmuniquevalueindex.cpp processing/qgsalgorithmurlopener.cpp processing/qgsalgorithmhttprequest.cpp diff --git a/src/analysis/processing/qgsalgorithmuniquevalues.cpp b/src/analysis/processing/qgsalgorithmuniquevalues.cpp new file mode 100644 index 00000000000..895212b62a8 --- /dev/null +++ b/src/analysis/processing/qgsalgorithmuniquevalues.cpp @@ -0,0 +1,193 @@ +/*************************************************************************** + qgsalgorithmuniquevalues.cpp + --------------------- + begin : May 2025 + copyright : (C) 2025 by Alexander Bruy + email : alexander dot bruy 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. * + * * + ***************************************************************************/ + +#include "qgsalgorithmuniquevalues.h" + +///@cond PRIVATE + +QString QgsUniqueValuesAlgorithm::name() const +{ + return QStringLiteral( "listuniquevalues" ); +} + +QString QgsUniqueValuesAlgorithm::displayName() const +{ + return QObject::tr( "List unique values" ); +} + +QStringList QgsUniqueValuesAlgorithm::tags() const +{ + return QObject::tr( "count,unique,values" ).split( ',' ); +} + +QString QgsUniqueValuesAlgorithm::group() const +{ + return QObject::tr( "Vector analysis" ); +} + +QString QgsUniqueValuesAlgorithm::groupId() const +{ + return QStringLiteral( "vectoranalysis" ); +} + +QString QgsUniqueValuesAlgorithm::shortHelpString() const +{ + return QObject::tr( "Returns list of unique values in given field(s) of a vector layer." ); +} + +QgsUniqueValuesAlgorithm *QgsUniqueValuesAlgorithm::createInstance() const +{ + return new QgsUniqueValuesAlgorithm(); +} + +void QgsUniqueValuesAlgorithm::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ) ) ); + addParameter( new QgsProcessingParameterField( QStringLiteral( "FIELDS" ), QObject::tr( "Target field(s)" ), QVariant(), QStringLiteral( "INPUT" ), Qgis::ProcessingFieldParameterDataType::Any, true ) ); + addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Unique values" ), Qgis::ProcessingSourceType::Vector, QVariant(), true ) ); + addParameter( new QgsProcessingParameterFileDestination( QStringLiteral( "OUTPUT_HTML_FILE" ), QObject::tr( "HTML report" ), QObject::tr( "HTML files (*.html *.htm)" ), QVariant(), true ) ); + addOutput( new QgsProcessingOutputNumber( QStringLiteral( "TOTAL_VALUES" ), QObject::tr( "Total unique values" ) ) ); + addOutput( new QgsProcessingOutputString( QStringLiteral( "UNIQUE_VALUES" ), QObject::tr( "Unique values" ) ) ); +} + +QVariantMap QgsUniqueValuesAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + std::unique_ptr source( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) ); + if ( !source ) + throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) ); + + const QStringList fieldNames = parameterAsStrings( parameters, QStringLiteral( "FIELDS" ), context ); + const QString outputHtml = parameterAsFileOutput( parameters, QStringLiteral( "OUTPUT_HTML_FILE" ), context ); + + QgsFields fields; + QList fieldIndices; + + for ( auto &fieldName : fieldNames ) + { + int fieldIndex = source->fields().lookupField( fieldName ); + if ( fieldIndex < 0 ) + { + feedback->reportError( QObject::tr( "Invalid field name &1" ).arg( fieldName ) ); + continue; + } + fields.append( source->fields().at( fieldIndex ) ); + fieldIndices << fieldIndex; + } + + QString dest; + std::unique_ptr sink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, dest, fields, Qgis::WkbType::NoGeometry, QgsCoordinateReferenceSystem() ) ); + if ( !sink ) + { + throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "OUTPUT" ) ) ); + } + + QSet values; + if ( fieldIndices.size() == 1 ) + { + const QSet unique = source->uniqueValues( fieldIndices.at( 0 ) ); + for ( auto &v : unique ) + { + values.insert( QgsAttributes() << v ); + } + } + else + { + // we have to scan whole table + // TODO: add this support to QgsVectorDataProvider, so we can run it on the backend + QgsFeatureRequest request; + request.setFlags( Qgis::FeatureRequestFlag::NoGeometry ); + request.setSubsetOfAttributes( fieldIndices ); + + const double step = source->featureCount() > 0 ? 100.0 / source->featureCount() : 0; + QgsFeatureIterator features = source->getFeatures( request ); + QgsFeature f; + long long i = 0; + while ( features.nextFeature( f ) ) + { + if ( feedback->isCanceled() ) + break; + + QgsAttributes attrs; + for ( auto &i : std::as_const( fieldIndices ) ) + { + attrs << f.attribute( i ); + } + values.insert( attrs ); + + i++; + feedback->setProgress( i * step ); + } + } + + QVariantMap outputs; + outputs.insert( QStringLiteral( "TOTAL_VALUES" ), values.size() ); + + QStringList valueList; + for ( auto it = values.constBegin(); it != values.constEnd(); ++it ) + { + QStringList s; + for ( auto &v : std::as_const( *it ) ) + { + s.append( v.toString() ); + } + valueList.append( s ); + } + outputs.insert( QStringLiteral( "UNIQUE_VALUES" ), valueList.join( ';' ) ); + + if ( sink ) + { + for ( auto it = values.constBegin(); it != values.constEnd(); ++it ) + { + if ( feedback->isCanceled() ) + break; + + QgsFeature f; + f.setAttributes( *it ); + if ( !sink->addFeature( f, QgsFeatureSink::Flag::FastInsert ) ) + throw QgsProcessingException( writeFeatureError( sink.get(), parameters, QStringLiteral( "OUTPUT" ) ) ); + } + sink->finalize(); + outputs.insert( QStringLiteral( "OUTPUT" ), dest ); + } + + if ( !outputHtml.isEmpty() ) + { + QFile file( outputHtml ); + if ( file.open( QIODevice::WriteOnly | QIODevice::Truncate ) ) + { + QTextStream out( &file ); +#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 ) + out.setCodec( "UTF-8" ); +#endif + out << QStringLiteral( "\n" ); + out << QObject::tr( "

Total unique values: %1

" ).arg( values.size() ); + out << QObject::tr( "

Unique values:

" ); + out << QStringLiteral( "
    " ); + for ( auto &v : std::as_const( valueList ) ) + { + out << QStringLiteral( "
  • %1
  • " ).arg( v ); + } + out << QStringLiteral( "
" ); + + outputs.insert( QStringLiteral( "OUTPUT_HTML_FILE" ), outputHtml ); + } + } + + return outputs; +} + +///@endcond diff --git a/src/analysis/processing/qgsalgorithmuniquevalues.h b/src/analysis/processing/qgsalgorithmuniquevalues.h new file mode 100644 index 00000000000..e43c04f231b --- /dev/null +++ b/src/analysis/processing/qgsalgorithmuniquevalues.h @@ -0,0 +1,52 @@ +/*************************************************************************** + qgsalgorithmuniquevalues.h + --------------------- + begin : May 2025 + copyright : (C) 2025 by Alexander Bruy + email : alexander dot bruy 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. * + * * + ***************************************************************************/ +#ifndef QGSALGORITHMUNIQUEVALUES_H +#define QGSALGORITHMUNIQUEVALUES_H + +#define SIP_NO_FILE + +#include "qgis_sip.h" +#include "qgsprocessingalgorithm.h" +#include "qgsapplication.h" + +///@cond PRIVATE + +/** + * Native unique values algorithm. + */ +class QgsUniqueValuesAlgorithm : public QgsProcessingAlgorithm +{ + public: + QgsUniqueValuesAlgorithm() = default; + void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override; + QIcon icon() const override { return QgsApplication::getThemeIcon( QStringLiteral( "/algorithms/mAlgorithmUniqueValues.svg" ) ); } + QString svgIconPath() const override { return QgsApplication::iconPath( QStringLiteral( "/algorithms/mAlgorithmUniqueValues.svg" ) ); } + QString name() const override; + QString displayName() const override; + QStringList tags() const override; + QString group() const override; + QString groupId() const override; + QString shortHelpString() const override; + QgsUniqueValuesAlgorithm *createInstance() const override SIP_FACTORY; + + protected: + QVariantMap processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; +}; + +///@endcond PRIVATE + +#endif // QGSALGORITHMUNIQUEVALUES_H diff --git a/src/analysis/processing/qgsnativealgorithms.cpp b/src/analysis/processing/qgsnativealgorithms.cpp index e545c5b5d5a..56db580a079 100644 --- a/src/analysis/processing/qgsnativealgorithms.cpp +++ b/src/analysis/processing/qgsnativealgorithms.cpp @@ -272,6 +272,7 @@ #include "qgsalgorithmtranslate.h" #include "qgsalgorithmtruncatetable.h" #include "qgsalgorithmunion.h" +#include "qgsalgorithmuniquevalues.h" #include "qgsalgorithmuniquevalueindex.h" #include "qgsalgorithmurlopener.h" #include "qgsalgorithmhttprequest.h" @@ -634,6 +635,7 @@ void QgsNativeAlgorithms::loadAlgorithms() addAlgorithm( new QgsTranslateAlgorithm() ); addAlgorithm( new QgsTruncateTableAlgorithm() ); addAlgorithm( new QgsUnionAlgorithm() ); + addAlgorithm( new QgsUniqueValuesAlgorithm() ); addAlgorithm( new QgsUpdateLayerMetadataAlgorithm() ); addAlgorithm( new QgsOpenUrlAlgorithm() ); addAlgorithm( new QgsHttpRequestAlgorithm() );