Permalink
Show file tree
Hide file tree
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
[FEATURE] Interactive curve editing for property overrides
This adds a new interactive "curve" to the assistant widgets. It allows you to fine tune exactly how input values get mapped to output sizes/colors/etc. Think GIMP or Photoshop curves, but for your data...
- Loading branch information
Showing
8 changed files
with
566 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
class QgsCurveEditorWidget : QWidget | ||
{ | ||
%TypeHeaderCode | ||
#include <qgscurveeditorwidget.h> | ||
%End | ||
|
||
public: | ||
|
||
QgsCurveEditorWidget( QWidget* parent /TransferThis/ = 0, const QgsCurveTransform& curve = QgsCurveTransform() ); | ||
|
||
QgsCurveTransform curve() const; | ||
|
||
void setCurve( const QgsCurveTransform& curve ); | ||
|
||
signals: | ||
|
||
void changed(); | ||
|
||
protected: | ||
|
||
virtual void keyPressEvent( QKeyEvent *event ); | ||
|
||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,312 @@ | ||
/*************************************************************************** | ||
qgscurveeditorwidget.cpp | ||
------------------------ | ||
begin : February 2017 | ||
copyright : (C) 2017 by Nyall Dawson | ||
email : nyall dot dawson at gmail dot com | ||
*************************************************************************** | ||
* * | ||
* This program is free software; you can redistribute it and/or modify * | ||
* it under the terms of the GNU General Public License as published by * | ||
* the Free Software Foundation; either version 2 of the License, or * | ||
* (at your option) any later version. * | ||
* * | ||
***************************************************************************/ | ||
|
||
|
||
#include "qgscurveeditorwidget.h" | ||
|
||
#include <qmath.h> | ||
#include <QPainter> | ||
#include <QVBoxLayout> | ||
#include <QMouseEvent> | ||
|
||
// QWT Charting widget | ||
#include <qwt_global.h> | ||
#include <qwt_plot_canvas.h> | ||
#include <qwt_plot.h> | ||
#include <qwt_plot_curve.h> | ||
#include <qwt_plot_grid.h> | ||
#include <qwt_plot_marker.h> | ||
#include <qwt_plot_picker.h> | ||
#include <qwt_picker_machine.h> | ||
#include <qwt_plot_layout.h> | ||
#include <qwt_symbol.h> | ||
#include <qwt_legend.h> | ||
|
||
QgsCurveEditorWidget::QgsCurveEditorWidget( QWidget* parent, const QgsCurveTransform& transform ) | ||
: QWidget( parent ) | ||
, mCurve( transform ) | ||
, mCurrentPlotMarkerIndex( -1 ) | ||
{ | ||
mPlot = new QwtPlot(); | ||
mPlot->setMinimumSize( QSize( 0, 100 ) ); | ||
mPlot->setAxisScale( QwtPlot::yLeft, 0, 1 ); | ||
mPlot->setAxisScale( QwtPlot::yRight, 0, 1 ); | ||
mPlot->setAxisScale( QwtPlot::xBottom, 0, 1 ); | ||
mPlot->setAxisScale( QwtPlot::xTop, 0, 1 ); | ||
|
||
QVBoxLayout* vlayout = new QVBoxLayout(); | ||
vlayout->addWidget( mPlot ); | ||
setLayout( vlayout ); | ||
|
||
// hide the ugly canvas frame | ||
mPlot->setFrameStyle( QFrame::NoFrame ); | ||
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000 | ||
QFrame* plotCanvasFrame = dynamic_cast<QFrame*>( mPlot->canvas() ); | ||
if ( plotCanvasFrame ) | ||
plotCanvasFrame->setFrameStyle( QFrame::NoFrame ); | ||
#else | ||
mPlot->canvas()->setFrameStyle( QFrame::NoFrame ); | ||
#endif | ||
|
||
mPlot->enableAxis( QwtPlot::yLeft, false ); | ||
mPlot->enableAxis( QwtPlot::xBottom, false ); | ||
|
||
// add a grid | ||
QwtPlotGrid * grid = new QwtPlotGrid(); | ||
QwtScaleDiv gridDiv( 0.0, 1.0, QList<double>(), QList<double>(), QList<double>() << 0.2 << 0.4 << 0.6 << 0.8 ); | ||
grid->setXDiv( gridDiv ); | ||
grid->setYDiv( gridDiv ); | ||
grid->setPen( QPen( QColor( 0, 0, 0, 50 ) ) ); | ||
grid->attach( mPlot ); | ||
|
||
mPlotCurve = new QwtPlotCurve(); | ||
mPlotCurve->setTitle( QStringLiteral( "Curve" ) ); | ||
mPlotCurve->setPen( QPen( QColor( 30, 30, 30 ), 0.0 ) ), | ||
mPlotCurve->setRenderHint( QwtPlotItem::RenderAntialiased, true ); | ||
mPlotCurve->attach( mPlot ); | ||
|
||
mPlotFilter = new QgsCurveEditorPlotEventFilter( mPlot ); | ||
connect( mPlotFilter, &QgsCurveEditorPlotEventFilter::mousePress, this, &QgsCurveEditorWidget::plotMousePress ); | ||
connect( mPlotFilter, &QgsCurveEditorPlotEventFilter::mouseRelease, this, &QgsCurveEditorWidget::plotMouseRelease ); | ||
connect( mPlotFilter, &QgsCurveEditorPlotEventFilter::mouseMove, this, &QgsCurveEditorWidget::plotMouseMove ); | ||
|
||
mPlotCurve->setVisible( true ); | ||
updatePlot(); | ||
} | ||
|
||
void QgsCurveEditorWidget::setCurve( const QgsCurveTransform& curve ) | ||
{ | ||
mCurve = curve; | ||
updatePlot(); | ||
emit changed(); | ||
} | ||
|
||
void QgsCurveEditorWidget::keyPressEvent( QKeyEvent* event ) | ||
{ | ||
if ( event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace ) | ||
{ | ||
QList< QgsPoint > cp = mCurve.controlPoints(); | ||
if ( mCurrentPlotMarkerIndex > 0 && mCurrentPlotMarkerIndex < cp.count() - 1 ) | ||
{ | ||
cp.removeAt( mCurrentPlotMarkerIndex ); | ||
mCurve.setControlPoints( cp ); | ||
updatePlot(); | ||
emit changed(); | ||
} | ||
} | ||
} | ||
|
||
void QgsCurveEditorWidget::plotMousePress( QPointF point ) | ||
{ | ||
mCurrentPlotMarkerIndex = findNearestControlPoint( point ); | ||
if ( mCurrentPlotMarkerIndex < 0 ) | ||
{ | ||
// add a new point | ||
mCurve.addControlPoint( point.x(), point.y() ); | ||
mCurrentPlotMarkerIndex = findNearestControlPoint( point ); | ||
emit changed(); | ||
} | ||
updatePlot(); | ||
} | ||
|
||
|
||
int QgsCurveEditorWidget::findNearestControlPoint( QPointF point ) const | ||
{ | ||
double minDist = 3.0 / mPlot->width(); | ||
int currentPlotMarkerIndex = -1; | ||
|
||
QList< QgsPoint > controlPoints = mCurve.controlPoints(); | ||
|
||
for ( int i = 0; i < controlPoints.count(); ++i ) | ||
{ | ||
QgsPoint currentPoint = controlPoints.at( i ); | ||
double currentDist; | ||
currentDist = qPow( point.x() - currentPoint.x(), 2.0 ) + qPow( point.y() - currentPoint.y(), 2.0 ); | ||
if ( currentDist < minDist ) | ||
{ | ||
minDist = currentDist; | ||
currentPlotMarkerIndex = i; | ||
} | ||
} | ||
return currentPlotMarkerIndex; | ||
} | ||
|
||
|
||
void QgsCurveEditorWidget::plotMouseRelease( QPointF ) | ||
{ | ||
} | ||
|
||
void QgsCurveEditorWidget::plotMouseMove( QPointF point ) | ||
{ | ||
if ( mCurrentPlotMarkerIndex < 0 ) | ||
return; | ||
|
||
QList< QgsPoint > cp = mCurve.controlPoints(); | ||
bool removePoint = false; | ||
if ( mCurrentPlotMarkerIndex == 0 ) | ||
{ | ||
point.setX( qMin( point.x(), cp.at( 1 ).x() - 0.01 ) ); | ||
} | ||
else | ||
{ | ||
removePoint = point.x() <= cp.at( mCurrentPlotMarkerIndex - 1 ).x(); | ||
} | ||
if ( mCurrentPlotMarkerIndex == cp.count() - 1 ) | ||
{ | ||
point.setX( qMax( point.x(), cp.at( mCurrentPlotMarkerIndex - 1 ).x() + 0.01 ) ); | ||
removePoint = false; | ||
} | ||
else | ||
{ | ||
removePoint = removePoint || point.x() >= cp.at( mCurrentPlotMarkerIndex + 1 ).x(); | ||
} | ||
|
||
if ( removePoint ) | ||
{ | ||
cp.removeAt( mCurrentPlotMarkerIndex ); | ||
mCurrentPlotMarkerIndex = -1; | ||
} | ||
else | ||
{ | ||
cp[ mCurrentPlotMarkerIndex ] = QgsPoint( point.x(), point.y() ); | ||
} | ||
mCurve.setControlPoints( cp ); | ||
updatePlot(); | ||
emit changed(); | ||
} | ||
|
||
void QgsCurveEditorWidget::addPlotMarker( double x, double y, bool isSelected ) | ||
{ | ||
QColor borderColor( 0, 0, 0 ); | ||
|
||
QColor brushColor = isSelected ? borderColor : QColor( 255, 255, 255, 0 ); | ||
|
||
QwtPlotMarker *marker = new QwtPlotMarker(); | ||
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000 | ||
marker->setSymbol( new QwtSymbol( QwtSymbol::Ellipse, QBrush( brushColor ), QPen( borderColor, isSelected ? 2 : 1 ), QSize( 6, 6 ) ) ); | ||
#else | ||
marker->setSymbol( QwtSymbol( QwtSymbol::Ellipse, QBrush( brushColor ), QPen( borderColor, isSelected ? 2 : 1 ), QSize( 6, 6 ) ) ); | ||
#endif | ||
marker->setValue( x, y ); | ||
marker->attach( mPlot ); | ||
marker->setRenderHint( QwtPlotItem::RenderAntialiased, true ); | ||
mMarkers << marker; | ||
} | ||
|
||
void QgsCurveEditorWidget::updatePlot() | ||
{ | ||
// remove existing markers | ||
Q_FOREACH ( QwtPlotMarker* marker, mMarkers ) | ||
{ | ||
marker->detach(); | ||
delete marker; | ||
} | ||
mMarkers.clear(); | ||
|
||
QPolygonF curvePoints; | ||
QVector< double > x; | ||
|
||
int i = 0; | ||
Q_FOREACH ( const QgsPoint& point, mCurve.controlPoints() ) | ||
{ | ||
x << point.x(); | ||
addPlotMarker( point.x(), point.y(), mCurrentPlotMarkerIndex == i ); | ||
i++; | ||
} | ||
|
||
//add extra intermediate points | ||
|
||
for ( double p = 0; p <= 1.0; p += 0.01 ) | ||
{ | ||
x << p; | ||
} | ||
std::sort( x.begin(), x.end() ); | ||
QVector< double > y = mCurve.y( x ); | ||
|
||
for ( int j = 0; j < x.count(); ++j ) | ||
{ | ||
curvePoints << QPointF( x.at( j ), y.at( j ) ); | ||
} | ||
|
||
#if defined(QWT_VERSION) && QWT_VERSION>=0x060000 | ||
mPlotCurve->setSamples( curvePoints ); | ||
#else | ||
mPlotCurve->setData( curvePoints ); | ||
#endif | ||
mPlot->replot(); | ||
} | ||
|
||
|
||
/// @cond PRIVATE | ||
|
||
QgsCurveEditorPlotEventFilter::QgsCurveEditorPlotEventFilter( QwtPlot *plot ) | ||
: QObject( plot ) | ||
, mPlot( plot ) | ||
{ | ||
mPlot->canvas()->installEventFilter( this ); | ||
} | ||
|
||
bool QgsCurveEditorPlotEventFilter::eventFilter( QObject *object, QEvent *event ) | ||
{ | ||
if ( !mPlot->isEnabled() ) | ||
return QObject::eventFilter( object, event ); | ||
|
||
switch ( event->type() ) | ||
{ | ||
case QEvent::MouseButtonPress: | ||
{ | ||
const QMouseEvent* mouseEvent = static_cast<QMouseEvent* >( event ); | ||
if ( mouseEvent->button() == Qt::LeftButton ) | ||
{ | ||
emit mousePress( mapPoint( mouseEvent->pos() ) ); | ||
} | ||
break; | ||
} | ||
case QEvent::MouseMove: | ||
{ | ||
const QMouseEvent* mouseEvent = static_cast<QMouseEvent* >( event ); | ||
if ( mouseEvent->buttons() & Qt::LeftButton ) | ||
{ | ||
// only emit when button pressed | ||
emit mouseMove( mapPoint( mouseEvent->pos() ) ); | ||
} | ||
break; | ||
} | ||
case QEvent::MouseButtonRelease: | ||
{ | ||
const QMouseEvent* mouseEvent = static_cast<QMouseEvent* >( event ); | ||
if ( mouseEvent->button() == Qt::LeftButton ) | ||
{ | ||
emit mouseRelease( mapPoint( mouseEvent->pos() ) ); | ||
} | ||
break; | ||
} | ||
default: | ||
break; | ||
} | ||
|
||
return QObject::eventFilter( object, event ); | ||
} | ||
|
||
QPointF QgsCurveEditorPlotEventFilter::mapPoint( QPointF point ) const | ||
{ | ||
if ( !mPlot ) | ||
return QPointF(); | ||
|
||
return QPointF( mPlot->canvasMap( QwtPlot::xBottom ).invTransform( point.x() ), | ||
mPlot->canvasMap( QwtPlot::yLeft ).invTransform( point.y() ) ); | ||
} | ||
|
||
///@endcond |
Oops, something went wrong.