mirror of
https://github.com/qgis/QGIS.git
synced 2025-06-18 00:04:02 -04:00
[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:
parent
0faf7c395f
commit
5c42c7636b
@ -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.
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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 )
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user