QGIS API Documentation  3.8.0-Zanzibar (11aff65)
qgsalgorithmcategorizeusingstyle.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsalgorithmcategorizeusingstyle.cpp
3  ---------------------
4  begin : August 2018
5  copyright : (C) 2018 by Nyall Dawson
6  email : nyall dot dawson at gmail dot com
7  ***************************************************************************/
8 
9 /***************************************************************************
10  * *
11  * This program is free software; you can redistribute it and/or modify *
12  * it under the terms of the GNU General Public License as published by *
13  * the Free Software Foundation; either version 2 of the License, or *
14  * (at your option) any later version. *
15  * *
16  ***************************************************************************/
17 
19 #include "qgsstyle.h"
21 #include "qgsvectorlayer.h"
23 
25 
26 QgsCategorizeUsingStyleAlgorithm::QgsCategorizeUsingStyleAlgorithm() = default;
27 
28 QgsCategorizeUsingStyleAlgorithm::~QgsCategorizeUsingStyleAlgorithm() = default;
29 
30 void QgsCategorizeUsingStyleAlgorithm::initAlgorithm( const QVariantMap & )
31 {
32  addParameter( new QgsProcessingParameterVectorLayer( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ),
33  QList< int >() << QgsProcessing::TypeVector ) );
34  addParameter( new QgsProcessingParameterExpression( QStringLiteral( "FIELD" ), QObject::tr( "Categorize using expression" ), QVariant(), QStringLiteral( "INPUT" ) ) );
35 
36  addParameter( new QgsProcessingParameterFile( QStringLiteral( "STYLE" ), QObject::tr( "Style database (leave blank to use saved symbols)" ), QgsProcessingParameterFile::File, QStringLiteral( "xml" ), QVariant(), true ) );
37  addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "CASE_SENSITIVE" ), QObject::tr( "Use case-sensitive match to symbol names" ), false ) );
38  addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "TOLERANT" ), QObject::tr( "Ignore non-alphanumeric characters while matching" ), false ) );
39 
40  addOutput( new QgsProcessingOutputVectorLayer( QStringLiteral( "OUTPUT" ), QObject::tr( "Categorized layer" ) ) );
41 
42  std::unique_ptr< QgsProcessingParameterFeatureSink > failCategories = qgis::make_unique< QgsProcessingParameterFeatureSink >( QStringLiteral( "NON_MATCHING_CATEGORIES" ), QObject::tr( "Non-matching categories" ),
43  QgsProcessing::TypeVector, QVariant(), true, false );
44  // not supported for outputs yet!
45  //failCategories->setFlags( failCategories->flags() | QgsProcessingParameterDefinition::FlagAdvanced );
46  addParameter( failCategories.release() );
47 
48  std::unique_ptr< QgsProcessingParameterFeatureSink > failSymbols = qgis::make_unique< QgsProcessingParameterFeatureSink >( QStringLiteral( "NON_MATCHING_SYMBOLS" ), QObject::tr( "Non-matching symbol names" ),
49  QgsProcessing::TypeVector, QVariant(), true, false );
50  //failSymbols->setFlags( failSymbols->flags() | QgsProcessingParameterDefinition::FlagAdvanced );
51  addParameter( failSymbols.release() );
52 }
53 
54 QString QgsCategorizeUsingStyleAlgorithm::name() const
55 {
56  return QStringLiteral( "categorizeusingstyle" );
57 }
58 
59 QString QgsCategorizeUsingStyleAlgorithm::displayName() const
60 {
61  return QObject::tr( "Create categorized renderer from styles" );
62 }
63 
64 QStringList QgsCategorizeUsingStyleAlgorithm::tags() const
65 {
66  return QObject::tr( "file,database,symbols,names,category,categories" ).split( ',' );
67 }
68 
69 QString QgsCategorizeUsingStyleAlgorithm::group() const
70 {
71  return QObject::tr( "Cartography" );
72 }
73 
74 QString QgsCategorizeUsingStyleAlgorithm::groupId() const
75 {
76  return QStringLiteral( "cartography" );
77 }
78 
79 QString QgsCategorizeUsingStyleAlgorithm::shortHelpString() const
80 {
81  return QObject::tr( "Sets a vector layer's renderer to a categorized renderer using matching symbols from a style database. If no "
82  "style file is specified, symbols from the user's current style library are used instead.\n\n"
83  "The specified expression (or field name) is used to create categories for the renderer. A category will be "
84  "created for each unique value within the layer.\n\n"
85  "Each category is individually matched to the symbols which exist within the specified QGIS XML style database. Whenever "
86  "a matching symbol name is found, the category's symbol will be set to this matched symbol.\n\n"
87  "The matching is case-insensitive by default, but can be made case-sensitive if required.\n\n"
88  "Optionally, non-alphanumeric characters in both the category value and symbol name can be ignored "
89  "while performing the match. This allows for greater tolerance when matching categories to symbols.\n\n"
90  "If desired, tables can also be output containing lists of the categories which could not be matched "
91  "to symbols, and symbols which were not matched to categories."
92  );
93 }
94 
95 QString QgsCategorizeUsingStyleAlgorithm::shortDescription() const
96 {
97  return QObject::tr( "Sets a vector layer's renderer to a categorized renderer using symbols from a style database." );
98 }
99 
100 QgsCategorizeUsingStyleAlgorithm *QgsCategorizeUsingStyleAlgorithm::createInstance() const
101 {
102  return new QgsCategorizeUsingStyleAlgorithm();
103 }
104 
105 class SetCategorizedRendererPostProcessor : public QgsProcessingLayerPostProcessorInterface
106 {
107  public:
108 
109  SetCategorizedRendererPostProcessor( std::unique_ptr< QgsCategorizedSymbolRenderer > renderer )
110  : mRenderer( std::move( renderer ) )
111  {}
112 
114  {
115  if ( QgsVectorLayer *vl = qobject_cast< QgsVectorLayer * >( layer ) )
116  {
117 
118  vl->setRenderer( mRenderer.release() );
119  vl->triggerRepaint();
120  }
121  }
122 
123  private:
124 
125  std::unique_ptr<QgsCategorizedSymbolRenderer> mRenderer;
126 };
127 
128 // Do most of the heavy lifting in a background thread, but save the thread-sensitive stuff for main thread execution!
129 
130 bool QgsCategorizeUsingStyleAlgorithm::prepareAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback * )
131 {
132  QgsVectorLayer *layer = parameterAsVectorLayer( parameters, QStringLiteral( "INPUT" ), context );
133  if ( !layer )
134  throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) );
135 
136  mField = parameterAsString( parameters, QStringLiteral( "FIELD" ), context );
137 
138  mLayerId = layer->id();
139  mLayerName = layer->name();
140  mLayerGeometryType = layer->geometryType();
141  mLayerFields = layer->fields();
142 
143  mExpressionContext << QgsExpressionContextUtils::globalScope()
146 
147  mExpression = QgsExpression( mField );
148  mExpression.prepare( &mExpressionContext );
149 
150  QgsFeatureRequest req;
151  req.setSubsetOfAttributes( mExpression.referencedColumns(), mLayerFields );
152  if ( !mExpression.needsGeometry() )
154 
155  mIterator = layer->getFeatures( req );
156 
157  return true;
158 }
159 
160 QVariantMap QgsCategorizeUsingStyleAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
161 {
162  const QString styleFile = parameterAsFile( parameters, QStringLiteral( "STYLE" ), context );
163  const bool caseSensitive = parameterAsBoolean( parameters, QStringLiteral( "CASE_SENSITIVE" ), context );
164  const bool tolerant = parameterAsBoolean( parameters, QStringLiteral( "TOLERANT" ), context );
165 
166  QgsStyle *style = nullptr;
167  std::unique_ptr< QgsStyle >importedStyle;
168  if ( !styleFile.isEmpty() )
169  {
170  importedStyle = qgis::make_unique< QgsStyle >();
171  if ( !importedStyle->importXml( styleFile ) )
172  {
173  throw QgsProcessingException( QObject::tr( "An error occurred while reading style file: %1" ).arg( importedStyle->errorString() ) );
174  }
175  style = importedStyle.get();
176  }
177  else
178  {
179  style = QgsStyle::defaultStyle();
180  }
181 
182  QgsFields nonMatchingCategoryFields;
183  nonMatchingCategoryFields.append( QgsField( QStringLiteral( "category" ), QVariant::String ) );
184  QString nonMatchingCategoriesDest;
185  std::unique_ptr< QgsFeatureSink > nonMatchingCategoriesSink( parameterAsSink( parameters, QStringLiteral( "NON_MATCHING_CATEGORIES" ), context, nonMatchingCategoriesDest, nonMatchingCategoryFields, QgsWkbTypes::NoGeometry ) );
186  if ( !nonMatchingCategoriesSink && parameters.contains( QStringLiteral( "NON_MATCHING_CATEGORIES" ) ) && parameters.value( QStringLiteral( "NON_MATCHING_CATEGORIES" ) ).isValid() )
187  throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "NON_MATCHING_CATEGORIES" ) ) );
188 
189  QgsFields nonMatchingSymbolFields;
190  nonMatchingSymbolFields.append( QgsField( QStringLiteral( "name" ), QVariant::String ) );
191  QString nonMatchingSymbolsDest;
192  std::unique_ptr< QgsFeatureSink > nonMatchingSymbolsSink( parameterAsSink( parameters, QStringLiteral( "NON_MATCHING_SYMBOLS" ), context, nonMatchingSymbolsDest, nonMatchingSymbolFields, QgsWkbTypes::NoGeometry ) );
193  if ( !nonMatchingSymbolsSink && parameters.contains( QStringLiteral( "NON_MATCHING_SYMBOLS" ) ) && parameters.value( QStringLiteral( "NON_MATCHING_SYMBOLS" ) ).isValid() )
194  throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "NON_MATCHING_SYMBOLS" ) ) );
195 
196  QSet<QVariant> uniqueVals;
197  QgsFeature feature;
198  while ( mIterator.nextFeature( feature ) )
199  {
200  mExpressionContext.setFeature( feature );
201  QVariant value = mExpression.evaluate( &mExpressionContext );
202  if ( uniqueVals.contains( value ) )
203  continue;
204  uniqueVals << value;
205  }
206 
207  QVariantList sortedUniqueVals = uniqueVals.toList();
208  std::sort( sortedUniqueVals.begin(), sortedUniqueVals.end() );
209 
210  QgsCategoryList cats;
211  cats.reserve( uniqueVals.count() );
212  std::unique_ptr< QgsSymbol > defaultSymbol( QgsSymbol::defaultSymbol( mLayerGeometryType ) );
213  for ( const QVariant &val : qgis::as_const( sortedUniqueVals ) )
214  {
215  cats.append( QgsRendererCategory( val, defaultSymbol->clone(), val.toString() ) );
216  }
217 
218  mRenderer = qgis::make_unique< QgsCategorizedSymbolRenderer >( mField, cats );
219 
220  const QgsSymbol::SymbolType type = mLayerGeometryType == QgsWkbTypes::PointGeometry ? QgsSymbol::Marker
221  : mLayerGeometryType == QgsWkbTypes::LineGeometry ? QgsSymbol::Line
222  : QgsSymbol::Fill;
223 
224  QVariantList unmatchedCategories;
225  QStringList unmatchedSymbols;
226  const int matched = mRenderer->matchToSymbols( style, type, unmatchedCategories, unmatchedSymbols, caseSensitive, tolerant );
227 
228  if ( matched > 0 )
229  {
230  feedback->pushInfo( QObject::tr( "Matched %1 categories to symbols from file." ).arg( matched ) );
231  }
232  else
233  {
234  feedback->reportError( QObject::tr( "No categories could be matched to symbols in file." ) );
235  }
236 
237  if ( !unmatchedCategories.empty() )
238  {
239  feedback->pushInfo( QObject::tr( "\n%1 categories could not be matched:" ).arg( unmatchedCategories.count() ) );
240  std::sort( unmatchedCategories.begin(), unmatchedCategories.end() );
241  for ( const QVariant &cat : qgis::as_const( unmatchedCategories ) )
242  {
243  feedback->pushInfo( QStringLiteral( "∙ “%1”" ).arg( cat.toString() ) );
244  if ( nonMatchingCategoriesSink )
245  {
246  QgsFeature f;
247  f.setAttributes( QgsAttributes() << cat.toString() );
248  nonMatchingCategoriesSink->addFeature( f, QgsFeatureSink::FastInsert );
249  }
250  }
251  }
252 
253  if ( !unmatchedSymbols.empty() )
254  {
255  feedback->pushInfo( QObject::tr( "\n%1 symbols in style were not matched:" ).arg( unmatchedSymbols.count() ) );
256  std::sort( unmatchedSymbols.begin(), unmatchedSymbols.end() );
257  for ( const QString &name : qgis::as_const( unmatchedSymbols ) )
258  {
259  feedback->pushInfo( QStringLiteral( "∙ “%1”" ).arg( name ) );
260  if ( nonMatchingSymbolsSink )
261  {
262  QgsFeature f;
263  f.setAttributes( QgsAttributes() << name );
264  nonMatchingSymbolsSink->addFeature( f, QgsFeatureSink::FastInsert );
265  }
266  }
267  }
268 
269  context.addLayerToLoadOnCompletion( mLayerId, QgsProcessingContext::LayerDetails( mLayerName, context.project(), mLayerName ) );
270  context.layerToLoadOnCompletionDetails( mLayerId ).setPostProcessor( new SetCategorizedRendererPostProcessor( std::move( mRenderer ) ) );
271 
272  QVariantMap results;
273  results.insert( QStringLiteral( "OUTPUT" ), mLayerId );
274  if ( nonMatchingCategoriesSink )
275  results.insert( QStringLiteral( "NON_MATCHING_CATEGORIES" ), nonMatchingCategoriesDest );
276  if ( nonMatchingSymbolsSink )
277  results.insert( QStringLiteral( "NON_MATCHING_SYMBOLS" ), nonMatchingSymbolsDest );
278  return results;
279 }
280 
282 
283 
284 
A boolean parameter for processing algorithms.
Class for parsing and evaluation of expressions (formerly called "search strings").
An input file or folder parameter for processing algorithms.
Use faster inserts, at the cost of updating the passed features to reflect changes made at the provid...
Base class for all map layer types.
Definition: qgsmaplayer.h:78
Base class for providing feedback from a processing algorithm.
Represents an individual category (class) from a QgsCategorizedSymbolRenderer.
An interface for layer post-processing handlers for execution following a processing algorithm operat...
An expression parameter for processing algorithms.
QgsFeatureRequest & setSubsetOfAttributes(const QgsAttributeList &attrs)
Set a subset of attributes that will be fetched.
void setPostProcessor(QgsProcessingLayerPostProcessorInterface *processor)
Sets the layer post-processor.
QgsWkbTypes::GeometryType geometryType() const
Returns point, line or polygon.
Container of fields for a vector layer.
Definition: qgsfields.h:42
void setAttributes(const QgsAttributes &attrs)
Sets the feature&#39;s attributes.
Definition: qgsfeature.cpp:127
static QgsExpressionContextScope * projectScope(const QgsProject *project)
Creates a new scope which contains variables and functions relating to a QGIS project.
Line symbol.
Definition: qgssymbol.h:86
The feature class encapsulates a single feature including its id, geometry and a list of field/values...
Definition: qgsfeature.h:55
A vector layer output for processing algorithms.
QgsProject * project() const
Returns the project in which the algorithm is being executed.
static QgsStyle * defaultStyle()
Returns default application-wide style.
Definition: qgsstyle.cpp:46
QList< QgsRendererCategory > QgsCategoryList
SymbolType
Type of the symbol.
Definition: qgssymbol.h:83
static QgsSymbol * defaultSymbol(QgsWkbTypes::GeometryType geomType)
Returns a new default symbol for the specified geometry type.
Definition: qgssymbol.cpp:293
static QgsExpressionContextScope * globalScope()
Creates a new scope which contains variables and functions relating to the global QGIS context...
QString id() const
Returns the layer&#39;s unique ID, which is used to access this layer from QgsProject.
QgsFields fields() const FINAL
Returns the list of fields of this layer.
virtual void pushInfo(const QString &info)
Pushes a general informational message from the algorithm.
This class wraps a request for features to a vector layer (or directly its vector data provider)...
Custom exception class for processing related exceptions.
Definition: qgsexception.h:82
bool append(const QgsField &field, FieldOrigin origin=OriginProvider, int originIndex=-1)
Appends a field. The field must have unique name, otherwise it is rejected (returns false) ...
Definition: qgsfields.cpp:59
A vector layer (with or without geometry) parameter for processing algorithms.
Encapsulate a field in an attribute table or data source.
Definition: qgsfield.h:48
Details for layers to load into projects.
void addLayerToLoadOnCompletion(const QString &layer, const QgsProcessingContext::LayerDetails &details)
Adds a layer to load (by ID or datasource) into the canvas upon completion of the algorithm or model...
Marker symbol.
Definition: qgssymbol.h:85
Fill symbol.
Definition: qgssymbol.h:87
QgsProcessingContext::LayerDetails & layerToLoadOnCompletionDetails(const QString &layer)
Returns a reference to the details for a given layer which is loaded on completion of the algorithm o...
Tables (i.e. vector layers with or without geometry). When used for a sink this indicates the sink ha...
Definition: qgsprocessing.h:53
virtual void postProcessLayer(QgsMapLayer *layer, QgsProcessingContext &context, QgsProcessingFeedback *feedback)=0
Post-processes the specified layer, following successful execution of a processing algorithm...
QgsFeatureIterator getFeatures(const QgsFeatureRequest &request=QgsFeatureRequest()) const FINAL
Queries the layer for features specified in request.
QString name
Definition: qgsmaplayer.h:82
virtual void reportError(const QString &error, bool fatalError=false)
Reports that the algorithm encountered an error while executing.
static QgsExpressionContextScope * layerScope(const QgsMapLayer *layer)
Creates a new scope which contains variables and functions relating to a QgsMapLayer.
Geometry is not required. It may still be returned if e.g. required for a filter condition.
A vector of attributes.
Definition: qgsattributes.h:57
Represents a vector layer which manages a vector based data sets.
Contains information about the context in which a processing algorithm is executed.
QgsFeatureRequest & setFlags(QgsFeatureRequest::Flags flags)
Sets flags that affect how features will be fetched.