mirror of
https://github.com/qgis/QGIS.git
synced 2025-03-21 00:05:53 -04:00
Merge pull request #4169 from nyalldawson/props_gui
[FEATURE] Interactive curve editing for property overrides
This commit is contained in:
commit
bde4ff99c4
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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 );
|
||||
|
@ -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
|
||||
|
31
python/gui/qgscurveeditorwidget.sip
Normal file
31
python/gui/qgscurveeditorwidget.sip
Normal 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 );
|
||||
|
||||
};
|
@ -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;
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 );
|
||||
|
@ -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
|
||||
|
481
src/gui/qgscurveeditorwidget.cpp
Normal file
481
src/gui/qgscurveeditorwidget.cpp
Normal 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
|
282
src/gui/qgscurveeditorwidget.h
Normal file
282
src/gui/qgscurveeditorwidget.h
Normal 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
|
@ -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 );
|
||||
|
@ -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++ )
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user