Branch data Line data Source code
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 : :
18 : : #include "qgsalgorithmcategorizeusingstyle.h"
19 : : #include "qgsstyle.h"
20 : : #include "qgscategorizedsymbolrenderer.h"
21 : : #include "qgsvectorlayer.h"
22 : : #include "qgsexpressioncontextutils.h"
23 : :
24 : : ///@cond PRIVATE
25 : :
26 : 0 : QgsCategorizeUsingStyleAlgorithm::QgsCategorizeUsingStyleAlgorithm() = default;
27 : :
28 : 0 : QgsCategorizeUsingStyleAlgorithm::~QgsCategorizeUsingStyleAlgorithm() = default;
29 : :
30 : 0 : void QgsCategorizeUsingStyleAlgorithm::initAlgorithm( const QVariantMap & )
31 : : {
32 : 0 : addParameter( new QgsProcessingParameterVectorLayer( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ),
33 : 0 : QList< int >() << QgsProcessing::TypeVector ) );
34 : 0 : addParameter( new QgsProcessingParameterExpression( QStringLiteral( "FIELD" ), QObject::tr( "Categorize using expression" ), QVariant(), QStringLiteral( "INPUT" ) ) );
35 : :
36 : 0 : addParameter( new QgsProcessingParameterFile( QStringLiteral( "STYLE" ), QObject::tr( "Style database (leave blank to use saved symbols)" ), QgsProcessingParameterFile::File, QStringLiteral( "xml" ), QVariant(), true ) );
37 : 0 : addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "CASE_SENSITIVE" ), QObject::tr( "Use case-sensitive match to symbol names" ), false ) );
38 : 0 : addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "TOLERANT" ), QObject::tr( "Ignore non-alphanumeric characters while matching" ), false ) );
39 : :
40 : 0 : addOutput( new QgsProcessingOutputVectorLayer( QStringLiteral( "OUTPUT" ), QObject::tr( "Categorized layer" ) ) );
41 : :
42 : 0 : std::unique_ptr< QgsProcessingParameterFeatureSink > failCategories = std::make_unique< QgsProcessingParameterFeatureSink >( QStringLiteral( "NON_MATCHING_CATEGORIES" ), QObject::tr( "Non-matching categories" ),
43 : 0 : QgsProcessing::TypeVector, QVariant(), true, false );
44 : : // not supported for outputs yet!
45 : : //failCategories->setFlags( failCategories->flags() | QgsProcessingParameterDefinition::FlagAdvanced );
46 : 0 : addParameter( failCategories.release() );
47 : :
48 : 0 : std::unique_ptr< QgsProcessingParameterFeatureSink > failSymbols = std::make_unique< QgsProcessingParameterFeatureSink >( QStringLiteral( "NON_MATCHING_SYMBOLS" ), QObject::tr( "Non-matching symbol names" ),
49 : 0 : QgsProcessing::TypeVector, QVariant(), true, false );
50 : : //failSymbols->setFlags( failSymbols->flags() | QgsProcessingParameterDefinition::FlagAdvanced );
51 : 0 : addParameter( failSymbols.release() );
52 : 0 : }
53 : :
54 : 0 : QgsProcessingAlgorithm::Flags QgsCategorizeUsingStyleAlgorithm::flags() const
55 : : {
56 : 0 : Flags f = QgsProcessingAlgorithm::flags();
57 : 0 : f |= FlagNotAvailableInStandaloneTool;
58 : 0 : return f;
59 : : }
60 : :
61 : 0 : QString QgsCategorizeUsingStyleAlgorithm::name() const
62 : 0 : {
63 : 0 : return QStringLiteral( "categorizeusingstyle" );
64 : : }
65 : :
66 : 0 : QString QgsCategorizeUsingStyleAlgorithm::displayName() const
67 : : {
68 : 0 : return QObject::tr( "Create categorized renderer from styles" );
69 : : }
70 : :
71 : 0 : QStringList QgsCategorizeUsingStyleAlgorithm::tags() const
72 : : {
73 : 0 : return QObject::tr( "file,database,symbols,names,category,categories" ).split( ',' );
74 : 0 : }
75 : :
76 : 0 : QString QgsCategorizeUsingStyleAlgorithm::group() const
77 : : {
78 : 0 : return QObject::tr( "Cartography" );
79 : : }
80 : :
81 : 0 : QString QgsCategorizeUsingStyleAlgorithm::groupId() const
82 : : {
83 : 0 : return QStringLiteral( "cartography" );
84 : : }
85 : :
86 : 0 : QString QgsCategorizeUsingStyleAlgorithm::shortHelpString() const
87 : : {
88 : 0 : return QObject::tr( "Sets a vector layer's renderer to a categorized renderer using matching symbols from a style database. If no "
89 : : "style file is specified, symbols from the user's current style library are used instead.\n\n"
90 : : "The specified expression (or field name) is used to create categories for the renderer. A category will be "
91 : : "created for each unique value within the layer.\n\n"
92 : : "Each category is individually matched to the symbols which exist within the specified QGIS XML style database. Whenever "
93 : : "a matching symbol name is found, the category's symbol will be set to this matched symbol.\n\n"
94 : : "The matching is case-insensitive by default, but can be made case-sensitive if required.\n\n"
95 : : "Optionally, non-alphanumeric characters in both the category value and symbol name can be ignored "
96 : : "while performing the match. This allows for greater tolerance when matching categories to symbols.\n\n"
97 : : "If desired, tables can also be output containing lists of the categories which could not be matched "
98 : : "to symbols, and symbols which were not matched to categories."
99 : : );
100 : : }
101 : :
102 : 0 : QString QgsCategorizeUsingStyleAlgorithm::shortDescription() const
103 : : {
104 : 0 : return QObject::tr( "Sets a vector layer's renderer to a categorized renderer using symbols from a style database." );
105 : : }
106 : :
107 : 0 : QgsCategorizeUsingStyleAlgorithm *QgsCategorizeUsingStyleAlgorithm::createInstance() const
108 : : {
109 : 0 : return new QgsCategorizeUsingStyleAlgorithm();
110 : 0 : }
111 : :
112 : 0 : class SetCategorizedRendererPostProcessor : public QgsProcessingLayerPostProcessorInterface
113 : : {
114 : : public:
115 : :
116 : 0 : SetCategorizedRendererPostProcessor( std::unique_ptr< QgsCategorizedSymbolRenderer > renderer )
117 : 0 : : mRenderer( std::move( renderer ) )
118 : 0 : {}
119 : :
120 : 0 : void postProcessLayer( QgsMapLayer *layer, QgsProcessingContext &, QgsProcessingFeedback * ) override
121 : : {
122 : 0 : if ( QgsVectorLayer *vl = qobject_cast< QgsVectorLayer * >( layer ) )
123 : : {
124 : :
125 : 0 : vl->setRenderer( mRenderer.release() );
126 : 0 : vl->triggerRepaint();
127 : 0 : }
128 : 0 : }
129 : :
130 : : private:
131 : :
132 : : std::unique_ptr<QgsCategorizedSymbolRenderer> mRenderer;
133 : : };
134 : :
135 : : // Do most of the heavy lifting in a background thread, but save the thread-sensitive stuff for main thread execution!
136 : :
137 : 0 : bool QgsCategorizeUsingStyleAlgorithm::prepareAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback * )
138 : : {
139 : 0 : QgsVectorLayer *layer = parameterAsVectorLayer( parameters, QStringLiteral( "INPUT" ), context );
140 : 0 : if ( !layer )
141 : 0 : throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) );
142 : :
143 : 0 : mField = parameterAsString( parameters, QStringLiteral( "FIELD" ), context );
144 : :
145 : 0 : mLayerId = layer->id();
146 : 0 : mLayerName = layer->name();
147 : 0 : mLayerGeometryType = layer->geometryType();
148 : 0 : mLayerFields = layer->fields();
149 : :
150 : 0 : mExpressionContext << QgsExpressionContextUtils::globalScope()
151 : 0 : << QgsExpressionContextUtils::projectScope( context.project() )
152 : 0 : << QgsExpressionContextUtils::layerScope( layer );
153 : :
154 : 0 : mExpression = QgsExpression( mField );
155 : 0 : mExpression.prepare( &mExpressionContext );
156 : :
157 : 0 : QgsFeatureRequest req;
158 : 0 : req.setSubsetOfAttributes( mExpression.referencedColumns(), mLayerFields );
159 : 0 : if ( !mExpression.needsGeometry() )
160 : 0 : req.setFlags( QgsFeatureRequest::NoGeometry );
161 : :
162 : 0 : mIterator = layer->getFeatures( req );
163 : :
164 : : return true;
165 : 0 : }
166 : :
167 : 0 : QVariantMap QgsCategorizeUsingStyleAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
168 : : {
169 : 0 : const QString styleFile = parameterAsFile( parameters, QStringLiteral( "STYLE" ), context );
170 : 0 : const bool caseSensitive = parameterAsBoolean( parameters, QStringLiteral( "CASE_SENSITIVE" ), context );
171 : 0 : const bool tolerant = parameterAsBoolean( parameters, QStringLiteral( "TOLERANT" ), context );
172 : :
173 : 0 : QgsStyle *style = nullptr;
174 : 0 : std::unique_ptr< QgsStyle >importedStyle;
175 : 0 : if ( !styleFile.isEmpty() )
176 : : {
177 : 0 : importedStyle = std::make_unique< QgsStyle >();
178 : 0 : if ( !importedStyle->importXml( styleFile ) )
179 : : {
180 : 0 : throw QgsProcessingException( QObject::tr( "An error occurred while reading style file: %1" ).arg( importedStyle->errorString() ) );
181 : : }
182 : 0 : style = importedStyle.get();
183 : 0 : }
184 : : else
185 : : {
186 : 0 : style = QgsStyle::defaultStyle();
187 : : }
188 : :
189 : 0 : QgsFields nonMatchingCategoryFields;
190 : 0 : nonMatchingCategoryFields.append( QgsField( QStringLiteral( "category" ), QVariant::String ) );
191 : 0 : QString nonMatchingCategoriesDest;
192 : 0 : std::unique_ptr< QgsFeatureSink > nonMatchingCategoriesSink( parameterAsSink( parameters, QStringLiteral( "NON_MATCHING_CATEGORIES" ), context, nonMatchingCategoriesDest, nonMatchingCategoryFields, QgsWkbTypes::NoGeometry ) );
193 : 0 : if ( !nonMatchingCategoriesSink && parameters.contains( QStringLiteral( "NON_MATCHING_CATEGORIES" ) ) && parameters.value( QStringLiteral( "NON_MATCHING_CATEGORIES" ) ).isValid() )
194 : 0 : throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "NON_MATCHING_CATEGORIES" ) ) );
195 : :
196 : 0 : QgsFields nonMatchingSymbolFields;
197 : 0 : nonMatchingSymbolFields.append( QgsField( QStringLiteral( "name" ), QVariant::String ) );
198 : 0 : QString nonMatchingSymbolsDest;
199 : 0 : std::unique_ptr< QgsFeatureSink > nonMatchingSymbolsSink( parameterAsSink( parameters, QStringLiteral( "NON_MATCHING_SYMBOLS" ), context, nonMatchingSymbolsDest, nonMatchingSymbolFields, QgsWkbTypes::NoGeometry ) );
200 : 0 : if ( !nonMatchingSymbolsSink && parameters.contains( QStringLiteral( "NON_MATCHING_SYMBOLS" ) ) && parameters.value( QStringLiteral( "NON_MATCHING_SYMBOLS" ) ).isValid() )
201 : 0 : throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "NON_MATCHING_SYMBOLS" ) ) );
202 : :
203 : 0 : QSet<QVariant> uniqueVals;
204 : 0 : QgsFeature feature;
205 : 0 : while ( mIterator.nextFeature( feature ) )
206 : : {
207 : 0 : mExpressionContext.setFeature( feature );
208 : 0 : QVariant value = mExpression.evaluate( &mExpressionContext );
209 : 0 : if ( uniqueVals.contains( value ) )
210 : 0 : continue;
211 : 0 : uniqueVals << value;
212 : 0 : }
213 : :
214 : 0 : QVariantList sortedUniqueVals = qgis::setToList( uniqueVals );
215 : 0 : std::sort( sortedUniqueVals.begin(), sortedUniqueVals.end() );
216 : :
217 : 0 : QgsCategoryList cats;
218 : 0 : cats.reserve( uniqueVals.count() );
219 : 0 : std::unique_ptr< QgsSymbol > defaultSymbol( QgsSymbol::defaultSymbol( mLayerGeometryType ) );
220 : 0 : for ( const QVariant &val : std::as_const( sortedUniqueVals ) )
221 : : {
222 : 0 : cats.append( QgsRendererCategory( val, defaultSymbol->clone(), val.toString() ) );
223 : : }
224 : :
225 : 0 : mRenderer = std::make_unique< QgsCategorizedSymbolRenderer >( mField, cats );
226 : :
227 : 0 : const QgsSymbol::SymbolType type = mLayerGeometryType == QgsWkbTypes::PointGeometry ? QgsSymbol::Marker
228 : 0 : : mLayerGeometryType == QgsWkbTypes::LineGeometry ? QgsSymbol::Line
229 : : : QgsSymbol::Fill;
230 : :
231 : 0 : QVariantList unmatchedCategories;
232 : 0 : QStringList unmatchedSymbols;
233 : 0 : const int matched = mRenderer->matchToSymbols( style, type, unmatchedCategories, unmatchedSymbols, caseSensitive, tolerant );
234 : :
235 : 0 : if ( matched > 0 )
236 : : {
237 : 0 : feedback->pushInfo( QObject::tr( "Matched %1 categories to symbols from file." ).arg( matched ) );
238 : 0 : }
239 : : else
240 : : {
241 : 0 : feedback->reportError( QObject::tr( "No categories could be matched to symbols in file." ) );
242 : : }
243 : :
244 : 0 : if ( !unmatchedCategories.empty() )
245 : : {
246 : 0 : feedback->pushInfo( QObject::tr( "\n%1 categories could not be matched:" ).arg( unmatchedCategories.count() ) );
247 : 0 : std::sort( unmatchedCategories.begin(), unmatchedCategories.end() );
248 : 0 : for ( const QVariant &cat : std::as_const( unmatchedCategories ) )
249 : : {
250 : 0 : feedback->pushInfo( QStringLiteral( "∙ “%1”" ).arg( cat.toString() ) );
251 : 0 : if ( nonMatchingCategoriesSink )
252 : : {
253 : 0 : QgsFeature f;
254 : 0 : f.setAttributes( QgsAttributes() << cat.toString() );
255 : 0 : nonMatchingCategoriesSink->addFeature( f, QgsFeatureSink::FastInsert );
256 : 0 : }
257 : : }
258 : 0 : }
259 : :
260 : 0 : if ( !unmatchedSymbols.empty() )
261 : : {
262 : 0 : feedback->pushInfo( QObject::tr( "\n%1 symbols in style were not matched:" ).arg( unmatchedSymbols.count() ) );
263 : 0 : std::sort( unmatchedSymbols.begin(), unmatchedSymbols.end() );
264 : 0 : for ( const QString &name : std::as_const( unmatchedSymbols ) )
265 : : {
266 : 0 : feedback->pushInfo( QStringLiteral( "∙ “%1”" ).arg( name ) );
267 : 0 : if ( nonMatchingSymbolsSink )
268 : : {
269 : 0 : QgsFeature f;
270 : 0 : f.setAttributes( QgsAttributes() << name );
271 : 0 : nonMatchingSymbolsSink->addFeature( f, QgsFeatureSink::FastInsert );
272 : 0 : }
273 : : }
274 : 0 : }
275 : :
276 : 0 : context.addLayerToLoadOnCompletion( mLayerId, QgsProcessingContext::LayerDetails( mLayerName, context.project(), mLayerName ) );
277 : 0 : context.layerToLoadOnCompletionDetails( mLayerId ).setPostProcessor( new SetCategorizedRendererPostProcessor( std::move( mRenderer ) ) );
278 : :
279 : 0 : QVariantMap results;
280 : 0 : results.insert( QStringLiteral( "OUTPUT" ), mLayerId );
281 : 0 : if ( nonMatchingCategoriesSink )
282 : 0 : results.insert( QStringLiteral( "NON_MATCHING_CATEGORIES" ), nonMatchingCategoriesDest );
283 : 0 : if ( nonMatchingSymbolsSink )
284 : 0 : results.insert( QStringLiteral( "NON_MATCHING_SYMBOLS" ), nonMatchingSymbolsDest );
285 : 0 : return results;
286 : 0 : }
287 : :
288 : : ///@endcond
289 : :
290 : :
291 : :
|