Merge pull request #4169 from nyalldawson/props_gui

[FEATURE] Interactive curve editing for property overrides
This commit is contained in:
Nyall Dawson 2017-02-22 12:09:24 +10:00 committed by GitHub
commit bde4ff99c4
19 changed files with 1812 additions and 80 deletions

View File

@ -22,13 +22,7 @@ class QgsHistogram
*/
void setValues( const QList<double>& values );
/** Assigns numeric source values for the histogram from a vector layer's field or as the
* result of an expression.
* @param layer vector layer
* @param fieldOrExpression field name or expression to be evaluated
* @returns true if values were successfully set
*/
bool setValues( QgsVectorLayer* layer, const QString& fieldOrExpression );
bool setValues( const QgsVectorLayer* layer, const QString& fieldOrExpression, QgsFeedback* feedback = 0 );
/** Calculates the optimal bin width using the Freedman-Diaconis rule. Bins widths are
* determined by the inter-quartile range of values and the number of values.

View File

@ -1,3 +1,35 @@
class QgsCurveTransform
{
%TypeHeaderCode
#include <qgspropertytransformer.h>
%End
public:
QgsCurveTransform();
QgsCurveTransform( const QList< QgsPoint >& controlPoints );
~QgsCurveTransform();
QgsCurveTransform( const QgsCurveTransform& other );
//QgsCurveTransform& operator=( const QgsCurveTransform& other );
QList< QgsPoint > controlPoints() const;
void setControlPoints( const QList< QgsPoint >& points );
void addControlPoint( double x, double y );
void removeControlPoint( double x, double y );
double y( double x ) const;
QVector< double > y( const QVector< double >& x ) const;
bool readXml( const QDomElement& elem, const QDomDocument& doc );
bool writeXml( QDomElement& transformElem, QDomDocument& doc ) const;
};
class QgsPropertyTransformer
{
%TypeHeaderCode
@ -28,6 +60,8 @@ class QgsPropertyTransformer
QgsPropertyTransformer( double minValue = 0.0, double maxValue = 1.0 );
QgsPropertyTransformer( const QgsPropertyTransformer& other );
virtual ~QgsPropertyTransformer();
virtual Type transformerType() const = 0;
@ -46,11 +80,18 @@ class QgsPropertyTransformer
void setMaxValue( double max );
QgsCurveTransform* curveTransform() const;
void setCurveTransform( QgsCurveTransform* transform /Transfer/ );
virtual QVariant transform( const QgsExpressionContext& context, const QVariant& value ) const = 0;
virtual QString toExpression( const QString& baseExpression ) const = 0;
static QgsPropertyTransformer* fromExpression( const QString& expression, QString& baseExpression /Out/, QString& fieldName /Out/ ) /Factory/;
protected:
double transformNumeric( double input ) const;
};
class QgsGenericNumericTransformer : QgsPropertyTransformer
{
@ -66,6 +107,8 @@ class QgsGenericNumericTransformer : QgsPropertyTransformer
double nullOutput = 0.0,
double exponent = 1.0 );
QgsGenericNumericTransformer( const QgsGenericNumericTransformer& other );
virtual Type transformerType() const;
virtual QgsGenericNumericTransformer* clone() /Factory/;
virtual bool writeXml( QDomElement& transformerElem, QDomDocument& doc ) const;
@ -108,6 +151,8 @@ class QgsSizeScaleTransformer : QgsPropertyTransformer
double nullSize = 0.0,
double exponent = 1.0 );
QgsSizeScaleTransformer( const QgsSizeScaleTransformer& other );
virtual Type transformerType() const;
virtual QgsSizeScaleTransformer* clone() /Factory/;
virtual bool writeXml( QDomElement& transformerElem, QDomDocument& doc ) const;

View File

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

View File

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

View File

@ -0,0 +1,31 @@
class QgsCurveEditorWidget : QWidget
{
%TypeHeaderCode
#include <qgscurveeditorwidget.h>
%End
public:
QgsCurveEditorWidget( QWidget* parent /TransferThis/ = 0, const QgsCurveTransform& curve = QgsCurveTransform() );
~QgsCurveEditorWidget();
QgsCurveTransform curve() const;
void setCurve( const QgsCurveTransform& curve );
void setHistogramSource( const QgsVectorLayer* layer, const QString& expression );
double minHistogramValueRange() const;
double maxHistogramValueRange() const;
public slots:
void setMinHistogramValueRange( double minValueRange );
void setMaxHistogramValueRange( double maxValueRange );
signals:
void changed();
protected:
virtual void keyPressEvent( QKeyEvent *event );
};

View File

@ -47,14 +47,14 @@ void QgsHistogram::setValues( const QList<double> &values )
prepareValues();
}
bool QgsHistogram::setValues( QgsVectorLayer *layer, const QString &fieldOrExpression )
bool QgsHistogram::setValues( const QgsVectorLayer *layer, const QString &fieldOrExpression, QgsFeedback* feedback )
{
mValues.clear();
if ( !layer )
return false;
bool ok;
mValues = layer->getDoubleValues( fieldOrExpression, ok );
mValues = layer->getDoubleValues( fieldOrExpression, ok, false, nullptr, feedback );
if ( !ok )
return false;

View File

@ -21,6 +21,7 @@
#include <QList>
#include "qgis_core.h"
#include "qgsfeedback.h"
class QgsVectorLayer;
@ -49,9 +50,10 @@ class CORE_EXPORT QgsHistogram
* result of an expression.
* @param layer vector layer
* @param fieldOrExpression field name or expression to be evaluated
* @param feedback optional feedback object to allow cancelation of calculation
* @returns true if values were successfully set
*/
bool setValues( QgsVectorLayer* layer, const QString& fieldOrExpression );
bool setValues( const QgsVectorLayer* layer, const QString& fieldOrExpression, QgsFeedback* feedback = nullptr );
/** Calculates the optimal bin width using the Freedman-Diaconis rule. Bins widths are
* determined by the inter-quartile range of values and the number of values.

View File

@ -50,11 +50,32 @@ QgsPropertyTransformer::QgsPropertyTransformer( double minValue, double maxValue
, mMaxValue( maxValue )
{}
QgsPropertyTransformer::QgsPropertyTransformer( const QgsPropertyTransformer& other )
: mMinValue( other.mMinValue )
, mMaxValue( other.mMaxValue )
, mCurveTransform( other.mCurveTransform ? new QgsCurveTransform( *other.mCurveTransform ) : nullptr )
{}
QgsPropertyTransformer& QgsPropertyTransformer::operator=( const QgsPropertyTransformer & other )
{
mMinValue = other.mMinValue;
mMaxValue = other.mMaxValue;
mCurveTransform.reset( other.mCurveTransform ? new QgsCurveTransform( *other.mCurveTransform ) : nullptr );
return *this;
}
bool QgsPropertyTransformer::writeXml( QDomElement& transformerElem, QDomDocument& doc ) const
{
Q_UNUSED( doc );
transformerElem.setAttribute( "minValue", QString::number( mMinValue ) );
transformerElem.setAttribute( "maxValue", QString::number( mMaxValue ) );
if ( mCurveTransform )
{
QDomElement curveElement = doc.createElement( "curve" );
mCurveTransform->writeXml( curveElement, doc );
transformerElem.appendChild( curveElement );
}
return true;
}
@ -69,11 +90,35 @@ QgsPropertyTransformer* QgsPropertyTransformer::fromExpression( const QString& e
return nullptr;
}
double QgsPropertyTransformer::transformNumeric( double input ) const
{
if ( !mCurveTransform )
return input;
if ( qgsDoubleNear( mMaxValue, mMinValue ) )
return input;
// convert input into target range
double scaledInput = ( input - mMinValue ) / ( mMaxValue - mMinValue );
return mMinValue + ( mMaxValue - mMinValue ) * mCurveTransform->y( scaledInput );
}
bool QgsPropertyTransformer::readXml( const QDomElement &transformerElem, const QDomDocument &doc )
{
Q_UNUSED( doc );
mMinValue = transformerElem.attribute( "minValue", "0.0" ).toDouble();
mMaxValue = transformerElem.attribute( "maxValue", "1.0" ).toDouble();
mCurveTransform.reset( nullptr );
QDomNodeList curveNodeList = transformerElem.elementsByTagName( "curve" );
if ( !curveNodeList.isEmpty() )
{
QDomElement curveElem = curveNodeList.at( 0 ).toElement();
mCurveTransform.reset( new QgsCurveTransform() );
mCurveTransform->readXml( curveElem, doc );
}
return true;
}
@ -89,14 +134,35 @@ QgsGenericNumericTransformer::QgsGenericNumericTransformer( double minValue, dou
, mExponent( exponent )
{}
QgsGenericNumericTransformer::QgsGenericNumericTransformer( const QgsGenericNumericTransformer& other )
: QgsPropertyTransformer( other )
, mMinOutput( other.mMinOutput )
, mMaxOutput( other.mMaxOutput )
, mNullOutput( other.mNullOutput )
, mExponent( other.mExponent )
{}
QgsGenericNumericTransformer& QgsGenericNumericTransformer::operator=( const QgsGenericNumericTransformer & other )
{
QgsPropertyTransformer::operator=( other );
mMinOutput = other.mMinOutput;
mMaxOutput = other.mMaxOutput;
mNullOutput = other.mNullOutput;
mExponent = other.mExponent;
return *this;
}
QgsGenericNumericTransformer *QgsGenericNumericTransformer::clone()
{
return new QgsGenericNumericTransformer( mMinValue,
mMaxValue,
mMinOutput,
mMaxOutput,
mNullOutput,
mExponent );
std::unique_ptr< QgsGenericNumericTransformer > t( new QgsGenericNumericTransformer( mMinValue,
mMaxValue,
mMinOutput,
mMaxOutput,
mNullOutput,
mExponent ) );
if ( mCurveTransform )
t->setCurveTransform( new QgsCurveTransform( *mCurveTransform ) );
return t.release();
}
bool QgsGenericNumericTransformer::writeXml( QDomElement &transformerElem, QDomDocument &doc ) const
@ -126,6 +192,7 @@ bool QgsGenericNumericTransformer::readXml( const QDomElement &transformerElem,
double QgsGenericNumericTransformer::value( double input ) const
{
input = transformNumeric( input );
if ( qgsDoubleNear( mExponent, 1.0 ) )
return mMinOutput + ( qBound( mMinValue, input, mMaxValue ) - mMinValue ) * ( mMaxOutput - mMinOutput ) / ( mMaxValue - mMinValue );
else
@ -257,15 +324,38 @@ QgsSizeScaleTransformer::QgsSizeScaleTransformer( ScaleType type, double minValu
setType( type );
}
QgsSizeScaleTransformer::QgsSizeScaleTransformer( const QgsSizeScaleTransformer& other )
: QgsPropertyTransformer( other )
, mType( other.mType )
, mMinSize( other.mMinSize )
, mMaxSize( other.mMaxSize )
, mNullSize( other.mNullSize )
, mExponent( other.mExponent )
{}
QgsSizeScaleTransformer& QgsSizeScaleTransformer::operator=( const QgsSizeScaleTransformer & other )
{
QgsPropertyTransformer::operator=( other );
mType = other.mType;
mMinSize = other.mMinSize;
mMaxSize = other.mMaxSize;
mNullSize = other.mNullSize;
mExponent = other.mExponent;
return *this;
}
QgsSizeScaleTransformer *QgsSizeScaleTransformer::clone()
{
return new QgsSizeScaleTransformer( mType,
mMinValue,
mMaxValue,
mMinSize,
mMaxSize,
mNullSize,
mExponent );
std::unique_ptr< QgsSizeScaleTransformer > t( new QgsSizeScaleTransformer( mType,
mMinValue,
mMaxValue,
mMinSize,
mMaxSize,
mNullSize,
mExponent ) );
if ( mCurveTransform )
t->setCurveTransform( new QgsCurveTransform( *mCurveTransform ) );
return t.release();
}
bool QgsSizeScaleTransformer::writeXml( QDomElement &transformerElem, QDomDocument &doc ) const
@ -297,6 +387,8 @@ bool QgsSizeScaleTransformer::readXml( const QDomElement &transformerElem, const
double QgsSizeScaleTransformer::size( double value ) const
{
value = transformNumeric( value );
switch ( mType )
{
case Linear:
@ -483,6 +575,7 @@ QgsColorRampTransformer::QgsColorRampTransformer( const QgsColorRampTransformer
QgsColorRampTransformer &QgsColorRampTransformer::operator=( const QgsColorRampTransformer & other )
{
QgsPropertyTransformer::operator=( other );
mMinValue = other.mMinValue;
mMaxValue = other.mMaxValue;
mGradientRamp.reset( other.mGradientRamp ? other.mGradientRamp->clone() : nullptr );
@ -493,11 +586,13 @@ QgsColorRampTransformer &QgsColorRampTransformer::operator=( const QgsColorRampT
QgsColorRampTransformer* QgsColorRampTransformer::clone()
{
QgsColorRampTransformer* c = new QgsColorRampTransformer( mMinValue, mMaxValue,
std::unique_ptr< QgsColorRampTransformer > c( new QgsColorRampTransformer( mMinValue, mMaxValue,
mGradientRamp ? mGradientRamp->clone() : nullptr,
mNullColor );
mNullColor ) );
c->setRampName( mRampName );
return c;
if ( mCurveTransform )
c->setCurveTransform( new QgsCurveTransform( *mCurveTransform ) );
return c.release();
}
bool QgsColorRampTransformer::writeXml( QDomElement &transformerElem, QDomDocument &doc ) const
@ -569,6 +664,7 @@ QString QgsColorRampTransformer::toExpression( const QString& baseExpression ) c
QColor QgsColorRampTransformer::color( double value ) const
{
value = transformNumeric( value );
double scaledVal = qBound( 0.0, ( value - mMinValue ) / ( mMaxValue - mMinValue ), 1.0 );
if ( !mGradientRamp )
@ -586,3 +682,340 @@ void QgsColorRampTransformer::setColorRamp( QgsColorRamp* ramp )
{
mGradientRamp.reset( ramp );
}
//
// QgsCurveTransform
//
bool sortByX( const QgsPoint& a, const QgsPoint& b )
{
return a.x() < b.x();
}
QgsCurveTransform::QgsCurveTransform()
{
mControlPoints << QgsPoint( 0, 0 ) << QgsPoint( 1, 1 );
calcSecondDerivativeArray();
}
QgsCurveTransform::QgsCurveTransform( const QList<QgsPoint>& 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<QgsPoint>& points )
{
mControlPoints = points;
std::sort( mControlPoints.begin(), mControlPoints.end(), sortByX );
for ( int i = 0; i < mControlPoints.count(); ++i )
{
mControlPoints[ i ] = QgsPoint( qBound( 0.0, mControlPoints.at( i ).x(), 1.0 ),
qBound( 0.0, mControlPoints.at( i ).y(), 1.0 ) );
}
calcSecondDerivativeArray();
}
void QgsCurveTransform::addControlPoint( double x, double y )
{
QgsPoint point( x, y );
if ( mControlPoints.contains( point ) )
return;
mControlPoints << point;
std::sort( mControlPoints.begin(), mControlPoints.end(), sortByX );
calcSecondDerivativeArray();
}
void QgsCurveTransform::removeControlPoint( double x, double y )
{
for ( int i = 0; i < mControlPoints.count(); ++i )
{
if ( qgsDoubleNear( mControlPoints.at( i ).x(), x )
&& qgsDoubleNear( mControlPoints.at( i ).y(), y ) )
{
mControlPoints.removeAt( i );
break;
}
}
calcSecondDerivativeArray();
}
// this code is adapted from https://github.com/OpenFibers/Photoshop-Curves
// which in turn was adapted from
// http://www.developpez.net/forums/d331608-3/autres-langages/algorithmes/contribuez/image-interpolation-spline-cubique/#post3513925 //#spellok
double QgsCurveTransform::y( double x ) const
{
int n = mControlPoints.count();
if ( n < 2 )
return qBound( 0.0, x, 1.0 ); // invalid
else if ( n < 3 )
{
// linear
if ( x <= mControlPoints.at( 0 ).x() )
return qBound( 0.0, mControlPoints.at( 0 ).y(), 1.0 );
else if ( x >= mControlPoints.at( n - 1 ).x() )
return qBound( 0.0, mControlPoints.at( 1 ).y(), 1.0 );
else
{
double dx = mControlPoints.at( 1 ).x() - mControlPoints.at( 0 ).x();
double dy = mControlPoints.at( 1 ).y() - mControlPoints.at( 0 ).y();
return qBound( 0.0, ( x - mControlPoints.at( 0 ).x() ) * ( dy / dx ) + mControlPoints.at( 0 ).y(), 1.0 );
}
}
// safety check
if ( x <= mControlPoints.at( 0 ).x() )
return qBound( 0.0, mControlPoints.at( 0 ).y(), 1.0 );
if ( x >= mControlPoints.at( n - 1 ).x() )
return qBound( 0.0, mControlPoints.at( n - 1 ).y(), 1.0 );
// find corresponding segment
QList<QgsPoint>::const_iterator pointIt = mControlPoints.constBegin();
QgsPoint currentControlPoint = *pointIt;
++pointIt;
QgsPoint nextControlPoint = *pointIt;
for ( int i = 0; i < n - 1; ++i )
{
if ( x < nextControlPoint.x() )
{
// found segment
double h = nextControlPoint.x() - currentControlPoint.x();
double t = ( x - currentControlPoint.x() ) / h;
double a = 1 - t;
return qBound( 0.0, a*currentControlPoint.y() + t*nextControlPoint.y() + ( h*h / 6 )*(( a*a*a - a )*mSecondDerivativeArray[i] + ( t*t*t - t )*mSecondDerivativeArray[i+1] ),
1.0 );
}
++pointIt;
if ( pointIt == mControlPoints.constEnd() )
break;
currentControlPoint = nextControlPoint;
nextControlPoint = *pointIt;
}
//should not happen
return qBound( 0.0, x, 1.0 );
}
// this code is adapted from https://github.com/OpenFibers/Photoshop-Curves
// which in turn was adapted from
// http://www.developpez.net/forums/d331608-3/autres-langages/algorithmes/contribuez/image-interpolation-spline-cubique/#post3513925 //#spellok
QVector<double> QgsCurveTransform::y( const QVector<double>& x ) const
{
QVector<double> 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<QgsPoint>::const_iterator pointIt = mControlPoints.constBegin();
QgsPoint currentControlPoint = *pointIt;
++pointIt;
QgsPoint nextControlPoint = *pointIt;
int xIndex = 0;
double currentX = x.at( xIndex );
// safety check
while ( currentX <= currentControlPoint.x() )
{
result << qBound( 0.0, currentControlPoint.y(), 1.0 );
xIndex++;
currentX = x.at( xIndex );
}
for ( int i = 0; i < n - 1; ++i )
{
while ( currentX < nextControlPoint.x() )
{
// found segment
double h = nextControlPoint.x() - currentControlPoint.x();
double t = ( currentX - currentControlPoint.x() ) / h;
double a = 1 - t;
result << qBound( 0.0, a*currentControlPoint.y() + t*nextControlPoint.y() + ( h*h / 6 )*(( a*a*a - a )*mSecondDerivativeArray[i] + ( t*t*t - t )*mSecondDerivativeArray[i+1] ), 1.0 );
xIndex++;
if ( xIndex == x.count() )
return result;
currentX = x.at( xIndex );
}
++pointIt;
if ( pointIt == mControlPoints.constEnd() )
break;
currentControlPoint = nextControlPoint;
nextControlPoint = *pointIt;
}
// safety check
while ( xIndex < x.count() )
{
result << qBound( 0.0, nextControlPoint.y(), 1.0 );
xIndex++;
}
return result;
}
bool QgsCurveTransform::readXml( const QDomElement& elem, const QDomDocument& )
{
QString xString = elem.attribute( QStringLiteral( "x" ) );
QString yString = elem.attribute( QStringLiteral( "y" ) );
QStringList xVals = xString.split( ',' );
QStringList yVals = yString.split( ',' );
if ( xVals.count() != yVals.count() )
return false;
QList< QgsPoint > newPoints;
bool ok = false;
for ( int i = 0; i < xVals.count(); ++i )
{
double x = xVals.at( i ).toDouble( &ok );
if ( !ok )
return false;
double y = yVals.at( i ).toDouble( &ok );
if ( !ok )
return false;
newPoints << QgsPoint( x, y );
}
setControlPoints( newPoints );
return true;
}
bool QgsCurveTransform::writeXml( QDomElement& transformElem, QDomDocument& ) const
{
QStringList x;
QStringList y;
Q_FOREACH ( const QgsPoint& p, mControlPoints )
{
x << qgsDoubleToString( p.x() );
y << qgsDoubleToString( p.y() );
}
transformElem.setAttribute( QStringLiteral( "x" ), x.join( ',' ) );
transformElem.setAttribute( QStringLiteral( "y" ), y.join( ',' ) );
return true;
}
// this code is adapted from https://github.com/OpenFibers/Photoshop-Curves
// which in turn was adapted from
// http://www.developpez.net/forums/d331608-3/autres-langages/algorithmes/contribuez/image-interpolation-spline-cubique/#post3513925 //#spellok
void QgsCurveTransform::calcSecondDerivativeArray()
{
int n = mControlPoints.count();
if ( n < 3 )
return; // cannot proceed
delete[] mSecondDerivativeArray;
double* matrix = new double[ n * 3 ];
double* result = new double[ n ];
matrix[0] = 0;
matrix[1] = 1;
matrix[2] = 0;
result[0] = 0;
QList<QgsPoint>::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;
}

View File

@ -18,6 +18,7 @@
#include "qgis_core.h"
#include "qgsexpression.h"
#include "qgsexpressioncontext.h"
#include "qgspoint.h"
#include <QVariant>
#include <QHash>
#include <QString>
@ -26,9 +27,123 @@
#include <QDomDocument>
#include <QColor>
#include <memory>
#include <algorithm>
class QgsColorRamp;
/**
* \ingroup core
* \class QgsCurveTransform
* \brief Handles scaling of input values to output values by using a curve created
* from smoothly joining a number of set control points.
*
* QgsCurveTransform assists in creation of curve type transforms, typically seen in
* raster image editing software (eg the curves dialog in GIMP or Photoshop).
* Transforms are created by passing a number of set control points through which
* the transform curve must pass. The curve is guaranteed to exactly pass through
* these control points. Between control points the curve is smoothly interpolated
* so that no disjoint sections or "corners" are present.
*
* If the first or last control point are not located at x = 0 and x = 1 respectively,
* then values outside this range will be mapped to the y value of either the first
* or last control point. In other words, the curve will have a flat segment
* for values outside of the control point range.
*
* \note Added in version 3.0
*/
class CORE_EXPORT QgsCurveTransform
{
public:
/**
* Constructs a default QgsCurveTransform which linearly maps values
* between 0 and 1 unchanged. I.e. y == x.
*/
QgsCurveTransform();
/**
* Constructs a QgsCurveTransform using a specified list of \a controlPoints.
* Behavior is undefined if duplicate x values exist in the control points
* list.
*/
QgsCurveTransform( const QList< QgsPoint >& controlPoints );
~QgsCurveTransform();
/**
* Copy constructor
*/
QgsCurveTransform( const QgsCurveTransform& other );
QgsCurveTransform& operator=( const QgsCurveTransform& other );
/**
* Returns a list of the control points for the transform.
* @see setControlPoints()
*/
QList< QgsPoint > controlPoints() const { return mControlPoints; }
/**
* Sets the list of control points for the transform. Any existing
* points are removed.
* @see controlPoints()
*/
void setControlPoints( const QList< QgsPoint >& points );
/**
* Adds a control point to the transform. Behavior is undefined if duplicate
* x values exist in the control points list.
* @see removeControlPoint()
*/
void addControlPoint( double x, double y );
/**
* Removes a control point from the transform. This will have no effect if a
* matching control point does not exist.
* @see addControlPoint()
*/
void removeControlPoint( double x, double y );
/**
* Returns the mapped y value corresponding to the specified \a x value.
*/
double y( double x ) const;
/**
* Returns a list of y values corresponding to a list of \a x values.
* Calling this method is faster then calling the double variant multiple
* times.
*/
QVector< double > y( const QVector< double >& x ) const;
/**
* Reads the curve's state from an XML element.
* @param elem source DOM element for transform's state
* @param doc DOM document
* @see writeXml()
*/
bool readXml( const QDomElement& elem, const QDomDocument& doc );
/**
* Writes the current state of the transform into an XML element
* @param transformElem destination element for the transform's state
* @param doc DOM document
* @see readXml()
*/
bool writeXml( QDomElement& transformElem, QDomDocument& doc ) const;
private:
void calcSecondDerivativeArray();
QList< QgsPoint > mControlPoints;
double* mSecondDerivativeArray = nullptr;
};
/**
* \ingroup core
* \class QgsPropertyTransformer
@ -62,6 +177,12 @@ class CORE_EXPORT QgsPropertyTransformer
*/
QgsPropertyTransformer( double minValue = 0.0, double maxValue = 1.0 );
/**
* Copy constructor.
*/
QgsPropertyTransformer( const QgsPropertyTransformer& other );
QgsPropertyTransformer& operator=( const QgsPropertyTransformer& other );
virtual ~QgsPropertyTransformer() = default;
/**
@ -120,6 +241,21 @@ class CORE_EXPORT QgsPropertyTransformer
*/
void setMaxValue( double max ) { mMaxValue = max; }
/**
* Returns the curve transform applied to input values before they are transformed
* by the individual transform subclasses.
* @see setCurveTransform()
*/
QgsCurveTransform* curveTransform() const { return mCurveTransform.get(); }
/**
* Sets a curve transform to apply to input values before they are transformed
* by the individual transform subclasses. Ownership of \a transform is transferred
* to the property transformer.
* @see curveTransform()
*/
void setCurveTransform( QgsCurveTransform* transform ) { mCurveTransform.reset( transform ); }
/**
* Calculates the transform of a value. Derived classes must implement this to perform their transformations
* on input values
@ -156,6 +292,15 @@ class CORE_EXPORT QgsPropertyTransformer
//! Maximum value expected by the transformer
double mMaxValue;
//! Optional curve transform
std::unique_ptr< QgsCurveTransform > mCurveTransform;
/**
* Applies base class numeric transformations. Derived classes should call this
* to transform an \a input numeric value before they apply any transform to the result.
* This applies any curve transforms which may exist on the transformer.
*/
double transformNumeric( double input ) const;
};
/**
@ -185,6 +330,12 @@ class CORE_EXPORT QgsGenericNumericTransformer : public QgsPropertyTransformer
double nullOutput = 0.0,
double exponent = 1.0 );
/**
* Copy constructor.
*/
QgsGenericNumericTransformer( const QgsGenericNumericTransformer& other );
QgsGenericNumericTransformer& operator=( const QgsGenericNumericTransformer& other );
virtual Type transformerType() const override { return GenericNumericTransformer; }
virtual QgsGenericNumericTransformer* clone() override;
virtual bool writeXml( QDomElement& transformerElem, QDomDocument& doc ) const override;
@ -315,6 +466,12 @@ class CORE_EXPORT QgsSizeScaleTransformer : public QgsPropertyTransformer
double nullSize = 0.0,
double exponent = 1.0 );
/**
* Copy constructor.
*/
QgsSizeScaleTransformer( const QgsSizeScaleTransformer& other );
QgsSizeScaleTransformer& operator=( const QgsSizeScaleTransformer& other );
virtual Type transformerType() const override { return SizeScaleTransformer; }
virtual QgsSizeScaleTransformer* clone() override;
virtual bool writeXml( QDomElement& transformerElem, QDomDocument& doc ) const override;

