port List unique values algorithm to C++

This commit is contained in:
Alexander Bruy 2025-05-08 14:56:45 +01:00 committed by Nyall Dawson
parent 78482ef736
commit 91f1808cc9
8 changed files with 250 additions and 215 deletions

View File

@ -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(),

View File

@ -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("<html><head>")
f.write(
'<meta http-equiv="Content-Type" content="text/html; \
charset=utf-8" /></head><body>'
)
f.write(self.tr("<p>Total unique values: ") + str(len(algData)) + "</p>")
f.write(self.tr("<p>Unique values:</p>"))
f.write("<ul>")
for s in algData:
f.write("<li>" + ",".join(str(attr) for attr in s) + "</li>")
f.write("</ul></body></html>")

View File

@ -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,

View File

@ -556,7 +556,7 @@ tests:
- 'Mean length: 3.0'
- 'NULL \(missing\) values: 1'
- algorithm: qgis:listuniquevalues
- algorithm: native:listuniquevalues
name: Unique values
params:
INPUT:

View File

@ -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

View File

@ -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 &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
{
std::unique_ptr<QgsProcessingFeatureSource> 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<int> 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<QgsFeatureSink> sink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, dest, fields, Qgis::WkbType::NoGeometry, QgsCoordinateReferenceSystem() ) );
if ( !sink )
{
throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "OUTPUT" ) ) );
}
QSet<QgsAttributes> values;
if ( fieldIndices.size() == 1 )
{
const QSet<QVariant> 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( "<html><head><meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\"/></head><body>\n" );
out << QObject::tr( "<p>Total unique values: %1</p>" ).arg( values.size() );
out << QObject::tr( "<p>Unique values:</p>" );
out << QStringLiteral( "<ul>" );
for ( auto &v : std::as_const( valueList ) )
{
out << QStringLiteral( "<li>%1</li>" ).arg( v );
}
out << QStringLiteral( "</ul></body></html>" );
outputs.insert( QStringLiteral( "OUTPUT_HTML_FILE" ), outputHtml );
}
}
return outputs;
}
///@endcond

View File

@ -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 &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override;
};
///@endcond PRIVATE
#endif // QGSALGORITHMUNIQUEVALUES_H

View File

@ -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() );