Skip to content
Permalink
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
nyalldawson committed Feb 22, 2017
1 parent 45861d3 commit cc9b5a47b753129752019f0d7deb7ad1b13d0db9
@@ -51,6 +51,7 @@
%Include qgsconfigureshortcutsdialog.sip
%Include qgscredentialdialog.sip
%Include qgscustomdrophandler.sip
%Include qgscurveeditorwidget.sip
%Include qgsdetaileditemdata.sip
%Include qgsdetaileditemdelegate.sip
%Include qgsdetaileditemwidget.sip
@@ -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 );

};
@@ -193,6 +193,7 @@ SET(QGIS_GUI_SRCS
qgscredentialdialog.cpp
qgscursors.cpp
qgscustomdrophandler.cpp
qgscurveeditorwidget.cpp
qgsdatumtransformdialog.cpp
qgsdetaileditemdata.cpp
qgsdetaileditemdelegate.cpp
@@ -345,6 +346,7 @@ SET(QGIS_GUI_MOC_HDRS
qgscompoundcolorwidget.h
qgsconfigureshortcutsdialog.h
qgscredentialdialog.h
qgscurveeditorwidget.h
qgsdatumtransformdialog.h
qgsdetaileditemdelegate.h
qgsdetaileditemwidget.h
@@ -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

0 comments on commit cc9b5a4

Please sign in to comment.
You can’t perform that action at this time.