QGIS API Documentation  3.4.15-Madeira (e83d02e274)
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 
QList< QgsPointXY > controlPoints() const
Returns a list of the control points for the transform.
double y(double x) const
Returns the mapped y value corresponding to the specified x value.
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:278
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.
QgsCurveTransform curve() const
Returns a curve representing the current curve from the widget.
void setMinHistogramValueRange(double minValueRange)
Sets the minimum expected value for the range of values shown in the histogram.
double x
Definition: qgspointxy.h:47
void setCurve(const QgsCurveTransform &curve)
Sets the curve to show in the widget.
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.