QGIS API Documentation  3.0.2-Girona (307d082)
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
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.
bool qgsDoubleNear(double a, double b, double epsilon=4 *DBL_EPSILON)
Compare two doubles (but allow some difference)
Definition: qgis.h:251
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.