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