QGIS API Documentation  3.20.0-Odense (decaadbb31)
qgsclassificationmethod.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsclassificationmethod.cpp
3  ---------------------
4  begin : September 2019
5  copyright : (C) 2019 by Denis Rouzaud
6  email : [email protected]
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"
20 #include "qgsvectorlayerutils.h"
21 #include "qgsvectorlayer.h"
23 #include "qgsapplication.h"
25 #include "qgsxmlutils.h"
26 
29 
30 
31 QList<double> QgsClassificationMethod::rangesToBreaks( const QList<QgsClassificationRange> &classes )
32 {
33  QList<double> values;
34  values.reserve( classes.count() );
35  for ( int i = 0 ; i < classes.count(); i++ )
36  values << classes.at( i ).upperBound();
37  return values;
38 }
39 
40 QgsClassificationMethod::QgsClassificationMethod( MethodProperties properties, int codeComplexity )
41  : mFlags( properties )
42  , mCodeComplexity( codeComplexity )
43  , mLabelFormat( QStringLiteral( "%1 - %2" ) )
44 {
45 }
46 
48 {
49  qDeleteAll( mParameters );
50 }
51 
53 {
54  c->setSymmetricMode( mSymmetricEnabled, mSymmetryPoint, mSymmetryAstride );
55  c->setLabelFormat( mLabelFormat );
56  c->setLabelPrecision( mLabelPrecision );
57  c->setLabelTrimTrailingZeroes( mLabelTrimTrailingZeroes );
58  c->setParameterValues( mParameterValues );
59 }
60 
61 QgsClassificationMethod *QgsClassificationMethod::create( const QDomElement &element, const QgsReadWriteContext &context )
62 {
63  const QString methodId = element.attribute( QStringLiteral( "id" ) );
65 
66  // symmetric
67  QDomElement symmetricModeElem = element.firstChildElement( QStringLiteral( "symmetricMode" ) );
68  if ( !symmetricModeElem.isNull() )
69  {
70  bool symmetricEnabled = symmetricModeElem.attribute( QStringLiteral( "enabled" ) ).toInt() == 1;
71  double symmetricPoint = symmetricModeElem.attribute( QStringLiteral( "symmetrypoint" ) ).toDouble();
72  bool astride = symmetricModeElem.attribute( QStringLiteral( "astride" ) ).toInt() == 1;
73  method->setSymmetricMode( symmetricEnabled, symmetricPoint, astride );
74  }
75 
76  // label format
77  QDomElement labelFormatElem = element.firstChildElement( QStringLiteral( "labelformat" ) );
78  if ( !labelFormatElem.isNull() )
79  {
80  QString format = labelFormatElem.attribute( QStringLiteral( "format" ), "%1" + QStringLiteral( " - " ) + "%2" );
81  int precision = labelFormatElem.attribute( QStringLiteral( "labelprecision" ), QStringLiteral( "4" ) ).toInt();
82  bool trimTrailingZeroes = labelFormatElem.attribute( QStringLiteral( "trimtrailingzeroes" ), QStringLiteral( "false" ) ) == QLatin1String( "true" );
83  method->setLabelFormat( format );
84  method->setLabelPrecision( precision );
85  method->setLabelTrimTrailingZeroes( trimTrailingZeroes );
86  }
87 
88  // parameters (processing parameters)
89  QDomElement parametersElem = element.firstChildElement( QStringLiteral( "parameters" ) );
90  const QVariantMap parameterValues = QgsXmlUtils::readVariant( parametersElem.firstChildElement() ).toMap();
92 
93  // Read specific properties from the implementation
94  QDomElement extraElem = element.firstChildElement( QStringLiteral( "extraInformation" ) );
95  if ( !extraElem.isNull() )
96  method->readXml( extraElem, context );
97 
98  return method;
99 }
100 
101 QDomElement QgsClassificationMethod::save( QDomDocument &doc, const QgsReadWriteContext &context ) const
102 {
103  QDomElement methodElem = doc.createElement( QStringLiteral( "classificationMethod" ) );
104 
105  methodElem.setAttribute( QStringLiteral( "id" ), id() );
106 
107  // symmetric
108  QDomElement symmetricModeElem = doc.createElement( QStringLiteral( "symmetricMode" ) );
109  symmetricModeElem.setAttribute( QStringLiteral( "enabled" ), symmetricModeEnabled() ? 1 : 0 );
110  symmetricModeElem.setAttribute( QStringLiteral( "symmetrypoint" ), symmetryPoint() );
111  symmetricModeElem.setAttribute( QStringLiteral( "astride" ), mSymmetryAstride ? 1 : 0 );
112  methodElem.appendChild( symmetricModeElem );
113 
114  // label format
115  QDomElement labelFormatElem = doc.createElement( QStringLiteral( "labelFormat" ) );
116  labelFormatElem.setAttribute( QStringLiteral( "format" ), labelFormat() );
117  labelFormatElem.setAttribute( QStringLiteral( "labelprecision" ), labelPrecision() );
118  labelFormatElem.setAttribute( QStringLiteral( "trimtrailingzeroes" ), labelTrimTrailingZeroes() ? 1 : 0 );
119  methodElem.appendChild( labelFormatElem );
120 
121  // parameters (processing parameters)
122  QDomElement parametersElem = doc.createElement( QStringLiteral( "parameters" ) );
123  parametersElem.appendChild( QgsXmlUtils::writeVariant( mParameterValues, doc ) );
124  methodElem.appendChild( parametersElem );
125 
126  // extra information
127  QDomElement extraElem = doc.createElement( QStringLiteral( "extraInformation" ) );
128  writeXml( extraElem, context );
129  methodElem.appendChild( extraElem );
130 
131  return methodElem;
132 }
133 
134 
135 void QgsClassificationMethod::setSymmetricMode( bool enabled, double symmetryPoint, bool astride )
136 {
137  mSymmetricEnabled = enabled;
138  mSymmetryPoint = symmetryPoint;
139  mSymmetryAstride = astride;
140 }
141 
143 {
144  // Limit the range of decimal places to a reasonable range
146  mLabelPrecision = precision;
147  mLabelNumberScale = 1.0;
148  mLabelNumberSuffix.clear();
149  while ( precision < 0 )
150  {
151  precision++;
152  mLabelNumberScale /= 10.0;
153  mLabelNumberSuffix.append( '0' );
154  }
155 }
156 
157 QString QgsClassificationMethod::formatNumber( double value ) const
158 {
159  static const QRegularExpression RE_TRAILING_ZEROES = QRegularExpression( "[.,]?0*$" );
160  static const QRegularExpression RE_NEGATIVE_ZERO = QRegularExpression( "^\\-0(?:[.,]0*)?$" );
161  if ( mLabelPrecision > 0 )
162  {
163  QString valueStr = QLocale().toString( value, 'f', mLabelPrecision );
164  if ( mLabelTrimTrailingZeroes )
165  valueStr = valueStr.remove( RE_TRAILING_ZEROES );
166  if ( RE_NEGATIVE_ZERO.match( valueStr ).hasMatch() )
167  valueStr = valueStr.mid( 1 );
168  return valueStr;
169  }
170  else
171  {
172  QString valueStr = QLocale().toString( value * mLabelNumberScale, 'f', 0 );
173  if ( valueStr == QLatin1String( "-0" ) )
174  valueStr = '0';
175  if ( valueStr != QLatin1String( "0" ) )
176  valueStr = valueStr + mLabelNumberSuffix;
177  return valueStr;
178  }
179 }
180 
182 {
183  mParameters.append( definition );
184 }
185 
187 {
188  for ( const QgsProcessingParameterDefinition *def : mParameters )
189  {
190  if ( def->name() == parameterName )
191  return def;
192  }
193  QgsMessageLog::logMessage( QStringLiteral( "No parameter definition found for %1 in %2 method." ).arg( parameterName ).arg( name() ) );
194  return nullptr;
195 }
196 
197 void QgsClassificationMethod::setParameterValues( const QVariantMap &values )
198 {
199  mParameterValues = values;
200  for ( auto it = mParameterValues.constBegin(); it != mParameterValues.constEnd(); ++it )
201  {
202  if ( !parameterDefinition( it.key() ) )
203  {
204  QgsMessageLog::logMessage( name(), QObject::tr( "Parameter %1 does not exist in the method" ).arg( it.key() ) );
205  }
206  }
207 }
208 
209 QList<QgsClassificationRange> QgsClassificationMethod::classes( const QgsVectorLayer *layer, const QString &expression, int nclasses )
210 {
211  if ( expression.isEmpty() )
212  return QList<QgsClassificationRange>();
213 
214  if ( nclasses < 1 )
215  nclasses = 1;
216 
217  QList<double> values;
218  double minimum;
219  double maximum;
220 
221 
222  int fieldIndex = layer->fields().indexFromName( expression );
223 
224  bool ok;
225  if ( valuesRequired() || fieldIndex == -1 )
226  {
227  values = QgsVectorLayerUtils::getDoubleValues( layer, expression, ok );
228  if ( !ok || values.isEmpty() )
229  return QList<QgsClassificationRange>();
230 
231  auto result = std::minmax_element( values.begin(), values.end() );
232  minimum = *result.first;
233  maximum = *result.second;
234  }
235  else
236  {
237  QVariant minVal;
238  QVariant maxVal;
239  layer->minimumAndMaximumValue( fieldIndex, minVal, maxVal );
240  minimum = minVal.toDouble();
241  maximum = maxVal.toDouble();
242  }
243 
244  // get the breaks, minimum and maximum might be updated by implementation
245  QList<double> breaks = calculateBreaks( minimum, maximum, values, nclasses );
246  breaks.insert( 0, minimum );
247  // create classes
248  return breaksToClasses( breaks );
249 }
250 
251 QList<QgsClassificationRange> QgsClassificationMethod::classes( const QList<double> &values, int nclasses )
252 {
253  auto result = std::minmax_element( values.begin(), values.end() );
254  double minimum = *result.first;
255  double maximum = *result.second;
256 
257  // get the breaks
258  QList<double> breaks = calculateBreaks( minimum, maximum, values, nclasses );
259  breaks.insert( 0, minimum );
260  // create classes
261  return breaksToClasses( breaks );
262 }
263 
264 QList<QgsClassificationRange> QgsClassificationMethod::classes( double minimum, double maximum, int nclasses )
265 {
266  if ( valuesRequired() )
267  {
268  QgsDebugMsg( QStringLiteral( "The classification method %1 tries to calculate classes without values while they are required." ).arg( name() ) );
269  }
270 
271  // get the breaks
272  QList<double> breaks = calculateBreaks( minimum, maximum, QList<double>(), nclasses );
273  breaks.insert( 0, minimum );
274  // create classes
275  return breaksToClasses( breaks );
276 }
277 
278 QList<QgsClassificationRange> QgsClassificationMethod::breaksToClasses( const QList<double> &breaks ) const
279 {
280  QList<QgsClassificationRange> classes;
281 
282  for ( int i = 1; i < breaks.count(); i++ )
283  {
284 
285  const double lowerValue = breaks.at( i - 1 );
286  const double upperValue = breaks.at( i );
287 
288  ClassPosition pos = Inner;
289  if ( i == 1 )
290  pos = LowerBound;
291  else if ( i == breaks.count() - 1 )
292  pos = UpperBound;
293 
294  QString label = labelForRange( lowerValue, upperValue, pos );
295  classes << QgsClassificationRange( label, lowerValue, upperValue );
296  }
297 
298  return classes;
299 }
300 
301 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  if ( breaks.count() < 2 )
309  return;
310 
311  std::sort( breaks.begin(), breaks.end() );
312  // breaks contain the maximum of the distrib but not the minimum
313  double distBelowSymmetricValue = std::fabs( breaks[0] - symmetryPoint );
314  double distAboveSymmetricValue = std::fabs( breaks[ breaks.size() - 2 ] - symmetryPoint ) ;
315  double absMin = std::min( distAboveSymmetricValue, distBelowSymmetricValue );
316 
317  // make symmetric
318  for ( int i = 0; i <= breaks.size() - 2; ++i )
319  {
320  // part after "absMin" is for doubles rounding issues
321  if ( std::fabs( breaks.at( i ) - symmetryPoint ) >= ( absMin - std::fabs( breaks[0] - breaks[1] ) / 100. ) )
322  {
323  breaks.removeAt( i );
324  --i;
325  }
326  }
327  // remove symmetry point
328  if ( astride ) // && breaks.indexOf( symmetryPoint ) != -1) // if symmetryPoint is found
329  {
330  breaks.removeAt( breaks.indexOf( symmetryPoint ) );
331  }
332 }
333 
335 {
336  return labelForRange( range.lowerValue(), range.upperValue(), position );
337 }
338 
339 QString QgsClassificationMethod::labelForRange( const double lowerValue, const double upperValue, ClassPosition position ) const
340 {
341  Q_UNUSED( position )
342 
343  const QString lowerLabel = valueToLabel( lowerValue );
344  const QString upperLabel = valueToLabel( upperValue );
345 
346  return labelFormat().arg( lowerLabel ).arg( upperLabel );
347 }
static QgsClassificationMethodRegistry * classificationMethodRegistry()
Returns the application's classification methods registry, used in graduated renderer.
QgsClassificationMethod * method(const QString &id)
Returns a new instance of the method for the given id.
QgsClassificationMethod is an abstract class for implementations of classification methods.
double symmetryPoint() const
Returns the symmetry point for symmetric mode.
bool symmetricModeEnabled() const
Returns if the symmetric mode is enabled.
static void makeBreaksSymmetric(QList< double > &breaks, double symmetryPoint, bool astride)
Remove the breaks that are above the existing opposite sign classes to keep colors symmetrically bala...
QList< QgsClassificationRange > classes(const QgsVectorLayer *layer, const QString &expression, int nclasses)
This will calculate the classes for a given layer to define the classes.
int labelPrecision() const
Returns the precision for the formatting of the labels.
void setLabelTrimTrailingZeroes(bool trimTrailingZeroes)
Defines if the trailing 0 are trimmed in the label.
QVariantMap parameterValues() const
Returns the values of the processing parameters.
void setSymmetricMode(bool enabled, double symmetryPoint=0, bool symmetryAstride=false)
Defines if the symmetric mode is enables and configures its parameters.
ClassPosition
Defines the class position.
@ LowerBound
The class is at the lower bound.
@ UpperBound
The class is at the upper bound.
@ Inner
The class is not at a bound.
void addParameter(QgsProcessingParameterDefinition *definition)
Add a parameter to the method.
virtual void readXml(const QDomElement &element, const QgsReadWriteContext &context)
Reads extra information to apply it to the method.
static QList< double > rangesToBreaks(const QList< QgsClassificationRange > &classes)
Transforms a list of classes to a list of breaks.
virtual void writeXml(QDomElement &element, const QgsReadWriteContext &context) const
Writes extra information about the method.
QString labelFormat() const
Returns the format of the label for the classes.
virtual bool valuesRequired() const
Returns if the method requires values to calculate the classes If not, bounds are sufficient.
void setParameterValues(const QVariantMap &values)
Defines the values of the additional parameters.
virtual QString name() const =0
The readable and translate name of the method.
QgsClassificationMethod(MethodProperties properties=NoFlag, int codeComplexity=1)
Creates a classification method.
void setLabelFormat(const QString &format)
Defines the format of the labels for the classes, using %1 and %2 for the bounds.
static QgsClassificationMethod * create(const QDomElement &element, const QgsReadWriteContext &context)
Reads the DOM element and return a new classification method from it.
const QgsProcessingParameterDefinition * parameterDefinition(const QString &parameterName) const
Returns the parameter from its name.
QString formatNumber(double value) const
Format the number according to label properties.
bool labelTrimTrailingZeroes() const
Returns if the trailing 0 are trimmed in the label.
void setLabelPrecision(int labelPrecision)
Defines the precision for the formatting of the labels.
void copyBase(QgsClassificationMethod *c) const
Copy the parameters (shall be used in clone implementation)
virtual QString labelForRange(double lowerValue, double upperValue, ClassPosition position=Inner) const
Returns the label for a range.
QDomElement save(QDomDocument &doc, const QgsReadWriteContext &context) const
Saves the method to a DOM element and return it.
QgsClassificationRange contains the information about a classification range.
int indexFromName(const QString &fieldName) const
Gets the field index from the field name.
Definition: qgsfields.cpp:202
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
Base class for the definition of processing parameters.
The class is used as a container of context for various read/write operations on other objects.
double upperValue() const
double lowerValue() const
static QList< double > getDoubleValues(const QgsVectorLayer *layer, const QString &fieldOrExpression, bool &ok, bool selectedOnly=false, int *nullCount=nullptr, QgsFeedback *feedback=nullptr)
Fetches all double values from a specified field name or expression.
Represents a vector layer which manages a vector based data sets.
QgsFields fields() const FINAL
Returns the list of fields of this layer.
void minimumAndMaximumValue(int index, QVariant &minimum, QVariant &maximum) const
Calculates both the minimum and maximum value for an attribute column.
static QDomElement writeVariant(const QVariant &value, QDomDocument &doc)
Write a QVariant to a QDomElement.
static QVariant readVariant(const QDomElement &element)
Read a QVariant from a QDomElement.
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
int precision