QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
qgscurveeditorwidget.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgscurveeditorwidget.cpp
3 ------------------------
4 begin : February 2017
5 copyright : (C) 2017 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16
18#include "qgsvectorlayer.h"
19
20#include <QPainter>
21#include <QVBoxLayout>
22#include <QMouseEvent>
23#include <algorithm>
24
25// QWT Charting widget
26#include <qwt_global.h>
27#include <qwt_plot_canvas.h>
28#include <qwt_plot.h>
29#include <qwt_plot_curve.h>
30#include <qwt_plot_grid.h>
31#include <qwt_plot_marker.h>
32#include <qwt_plot_picker.h>
33#include <qwt_picker_machine.h>
34#include <qwt_plot_layout.h>
35#include <qwt_symbol.h>
36#include <qwt_legend.h>
37#include <qwt_scale_div.h>
38#include <qwt_scale_map.h>
39
40#include <qwt_plot_renderer.h>
41#include <qwt_plot_histogram.h>
42
44 : QWidget( parent )
45 , mCurve( transform )
46{
47 mPlot = new QwtPlot();
48 mPlot->setMinimumSize( QSize( 0, 100 ) );
49 mPlot->setAxisScale( QwtPlot::yLeft, 0, 1 );
50 mPlot->setAxisScale( QwtPlot::yRight, 0, 1 );
51 mPlot->setAxisScale( QwtPlot::xBottom, 0, 1 );
52 mPlot->setAxisScale( QwtPlot::xTop, 0, 1 );
53
54 QVBoxLayout *vlayout = new QVBoxLayout();
55 vlayout->addWidget( mPlot );
56 setLayout( vlayout );
57
58 // hide the ugly canvas frame
59 mPlot->setFrameStyle( QFrame::NoFrame );
60 QFrame *plotCanvasFrame = dynamic_cast<QFrame *>( mPlot->canvas() );
61 if ( plotCanvasFrame )
62 plotCanvasFrame->setFrameStyle( QFrame::NoFrame );
63
64 mPlot->enableAxis( QwtPlot::yLeft, false );
65 mPlot->enableAxis( QwtPlot::xBottom, false );
66
67 // add a grid
68 QwtPlotGrid *grid = new QwtPlotGrid();
69 const QwtScaleDiv gridDiv( 0.0, 1.0, QList<double>(), QList<double>(), QList<double>() << 0.2 << 0.4 << 0.6 << 0.8 );
70 grid->setXDiv( gridDiv );
71 grid->setYDiv( gridDiv );
72 grid->setPen( QPen( QColor( 0, 0, 0, 50 ) ) );
73 grid->attach( mPlot );
74
75 mPlotCurve = new QwtPlotCurve();
76 mPlotCurve->setTitle( QStringLiteral( "Curve" ) );
77 mPlotCurve->setPen( QPen( QColor( 30, 30, 30 ), 0.0 ) ),
78 mPlotCurve->setRenderHint( QwtPlotItem::RenderAntialiased, true );
79 mPlotCurve->attach( mPlot );
80
81 mPlotFilter = new QgsCurveEditorPlotEventFilter( mPlot );
82 connect( mPlotFilter, &QgsCurveEditorPlotEventFilter::mousePress, this, &QgsCurveEditorWidget::plotMousePress );
83 connect( mPlotFilter, &QgsCurveEditorPlotEventFilter::mouseRelease, this, &QgsCurveEditorWidget::plotMouseRelease );
84 connect( mPlotFilter, &QgsCurveEditorPlotEventFilter::mouseMove, this, &QgsCurveEditorWidget::plotMouseMove );
85
86 mPlotCurve->setVisible( true );
87 updatePlot();
88}
89
91{
92 if ( mGatherer && mGatherer->isRunning() )
93 {
94 connect( mGatherer.get(), &QgsHistogramValuesGatherer::finished, mGatherer.get(), &QgsHistogramValuesGatherer::deleteLater );
95 mGatherer->stop();
96 ( void )mGatherer.release();
97 }
98}
99
101{
102 mCurve = curve;
103 updatePlot();
104 emit changed();
105}
106
107void QgsCurveEditorWidget::setHistogramSource( const QgsVectorLayer *layer, const QString &expression )
108{
109 if ( !mGatherer )
110 {
111 mGatherer.reset( new QgsHistogramValuesGatherer() );
112 connect( mGatherer.get(), &QgsHistogramValuesGatherer::calculatedHistogram, this, [ = ]
113 {
114 mHistogram.reset( new QgsHistogram( mGatherer->histogram() ) );
115 updateHistogram();
116 } );
117 }
118
119 const bool changed = mGatherer->layer() != layer || mGatherer->expression() != expression;
120 if ( changed )
121 {
122 mGatherer->setExpression( expression );
123 mGatherer->setLayer( layer );
124 mGatherer->start();
125 if ( mGatherer->isRunning() )
126 {
127 //stop any currently running task
128 mGatherer->stop();
129 while ( mGatherer->isRunning() )
130 {
131 QCoreApplication::processEvents();
132 }
133 }
134 mGatherer->start();
135 }
136 else
137 {
138 updateHistogram();
139 }
140}
141
143{
144 mMinValueRange = minValueRange;
145 updateHistogram();
146}
147
149{
150 mMaxValueRange = maxValueRange;
151 updateHistogram();
152}
153
155{
156 if ( event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace )
157 {
158 QList< QgsPointXY > cp = mCurve.controlPoints();
159 if ( mCurrentPlotMarkerIndex > 0 && mCurrentPlotMarkerIndex < cp.count() - 1 )
160 {
161 cp.removeAt( mCurrentPlotMarkerIndex );
162 mCurve.setControlPoints( cp );
163 updatePlot();
164 emit changed();
165 }
166 }
167}
168
169void QgsCurveEditorWidget::plotMousePress( QPointF point )
170{
171 mCurrentPlotMarkerIndex = findNearestControlPoint( point );
172 if ( mCurrentPlotMarkerIndex < 0 )
173 {
174 // add a new point
175 mCurve.addControlPoint( point.x(), point.y() );
176 mCurrentPlotMarkerIndex = findNearestControlPoint( point );
177 emit changed();
178 }
179 updatePlot();
180}
181
182
183int QgsCurveEditorWidget::findNearestControlPoint( QPointF point ) const
184{
185 double minDist = 3.0 / mPlot->width();
186 int currentPlotMarkerIndex = -1;
187
188 const QList< QgsPointXY > controlPoints = mCurve.controlPoints();
189
190 for ( int i = 0; i < controlPoints.count(); ++i )
191 {
192 const QgsPointXY currentPoint = controlPoints.at( i );
193 double currentDist;
194 currentDist = std::pow( point.x() - currentPoint.x(), 2.0 ) + std::pow( point.y() - currentPoint.y(), 2.0 );
195 if ( currentDist < minDist )
196 {
197 minDist = currentDist;
198 currentPlotMarkerIndex = i;
199 }
200 }
201 return currentPlotMarkerIndex;
202}
203
204
205void QgsCurveEditorWidget::plotMouseRelease( QPointF )
206{
207}
208
209void QgsCurveEditorWidget::plotMouseMove( QPointF point )
210{
211 if ( mCurrentPlotMarkerIndex < 0 )
212 return;
213
214 QList< QgsPointXY > cp = mCurve.controlPoints();
215 bool removePoint = false;
216 if ( mCurrentPlotMarkerIndex == 0 )
217 {
218 point.setX( std::min( point.x(), cp.at( 1 ).x() - 0.01 ) );
219 }
220 else
221 {
222 removePoint = point.x() <= cp.at( mCurrentPlotMarkerIndex - 1 ).x();
223 }
224 if ( mCurrentPlotMarkerIndex == cp.count() - 1 )
225 {
226 point.setX( std::max( point.x(), cp.at( mCurrentPlotMarkerIndex - 1 ).x() + 0.01 ) );
227 removePoint = false;
228 }
229 else
230 {
231 removePoint = removePoint || point.x() >= cp.at( mCurrentPlotMarkerIndex + 1 ).x();
232 }
233
234 if ( removePoint )
235 {
236 cp.removeAt( mCurrentPlotMarkerIndex );
237 mCurrentPlotMarkerIndex = -1;
238 }
239 else
240 {
241 cp[ mCurrentPlotMarkerIndex ] = QgsPointXY( point.x(), point.y() );
242 }
243 mCurve.setControlPoints( cp );
244 updatePlot();
245 emit changed();
246}
247
248void QgsCurveEditorWidget::addPlotMarker( double x, double y, bool isSelected )
249{
250 const QColor borderColor( 0, 0, 0 );
251
252 const QColor brushColor = isSelected ? borderColor : QColor( 255, 255, 255, 0 );
253
254 QwtPlotMarker *marker = new QwtPlotMarker();
255 marker->setSymbol( new QwtSymbol( QwtSymbol::Ellipse, QBrush( brushColor ), QPen( borderColor, isSelected ? 2 : 1 ), QSize( 8, 8 ) ) );
256 marker->setValue( x, y );
257 marker->attach( mPlot );
258 marker->setRenderHint( QwtPlotItem::RenderAntialiased, true );
259 mMarkers << marker;
260}
261
262void QgsCurveEditorWidget::updateHistogram()
263{
264 if ( !mHistogram )
265 return;
266
267 //draw histogram
268 const QBrush histoBrush( QColor( 0, 0, 0, 70 ) );
269
270 delete mPlotHistogram;
271 mPlotHistogram = createPlotHistogram( histoBrush );
272 QVector<QwtIntervalSample> dataHisto;
273
274 const int bins = 40;
275 QList<double> edges = mHistogram->binEdges( bins );
276 const QList<int> counts = mHistogram->counts( bins );
277
278 // scale counts to 0->1
279 const double max = *std::max_element( counts.constBegin(), counts.constEnd() );
280
281 // scale bin edges to fit in 0->1 range
282 if ( !qgsDoubleNear( mMinValueRange, mMaxValueRange ) )
283 {
284 std::transform( edges.begin(), edges.end(), edges.begin(),
285 [this]( double d ) -> double { return ( d - mMinValueRange ) / ( mMaxValueRange - mMinValueRange ); } );
286 }
287
288 for ( int bin = 0; bin < bins; ++bin )
289 {
290 const double binValue = counts.at( bin ) / max;
291
292 const double upperEdge = edges.at( bin + 1 );
293
294 dataHisto << QwtIntervalSample( binValue, edges.at( bin ), upperEdge );
295 }
296
297 mPlotHistogram->setSamples( dataHisto );
298 mPlotHistogram->attach( mPlot );
299 mPlot->replot();
300}
301
302void QgsCurveEditorWidget::updatePlot()
303{
304 // remove existing markers
305 const auto constMMarkers = mMarkers;
306 for ( QwtPlotMarker *marker : constMMarkers )
307 {
308 marker->detach();
309 delete marker;
310 }
311 mMarkers.clear();
312
313 QPolygonF curvePoints;
314 QVector< double > x;
315
316 int i = 0;
317 const auto constControlPoints = mCurve.controlPoints();
318 for ( const QgsPointXY &point : constControlPoints )
319 {
320 x << point.x();
321 addPlotMarker( point.x(), point.y(), mCurrentPlotMarkerIndex == i );
322 i++;
323 }
324
325 //add extra intermediate points
326
327 for ( double p = 0; p <= 1.0; p += 0.01 )
328 {
329 x << p;
330 }
331 std::sort( x.begin(), x.end() );
332 const QVector< double > y = mCurve.y( x );
333
334 for ( int j = 0; j < x.count(); ++j )
335 {
336 curvePoints << QPointF( x.at( j ), y.at( j ) );
337 }
338
339 mPlotCurve->setSamples( curvePoints );
340 mPlot->replot();
341}
342
343QwtPlotHistogram *QgsCurveEditorWidget::createPlotHistogram( const QBrush &brush, const QPen &pen ) const
344{
345 QwtPlotHistogram *histogram = new QwtPlotHistogram( QString() );
346 histogram->setBrush( brush );
347 if ( pen != Qt::NoPen )
348 {
349 histogram->setPen( pen );
350 }
351 else if ( brush.color().lightness() > 200 )
352 {
353 QPen p;
354 p.setColor( brush.color().darker( 150 ) );
355 p.setWidth( 0 );
356 p.setCosmetic( true );
357 histogram->setPen( p );
358 }
359 else
360 {
361 histogram->setPen( QPen( Qt::NoPen ) );
362 }
363 return histogram;
364}
365
367
368QgsCurveEditorPlotEventFilter::QgsCurveEditorPlotEventFilter( QwtPlot *plot )
369 : QObject( plot )
370 , mPlot( plot )
371{
372 mPlot->canvas()->installEventFilter( this );
373}
374
375bool QgsCurveEditorPlotEventFilter::eventFilter( QObject *object, QEvent *event )
376{
377 if ( !mPlot->isEnabled() )
378 return QObject::eventFilter( object, event );
379
380 switch ( event->type() )
381 {
382 case QEvent::MouseButtonPress:
383 {
384 const QMouseEvent *mouseEvent = static_cast<QMouseEvent * >( event );
385 if ( mouseEvent->button() == Qt::LeftButton )
386 {
387 emit mousePress( mapPoint( mouseEvent->pos() ) );
388 }
389 break;
390 }
391 case QEvent::MouseMove:
392 {
393 const QMouseEvent *mouseEvent = static_cast<QMouseEvent * >( event );
394 if ( mouseEvent->buttons() & Qt::LeftButton )
395 {
396 // only emit when button pressed
397 emit mouseMove( mapPoint( mouseEvent->pos() ) );
398 }
399 break;
400 }
401 case QEvent::MouseButtonRelease:
402 {
403 const QMouseEvent *mouseEvent = static_cast<QMouseEvent * >( event );
404 if ( mouseEvent->button() == Qt::LeftButton )
405 {
406 emit mouseRelease( mapPoint( mouseEvent->pos() ) );
407 }
408 break;
409 }
410 default:
411 break;
412 }
413
414 return QObject::eventFilter( object, event );
415}
416
417QPointF QgsCurveEditorPlotEventFilter::mapPoint( QPointF point ) const
418{
419 if ( !mPlot )
420 return QPointF();
421
422 return QPointF( mPlot->canvasMap( QwtPlot::xBottom ).invTransform( point.x() ),
423 mPlot->canvasMap( QwtPlot::yLeft ).invTransform( point.y() ) );
424}
425
426
QgsCurveTransform curve() const
Returns a curve representing the current curve from the widget.
void setMaxHistogramValueRange(double maxValueRange)
Sets the maximum expected value for the range of values shown in the histogram.
void changed()
Emitted when the widget curve changes.
void setMinHistogramValueRange(double minValueRange)
Sets the minimum expected value for the range of values shown in the histogram.
void keyPressEvent(QKeyEvent *event) override
void setCurve(const QgsCurveTransform &curve)
Sets the curve to show in the widget.
void setHistogramSource(const QgsVectorLayer *layer, const QString &expression)
Sets a layer and expression source for values to show in a histogram behind the curve.
QgsCurveEditorWidget(QWidget *parent=nullptr, const QgsCurveTransform &curve=QgsCurveTransform())
Constructor for QgsCurveEditorWidget.
Handles scaling of input values to output values by using a curve created from smoothly joining a num...
void setControlPoints(const QList< QgsPointXY > &points)
Sets the list of control points for the transform.
void addControlPoint(double x, double y)
Adds a control point to the transform.
double y(double x) const
Returns the mapped y value corresponding to the specified x value.
QList< QgsPointXY > controlPoints() const
Returns a list of the control points for the transform.
A class to represent a 2D point.
Definition: qgspointxy.h:60
double y
Definition: qgspointxy.h:64
Q_GADGET double x
Definition: qgspointxy.h:63
Represents a vector layer which manages a vector based data sets.
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:5207