From 5c3198daea1bc3243d21d893bf48614bb24e7c92 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 16 Feb 2017 18:47:03 +1000 Subject: [PATCH 1/8] Implement a photoshop-esque curve transformer Handles scaling of input values to output values by using a curve created from smoothly joining a number of set control points. This 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. --- python/core/qgspropertytransformer.sip | 32 +++ src/core/qgspropertytransformer.cpp | 331 +++++++++++++++++++++++++ src/core/qgspropertytransformer.h | 115 +++++++++ tests/src/core/testqgsproperty.cpp | 125 ++++++++++ 4 files changed, 603 insertions(+) diff --git a/python/core/qgspropertytransformer.sip b/python/core/qgspropertytransformer.sip index 26111e89c64..efc5417398b 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 diff --git a/src/core/qgspropertytransformer.cpp b/src/core/qgspropertytransformer.cpp index 3302531848e..7ba995aa9b4 100644 --- a/src/core/qgspropertytransformer.cpp +++ b/src/core/qgspropertytransformer.cpp @@ -586,3 +586,334 @@ 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 ); + 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 x; // invalid + else if ( n < 3 ) + { + // linear + if ( x <= mControlPoints.at( 0 ).x() ) + return mControlPoints.at( 0 ).y(); + else if ( x >= mControlPoints.at( n - 1 ).x() ) + return mControlPoints.at( 1 ).y(); + else + { + double dx = mControlPoints.at( 1 ).x() - mControlPoints.at( 0 ).x(); + double dy = mControlPoints.at( 1 ).y() - mControlPoints.at( 0 ).y(); + return x * ( dy / dx ) + mControlPoints.at( 0 ).y(); + } + } + + // safety check + if ( x <= mControlPoints.at( 0 ).x() ) + return mControlPoints.at( 0 ).y(); + if ( x >= mControlPoints.at( n - 1 ).x() ) + return mControlPoints.at( n - 1 ).y(); + + // 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 a*currentControlPoint.y() + t*nextControlPoint.y() + ( h*h / 6 )*(( a*a*a - a )*mSecondDerivativeArray[i] + ( t*t*t - t )*mSecondDerivativeArray[i+1] ); + } + + ++pointIt; + if ( pointIt == mControlPoints.constEnd() ) + break; + + currentControlPoint = nextControlPoint; + nextControlPoint = *pointIt; + } + + //should not happen + return x; +} + +// 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 << currentControlPoint.y(); + 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 << a*currentControlPoint.y() + t*nextControlPoint.y() + ( h*h / 6 )*(( a*a*a - a )*mSecondDerivativeArray[i] + ( t*t*t - t )*mSecondDerivativeArray[i+1] ); + 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 << nextControlPoint.y(); + 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..b54eab597bb 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 diff --git a/tests/src/core/testqgsproperty.cpp b/tests/src/core/testqgsproperty.cpp index 7eb348174b5..00fdd37bf2b 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 ); }; @@ -1581,5 +1584,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" From dcf6104753d326c24b84a245e74e97d6bf9bf391 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 21 Feb 2017 13:59:17 +1000 Subject: [PATCH 2/8] Allow QgsPropertyTransformers to have an associated QgsCurveTransform --- python/core/qgspropertytransformer.sip | 13 +++ src/core/qgspropertytransformer.cpp | 130 +++++++++++++++++++++---- src/core/qgspropertytransformer.h | 42 ++++++++ tests/src/core/testqgsproperty.cpp | 78 +++++++++++++++ 4 files changed, 244 insertions(+), 19 deletions(-) diff --git a/python/core/qgspropertytransformer.sip b/python/core/qgspropertytransformer.sip index efc5417398b..eb5c9e3de2e 100644 --- a/python/core/qgspropertytransformer.sip +++ b/python/core/qgspropertytransformer.sip @@ -60,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; @@ -78,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 { @@ -98,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; @@ -140,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/src/core/qgspropertytransformer.cpp b/src/core/qgspropertytransformer.cpp index 7ba995aa9b4..b3f67bef18f 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 @@ -145,7 +211,7 @@ QVariant QgsGenericNumericTransformer::transform( const QgsExpressionContext& co if ( ok ) { //apply scaling to value - return value( dblValue ); + return value( transformNumeric( dblValue ) ); } else { @@ -257,15 +323,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 @@ -344,7 +433,7 @@ QVariant QgsSizeScaleTransformer::transform( const QgsExpressionContext& context if ( ok ) { //apply scaling to value - return size( dblValue ); + return size( transformNumeric( dblValue ) ); } else { @@ -483,6 +572,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 +583,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 @@ -546,7 +638,7 @@ QVariant QgsColorRampTransformer::transform( const QgsExpressionContext &context if ( ok ) { //apply scaling to value - return color( dblValue ); + return color( transformNumeric( dblValue ) ); } else { diff --git a/src/core/qgspropertytransformer.h b/src/core/qgspropertytransformer.h index b54eab597bb..cd7c4c3d353 100644 --- a/src/core/qgspropertytransformer.h +++ b/src/core/qgspropertytransformer.h @@ -177,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; /** @@ -235,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 @@ -271,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; }; /** @@ -300,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; @@ -430,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/tests/src/core/testqgsproperty.cpp b/tests/src/core/testqgsproperty.cpp index 00fdd37bf2b..eff98dadb2e 100644 --- a/tests/src/core/testqgsproperty.cpp +++ b/tests/src/core/testqgsproperty.cpp @@ -718,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 @@ -733,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 ) ); @@ -744,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() ); @@ -753,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 @@ -887,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 @@ -903,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 ) ); @@ -915,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() ); @@ -925,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 @@ -1092,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 @@ -1106,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 ) ); @@ -1118,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() ); @@ -1127,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 ); @@ -1136,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; @@ -1146,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 From 45861d39f8f9a1b0a9c84ec7dc598fbd0cf707ba Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 21 Feb 2017 16:18:52 +1000 Subject: [PATCH 3/8] Restrict curve points to 0-1 range --- src/core/qgspropertytransformer.cpp | 38 ++++++++++++++++++----------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/core/qgspropertytransformer.cpp b/src/core/qgspropertytransformer.cpp index b3f67bef18f..c58a44ba399 100644 --- a/src/core/qgspropertytransformer.cpp +++ b/src/core/qgspropertytransformer.cpp @@ -192,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 @@ -211,7 +212,7 @@ QVariant QgsGenericNumericTransformer::transform( const QgsExpressionContext& co if ( ok ) { //apply scaling to value - return value( transformNumeric( dblValue ) ); + return value( dblValue ); } else { @@ -386,6 +387,8 @@ bool QgsSizeScaleTransformer::readXml( const QDomElement &transformerElem, const double QgsSizeScaleTransformer::size( double value ) const { + value = transformNumeric( value ); + switch ( mType ) { case Linear: @@ -433,7 +436,7 @@ QVariant QgsSizeScaleTransformer::transform( const QgsExpressionContext& context if ( ok ) { //apply scaling to value - return size( transformNumeric( dblValue ) ); + return size( dblValue ); } else { @@ -638,7 +641,7 @@ QVariant QgsColorRampTransformer::transform( const QgsExpressionContext &context if ( ok ) { //apply scaling to value - return color( transformNumeric( dblValue ) ); + return color( dblValue ); } else { @@ -661,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 ) @@ -733,6 +737,11 @@ 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(); } @@ -769,27 +778,27 @@ double QgsCurveTransform::y( double x ) const { int n = mControlPoints.count(); if ( n < 2 ) - return x; // invalid + return qBound( 0.0, x, 1.0 ); // invalid else if ( n < 3 ) { // linear if ( x <= mControlPoints.at( 0 ).x() ) - return mControlPoints.at( 0 ).y(); + return qBound( 0.0, mControlPoints.at( 0 ).y(), 1.0 ); else if ( x >= mControlPoints.at( n - 1 ).x() ) - return mControlPoints.at( 1 ).y(); + 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 x * ( dy / dx ) + 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 mControlPoints.at( 0 ).y(); + return qBound( 0.0, mControlPoints.at( 0 ).y(), 1.0 ); if ( x >= mControlPoints.at( n - 1 ).x() ) - return mControlPoints.at( n - 1 ).y(); + return qBound( 0.0, mControlPoints.at( n - 1 ).y(), 1.0 ); // find corresponding segment QList::const_iterator pointIt = mControlPoints.constBegin(); @@ -807,7 +816,8 @@ double QgsCurveTransform::y( double x ) const double a = 1 - t; - return a*currentControlPoint.y() + t*nextControlPoint.y() + ( h*h / 6 )*(( a*a*a - a )*mSecondDerivativeArray[i] + ( t*t*t - t )*mSecondDerivativeArray[i+1] ); + 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; @@ -819,7 +829,7 @@ double QgsCurveTransform::y( double x ) const } //should not happen - return x; + return qBound( 0.0, x, 1.0 ); } // this code is adapted from https://github.com/OpenFibers/Photoshop-Curves @@ -851,7 +861,7 @@ QVector QgsCurveTransform::y( const QVector& x ) const // safety check while ( currentX <= currentControlPoint.x() ) { - result << currentControlPoint.y(); + result << qBound( 0.0, currentControlPoint.y(), 1.0 ); xIndex++; currentX = x.at( xIndex ); } @@ -867,7 +877,7 @@ QVector QgsCurveTransform::y( const QVector& x ) const double a = 1 - t; - result << a*currentControlPoint.y() + t*nextControlPoint.y() + ( h*h / 6 )*(( a*a*a - a )*mSecondDerivativeArray[i] + ( t*t*t - t )*mSecondDerivativeArray[i+1] ); + 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; @@ -886,7 +896,7 @@ QVector QgsCurveTransform::y( const QVector& x ) const // safety check while ( xIndex < x.count() ) { - result << nextControlPoint.y(); + result << qBound( 0.0, nextControlPoint.y(), 1.0 ); xIndex++; } From cc9b5a47b753129752019f0d7deb7ad1b13d0db9 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 21 Feb 2017 16:19:10 +1000 Subject: [PATCH 4/8] [FEATURE] Interactive curve editing for property overrides This adds a new interactive "curve" to the assistant widgets. It allows you to fine tune exactly how input values get mapped to output sizes/colors/etc. Think GIMP or Photoshop curves, but for your data... --- python/gui/gui.sip | 1 + python/gui/qgscurveeditorwidget.sip | 23 ++ src/gui/CMakeLists.txt | 2 + src/gui/qgscurveeditorwidget.cpp | 312 +++++++++++++++++++++++ src/gui/qgscurveeditorwidget.h | 118 +++++++++ src/gui/qgspropertyassistantwidget.cpp | 34 ++- src/gui/qgspropertyassistantwidget.h | 6 +- src/ui/qgspropertyassistantwidgetbase.ui | 98 +++++-- 8 files changed, 566 insertions(+), 28 deletions(-) create mode 100644 python/gui/qgscurveeditorwidget.sip create mode 100644 src/gui/qgscurveeditorwidget.cpp create mode 100644 src/gui/qgscurveeditorwidget.h 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..799aa05acdd --- /dev/null +++ b/python/gui/qgscurveeditorwidget.sip @@ -0,0 +1,23 @@ +class QgsCurveEditorWidget : QWidget +{ +%TypeHeaderCode +#include +%End + + public: + + QgsCurveEditorWidget( QWidget* parent /TransferThis/ = 0, const QgsCurveTransform& curve = QgsCurveTransform() ); + + QgsCurveTransform curve() const; + + void setCurve( const QgsCurveTransform& curve ); + + signals: + + void changed(); + + protected: + + virtual void keyPressEvent( QKeyEvent *event ); + +}; 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..d3ae90e2865 --- /dev/null +++ b/src/gui/qgscurveeditorwidget.cpp @@ -0,0 +1,312 @@ +/*************************************************************************** + 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 + +// QWT Charting widget +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(); +} + +void QgsCurveEditorWidget::setCurve( const QgsCurveTransform& curve ) +{ + mCurve = curve; + updatePlot(); + emit changed(); +} + +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( 6, 6 ) ) ); +#else + marker->setSymbol( QwtSymbol( QwtSymbol::Ellipse, QBrush( brushColor ), QPen( borderColor, isSelected ? 2 : 1 ), QSize( 6, 6 ) ) ); +#endif + marker->setValue( x, y ); + marker->attach( mPlot ); + marker->setRenderHint( QwtPlotItem::RenderAntialiased, true ); + mMarkers << marker; +} + +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(); +} + + +/// @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..c87be3294f3 --- /dev/null +++ b/src/gui/qgscurveeditorwidget.h @@ -0,0 +1,118 @@ +/*************************************************************************** + 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 "qgis_gui.h" +#include "qgspropertytransformer.h" + +class QwtPlot; +class QwtPlotCurve; +class QwtPlotMarker; +class QgsCurveEditorPlotEventFilter; + +/** \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() ); + + /** + * 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 ); + + 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; + + void updatePlot(); + void addPlotMarker( double x, double y, bool isSelected = false ); + + int findNearestControlPoint( QPointF point ) const; +}; + + +// +// 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/qgspropertyassistantwidget.cpp b/src/gui/qgspropertyassistantwidget.cpp index 165c1677add..59cfea5a12b 100644 --- a/src/gui/qgspropertyassistantwidget.cpp +++ b/src/gui/qgspropertyassistantwidget.cpp @@ -51,6 +51,12 @@ QgsPropertyAssistantWidget::QgsPropertyAssistantWidget( QWidget* parent , { minValueSpinBox->setValue( initialState.transformer()->minValue() ); maxValueSpinBox->setValue( initialState.transformer()->maxValue() ); + + if ( initialState.transformer()->curveTransform() ) + { + mTransformCurveCheckBox->setChecked( true ); + mCurveEditor->setCurve( *initialState.transformer()->curveTransform() ); + } } connect( computeValuesButton, &QPushButton::clicked, this, &QgsPropertyAssistantWidget::computeValuesFromLayer ); @@ -108,10 +114,12 @@ QgsPropertyAssistantWidget::QgsPropertyAssistantWidget( QWidget* parent , mOutputWidget->layout()->addWidget( mTransformerWidget ); connect( mTransformerWidget, &QgsPropertyAbstractTransformerWidget::widgetChanged, this, &QgsPropertyAssistantWidget::widgetChanged ); } + 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 +139,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 ) @@ -180,8 +199,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 +373,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 +396,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 +423,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 +473,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 +489,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 From 0faf7c395f717b87ca934a9441ec03ad6352bfd2 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 22 Feb 2017 07:38:52 +1000 Subject: [PATCH 5/8] Allow value fetching and histogram calculation to be canceled --- python/core/qgshistogram.sip | 8 +------- python/core/qgsvectorlayer.sip | 22 ++-------------------- src/core/qgshistogram.cpp | 4 ++-- src/core/qgshistogram.h | 4 +++- src/core/qgsvectorlayer.cpp | 14 ++++++++++++-- src/core/qgsvectorlayer.h | 6 ++++-- 6 files changed, 24 insertions(+), 34 deletions(-) diff --git a/python/core/qgshistogram.sip b/python/core/qgshistogram.sip index f65399da52a..16126ee5287 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( 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/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/src/core/qgshistogram.cpp b/src/core/qgshistogram.cpp index 0d5306e8126..2736ed105b9 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( 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..61499f3d1e1 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( 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/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 ); From 5c42c7636bc6857a6ca443d9ede235c23cfddc60 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 22 Feb 2017 11:09:14 +1000 Subject: [PATCH 6/8] [FEATURE] Show a histogram for values behind curve editor in property assistant Makes it easier to set suitable curves. Populated in the background for a nice reponsive widget! --- python/core/qgshistogram.sip | 2 +- python/gui/qgscurveeditorwidget.sip | 8 ++ src/core/qgshistogram.cpp | 2 +- src/core/qgshistogram.h | 2 +- src/gui/qgscurveeditorwidget.cpp | 169 +++++++++++++++++++++++++ src/gui/qgscurveeditorwidget.h | 164 ++++++++++++++++++++++++ src/gui/qgspropertyassistantwidget.cpp | 16 +++ 7 files changed, 360 insertions(+), 3 deletions(-) diff --git a/python/core/qgshistogram.sip b/python/core/qgshistogram.sip index 16126ee5287..31aaef2a95a 100644 --- a/python/core/qgshistogram.sip +++ b/python/core/qgshistogram.sip @@ -22,7 +22,7 @@ class QgsHistogram */ void setValues( const QList& values ); - bool setValues( QgsVectorLayer* layer, const QString& fieldOrExpression, QgsFeedback* feedback = 0 ); + 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/gui/qgscurveeditorwidget.sip b/python/gui/qgscurveeditorwidget.sip index 799aa05acdd..7bbca6f781c 100644 --- a/python/gui/qgscurveeditorwidget.sip +++ b/python/gui/qgscurveeditorwidget.sip @@ -7,10 +7,18 @@ class QgsCurveEditorWidget : QWidget 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: diff --git a/src/core/qgshistogram.cpp b/src/core/qgshistogram.cpp index 2736ed105b9..12d80cb8023 100644 --- a/src/core/qgshistogram.cpp +++ b/src/core/qgshistogram.cpp @@ -47,7 +47,7 @@ void QgsHistogram::setValues( const QList &values ) prepareValues(); } -bool QgsHistogram::setValues( QgsVectorLayer *layer, const QString &fieldOrExpression, QgsFeedback* feedback ) +bool QgsHistogram::setValues( const QgsVectorLayer *layer, const QString &fieldOrExpression, QgsFeedback* feedback ) { mValues.clear(); if ( !layer ) diff --git a/src/core/qgshistogram.h b/src/core/qgshistogram.h index 61499f3d1e1..4d812080070 100644 --- a/src/core/qgshistogram.h +++ b/src/core/qgshistogram.h @@ -53,7 +53,7 @@ class CORE_EXPORT QgsHistogram * @param feedback optional feedback object to allow cancelation of calculation * @returns true if values were successfully set */ - bool setValues( QgsVectorLayer* layer, const QString& fieldOrExpression, QgsFeedback* feedback = nullptr ); + 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/gui/qgscurveeditorwidget.cpp b/src/gui/qgscurveeditorwidget.cpp index d3ae90e2865..93c622b6ac8 100644 --- a/src/gui/qgscurveeditorwidget.cpp +++ b/src/gui/qgscurveeditorwidget.cpp @@ -20,6 +20,7 @@ #include #include #include +#include // QWT Charting widget #include @@ -34,6 +35,13 @@ #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 ) @@ -86,6 +94,16 @@ QgsCurveEditorWidget::QgsCurveEditorWidget( QWidget* parent, const QgsCurveTrans 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; @@ -93,6 +111,53 @@ void QgsCurveEditorWidget::setCurve( const QgsCurveTransform& curve ) 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 ) @@ -205,6 +270,63 @@ void QgsCurveEditorWidget::addPlotMarker( double x, double y, bool isSelected ) 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 @@ -249,6 +371,52 @@ void QgsCurveEditorWidget::updatePlot() } +#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 ) @@ -309,4 +477,5 @@ QPointF QgsCurveEditorPlotEventFilter::mapPoint( QPointF point ) const mPlot->canvasMap( QwtPlot::yLeft ).invTransform( point.y() ) ); } + ///@endcond diff --git a/src/gui/qgscurveeditorwidget.h b/src/gui/qgscurveeditorwidget.h index c87be3294f3..62d8d17e9a8 100644 --- a/src/gui/qgscurveeditorwidget.h +++ b/src/gui/qgscurveeditorwidget.h @@ -17,14 +17,118 @@ #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. @@ -41,6 +145,8 @@ class GUI_EXPORT QgsCurveEditorWidget : public QWidget */ QgsCurveEditorWidget( QWidget* parent = nullptr, const QgsCurveTransform& curve = QgsCurveTransform() ); + ~QgsCurveEditorWidget(); + /** * Returns a curve representing the current curve from the widget. * @see setCurve() @@ -53,6 +159,45 @@ class GUI_EXPORT QgsCurveEditorWidget : public QWidget */ 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 @@ -79,11 +224,30 @@ class GUI_EXPORT QgsCurveEditorWidget : public QWidget 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 + }; diff --git a/src/gui/qgspropertyassistantwidget.cpp b/src/gui/qgspropertyassistantwidget.cpp index 59cfea5a12b..55700ec73bd 100644 --- a/src/gui/qgspropertyassistantwidget.cpp +++ b/src/gui/qgspropertyassistantwidget.cpp @@ -113,6 +113,18 @@ 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 ); @@ -185,6 +197,10 @@ void QgsPropertyAssistantWidget::computeValuesFromLayer() whileBlocking( minValueSpinBox )->setValue( minValue ); whileBlocking( maxValueSpinBox )->setValue( maxValue ); + + mCurveEditor->setMinHistogramValueRange( minValueSpinBox->value() ); + mCurveEditor->setMaxHistogramValueRange( maxValueSpinBox->value() ); + emit widgetChanged(); } From 1688ff5c4e6609d652d321b204a76edb76d85963 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 22 Feb 2017 11:12:31 +1000 Subject: [PATCH 7/8] Expand curve editor if enabled --- src/gui/qgspropertyassistantwidget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gui/qgspropertyassistantwidget.cpp b/src/gui/qgspropertyassistantwidget.cpp index 55700ec73bd..59e7593ed53 100644 --- a/src/gui/qgspropertyassistantwidget.cpp +++ b/src/gui/qgspropertyassistantwidget.cpp @@ -55,6 +55,7 @@ QgsPropertyAssistantWidget::QgsPropertyAssistantWidget( QWidget* parent , if ( initialState.transformer()->curveTransform() ) { mTransformCurveCheckBox->setChecked( true ); + mTransformCurveCheckBox->setCollapsed( false ); mCurveEditor->setCurve( *initialState.transformer()->curveTransform() ); } } From 7da28d19b1c09dee1f44921eb7338654c4cea35d Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 22 Feb 2017 11:16:38 +1000 Subject: [PATCH 8/8] Harmonize plot appearance between gradient editor and curve editor --- src/gui/qgscurveeditorwidget.cpp | 4 ++-- src/gui/qgsgradientcolorrampdialog.cpp | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/gui/qgscurveeditorwidget.cpp b/src/gui/qgscurveeditorwidget.cpp index 93c622b6ac8..5dd724d4b74 100644 --- a/src/gui/qgscurveeditorwidget.cpp +++ b/src/gui/qgscurveeditorwidget.cpp @@ -260,9 +260,9 @@ void QgsCurveEditorWidget::addPlotMarker( double x, double y, bool isSelected ) 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( 6, 6 ) ) ); + 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( 6, 6 ) ) ); + 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/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 );