[FEATURE] Show a histogram for values behind curve editor

in property assistant

Makes it easier to set suitable curves. Populated in the
background for a nice reponsive widget!
This commit is contained in:
Nyall Dawson 2017-02-22 11:09:14 +10:00
parent 0faf7c395f
commit 5c42c7636b
7 changed files with 360 additions and 3 deletions

View File

@ -22,7 +22,7 @@ class QgsHistogram
*/
void setValues( const QList<double>& values );
bool setValues( QgsVectorLayer* layer, const QString& fieldOrExpression, QgsFeedback* feedback = 0 );
bool setValues( const QgsVectorLayer* layer, const QString& fieldOrExpression, QgsFeedback* feedback = 0 );
/** Calculates the optimal bin width using the Freedman-Diaconis rule. Bins widths are
* determined by the inter-quartile range of values and the number of values.

View File

@ -7,10 +7,18 @@ class QgsCurveEditorWidget : QWidget
public:
QgsCurveEditorWidget( QWidget* parent /TransferThis/ = 0, const QgsCurveTransform& curve = QgsCurveTransform() );
~QgsCurveEditorWidget();
QgsCurveTransform curve() const;
void setCurve( const QgsCurveTransform& curve );
void setHistogramSource( const QgsVectorLayer* layer, const QString& expression );
double minHistogramValueRange() const;
double maxHistogramValueRange() const;
public slots:
void setMinHistogramValueRange( double minValueRange );
void setMaxHistogramValueRange( double maxValueRange );
signals:

View File

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

View File

@ -53,7 +53,7 @@ class CORE_EXPORT QgsHistogram
* @param feedback optional feedback object to allow cancelation of calculation
* @returns true if values were successfully set
*/
bool setValues( QgsVectorLayer* layer, const QString& fieldOrExpression, QgsFeedback* feedback = nullptr );
bool setValues( const QgsVectorLayer* layer, const QString& fieldOrExpression, QgsFeedback* feedback = nullptr );
/** Calculates the optimal bin width using the Freedman-Diaconis rule. Bins widths are
* determined by the inter-quartile range of values and the number of values.

View File

@ -20,6 +20,7 @@
#include <QPainter>
#include <QVBoxLayout>
#include <QMouseEvent>
#include <algorithm>
// QWT Charting widget
#include <qwt_global.h>
@ -34,6 +35,13 @@
#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 )
@ -86,6 +94,16 @@ QgsCurveEditorWidget::QgsCurveEditorWidget( QWidget* parent, const QgsCurveTrans
updatePlot();
}
QgsCurveEditorWidget::~QgsCurveEditorWidget()
{
if ( mGatherer && mGatherer->isRunning() )
{
connect( mGatherer.get(), &QgsHistogramValuesGatherer::finished, mGatherer.get(), &QgsHistogramValuesGatherer::deleteLater );
mGatherer->stop();
( void )mGatherer.release();
}
}
void QgsCurveEditorWidget::setCurve( const QgsCurveTransform& curve )
{
mCurve = curve;
@ -93,6 +111,53 @@ void QgsCurveEditorWidget::setCurve( const QgsCurveTransform& curve )
emit changed();
}
void QgsCurveEditorWidget::setHistogramSource( const QgsVectorLayer* layer, const QString& expression )
{
if ( !mGatherer )
{
mGatherer.reset( new QgsHistogramValuesGatherer() );
connect( mGatherer.get(), &QgsHistogramValuesGatherer::calculatedHistogram, this, [=]
{
mHistogram.reset( new QgsHistogram( mGatherer->histogram() ) );
updateHistogram();
} );
}
bool changed = mGatherer->layer() != layer || mGatherer->expression() != expression;
if ( changed )
{
mGatherer->setExpression( expression );
mGatherer->setLayer( layer );
mGatherer->start();
if ( mGatherer->isRunning() )
{
//stop any currently running task
mGatherer->stop();
while ( mGatherer->isRunning() )
{
QCoreApplication::processEvents();
}
}
mGatherer->start();
}
else
{
updateHistogram();
}
}
void QgsCurveEditorWidget::setMinHistogramValueRange( double minValueRange )
{
mMinValueRange = minValueRange;
updateHistogram();
}
void QgsCurveEditorWidget::setMaxHistogramValueRange( double maxValueRange )
{
mMaxValueRange = maxValueRange;
updateHistogram();
}
void QgsCurveEditorWidget::keyPressEvent( QKeyEvent* event )
{
if ( event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace )
@ -205,6 +270,63 @@ void QgsCurveEditorWidget::addPlotMarker( double x, double y, bool isSelected )
mMarkers << marker;
}
void QgsCurveEditorWidget::updateHistogram()
{
if ( !mHistogram )
return;
//draw histogram
QBrush histoBrush( QColor( 0, 0, 0, 70 ) );
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000
delete mPlotHistogram;
mPlotHistogram = createPlotHistogram( histoBrush );
QVector<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
@ -249,6 +371,52 @@ void QgsCurveEditorWidget::updatePlot()
}
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000
QwtPlotHistogram* QgsCurveEditorWidget::createPlotHistogram( const QBrush& brush, const QPen& pen ) const
{
QwtPlotHistogram* histogram = new QwtPlotHistogram( QString() );
histogram->setBrush( brush );
if ( pen != Qt::NoPen )
{
histogram->setPen( pen );
}
else if ( brush.color().lightness() > 200 )
{
QPen p;
p.setColor( brush.color().darker( 150 ) );
p.setWidth( 0 );
p.setCosmetic( true );
histogram->setPen( p );
}
else
{
histogram->setPen( QPen( Qt::NoPen ) );
}
return histogram;
}
#else
HistogramItem * QgsCurveEditorWidget::createHistoItem( const QBrush& brush, const QPen& pen ) const
{
HistogramItem* item = new HistogramItem( QString() );
item->setColor( brush.color() );
item->setFlat( true );
item->setSpacing( 0 );
if ( pen != Qt::NoPen )
{
item->setPen( pen );
}
else if ( brush.color().lightness() > 200 )
{
QPen p;
p.setColor( brush.color().darker( 150 ) );
p.setWidth( 0 );
p.setCosmetic( true );
item->setPen( p );
}
return item;
}
#endif
/// @cond PRIVATE
QgsCurveEditorPlotEventFilter::QgsCurveEditorPlotEventFilter( QwtPlot *plot )
@ -309,4 +477,5 @@ QPointF QgsCurveEditorPlotEventFilter::mapPoint( QPointF point ) const
mPlot->canvasMap( QwtPlot::yLeft ).invTransform( point.y() ) );
}
///@endcond

View File

@ -17,14 +17,118 @@
#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.
@ -41,6 +145,8 @@ class GUI_EXPORT QgsCurveEditorWidget : public QWidget
*/
QgsCurveEditorWidget( QWidget* parent = nullptr, const QgsCurveTransform& curve = QgsCurveTransform() );
~QgsCurveEditorWidget();
/**
* Returns a curve representing the current curve from the widget.
* @see setCurve()
@ -53,6 +159,45 @@ class GUI_EXPORT QgsCurveEditorWidget : public QWidget
*/
void setCurve( const QgsCurveTransform& curve );
/**
* Sets a \a layer and \a expression source for values to show in a histogram
* behind the curve. The histogram is generated in a background thread to keep
* the widget responsive.
* @see minHistogramValueRange()
* @see maxHistogramValueRange()
*/
void setHistogramSource( const QgsVectorLayer* layer, const QString& expression );
/**
* Returns the minimum expected value for the range of values shown in the histogram.
* @see maxHistogramValueRange()
* @see setMinHistogramValueRange()
*/
double minHistogramValueRange() const { return mMinValueRange; }
/**
* Returns the maximum expected value for the range of values shown in the histogram.
* @see minHistogramValueRange()
* @see setMaxHistogramValueRange()
*/
double maxHistogramValueRange() const { return mMaxValueRange; }
public slots:
/**
* Sets the minimum expected value for the range of values shown in the histogram.
* @see setMaxHistogramValueRange()
* @see minHistogramValueRange()
*/
void setMinHistogramValueRange( double minValueRange );
/**
* Sets the maximum expected value for the range of values shown in the histogram.
* @see setMinHistogramValueRange()
* @see maxHistogramValueRange()
*/
void setMaxHistogramValueRange( double maxValueRange );
signals:
//! Emitted when the widget curve changes
@ -79,11 +224,30 @@ class GUI_EXPORT QgsCurveEditorWidget : public QWidget
QList< QwtPlotMarker* > mMarkers;
QgsCurveEditorPlotEventFilter* mPlotFilter = nullptr;
int mCurrentPlotMarkerIndex;
//! Background histogram gatherer thread
std::unique_ptr< QgsHistogramValuesGatherer > mGatherer;
std::unique_ptr< QgsHistogram > mHistogram;
double mMinValueRange = 0.0;
double mMaxValueRange = 1.0;
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000
QwtPlotHistogram *mPlotHistogram = nullptr;
#else
HistogramItem *mPlotHistogramItem = nullptr;
#endif
void updatePlot();
void addPlotMarker( double x, double y, bool isSelected = false );
void updateHistogram();
int findNearestControlPoint( QPointF point ) const;
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000
QwtPlotHistogram* createPlotHistogram( const QBrush &brush, const QPen &pen = Qt::NoPen ) const;
#else
HistogramItem* createHistoItem( const QBrush& brush, const QPen& pen = Qt::NoPen ) const;
#endif
};

