diff --git a/src/core/symbology/qgsgraduatedsymbolrenderer.cpp b/src/core/symbology/qgsgraduatedsymbolrenderer.cpp index 8d6cbe48b4e..b5ba4034e5c 100644 --- a/src/core/symbology/qgsgraduatedsymbolrenderer.cpp +++ b/src/core/symbology/qgsgraduatedsymbolrenderer.cpp @@ -13,6 +13,10 @@ * * ***************************************************************************/ +#include +#include + +#include #include "qgsgraduatedsymbolrenderer.h" @@ -35,12 +39,11 @@ #include "qgsvectorlayerutils.h" #include "qgsexpressioncontextutils.h" #include "qgsstyleentityvisitor.h" - -#include -#include -#include // for legend -#include // for jenks classification -#include +#include "qgsclassificationmethod.h" +#include "qgsclassificationequalinterval.h" +#include "qgsapplication.h" +#include "qgsclassificationmethodregistry.h" +#include "qgsclassificationcustom.h" QgsGraduatedSymbolRenderer::QgsGraduatedSymbolRenderer( const QString &attrName, const QgsRangeList &ranges ) @@ -57,6 +60,8 @@ QgsGraduatedSymbolRenderer::QgsGraduatedSymbolRenderer( const QString &attrName, { mRanges << range; } + + mClassificationMethod.reset( new QgsClassificationCustom() ); } QgsGraduatedSymbolRenderer::~QgsGraduatedSymbolRenderer() @@ -232,9 +237,17 @@ bool QgsGraduatedSymbolRenderer::updateRangeUpperValue( int rangeIndex, double v if ( rangeIndex < 0 || rangeIndex >= mRanges.size() ) return false; QgsRendererRange &range = mRanges[rangeIndex]; - bool isDefaultLabel = range.label() == mLabelFormat.labelForRange( range ); + QgsClassificationMethod::ClassPosition pos = QgsClassificationMethod::Inner; + if ( rangeIndex == 0 ) + pos = QgsClassificationMethod::LowerBound; + else if ( rangeIndex == mRanges.count() ) + pos = QgsClassificationMethod::UpperBound; + + bool isDefaultLabel = mClassificationMethod->labelForRange( range, pos ) == range.label(); range.setUpperValue( value ); - if ( isDefaultLabel ) range.setLabel( mLabelFormat.labelForRange( range ) ); + if ( isDefaultLabel ) + range.setLabel( mClassificationMethod->labelForRange( range, pos ) ); + return true; } @@ -242,10 +255,19 @@ bool QgsGraduatedSymbolRenderer::updateRangeLowerValue( int rangeIndex, double v { if ( rangeIndex < 0 || rangeIndex >= mRanges.size() ) return false; + QgsRendererRange &range = mRanges[rangeIndex]; - bool isDefaultLabel = range.label() == mLabelFormat.labelForRange( range ); + QgsClassificationMethod::ClassPosition pos = QgsClassificationMethod::Inner; + if ( rangeIndex == 0 ) + pos = QgsClassificationMethod::LowerBound; + else if ( rangeIndex == mRanges.count() ) + pos = QgsClassificationMethod::UpperBound; + + bool isDefaultLabel = mClassificationMethod->labelForRange( range, pos ) == range.label(); range.setLowerValue( value ); - if ( isDefaultLabel ) range.setLabel( mLabelFormat.labelForRange( range ) ); + if ( isDefaultLabel ) + range.setLabel( mClassificationMethod->labelForRange( range, pos ) ); + return true; } @@ -268,11 +290,8 @@ QString QgsGraduatedSymbolRenderer::dump() const QgsGraduatedSymbolRenderer *QgsGraduatedSymbolRenderer::clone() const { QgsGraduatedSymbolRenderer *r = new QgsGraduatedSymbolRenderer( mAttrName, mRanges ); - r->setMode( mMode ); - r->setUseSymmetricMode( mUseSymmetricMode ); - r->setSymmetryPoint( mSymmetryPoint ); - r->setListForCboPrettyBreaks( mListForCboPrettyBreaks ); - r->setAstride( mAstride ); + + r->setClassificationMethod( mClassificationMethod->clone() ); if ( mSourceSymbol ) r->setSourceSymbol( mSourceSymbol->clone() ); @@ -282,7 +301,6 @@ QgsGraduatedSymbolRenderer *QgsGraduatedSymbolRenderer::clone() const } r->setUsingSymbolLevels( usingSymbolLevels() ); r->setDataDefinedSizeLegend( mDataDefinedSizeLegend ? new QgsDataDefinedSizeLegend( *mDataDefinedSizeLegend ) : nullptr ); - r->setLabelFormat( labelFormat() ); r->setGraduatedMethod( graduatedMethod() ); copyRendererData( r ); return r; @@ -337,317 +355,16 @@ bool QgsGraduatedSymbolRenderer::accept( QgsStyleEntityVisitorInterface *visitor void QgsGraduatedSymbolRenderer::makeBreaksSymmetric( QList &breaks, double symmetryPoint, bool astride ) { - // remove the breaks that are above the existing opposite sign classes - // to keep colors symmetrically balanced around symmetryPoint - // if astride is true, remove the symmetryPoint break so that - // the 2 classes form only one - - if ( breaks.size() > 1 ) //to avoid crash when only 1 class - { - std::sort( breaks.begin(), breaks.end() ); - // breaks contain the maximum of the distrib but not the minimum - double distBelowSymmetricValue = std::fabs( breaks[0] - symmetryPoint ); - double distAboveSymmetricValue = std::fabs( breaks[ breaks.size() - 2 ] - symmetryPoint ) ; - double absMin = std::min( distAboveSymmetricValue, distBelowSymmetricValue ); - - // make symmetric - for ( int i = 0; i <= breaks.size() - 2; ++i ) - { - // part after "absMin" is for doubles rounding issues - if ( std::fabs( breaks.at( i ) - symmetryPoint ) >= ( absMin - std::fabs( breaks[0] - breaks[1] ) / 100. ) ) - { - breaks.removeAt( i ); - --i; - } - } - // remove symmetry point - if ( astride ) // && breaks.indexOf( symmetryPoint ) != -1) // if symmetryPoint is found - { - breaks.removeAt( breaks.indexOf( symmetryPoint ) ); - } - } + return QgsClassificationMethod::makeBreaksSymmetric( breaks, symmetryPoint, astride ); } QList QgsGraduatedSymbolRenderer::calcEqualIntervalBreaks( double minimum, double maximum, int classes, bool useSymmetricMode, double symmetryPoint, bool astride ) { - // Equal interval algorithm - // Returns breaks based on dividing the range ('minimum' to 'maximum') into 'classes' parts. - QList breaks; - if ( !useSymmetricMode ) // nomal mode - { - double step = ( maximum - minimum ) / classes; - - double value = minimum; - breaks.reserve( classes ); - for ( int i = 0; i < classes; i++ ) - { - value += step; - breaks.append( value ); - } - // floating point arithmetics is not precise: - // set the last break to be exactly maximum so we do not miss it - breaks[classes - 1] = maximum; - } - else if ( useSymmetricMode ) // symmetric mode - { - double distBelowSymmetricValue = std::abs( minimum - symmetryPoint ); - double distAboveSymmetricValue = std::abs( maximum - symmetryPoint ) ; - - if ( astride ) - { - if ( classes % 2 == 0 ) // we want odd number of classes - ++classes; - } - else - { - if ( classes % 2 == 1 ) // we want even number of classes - ++classes; - } - double step = 2 * std::min( distBelowSymmetricValue, distAboveSymmetricValue ) / classes; - - breaks.reserve( classes ); - double value = ( distBelowSymmetricValue < distAboveSymmetricValue ) ? minimum : maximum - classes * step; - - for ( int i = 0; i < classes; i++ ) - { - value += step; - breaks.append( value ); - } - breaks[classes - 1] = maximum; - } - return breaks; -} - -static QList _calcQuantileBreaks( QList values, int classes ) -{ - // q-th quantile of a data set: - // value where q fraction of data is below and (1-q) fraction is above this value - // Xq = (1 - r) * X_NI1 + r * X_NI2 - // NI1 = (int) (q * (n+1)) - // NI2 = NI1 + 1 - // r = q * (n+1) - (int) (q * (n+1)) - // (indices of X: 1...n) - - // sort the values first - std::sort( values.begin(), values.end() ); - - QList breaks; - - // If there are no values to process: bail out - if ( values.isEmpty() ) - return breaks; - - int n = values.count(); - double Xq = n > 0 ? values[0] : 0.0; - - breaks.reserve( classes ); - for ( int i = 1; i < classes; i++ ) - { - if ( n > 1 ) - { - double q = i / static_cast< double >( classes ); - double a = q * ( n - 1 ); - int aa = static_cast< int >( a ); - - double r = a - aa; - Xq = ( 1 - r ) * values[aa] + r * values[aa + 1]; - } - breaks.append( Xq ); - } - - breaks.append( values[ n - 1 ] ); - - return breaks; -} - -static QList _calcStdDevBreaks( QList values, int classes, QList &labels, bool useSymmetricMode, double symmetryPoint, bool astride ) -{ - - // C++ implementation of the standard deviation class interval algorithm - // as implemented in the 'classInt' package available for the R statistical - // prgramming language. - - // Returns breaks based on 'prettyBreaks' of the centred and scaled - // values of 'values', and may have a number of classes different from 'classes'. - - // If there are no values to process: bail out - if ( values.isEmpty() ) - return QList(); - - double mean = 0.0; - double stdDev = 0.0; - int n = values.count(); - double minimum = values[0]; - double maximum = values[0]; - - for ( int i = 0; i < n; i++ ) - { - mean += values[i]; - minimum = std::min( values[i], minimum ); // could use precomputed max and min - maximum = std::max( values[i], maximum ); // but have to go through entire list anyway - } - mean = mean / static_cast< double >( n ); - - double sd = 0.0; - for ( int i = 0; i < n; i++ ) - { - sd = values[i] - mean; - stdDev += sd * sd; - } - stdDev = std::sqrt( stdDev / n ); - - if ( !useSymmetricMode ) - symmetryPoint = mean; // otherwise symmetryPoint = symmetryPoint - - QList breaks = QgsSymbolLayerUtils::prettyBreaks( ( minimum - symmetryPoint ) / stdDev, ( maximum - symmetryPoint ) / stdDev, classes ); - QgsGraduatedSymbolRenderer::makeBreaksSymmetric( breaks, 0.0, astride ); //0.0 because breaks where computed on a centered distribution - - for ( int i = 0; i < breaks.count(); i++ ) //unNormalize breaks and put labels - { - labels.append( breaks[i] ); - breaks[i] = ( breaks[i] * stdDev ) + symmetryPoint; - } - return breaks; -} // _calcStdDevBreaks - -static QList _calcJenksBreaks( QList values, int classes, - double minimum, double maximum, - int maximumSize = 3000 ) -{ - // Jenks Optimal (Natural Breaks) algorithm - // Based on the Jenks algorithm from the 'classInt' package available for - // the R statistical prgramming language, and from Python code from here: - // http://danieljlewis.org/2010/06/07/jenks-natural-breaks-algorithm-in-python/ - // and is based on a JAVA and Fortran code available here: - // https://stat.ethz.ch/pipermail/r-sig-geo/2006-March/000811.html - - // Returns class breaks such that classes are internally homogeneous while - // assuring heterogeneity among classes. - - if ( values.isEmpty() ) - return QList(); - - if ( classes <= 1 ) - { - return QList() << maximum; - } - - if ( classes >= values.size() ) - { - return values; - } - - QVector sample; - - // if we have lots of values, we need to take a random sample - if ( values.size() > maximumSize ) - { - // for now, sample at least maximumSize values or a 10% sample, whichever - // is larger. This will produce a more representative sample for very large - // layers, but could end up being computationally intensive... - - sample.resize( std::max( maximumSize, values.size() / 10 ) ); - - QgsDebugMsg( QStringLiteral( "natural breaks (jenks) sample size: %1" ).arg( sample.size() ) ); - QgsDebugMsg( QStringLiteral( "values:%1" ).arg( values.size() ) ); - - sample[ 0 ] = minimum; - sample[ 1 ] = maximum; - for ( int i = 2; i < sample.size(); i++ ) - { - // pick a random integer from 0 to n - double r = qrand(); - int j = std::floor( r / RAND_MAX * ( values.size() - 1 ) ); - sample[ i ] = values[ j ]; - } - } - else - { - sample = values.toVector(); - } - - int n = sample.size(); - - // sort the sample values - std::sort( sample.begin(), sample.end() ); - - QVector< QVector > matrixOne( n + 1 ); - QVector< QVector > matrixTwo( n + 1 ); - - for ( int i = 0; i <= n; i++ ) - { - matrixOne[i].resize( classes + 1 ); - matrixTwo[i].resize( classes + 1 ); - } - - for ( int i = 1; i <= classes; i++ ) - { - matrixOne[0][i] = 1; - matrixOne[1][i] = 1; - matrixTwo[0][i] = 0.0; - for ( int j = 2; j <= n; j++ ) - { - matrixTwo[j][i] = std::numeric_limits::max(); - } - } - - for ( int l = 2; l <= n; l++ ) - { - double s1 = 0.0; - double s2 = 0.0; - int w = 0; - - double v = 0.0; - - for ( int m = 1; m <= l; m++ ) - { - int i3 = l - m + 1; - - double val = sample[ i3 - 1 ]; - - s2 += val * val; - s1 += val; - w++; - - v = s2 - ( s1 * s1 ) / static_cast< double >( w ); - int i4 = i3 - 1; - if ( i4 != 0 ) - { - for ( int j = 2; j <= classes; j++ ) - { - if ( matrixTwo[l][j] >= v + matrixTwo[i4][j - 1] ) - { - matrixOne[l][j] = i4; - matrixTwo[l][j] = v + matrixTwo[i4][j - 1]; - } - } - } - } - matrixOne[l][1] = 1; - matrixTwo[l][1] = v; - } - - QVector breaks( classes ); - breaks[classes - 1] = sample[n - 1]; - - for ( int j = classes, k = n; j >= 2; j-- ) - { - int id = matrixOne[k][j] - 1; - breaks[j - 2] = sample[id]; - k = matrixOne[k][j] - 1; - } - - return breaks.toList(); -} //_calcJenksBreaks - -static QStringList _breaksAsStrings( const QList &breaks ) // get QStringList from QList without maxi break (min is not in) -{ - QStringList breaksAsStrings; - for ( int i = 0; i < breaks.count() - 1; i++ ) - { - breaksAsStrings << QString::number( breaks.at( i ), 'f', 2 ); - } - return breaksAsStrings; + QgsClassificationEqualInterval *method = new QgsClassificationEqualInterval(); + method->setSymmetricMode( useSymmetricMode, symmetryPoint, astride ); + QList _classes = method->classes( minimum, maximum, classes ); + delete method; + return QgsClassificationMethod::listToValues( _classes ); } QgsGraduatedSymbolRenderer *QgsGraduatedSymbolRenderer::createRenderer( @@ -664,17 +381,26 @@ QgsGraduatedSymbolRenderer *QgsGraduatedSymbolRenderer::createRenderer( bool astride ) { + Q_UNUSED( listForCboPrettyBreaks ) + QgsRangeList ranges; QgsGraduatedSymbolRenderer *r = new QgsGraduatedSymbolRenderer( attrName, ranges ); r->setSourceSymbol( symbol->clone() ); r->setSourceColorRamp( ramp->clone() ); - r->setMode( mode ); - r->setUseSymmetricMode( useSymmetricMode ); - r->setSymmetryPoint( symmetryPoint ); - r->setListForCboPrettyBreaks( listForCboPrettyBreaks ); - r->setAstride( astride ); - r->setLabelFormat( labelFormat ); - r->updateClasses( vlayer, mode, classes, useSymmetricMode, symmetryPoint, astride ); + + QString methodId = methodIdFromMode( mode ); + QgsClassificationMethod *method = QgsApplication::classificationMethodRegistry()->method( methodId ); + + if ( method ) + { + method->setSymmetricMode( useSymmetricMode, symmetryPoint, astride ); + method->setLabelFormat( labelFormat.format() ); + method->setLabelTrimTrailingZeroes( labelFormat.trimTrailingZeroes() ); + method->setLabelPrecision( labelFormat.precision() ); + } + r->setClassificationMethod( method ); + + r->updateClasses( vlayer, classes ); return r; } @@ -683,122 +409,30 @@ void QgsGraduatedSymbolRenderer::updateClasses( QgsVectorLayer *vlayer, Mode mod { if ( mAttrName.isEmpty() ) return; - setMode( mode ); - setSymmetryPoint( symmetryPoint ); - setUseSymmetricMode( useSymmetricMode ); - setAstride( astride ); - // Custom classes are not recalculated - if ( mode == Custom ) + QString methodId = methodIdFromMode( mode ); + QgsClassificationMethod *method = QgsApplication::classificationMethodRegistry()->method( methodId ); + if ( method ) + { + method->setSymmetricMode( useSymmetricMode, symmetryPoint, astride ); + } + + updateClasses( vlayer, nclasses ); +} + +void QgsGraduatedSymbolRenderer::updateClasses( const QgsVectorLayer *vl, int numberOfClasses ) +{ + if ( mClassificationMethod->id() == QgsClassificationCustom::METHOD_ID ) return; - if ( nclasses < 1 ) - nclasses = 1; + QList classes = mClassificationMethod->classes( vl, mAttrName, numberOfClasses ); - QList values; - bool valuesLoaded = false; - double minimum; - double maximum; - - int attrNum = vlayer->fields().lookupField( mAttrName ); - - bool ok; - if ( attrNum == -1 ) - { - values = QgsVectorLayerUtils::getDoubleValues( vlayer, mAttrName, ok ); - if ( !ok || values.isEmpty() ) - return; - - auto result = std::minmax_element( values.begin(), values.end() ); - minimum = *result.first; - maximum = *result.second; - valuesLoaded = true; - } - else - { - minimum = vlayer->minimumValue( attrNum ).toDouble(); - maximum = vlayer->maximumValue( attrNum ).toDouble(); - } - - QgsDebugMsg( QStringLiteral( "min %1 // max %2" ).arg( minimum ).arg( maximum ) ); - QList breaks; - QList labels; - - switch ( mode ) - { - case EqualInterval: - { - breaks = QgsGraduatedSymbolRenderer::calcEqualIntervalBreaks( minimum, maximum, nclasses, mUseSymmetricMode, symmetryPoint, astride ); - break; - } - - case Pretty: - { - breaks = QgsSymbolLayerUtils::prettyBreaks( minimum, maximum, nclasses ); - setListForCboPrettyBreaks( _breaksAsStrings( breaks ) ); - - if ( useSymmetricMode ) - QgsGraduatedSymbolRenderer::makeBreaksSymmetric( breaks, symmetryPoint, astride ); - break; - } - - case Quantile: - case Jenks: - case StdDev: - { - // get values from layer - if ( !valuesLoaded ) - { - values = QgsVectorLayerUtils::getDoubleValues( vlayer, mAttrName, ok ); - } - // calculate the breaks - if ( mode == Quantile ) - breaks = _calcQuantileBreaks( values, nclasses ); - else if ( mode == Jenks ) - breaks = _calcJenksBreaks( values, nclasses, minimum, maximum ); - else if ( mode == StdDev ) - breaks = _calcStdDevBreaks( values, nclasses, labels, mUseSymmetricMode, symmetryPoint, astride ); - break; - } - - case Custom: - Q_ASSERT( false ); - break; - } - - double lower, upper = minimum; - QString label; deleteAllClasses(); - // "breaks" list contains all values at class breaks plus maximum as last break - int i = 0; - for ( QList::iterator it = breaks.begin(); it != breaks.end(); ++it, ++i ) + for ( QList::iterator it = classes.begin(); it != classes.end(); ++it ) { - lower = upper; // upper border from last interval - upper = *it; - - // Label - either StdDev label or default label for a range - if ( mode == StdDev ) - { - if ( i == 0 ) - { - label = "< " + QString::number( labels[i], 'f', 2 ) + " Std Dev"; - } - else if ( i == labels.count() - 1 ) - { - label = ">= " + QString::number( labels[i - 1], 'f', 2 ) + " Std Dev"; - } - else - { - label = QString::number( labels[i - 1], 'f', 2 ) + " Std Dev" + " - " + QString::number( labels[i], 'f', 2 ) + " Std Dev"; - } - } - else - { - label = mLabelFormat.labelForRange( lower, upper ); - } - QgsSymbol *newSymbol = mSourceSymbol ? mSourceSymbol->clone() : QgsSymbol::defaultSymbol( vlayer->geometryType() ); - addClass( QgsRendererRange( lower, upper, newSymbol, label ) ); + QgsSymbol *newSymbol = mSourceSymbol ? mSourceSymbol->clone() : QgsSymbol::defaultSymbol( vl->geometryType() ); + addClass( QgsRendererRange( *it, newSymbol ) ); } updateColorRamp( nullptr ); } @@ -873,37 +507,62 @@ QgsFeatureRenderer *QgsGraduatedSymbolRenderer::create( QDomElement &element, co } // try to load mode - QDomElement modeElem = element.firstChildElement( QStringLiteral( "mode" ) ); + + QDomElement modeElem = element.firstChildElement( QStringLiteral( "mode" ) ); // old format, backward compatibility + QDomElement methodElem = element.firstChildElement( QStringLiteral( "classificationMethod" ) ); + QgsClassificationMethod *method = nullptr; + + // TODO QGIS 4 Remove + // backward compatibility for QGIS project < 3.10 if ( !modeElem.isNull() ) { QString modeString = modeElem.attribute( QStringLiteral( "name" ) ); + QString methodId; if ( modeString == QLatin1String( "equal" ) ) - r->setMode( EqualInterval ); + methodId = QLatin1String( "EqualInterval" ); else if ( modeString == QLatin1String( "quantile" ) ) - r->setMode( Quantile ); + methodId = QLatin1String( "Quantile" ); else if ( modeString == QLatin1String( "jenks" ) ) - r->setMode( Jenks ); + methodId = QLatin1String( "Jenks" ); else if ( modeString == QLatin1String( "stddev" ) ) - r->setMode( StdDev ); + methodId = QLatin1String( "StdDev" ); else if ( modeString == QLatin1String( "pretty" ) ) - r->setMode( Pretty ); - } + methodId = QLatin1String( "Pretty" ); - // symmetric mode - QDomElement symmetricModeElem = element.firstChildElement( QStringLiteral( "symmetricMode" ) ); - if ( !symmetricModeElem.isNull() ) + method = QgsApplication::classificationMethodRegistry()->method( methodId ); + + // symmetric mode + QDomElement symmetricModeElem = element.firstChildElement( QStringLiteral( "symmetricMode" ) ); + if ( !symmetricModeElem.isNull() ) + { + // symmetry + QString symmetricEnabled = symmetricModeElem.attribute( QStringLiteral( "enabled" ) ); + QString symmetricPointString = symmetricModeElem.attribute( QStringLiteral( "symmetryPoint" ) ); + QString astrideEnabled = symmetricModeElem.attribute( QStringLiteral( "astride" ) ); + method->setSymmetricMode( symmetricEnabled == QLatin1String( "true" ), symmetricPointString.toDouble(), astrideEnabled == QLatin1String( "true" ) ); + } + QDomElement labelFormatElem = element.firstChildElement( QStringLiteral( "labelformat" ) ); + if ( !labelFormatElem.isNull() ) + { + // label format + QString format = labelFormatElem.attribute( QStringLiteral( "format" ), "%1" + QStringLiteral( " - " ) + "%2" ); + int precision = labelFormatElem.attribute( QStringLiteral( "decimalplaces" ), QStringLiteral( "4" ) ).toInt(); + bool trimTrailingZeroes = labelFormatElem.attribute( QStringLiteral( "trimtrailingzeroes" ), QStringLiteral( "false" ) ) == QLatin1String( "true" ); + method->setLabelFormat( format ); + method->setLabelPrecision( precision ); + method->setLabelTrimTrailingZeroes( trimTrailingZeroes ); + } + // End of backward compatibility + } + else { - QString symmetricEnabled = symmetricModeElem.attribute( QStringLiteral( "enabled" ) ); - symmetricEnabled == QLatin1String( "true" ) ? r->setUseSymmetricMode( true ) : r->setUseSymmetricMode( false ); - - QString symmetricPointString = symmetricModeElem.attribute( QStringLiteral( "symmetryPoint" ) ); - r->setSymmetryPoint( symmetricPointString.toDouble() ); - QString breaksForPretty = symmetricModeElem.attribute( QStringLiteral( "valueForCboPrettyBreaks" ) ); - r->setListForCboPrettyBreaks( breaksForPretty.split( '/' ) ); - - QString astrideEnabled = symmetricModeElem.attribute( QStringLiteral( "astride" ) ); - astrideEnabled == QLatin1String( "true" ) ? r->setAstride( true ) : r->setAstride( false ); + // QGIS project 3.10+ + method = QgsClassificationMethod::create( methodElem, context ); } + + // apply the method + r->setClassificationMethod( method ); + QDomElement rotationElem = element.firstChildElement( QStringLiteral( "rotation" ) ); if ( !rotationElem.isNull() && !rotationElem.attribute( QStringLiteral( "field" ) ).isEmpty() ) { @@ -933,20 +592,12 @@ QgsFeatureRenderer *QgsGraduatedSymbolRenderer::create( QDomElement &element, co } } - QDomElement labelFormatElem = element.firstChildElement( QStringLiteral( "labelformat" ) ); - if ( ! labelFormatElem.isNull() ) - { - QgsRendererRangeLabelFormat labelFormat; - labelFormat.setFromDomElement( labelFormatElem ); - r->setLabelFormat( labelFormat ); - } - QDomElement ddsLegendSizeElem = element.firstChildElement( QStringLiteral( "data-defined-size-legend" ) ); if ( !ddsLegendSizeElem.isNull() ) { r->mDataDefinedSizeLegend.reset( QgsDataDefinedSizeLegend::readXml( ddsLegendSizeElem, context ) ); } - // TODO: symbol levels +// TODO: symbol levels return r; } @@ -1002,54 +653,9 @@ QDomElement QgsGraduatedSymbolRenderer::save( QDomDocument &doc, const QgsReadWr rendererElem.appendChild( colorRampElem ); } - // save mode - QString modeString; - switch ( mMode ) - { - case EqualInterval: - modeString = QStringLiteral( "equal" ); - break; - case Quantile: - modeString = QStringLiteral( "quantile" ); - break; - case Jenks: - modeString = QStringLiteral( "jenks" ); - break; - case StdDev: - modeString = QStringLiteral( "stddev" ); - break; - case Pretty: - modeString = QStringLiteral( "pretty" ); - break; - case Custom: - break; - } - if ( !modeString.isEmpty() ) - { - QDomElement modeElem = doc.createElement( QStringLiteral( "mode" ) ); - modeElem.setAttribute( QStringLiteral( "name" ), modeString ); - rendererElem.appendChild( modeElem ); - } - - // symmetry - QDomElement symmetricModeElem = doc.createElement( QStringLiteral( "symmetricMode" ) ); - symmetricModeElem.setAttribute( QStringLiteral( "enabled" ), mUseSymmetricMode ? QStringLiteral( "true" ) : QStringLiteral( "false" ) ); - symmetricModeElem.setAttribute( QStringLiteral( "symmetryPoint" ), mSymmetryPoint ); - symmetricModeElem.setAttribute( QStringLiteral( "astride" ), mAstride ? QStringLiteral( "true" ) : QStringLiteral( "false" ) ); - if ( Pretty == mMode ) - { - QString breaks; - for ( int i = 0; i < mListForCboPrettyBreaks.size() - 1; i++ ) // -1 to write 1/2/3 instead of 1/2/3/ - { - breaks.append( mListForCboPrettyBreaks.at( i ) ); - breaks.append( '/' ); - } - if ( mListForCboPrettyBreaks.size() > 0 ) //make sure we can go at size-1 - breaks.append( mListForCboPrettyBreaks.at( mListForCboPrettyBreaks.size() - 1 ) ); //add the last break - symmetricModeElem.setAttribute( QStringLiteral( "valueForCboPrettyBreaks" ), breaks ); - } - - rendererElem.appendChild( symmetricModeElem ); + // save classification method + QDomElement classificationMethodElem = mClassificationMethod->save( doc, context ); + rendererElem.appendChild( classificationMethodElem ); QDomElement rotationElem = doc.createElement( QStringLiteral( "rotation" ) ); rendererElem.appendChild( rotationElem ); @@ -1057,10 +663,6 @@ QDomElement QgsGraduatedSymbolRenderer::save( QDomDocument &doc, const QgsReadWr QDomElement sizeScaleElem = doc.createElement( QStringLiteral( "sizescale" ) ); rendererElem.appendChild( sizeScaleElem ); - QDomElement labelFormatElem = doc.createElement( QStringLiteral( "labelformat" ) ); - mLabelFormat.saveToDomElement( labelFormatElem ); - rendererElem.appendChild( labelFormatElem ); - if ( mPaintEffect && !QgsPaintEffectRegistry::isDefaultStack( mPaintEffect ) ) mPaintEffect->saveProperties( doc, rendererElem ); @@ -1094,6 +696,27 @@ QgsLegendSymbolList QgsGraduatedSymbolRenderer::baseLegendSymbolItems() const return lst; } +Q_NOWARN_DEPRECATED_PUSH +QString QgsGraduatedSymbolRenderer::methodIdFromMode( QgsGraduatedSymbolRenderer::Mode mode ) +{ + switch ( mode ) + { + case EqualInterval: + return QLatin1String( "EqualInterval" ); + case Quantile: + return QLatin1String( "Quantile" ); + case Jenks: + return QLatin1String( "Jenks" ); + case StdDev: + return QLatin1String( "StdDev" ); + case Pretty: + return QLatin1String( "Pretty" ); + case Custom: + return QString(); + } +} +Q_NOWARN_DEPRECATED_POP + QgsLegendSymbolList QgsGraduatedSymbolRenderer::legendSymbolItems() const { if ( mDataDefinedSizeLegend && mSourceSymbol && mSourceSymbol->type() == QgsSymbol::Marker ) @@ -1318,7 +941,7 @@ void QgsGraduatedSymbolRenderer::addClass( QgsSymbol *symbol ) void QgsGraduatedSymbolRenderer::addClass( double lower, double upper ) { QgsSymbol *newSymbol = mSourceSymbol->clone(); - QString label = mLabelFormat.labelForRange( lower, upper ); + QString label = mClassificationMethod->labelForRange( lower, upper ); mRanges.append( QgsRendererRange( lower, upper, newSymbol, label ) ); } @@ -1333,13 +956,14 @@ void QgsGraduatedSymbolRenderer::addBreak( double breakValue, bool updateSymbols QgsRendererRange newRange = QgsRendererRange(); newRange.setLowerValue( breakValue ); newRange.setUpperValue( range.upperValue() ); - newRange.setLabel( mLabelFormat.labelForRange( newRange ) ); + newRange.setLabel( mClassificationMethod->labelForRange( newRange ) ); newRange.setSymbol( mSourceSymbol->clone() ); //update old range - bool isDefaultLabel = range.label() == mLabelFormat.labelForRange( range ); + bool isDefaultLabel = range.label() == mClassificationMethod->labelForRange( range ); range.setUpperValue( breakValue ); - if ( isDefaultLabel ) range.setLabel( mLabelFormat.labelForRange( range.lowerValue(), breakValue ) ); + if ( isDefaultLabel ) + range.setLabel( mClassificationMethod->labelForRange( range.lowerValue(), breakValue ) ); it.setValue( range ); it.insert( newRange ); @@ -1378,14 +1002,28 @@ void QgsGraduatedSymbolRenderer::deleteAllClasses() void QgsGraduatedSymbolRenderer::setLabelFormat( const QgsRendererRangeLabelFormat &labelFormat, bool updateRanges ) { - if ( updateRanges && labelFormat != mLabelFormat ) - { - for ( QgsRangeList::iterator it = mRanges.begin(); it != mRanges.end(); ++it ) - { - it->setLabel( labelFormat.labelForRange( *it ) ); - } - } mLabelFormat = labelFormat; + mClassificationMethod->setLabelFormat( labelFormat.format() ); + mClassificationMethod->setLabelPrecision( labelFormat.precision() ); + mClassificationMethod->setLabelTrimTrailingZeroes( labelFormat.trimTrailingZeroes() ); + + if ( updateRanges ) + { + updateRangeLabels(); + } +} + +void QgsGraduatedSymbolRenderer::updateRangeLabels() +{ + for ( int i = 0; i < mRanges.count(); i++ ) + { + QgsClassificationMethod::ClassPosition pos = QgsClassificationMethod::Inner; + if ( i == 0 ) + pos = QgsClassificationMethod::LowerBound; + else if ( i == mRanges.count() - 1 ) + pos = QgsClassificationMethod::UpperBound; + mRanges[i].setLabel( mClassificationMethod->labelForRange( mRanges[i], pos ) ); + } } @@ -1416,7 +1054,8 @@ void QgsGraduatedSymbolRenderer::calculateLabelPrecision( bool updateRanges ) nextDpMinRange *= 10.0; } mLabelFormat.setPrecision( ndp ); - if ( updateRanges ) setLabelFormat( mLabelFormat, true ); + if ( updateRanges ) + setLabelFormat( mLabelFormat, true ); } void QgsGraduatedSymbolRenderer::moveClass( int from, int to ) @@ -1520,6 +1159,16 @@ void QgsGraduatedSymbolRenderer::sortByLabel( Qt::SortOrder order ) } } +QgsClassificationMethod *QgsGraduatedSymbolRenderer::classificationMethod() const +{ + return mClassificationMethod.get(); +} + +void QgsGraduatedSymbolRenderer::setClassificationMethod( QgsClassificationMethod *method ) +{ + mClassificationMethod.reset( method ); +} + QgsGraduatedSymbolRenderer *QgsGraduatedSymbolRenderer::convertFromRenderer( const QgsFeatureRenderer *renderer ) { QgsGraduatedSymbolRenderer *r = nullptr; @@ -1581,3 +1230,5 @@ const char *QgsGraduatedSymbolRenderer::graduatedMethodStr( GraduatedMethod meth } return ""; } + + diff --git a/src/core/symbology/qgsgraduatedsymbolrenderer.h b/src/core/symbology/qgsgraduatedsymbolrenderer.h index f60f04cc863..7589b201945 100644 --- a/src/core/symbology/qgsgraduatedsymbolrenderer.h +++ b/src/core/symbology/qgsgraduatedsymbolrenderer.h @@ -15,16 +15,19 @@ #ifndef QGSGRADUATEDSYMBOLRENDERER_H #define QGSGRADUATEDSYMBOLRENDERER_H +#include + + #include "qgis_core.h" #include "qgis_sip.h" #include "qgis.h" #include "qgssymbol.h" #include "qgsrenderer.h" +#include "qgsrendererrange.h" #include "qgsexpression.h" #include "qgsdatadefinedsizelegend.h" -#include "qgsrendererrange.h" +#include "qgsclassificationmethod.h" -#include class QgsVectorLayer; class QgsColorRamp; @@ -105,7 +108,21 @@ class CORE_EXPORT QgsGraduatedSymbolRenderer : public QgsFeatureRenderer void sortByValue( Qt::SortOrder order = Qt::AscendingOrder ); void sortByLabel( Qt::SortOrder order = Qt::AscendingOrder ); - enum Mode + /** + * Returns the classification method + * \since QGIS 3.10 + */ + QgsClassificationMethod *classificationMethod() const; + + /** + * Defines the classification method + * This will take ownership of the method + * \since QGIS 3.10 + */ + void setClassificationMethod( QgsClassificationMethod *method SIP_TRANSFER ); + + //! \deprecated since QGIS 3.10 use QgsClassificationMethod::MethodId instead + enum Q_DECL_DEPRECATED Mode { EqualInterval, Quantile, @@ -115,58 +132,68 @@ class CORE_EXPORT QgsGraduatedSymbolRenderer : public QgsFeatureRenderer Custom }; - Mode mode() const { return mMode; } - void setMode( Mode mode ) { mMode = mode; } + //! \deprecated since QGIS 3.10 use classficationMethod instead + Q_DECL_DEPRECATED Mode mode() const SIP_DEPRECATED { return mMode; } + //! \deprecated since QGIS 3.10 use classficationMethod instead + Q_DECL_DEPRECATED void setMode( Mode mode ) SIP_DEPRECATED { mMode = mode; } /** * Returns if we want to classify symmetric around a given value * \since QGIS 3.4 + * \deprecated since QGIS 3.10 use classficationMethod instead */ - bool useSymmetricMode() const { return mUseSymmetricMode; } + Q_DECL_DEPRECATED bool useSymmetricMode() const SIP_DEPRECATED { return mUseSymmetricMode; } /** * Set if we want to classify symmetric around a given value * \since QGIS 3.4 + * \deprecated since QGIS 3.10 use classficationMethod instead */ - void setUseSymmetricMode( bool useSymmetricMode ) { mUseSymmetricMode = useSymmetricMode; } + Q_DECL_DEPRECATED void setUseSymmetricMode( bool useSymmetricMode ) SIP_DEPRECATED { mUseSymmetricMode = useSymmetricMode; } /** * Returns the pivot value for symmetric classification * \since QGIS 3.4 + * \deprecated since QGIS 3.10 use classficationMethod instead */ - double symmetryPoint() const { return mSymmetryPoint; } + Q_DECL_DEPRECATED double symmetryPoint() const SIP_DEPRECATED { return mSymmetryPoint; } /** * Set the pivot point * \since QGIS 3.4 + * \deprecated since QGIS 3.10 use classficationMethod instead */ - void setSymmetryPoint( double symmetryPoint ) { mSymmetryPoint = symmetryPoint; } + Q_DECL_DEPRECATED void setSymmetryPoint( double symmetryPoint ) SIP_DEPRECATED { mSymmetryPoint = symmetryPoint; } /** * Returns the list of breaks used in the prettybreaks mode. Needed to recover this list in saved configuration, or when property window in closed and reopened * \note Not available in Python bindings, not stable API * \since QGIS 3.4 + * \deprecated since QGIS 3.10 use classficationMethod instead */ - QStringList listForCboPrettyBreaks() const { return mListForCboPrettyBreaks; } SIP_SKIP + Q_DECL_DEPRECATED QStringList listForCboPrettyBreaks() const SIP_DEPRECATED { return mListForCboPrettyBreaks; } SIP_SKIP /** * Set the list of breaks used in the prettybreaks mode, which is needed to recover this list in saved configuration, or when property window is closed and reopened * \note Not available in Python bindings, not stable API * \since QGIS 3.4 + * \deprecated since QGIS 3.10 use classficationMethod instead */ - void setListForCboPrettyBreaks( const QStringList &listForCboPrettyBreaks ) { mListForCboPrettyBreaks = listForCboPrettyBreaks; } SIP_SKIP + Q_DECL_DEPRECATED void setListForCboPrettyBreaks( const QStringList &listForCboPrettyBreaks ) SIP_DEPRECATED { mListForCboPrettyBreaks = listForCboPrettyBreaks; } SIP_SKIP /** * Returns if we want to have a central class astride the pivot value * \since QGIS 3.4 + * \deprecated since QGIS 3.10 use classficationMethod instead */ - bool astride() const { return mAstride; } + Q_DECL_DEPRECATED bool astride() const SIP_DEPRECATED { return mAstride; } /** * Set if we want a central class astride the pivot value * \since QGIS 3.4 + * \deprecated since QGIS 3.10 use classficationMethod instead */ - void setAstride( bool astride ) { mAstride = astride; } + Q_DECL_DEPRECATED void setAstride( bool astride ) SIP_DEPRECATED { mAstride = astride; } /** * Remove the breaks that are above the existing opposite sign classes to keep colors symmetrically balanced around symmetryPoint @@ -175,8 +202,9 @@ class CORE_EXPORT QgsGraduatedSymbolRenderer : public QgsFeatureRenderer * \param symmetryPoint The point around which we want a symmetry * \param astride A bool indicating if the symmetry is made astride the symmetryPoint or not ( [-1,1] vs. [-1,0][0,1] ) * \since QGIS 3.4 + * \deprecated since QGIS 3.10, use QgsClassificationMethod::makeBreaksSymmetric instead */ - static void makeBreaksSymmetric( QList &breaks SIP_INOUT, double symmetryPoint, bool astride ); + Q_DECL_DEPRECATED static void makeBreaksSymmetric( QList &breaks SIP_INOUT, double symmetryPoint, bool astride ) SIP_DEPRECATED; /** * Compute the equal interval classification @@ -186,8 +214,9 @@ class CORE_EXPORT QgsGraduatedSymbolRenderer : public QgsFeatureRenderer * \param useSymmetricMode A bool indicating if we want to have classes and hence colors ramp symmetric around a value * \param symmetryPoint The point around which we want a symmetry * \param astride A bool indicating if the symmetry is made astride the symmetryPoint or not ( [-1,1] vs. [-1,0][0,1] ) + * \deprecated since QGIS 3.10 use QgsClassificationEqualInterval class instead */ - static QList calcEqualIntervalBreaks( double minimum, double maximum, int classes, bool useSymmetricMode, double symmetryPoint, bool astride ); + Q_DECL_DEPRECATED static QList calcEqualIntervalBreaks( double minimum, double maximum, int classes, bool useSymmetricMode, double symmetryPoint, bool astride ); /** * Recalculate classes for a layer @@ -198,22 +227,31 @@ class CORE_EXPORT QgsGraduatedSymbolRenderer : public QgsFeatureRenderer * \param symmetryPoint The value around which the classes will be symmetric if useSymmetricMode is checked * \param astride A bool indicating if the symmetry is made astride the symmetryPoint or not ( [-1,1] vs. [-1,0][0,1] ) * \since QGIS 2.6 (three first arguments) and 3.4 (three last arguments) + * \deprecated since QGIS 3.10 */ - void updateClasses( QgsVectorLayer *vlayer, Mode mode, int nclasses, bool useSymmetricMode = false, double symmetryPoint = 0.0, bool astride = false ); + Q_DECL_DEPRECATED void updateClasses( QgsVectorLayer *vlayer, Mode mode, int nclasses, bool useSymmetricMode = false, double symmetryPoint = 0.0, bool astride = false ) SIP_DEPRECATED; + + /** + * Recalculate classes for a layer + * \param vlayer The layer being rendered (from which data values are calculated) + */ + void updateClasses( const QgsVectorLayer *vl, int numberOfClasses ); /** * Returns the label format used to generate default classification labels * \since QGIS 2.6 + * \deprecated since QGIS 3.10 use classificationMethod() and QgsClassificationMethod::setLabelFormat instead */ - const QgsRendererRangeLabelFormat &labelFormat() const { return mLabelFormat; } + Q_DECL_DEPRECATED const QgsRendererRangeLabelFormat &labelFormat() const { return mLabelFormat; } SIP_DEPRECATED /** * Set the label format used to generate default classification labels * \param labelFormat The string appended to classification labels * \param updateRanges If TRUE then ranges ending with the old unit string are updated to the new. * \since QGIS 2.6 + * \deprecated since QGIS 3.10 use classificationMethod() and QgsClassificationMethod::setLabelFormat instead */ - void setLabelFormat( const QgsRendererRangeLabelFormat &labelFormat, bool updateRanges = false ); + Q_DECL_DEPRECATED void setLabelFormat( const QgsRendererRangeLabelFormat &labelFormat, bool updateRanges = false ) SIP_DEPRECATED; /** * Reset the label decimal places to a numberbased on the minimum class interval @@ -222,6 +260,8 @@ class CORE_EXPORT QgsGraduatedSymbolRenderer : public QgsFeatureRenderer */ void calculateLabelPrecision( bool updateRanges = true ); + Q_NOWARN_DEPRECATED_PUSH + /** * Creates a new graduated renderer. * \param vlayer vector layer @@ -236,8 +276,9 @@ class CORE_EXPORT QgsGraduatedSymbolRenderer : public QgsFeatureRenderer * \param listForCboPrettyBreaks The list of potential pivot values for symmetric mode with prettybreaks mode * \param astride A bool indicating if the symmetry is made astride the symmetryPoint or not ( [-1,1] vs. [-1,0][0,1] ) * \returns new QgsGraduatedSymbolRenderer object + * \deprecated since QGIS 3.10 */ - static QgsGraduatedSymbolRenderer *createRenderer( QgsVectorLayer *vlayer, + Q_DECL_DEPRECATED static QgsGraduatedSymbolRenderer *createRenderer( QgsVectorLayer *vlayer, const QString &attrName, int classes, Mode mode, @@ -247,7 +288,8 @@ class CORE_EXPORT QgsGraduatedSymbolRenderer : public QgsFeatureRenderer bool useSymmetricMode = false, double symmetryPoint = 0.0, QStringList listForCboPrettyBreaks = QStringList(), - bool astride = false ); + bool astride = false ) SIP_DEPRECATED; + Q_NOWARN_DEPRECATED_POP //! create renderer from XML element static QgsFeatureRenderer *create( QDomElement &element, const QgsReadWriteContext &context ) SIP_FACTORY; @@ -370,6 +412,12 @@ class CORE_EXPORT QgsGraduatedSymbolRenderer : public QgsFeatureRenderer */ QgsDataDefinedSizeLegend *dataDefinedSizeLegend() const; + /** + * Updates the labels of the ranges + * \since QGIS 3.10 + */ + void updateRangeLabels(); + protected: QString mAttrName; QgsRangeList mRanges; @@ -400,6 +448,8 @@ class CORE_EXPORT QgsGraduatedSymbolRenderer : public QgsFeatureRenderer //! \note not available in Python bindings static const char *graduatedMethodStr( GraduatedMethod method ) SIP_SKIP; + std::shared_ptr mClassificationMethod; + bool mUseSymmetricMode = false; double mSymmetryPoint = 0.0; QStringList mListForCboPrettyBreaks = QStringList(); @@ -415,6 +465,11 @@ class CORE_EXPORT QgsGraduatedSymbolRenderer : public QgsFeatureRenderer //! Returns list of legend symbol items from individual ranges QgsLegendSymbolList baseLegendSymbolItems() const; + // TODO QGIS 4: remove + Q_NOWARN_DEPRECATED_PUSH + static QString methodIdFromMode( QgsGraduatedSymbolRenderer::Mode mode ); + Q_NOWARN_DEPRECATED_POP + #ifdef SIP_RUN QgsGraduatedSymbolRenderer( const QgsGraduatedSymbolRenderer & ); QgsGraduatedSymbolRenderer &operator=( const QgsGraduatedSymbolRenderer & ); diff --git a/src/core/symbology/qgsrendererrange.cpp b/src/core/symbology/qgsrendererrange.cpp index d1b342c4e3d..2dfc79595b9 100644 --- a/src/core/symbology/qgsrendererrange.cpp +++ b/src/core/symbology/qgsrendererrange.cpp @@ -14,6 +14,17 @@ ***************************************************************************/ #include "qgsrendererrange.h" +#include "qgsclassificationmethod.h" + + +QgsRendererRange::QgsRendererRange( const QgsClassificationRange &range, QgsSymbol *symbol, bool render ) + : mLowerValue( range.lowerBound() ) + , mUpperValue( range.upperBound() ) + , mSymbol( symbol ) + , mLabel( range.label() ) + , mRender( render ) +{ +} QgsRendererRange::QgsRendererRange( double lowerValue, double upperValue, QgsSymbol *symbol, const QString &label, bool render ) : mLowerValue( lowerValue ) diff --git a/src/core/symbology/qgsrendererrange.h b/src/core/symbology/qgsrendererrange.h index 946fd03b182..6f46a7af734 100644 --- a/src/core/symbology/qgsrendererrange.h +++ b/src/core/symbology/qgsrendererrange.h @@ -26,6 +26,7 @@ class QDomDocument; class QDomElement; class QgsSymbol; +class QgsClassificationRange; /** @@ -40,6 +41,7 @@ class CORE_EXPORT QgsRendererRange * Constructor for QgsRendererRange. */ QgsRendererRange() = default; + QgsRendererRange( const QgsClassificationRange &range, QgsSymbol *symbol SIP_TRANSFER, bool render = true ); QgsRendererRange( double lowerValue, double upperValue, QgsSymbol *symbol SIP_TRANSFER, const QString &label, bool render = true ); QgsRendererRange( const QgsRendererRange &range ); @@ -88,12 +90,14 @@ class CORE_EXPORT QgsRendererRange typedef QList QgsRangeList; + /** * \ingroup core * \class QgsRendererRangeLabelFormat * \since QGIS 2.6 + * \deprecated since QGIS 3.10, use QgsClassificationMethod instead */ -class CORE_EXPORT QgsRendererRangeLabelFormat +class Q_DECL_DEPRECATED CORE_EXPORT QgsRendererRangeLabelFormat SIP_DEPRECATED { public: QgsRendererRangeLabelFormat();