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