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.
This commit is contained in:
Alexander Bruy 2023-05-30 15:59:25 +03:00 committed by Nyall Dawson
parent b808af6340
commit d0c7c6e930
6 changed files with 104 additions and 17 deletions

View File

@ -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",

View File

@ -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.",

View File

@ -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"]
}

View File

@ -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<QgsExpressionFunction *> &QgsExpression::Functions()
<< new QgsStaticExpressionFunction( QStringLiteral( "min" ), -1, fcnMin, QStringLiteral( "Math" ), QString(), false, QSet<QString>(), 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<QString>(), 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<QString>(), false, QStringList() << QStringLiteral( "$pi" ) )

View File

@ -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 );

View File

@ -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 ) );
}