QGIS API Documentation  3.13.0-Master (740be229cb)
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  const auto constMMarkers = mMarkers;
304  for ( QwtPlotMarker *marker : constMMarkers )
305  {
306  marker->detach();
307  delete marker;
308  }
309  mMarkers.clear();
310 
311  QPolygonF curvePoints;
312  QVector< double > x;
313 
314  int i = 0;
315  const auto constControlPoints = mCurve.controlPoints();
316  for ( const QgsPointXY &point : constControlPoints )
317  {
318  x << point.x();
319  addPlotMarker( point.x(), point.y(), mCurrentPlotMarkerIndex == i );
320  i++;
321  }
322 
323  //add extra intermediate points
324 
325  for ( double p = 0; p <= 1.0; p += 0.01 )
326  {
327  x << p;
328  }
329  std::sort( x.begin(), x.end() );
330  QVector< double > y = mCurve.y( x );
331 
332  for ( int j = 0; j < x.count(); ++j )
333  {
334  curvePoints << QPointF( x.at( j ), y.at( j ) );
335  }
336 
337  mPlotCurve->setSamples( curvePoints );
338  mPlot->replot();
339 }
340 
341 QwtPlotHistogram *QgsCurveEditorWidget::createPlotHistogram( const QBrush &brush, const QPen &pen ) const
342 {
343  QwtPlotHistogram *histogram = new QwtPlotHistogram( QString() );
344  histogram->setBrush( brush );
345  if ( pen != Qt::NoPen )
346  {
347  histogram->setPen( pen );
348  }
349  else if ( brush.color().lightness() > 200 )
350  {
351  QPen p;
352  p.setColor( brush.color().darker( 150 ) );
353  p.setWidth( 0 );
354  p.setCosmetic( true );
355  histogram->setPen( p );
356  }
357  else
358  {
359  histogram->setPen( QPen( Qt::NoPen ) );
360  }
361  return histogram;
362 }
363 
365 
366 QgsCurveEditorPlotEventFilter::QgsCurveEditorPlotEventFilter( QwtPlot *plot )
367  : QObject( plot )
368  , mPlot( plot )
369 {
370  mPlot->canvas()->installEventFilter( this );
371 }
372 
373 bool QgsCurveEditorPlotEventFilter::eventFilter( QObject *object, QEvent *event )
374 {
375  if ( !mPlot->isEnabled() )
376  return QObject::eventFilter( object, event );
377 
378  switch ( event->type() )
379  {
380  case QEvent::MouseButtonPress:
381  {
382  const QMouseEvent *mouseEvent = static_cast<QMouseEvent * >( event );
383  if ( mouseEvent->button() == Qt::LeftButton )
384  {
385  emit mousePress( mapPoint( mouseEvent->pos() ) );
386  }
387  break;
388  }
389  case QEvent::MouseMove:
390  {
391  const QMouseEvent *mouseEvent = static_cast<QMouseEvent * >( event );
392  if ( mouseEvent->buttons() & Qt::LeftButton )
393  {
394  // only emit when button pressed
395  emit mouseMove( mapPoint( mouseEvent->pos() ) );
396  }
397  break;
398  }
399  case QEvent::MouseButtonRelease:
400  {
401  const QMouseEvent *mouseEvent = static_cast<QMouseEvent * >( event );
402  if ( mouseEvent->button() == Qt::LeftButton )
403  {
404  emit mouseRelease( mapPoint( mouseEvent->pos() ) );
405  }
406  break;
407  }
408  default:
409  break;
410  }
411 
412  return QObject::eventFilter( object, event );
413 }
414 
415 QPointF QgsCurveEditorPlotEventFilter::mapPoint( QPointF point ) const
416 {
417  if ( !mPlot )
418  return QPointF();
419 
420  return QPointF( mPlot->canvasMap( QwtPlot::xBottom ).invTransform( point.x() ),
421  mPlot->canvasMap( QwtPlot::yLeft ).invTransform( point.y() ) );
422 }
423 
424 
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:315
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.