From d0c7c6e9302e235240294598132d6efcbaf5aaeb Mon Sep 17 00:00:00 2001 From: Alexander Bruy Date: Tue, 30 May 2023 15:59:25 +0300 Subject: [PATCH] fix formula for exponential interpolation The scale_exp function renamed to scale_polynomial and has an alias scale_exp to preserve backward compatibility. A new function using correct formula was added as scale_exponential. --- .../db_plugins/vlayers/sql_dictionary.py | 2 +- .../json/{scale_exp => scale_exponential} | 2 +- resources/function_help/json/scale_polynomial | 35 ++++++++++++++++ src/core/expression/qgsexpressionfunction.cpp | 42 +++++++++++++++++-- tests/src/core/testqgsexpression.cpp | 16 +++++++ tests/src/core/testqgsproperty.cpp | 24 +++++------ 6 files changed, 104 insertions(+), 17 deletions(-) rename resources/function_help/json/{scale_exp => scale_exponential} (98%) create mode 100644 resources/function_help/json/scale_polynomial diff --git a/python/plugins/db_manager/db_plugins/vlayers/sql_dictionary.py b/python/plugins/db_manager/db_plugins/vlayers/sql_dictionary.py index 966c790f7fa..26c935b7c0e 100644 --- a/python/plugins/db_manager/db_plugins/vlayers/sql_dictionary.py +++ b/python/plugins/db_manager/db_plugins/vlayers/sql_dictionary.py @@ -119,7 +119,7 @@ spatialite_functions = [ # from www.gaia-gis.it/spatialite-2.3.0/spatialite-sql ] qgis_functions = [ - "atan2", "round", "rand", "randf", "clamp", "scale_linear", "scale_exp", "_pi", "to_int", "toint", "to_real", "toreal", + "atan2", "round", "rand", "randf", "clamp", "scale_linear", "scale_polynomial", "scale_exponential", "_pi", "to_int", "toint", "to_real", "toreal", "to_string", "tostring", "to_datetime", "todatetime", "to_date", "todate", "to_time", "totime", "to_interval", "tointerval", "regexp_match", "now", "_now", "age", "year", "month", "week", "day", "hour", "minute", "second", "day_of_week", "title", "levenshtein", "longest_common_substring", "hamming_distance", "wordwrap", "regexp_replace", "regexp_substr", "concat", diff --git a/resources/function_help/json/scale_exp b/resources/function_help/json/scale_exponential similarity index 98% rename from resources/function_help/json/scale_exp rename to resources/function_help/json/scale_exponential index 6329390b4d0..b201e3d4f22 100644 --- a/resources/function_help/json/scale_exp +++ b/resources/function_help/json/scale_exponential @@ -1,5 +1,5 @@ { - "name": "scale_exp", + "name": "scale_exponential", "type": "function", "groups": ["Math"], "description": "Transforms a given value from an input domain to an output range using an exponential curve. This function can be used to ease values in or out of the specified output range.", diff --git a/resources/function_help/json/scale_polynomial b/resources/function_help/json/scale_polynomial new file mode 100644 index 00000000000..1caaa55f64d --- /dev/null +++ b/resources/function_help/json/scale_polynomial @@ -0,0 +1,35 @@ +{ + "name": "scale_polynomial", + "type": "function", + "groups": ["Math"], + "description": "Transforms a given value from an input domain to an output range using a polynomial curve. This function can be used to ease values in or out of the specified output range.", + "arguments": [{ + "arg": "value", + "description": "A value in the input domain. The function will return a corresponding scaled value in the output range." + }, { + "arg": "domain_min", + "description": "Specifies the minimum value in the input domain, the smallest value the input value should take." + }, { + "arg": "domain_max", + "description": "Specifies the maximum value in the input domain, the largest value the input value should take." + }, { + "arg": "range_min", + "description": "Specifies the minimum value in the output range, the smallest value which should be output by the function." + }, { + "arg": "range_max", + "description": "Specifies the maximum value in the output range, the largest value which should be output by the function." + }, { + "arg": "exponent", + "description": "A positive value (greater than 0), which dictates the way input values are mapped to the output range. Large exponents will cause the output values to 'ease in', starting slowly before accelerating as the input values approach the domain maximum. Smaller exponents (less than 1) will cause output values to 'ease out', where the mapping starts quickly but slows as it approaches the domain maximum." + }], + "examples": [{ + "expression": "scale_polynomial(5,0,10,0,100,2)", + "returns": "25", + "note": "easing in, using an exponent of 2" + }, { + "expression": "scale_polynomial(3,0,10,0,100,0.5)", + "returns": "54.772", + "note": "easing out, using an exponent of 0.5" + }], + "tags": ["polynomial", "curve", "ease", "transforms", "output", "given", "input", "domain", "range", "specified", "values"] +} diff --git a/src/core/expression/qgsexpressionfunction.cpp b/src/core/expression/qgsexpressionfunction.cpp index 9335884e974..57a3aa87531 100644 --- a/src/core/expression/qgsexpressionfunction.cpp +++ b/src/core/expression/qgsexpressionfunction.cpp @@ -516,7 +516,41 @@ static QVariant fcnLinearScale( const QVariantList &values, const QgsExpressionC return QVariant( m * val + c ); } -static QVariant fcnExpScale( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * ) +static QVariant fcnPolynomialScale( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * ) +{ + double val = QgsExpressionUtils::getDoubleValue( values.at( 0 ), parent ); + double domainMin = QgsExpressionUtils::getDoubleValue( values.at( 1 ), parent ); + double domainMax = QgsExpressionUtils::getDoubleValue( values.at( 2 ), parent ); + double rangeMin = QgsExpressionUtils::getDoubleValue( values.at( 3 ), parent ); + double rangeMax = QgsExpressionUtils::getDoubleValue( values.at( 4 ), parent ); + double exponent = QgsExpressionUtils::getDoubleValue( values.at( 5 ), parent ); + + if ( domainMin >= domainMax ) + { + parent->setEvalErrorString( QObject::tr( "Domain max must be greater than domain min" ) ); + return QVariant(); + } + if ( exponent <= 0 ) + { + parent->setEvalErrorString( QObject::tr( "Exponent must be greater than 0" ) ); + return QVariant(); + } + + // outside of domain? + if ( val >= domainMax ) + { + return rangeMax; + } + else if ( val <= domainMin ) + { + return rangeMin; + } + + // Return polynomially scaled value + return QVariant( ( ( rangeMax - rangeMin ) / std::pow( domainMax - domainMin, exponent ) ) * std::pow( val - domainMin, exponent ) + rangeMin ); +} + +static QVariant fcnExponentialScale( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * ) { double val = QgsExpressionUtils::getDoubleValue( values.at( 0 ), parent ); double domainMin = QgsExpressionUtils::getDoubleValue( values.at( 1 ), parent ); @@ -547,7 +581,8 @@ static QVariant fcnExpScale( const QVariantList &values, const QgsExpressionCont } // Return exponentially scaled value - return QVariant( ( ( rangeMax - rangeMin ) / std::pow( domainMax - domainMin, exponent ) ) * std::pow( val - domainMin, exponent ) + rangeMin ); + double ratio = ( std::pow( exponent, val - domainMin ) - 1 ) / ( std::pow( exponent, domainMax - domainMin ) - 1 ); + return QVariant( ( rangeMax - rangeMin ) * ratio + rangeMin ); } static QVariant fcnMax( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * ) @@ -8022,7 +8057,8 @@ const QList &QgsExpression::Functions() << new QgsStaticExpressionFunction( QStringLiteral( "min" ), -1, fcnMin, QStringLiteral( "Math" ), QString(), false, QSet(), false, QStringList(), /* handlesNull = */ true ) << new QgsStaticExpressionFunction( QStringLiteral( "clamp" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "min" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "value" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "max" ) ), fcnClamp, QStringLiteral( "Math" ) ) << new QgsStaticExpressionFunction( QStringLiteral( "scale_linear" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "value" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "domain_min" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "domain_max" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "range_min" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "range_max" ) ), fcnLinearScale, QStringLiteral( "Math" ) ) - << new QgsStaticExpressionFunction( QStringLiteral( "scale_exp" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "value" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "domain_min" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "domain_max" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "range_min" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "range_max" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "exponent" ) ), fcnExpScale, QStringLiteral( "Math" ) ) + << new QgsStaticExpressionFunction( QStringLiteral( "scale_polynomial" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "value" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "domain_min" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "domain_max" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "range_min" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "range_max" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "exponent" ) ), fcnPolynomialScale, QStringLiteral( "Math" ), QString(), false, QSet(), false, QStringList() << QStringLiteral( "scale_exp" ) ) + << new QgsStaticExpressionFunction( QStringLiteral( "scale_exponential" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "value" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "domain_min" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "domain_max" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "range_min" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "range_max" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "exponent" ) ), fcnExponentialScale, QStringLiteral( "Math" ) ) << new QgsStaticExpressionFunction( QStringLiteral( "floor" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "value" ) ), fcnFloor, QStringLiteral( "Math" ) ) << new QgsStaticExpressionFunction( QStringLiteral( "ceil" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "value" ) ), fcnCeil, QStringLiteral( "Math" ) ) << new QgsStaticExpressionFunction( QStringLiteral( "pi" ), 0, fcnPi, QStringLiteral( "Math" ), QString(), false, QSet(), false, QStringList() << QStringLiteral( "$pi" ) ) diff --git a/tests/src/core/testqgsexpression.cpp b/tests/src/core/testqgsexpression.cpp index c518d2488af..ea333ba1e64 100644 --- a/tests/src/core/testqgsexpression.cpp +++ b/tests/src/core/testqgsexpression.cpp @@ -1000,6 +1000,15 @@ class TestQgsExpression: public QObject QTest::newRow( "scale_linear(-1,0,10,100,200)" ) << "scale_linear(-1,0,10,100,200)" << false << QVariant( 100. ); QTest::newRow( "scale_linear(11,0,10,100,200)" ) << "scale_linear(11,0,10,100,200)" << false << QVariant( 200. ); + // previously had name scale_exp, but renamed to scale_polynomial as it uses polynomial interpolation formula + // see https://github.com/qgis/QGIS/pull/53164 for more details + QTest::newRow( "scale_polynomial(0.5,0,1,0,1,2)" ) << "scale_polynomial(0.5,0,1,0,1,2)" << false << QVariant( 0.25 ); + QTest::newRow( "scale_polynomial(0,0,10,100,200,2)" ) << "scale_polynomial(0,0,10,100,200,2)" << false << QVariant( 100. ); + QTest::newRow( "scale_polynomial(5,0,10,100,200,2)" ) << "scale_polynomial(5,0,10,100,200,2)" << false << QVariant( 125. ); + QTest::newRow( "scale_polynomial(10,0,10,100,200,0.5)" ) << "scale_polynomial(10,0,10,100,200,0.5)" << false << QVariant( 200. ); + QTest::newRow( "scale_polynomial(-1,0,10,100,200,0.5)" ) << "scale_polynomial(-1,0,10,100,200,0.5)" << false << QVariant( 100. ); + QTest::newRow( "scale_polynomial(4,0,9,0,90,0.5)" ) << "scale_polynomial(4,0,9,0,90,0.5)" << false << QVariant( 60. ); + // this is an alias for scale_polynomial to preserve backwar compatibility QTest::newRow( "scale_exp(0.5,0,1,0,1,2)" ) << "scale_exp(0.5,0,1,0,1,2)" << false << QVariant( 0.25 ); QTest::newRow( "scale_exp(0,0,10,100,200,2)" ) << "scale_exp(0,0,10,100,200,2)" << false << QVariant( 100. ); QTest::newRow( "scale_exp(5,0,10,100,200,2)" ) << "scale_exp(5,0,10,100,200,2)" << false << QVariant( 125. ); @@ -1007,6 +1016,13 @@ class TestQgsExpression: public QObject QTest::newRow( "scale_exp(-1,0,10,100,200,0.5)" ) << "scale_exp(-1,0,10,100,200,0.5)" << false << QVariant( 100. ); QTest::newRow( "scale_exp(4,0,9,0,90,0.5)" ) << "scale_exp(4,0,9,0,90,0.5)" << false << QVariant( 60. ); + QTest::newRow( "scale_exponential(0.5,0,1,0,1,2)" ) << "scale_exponential(0.5,0,1,0,1,2)" << false << QVariant( 0.414213562373 ); + QTest::newRow( "scale_exponential(0,0,10,100,200,2)" ) << "scale_exponential(0,0,10,100,200,2)" << false << QVariant( 100. ); + QTest::newRow( "scale_exponential(5,0,10,100,200,2)" ) << "scale_exponential(5,0,10,100,200,2)" << false << QVariant( 103.0303030303 ); + QTest::newRow( "scale_exponential(10,0,10,100,200,0.5)" ) << "scale_exponential(10,0,10,100,200,0.5)" << false << QVariant( 200. ); + QTest::newRow( "scale_exponential(-1,0,10,100,200,0.5)" ) << "scale_exponential(-1,0,10,100,200,0.5)" << false << QVariant( 100. ); + QTest::newRow( "scale_exponential(4,0,9,0,90,0.5)" ) << "scale_exponential(4,0,9,0,90,0.5)" << false << QVariant( 84.5401174168 ); + // cast functions QTest::newRow( "double to int" ) << "toint(3.2)" << false << QVariant( 3 ); QTest::newRow( "text to int" ) << "toint('53')" << false << QVariant( 53 ); diff --git a/tests/src/core/testqgsproperty.cpp b/tests/src/core/testqgsproperty.cpp index ed74c59179a..9c07795c6a3 100644 --- a/tests/src/core/testqgsproperty.cpp +++ b/tests/src/core/testqgsproperty.cpp @@ -952,7 +952,7 @@ void TestQgsProperty::genericNumericTransformer() 1.0 ); QCOMPARE( t3.toExpression( "5+6" ), QStringLiteral( "coalesce(scale_linear(5+6, 15, 25, 150, 250), -10)" ) ); t3.setExponent( 1.6 ); - QCOMPARE( t3.toExpression( "5+6" ), QStringLiteral( "coalesce(scale_exp(5+6, 15, 25, 150, 250, 1.6), -10)" ) ); + QCOMPARE( t3.toExpression( "5+6" ), QStringLiteral( "coalesce(scale_polynomial(5+6, 15, 25, 150, 250, 1.6), -10)" ) ); // test size scale transformer inside property QgsProperty p; @@ -1006,7 +1006,7 @@ void TestQgsProperty::genericNumericTransformerFromExpression() QCOMPARE( exp->minOutputValue(), 2. ); QCOMPARE( exp->maxOutputValue(), 10. ); - exp.reset( QgsGenericNumericTransformer::fromExpression( QStringLiteral( "coalesce(scale_exp(column, 1, 7, 2, 10, 0.51), 1)" ), baseExpression, fieldName ) ); + exp.reset( QgsGenericNumericTransformer::fromExpression( QStringLiteral( "coalesce(scale_polynomial(column, 1, 7, 2, 10, 0.51), 1)" ), baseExpression, fieldName ) ); QVERIFY( exp.get() ); QCOMPARE( exp->minValue(), 1. ); QCOMPARE( exp->maxValue(), 7. ); @@ -1015,8 +1015,8 @@ void TestQgsProperty::genericNumericTransformerFromExpression() QCOMPARE( exp->exponent(), 0.51 ); QCOMPARE( exp->nullOutputValue(), 1.0 ); - QVERIFY( !QgsGenericNumericTransformer::fromExpression( QStringLiteral( "coalesce(scale_exp(column, 1, 7, a, 10, 0.5), 0)" ), baseExpression, fieldName ) ); - QVERIFY( !QgsGenericNumericTransformer::fromExpression( QStringLiteral( "coalesce(scale_exp(column, 1, 7), 0)" ), baseExpression, fieldName ) ); + QVERIFY( !QgsGenericNumericTransformer::fromExpression( QStringLiteral( "coalesce(scale_polynomial(column, 1, 7, a, 10, 0.5), 0)" ), baseExpression, fieldName ) ); + QVERIFY( !QgsGenericNumericTransformer::fromExpression( QStringLiteral( "coalesce(scale_polynomial(column, 1, 7), 0)" ), baseExpression, fieldName ) ); QVERIFY( !QgsGenericNumericTransformer::fromExpression( QStringLiteral( "1+2" ), baseExpression, fieldName ) ); QVERIFY( !QgsGenericNumericTransformer::fromExpression( QString(), baseExpression, fieldName ) ); } @@ -1171,7 +1171,7 @@ void TestQgsProperty::sizeScaleTransformer() QCOMPARE( t2.toExpression( "5+6" ), QStringLiteral( "coalesce(scale_linear(5+6, 15, 25, 150, 250), -10)" ) ); t2.setType( QgsSizeScaleTransformer::Exponential ); t2.setExponent( 1.6 ); - QCOMPARE( t2.toExpression( "5+6" ), QStringLiteral( "coalesce(scale_exp(5+6, 15, 25, 150, 250, 1.6), -10)" ) ); + QCOMPARE( t2.toExpression( "5+6" ), QStringLiteral( "coalesce(scale_polynomial(5+6, 15, 25, 150, 250, 1.6), -10)" ) ); // test size scale transformer inside property QgsProperty p; @@ -1209,11 +1209,11 @@ void TestQgsProperty::sizeScaleTransformerFromExpression() QCOMPARE( exp->maxSize(), 10. ); QCOMPARE( exp->nullSize(), 0.0 ); - exp.reset( QgsSizeScaleTransformer::fromExpression( QStringLiteral( "coalesce(scale_exp(column, 1, 7, 2, 10, 0.5), 0)" ), baseExpression, fieldName ) ); + exp.reset( QgsSizeScaleTransformer::fromExpression( QStringLiteral( "coalesce(scale_polynomial(column, 1, 7, 2, 10, 0.5), 0)" ), baseExpression, fieldName ) ); QVERIFY( exp.get() ); QCOMPARE( exp->type(), QgsSizeScaleTransformer::Area ); - exp.reset( QgsSizeScaleTransformer::fromExpression( QStringLiteral( "coalesce(scale_exp(column, 1, 7, 2, 10, 0.57), 0)" ), baseExpression, fieldName ) ); + exp.reset( QgsSizeScaleTransformer::fromExpression( QStringLiteral( "coalesce(scale_polynomial(column, 1, 7, 2, 10, 0.57), 0)" ), baseExpression, fieldName ) ); QVERIFY( exp.get() ); QCOMPARE( exp->type(), QgsSizeScaleTransformer::Flannery ); @@ -1237,21 +1237,21 @@ void TestQgsProperty::sizeScaleTransformerFromExpression() QCOMPARE( exp->minSize(), 2. ); QCOMPARE( exp->maxSize(), 10. ); - exp.reset( QgsSizeScaleTransformer::fromExpression( QStringLiteral( "scale_exp(column, 1, 7, 2, 10, 0.5)" ), baseExpression, fieldName ) ); + exp.reset( QgsSizeScaleTransformer::fromExpression( QStringLiteral( "scale_polynomial(column, 1, 7, 2, 10, 0.5)" ), baseExpression, fieldName ) ); QVERIFY( exp.get() ); QCOMPARE( exp->type(), QgsSizeScaleTransformer::Area ); - exp.reset( QgsSizeScaleTransformer::fromExpression( QStringLiteral( "scale_exp(column, 1, 7, 2, 10, 0.57)" ), baseExpression, fieldName ) ); + exp.reset( QgsSizeScaleTransformer::fromExpression( QStringLiteral( "scale_polynomial(column, 1, 7, 2, 10, 0.57)" ), baseExpression, fieldName ) ); QVERIFY( exp.get() ); QCOMPARE( exp->type(), QgsSizeScaleTransformer::Flannery ); - exp.reset( QgsSizeScaleTransformer::fromExpression( QStringLiteral( "coalesce(scale_exp(column, 1, 7, 2, 10, 0.51), 22)" ), baseExpression, fieldName ) ); + exp.reset( QgsSizeScaleTransformer::fromExpression( QStringLiteral( "coalesce(scale_polynomial(column, 1, 7, 2, 10, 0.51), 22)" ), baseExpression, fieldName ) ); QVERIFY( exp.get() ); QCOMPARE( exp->type(), QgsSizeScaleTransformer::Exponential ); QCOMPARE( exp->nullSize(), 22.0 ); - QVERIFY( !QgsSizeScaleTransformer::fromExpression( QStringLiteral( "coalesce(scale_exp(column, 1, 7, a, 10, 0.5), 0)" ), baseExpression, fieldName ) ); - QVERIFY( !QgsSizeScaleTransformer::fromExpression( QStringLiteral( "coalesce(scale_exp(column, 1, 7), 0)" ), baseExpression, fieldName ) ); + QVERIFY( !QgsSizeScaleTransformer::fromExpression( QStringLiteral( "coalesce(scale_polynomial(column, 1, 7, a, 10, 0.5), 0)" ), baseExpression, fieldName ) ); + QVERIFY( !QgsSizeScaleTransformer::fromExpression( QStringLiteral( "coalesce(scale_polynomial(column, 1, 7), 0)" ), baseExpression, fieldName ) ); QVERIFY( !QgsSizeScaleTransformer::fromExpression( QStringLiteral( "1+2" ), baseExpression, fieldName ) ); QVERIFY( !QgsSizeScaleTransformer::fromExpression( QString(), baseExpression, fieldName ) ); }