Branch data Line data Source code
1 : : /***************************************************************************
2 : : qgsclassificationmethod.cpp
3 : : ---------------------
4 : : begin : September 2019
5 : : copyright : (C) 2019 by Denis Rouzaud
6 : : email : denis@opengis.ch
7 : : ***************************************************************************
8 : : * *
9 : : * This program is free software; you can redistribute it and/or modify *
10 : : * it under the terms of the GNU General Public License as published by *
11 : : * the Free Software Foundation; either version 2 of the License, or *
12 : : * (at your option) any later version. *
13 : : * *
14 : : ***************************************************************************/
15 : :
16 : : #include <QRegularExpression>
17 : :
18 : : #include "qgis.h"
19 : : #include "qgsclassificationmethod.h"
20 : : #include "qgsvectorlayerutils.h"
21 : : #include "qgsvectorlayer.h"
22 : : #include "qgsgraduatedsymbolrenderer.h"
23 : : #include "qgsapplication.h"
24 : : #include "qgsclassificationmethodregistry.h"
25 : : #include "qgsxmlutils.h"
26 : :
27 : : const int QgsClassificationMethod::MAX_PRECISION = 15;
28 : : const int QgsClassificationMethod::MIN_PRECISION = -6;
29 : :
30 : :
31 : 0 : QList<double> QgsClassificationMethod::rangesToBreaks( const QList<QgsClassificationRange> &classes )
32 : : {
33 : 0 : QList<double> values;
34 : 0 : values.reserve( classes.count() );
35 : 0 : for ( int i = 0 ; i < classes.count(); i++ )
36 : 0 : values << classes.at( i ).upperBound();
37 : 0 : return values;
38 : 0 : }
39 : :
40 : 30 : QgsClassificationMethod::QgsClassificationMethod( MethodProperties properties, int codeComplexity )
41 : 30 : : mFlags( properties )
42 : 30 : , mCodeComplexity( codeComplexity )
43 : 60 : , mLabelFormat( QStringLiteral( "%1 - %2" ) )
44 : 30 : {
45 : 30 : }
46 : :
47 : 30 : QgsClassificationMethod::~QgsClassificationMethod()
48 : 30 : {
49 : 30 : qDeleteAll( mParameters );
50 : 30 : }
51 : :
52 : 0 : void QgsClassificationMethod::copyBase( QgsClassificationMethod *c ) const
53 : : {
54 : 0 : c->setSymmetricMode( mSymmetricEnabled, mSymmetryPoint, mSymmetryAstride );
55 : 0 : c->setLabelFormat( mLabelFormat );
56 : 0 : c->setLabelPrecision( mLabelPrecision );
57 : 0 : c->setLabelTrimTrailingZeroes( mLabelTrimTrailingZeroes );
58 : 0 : c->setParameterValues( mParameterValues );
59 : 0 : }
60 : :
61 : 0 : QgsClassificationMethod *QgsClassificationMethod::create( const QDomElement &element, const QgsReadWriteContext &context )
62 : : {
63 : 0 : const QString methodId = element.attribute( QStringLiteral( "id" ) );
64 : 0 : QgsClassificationMethod *method = QgsApplication::classificationMethodRegistry()->method( methodId );
65 : :
66 : : // symmetric
67 : 0 : QDomElement symmetricModeElem = element.firstChildElement( QStringLiteral( "symmetricMode" ) );
68 : 0 : if ( !symmetricModeElem.isNull() )
69 : : {
70 : 0 : bool symmetricEnabled = symmetricModeElem.attribute( QStringLiteral( "enabled" ) ).toInt() == 1;
71 : 0 : double symmetricPoint = symmetricModeElem.attribute( QStringLiteral( "symmetrypoint" ) ).toDouble();
72 : 0 : bool astride = symmetricModeElem.attribute( QStringLiteral( "astride" ) ).toInt() == 1;
73 : 0 : method->setSymmetricMode( symmetricEnabled, symmetricPoint, astride );
74 : 0 : }
75 : :
76 : : // label format
77 : 0 : QDomElement labelFormatElem = element.firstChildElement( QStringLiteral( "labelformat" ) );
78 : 0 : if ( !labelFormatElem.isNull() )
79 : : {
80 : 0 : QString format = labelFormatElem.attribute( QStringLiteral( "format" ), "%1" + QStringLiteral( " - " ) + "%2" );
81 : 0 : int precision = labelFormatElem.attribute( QStringLiteral( "labelprecision" ), QStringLiteral( "4" ) ).toInt();
82 : 0 : bool trimTrailingZeroes = labelFormatElem.attribute( QStringLiteral( "trimtrailingzeroes" ), QStringLiteral( "false" ) ) == QLatin1String( "true" );
83 : 0 : method->setLabelFormat( format );
84 : 0 : method->setLabelPrecision( precision );
85 : 0 : method->setLabelTrimTrailingZeroes( trimTrailingZeroes );
86 : 0 : }
87 : :
88 : : // parameters (processing parameters)
89 : 0 : QDomElement parametersElem = element.firstChildElement( QStringLiteral( "parameters" ) );
90 : 0 : const QVariantMap parameterValues = QgsXmlUtils::readVariant( parametersElem.firstChildElement() ).toMap();
91 : 0 : method->setParameterValues( parameterValues );
92 : :
93 : : // Read specific properties from the implementation
94 : 0 : QDomElement extraElem = element.firstChildElement( QStringLiteral( "extraInformation" ) );
95 : 0 : if ( !extraElem.isNull() )
96 : 0 : method->readXml( extraElem, context );
97 : :
98 : 0 : return method;
99 : 0 : }
100 : :
101 : 0 : QDomElement QgsClassificationMethod::save( QDomDocument &doc, const QgsReadWriteContext &context ) const
102 : : {
103 : 0 : QDomElement methodElem = doc.createElement( QStringLiteral( "classificationMethod" ) );
104 : :
105 : 0 : methodElem.setAttribute( QStringLiteral( "id" ), id() );
106 : :
107 : : // symmetric
108 : 0 : QDomElement symmetricModeElem = doc.createElement( QStringLiteral( "symmetricMode" ) );
109 : 0 : symmetricModeElem.setAttribute( QStringLiteral( "enabled" ), symmetricModeEnabled() ? 1 : 0 );
110 : 0 : symmetricModeElem.setAttribute( QStringLiteral( "symmetrypoint" ), symmetryPoint() );
111 : 0 : symmetricModeElem.setAttribute( QStringLiteral( "astride" ), mSymmetryAstride ? 1 : 0 );
112 : 0 : methodElem.appendChild( symmetricModeElem );
113 : :
114 : : // label format
115 : 0 : QDomElement labelFormatElem = doc.createElement( QStringLiteral( "labelFormat" ) );
116 : 0 : labelFormatElem.setAttribute( QStringLiteral( "format" ), labelFormat() );
117 : 0 : labelFormatElem.setAttribute( QStringLiteral( "labelprecision" ), labelPrecision() );
118 : 0 : labelFormatElem.setAttribute( QStringLiteral( "trimtrailingzeroes" ), labelTrimTrailingZeroes() ? 1 : 0 );
119 : 0 : methodElem.appendChild( labelFormatElem );
120 : :
121 : : // parameters (processing parameters)
122 : 0 : QDomElement parametersElem = doc.createElement( QStringLiteral( "parameters" ) );
123 : 0 : parametersElem.appendChild( QgsXmlUtils::writeVariant( mParameterValues, doc ) );
124 : 0 : methodElem.appendChild( parametersElem );
125 : :
126 : : // extra information
127 : 0 : QDomElement extraElem = doc.createElement( QStringLiteral( "extraInformation" ) );
128 : 0 : writeXml( extraElem, context );
129 : 0 : methodElem.appendChild( extraElem );
130 : :
131 : 0 : return methodElem;
132 : 0 : }
133 : :
134 : :
135 : 0 : void QgsClassificationMethod::setSymmetricMode( bool enabled, double symmetryPoint, bool astride )
136 : : {
137 : 0 : mSymmetricEnabled = enabled;
138 : 0 : mSymmetryPoint = symmetryPoint;
139 : 0 : mSymmetryAstride = astride;
140 : 0 : }
141 : :
142 : 0 : void QgsClassificationMethod::setLabelPrecision( int precision )
143 : : {
144 : : // Limit the range of decimal places to a reasonable range
145 : 0 : precision = std::clamp( precision, MIN_PRECISION, MAX_PRECISION );
146 : 0 : mLabelPrecision = precision;
147 : 0 : mLabelNumberScale = 1.0;
148 : 0 : mLabelNumberSuffix.clear();
149 : 0 : while ( precision < 0 )
150 : : {
151 : 0 : precision++;
152 : 0 : mLabelNumberScale /= 10.0;
153 : 0 : mLabelNumberSuffix.append( '0' );
154 : : }
155 : 0 : }
156 : :
157 : 0 : QString QgsClassificationMethod::formatNumber( double value ) const
158 : : {
159 : 0 : static const QRegularExpression RE_TRAILING_ZEROES = QRegularExpression( "[.,]?0*$" );
160 : 0 : static const QRegularExpression RE_NEGATIVE_ZERO = QRegularExpression( "^\\-0(?:[.,]0*)?$" );
161 : 0 : if ( mLabelPrecision > 0 )
162 : : {
163 : 0 : QString valueStr = QLocale().toString( value, 'f', mLabelPrecision );
164 : 0 : if ( mLabelTrimTrailingZeroes )
165 : 0 : valueStr = valueStr.remove( RE_TRAILING_ZEROES );
166 : 0 : if ( RE_NEGATIVE_ZERO.match( valueStr ).hasMatch() )
167 : 0 : valueStr = valueStr.mid( 1 );
168 : 0 : return valueStr;
169 : 0 : }
170 : : else
171 : : {
172 : 0 : QString valueStr = QLocale().toString( value * mLabelNumberScale, 'f', 0 );
173 : 0 : if ( valueStr == QLatin1String( "-0" ) )
174 : 0 : valueStr = '0';
175 : 0 : if ( valueStr != QLatin1String( "0" ) )
176 : 0 : valueStr = valueStr + mLabelNumberSuffix;
177 : 0 : return valueStr;
178 : 0 : }
179 : 0 : }
180 : :
181 : 5 : void QgsClassificationMethod::addParameter( QgsProcessingParameterDefinition *definition )
182 : : {
183 : 5 : mParameters.append( definition );
184 : 5 : }
185 : :
186 : 0 : const QgsProcessingParameterDefinition *QgsClassificationMethod::parameterDefinition( const QString ¶meterName ) const
187 : : {
188 : 0 : for ( const QgsProcessingParameterDefinition *def : mParameters )
189 : : {
190 : 0 : if ( def->name() == parameterName )
191 : 0 : return def;
192 : : }
193 : 0 : QgsMessageLog::logMessage( QStringLiteral( "No parameter definition found for %1 in %2 method." ).arg( parameterName ).arg( name() ) );
194 : 0 : return nullptr;
195 : 0 : }
196 : :
197 : 0 : void QgsClassificationMethod::setParameterValues( const QVariantMap &values )
198 : : {
199 : 0 : mParameterValues = values;
200 : 0 : for ( auto it = mParameterValues.constBegin(); it != mParameterValues.constEnd(); ++it )
201 : : {
202 : 0 : if ( !parameterDefinition( it.key() ) )
203 : : {
204 : 0 : QgsMessageLog::logMessage( name(), QObject::tr( "Parameter %1 does not exist in the method" ).arg( it.key() ) );
205 : 0 : }
206 : 0 : }
207 : 0 : }
208 : :
209 : 0 : QList<QgsClassificationRange> QgsClassificationMethod::classes( const QgsVectorLayer *layer, const QString &expression, int nclasses )
210 : : {
211 : 0 : if ( expression.isEmpty() )
212 : 0 : return QList<QgsClassificationRange>();
213 : :
214 : 0 : if ( nclasses < 1 )
215 : 0 : nclasses = 1;
216 : :
217 : 0 : QList<double> values;
218 : : double minimum;
219 : : double maximum;
220 : :
221 : :
222 : 0 : int fieldIndex = layer->fields().indexFromName( expression );
223 : :
224 : : bool ok;
225 : 0 : if ( valuesRequired() || fieldIndex == -1 )
226 : : {
227 : 0 : values = QgsVectorLayerUtils::getDoubleValues( layer, expression, ok );
228 : 0 : if ( !ok || values.isEmpty() )
229 : 0 : return QList<QgsClassificationRange>();
230 : :
231 : 0 : auto result = std::minmax_element( values.begin(), values.end() );
232 : 0 : minimum = *result.first;
233 : 0 : maximum = *result.second;
234 : 0 : }
235 : : else
236 : : {
237 : 0 : QVariant minVal;
238 : 0 : QVariant maxVal;
239 : 0 : layer->minimumAndMaximumValue( fieldIndex, minVal, maxVal );
240 : 0 : minimum = minVal.toDouble();
241 : 0 : maximum = maxVal.toDouble();
242 : 0 : }
243 : :
244 : : // get the breaks, minimum and maximum might be updated by implementation
245 : 0 : QList<double> breaks = calculateBreaks( minimum, maximum, values, nclasses );
246 : 0 : breaks.insert( 0, minimum );
247 : : // create classes
248 : 0 : return breaksToClasses( breaks );
249 : 0 : }
250 : :
251 : 0 : QList<QgsClassificationRange> QgsClassificationMethod::classes( const QList<double> &values, int nclasses )
252 : : {
253 : 0 : auto result = std::minmax_element( values.begin(), values.end() );
254 : 0 : double minimum = *result.first;
255 : 0 : double maximum = *result.second;
256 : :
257 : : // get the breaks
258 : 0 : QList<double> breaks = calculateBreaks( minimum, maximum, values, nclasses );
259 : 0 : breaks.insert( 0, minimum );
260 : : // create classes
261 : 0 : return breaksToClasses( breaks );
262 : 0 : }
263 : :
264 : 0 : QList<QgsClassificationRange> QgsClassificationMethod::classes( double minimum, double maximum, int nclasses )
265 : : {
266 : 0 : if ( valuesRequired() )
267 : : {
268 : 0 : QgsDebugMsg( QStringLiteral( "The classification method %1 tries to calculate classes without values while they are required." ).arg( name() ) );
269 : 0 : }
270 : :
271 : : // get the breaks
272 : 0 : QList<double> breaks = calculateBreaks( minimum, maximum, QList<double>(), nclasses );
273 : 0 : breaks.insert( 0, minimum );
274 : : // create classes
275 : 0 : return breaksToClasses( breaks );
276 : 0 : }
277 : :
278 : 0 : QList<QgsClassificationRange> QgsClassificationMethod::breaksToClasses( const QList<double> &breaks ) const
279 : : {
280 : 0 : QList<QgsClassificationRange> classes;
281 : :
282 : 0 : for ( int i = 1; i < breaks.count(); i++ )
283 : : {
284 : :
285 : 0 : const double lowerValue = breaks.at( i - 1 );
286 : 0 : const double upperValue = breaks.at( i );
287 : :
288 : 0 : ClassPosition pos = Inner;
289 : 0 : if ( i == 1 )
290 : 0 : pos = LowerBound;
291 : 0 : else if ( i == breaks.count() - 1 )
292 : 0 : pos = UpperBound;
293 : :
294 : 0 : QString label = labelForRange( lowerValue, upperValue, pos );
295 : 0 : classes << QgsClassificationRange( label, lowerValue, upperValue );
296 : 0 : }
297 : :
298 : 0 : return classes;
299 : 0 : }
300 : :
301 : 0 : void QgsClassificationMethod::makeBreaksSymmetric( QList<double> &breaks, double symmetryPoint, bool astride )
302 : : {
303 : : // remove the breaks that are above the existing opposite sign classes
304 : : // to keep colors symmetrically balanced around symmetryPoint
305 : : // if astride is true, remove the symmetryPoint break so that
306 : : // the 2 classes form only one
307 : :
308 : 0 : if ( breaks.count() < 2 )
309 : 0 : return;
310 : :
311 : 0 : std::sort( breaks.begin(), breaks.end() );
312 : : // breaks contain the maximum of the distrib but not the minimum
313 : 0 : double distBelowSymmetricValue = std::fabs( breaks[0] - symmetryPoint );
314 : 0 : double distAboveSymmetricValue = std::fabs( breaks[ breaks.size() - 2 ] - symmetryPoint ) ;
315 : 0 : double absMin = std::min( distAboveSymmetricValue, distBelowSymmetricValue );
316 : :
317 : : // make symmetric
318 : 0 : for ( int i = 0; i <= breaks.size() - 2; ++i )
319 : : {
320 : : // part after "absMin" is for doubles rounding issues
321 : 0 : if ( std::fabs( breaks.at( i ) - symmetryPoint ) >= ( absMin - std::fabs( breaks[0] - breaks[1] ) / 100. ) )
322 : : {
323 : 0 : breaks.removeAt( i );
324 : 0 : --i;
325 : 0 : }
326 : 0 : }
327 : : // remove symmetry point
328 : 0 : if ( astride ) // && breaks.indexOf( symmetryPoint ) != -1) // if symmetryPoint is found
329 : : {
330 : 0 : breaks.removeAt( breaks.indexOf( symmetryPoint ) );
331 : 0 : }
332 : 0 : }
333 : :
334 : 0 : QString QgsClassificationMethod::labelForRange( const QgsRendererRange &range, QgsClassificationMethod::ClassPosition position ) const
335 : : {
336 : 0 : return labelForRange( range.lowerValue(), range.upperValue(), position );
337 : : }
338 : :
339 : 0 : QString QgsClassificationMethod::labelForRange( const double lowerValue, const double upperValue, ClassPosition position ) const
340 : : {
341 : 30 : Q_UNUSED( position )
342 : 30 :
343 : 30 : const QString lowerLabel = valueToLabel( lowerValue );
344 : 30 : const QString upperLabel = valueToLabel( upperValue );
345 : 30 :
346 : 0 : return labelFormat().arg( lowerLabel ).arg( upperLabel );
347 : 0 : }
|