View File

@ -113,6 +113,18 @@ QgsPropertyAssistantWidget::QgsPropertyAssistantWidget( QWidget* parent ,
{
mOutputWidget->layout()->addWidget( mTransformerWidget );
connect( mTransformerWidget, &QgsPropertyAbstractTransformerWidget::widgetChanged, this, &QgsPropertyAssistantWidget::widgetChanged );
mCurveEditor->setMinHistogramValueRange( minValueSpinBox->value() );
mCurveEditor->setMaxHistogramValueRange( maxValueSpinBox->value() );
mCurveEditor->setHistogramSource( mLayer, mExpressionWidget->currentField() );
connect( mExpressionWidget, static_cast < void ( QgsFieldExpressionWidget::* )( const QString& ) > ( &QgsFieldExpressionWidget::fieldChanged ), this, [=]( const QString & expression )
{
mCurveEditor->setHistogramSource( mLayer, expression );
}
);
connect( minValueSpinBox, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), mCurveEditor, &QgsCurveEditorWidget::setMinHistogramValueRange );
connect( maxValueSpinBox, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), mCurveEditor, &QgsCurveEditorWidget::setMaxHistogramValueRange );
}
mTransformCurveCheckBox->setVisible( mTransformerWidget );
@ -185,6 +197,10 @@ void QgsPropertyAssistantWidget::computeValuesFromLayer()
whileBlocking( minValueSpinBox )->setValue( minValue );
whileBlocking( maxValueSpinBox )->setValue( maxValue );
mCurveEditor->setMinHistogramValueRange( minValueSpinBox->value() );
mCurveEditor->setMaxHistogramValueRange( maxValueSpinBox->value() );
emit widgetChanged();
}