View File

@ -3565,7 +3565,7 @@ QVariant QgsVectorLayer::aggregate( QgsAggregateCalculator::Aggregate aggregate,
return c.calculate( aggregate, fieldOrExpression, context, ok );
}
QList<QVariant> QgsVectorLayer::getValues( const QString &fieldOrExpression, bool& ok, bool selectedOnly ) const
QList<QVariant> QgsVectorLayer::getValues( const QString &fieldOrExpression, bool& ok, bool selectedOnly, QgsFeedback* feedback ) const
{
QList<QVariant> values;
@ -3623,12 +3623,17 @@ QList<QVariant> QgsVectorLayer::getValues( const QString &fieldOrExpression, boo
{
values << f.attribute( attrNum );
}
if ( feedback && feedback->isCanceled() )
{
ok = false;
return values;
}
}
ok = true;
return values;
}
QList<double> QgsVectorLayer::getDoubleValues( const QString &fieldOrExpression, bool& ok, bool selectedOnly, int* nullCount ) const
QList<double> QgsVectorLayer::getDoubleValues( const QString &fieldOrExpression, bool& ok, bool selectedOnly, int* nullCount, QgsFeedback* feedback ) const
{
QList<double> values;
@ -3650,6 +3655,11 @@ QList<double> QgsVectorLayer::getDoubleValues( const QString &fieldOrExpression,
if ( nullCount )
*nullCount += 1;
}
if ( feedback && feedback->isCanceled() )
{
ok = false;
return values;
}
}
ok = true;
return values;

