QGIS API Documentation  3.26.3-Buenos Aires (65e4edfdad)
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 
QgsPointXY::y
double y
Definition: qgspointxy.h:63
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:107
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:148
qgscurveeditorwidget.h
QgsCurveEditorWidget::setCurve
void setCurve(const QgsCurveTransform &curve)
Sets the curve to show in the widget.
Definition: qgscurveeditorwidget.cpp:100
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:2265
QgsCurveTransform
Handles scaling of input values to output values by using a curve created from smoothly joining a num...
Definition: qgspropertytransformer.h:56
qgsvectorlayer.h
QgsPointXY
A class to represent a 2D point.
Definition: qgspointxy.h:58
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:154
QgsCurveEditorWidget::setMinHistogramValueRange
void setMinHistogramValueRange(double minValueRange)
Sets the minimum expected value for the range of values shown in the histogram.
Definition: qgscurveeditorwidget.cpp:142
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:391
QgsCurveTransform::addControlPoint
void addControlPoint(double x, double y)
Adds a control point to the transform.
Definition: qgspropertytransformer.cpp:717
QgsPointXY::x
double x
Definition: qgspointxy.h:62
QgsCurveEditorWidget::~QgsCurveEditorWidget
~QgsCurveEditorWidget() override
Definition: qgscurveeditorwidget.cpp:90
QgsCurveEditorWidget::QgsCurveEditorWidget
QgsCurveEditorWidget(QWidget *parent=nullptr, const QgsCurveTransform &curve=QgsCurveTransform())
Constructor for QgsCurveEditorWidget.
Definition: qgscurveeditorwidget.cpp:43