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