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