diff --git a/python/core/qgshistogram.sip b/python/core/qgshistogram.sip index f65399da52a..31aaef2a95a 100644 --- a/python/core/qgshistogram.sip +++ b/python/core/qgshistogram.sip @@ -22,13 +22,7 @@ class QgsHistogram */ void setValues( const QList& values ); - /** Assigns numeric source values for the histogram from a vector layer's field or as the - * result of an expression. - * @param layer vector layer - * @param fieldOrExpression field name or expression to be evaluated - * @returns true if values were successfully set - */ - bool setValues( QgsVectorLayer* layer, const QString& fieldOrExpression ); + bool setValues( const QgsVectorLayer* layer, const QString& fieldOrExpression, QgsFeedback* feedback = 0 ); /** Calculates the optimal bin width using the Freedman-Diaconis rule. Bins widths are * determined by the inter-quartile range of values and the number of values. diff --git a/python/core/qgspropertytransformer.sip b/python/core/qgspropertytransformer.sip index 26111e89c64..eb5c9e3de2e 100644 --- a/python/core/qgspropertytransformer.sip +++ b/python/core/qgspropertytransformer.sip @@ -1,3 +1,35 @@ +class QgsCurveTransform +{ +%TypeHeaderCode +#include +%End + + public: + + QgsCurveTransform(); + QgsCurveTransform( const QList< QgsPoint >& controlPoints ); + ~QgsCurveTransform(); + QgsCurveTransform( const QgsCurveTransform& other ); + + //QgsCurveTransform& operator=( const QgsCurveTransform& other ); + + QList< QgsPoint > controlPoints() const; + + void setControlPoints( const QList< QgsPoint >& points ); + + void addControlPoint( double x, double y ); + + void removeControlPoint( double x, double y ); + + double y( double x ) const; + + QVector< double > y( const QVector< double >& x ) const; + + bool readXml( const QDomElement& elem, const QDomDocument& doc ); + + bool writeXml( QDomElement& transformElem, QDomDocument& doc ) const; + +}; class QgsPropertyTransformer { %TypeHeaderCode @@ -28,6 +60,8 @@ class QgsPropertyTransformer QgsPropertyTransformer( double minValue = 0.0, double maxValue = 1.0 ); + QgsPropertyTransformer( const QgsPropertyTransformer& other ); + virtual ~QgsPropertyTransformer(); virtual Type transformerType() const = 0; @@ -46,11 +80,18 @@ class QgsPropertyTransformer void setMaxValue( double max ); + QgsCurveTransform* curveTransform() const; + + void setCurveTransform( QgsCurveTransform* transform /Transfer/ ); + virtual QVariant transform( const QgsExpressionContext& context, const QVariant& value ) const = 0; virtual QString toExpression( const QString& baseExpression ) const = 0; static QgsPropertyTransformer* fromExpression( const QString& expression, QString& baseExpression /Out/, QString& fieldName /Out/ ) /Factory/; + protected: + + double transformNumeric( double input ) const; }; class QgsGenericNumericTransformer : QgsPropertyTransformer { @@ -66,6 +107,8 @@ class QgsGenericNumericTransformer : QgsPropertyTransformer double nullOutput = 0.0, double exponent = 1.0 ); + QgsGenericNumericTransformer( const QgsGenericNumericTransformer& other ); + virtual Type transformerType() const; virtual QgsGenericNumericTransformer* clone() /Factory/; virtual bool writeXml( QDomElement& transformerElem, QDomDocument& doc ) const; @@ -108,6 +151,8 @@ class QgsSizeScaleTransformer : QgsPropertyTransformer double nullSize = 0.0, double exponent = 1.0 ); + QgsSizeScaleTransformer( const QgsSizeScaleTransformer& other ); + virtual Type transformerType() const; virtual QgsSizeScaleTransformer* clone() /Factory/; virtual bool writeXml( QDomElement& transformerElem, QDomDocument& doc ) const; diff --git a/python/core/qgsvectorlayer.sip b/python/core/qgsvectorlayer.sip index a61341786d5..40f35fcb983 100644 --- a/python/core/qgsvectorlayer.sip +++ b/python/core/qgsvectorlayer.sip @@ -1352,27 +1352,9 @@ class QgsVectorLayer : QgsMapLayer, QgsExpressionContextGenerator QgsExpressionContext* context = nullptr, bool* ok = nullptr ) const; - /** Fetches all values from a specified field name or expression. - * @param fieldOrExpression field name or an expression string - * @param ok will be set to false if field or expression is invalid, otherwise true - * @param selectedOnly set to true to get values from selected features only - * @returns list of fetched values - * @note added in QGIS 2.9 - * @see getDoubleValues - */ - QList< QVariant > getValues( const QString &fieldOrExpression, bool &ok, bool selectedOnly = false ) const; + QList< QVariant > getValues( const QString &fieldOrExpression, bool &ok, bool selectedOnly = false, QgsFeedback* feedback = 0 ) const; - /** Fetches all double values from a specified field name or expression. Null values or - * invalid expression results are skipped. - * @param fieldOrExpression field name or an expression string evaluating to a double value - * @param ok will be set to false if field or expression is invalid, otherwise true - * @param selectedOnly set to true to get values from selected features only - * @param nullCount optional pointer to integer to store number of null values encountered in - * @returns list of fetched values - * @note added in QGIS 2.9 - * @see getValues - */ - QList< double > getDoubleValues( const QString &fieldOrExpression, bool &ok, bool selectedOnly = false, int* nullCount = 0 ) const; + QList< double > getDoubleValues( const QString &fieldOrExpression, bool &ok, bool selectedOnly = false, int* nullCount = 0, QgsFeedback* feedback = 0 ) const; /** Set the blending mode used for rendering each feature */ void setFeatureBlendMode( QPainter::CompositionMode blendMode ); diff --git a/python/gui/gui.sip b/python/gui/gui.sip index 225ef6a593c..98cb3be40e3 100644 --- a/python/gui/gui.sip +++ b/python/gui/gui.sip @@ -51,6 +51,7 @@ %Include qgsconfigureshortcutsdialog.sip %Include qgscredentialdialog.sip %Include qgscustomdrophandler.sip +%Include qgscurveeditorwidget.sip %Include qgsdetaileditemdata.sip %Include qgsdetaileditemdelegate.sip %Include qgsdetaileditemwidget.sip diff --git a/python/gui/qgscurveeditorwidget.sip b/python/gui/qgscurveeditorwidget.sip new file mode 100644 index 00000000000..7bbca6f781c --- /dev/null +++ b/python/gui/qgscurveeditorwidget.sip @@ -0,0 +1,31 @@ +class QgsCurveEditorWidget : QWidget +{ +%TypeHeaderCode +#include +%End + + public: + + QgsCurveEditorWidget( QWidget* parent /TransferThis/ = 0, const QgsCurveTransform& curve = QgsCurveTransform() ); + ~QgsCurveEditorWidget(); + + QgsCurveTransform curve() const; + + void setCurve( const QgsCurveTransform& curve ); + void setHistogramSource( const QgsVectorLayer* layer, const QString& expression ); + double minHistogramValueRange() const; + double maxHistogramValueRange() const; + + public slots: + void setMinHistogramValueRange( double minValueRange ); + void setMaxHistogramValueRange( double maxValueRange ); + + signals: + + void changed(); + + protected: + + virtual void keyPressEvent( QKeyEvent *event ); + +}; diff --git a/src/core/qgshistogram.cpp b/src/core/qgshistogram.cpp index 0d5306e8126..12d80cb8023 100644 --- a/src/core/qgshistogram.cpp +++ b/src/core/qgshistogram.cpp @@ -47,14 +47,14 @@ void QgsHistogram::setValues( const QList &values ) prepareValues(); } -bool QgsHistogram::setValues( QgsVectorLayer *layer, const QString &fieldOrExpression ) +bool QgsHistogram::setValues( const QgsVectorLayer *layer, const QString &fieldOrExpression, QgsFeedback* feedback ) { mValues.clear(); if ( !layer ) return false; bool ok; - mValues = layer->getDoubleValues( fieldOrExpression, ok ); + mValues = layer->getDoubleValues( fieldOrExpression, ok, false, nullptr, feedback ); if ( !ok ) return false; diff --git a/src/core/qgshistogram.h b/src/core/qgshistogram.h index 7505de49364..4d812080070 100644 --- a/src/core/qgshistogram.h +++ b/src/core/qgshistogram.h @@ -21,6 +21,7 @@ #include #include "qgis_core.h" +#include "qgsfeedback.h" class QgsVectorLayer; @@ -49,9 +50,10 @@ class CORE_EXPORT QgsHistogram * result of an expression. * @param layer vector layer * @param fieldOrExpression field name or expression to be evaluated + * @param feedback optional feedback object to allow cancelation of calculation * @returns true if values were successfully set */ - bool setValues( QgsVectorLayer* layer, const QString& fieldOrExpression ); + bool setValues( const QgsVectorLayer* layer, const QString& fieldOrExpression, QgsFeedback* feedback = nullptr ); /** Calculates the optimal bin width using the Freedman-Diaconis rule. Bins widths are * determined by the inter-quartile range of values and the number of values. diff --git a/src/core/qgspropertytransformer.cpp b/src/core/qgspropertytransformer.cpp index 3302531848e..c58a44ba399 100644 --- a/src/core/qgspropertytransformer.cpp +++ b/src/core/qgspropertytransformer.cpp @@ -50,11 +50,32 @@ QgsPropertyTransformer::QgsPropertyTransformer( double minValue, double maxValue , mMaxValue( maxValue ) {} +QgsPropertyTransformer::QgsPropertyTransformer( const QgsPropertyTransformer& other ) + : mMinValue( other.mMinValue ) + , mMaxValue( other.mMaxValue ) + , mCurveTransform( other.mCurveTransform ? new QgsCurveTransform( *other.mCurveTransform ) : nullptr ) +{} + +QgsPropertyTransformer& QgsPropertyTransformer::operator=( const QgsPropertyTransformer & other ) +{ + mMinValue = other.mMinValue; + mMaxValue = other.mMaxValue; + mCurveTransform.reset( other.mCurveTransform ? new QgsCurveTransform( *other.mCurveTransform ) : nullptr ); + return *this; +} + bool QgsPropertyTransformer::writeXml( QDomElement& transformerElem, QDomDocument& doc ) const { Q_UNUSED( doc ); transformerElem.setAttribute( "minValue", QString::number( mMinValue ) ); transformerElem.setAttribute( "maxValue", QString::number( mMaxValue ) ); + + if ( mCurveTransform ) + { + QDomElement curveElement = doc.createElement( "curve" ); + mCurveTransform->writeXml( curveElement, doc ); + transformerElem.appendChild( curveElement ); + } return true; } @@ -69,11 +90,35 @@ QgsPropertyTransformer* QgsPropertyTransformer::fromExpression( const QString& e return nullptr; } +double QgsPropertyTransformer::transformNumeric( double input ) const +{ + if ( !mCurveTransform ) + return input; + + if ( qgsDoubleNear( mMaxValue, mMinValue ) ) + return input; + + // convert input into target range + double scaledInput = ( input - mMinValue ) / ( mMaxValue - mMinValue ); + + return mMinValue + ( mMaxValue - mMinValue ) * mCurveTransform->y( scaledInput ); +} + bool QgsPropertyTransformer::readXml( const QDomElement &transformerElem, const QDomDocument &doc ) { Q_UNUSED( doc ); mMinValue = transformerElem.attribute( "minValue", "0.0" ).toDouble(); mMaxValue = transformerElem.attribute( "maxValue", "1.0" ).toDouble(); + mCurveTransform.reset( nullptr ); + + QDomNodeList curveNodeList = transformerElem.elementsByTagName( "curve" ); + if ( !curveNodeList.isEmpty() ) + { + QDomElement curveElem = curveNodeList.at( 0 ).toElement(); + mCurveTransform.reset( new QgsCurveTransform() ); + mCurveTransform->readXml( curveElem, doc ); + } + return true; } @@ -89,14 +134,35 @@ QgsGenericNumericTransformer::QgsGenericNumericTransformer( double minValue, dou , mExponent( exponent ) {} +QgsGenericNumericTransformer::QgsGenericNumericTransformer( const QgsGenericNumericTransformer& other ) + : QgsPropertyTransformer( other ) + , mMinOutput( other.mMinOutput ) + , mMaxOutput( other.mMaxOutput ) + , mNullOutput( other.mNullOutput ) + , mExponent( other.mExponent ) +{} + +QgsGenericNumericTransformer& QgsGenericNumericTransformer::operator=( const QgsGenericNumericTransformer & other ) +{ + QgsPropertyTransformer::operator=( other ); + mMinOutput = other.mMinOutput; + mMaxOutput = other.mMaxOutput; + mNullOutput = other.mNullOutput; + mExponent = other.mExponent; + return *this; +} + QgsGenericNumericTransformer *QgsGenericNumericTransformer::clone() { - return new QgsGenericNumericTransformer( mMinValue, - mMaxValue, - mMinOutput, - mMaxOutput, - mNullOutput, - mExponent ); + std::unique_ptr< QgsGenericNumericTransformer > t( new QgsGenericNumericTransformer( mMinValue, + mMaxValue, + mMinOutput, + mMaxOutput, + mNullOutput, + mExponent ) ); + if ( mCurveTransform ) + t->setCurveTransform( new QgsCurveTransform( *mCurveTransform ) ); + return t.release(); } bool QgsGenericNumericTransformer::writeXml( QDomElement &transformerElem, QDomDocument &doc ) const @@ -126,6 +192,7 @@ bool QgsGenericNumericTransformer::readXml( const QDomElement &transformerElem, double QgsGenericNumericTransformer::value( double input ) const { + input = transformNumeric( input ); if ( qgsDoubleNear( mExponent, 1.0 ) ) return mMinOutput + ( qBound( mMinValue, input, mMaxValue ) - mMinValue ) * ( mMaxOutput - mMinOutput ) / ( mMaxValue - mMinValue ); else @@ -257,15 +324,38 @@ QgsSizeScaleTransformer::QgsSizeScaleTransformer( ScaleType type, double minValu setType( type ); } +QgsSizeScaleTransformer::QgsSizeScaleTransformer( const QgsSizeScaleTransformer& other ) + : QgsPropertyTransformer( other ) + , mType( other.mType ) + , mMinSize( other.mMinSize ) + , mMaxSize( other.mMaxSize ) + , mNullSize( other.mNullSize ) + , mExponent( other.mExponent ) +{} + +QgsSizeScaleTransformer& QgsSizeScaleTransformer::operator=( const QgsSizeScaleTransformer & other ) +{ + QgsPropertyTransformer::operator=( other ); + mType = other.mType; + mMinSize = other.mMinSize; + mMaxSize = other.mMaxSize; + mNullSize = other.mNullSize; + mExponent = other.mExponent; + return *this; +} + QgsSizeScaleTransformer *QgsSizeScaleTransformer::clone() { - return new QgsSizeScaleTransformer( mType, - mMinValue, - mMaxValue, - mMinSize, - mMaxSize, - mNullSize, - mExponent ); + std::unique_ptr< QgsSizeScaleTransformer > t( new QgsSizeScaleTransformer( mType, + mMinValue, + mMaxValue, + mMinSize, + mMaxSize, + mNullSize, + mExponent ) ); + if ( mCurveTransform ) + t->setCurveTransform( new QgsCurveTransform( *mCurveTransform ) ); + return t.release(); } bool QgsSizeScaleTransformer::writeXml( QDomElement &transformerElem, QDomDocument &doc ) const @@ -297,6 +387,8 @@ bool QgsSizeScaleTransformer::readXml( const QDomElement &transformerElem, const double QgsSizeScaleTransformer::size( double value ) const { + value = transformNumeric( value ); + switch ( mType ) { case Linear: @@ -483,6 +575,7 @@ QgsColorRampTransformer::QgsColorRampTransformer( const QgsColorRampTransformer QgsColorRampTransformer &QgsColorRampTransformer::operator=( const QgsColorRampTransformer & other ) { + QgsPropertyTransformer::operator=( other ); mMinValue = other.mMinValue; mMaxValue = other.mMaxValue; mGradientRamp.reset( other.mGradientRamp ? other.mGradientRamp->clone() : nullptr ); @@ -493,11 +586,13 @@ QgsColorRampTransformer &QgsColorRampTransformer::operator=( const QgsColorRampT QgsColorRampTransformer* QgsColorRampTransformer::clone() { - QgsColorRampTransformer* c = new QgsColorRampTransformer( mMinValue, mMaxValue, + std::unique_ptr< QgsColorRampTransformer > c( new QgsColorRampTransformer( mMinValue, mMaxValue, mGradientRamp ? mGradientRamp->clone() : nullptr, - mNullColor ); + mNullColor ) ); c->setRampName( mRampName ); - return c; + if ( mCurveTransform ) + c->setCurveTransform( new QgsCurveTransform( *mCurveTransform ) ); + return c.release(); } bool QgsColorRampTransformer::writeXml( QDomElement &transformerElem, QDomDocument &doc ) const @@ -569,6 +664,7 @@ QString QgsColorRampTransformer::toExpression( const QString& baseExpression ) c QColor QgsColorRampTransformer::color( double value ) const { + value = transformNumeric( value ); double scaledVal = qBound( 0.0, ( value - mMinValue ) / ( mMaxValue - mMinValue ), 1.0 ); if ( !mGradientRamp ) @@ -586,3 +682,340 @@ void QgsColorRampTransformer::setColorRamp( QgsColorRamp* ramp ) { mGradientRamp.reset( ramp ); } + + +// +// QgsCurveTransform +// + +bool sortByX( const QgsPoint& a, const QgsPoint& b ) +{ + return a.x() < b.x(); +} + +QgsCurveTransform::QgsCurveTransform() +{ + mControlPoints << QgsPoint( 0, 0 ) << QgsPoint( 1, 1 ); + calcSecondDerivativeArray(); +} + +QgsCurveTransform::QgsCurveTransform( const QList& controlPoints ) + : mControlPoints( controlPoints ) +{ + std::sort( mControlPoints.begin(), mControlPoints.end(), sortByX ); + calcSecondDerivativeArray(); +} + +QgsCurveTransform::~QgsCurveTransform() +{ + delete [] mSecondDerivativeArray; +} + +QgsCurveTransform::QgsCurveTransform( const QgsCurveTransform& other ) + : mControlPoints( other.mControlPoints ) +{ + if ( other.mSecondDerivativeArray ) + { + mSecondDerivativeArray = new double[ mControlPoints.count()]; + memcpy( mSecondDerivativeArray, other.mSecondDerivativeArray, sizeof( double ) * mControlPoints.count() ); + } +} + +QgsCurveTransform& QgsCurveTransform::operator=( const QgsCurveTransform & other ) +{ + mControlPoints = other.mControlPoints; + if ( other.mSecondDerivativeArray ) + { + delete [] mSecondDerivativeArray; + mSecondDerivativeArray = new double[ mControlPoints.count()]; + memcpy( mSecondDerivativeArray, other.mSecondDerivativeArray, sizeof( double ) * mControlPoints.count() ); + } + return *this; +} + +void QgsCurveTransform::setControlPoints( const QList& points ) +{ + mControlPoints = points; + std::sort( mControlPoints.begin(), mControlPoints.end(), sortByX ); + for ( int i = 0; i < mControlPoints.count(); ++i ) + { + mControlPoints[ i ] = QgsPoint( qBound( 0.0, mControlPoints.at( i ).x(), 1.0 ), + qBound( 0.0, mControlPoints.at( i ).y(), 1.0 ) ); + } + calcSecondDerivativeArray(); +} + +void QgsCurveTransform::addControlPoint( double x, double y ) +{ + QgsPoint point( x, y ); + if ( mControlPoints.contains( point ) ) + return; + + mControlPoints << point; + std::sort( mControlPoints.begin(), mControlPoints.end(), sortByX ); + calcSecondDerivativeArray(); +} + +void QgsCurveTransform::removeControlPoint( double x, double y ) +{ + for ( int i = 0; i < mControlPoints.count(); ++i ) + { + if ( qgsDoubleNear( mControlPoints.at( i ).x(), x ) + && qgsDoubleNear( mControlPoints.at( i ).y(), y ) ) + { + mControlPoints.removeAt( i ); + break; + } + } + calcSecondDerivativeArray(); +} + +// this code is adapted from https://github.com/OpenFibers/Photoshop-Curves +// which in turn was adapted from +// http://www.developpez.net/forums/d331608-3/autres-langages/algorithmes/contribuez/image-interpolation-spline-cubique/#post3513925 //#spellok + +double QgsCurveTransform::y( double x ) const +{ + int n = mControlPoints.count(); + if ( n < 2 ) + return qBound( 0.0, x, 1.0 ); // invalid + else if ( n < 3 ) + { + // linear + if ( x <= mControlPoints.at( 0 ).x() ) + return qBound( 0.0, mControlPoints.at( 0 ).y(), 1.0 ); + else if ( x >= mControlPoints.at( n - 1 ).x() ) + return qBound( 0.0, mControlPoints.at( 1 ).y(), 1.0 ); + else + { + double dx = mControlPoints.at( 1 ).x() - mControlPoints.at( 0 ).x(); + double dy = mControlPoints.at( 1 ).y() - mControlPoints.at( 0 ).y(); + return qBound( 0.0, ( x - mControlPoints.at( 0 ).x() ) * ( dy / dx ) + mControlPoints.at( 0 ).y(), 1.0 ); + } + } + + // safety check + if ( x <= mControlPoints.at( 0 ).x() ) + return qBound( 0.0, mControlPoints.at( 0 ).y(), 1.0 ); + if ( x >= mControlPoints.at( n - 1 ).x() ) + return qBound( 0.0, mControlPoints.at( n - 1 ).y(), 1.0 ); + + // find corresponding segment + QList::const_iterator pointIt = mControlPoints.constBegin(); + QgsPoint currentControlPoint = *pointIt; + ++pointIt; + QgsPoint nextControlPoint = *pointIt; + + for ( int i = 0; i < n - 1; ++i ) + { + if ( x < nextControlPoint.x() ) + { + // found segment + double h = nextControlPoint.x() - currentControlPoint.x(); + double t = ( x - currentControlPoint.x() ) / h; + + double a = 1 - t; + + return qBound( 0.0, a*currentControlPoint.y() + t*nextControlPoint.y() + ( h*h / 6 )*(( a*a*a - a )*mSecondDerivativeArray[i] + ( t*t*t - t )*mSecondDerivativeArray[i+1] ), + 1.0 ); + } + + ++pointIt; + if ( pointIt == mControlPoints.constEnd() ) + break; + + currentControlPoint = nextControlPoint; + nextControlPoint = *pointIt; + } + + //should not happen + return qBound( 0.0, x, 1.0 ); +} + +// this code is adapted from https://github.com/OpenFibers/Photoshop-Curves +// which in turn was adapted from +// http://www.developpez.net/forums/d331608-3/autres-langages/algorithmes/contribuez/image-interpolation-spline-cubique/#post3513925 //#spellok + +QVector QgsCurveTransform::y( const QVector& x ) const +{ + QVector result; + + int n = mControlPoints.count(); + if ( n < 3 ) + { + // invalid control points - use simple transform + Q_FOREACH ( double i, x ) + result << y( i ); + + return result; + } + + // find corresponding segment + QList::const_iterator pointIt = mControlPoints.constBegin(); + QgsPoint currentControlPoint = *pointIt; + ++pointIt; + QgsPoint nextControlPoint = *pointIt; + + int xIndex = 0; + double currentX = x.at( xIndex ); + // safety check + while ( currentX <= currentControlPoint.x() ) + { + result << qBound( 0.0, currentControlPoint.y(), 1.0 ); + xIndex++; + currentX = x.at( xIndex ); + } + + for ( int i = 0; i < n - 1; ++i ) + { + while ( currentX < nextControlPoint.x() ) + { + // found segment + double h = nextControlPoint.x() - currentControlPoint.x(); + + double t = ( currentX - currentControlPoint.x() ) / h; + + double a = 1 - t; + + result << qBound( 0.0, a*currentControlPoint.y() + t*nextControlPoint.y() + ( h*h / 6 )*(( a*a*a - a )*mSecondDerivativeArray[i] + ( t*t*t - t )*mSecondDerivativeArray[i+1] ), 1.0 ); + xIndex++; + if ( xIndex == x.count() ) + return result; + + currentX = x.at( xIndex ); + } + + ++pointIt; + if ( pointIt == mControlPoints.constEnd() ) + break; + + currentControlPoint = nextControlPoint; + nextControlPoint = *pointIt; + } + + // safety check + while ( xIndex < x.count() ) + { + result << qBound( 0.0, nextControlPoint.y(), 1.0 ); + xIndex++; + } + + return result; +} + +bool QgsCurveTransform::readXml( const QDomElement& elem, const QDomDocument& ) +{ + QString xString = elem.attribute( QStringLiteral( "x" ) ); + QString yString = elem.attribute( QStringLiteral( "y" ) ); + + QStringList xVals = xString.split( ',' ); + QStringList yVals = yString.split( ',' ); + if ( xVals.count() != yVals.count() ) + return false; + + QList< QgsPoint > newPoints; + bool ok = false; + for ( int i = 0; i < xVals.count(); ++i ) + { + double x = xVals.at( i ).toDouble( &ok ); + if ( !ok ) + return false; + double y = yVals.at( i ).toDouble( &ok ); + if ( !ok ) + return false; + newPoints << QgsPoint( x, y ); + } + setControlPoints( newPoints ); + return true; +} + +bool QgsCurveTransform::writeXml( QDomElement& transformElem, QDomDocument& ) const +{ + QStringList x; + QStringList y; + Q_FOREACH ( const QgsPoint& p, mControlPoints ) + { + x << qgsDoubleToString( p.x() ); + y << qgsDoubleToString( p.y() ); + } + + transformElem.setAttribute( QStringLiteral( "x" ), x.join( ',' ) ); + transformElem.setAttribute( QStringLiteral( "y" ), y.join( ',' ) ); + + return true; +} + +// this code is adapted from https://github.com/OpenFibers/Photoshop-Curves +// which in turn was adapted from +// http://www.developpez.net/forums/d331608-3/autres-langages/algorithmes/contribuez/image-interpolation-spline-cubique/#post3513925 //#spellok + +void QgsCurveTransform::calcSecondDerivativeArray() +{ + int n = mControlPoints.count(); + if ( n < 3 ) + return; // cannot proceed + + delete[] mSecondDerivativeArray; + + double* matrix = new double[ n * 3 ]; + double* result = new double[ n ]; + matrix[0] = 0; + matrix[1] = 1; + matrix[2] = 0; + result[0] = 0; + QList::const_iterator pointIt = mControlPoints.constBegin(); + QgsPoint pointIm1 = *pointIt; + ++pointIt; + QgsPoint pointI = *pointIt; + ++pointIt; + QgsPoint pointIp1 = *pointIt; + + for ( int i = 1; i < n - 1; ++i ) + { + matrix[i * 3 + 0 ] = ( pointI.x() - pointIm1.x() ) / 6.0; + matrix[i * 3 + 1 ] = ( pointIp1.x() - pointIm1.x() ) / 3.0; + matrix[i * 3 + 2 ] = ( pointIp1.x() - pointI.x() ) / 6.0; + result[i] = ( pointIp1.y() - pointI.y() ) / ( pointIp1.x() - pointI.x() ) - ( pointI.y() - pointIm1.y() ) / ( pointI.x() - pointIm1.x() ); + + // shuffle points + pointIm1 = pointI; + pointI = pointIp1; + ++pointIt; + if ( pointIt == mControlPoints.constEnd() ) + break; + + pointIp1 = *pointIt; + } + matrix[( n-1 )*3 + 0] = 0; + matrix[( n-1 )*3 + 1] = 1; + matrix[( n-1 ) * 3 +2] = 0; + result[n-1] = 0; + + // solving pass1 (up->down) + for ( int i = 1; i < n; ++i ) + { + double k = matrix[i * 3 + 0] / matrix[( i-1 ) * 3 + 1]; + matrix[i * 3 + 1] -= k * matrix[( i-1 )*3+2]; + matrix[i * 3 + 0] = 0; + result[i] -= k * result[i-1]; + } + // solving pass2 (down->up) + for ( int i = n - 2; i >= 0; --i ) + { + double k = matrix[i*3+2] / matrix[( i+1 )*3+1]; + matrix[i*3+1] -= k * matrix[( i+1 )*3+0]; + matrix[i*3+2] = 0; + result[i] -= k * result[i+1]; + } + + // return second derivative value for each point + mSecondDerivativeArray = new double[n]; + for ( int i = 0;i < n;++i ) + { + mSecondDerivativeArray[i] = result[i] / matrix[( i*3 )+1]; + } + + delete[] result; + delete[] matrix; +} + diff --git a/src/core/qgspropertytransformer.h b/src/core/qgspropertytransformer.h index e5cc10f1bea..cd7c4c3d353 100644 --- a/src/core/qgspropertytransformer.h +++ b/src/core/qgspropertytransformer.h @@ -18,6 +18,7 @@ #include "qgis_core.h" #include "qgsexpression.h" #include "qgsexpressioncontext.h" +#include "qgspoint.h" #include #include #include @@ -26,9 +27,123 @@ #include #include #include +#include class QgsColorRamp; + +/** + * \ingroup core + * \class QgsCurveTransform + * \brief Handles scaling of input values to output values by using a curve created + * from smoothly joining a number of set control points. + * + * QgsCurveTransform assists in creation of curve type transforms, typically seen in + * raster image editing software (eg the curves dialog in GIMP or Photoshop). + * Transforms are created by passing a number of set control points through which + * the transform curve must pass. The curve is guaranteed to exactly pass through + * these control points. Between control points the curve is smoothly interpolated + * so that no disjoint sections or "corners" are present. + * + * If the first or last control point are not located at x = 0 and x = 1 respectively, + * then values outside this range will be mapped to the y value of either the first + * or last control point. In other words, the curve will have a flat segment + * for values outside of the control point range. + * + * \note Added in version 3.0 + */ + +class CORE_EXPORT QgsCurveTransform +{ + public: + + /** + * Constructs a default QgsCurveTransform which linearly maps values + * between 0 and 1 unchanged. I.e. y == x. + */ + QgsCurveTransform(); + + /** + * Constructs a QgsCurveTransform using a specified list of \a controlPoints. + * Behavior is undefined if duplicate x values exist in the control points + * list. + */ + QgsCurveTransform( const QList< QgsPoint >& controlPoints ); + + ~QgsCurveTransform(); + + /** + * Copy constructor + */ + QgsCurveTransform( const QgsCurveTransform& other ); + + QgsCurveTransform& operator=( const QgsCurveTransform& other ); + + /** + * Returns a list of the control points for the transform. + * @see setControlPoints() + */ + QList< QgsPoint > controlPoints() const { return mControlPoints; } + + /** + * Sets the list of control points for the transform. Any existing + * points are removed. + * @see controlPoints() + */ + void setControlPoints( const QList< QgsPoint >& points ); + + /** + * Adds a control point to the transform. Behavior is undefined if duplicate + * x values exist in the control points list. + * @see removeControlPoint() + */ + void addControlPoint( double x, double y ); + + /** + * Removes a control point from the transform. This will have no effect if a + * matching control point does not exist. + * @see addControlPoint() + */ + void removeControlPoint( double x, double y ); + + /** + * Returns the mapped y value corresponding to the specified \a x value. + */ + double y( double x ) const; + + /** + * Returns a list of y values corresponding to a list of \a x values. + * Calling this method is faster then calling the double variant multiple + * times. + */ + QVector< double > y( const QVector< double >& x ) const; + + /** + * Reads the curve's state from an XML element. + * @param elem source DOM element for transform's state + * @param doc DOM document + * @see writeXml() + */ + bool readXml( const QDomElement& elem, const QDomDocument& doc ); + + /** + * Writes the current state of the transform into an XML element + * @param transformElem destination element for the transform's state + * @param doc DOM document + * @see readXml() + */ + bool writeXml( QDomElement& transformElem, QDomDocument& doc ) const; + + private: + + void calcSecondDerivativeArray(); + + QList< QgsPoint > mControlPoints; + + double* mSecondDerivativeArray = nullptr; +}; + + /** * \ingroup core * \class QgsPropertyTransformer @@ -62,6 +177,12 @@ class CORE_EXPORT QgsPropertyTransformer */ QgsPropertyTransformer( double minValue = 0.0, double maxValue = 1.0 ); + /** + * Copy constructor. + */ + QgsPropertyTransformer( const QgsPropertyTransformer& other ); + QgsPropertyTransformer& operator=( const QgsPropertyTransformer& other ); + virtual ~QgsPropertyTransformer() = default; /** @@ -120,6 +241,21 @@ class CORE_EXPORT QgsPropertyTransformer */ void setMaxValue( double max ) { mMaxValue = max; } + /** + * Returns the curve transform applied to input values before they are transformed + * by the individual transform subclasses. + * @see setCurveTransform() + */ + QgsCurveTransform* curveTransform() const { return mCurveTransform.get(); } + + /** + * Sets a curve transform to apply to input values before they are transformed + * by the individual transform subclasses. Ownership of \a transform is transferred + * to the property transformer. + * @see curveTransform() + */ + void setCurveTransform( QgsCurveTransform* transform ) { mCurveTransform.reset( transform ); } + /** * Calculates the transform of a value. Derived classes must implement this to perform their transformations * on input values @@ -156,6 +292,15 @@ class CORE_EXPORT QgsPropertyTransformer //! Maximum value expected by the transformer double mMaxValue; + //! Optional curve transform + std::unique_ptr< QgsCurveTransform > mCurveTransform; + + /** + * Applies base class numeric transformations. Derived classes should call this + * to transform an \a input numeric value before they apply any transform to the result. + * This applies any curve transforms which may exist on the transformer. + */ + double transformNumeric( double input ) const; }; /** @@ -185,6 +330,12 @@ class CORE_EXPORT QgsGenericNumericTransformer : public QgsPropertyTransformer double nullOutput = 0.0, double exponent = 1.0 ); + /** + * Copy constructor. + */ + QgsGenericNumericTransformer( const QgsGenericNumericTransformer& other ); + QgsGenericNumericTransformer& operator=( const QgsGenericNumericTransformer& other ); + virtual Type transformerType() const override { return GenericNumericTransformer; } virtual QgsGenericNumericTransformer* clone() override; virtual bool writeXml( QDomElement& transformerElem, QDomDocument& doc ) const override; @@ -315,6 +466,12 @@ class CORE_EXPORT QgsSizeScaleTransformer : public QgsPropertyTransformer double nullSize = 0.0, double exponent = 1.0 ); + /** + * Copy constructor. + */ + QgsSizeScaleTransformer( const QgsSizeScaleTransformer& other ); + QgsSizeScaleTransformer& operator=( const QgsSizeScaleTransformer& other ); + virtual Type transformerType() const override { return SizeScaleTransformer; } virtual QgsSizeScaleTransformer* clone() override; virtual bool writeXml( QDomElement& transformerElem, QDomDocument& doc ) const override; diff --git a/src/core/qgsvectorlayer.cpp b/src/core/qgsvectorlayer.cpp index d0bb199db73..151a330463b 100644 --- a/src/core/qgsvectorlayer.cpp +++ b/src/core/qgsvectorlayer.cpp @@ -3565,7 +3565,7 @@ QVariant QgsVectorLayer::aggregate( QgsAggregateCalculator::Aggregate aggregate, return c.calculate( aggregate, fieldOrExpression, context, ok ); } -QList QgsVectorLayer::getValues( const QString &fieldOrExpression, bool& ok, bool selectedOnly ) const +QList QgsVectorLayer::getValues( const QString &fieldOrExpression, bool& ok, bool selectedOnly, QgsFeedback* feedback ) const { QList values; @@ -3623,12 +3623,17 @@ QList QgsVectorLayer::getValues( const QString &fieldOrExpression, boo { values << f.attribute( attrNum ); } + if ( feedback && feedback->isCanceled() ) + { + ok = false; + return values; + } } ok = true; return values; } -QList QgsVectorLayer::getDoubleValues( const QString &fieldOrExpression, bool& ok, bool selectedOnly, int* nullCount ) const +QList QgsVectorLayer::getDoubleValues( const QString &fieldOrExpression, bool& ok, bool selectedOnly, int* nullCount, QgsFeedback* feedback ) const { QList values; @@ -3650,6 +3655,11 @@ QList QgsVectorLayer::getDoubleValues( const QString &fieldOrExpression, if ( nullCount ) *nullCount += 1; } + if ( feedback && feedback->isCanceled() ) + { + ok = false; + return values; + } } ok = true; return values; diff --git a/src/core/qgsvectorlayer.h b/src/core/qgsvectorlayer.h index 7712132a9e7..0c90768c592 100644 --- a/src/core/qgsvectorlayer.h +++ b/src/core/qgsvectorlayer.h @@ -1497,11 +1497,12 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte * @param fieldOrExpression field name or an expression string * @param ok will be set to false if field or expression is invalid, otherwise true * @param selectedOnly set to true to get values from selected features only + * @param feedback optional feedback object to allow cancelation * @returns list of fetched values * @note added in QGIS 2.9 * @see getDoubleValues */ - QList< QVariant > getValues( const QString &fieldOrExpression, bool &ok, bool selectedOnly = false ) const; + QList< QVariant > getValues( const QString &fieldOrExpression, bool &ok, bool selectedOnly = false, QgsFeedback* feedback = nullptr ) const; /** Fetches all double values from a specified field name or expression. Null values or * invalid expression results are skipped. @@ -1509,11 +1510,12 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte * @param ok will be set to false if field or expression is invalid, otherwise true * @param selectedOnly set to true to get values from selected features only * @param nullCount optional pointer to integer to store number of null values encountered in + * @param feedback optional feedback object to allow cancelation * @returns list of fetched values * @note added in QGIS 2.9 * @see getValues */ - QList< double > getDoubleValues( const QString &fieldOrExpression, bool &ok, bool selectedOnly = false, int* nullCount = nullptr ) const; + QList< double > getDoubleValues( const QString &fieldOrExpression, bool &ok, bool selectedOnly = false, int* nullCount = nullptr, QgsFeedback* feedback = nullptr ) const; //! Set the blending mode used for rendering each feature void setFeatureBlendMode( QPainter::CompositionMode blendMode ); diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index f829bed14a6..b1c93d4529f 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -193,6 +193,7 @@ SET(QGIS_GUI_SRCS qgscredentialdialog.cpp qgscursors.cpp qgscustomdrophandler.cpp + qgscurveeditorwidget.cpp qgsdatumtransformdialog.cpp qgsdetaileditemdata.cpp qgsdetaileditemdelegate.cpp @@ -345,6 +346,7 @@ SET(QGIS_GUI_MOC_HDRS qgscompoundcolorwidget.h qgsconfigureshortcutsdialog.h qgscredentialdialog.h + qgscurveeditorwidget.h qgsdatumtransformdialog.h qgsdetaileditemdelegate.h qgsdetaileditemwidget.h diff --git a/src/gui/qgscurveeditorwidget.cpp b/src/gui/qgscurveeditorwidget.cpp new file mode 100644 index 00000000000..5dd724d4b74 --- /dev/null +++ b/src/gui/qgscurveeditorwidget.cpp @@ -0,0 +1,481 @@ +/*************************************************************************** + qgscurveeditorwidget.cpp + ------------------------ + begin : February 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + + +#include "qgscurveeditorwidget.h" + +#include +#include +#include +#include +#include + +// QWT Charting widget +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(QWT_VERSION) && QWT_VERSION>=0x060000 +#include +#include +#else +#include "../raster/qwt5_histogram_item.h" +#endif + +QgsCurveEditorWidget::QgsCurveEditorWidget( QWidget* parent, const QgsCurveTransform& transform ) + : QWidget( parent ) + , mCurve( transform ) + , mCurrentPlotMarkerIndex( -1 ) +{ + mPlot = new QwtPlot(); + mPlot->setMinimumSize( QSize( 0, 100 ) ); + mPlot->setAxisScale( QwtPlot::yLeft, 0, 1 ); + mPlot->setAxisScale( QwtPlot::yRight, 0, 1 ); + mPlot->setAxisScale( QwtPlot::xBottom, 0, 1 ); + mPlot->setAxisScale( QwtPlot::xTop, 0, 1 ); + + QVBoxLayout* vlayout = new QVBoxLayout(); + vlayout->addWidget( mPlot ); + setLayout( vlayout ); + + // hide the ugly canvas frame + mPlot->setFrameStyle( QFrame::NoFrame ); +#if defined(QWT_VERSION) && QWT_VERSION>=0x060000 + QFrame* plotCanvasFrame = dynamic_cast( mPlot->canvas() ); + if ( plotCanvasFrame ) + plotCanvasFrame->setFrameStyle( QFrame::NoFrame ); +#else + mPlot->canvas()->setFrameStyle( QFrame::NoFrame ); +#endif + + mPlot->enableAxis( QwtPlot::yLeft, false ); + mPlot->enableAxis( QwtPlot::xBottom, false ); + + // add a grid + QwtPlotGrid * grid = new QwtPlotGrid(); + QwtScaleDiv gridDiv( 0.0, 1.0, QList(), QList(), QList() << 0.2 << 0.4 << 0.6 << 0.8 ); + grid->setXDiv( gridDiv ); + grid->setYDiv( gridDiv ); + grid->setPen( QPen( QColor( 0, 0, 0, 50 ) ) ); + grid->attach( mPlot ); + + mPlotCurve = new QwtPlotCurve(); + mPlotCurve->setTitle( QStringLiteral( "Curve" ) ); + mPlotCurve->setPen( QPen( QColor( 30, 30, 30 ), 0.0 ) ), + mPlotCurve->setRenderHint( QwtPlotItem::RenderAntialiased, true ); + mPlotCurve->attach( mPlot ); + + mPlotFilter = new QgsCurveEditorPlotEventFilter( mPlot ); + connect( mPlotFilter, &QgsCurveEditorPlotEventFilter::mousePress, this, &QgsCurveEditorWidget::plotMousePress ); + connect( mPlotFilter, &QgsCurveEditorPlotEventFilter::mouseRelease, this, &QgsCurveEditorWidget::plotMouseRelease ); + connect( mPlotFilter, &QgsCurveEditorPlotEventFilter::mouseMove, this, &QgsCurveEditorWidget::plotMouseMove ); + + mPlotCurve->setVisible( true ); + updatePlot(); +} + +QgsCurveEditorWidget::~QgsCurveEditorWidget() +{ + if ( mGatherer && mGatherer->isRunning() ) + { + connect( mGatherer.get(), &QgsHistogramValuesGatherer::finished, mGatherer.get(), &QgsHistogramValuesGatherer::deleteLater ); + mGatherer->stop(); + ( void )mGatherer.release(); + } +} + +void QgsCurveEditorWidget::setCurve( const QgsCurveTransform& curve ) +{ + mCurve = curve; + updatePlot(); + emit changed(); +} + +void QgsCurveEditorWidget::setHistogramSource( const QgsVectorLayer* layer, const QString& expression ) +{ + if ( !mGatherer ) + { + mGatherer.reset( new QgsHistogramValuesGatherer() ); + connect( mGatherer.get(), &QgsHistogramValuesGatherer::calculatedHistogram, this, [=] + { + mHistogram.reset( new QgsHistogram( mGatherer->histogram() ) ); + updateHistogram(); + } ); + } + + bool changed = mGatherer->layer() != layer || mGatherer->expression() != expression; + if ( changed ) + { + mGatherer->setExpression( expression ); + mGatherer->setLayer( layer ); + mGatherer->start(); + if ( mGatherer->isRunning() ) + { + //stop any currently running task + mGatherer->stop(); + while ( mGatherer->isRunning() ) + { + QCoreApplication::processEvents(); + } + } + mGatherer->start(); + } + else + { + updateHistogram(); + } +} + +void QgsCurveEditorWidget::setMinHistogramValueRange( double minValueRange ) +{ + mMinValueRange = minValueRange; + updateHistogram(); +} + +void QgsCurveEditorWidget::setMaxHistogramValueRange( double maxValueRange ) +{ + mMaxValueRange = maxValueRange; + updateHistogram(); +} + +void QgsCurveEditorWidget::keyPressEvent( QKeyEvent* event ) +{ + if ( event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace ) + { + QList< QgsPoint > cp = mCurve.controlPoints(); + if ( mCurrentPlotMarkerIndex > 0 && mCurrentPlotMarkerIndex < cp.count() - 1 ) + { + cp.removeAt( mCurrentPlotMarkerIndex ); + mCurve.setControlPoints( cp ); + updatePlot(); + emit changed(); + } + } +} + +void QgsCurveEditorWidget::plotMousePress( QPointF point ) +{ + mCurrentPlotMarkerIndex = findNearestControlPoint( point ); + if ( mCurrentPlotMarkerIndex < 0 ) + { + // add a new point + mCurve.addControlPoint( point.x(), point.y() ); + mCurrentPlotMarkerIndex = findNearestControlPoint( point ); + emit changed(); + } + updatePlot(); +} + + +int QgsCurveEditorWidget::findNearestControlPoint( QPointF point ) const +{ + double minDist = 3.0 / mPlot->width(); + int currentPlotMarkerIndex = -1; + + QList< QgsPoint > controlPoints = mCurve.controlPoints(); + + for ( int i = 0; i < controlPoints.count(); ++i ) + { + QgsPoint currentPoint = controlPoints.at( i ); + double currentDist; + currentDist = qPow( point.x() - currentPoint.x(), 2.0 ) + qPow( point.y() - currentPoint.y(), 2.0 ); + if ( currentDist < minDist ) + { + minDist = currentDist; + currentPlotMarkerIndex = i; + } + } + return currentPlotMarkerIndex; +} + + +void QgsCurveEditorWidget::plotMouseRelease( QPointF ) +{ +} + +void QgsCurveEditorWidget::plotMouseMove( QPointF point ) +{ + if ( mCurrentPlotMarkerIndex < 0 ) + return; + + QList< QgsPoint > cp = mCurve.controlPoints(); + bool removePoint = false; + if ( mCurrentPlotMarkerIndex == 0 ) + { + point.setX( qMin( point.x(), cp.at( 1 ).x() - 0.01 ) ); + } + else + { + removePoint = point.x() <= cp.at( mCurrentPlotMarkerIndex - 1 ).x(); + } + if ( mCurrentPlotMarkerIndex == cp.count() - 1 ) + { + point.setX( qMax( point.x(), cp.at( mCurrentPlotMarkerIndex - 1 ).x() + 0.01 ) ); + removePoint = false; + } + else + { + removePoint = removePoint || point.x() >= cp.at( mCurrentPlotMarkerIndex + 1 ).x(); + } + + if ( removePoint ) + { + cp.removeAt( mCurrentPlotMarkerIndex ); + mCurrentPlotMarkerIndex = -1; + } + else + { + cp[ mCurrentPlotMarkerIndex ] = QgsPoint( point.x(), point.y() ); + } + mCurve.setControlPoints( cp ); + updatePlot(); + emit changed(); +} + +void QgsCurveEditorWidget::addPlotMarker( double x, double y, bool isSelected ) +{ + QColor borderColor( 0, 0, 0 ); + + QColor brushColor = isSelected ? borderColor : QColor( 255, 255, 255, 0 ); + + QwtPlotMarker *marker = new QwtPlotMarker(); +#if defined(QWT_VERSION) && QWT_VERSION>=0x060000 + marker->setSymbol( new QwtSymbol( QwtSymbol::Ellipse, QBrush( brushColor ), QPen( borderColor, isSelected ? 2 : 1 ), QSize( 8, 8 ) ) ); +#else + marker->setSymbol( QwtSymbol( QwtSymbol::Ellipse, QBrush( brushColor ), QPen( borderColor, isSelected ? 2 : 1 ), QSize( 8, 8 ) ) ); +#endif + marker->setValue( x, y ); + marker->attach( mPlot ); + marker->setRenderHint( QwtPlotItem::RenderAntialiased, true ); + mMarkers << marker; +} + +void QgsCurveEditorWidget::updateHistogram() +{ + if ( !mHistogram ) + return; + + //draw histogram + QBrush histoBrush( QColor( 0, 0, 0, 70 ) ); + +#if defined(QWT_VERSION) && QWT_VERSION>=0x060000 + delete mPlotHistogram; + mPlotHistogram = createPlotHistogram( histoBrush ); + QVector dataHisto; +#else + delete mPlotHistogramItem; + mPlotHistogramItem = createHistoItem( histoBrush ); + QwtArray intervalsHisto; + QwtArray valuesHisto; +#endif + + int bins = 40; + QList edges = mHistogram->binEdges( bins ); + QList counts = mHistogram->counts( bins ); + + // scale counts to 0->1 + double max = *std::max_element( counts.constBegin(), counts.constEnd() ); + + // scale bin edges to fit in 0->1 range + if ( !qgsDoubleNear( mMinValueRange, mMaxValueRange ) ) + { + std::transform( edges.begin(), edges.end(), edges.begin(), + [this]( double d ) -> double { return ( d - mMinValueRange ) / ( mMaxValueRange - mMinValueRange ); } ); + } + + for ( int bin = 0; bin < bins; ++bin ) + { + double binValue = counts.at( bin ) / max; + + double upperEdge = edges.at( bin + 1 ); + +#if defined(QWT_VERSION) && QWT_VERSION>=0x060000 + dataHisto << QwtIntervalSample( binValue, edges.at( bin ), upperEdge ); +#else + intervalsHisto.append( QwtDoubleInterval( edges.at( bin ), upperEdge ) ); + valuesHisto.append( double( binValue ) ); +#endif + } + +#if defined(QWT_VERSION) && QWT_VERSION>=0x060000 + mPlotHistogram->setSamples( dataHisto ); + mPlotHistogram->attach( mPlot ); +#else + mPlotHistogramItem->setData( QwtIntervalData( intervalsHisto, valuesHisto ) ); + mPlotHistogramItem->attach( mPlot ); +#endif + mPlot->replot(); +} + +void QgsCurveEditorWidget::updatePlot() +{ + // remove existing markers + Q_FOREACH ( QwtPlotMarker* marker, mMarkers ) + { + marker->detach(); + delete marker; + } + mMarkers.clear(); + + QPolygonF curvePoints; + QVector< double > x; + + int i = 0; + Q_FOREACH ( const QgsPoint& point, mCurve.controlPoints() ) + { + x << point.x(); + addPlotMarker( point.x(), point.y(), mCurrentPlotMarkerIndex == i ); + i++; + } + + //add extra intermediate points + + for ( double p = 0; p <= 1.0; p += 0.01 ) + { + x << p; + } + std::sort( x.begin(), x.end() ); + QVector< double > y = mCurve.y( x ); + + for ( int j = 0; j < x.count(); ++j ) + { + curvePoints << QPointF( x.at( j ), y.at( j ) ); + } + +#if defined(QWT_VERSION) && QWT_VERSION>=0x060000 + mPlotCurve->setSamples( curvePoints ); +#else + mPlotCurve->setData( curvePoints ); +#endif + mPlot->replot(); +} + + +#if defined(QWT_VERSION) && QWT_VERSION>=0x060000 +QwtPlotHistogram* QgsCurveEditorWidget::createPlotHistogram( const QBrush& brush, const QPen& pen ) const +{ + QwtPlotHistogram* histogram = new QwtPlotHistogram( QString() ); + histogram->setBrush( brush ); + if ( pen != Qt::NoPen ) + { + histogram->setPen( pen ); + } + else if ( brush.color().lightness() > 200 ) + { + QPen p; + p.setColor( brush.color().darker( 150 ) ); + p.setWidth( 0 ); + p.setCosmetic( true ); + histogram->setPen( p ); + } + else + { + histogram->setPen( QPen( Qt::NoPen ) ); + } + return histogram; +} +#else +HistogramItem * QgsCurveEditorWidget::createHistoItem( const QBrush& brush, const QPen& pen ) const +{ + HistogramItem* item = new HistogramItem( QString() ); + item->setColor( brush.color() ); + item->setFlat( true ); + item->setSpacing( 0 ); + if ( pen != Qt::NoPen ) + { + item->setPen( pen ); + } + else if ( brush.color().lightness() > 200 ) + { + QPen p; + p.setColor( brush.color().darker( 150 ) ); + p.setWidth( 0 ); + p.setCosmetic( true ); + item->setPen( p ); + } + return item; +} +#endif + +/// @cond PRIVATE + +QgsCurveEditorPlotEventFilter::QgsCurveEditorPlotEventFilter( QwtPlot *plot ) + : QObject( plot ) + , mPlot( plot ) +{ + mPlot->canvas()->installEventFilter( this ); +} + +bool QgsCurveEditorPlotEventFilter::eventFilter( QObject *object, QEvent *event ) +{ + if ( !mPlot->isEnabled() ) + return QObject::eventFilter( object, event ); + + switch ( event->type() ) + { + case QEvent::MouseButtonPress: + { + const QMouseEvent* mouseEvent = static_cast( event ); + if ( mouseEvent->button() == Qt::LeftButton ) + { + emit mousePress( mapPoint( mouseEvent->pos() ) ); + } + break; + } + case QEvent::MouseMove: + { + const QMouseEvent* mouseEvent = static_cast( event ); + if ( mouseEvent->buttons() & Qt::LeftButton ) + { + // only emit when button pressed + emit mouseMove( mapPoint( mouseEvent->pos() ) ); + } + break; + } + case QEvent::MouseButtonRelease: + { + const QMouseEvent* mouseEvent = static_cast( event ); + if ( mouseEvent->button() == Qt::LeftButton ) + { + emit mouseRelease( mapPoint( mouseEvent->pos() ) ); + } + break; + } + default: + break; + } + + return QObject::eventFilter( object, event ); +} + +QPointF QgsCurveEditorPlotEventFilter::mapPoint( QPointF point ) const +{ + if ( !mPlot ) + return QPointF(); + + return QPointF( mPlot->canvasMap( QwtPlot::xBottom ).invTransform( point.x() ), + mPlot->canvasMap( QwtPlot::yLeft ).invTransform( point.y() ) ); +} + + +///@endcond diff --git a/src/gui/qgscurveeditorwidget.h b/src/gui/qgscurveeditorwidget.h new file mode 100644 index 00000000000..62d8d17e9a8 --- /dev/null +++ b/src/gui/qgscurveeditorwidget.h @@ -0,0 +1,282 @@ +/*************************************************************************** + qgscurveeditorwidget.h + ---------------------- + begin : February 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSCURVEEDITORWIDGET_H +#define QGSCURVEEDITORWIDGET_H + +#include +#include +#include +#include +#include +#include +#include "qgis_gui.h" +#include "qgspropertytransformer.h" +#include "qgshistogram.h" +#include "qgsvectorlayer.h" + +class QwtPlot; +class QwtPlotCurve; +class QwtPlotMarker; +class QwtPlotHistogram; +class HistogramItem; +class QgsCurveEditorPlotEventFilter; + +// fix for qwt5/qwt6 QwtDoublePoint vs. QPointF +#if defined(QWT_VERSION) && QWT_VERSION>=0x060000 +typedef QPointF QwtDoublePoint; +#endif + +// just internal guff - definitely not for exposing to public API! +///@cond PRIVATE + +/** \class QgsHistogramValuesGatherer + * Calculates a histogram in a thread. + */ +class QgsHistogramValuesGatherer: public QThread +{ + Q_OBJECT + + public: + QgsHistogramValuesGatherer() = default; + + virtual void run() override + { + mWasCanceled = false; + if ( mExpression.isEmpty() || !mLayer ) + { + mHistogram.setValues( QList() ); + return; + } + + // allow responsive cancelation + mFeedback = new QgsFeedback(); + + mHistogram.setValues( mLayer, mExpression, mFeedback ); + + // be overly cautious - it's *possible* stop() might be called between deleting mFeedback and nulling it + mFeedbackMutex.lock(); + delete mFeedback; + mFeedback = nullptr; + mFeedbackMutex.unlock(); + + emit calculatedHistogram(); + } + + //! Informs the gatherer to immediately stop collecting values + void stop() + { + // be cautious, in case gatherer stops naturally just as we are canceling it and mFeedback gets deleted + mFeedbackMutex.lock(); + if ( mFeedback ) + mFeedback->cancel(); + mFeedbackMutex.unlock(); + + mWasCanceled = true; + } + + //! Returns true if collection was canceled before completion + bool wasCanceled() const { return mWasCanceled; } + + const QgsHistogram& histogram() const { return mHistogram; } + + const QgsVectorLayer* layer() const + { + return mLayer; + } + void setLayer( const QgsVectorLayer* layer ) + { + mLayer = const_cast< QgsVectorLayer* >( layer ); + } + + QString expression() const + { + return mExpression; + } + void setExpression( const QString& expression ) + { + mExpression = expression; + } + + signals: + + /** + * Emitted when histogram has been calculated + */ + void calculatedHistogram(); + + private: + + QPointer< const QgsVectorLayer > mLayer = nullptr; + QString mExpression; + QgsHistogram mHistogram; + QgsFeedback* mFeedback = nullptr; + QMutex mFeedbackMutex; + bool mWasCanceled = false; +}; + +///@endcond + +/** \ingroup gui + * \class QgsCurveEditorWidget + * A widget for manipulating QgsCurveTransform curves. + * \note added in QGIS 3.0 + */ +class GUI_EXPORT QgsCurveEditorWidget : public QWidget +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsCurveEditorWidget. + */ + QgsCurveEditorWidget( QWidget* parent = nullptr, const QgsCurveTransform& curve = QgsCurveTransform() ); + + ~QgsCurveEditorWidget(); + + /** + * Returns a curve representing the current curve from the widget. + * @see setCurve() + */ + QgsCurveTransform curve() const { return mCurve; } + + /** + * Sets the \a curve to show in the widget. + * @see curve() + */ + void setCurve( const QgsCurveTransform& curve ); + + /** + * Sets a \a layer and \a expression source for values to show in a histogram + * behind the curve. The histogram is generated in a background thread to keep + * the widget responsive. + * @see minHistogramValueRange() + * @see maxHistogramValueRange() + */ + void setHistogramSource( const QgsVectorLayer* layer, const QString& expression ); + + /** + * Returns the minimum expected value for the range of values shown in the histogram. + * @see maxHistogramValueRange() + * @see setMinHistogramValueRange() + */ + double minHistogramValueRange() const { return mMinValueRange; } + + /** + * Returns the maximum expected value for the range of values shown in the histogram. + * @see minHistogramValueRange() + * @see setMaxHistogramValueRange() + */ + double maxHistogramValueRange() const { return mMaxValueRange; } + + public slots: + + /** + * Sets the minimum expected value for the range of values shown in the histogram. + * @see setMaxHistogramValueRange() + * @see minHistogramValueRange() + */ + void setMinHistogramValueRange( double minValueRange ); + + /** + * Sets the maximum expected value for the range of values shown in the histogram. + * @see setMinHistogramValueRange() + * @see maxHistogramValueRange() + */ + void setMaxHistogramValueRange( double maxValueRange ); + + signals: + + //! Emitted when the widget curve changes + void changed(); + + protected: + + virtual void keyPressEvent( QKeyEvent *event ) override ; + + private slots: + + void plotMousePress( QPointF point ); + void plotMouseRelease( QPointF point ); + void plotMouseMove( QPointF point ); + + private: + + QgsCurveTransform mCurve; + + QwtPlot* mPlot = nullptr; + + QwtPlotCurve* mPlotCurve = nullptr; + + QList< QwtPlotMarker* > mMarkers; + QgsCurveEditorPlotEventFilter* mPlotFilter = nullptr; + int mCurrentPlotMarkerIndex; + //! Background histogram gatherer thread + std::unique_ptr< QgsHistogramValuesGatherer > mGatherer; + std::unique_ptr< QgsHistogram > mHistogram; + double mMinValueRange = 0.0; + double mMaxValueRange = 1.0; + +#if defined(QWT_VERSION) && QWT_VERSION>=0x060000 + QwtPlotHistogram *mPlotHistogram = nullptr; +#else + HistogramItem *mPlotHistogramItem = nullptr; +#endif + + void updatePlot(); + void addPlotMarker( double x, double y, bool isSelected = false ); + void updateHistogram(); + + int findNearestControlPoint( QPointF point ) const; + +#if defined(QWT_VERSION) && QWT_VERSION>=0x060000 + QwtPlotHistogram* createPlotHistogram( const QBrush &brush, const QPen &pen = Qt::NoPen ) const; +#else + HistogramItem* createHistoItem( const QBrush& brush, const QPen& pen = Qt::NoPen ) const; +#endif + +}; + + +// +// NOTE: +// For private only, not part of stable api or exposed to Python bindings +// +/// @cond PRIVATE +class GUI_EXPORT QgsCurveEditorPlotEventFilter: public QObject +{ + Q_OBJECT + + public: + + QgsCurveEditorPlotEventFilter( QwtPlot *plot ); + + virtual bool eventFilter( QObject* object, QEvent* event ) override; + + signals: + + void mousePress( QPointF ); + void mouseRelease( QPointF ); + void mouseMove( QPointF ); + + private: + + QwtPlot* mPlot; + QPointF mapPoint( QPointF point ) const; +}; +///@endcond + +#endif // QGSCURVEEDITORWIDGET_H diff --git a/src/gui/qgsgradientcolorrampdialog.cpp b/src/gui/qgsgradientcolorrampdialog.cpp index f37b6916aa3..82067347dbc 100644 --- a/src/gui/qgsgradientcolorrampdialog.cpp +++ b/src/gui/qgsgradientcolorrampdialog.cpp @@ -101,6 +101,14 @@ QgsGradientColorRampDialog::QgsGradientColorRampDialog( const QgsGradientColorRa mPlot->setAxisScale( QwtPlot::yLeft, 0.0, 1.0 ); mPlot->enableAxis( QwtPlot::yLeft, false ); + // add a grid + QwtPlotGrid * grid = new QwtPlotGrid(); + QwtScaleDiv gridDiv( 0.0, 1.0, QList(), QList(), QList() << 0.2 << 0.4 << 0.6 << 0.8 ); + grid->setXDiv( gridDiv ); + grid->setYDiv( gridDiv ); + grid->setPen( QPen( QColor( 0, 0, 0, 50 ) ) ); + grid->attach( mPlot ); + mLightnessCurve = new QwtPlotCurve(); mLightnessCurve->setTitle( QStringLiteral( "Lightness" ) ); mLightnessCurve->setPen( QPen( QColor( 70, 150, 255 ), 0.0 ) ), @@ -453,9 +461,9 @@ void QgsGradientColorRampDialog::addPlotMarker( double x, double y, const QColor QwtPlotMarker *marker = new QwtPlotMarker(); #if defined(QWT_VERSION) && QWT_VERSION>=0x060000 - marker->setSymbol( new QwtSymbol( QwtSymbol::Ellipse, QBrush( brushColor ), QPen( borderColor, isSelected ? 2 : 1 ), QSize( 10, 10 ) ) ); + marker->setSymbol( new QwtSymbol( QwtSymbol::Ellipse, QBrush( brushColor ), QPen( borderColor, isSelected ? 2 : 1 ), QSize( 8, 8 ) ) ); #else - marker->setSymbol( QwtSymbol( QwtSymbol::Ellipse, QBrush( brushColor ), QPen( borderColor, isSelected ? 2 : 1 ), QSize( 10, 10 ) ) ); + marker->setSymbol( QwtSymbol( QwtSymbol::Ellipse, QBrush( brushColor ), QPen( borderColor, isSelected ? 2 : 1 ), QSize( 8, 8 ) ) ); #endif marker->setValue( x, y ); marker->attach( mPlot ); diff --git a/src/gui/qgspropertyassistantwidget.cpp b/src/gui/qgspropertyassistantwidget.cpp index 165c1677add..59e7593ed53 100644 --- a/src/gui/qgspropertyassistantwidget.cpp +++ b/src/gui/qgspropertyassistantwidget.cpp @@ -51,6 +51,13 @@ QgsPropertyAssistantWidget::QgsPropertyAssistantWidget( QWidget* parent , { minValueSpinBox->setValue( initialState.transformer()->minValue() ); maxValueSpinBox->setValue( initialState.transformer()->maxValue() ); + + if ( initialState.transformer()->curveTransform() ) + { + mTransformCurveCheckBox->setChecked( true ); + mTransformCurveCheckBox->setCollapsed( false ); + mCurveEditor->setCurve( *initialState.transformer()->curveTransform() ); + } } connect( computeValuesButton, &QPushButton::clicked, this, &QgsPropertyAssistantWidget::computeValuesFromLayer ); @@ -107,11 +114,25 @@ QgsPropertyAssistantWidget::QgsPropertyAssistantWidget( QWidget* parent , { mOutputWidget->layout()->addWidget( mTransformerWidget ); connect( mTransformerWidget, &QgsPropertyAbstractTransformerWidget::widgetChanged, this, &QgsPropertyAssistantWidget::widgetChanged ); + + mCurveEditor->setMinHistogramValueRange( minValueSpinBox->value() ); + mCurveEditor->setMaxHistogramValueRange( maxValueSpinBox->value() ); + + mCurveEditor->setHistogramSource( mLayer, mExpressionWidget->currentField() ); + connect( mExpressionWidget, static_cast < void ( QgsFieldExpressionWidget::* )( const QString& ) > ( &QgsFieldExpressionWidget::fieldChanged ), this, [=]( const QString & expression ) + { + mCurveEditor->setHistogramSource( mLayer, expression ); + } + ); + connect( minValueSpinBox, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), mCurveEditor, &QgsCurveEditorWidget::setMinHistogramValueRange ); + connect( maxValueSpinBox, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), mCurveEditor, &QgsCurveEditorWidget::setMaxHistogramValueRange ); } + mTransformCurveCheckBox->setVisible( mTransformerWidget ); connect( minValueSpinBox, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), this, &QgsPropertyAssistantWidget::widgetChanged ); connect( maxValueSpinBox, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), this, &QgsPropertyAssistantWidget::widgetChanged ); connect( mExpressionWidget, static_cast < void ( QgsFieldExpressionWidget::* )( const QString& ) > ( &QgsFieldExpressionWidget::fieldChanged ), this, &QgsPropertyAssistantWidget::widgetChanged ); + connect( mCurveEditor, &QgsCurveEditorWidget::changed, this, &QgsPropertyAssistantWidget::widgetChanged ); connect( this, &QgsPropertyAssistantWidget::widgetChanged, this, &QgsPropertyAssistantWidget::updatePreview ); updatePreview(); } @@ -131,7 +152,18 @@ void QgsPropertyAssistantWidget::updateProperty( QgsProperty& property ) property.setField( mExpressionWidget->currentField() ); if ( mTransformerWidget ) - property.setTransformer( mTransformerWidget->createTransformer( minValueSpinBox->value(), maxValueSpinBox->value() ) ); + { + std::unique_ptr< QgsPropertyTransformer> t( mTransformerWidget->createTransformer( minValueSpinBox->value(), maxValueSpinBox->value() ) ); + if ( mTransformCurveCheckBox->isChecked() ) + { + t->setCurveTransform( new QgsCurveTransform( mCurveEditor->curve() ) ); + } + else + { + t->setCurveTransform( nullptr ); + } + property.setTransformer( t.release() ); + } } void QgsPropertyAssistantWidget::setDockMode( bool dockMode ) @@ -166,6 +198,10 @@ void QgsPropertyAssistantWidget::computeValuesFromLayer() whileBlocking( minValueSpinBox )->setValue( minValue ); whileBlocking( maxValueSpinBox )->setValue( maxValue ); + + mCurveEditor->setMinHistogramValueRange( minValueSpinBox->value() ); + mCurveEditor->setMaxHistogramValueRange( maxValueSpinBox->value() ); + emit widgetChanged(); } @@ -180,8 +216,9 @@ void QgsPropertyAssistantWidget::updatePreview() QList breaks = QgsSymbolLayerUtils::prettyBreaks( minValueSpinBox->value(), maxValueSpinBox->value(), 8 ); + QgsCurveTransform curve = mCurveEditor->curve(); QList< QgsSymbolLegendNode* > nodes = mTransformerWidget->generatePreviews( breaks, mLayerTreeLayer, mSymbol.get(), minValueSpinBox->value(), - maxValueSpinBox->value() ); + maxValueSpinBox->value(), mTransformCurveCheckBox->isChecked() ? &curve : nullptr ); int widthMax = 0; int i = 0; @@ -353,7 +390,7 @@ QgsSizeScaleTransformer* QgsPropertySizeAssistantWidget::createTransformer( doub return transformer; } -QList< QgsSymbolLegendNode* > QgsPropertySizeAssistantWidget::generatePreviews( const QList& breaks, QgsLayerTreeLayer* parent, const QgsSymbol* symbol, double minValue, double maxValue ) const +QList< QgsSymbolLegendNode* > QgsPropertySizeAssistantWidget::generatePreviews( const QList& breaks, QgsLayerTreeLayer* parent, const QgsSymbol* symbol, double minValue, double maxValue, QgsCurveTransform* curve ) const { QList< QgsSymbolLegendNode* > nodes; @@ -376,6 +413,8 @@ QList< QgsSymbolLegendNode* > QgsPropertySizeAssistantWidget::generatePreviews( return nodes; std::unique_ptr< QgsSizeScaleTransformer > t( createTransformer( minValue, maxValue ) ); + if ( curve ) + t->setCurveTransform( new QgsCurveTransform( *curve ) ); for ( int i = 0; i < breaks.length(); i++ ) { @@ -401,7 +440,7 @@ QList< QgsSymbolLegendNode* > QgsPropertySizeAssistantWidget::generatePreviews( return nodes; } -QList QgsPropertyAbstractTransformerWidget::generatePreviews( const QList& , QgsLayerTreeLayer* , const QgsSymbol*, double, double ) const +QList QgsPropertyAbstractTransformerWidget::generatePreviews( const QList& , QgsLayerTreeLayer* , const QgsSymbol*, double, double, QgsCurveTransform* ) const { return QList< QgsSymbolLegendNode* >(); } @@ -451,7 +490,7 @@ QgsColorRampTransformer* QgsPropertyColorAssistantWidget::createTransformer( dou return transformer; } -QList QgsPropertyColorAssistantWidget::generatePreviews( const QList& breaks, QgsLayerTreeLayer* parent, const QgsSymbol* symbol, double minValue, double maxValue ) const +QList QgsPropertyColorAssistantWidget::generatePreviews( const QList& breaks, QgsLayerTreeLayer* parent, const QgsSymbol* symbol, double minValue, double maxValue , QgsCurveTransform* curve ) const { QList< QgsSymbolLegendNode* > nodes; @@ -467,6 +506,8 @@ QList QgsPropertyColorAssistantWidget::generatePreviews( c return nodes; std::unique_ptr< QgsColorRampTransformer > t( createTransformer( minValue, maxValue ) ); + if ( curve ) + t->setCurveTransform( new QgsCurveTransform( *curve ) ); for ( int i = 0; i < breaks.length(); i++ ) { diff --git a/src/gui/qgspropertyassistantwidget.h b/src/gui/qgspropertyassistantwidget.h index abc8f819c73..ab89f467337 100644 --- a/src/gui/qgspropertyassistantwidget.h +++ b/src/gui/qgspropertyassistantwidget.h @@ -48,7 +48,7 @@ class GUI_EXPORT QgsPropertyAbstractTransformerWidget : public QWidget virtual QgsPropertyTransformer* createTransformer( double minValue, double maxValue ) const = 0; - virtual QList< QgsSymbolLegendNode* > generatePreviews( const QList& breaks, QgsLayerTreeLayer* parent, const QgsSymbol* symbol, double minValue, double maxValue ) const; + virtual QList< QgsSymbolLegendNode* > generatePreviews( const QList& breaks, QgsLayerTreeLayer* parent, const QgsSymbol* symbol, double minValue, double maxValue, QgsCurveTransform* curve ) const; signals: @@ -82,7 +82,7 @@ class GUI_EXPORT QgsPropertySizeAssistantWidget : public QgsPropertyAbstractTran virtual QgsSizeScaleTransformer* createTransformer( double minValue, double maxValue ) const override; - QList< QgsSymbolLegendNode* > generatePreviews( const QList& breaks, QgsLayerTreeLayer* parent, const QgsSymbol* symbol, double minValue, double maxValue ) const override; + QList< QgsSymbolLegendNode* > generatePreviews( const QList& breaks, QgsLayerTreeLayer* parent, const QgsSymbol* symbol, double minValue, double maxValue, QgsCurveTransform* curve ) const override; }; class GUI_EXPORT QgsPropertyColorAssistantWidget : public QgsPropertyAbstractTransformerWidget, private Ui::PropertyColorAssistant @@ -95,7 +95,7 @@ class GUI_EXPORT QgsPropertyColorAssistantWidget : public QgsPropertyAbstractTra virtual QgsColorRampTransformer* createTransformer( double minValue, double maxValue ) const override; - QList< QgsSymbolLegendNode* > generatePreviews( const QList& breaks, QgsLayerTreeLayer* parent, const QgsSymbol* symbol, double minValue, double maxValue ) const override; + QList< QgsSymbolLegendNode* > generatePreviews( const QList& breaks, QgsLayerTreeLayer* parent, const QgsSymbol* symbol, double minValue, double maxValue, QgsCurveTransform* curve ) const override; }; ///@endcond PRIVATE diff --git a/src/ui/qgspropertyassistantwidgetbase.ui b/src/ui/qgspropertyassistantwidgetbase.ui index 026f0bb17e4..22fc30bc7d9 100644 --- a/src/ui/qgspropertyassistantwidgetbase.ui +++ b/src/ui/qgspropertyassistantwidgetbase.ui @@ -6,22 +6,12 @@ 0 0 - 522 - 332 + 525 + 426 - - - - - - 200 - 0 - - - - - + + Output @@ -60,6 +50,16 @@ + + + + QFrame::NoFrame + + + QFrame::Plain + + + @@ -153,14 +153,60 @@ - - - - QFrame::NoFrame + + + + + 200 + 0 + - - QFrame::Plain + + + + + + Apply transform curve + + true + + + false + + + true + + + composeritem + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 100 + + + + Qt::StrongFocus + + + + @@ -183,6 +229,18 @@
qgspanelwidget.h
1 + + QgsCollapsibleGroupBoxBasic + QGroupBox +
qgscollapsiblegroupbox.h
+ 1 +
+ + QgsCurveEditorWidget + QWidget +
qgscurveeditorwidget.h
+ 1 +
mExpressionWidget diff --git a/tests/src/core/testqgsproperty.cpp b/tests/src/core/testqgsproperty.cpp index 7eb348174b5..eff98dadb2e 100644 --- a/tests/src/core/testqgsproperty.cpp +++ b/tests/src/core/testqgsproperty.cpp @@ -16,6 +16,7 @@ ***************************************************************************/ #include "qgstest.h" +#include "qgstestutils.h" #include "qgsproperty.h" #include "qgspropertycollection.h" #include "qgsvectorlayer.h" @@ -90,10 +91,12 @@ class TestQgsProperty : public QObject void asExpression(); //test converting property to expression void propertyCollection(); //test for QgsPropertyCollection void collectionStack(); //test for QgsPropertyCollectionStack + void curveTransform(); private: QgsPropertiesDefinition mDefinitions; + void checkCurveResult( const QList< QgsPoint >& controlPoints, const QVector& x, const QVector& y ); }; @@ -715,6 +718,26 @@ void TestQgsProperty::genericNumericTransformer() //non numeric value QCOMPARE( t1.transform( context, QVariant( "ffff" ) ), QVariant( "ffff" ) ); + // add a curve + QVERIFY( !t1.curveTransform() ); + t1.setCurveTransform( new QgsCurveTransform( QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) ) ); + QVERIFY( t1.curveTransform() ); + QCOMPARE( t1.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) ); + + QCOMPARE( t1.transform( context, 10 ).toInt(), 180 ); + QCOMPARE( t1.transform( context, 20 ).toInt(), 120 ); + + // copy + QgsGenericNumericTransformer s1( t1 ); + QVERIFY( s1.curveTransform() ); + QCOMPARE( s1.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) ); + + // assignment + QgsGenericNumericTransformer s2; + s2 = t1; + QVERIFY( s2.curveTransform() ); + QCOMPARE( s2.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) ); + //saving and restoring //create a test dom element @@ -730,6 +753,7 @@ void TestQgsProperty::genericNumericTransformer() 250, -10, 99 ); + t2.setCurveTransform( new QgsCurveTransform( QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) ) ); QDomElement element = doc.createElement( "xform" ); QVERIFY( t2.writeXml( element, doc ) ); @@ -741,6 +765,8 @@ void TestQgsProperty::genericNumericTransformer() QCOMPARE( r1.maxOutputValue(), 250.0 ); QCOMPARE( r1.nullOutputValue(), -10.0 ); QCOMPARE( r1.exponent(), 99.0 ); + QVERIFY( r1.curveTransform() ); + QCOMPARE( r1.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) ); // test cloning std::unique_ptr< QgsGenericNumericTransformer > r2( t2.clone() ); @@ -750,6 +776,8 @@ void TestQgsProperty::genericNumericTransformer() QCOMPARE( r2->maxOutputValue(), 250.0 ); QCOMPARE( r2->nullOutputValue(), -10.0 ); QCOMPARE( r2->exponent(), 99.0 ); + QVERIFY( r2->curveTransform() ); + QCOMPARE( r2->curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) ); //test various min/max value/size and scaling methods @@ -884,6 +912,25 @@ void TestQgsProperty::sizeScaleTransformer() //non numeric value QCOMPARE( scale.transform( context, QVariant( "ffff" ) ), QVariant( "ffff" ) ); + // add a curve + QVERIFY( !scale.curveTransform() ); + scale.setCurveTransform( new QgsCurveTransform( QList< QgsPoint >() << QgsPoint( 0, 0.2 ) << QgsPoint( 1, 0.8 ) ) ); + QVERIFY( scale.curveTransform() ); + QCOMPARE( scale.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.2 ) << QgsPoint( 1, 0.8 ) ); + QCOMPARE( scale.transform( context, 10 ).toInt(), 120 ); + QCOMPARE( scale.transform( context, 20 ).toInt(), 180 ); + + // copy + QgsSizeScaleTransformer s1( scale ); + QVERIFY( s1.curveTransform() ); + QCOMPARE( s1.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.2 ) << QgsPoint( 1, 0.8 ) ); + + // assignment + QgsSizeScaleTransformer s2; + s2 = scale; + QVERIFY( s2.curveTransform() ); + QCOMPARE( s2.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.2 ) << QgsPoint( 1, 0.8 ) ); + //saving and restoring //create a test dom element @@ -900,6 +947,7 @@ void TestQgsProperty::sizeScaleTransformer() 250, -10, 99 ); + t1.setCurveTransform( new QgsCurveTransform( QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) ) ); QDomElement element = doc.createElement( "xform" ); QVERIFY( t1.writeXml( element, doc ) ); @@ -912,6 +960,8 @@ void TestQgsProperty::sizeScaleTransformer() QCOMPARE( r1.nullSize(), -10.0 ); QCOMPARE( r1.exponent(), 99.0 ); QCOMPARE( r1.type(), QgsSizeScaleTransformer::Exponential ); + QVERIFY( r1.curveTransform() ); + QCOMPARE( r1.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) ); // test cloning std::unique_ptr< QgsSizeScaleTransformer > r2( t1.clone() ); @@ -922,6 +972,8 @@ void TestQgsProperty::sizeScaleTransformer() QCOMPARE( r2->nullSize(), -10.0 ); QCOMPARE( r2->exponent(), 99.0 ); QCOMPARE( r2->type(), QgsSizeScaleTransformer::Exponential ); + QVERIFY( r2->curveTransform() ); + QCOMPARE( r2->curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) ); //test various min/max value/size and scaling methods @@ -1089,6 +1141,26 @@ void TestQgsProperty::colorRampTransformer() //non numeric value QCOMPARE( scale.transform( context, QVariant( "ffff" ) ), QVariant( "ffff" ) ); + // add a curve + QVERIFY( !scale.curveTransform() ); + scale.setCurveTransform( new QgsCurveTransform( QList< QgsPoint >() << QgsPoint( 0, 0.2 ) << QgsPoint( 1, 0.8 ) ) ); + QVERIFY( scale.curveTransform() ); + QCOMPARE( scale.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.2 ) << QgsPoint( 1, 0.8 ) ); + + QCOMPARE( scale.transform( context, 10 ).value().name(), QString( "#333333" ) ); + QCOMPARE( scale.transform( context, 20 ).value().name(), QString( "#cccccc" ) ); + + // copy + QgsColorRampTransformer s1( scale ); + QVERIFY( s1.curveTransform() ); + QCOMPARE( s1.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.2 ) << QgsPoint( 1, 0.8 ) ); + + // assignment + QgsColorRampTransformer s2; + s2 = scale; + QVERIFY( s2.curveTransform() ); + QCOMPARE( s2.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.2 ) << QgsPoint( 1, 0.8 ) ); + //saving and restoring //create a test dom element @@ -1103,6 +1175,7 @@ void TestQgsProperty::colorRampTransformer() new QgsGradientColorRamp( QColor( 10, 20, 30 ), QColor( 200, 190, 180 ) ), QColor( 100, 150, 200 ) ); t1.setRampName( "rampname " ); + t1.setCurveTransform( new QgsCurveTransform( QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) ) ); QDomElement element = doc.createElement( "xform" ); QVERIFY( t1.writeXml( element, doc ) ); @@ -1115,6 +1188,8 @@ void TestQgsProperty::colorRampTransformer() QVERIFY( dynamic_cast< QgsGradientColorRamp* >( r1.colorRamp() ) ); QCOMPARE( static_cast< QgsGradientColorRamp* >( r1.colorRamp() )->color1(), QColor( 10, 20, 30 ) ); QCOMPARE( static_cast< QgsGradientColorRamp* >( r1.colorRamp() )->color2(), QColor( 200, 190, 180 ) ); + QVERIFY( r1.curveTransform() ); + QCOMPARE( r1.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) ); // test cloning std::unique_ptr< QgsColorRampTransformer > r2( t1.clone() ); @@ -1124,6 +1199,8 @@ void TestQgsProperty::colorRampTransformer() QCOMPARE( r2->rampName(), QStringLiteral( "rampname " ) ); QCOMPARE( static_cast< QgsGradientColorRamp* >( r2->colorRamp() )->color1(), QColor( 10, 20, 30 ) ); QCOMPARE( static_cast< QgsGradientColorRamp* >( r2->colorRamp() )->color2(), QColor( 200, 190, 180 ) ); + QVERIFY( r2->curveTransform() ); + QCOMPARE( r2->curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) ); // copy constructor QgsColorRampTransformer r3( t1 ); @@ -1133,6 +1210,8 @@ void TestQgsProperty::colorRampTransformer() QCOMPARE( r3.rampName(), QStringLiteral( "rampname " ) ); QCOMPARE( static_cast< QgsGradientColorRamp* >( r3.colorRamp() )->color1(), QColor( 10, 20, 30 ) ); QCOMPARE( static_cast< QgsGradientColorRamp* >( r3.colorRamp() )->color2(), QColor( 200, 190, 180 ) ); + QVERIFY( r3.curveTransform() ); + QCOMPARE( r3.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) ); // assignment operator QgsColorRampTransformer r4; @@ -1143,6 +1222,8 @@ void TestQgsProperty::colorRampTransformer() QCOMPARE( r4.rampName(), QStringLiteral( "rampname " ) ); QCOMPARE( static_cast< QgsGradientColorRamp* >( r4.colorRamp() )->color1(), QColor( 10, 20, 30 ) ); QCOMPARE( static_cast< QgsGradientColorRamp* >( r4.colorRamp() )->color2(), QColor( 200, 190, 180 ) ); + QVERIFY( r4.curveTransform() ); + QCOMPARE( r4.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) ); //test various min/max value/color and scaling methods @@ -1581,5 +1662,127 @@ void TestQgsProperty::collectionStack() QVERIFY( !stack4.hasDynamicProperties() ); } +void TestQgsProperty::curveTransform() +{ + QgsCurveTransform t; + // linear transform + QCOMPARE( t.y( -1 ), 0.0 ); + QCOMPARE( t.y( 0 ), 0.0 ); + QCOMPARE( t.y( 0.2 ), 0.2 ); + QCOMPARE( t.y( 0.5 ), 0.5 ); + QCOMPARE( t.y( 0.8 ), 0.8 ); + QCOMPARE( t.y( 1 ), 1.0 ); + QCOMPARE( t.y( 2 ), 1.0 ); + + QVector< double > x; + x << -1 << 0 << 0.2 << 0.5 << 0.8 << 1 << 2; + QVector< double > y = t.y( x ); + QCOMPARE( y[0], 0.0 ); + QCOMPARE( y[1], 0.0 ); + QCOMPARE( y[2], 0.2 ); + QCOMPARE( y[3], 0.5 ); + QCOMPARE( y[4], 0.8 ); + QCOMPARE( y[5], 1.0 ); + QCOMPARE( y[6], 1.0 ); + + // linear transform with y =/= x + checkCurveResult( QList< QgsPoint >() << QgsPoint( 0, 0.2 ) << QgsPoint( 1.0, 0.8 ), + QVector< double >() << -1 << 0 << 0.2 << 0.5 << 0.8 << 1 << 2, + QVector< double >() << 0.2 << 0.2 << 0.32 << 0.5 << 0.68 << 0.8 << 0.8 ); + + // reverse linear transform with y = -x + checkCurveResult( QList< QgsPoint >() << QgsPoint( 0.0, 1.0 ) << QgsPoint( 1.0, 0 ), + QVector< double >() << -1 << 0 << 0.2 << 0.5 << 0.8 << 1 << 2, + QVector< double >() << 1.0 << 1.0 << 0.8 << 0.5 << 0.2 << 0.0 << 0.0 ); + + // ok, time for some more complex tests... + + // 3 control points, but linear + checkCurveResult( QList< QgsPoint >() << QgsPoint( 0, 0.0 ) << QgsPoint( 0.2, 0.2 ) << QgsPoint( 1.0, 1.0 ), + QVector< double >() << -1 << 0 << 0.2 << 0.5 << 0.8 << 1 << 2, + QVector< double >() << 0.0 << 0.0 << 0.2 << 0.5 << 0.8 << 1.0 << 1.0 ); + + // test for "flat" response for x outside of control point range + checkCurveResult( QList< QgsPoint >() << QgsPoint( 0.2, 0.2 ) << QgsPoint( 0.5, 0.5 ) << QgsPoint( 0.8, 0.8 ), + QVector< double >() << -1 << 0 << 0.1 << 0.2 << 0.5 << 0.8 << 0.9 << 1 << 2, + QVector< double >() << 0.2 << 0.2 << 0.2 << 0.2 << 0.5 << 0.8 << 0.8 << 0.8 << 0.8 ); + + //curves! + checkCurveResult( QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 0.4, 0.6 ) << QgsPoint( 0.6, 0.8 ) << QgsPoint( 1.0, 1.0 ), + QVector< double >() << -1 << 0 << 0.2 << 0.4 << 0.5 << 0.6 << 0.8 << 0.9 << 1.0 << 2.0, + QVector< double >() << 0.0 << 0.0 << 0.321429 << 0.6 << 0.710714 << 0.8 << 0.921429 << 0.963393 << 1.0 << 1.0 ); + + //curves with more control points + checkCurveResult( QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 0.2, 0.6 ) << QgsPoint( 0.4, 0.6 ) << QgsPoint( 0.6, 0.8 ) << QgsPoint( 0.8, 0.3 ) << QgsPoint( 1.0, 1.0 ), + QVector< double >() << -1 << 0 << 0.2 << 0.4 << 0.5 << 0.6 << 0.8 << 0.9 << 1.0 << 2.0, + QVector< double >() << 0.0 << 0.0 << 0.6 << 0.6 << 0.751316 << 0.8 << 0.3 << 0.508074 << 1.0 << 1.0 ); + + // general tests + QList< QgsPoint > points = QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 0.4, 0.6 ) << QgsPoint( 0.6, 0.8 ) << QgsPoint( 1.0, 1.0 ); + QgsCurveTransform src( points ); + QCOMPARE( src.controlPoints(), points ); + points = QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 0.5, 0.6 ) << QgsPoint( 0.6, 0.8 ) << QgsPoint( 1.0, 1.0 ); + src.setControlPoints( points ); + QCOMPARE( src.controlPoints(), points ); + + src.setControlPoints( QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 1.0, 1.0 ) ); + src.addControlPoint( 0.2, 0.3 ); + src.addControlPoint( 0.1, 0.4 ); + QCOMPARE( src.controlPoints(), QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 0.1, 0.4 ) << QgsPoint( 0.2, 0.3 ) << QgsPoint( 1.0, 1.0 ) ); + + // remove non-existent point + src.removeControlPoint( 0.6, 0.7 ); + QCOMPARE( src.controlPoints(), QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 0.1, 0.4 ) << QgsPoint( 0.2, 0.3 ) << QgsPoint( 1.0, 1.0 ) ); + + // remove valid point + src.removeControlPoint( 0.1, 0.4 ); + QCOMPARE( src.controlPoints(), QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 0.2, 0.3 ) << QgsPoint( 1.0, 1.0 ) ); + + // copy constructor + QgsCurveTransform dest( src ); + QCOMPARE( dest.controlPoints(), QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 0.2, 0.3 ) << QgsPoint( 1.0, 1.0 ) ); + // check a value to ensure that derivative matrix was copied ok + QGSCOMPARENEAR( dest.y( 0.5 ), 0.1, 0.638672 ); + + // assignment operator + QgsCurveTransform dest2; + dest2 = src; + QCOMPARE( dest2.controlPoints(), QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 0.2, 0.3 ) << QgsPoint( 1.0, 1.0 ) ); + QGSCOMPARENEAR( dest2.y( 0.5 ), 0.1, 0.638672 ); + + // writing and reading from xml + QDomImplementation DomImplementation; + QDomDocumentType documentType = + DomImplementation.createDocumentType( + "qgis", "http://mrcc.com/qgis.dtd", "SYSTEM" ); + QDomDocument doc( documentType ); + + QDomElement element = doc.createElement( "xform" ); + QVERIFY( src.writeXml( element, doc ) ); + + QgsCurveTransform r1; + QVERIFY( r1.readXml( element, doc ) ); + QCOMPARE( r1.controlPoints(), src.controlPoints() ); + QGSCOMPARENEAR( dest2.y( 0.5 ), 0.1, 0.638672 ); +} + +void TestQgsProperty::checkCurveResult( const QList& controlPoints, const QVector& x, const QVector& y ) +{ + // build transform + QgsCurveTransform t( controlPoints ); + + // we check two approaches + for ( int i = 0; i < x.count(); ++i ) + { + QGSCOMPARENEAR( t.y( x.at( i ) ), y.at( i ), 0.0001 ); + } + + QVector< double > results = t.y( x ); + for ( int i = 0; i < y.count(); ++i ) + { + QGSCOMPARENEAR( results.at( i ), y.at( i ), 0.0001 ); + } +} + QGSTEST_MAIN( TestQgsProperty ) #include "testqgsproperty.moc"