View File

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

View File

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

View File

@ -0,0 +1,481 @@
/***************************************************************************
qgscurveeditorwidget.cpp
------------------------
begin : February 2017
copyright : (C) 2017 by Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgscurveeditorwidget.h"
#include <qmath.h>
#include <QPainter>
#include <QVBoxLayout>
#include <QMouseEvent>
#include <algorithm>
// QWT Charting widget
#include <qwt_global.h>
#include <qwt_plot_canvas.h>
#include <qwt_plot.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_grid.h>
#include <qwt_plot_marker.h>
#include <qwt_plot_picker.h>
#include <qwt_picker_machine.h>
#include <qwt_plot_layout.h>
#include <qwt_symbol.h>
#include <qwt_legend.h>
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000
#include <qwt_plot_renderer.h>
#include <qwt_plot_histogram.h>
#else
#include "../raster/qwt5_histogram_item.h"
#endif
QgsCurveEditorWidget::QgsCurveEditorWidget( QWidget* parent, const QgsCurveTransform& transform )
: QWidget( parent )
, mCurve( transform )
, mCurrentPlotMarkerIndex( -1 )
{
mPlot = new QwtPlot();
mPlot->setMinimumSize( QSize( 0, 100 ) );
mPlot->setAxisScale( QwtPlot::yLeft, 0, 1 );
mPlot->setAxisScale( QwtPlot::yRight, 0, 1 );
mPlot->setAxisScale( QwtPlot::xBottom, 0, 1 );
mPlot->setAxisScale( QwtPlot::xTop, 0, 1 );
QVBoxLayout* vlayout = new QVBoxLayout();
vlayout->addWidget( mPlot );
setLayout( vlayout );
// hide the ugly canvas frame
mPlot->setFrameStyle( QFrame::NoFrame );
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000
QFrame* plotCanvasFrame = dynamic_cast<QFrame*>( 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<double>(), QList<double>(), QList<double>() << 0.2 << 0.4 << 0.6 << 0.8 );
grid->setXDiv( gridDiv );
grid->setYDiv( gridDiv );
grid->setPen( QPen( QColor( 0, 0, 0, 50 ) ) );
grid->attach( mPlot );
mPlotCurve = new QwtPlotCurve();
mPlotCurve->setTitle( QStringLiteral( "Curve" ) );
mPlotCurve->setPen( QPen( QColor( 30, 30, 30 ), 0.0 ) ),
mPlotCurve->setRenderHint( QwtPlotItem::RenderAntialiased, true );
mPlotCurve->attach( mPlot );
mPlotFilter = new QgsCurveEditorPlotEventFilter( mPlot );
connect( mPlotFilter, &QgsCurveEditorPlotEventFilter::mousePress, this, &QgsCurveEditorWidget::plotMousePress );
connect( mPlotFilter, &QgsCurveEditorPlotEventFilter::mouseRelease, this, &QgsCurveEditorWidget::plotMouseRelease );
connect( mPlotFilter, &QgsCurveEditorPlotEventFilter::mouseMove, this, &QgsCurveEditorWidget::plotMouseMove );
mPlotCurve->setVisible( true );
updatePlot();
}
QgsCurveEditorWidget::~QgsCurveEditorWidget()
{
if ( mGatherer && mGatherer->isRunning() )
{
connect( mGatherer.get(), &QgsHistogramValuesGatherer::finished, mGatherer.get(), &QgsHistogramValuesGatherer::deleteLater );
mGatherer->stop();
( void )mGatherer.release();
}
}
void QgsCurveEditorWidget::setCurve( const QgsCurveTransform& curve )
{
mCurve = curve;
updatePlot();
emit changed();
}
void QgsCurveEditorWidget::setHistogramSource( const QgsVectorLayer* layer, const QString& expression )
{
if ( !mGatherer )
{
mGatherer.reset( new QgsHistogramValuesGatherer() );
connect( mGatherer.get(), &QgsHistogramValuesGatherer::calculatedHistogram, this, [=]
{
mHistogram.reset( new QgsHistogram( mGatherer->histogram() ) );
updateHistogram();
} );
}
bool changed = mGatherer->layer() != layer || mGatherer->expression() != expression;
if ( changed )
{
mGatherer->setExpression( expression );
mGatherer->setLayer( layer );
mGatherer->start();
if ( mGatherer->isRunning() )
{
//stop any currently running task
mGatherer->stop();
while ( mGatherer->isRunning() )
{
QCoreApplication::processEvents();
}
}
mGatherer->start();
}
else
{
updateHistogram();
}
}
void QgsCurveEditorWidget::setMinHistogramValueRange( double minValueRange )
{
mMinValueRange = minValueRange;
updateHistogram();
}
void QgsCurveEditorWidget::setMaxHistogramValueRange( double maxValueRange )
{
mMaxValueRange = maxValueRange;
updateHistogram();
}
void QgsCurveEditorWidget::keyPressEvent( QKeyEvent* event )
{
if ( event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace )
{
QList< QgsPoint > cp = mCurve.controlPoints();
if ( mCurrentPlotMarkerIndex > 0 && mCurrentPlotMarkerIndex < cp.count() - 1 )
{
cp.removeAt( mCurrentPlotMarkerIndex );
mCurve.setControlPoints( cp );
updatePlot();
emit changed();
}
}
}
void QgsCurveEditorWidget::plotMousePress( QPointF point )
{
mCurrentPlotMarkerIndex = findNearestControlPoint( point );
if ( mCurrentPlotMarkerIndex < 0 )
{
// add a new point
mCurve.addControlPoint( point.x(), point.y() );
mCurrentPlotMarkerIndex = findNearestControlPoint( point );
emit changed();
}
updatePlot();
}
int QgsCurveEditorWidget::findNearestControlPoint( QPointF point ) const
{
double minDist = 3.0 / mPlot->width();
int currentPlotMarkerIndex = -1;
QList< QgsPoint > controlPoints = mCurve.controlPoints();
for ( int i = 0; i < controlPoints.count(); ++i )
{
QgsPoint currentPoint = controlPoints.at( i );
double currentDist;
currentDist = qPow( point.x() - currentPoint.x(), 2.0 ) + qPow( point.y() - currentPoint.y(), 2.0 );
if ( currentDist < minDist )
{
minDist = currentDist;
currentPlotMarkerIndex = i;
}
}
return currentPlotMarkerIndex;
}
void QgsCurveEditorWidget::plotMouseRelease( QPointF )
{
}
void QgsCurveEditorWidget::plotMouseMove( QPointF point )
{
if ( mCurrentPlotMarkerIndex < 0 )
return;
QList< QgsPoint > cp = mCurve.controlPoints();
bool removePoint = false;
if ( mCurrentPlotMarkerIndex == 0 )
{
point.setX( qMin( point.x(), cp.at( 1 ).x() - 0.01 ) );
}
else
{
removePoint = point.x() <= cp.at( mCurrentPlotMarkerIndex - 1 ).x();
}
if ( mCurrentPlotMarkerIndex == cp.count() - 1 )
{
point.setX( qMax( point.x(), cp.at( mCurrentPlotMarkerIndex - 1 ).x() + 0.01 ) );
removePoint = false;
}
else
{
removePoint = removePoint || point.x() >= cp.at( mCurrentPlotMarkerIndex + 1 ).x();
}
if ( removePoint )
{
cp.removeAt( mCurrentPlotMarkerIndex );
mCurrentPlotMarkerIndex = -1;
}
else
{
cp[ mCurrentPlotMarkerIndex ] = QgsPoint( point.x(), point.y() );
}
mCurve.setControlPoints( cp );
updatePlot();
emit changed();
}
void QgsCurveEditorWidget::addPlotMarker( double x, double y, bool isSelected )
{
QColor borderColor( 0, 0, 0 );
QColor brushColor = isSelected ? borderColor : QColor( 255, 255, 255, 0 );
QwtPlotMarker *marker = new QwtPlotMarker();
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000
marker->setSymbol( new QwtSymbol( QwtSymbol::Ellipse, QBrush( brushColor ), QPen( borderColor, isSelected ? 2 : 1 ), QSize( 8, 8 ) ) );
#else
marker->setSymbol( QwtSymbol( QwtSymbol::Ellipse, QBrush( brushColor ), QPen( borderColor, isSelected ? 2 : 1 ), QSize( 8, 8 ) ) );
#endif
marker->setValue( x, y );
marker->attach( mPlot );
marker->setRenderHint( QwtPlotItem::RenderAntialiased, true );
mMarkers << marker;
}
void QgsCurveEditorWidget::updateHistogram()
{
if ( !mHistogram )
return;
//draw histogram
QBrush histoBrush( QColor( 0, 0, 0, 70 ) );
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000
delete mPlotHistogram;
mPlotHistogram = createPlotHistogram( histoBrush );
QVector<QwtIntervalSample> dataHisto;
#else
delete mPlotHistogramItem;
mPlotHistogramItem = createHistoItem( histoBrush );
QwtArray<QwtDoubleInterval> intervalsHisto;
QwtArray<double> valuesHisto;
#endif
int bins = 40;
QList<double> edges = mHistogram->binEdges( bins );
QList<int> counts = mHistogram->counts( bins );
// scale counts to 0->1
double max = *std::max_element( counts.constBegin(), counts.constEnd() );
// scale bin edges to fit in 0->1 range
if ( !qgsDoubleNear( mMinValueRange, mMaxValueRange ) )
{
std::transform( edges.begin(), edges.end(), edges.begin(),
[this]( double d ) -> double { return ( d - mMinValueRange ) / ( mMaxValueRange - mMinValueRange ); } );
}
for ( int bin = 0; bin < bins; ++bin )
{
double binValue = counts.at( bin ) / max;
double upperEdge = edges.at( bin + 1 );
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000
dataHisto << QwtIntervalSample( binValue, edges.at( bin ), upperEdge );
#else
intervalsHisto.append( QwtDoubleInterval( edges.at( bin ), upperEdge ) );
valuesHisto.append( double( binValue ) );
#endif
}
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000
mPlotHistogram->setSamples( dataHisto );
mPlotHistogram->attach( mPlot );
#else
mPlotHistogramItem->setData( QwtIntervalData( intervalsHisto, valuesHisto ) );
mPlotHistogramItem->attach( mPlot );
#endif
mPlot->replot();
}
void QgsCurveEditorWidget::updatePlot()
{
// remove existing markers
Q_FOREACH ( QwtPlotMarker* marker, mMarkers )
{
marker->detach();
delete marker;
}
mMarkers.clear();
QPolygonF curvePoints;
QVector< double > x;
int i = 0;
Q_FOREACH ( const QgsPoint& point, mCurve.controlPoints() )
{
x << point.x();
addPlotMarker( point.x(), point.y(), mCurrentPlotMarkerIndex == i );
i++;
}
//add extra intermediate points
for ( double p = 0; p <= 1.0; p += 0.01 )
{
x << p;
}
std::sort( x.begin(), x.end() );
QVector< double > y = mCurve.y( x );
for ( int j = 0; j < x.count(); ++j )
{
curvePoints << QPointF( x.at( j ), y.at( j ) );
}
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000
mPlotCurve->setSamples( curvePoints );
#else
mPlotCurve->setData( curvePoints );
#endif
mPlot->replot();
}
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000
QwtPlotHistogram* QgsCurveEditorWidget::createPlotHistogram( const QBrush& brush, const QPen& pen ) const
{
QwtPlotHistogram* histogram = new QwtPlotHistogram( QString() );
histogram->setBrush( brush );
if ( pen != Qt::NoPen )
{
histogram->setPen( pen );
}
else if ( brush.color().lightness() > 200 )
{
QPen p;
p.setColor( brush.color().darker( 150 ) );
p.setWidth( 0 );
p.setCosmetic( true );
histogram->setPen( p );
}
else
{
histogram->setPen( QPen( Qt::NoPen ) );
}
return histogram;
}
#else
HistogramItem * QgsCurveEditorWidget::createHistoItem( const QBrush& brush, const QPen& pen ) const
{
HistogramItem* item = new HistogramItem( QString() );
item->setColor( brush.color() );
item->setFlat( true );
item->setSpacing( 0 );
if ( pen != Qt::NoPen )
{
item->setPen( pen );
}
else if ( brush.color().lightness() > 200 )
{
QPen p;
p.setColor( brush.color().darker( 150 ) );
p.setWidth( 0 );
p.setCosmetic( true );
item->setPen( p );
}
return item;
}
#endif
/// @cond PRIVATE
QgsCurveEditorPlotEventFilter::QgsCurveEditorPlotEventFilter( QwtPlot *plot )
: QObject( plot )
, mPlot( plot )
{
mPlot->canvas()->installEventFilter( this );
}
bool QgsCurveEditorPlotEventFilter::eventFilter( QObject *object, QEvent *event )
{
if ( !mPlot->isEnabled() )
return QObject::eventFilter( object, event );
switch ( event->type() )
{
case QEvent::MouseButtonPress:
{
const QMouseEvent* mouseEvent = static_cast<QMouseEvent* >( event );
if ( mouseEvent->button() == Qt::LeftButton )
{
emit mousePress( mapPoint( mouseEvent->pos() ) );
}
break;
}
case QEvent::MouseMove:
{
const QMouseEvent* mouseEvent = static_cast<QMouseEvent* >( 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<QMouseEvent* >( 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

View File

@ -0,0 +1,282 @@
/***************************************************************************
qgscurveeditorwidget.h
----------------------
begin : February 2017
copyright : (C) 2017 by Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#ifndef QGSCURVEEDITORWIDGET_H
#define QGSCURVEEDITORWIDGET_H
#include <QWidget>
#include <QThread>
#include <QMutex>
#include <QPen>
#include <QPointer>
#include <qwt_global.h>
#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<double>() );
return;
}
// allow responsive cancelation
mFeedback = new QgsFeedback();
mHistogram.setValues( mLayer, mExpression, mFeedback );
// be overly cautious - it's *possible* stop() might be called between deleting mFeedback and nulling it
mFeedbackMutex.lock();
delete mFeedback;
mFeedback = nullptr;
mFeedbackMutex.unlock();
emit calculatedHistogram();
}
//! Informs the gatherer to immediately stop collecting values
void stop()
{
// be cautious, in case gatherer stops naturally just as we are canceling it and mFeedback gets deleted
mFeedbackMutex.lock();
if ( mFeedback )
mFeedback->cancel();
mFeedbackMutex.unlock();
mWasCanceled = true;
}
//! Returns true if collection was canceled before completion
bool wasCanceled() const { return mWasCanceled; }
const QgsHistogram& histogram() const { return mHistogram; }
const QgsVectorLayer* layer() const
{
return mLayer;
}
void setLayer( const QgsVectorLayer* layer )
{
mLayer = const_cast< QgsVectorLayer* >( layer );
}
QString expression() const
{
return mExpression;
}
void setExpression( const QString& expression )
{
mExpression = expression;
}
signals:
/**
* Emitted when histogram has been calculated
*/
void calculatedHistogram();
private:
QPointer< const QgsVectorLayer > mLayer = nullptr;
QString mExpression;
QgsHistogram mHistogram;
QgsFeedback* mFeedback = nullptr;
QMutex mFeedbackMutex;
bool mWasCanceled = false;
};
///@endcond
/** \ingroup gui
* \class QgsCurveEditorWidget
* A widget for manipulating QgsCurveTransform curves.
* \note added in QGIS 3.0
*/
class GUI_EXPORT QgsCurveEditorWidget : public QWidget
{
Q_OBJECT
public:
/**
* Constructor for QgsCurveEditorWidget.
*/
QgsCurveEditorWidget( QWidget* parent = nullptr, const QgsCurveTransform& curve = QgsCurveTransform() );
~QgsCurveEditorWidget();
/**
* Returns a curve representing the current curve from the widget.
* @see setCurve()
*/
QgsCurveTransform curve() const { return mCurve; }
/**
* Sets the \a curve to show in the widget.
* @see curve()
*/
void setCurve( const QgsCurveTransform& curve );
/**
* Sets a \a layer and \a expression source for values to show in a histogram
* behind the curve. The histogram is generated in a background thread to keep
* the widget responsive.
* @see minHistogramValueRange()
* @see maxHistogramValueRange()
*/
void setHistogramSource( const QgsVectorLayer* layer, const QString& expression );
/**
* Returns the minimum expected value for the range of values shown in the histogram.
* @see maxHistogramValueRange()
* @see setMinHistogramValueRange()
*/
double minHistogramValueRange() const { return mMinValueRange; }
/**
* Returns the maximum expected value for the range of values shown in the histogram.
* @see minHistogramValueRange()
* @see setMaxHistogramValueRange()
*/
double maxHistogramValueRange() const { return mMaxValueRange; }
public slots:
/**
* Sets the minimum expected value for the range of values shown in the histogram.
* @see setMaxHistogramValueRange()
* @see minHistogramValueRange()
*/
void setMinHistogramValueRange( double minValueRange );
/**
* Sets the maximum expected value for the range of values shown in the histogram.
* @see setMinHistogramValueRange()
* @see maxHistogramValueRange()
*/
void setMaxHistogramValueRange( double maxValueRange );
signals:
//! Emitted when the widget curve changes
void changed();
protected:
virtual void keyPressEvent( QKeyEvent *event ) override ;
private slots:
void plotMousePress( QPointF point );
void plotMouseRelease( QPointF point );
void plotMouseMove( QPointF point );
private:
QgsCurveTransform mCurve;
QwtPlot* mPlot = nullptr;
QwtPlotCurve* mPlotCurve = nullptr;
QList< QwtPlotMarker* > mMarkers;
QgsCurveEditorPlotEventFilter* mPlotFilter = nullptr;
int mCurrentPlotMarkerIndex;
//! Background histogram gatherer thread
std::unique_ptr< QgsHistogramValuesGatherer > mGatherer;
std::unique_ptr< QgsHistogram > mHistogram;
double mMinValueRange = 0.0;
double mMaxValueRange = 1.0;
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000
QwtPlotHistogram *mPlotHistogram = nullptr;
#else
HistogramItem *mPlotHistogramItem = nullptr;
#endif
void updatePlot();
void addPlotMarker( double x, double y, bool isSelected = false );
void updateHistogram();
int findNearestControlPoint( QPointF point ) const;
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000
QwtPlotHistogram* createPlotHistogram( const QBrush &brush, const QPen &pen = Qt::NoPen ) const;
#else
HistogramItem* createHistoItem( const QBrush& brush, const QPen& pen = Qt::NoPen ) const;
#endif
};
//
// NOTE:
// For private only, not part of stable api or exposed to Python bindings
//
/// @cond PRIVATE
class GUI_EXPORT QgsCurveEditorPlotEventFilter: public QObject
{
Q_OBJECT
public:
QgsCurveEditorPlotEventFilter( QwtPlot *plot );
virtual bool eventFilter( QObject* object, QEvent* event ) override;
signals:
void mousePress( QPointF );
void mouseRelease( QPointF );
void mouseMove( QPointF );
private:
QwtPlot* mPlot;
QPointF mapPoint( QPointF point ) const;
};
///@endcond
#endif // QGSCURVEEDITORWIDGET_H

