QGIS API Documentation  3.16.0-Hannover (43b64b13f3)
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 
QgsPointXY::y
double y
Definition: qgspointxy.h:48
QgsCurveEditorWidget::setHistogramSource
void setHistogramSource(const QgsVectorLayer *layer, const QString &expression)
Sets a layer and expression source for values to show in a histogram behind the curve.
Definition: qgscurveeditorwidget.cpp:105
QgsPointXY::x
Q_GADGET double x
Definition: qgspointxy.h:47
QgsCurveTransform::setControlPoints
void setControlPoints(const QList< QgsPointXY > &points)
Sets the list of control points for the transform.
Definition: qgspropertytransformer.cpp:705
QgsCurveEditorWidget::setMaxHistogramValueRange
void setMaxHistogramValueRange(double maxValueRange)
Sets the maximum expected value for the range of values shown in the histogram.
Definition: qgscurveeditorwidget.cpp:146
qgscurveeditorwidget.h
QgsCurveEditorWidget::setCurve
void setCurve(const QgsCurveTransform &curve)
Sets the curve to show in the widget.
Definition: qgscurveeditorwidget.cpp:98
QgsCurveEditorWidget::changed
void changed()
Emitted when the widget curve changes.
QgsCurveEditorWidget::curve
QgsCurveTransform curve() const
Returns a curve representing the current curve from the widget.
Definition: qgscurveeditorwidget.h:160
qgsDoubleNear
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
QgsCurveTransform
Handles scaling of input values to output values by using a curve created from smoothly joining a num...
Definition: qgspropertytransformer.h:57
qgsvectorlayer.h
QgsPointXY
A class to represent a 2D point.
Definition: qgspointxy.h:44
QgsCurveTransform::y
double y(double x) const
Returns the mapped y value corresponding to the specified x value.
Definition: qgspropertytransformer.cpp:746
QgsCurveEditorWidget::keyPressEvent
void keyPressEvent(QKeyEvent *event) override
Definition: qgscurveeditorwidget.cpp:152
QgsCurveEditorWidget::setMinHistogramValueRange
void setMinHistogramValueRange(double minValueRange)
Sets the minimum expected value for the range of values shown in the histogram.
Definition: qgscurveeditorwidget.cpp:140
QgsCurveTransform::controlPoints
QList< QgsPointXY > controlPoints() const
Returns a list of the control points for the transform.
Definition: qgspropertytransformer.h:86
QgsVectorLayer
Represents a vector layer which manages a vector based data sets.
Definition: qgsvectorlayer.h:387
QgsCurveTransform::addControlPoint
void addControlPoint(double x, double y)
Adds a control point to the transform.
Definition: qgspropertytransformer.cpp:717
QgsCurveEditorWidget::~QgsCurveEditorWidget
~QgsCurveEditorWidget() override
Definition: qgscurveeditorwidget.cpp:88
QgsCurveEditorWidget::QgsCurveEditorWidget
QgsCurveEditorWidget(QWidget *parent=nullptr, const QgsCurveTransform &curve=QgsCurveTransform())
Constructor for QgsCurveEditorWidget.
Definition: qgscurveeditorwidget.cpp:41