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("")
- for s in algData:
- f.write("- " + ",".join(str(attr) for attr in s) + "
")
- 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() );