View File

@ -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<double>(), QList<double>(), QList<double>() << 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 );

View File

@ -51,6 +51,13 @@ QgsPropertyAssistantWidget::QgsPropertyAssistantWidget( QWidget* parent ,
{
minValueSpinBox->setValue( initialState.transformer()->minValue() );
maxValueSpinBox->setValue( initialState.transformer()->maxValue() );
if ( initialState.transformer()->curveTransform() )
{
mTransformCurveCheckBox->setChecked( true );
mTransformCurveCheckBox->setCollapsed( false );
mCurveEditor->setCurve( *initialState.transformer()->curveTransform() );
}
}
connect( computeValuesButton, &QPushButton::clicked, this, &QgsPropertyAssistantWidget::computeValuesFromLayer );
@ -107,11 +114,25 @@ QgsPropertyAssistantWidget::QgsPropertyAssistantWidget( QWidget* parent ,
{
mOutputWidget->layout()->addWidget( mTransformerWidget );
connect( mTransformerWidget, &QgsPropertyAbstractTransformerWidget::widgetChanged, this, &QgsPropertyAssistantWidget::widgetChanged );
mCurveEditor->setMinHistogramValueRange( minValueSpinBox->value() );
mCurveEditor->setMaxHistogramValueRange( maxValueSpinBox->value() );
mCurveEditor->setHistogramSource( mLayer, mExpressionWidget->currentField() );
connect( mExpressionWidget, static_cast < void ( QgsFieldExpressionWidget::* )( const QString& ) > ( &QgsFieldExpressionWidget::fieldChanged ), this, [=]( const QString & expression )
{
mCurveEditor->setHistogramSource( mLayer, expression );
}
);
connect( minValueSpinBox, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), mCurveEditor, &QgsCurveEditorWidget::setMinHistogramValueRange );
connect( maxValueSpinBox, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), mCurveEditor, &QgsCurveEditorWidget::setMaxHistogramValueRange );
}
mTransformCurveCheckBox->setVisible( mTransformerWidget );
connect( minValueSpinBox, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), this, &QgsPropertyAssistantWidget::widgetChanged );
connect( maxValueSpinBox, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), this, &QgsPropertyAssistantWidget::widgetChanged );
connect( mExpressionWidget, static_cast < void ( QgsFieldExpressionWidget::* )( const QString& ) > ( &QgsFieldExpressionWidget::fieldChanged ), this, &QgsPropertyAssistantWidget::widgetChanged );
connect( mCurveEditor, &QgsCurveEditorWidget::changed, this, &QgsPropertyAssistantWidget::widgetChanged );
connect( this, &QgsPropertyAssistantWidget::widgetChanged, this, &QgsPropertyAssistantWidget::updatePreview );
updatePreview();
}
@ -131,7 +152,18 @@ void QgsPropertyAssistantWidget::updateProperty( QgsProperty& property )
property.setField( mExpressionWidget->currentField() );
if ( mTransformerWidget )
property.setTransformer( mTransformerWidget->createTransformer( minValueSpinBox->value(), maxValueSpinBox->value() ) );
{
std::unique_ptr< QgsPropertyTransformer> t( mTransformerWidget->createTransformer( minValueSpinBox->value(), maxValueSpinBox->value() ) );
if ( mTransformCurveCheckBox->isChecked() )
{
t->setCurveTransform( new QgsCurveTransform( mCurveEditor->curve() ) );
}
else
{
t->setCurveTransform( nullptr );
}
property.setTransformer( t.release() );
}
}
void QgsPropertyAssistantWidget::setDockMode( bool dockMode )
@ -166,6 +198,10 @@ void QgsPropertyAssistantWidget::computeValuesFromLayer()
whileBlocking( minValueSpinBox )->setValue( minValue );
whileBlocking( maxValueSpinBox )->setValue( maxValue );
mCurveEditor->setMinHistogramValueRange( minValueSpinBox->value() );
mCurveEditor->setMaxHistogramValueRange( maxValueSpinBox->value() );
emit widgetChanged();
}
@ -180,8 +216,9 @@ void QgsPropertyAssistantWidget::updatePreview()
QList<double> breaks = QgsSymbolLayerUtils::prettyBreaks( minValueSpinBox->value(),
maxValueSpinBox->value(), 8 );
QgsCurveTransform curve = mCurveEditor->curve();
QList< QgsSymbolLegendNode* > nodes = mTransformerWidget->generatePreviews( breaks, mLayerTreeLayer, mSymbol.get(), minValueSpinBox->value(),
maxValueSpinBox->value() );
maxValueSpinBox->value(), mTransformCurveCheckBox->isChecked() ? &curve : nullptr );
int widthMax = 0;
int i = 0;
@ -353,7 +390,7 @@ QgsSizeScaleTransformer* QgsPropertySizeAssistantWidget::createTransformer( doub
return transformer;
}
QList< QgsSymbolLegendNode* > QgsPropertySizeAssistantWidget::generatePreviews( const QList<double>& breaks, QgsLayerTreeLayer* parent, const QgsSymbol* symbol, double minValue, double maxValue ) const
QList< QgsSymbolLegendNode* > QgsPropertySizeAssistantWidget::generatePreviews( const QList<double>& breaks, QgsLayerTreeLayer* parent, const QgsSymbol* symbol, double minValue, double maxValue, QgsCurveTransform* curve ) const
{
QList< QgsSymbolLegendNode* > nodes;
@ -376,6 +413,8 @@ QList< QgsSymbolLegendNode* > QgsPropertySizeAssistantWidget::generatePreviews(
return nodes;
std::unique_ptr< QgsSizeScaleTransformer > t( createTransformer( minValue, maxValue ) );
if ( curve )
t->setCurveTransform( new QgsCurveTransform( *curve ) );
for ( int i = 0; i < breaks.length(); i++ )
{
@ -401,7 +440,7 @@ QList< QgsSymbolLegendNode* > QgsPropertySizeAssistantWidget::generatePreviews(
return nodes;
}
QList<QgsSymbolLegendNode*> QgsPropertyAbstractTransformerWidget::generatePreviews( const QList<double>& , QgsLayerTreeLayer* , const QgsSymbol*, double, double ) const
QList<QgsSymbolLegendNode*> QgsPropertyAbstractTransformerWidget::generatePreviews( const QList<double>& , QgsLayerTreeLayer* , const QgsSymbol*, double, double, QgsCurveTransform* ) const
{
return QList< QgsSymbolLegendNode* >();
}
@ -451,7 +490,7 @@ QgsColorRampTransformer* QgsPropertyColorAssistantWidget::createTransformer( dou
return transformer;
}
QList<QgsSymbolLegendNode*> QgsPropertyColorAssistantWidget::generatePreviews( const QList<double>& breaks, QgsLayerTreeLayer* parent, const QgsSymbol* symbol, double minValue, double maxValue ) const
QList<QgsSymbolLegendNode*> QgsPropertyColorAssistantWidget::generatePreviews( const QList<double>& breaks, QgsLayerTreeLayer* parent, const QgsSymbol* symbol, double minValue, double maxValue , QgsCurveTransform* curve ) const
{
QList< QgsSymbolLegendNode* > nodes;
@ -467,6 +506,8 @@ QList<QgsSymbolLegendNode*> 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++ )
{

View File

@ -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<double>& breaks, QgsLayerTreeLayer* parent, const QgsSymbol* symbol, double minValue, double maxValue ) const;
virtual QList< QgsSymbolLegendNode* > generatePreviews( const QList<double>& 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<double>& breaks, QgsLayerTreeLayer* parent, const QgsSymbol* symbol, double minValue, double maxValue ) const override;
QList< QgsSymbolLegendNode* > generatePreviews( const QList<double>& 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<double>& breaks, QgsLayerTreeLayer* parent, const QgsSymbol* symbol, double minValue, double maxValue ) const override;
QList< QgsSymbolLegendNode* > generatePreviews( const QList<double>& breaks, QgsLayerTreeLayer* parent, const QgsSymbol* symbol, double minValue, double maxValue, QgsCurveTransform* curve ) const override;
};
///@endcond PRIVATE

View File

@ -6,22 +6,12 @@
<rect>
<x>0</x>
<y>0</y>
<width>522</width>
<height>332</height>
<width>525</width>
<height>426</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,0,0">
<item row="0" column="1" rowspan="2">
<widget class="QTreeView" name="mLegendPreview">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item row="1" column="0">
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,0,0,0">
<item row="2" column="0">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Output</string>
@ -60,6 +50,16 @@
</layout>
</widget>
</item>
<item row="3" column="0">
<widget class="QFrame" name="mLegendVerticalFrame">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QGroupBox" name="groupBox">
<property name="title">
@ -153,14 +153,60 @@
</layout>
</widget>
</item>
<item row="2" column="0">
<widget class="QFrame" name="mLegendVerticalFrame">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
<item row="0" column="1" rowspan="3">
<widget class="QTreeView" name="mLegendPreview">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</widget>
</item>
<item row="1" column="0">
<widget class="QgsCollapsibleGroupBoxBasic" name="mTransformCurveCheckBox">
<property name="title">
<string>Apply transform curve</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
<property name="collapsed" stdset="0">
<bool>true</bool>
</property>
<property name="syncGroup" stdset="0">
<string notr="true">composeritem</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QgsCurveEditorWidget" name="mCurveEditor" native="true">
<property name="minimumSize">
<size>
<width>0</width>
<height>100</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
@ -183,6 +229,18 @@
<header>qgspanelwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>QgsCollapsibleGroupBoxBasic</class>
<extends>QGroupBox</extends>
<header location="global">qgscollapsiblegroupbox.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>QgsCurveEditorWidget</class>
<extends>QWidget</extends>
<header location="global">qgscurveeditorwidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>mExpressionWidget</tabstop>

View File

@ -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<double>& x, const QVector<double>& y );
};
@ -715,6 +718,26 @@ void TestQgsProperty::genericNumericTransformer()
//non numeric value
QCOMPARE( t1.transform( context, QVariant( "ffff" ) ), QVariant( "ffff" ) );
// add a curve
QVERIFY( !t1.curveTransform() );
t1.setCurveTransform( new QgsCurveTransform( QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) ) );
QVERIFY( t1.curveTransform() );
QCOMPARE( t1.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) );
QCOMPARE( t1.transform( context, 10 ).toInt(), 180 );
QCOMPARE( t1.transform( context, 20 ).toInt(), 120 );
// copy
QgsGenericNumericTransformer s1( t1 );
QVERIFY( s1.curveTransform() );
QCOMPARE( s1.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) );
// assignment
QgsGenericNumericTransformer s2;
s2 = t1;
QVERIFY( s2.curveTransform() );
QCOMPARE( s2.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) );
//saving and restoring
//create a test dom element
@ -730,6 +753,7 @@ void TestQgsProperty::genericNumericTransformer()
250,
-10,
99 );
t2.setCurveTransform( new QgsCurveTransform( QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) ) );
QDomElement element = doc.createElement( "xform" );
QVERIFY( t2.writeXml( element, doc ) );
@ -741,6 +765,8 @@ void TestQgsProperty::genericNumericTransformer()
QCOMPARE( r1.maxOutputValue(), 250.0 );
QCOMPARE( r1.nullOutputValue(), -10.0 );
QCOMPARE( r1.exponent(), 99.0 );
QVERIFY( r1.curveTransform() );
QCOMPARE( r1.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) );
// test cloning
std::unique_ptr< QgsGenericNumericTransformer > r2( t2.clone() );
@ -750,6 +776,8 @@ void TestQgsProperty::genericNumericTransformer()
QCOMPARE( r2->maxOutputValue(), 250.0 );
QCOMPARE( r2->nullOutputValue(), -10.0 );
QCOMPARE( r2->exponent(), 99.0 );
QVERIFY( r2->curveTransform() );
QCOMPARE( r2->curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) );
//test various min/max value/size and scaling methods
@ -884,6 +912,25 @@ void TestQgsProperty::sizeScaleTransformer()
//non numeric value
QCOMPARE( scale.transform( context, QVariant( "ffff" ) ), QVariant( "ffff" ) );
// add a curve
QVERIFY( !scale.curveTransform() );
scale.setCurveTransform( new QgsCurveTransform( QList< QgsPoint >() << QgsPoint( 0, 0.2 ) << QgsPoint( 1, 0.8 ) ) );
QVERIFY( scale.curveTransform() );
QCOMPARE( scale.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.2 ) << QgsPoint( 1, 0.8 ) );
QCOMPARE( scale.transform( context, 10 ).toInt(), 120 );
QCOMPARE( scale.transform( context, 20 ).toInt(), 180 );
// copy
QgsSizeScaleTransformer s1( scale );
QVERIFY( s1.curveTransform() );
QCOMPARE( s1.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.2 ) << QgsPoint( 1, 0.8 ) );
// assignment
QgsSizeScaleTransformer s2;
s2 = scale;
QVERIFY( s2.curveTransform() );
QCOMPARE( s2.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.2 ) << QgsPoint( 1, 0.8 ) );
//saving and restoring
//create a test dom element
@ -900,6 +947,7 @@ void TestQgsProperty::sizeScaleTransformer()
250,
-10,
99 );
t1.setCurveTransform( new QgsCurveTransform( QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) ) );
QDomElement element = doc.createElement( "xform" );
QVERIFY( t1.writeXml( element, doc ) );
@ -912,6 +960,8 @@ void TestQgsProperty::sizeScaleTransformer()
QCOMPARE( r1.nullSize(), -10.0 );
QCOMPARE( r1.exponent(), 99.0 );
QCOMPARE( r1.type(), QgsSizeScaleTransformer::Exponential );
QVERIFY( r1.curveTransform() );
QCOMPARE( r1.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) );
// test cloning
std::unique_ptr< QgsSizeScaleTransformer > r2( t1.clone() );
@ -922,6 +972,8 @@ void TestQgsProperty::sizeScaleTransformer()
QCOMPARE( r2->nullSize(), -10.0 );
QCOMPARE( r2->exponent(), 99.0 );
QCOMPARE( r2->type(), QgsSizeScaleTransformer::Exponential );
QVERIFY( r2->curveTransform() );
QCOMPARE( r2->curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) );
//test various min/max value/size and scaling methods
@ -1089,6 +1141,26 @@ void TestQgsProperty::colorRampTransformer()
//non numeric value
QCOMPARE( scale.transform( context, QVariant( "ffff" ) ), QVariant( "ffff" ) );
// add a curve
QVERIFY( !scale.curveTransform() );
scale.setCurveTransform( new QgsCurveTransform( QList< QgsPoint >() << QgsPoint( 0, 0.2 ) << QgsPoint( 1, 0.8 ) ) );
QVERIFY( scale.curveTransform() );
QCOMPARE( scale.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.2 ) << QgsPoint( 1, 0.8 ) );
QCOMPARE( scale.transform( context, 10 ).value<QColor>().name(), QString( "#333333" ) );
QCOMPARE( scale.transform( context, 20 ).value<QColor>().name(), QString( "#cccccc" ) );
// copy
QgsColorRampTransformer s1( scale );
QVERIFY( s1.curveTransform() );
QCOMPARE( s1.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.2 ) << QgsPoint( 1, 0.8 ) );
// assignment
QgsColorRampTransformer s2;
s2 = scale;
QVERIFY( s2.curveTransform() );
QCOMPARE( s2.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.2 ) << QgsPoint( 1, 0.8 ) );
//saving and restoring
//create a test dom element
@ -1103,6 +1175,7 @@ void TestQgsProperty::colorRampTransformer()
new QgsGradientColorRamp( QColor( 10, 20, 30 ), QColor( 200, 190, 180 ) ),
QColor( 100, 150, 200 ) );
t1.setRampName( "rampname " );
t1.setCurveTransform( new QgsCurveTransform( QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) ) );
QDomElement element = doc.createElement( "xform" );
QVERIFY( t1.writeXml( element, doc ) );
@ -1115,6 +1188,8 @@ void TestQgsProperty::colorRampTransformer()
QVERIFY( dynamic_cast< QgsGradientColorRamp* >( r1.colorRamp() ) );
QCOMPARE( static_cast< QgsGradientColorRamp* >( r1.colorRamp() )->color1(), QColor( 10, 20, 30 ) );
QCOMPARE( static_cast< QgsGradientColorRamp* >( r1.colorRamp() )->color2(), QColor( 200, 190, 180 ) );
QVERIFY( r1.curveTransform() );
QCOMPARE( r1.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) );
// test cloning
std::unique_ptr< QgsColorRampTransformer > r2( t1.clone() );
@ -1124,6 +1199,8 @@ void TestQgsProperty::colorRampTransformer()
QCOMPARE( r2->rampName(), QStringLiteral( "rampname " ) );
QCOMPARE( static_cast< QgsGradientColorRamp* >( r2->colorRamp() )->color1(), QColor( 10, 20, 30 ) );
QCOMPARE( static_cast< QgsGradientColorRamp* >( r2->colorRamp() )->color2(), QColor( 200, 190, 180 ) );
QVERIFY( r2->curveTransform() );
QCOMPARE( r2->curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) );
// copy constructor
QgsColorRampTransformer r3( t1 );
@ -1133,6 +1210,8 @@ void TestQgsProperty::colorRampTransformer()
QCOMPARE( r3.rampName(), QStringLiteral( "rampname " ) );
QCOMPARE( static_cast< QgsGradientColorRamp* >( r3.colorRamp() )->color1(), QColor( 10, 20, 30 ) );
QCOMPARE( static_cast< QgsGradientColorRamp* >( r3.colorRamp() )->color2(), QColor( 200, 190, 180 ) );
QVERIFY( r3.curveTransform() );
QCOMPARE( r3.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) );
// assignment operator
QgsColorRampTransformer r4;
@ -1143,6 +1222,8 @@ void TestQgsProperty::colorRampTransformer()
QCOMPARE( r4.rampName(), QStringLiteral( "rampname " ) );
QCOMPARE( static_cast< QgsGradientColorRamp* >( r4.colorRamp() )->color1(), QColor( 10, 20, 30 ) );
QCOMPARE( static_cast< QgsGradientColorRamp* >( r4.colorRamp() )->color2(), QColor( 200, 190, 180 ) );
QVERIFY( r4.curveTransform() );
QCOMPARE( r4.curveTransform()->controlPoints(), QList< QgsPoint >() << QgsPoint( 0, 0.8 ) << QgsPoint( 1, 0.2 ) );
//test various min/max value/color and scaling methods
@ -1581,5 +1662,127 @@ void TestQgsProperty::collectionStack()
QVERIFY( !stack4.hasDynamicProperties() );
}
void TestQgsProperty::curveTransform()
{
QgsCurveTransform t;
// linear transform
QCOMPARE( t.y( -1 ), 0.0 );
QCOMPARE( t.y( 0 ), 0.0 );
QCOMPARE( t.y( 0.2 ), 0.2 );
QCOMPARE( t.y( 0.5 ), 0.5 );
QCOMPARE( t.y( 0.8 ), 0.8 );
QCOMPARE( t.y( 1 ), 1.0 );
QCOMPARE( t.y( 2 ), 1.0 );
QVector< double > x;
x << -1 << 0 << 0.2 << 0.5 << 0.8 << 1 << 2;
QVector< double > y = t.y( x );
QCOMPARE( y[0], 0.0 );
QCOMPARE( y[1], 0.0 );
QCOMPARE( y[2], 0.2 );
QCOMPARE( y[3], 0.5 );
QCOMPARE( y[4], 0.8 );
QCOMPARE( y[5], 1.0 );
QCOMPARE( y[6], 1.0 );
// linear transform with y =/= x
checkCurveResult( QList< QgsPoint >() << QgsPoint( 0, 0.2 ) << QgsPoint( 1.0, 0.8 ),
QVector< double >() << -1 << 0 << 0.2 << 0.5 << 0.8 << 1 << 2,
QVector< double >() << 0.2 << 0.2 << 0.32 << 0.5 << 0.68 << 0.8 << 0.8 );
// reverse linear transform with y = -x
checkCurveResult( QList< QgsPoint >() << QgsPoint( 0.0, 1.0 ) << QgsPoint( 1.0, 0 ),
QVector< double >() << -1 << 0 << 0.2 << 0.5 << 0.8 << 1 << 2,
QVector< double >() << 1.0 << 1.0 << 0.8 << 0.5 << 0.2 << 0.0 << 0.0 );
// ok, time for some more complex tests...
// 3 control points, but linear
checkCurveResult( QList< QgsPoint >() << QgsPoint( 0, 0.0 ) << QgsPoint( 0.2, 0.2 ) << QgsPoint( 1.0, 1.0 ),
QVector< double >() << -1 << 0 << 0.2 << 0.5 << 0.8 << 1 << 2,
QVector< double >() << 0.0 << 0.0 << 0.2 << 0.5 << 0.8 << 1.0 << 1.0 );
// test for "flat" response for x outside of control point range
checkCurveResult( QList< QgsPoint >() << QgsPoint( 0.2, 0.2 ) << QgsPoint( 0.5, 0.5 ) << QgsPoint( 0.8, 0.8 ),
QVector< double >() << -1 << 0 << 0.1 << 0.2 << 0.5 << 0.8 << 0.9 << 1 << 2,
QVector< double >() << 0.2 << 0.2 << 0.2 << 0.2 << 0.5 << 0.8 << 0.8 << 0.8 << 0.8 );
//curves!
checkCurveResult( QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 0.4, 0.6 ) << QgsPoint( 0.6, 0.8 ) << QgsPoint( 1.0, 1.0 ),
QVector< double >() << -1 << 0 << 0.2 << 0.4 << 0.5 << 0.6 << 0.8 << 0.9 << 1.0 << 2.0,
QVector< double >() << 0.0 << 0.0 << 0.321429 << 0.6 << 0.710714 << 0.8 << 0.921429 << 0.963393 << 1.0 << 1.0 );
//curves with more control points
checkCurveResult( QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 0.2, 0.6 ) << QgsPoint( 0.4, 0.6 ) << QgsPoint( 0.6, 0.8 ) << QgsPoint( 0.8, 0.3 ) << QgsPoint( 1.0, 1.0 ),
QVector< double >() << -1 << 0 << 0.2 << 0.4 << 0.5 << 0.6 << 0.8 << 0.9 << 1.0 << 2.0,
QVector< double >() << 0.0 << 0.0 << 0.6 << 0.6 << 0.751316 << 0.8 << 0.3 << 0.508074 << 1.0 << 1.0 );
// general tests
QList< QgsPoint > points = QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 0.4, 0.6 ) << QgsPoint( 0.6, 0.8 ) << QgsPoint( 1.0, 1.0 );
QgsCurveTransform src( points );
QCOMPARE( src.controlPoints(), points );
points = QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 0.5, 0.6 ) << QgsPoint( 0.6, 0.8 ) << QgsPoint( 1.0, 1.0 );
src.setControlPoints( points );
QCOMPARE( src.controlPoints(), points );
src.setControlPoints( QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 1.0, 1.0 ) );
src.addControlPoint( 0.2, 0.3 );
src.addControlPoint( 0.1, 0.4 );
QCOMPARE( src.controlPoints(), QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 0.1, 0.4 ) << QgsPoint( 0.2, 0.3 ) << QgsPoint( 1.0, 1.0 ) );
// remove non-existent point
src.removeControlPoint( 0.6, 0.7 );
QCOMPARE( src.controlPoints(), QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 0.1, 0.4 ) << QgsPoint( 0.2, 0.3 ) << QgsPoint( 1.0, 1.0 ) );
// remove valid point
src.removeControlPoint( 0.1, 0.4 );
QCOMPARE( src.controlPoints(), QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 0.2, 0.3 ) << QgsPoint( 1.0, 1.0 ) );
// copy constructor
QgsCurveTransform dest( src );
QCOMPARE( dest.controlPoints(), QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 0.2, 0.3 ) << QgsPoint( 1.0, 1.0 ) );
// check a value to ensure that derivative matrix was copied ok
QGSCOMPARENEAR( dest.y( 0.5 ), 0.1, 0.638672 );
// assignment operator
QgsCurveTransform dest2;
dest2 = src;
QCOMPARE( dest2.controlPoints(), QList< QgsPoint >() << QgsPoint( 0.0, 0.0 ) << QgsPoint( 0.2, 0.3 ) << QgsPoint( 1.0, 1.0 ) );
QGSCOMPARENEAR( dest2.y( 0.5 ), 0.1, 0.638672 );
// writing and reading from xml
QDomImplementation DomImplementation;
QDomDocumentType documentType =
DomImplementation.createDocumentType(
"qgis", "http://mrcc.com/qgis.dtd", "SYSTEM" );
QDomDocument doc( documentType );
QDomElement element = doc.createElement( "xform" );
QVERIFY( src.writeXml( element, doc ) );
QgsCurveTransform r1;
QVERIFY( r1.readXml( element, doc ) );
QCOMPARE( r1.controlPoints(), src.controlPoints() );
QGSCOMPARENEAR( dest2.y( 0.5 ), 0.1, 0.638672 );
}
void TestQgsProperty::checkCurveResult( const QList<QgsPoint>& controlPoints, const QVector<double>& x, const QVector<double>& 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"