Skip to content

Commit

Permalink
[FEATURE] Flash features in canvas
Browse files Browse the repository at this point in the history
This adds:
- API call to QgsMapCanvas to flash a set of features
- A right click menu option in the attribute table to flash
the clicked feature
- An option in the Search by Form dialog to flash matching
features

When triggered, the features flash allowing easy identification
without having to alter the current selection or map extent
  • Loading branch information
nyalldawson committed Oct 1, 2017
1 parent 4374c6e commit 3e9fa72
Show file tree
Hide file tree
Showing 10 changed files with 203 additions and 0 deletions.
6 changes: 6 additions & 0 deletions python/gui/qgsattributeform.sip
Expand Up @@ -163,6 +163,12 @@ class QgsAttributeForm : QWidget
%Docstring
Emitted when the user chooses to zoom to a filtered set of features.
.. versionadded:: 3.0
%End

void flashFeatures( const QString &filter );
%Docstring
Emitted when the user chooses to flash a filtered set of features.
.. versionadded:: 3.0
%End

public slots:
Expand Down
13 changes: 13 additions & 0 deletions python/gui/qgsmapcanvas.sip
Expand Up @@ -232,6 +232,19 @@ Zoom to the next extent (view)
void panToSelected( QgsVectorLayer *layer = 0 );
%Docstring
Pan to the selected features of current (vector) layer keeping same extent.
%End

void flashFeatureIds( QgsVectorLayer *layer, const QgsFeatureIds &ids,
const QColor &startColor = QColor( 255, 0, 0, 255 ), const QColor &endColor = QColor( 255, 0, 0, 0 ),
int flashes = 3, int duration = 500 );
%Docstring
Causes a set of features with matching ``ids`` from a vector ``layer`` to flash
within the canvas.

The ``startColor`` and ``endColor`` can be specified, along with the number of
``flashes`` and ``duration`` of each flash (in milliseconds).

.. versionadded:: 3.0
%End

void setMapTool( QgsMapTool *mapTool );
Expand Down
33 changes: 33 additions & 0 deletions src/app/qgsselectbyformdialog.cpp
Expand Up @@ -63,6 +63,7 @@ void QgsSelectByFormDialog::setMapCanvas( QgsMapCanvas *canvas )
{
mMapCanvas = canvas;
connect( mForm, &QgsAttributeForm::zoomToFeatures, this, &QgsSelectByFormDialog::zoomToFeatures );
connect( mForm, &QgsAttributeForm::flashFeatures, this, &QgsSelectByFormDialog::flashFeatures );
}

void QgsSelectByFormDialog::zoomToFeatures( const QString &filter )
Expand Down Expand Up @@ -112,3 +113,35 @@ void QgsSelectByFormDialog::zoomToFeatures( const QString &filter )
timeout );
}
}

void QgsSelectByFormDialog::flashFeatures( const QString &filter )
{
QgsExpressionContext context( QgsExpressionContextUtils::globalProjectLayerScopes( mLayer ) );

QgsFeatureRequest request = QgsFeatureRequest().setFilterExpression( filter )
.setFlags( QgsFeatureRequest::NoGeometry )
.setExpressionContext( context )
.setSubsetOfAttributes( QgsAttributeList() );

QgsFeatureIterator features = mLayer->getFeatures( request );
QgsFeature feat;
QgsFeatureIds ids;
while ( features.nextFeature( feat ) )
{
ids.insert( feat.id() );
}

QgsSettings settings;
int timeout = settings.value( QStringLiteral( "qgis/messageTimeout" ), 5 ).toInt();
if ( !ids.empty() )
{
mMapCanvas->flashFeatureIds( mLayer, ids );
}
else if ( mMessageBar )
{
mMessageBar->pushMessage( QString(),
tr( "No matching features found" ),
QgsMessageBar::INFO,
timeout );
}
}
1 change: 1 addition & 0 deletions src/app/qgsselectbyformdialog.h
Expand Up @@ -64,6 +64,7 @@ class APP_EXPORT QgsSelectByFormDialog : public QDialog
private slots:

void zoomToFeatures( const QString &filter );
void flashFeatures( const QString &filter );

private:

Expand Down
18 changes: 18 additions & 0 deletions src/gui/attributetable/qgsdualview.cpp
Expand Up @@ -540,6 +540,7 @@ void QgsDualView::viewWillShowContextMenu( QMenu *menu, const QModelIndex &atInd
{
menu->addAction( tr( "Zoom to feature" ), this, SLOT( zoomToCurrentFeature() ) );
menu->addAction( tr( "Pan to feature" ), this, SLOT( panToCurrentFeature() ) );
menu->addAction( tr( "Flash feature" ), this, SLOT( flashCurrentFeature() ) );
}

//add user-defined actions to context menu
Expand Down Expand Up @@ -780,6 +781,23 @@ void QgsDualView::panToCurrentFeature()
}
}

void QgsDualView::flashCurrentFeature()
{
QModelIndex currentIndex = mTableView->currentIndex();
if ( !currentIndex.isValid() )
{
return;
}

QgsFeatureIds ids;
ids.insert( mFilterModel->rowToId( currentIndex ) );
QgsMapCanvas *canvas = mFilterModel->mapCanvas();
if ( canvas )
{
canvas->flashFeatureIds( mLayer, ids );
}
}

void QgsDualView::rebuildFullLayerCache()
{
connect( mLayerCache, &QgsVectorLayerCache::progress, this, &QgsDualView::progress, Qt::UniqueConnection );
Expand Down
2 changes: 2 additions & 0 deletions src/gui/attributetable/qgsdualview.h
Expand Up @@ -328,6 +328,8 @@ class GUI_EXPORT QgsDualView : public QStackedWidget, private Ui::QgsDualViewBas
//! Pans to the active feature
void panToCurrentFeature();

void flashCurrentFeature();

void rebuildFullLayerCache();

private:
Expand Down
15 changes: 15 additions & 0 deletions src/gui/qgsattributeform.cpp
Expand Up @@ -410,6 +410,15 @@ void QgsAttributeForm::searchZoomTo()
emit zoomToFeatures( filter );
}

void QgsAttributeForm::searchFlash()
{
QString filter = createFilterExpression();
if ( filter.isEmpty() )
return;

emit flashFeatures( filter );
}

void QgsAttributeForm::filterAndTriggered()
{
QString filter = createFilterExpression();
Expand Down Expand Up @@ -1353,6 +1362,12 @@ void QgsAttributeForm::init()
boxLayout->addWidget( clearButton );
boxLayout->addStretch( 1 );

QPushButton *flashButton = new QPushButton();
flashButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
flashButton->setText( tr( "&Flash features" ) );
connect( flashButton, &QToolButton::clicked, this, &QgsAttributeForm::searchFlash );
boxLayout->addWidget( flashButton );

QPushButton *zoomButton = new QPushButton();
zoomButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
zoomButton->setText( tr( "&Zoom to features" ) );
Expand Down
7 changes: 7 additions & 0 deletions src/gui/qgsattributeform.h
Expand Up @@ -202,6 +202,12 @@ class GUI_EXPORT QgsAttributeForm : public QWidget
*/
void zoomToFeatures( const QString &filter );

/**
* Emitted when the user chooses to flash a filtered set of features.
* \since QGIS 3.0
*/
void flashFeatures( const QString &filter );

public slots:

/**
Expand Down Expand Up @@ -263,6 +269,7 @@ class GUI_EXPORT QgsAttributeForm : public QWidget
void filterTriggered();

void searchZoomTo();
void searchFlash();
void searchSetSelection();
void searchAddToSelection();
void searchRemoveFromSelection();
Expand Down
95 changes: 95 additions & 0 deletions src/gui/qgsmapcanvas.cpp
Expand Up @@ -1058,6 +1058,101 @@ void QgsMapCanvas::panToSelected( QgsVectorLayer *layer )
refresh();
}

void QgsMapCanvas::flashFeatureIds( QgsVectorLayer *layer, const QgsFeatureIds &ids,
const QColor &color1, const QColor &color2,
int flashes, int duration )
{
if ( !layer )
{
return;
}

QList< QgsGeometry > geoms;

QgsFeatureIterator it = layer->getFeatures( QgsFeatureRequest().setFilterFids( ids ).setSubsetOfAttributes( QgsAttributeList() ) );
QgsFeature fet;
while ( it.nextFeature( fet ) )
{
if ( !fet.hasGeometry() )
continue;
geoms << fet.geometry();
}

if ( geoms.isEmpty() )
return;

QgsWkbTypes::GeometryType geomType = QgsWkbTypes::geometryType( layer->wkbType() );
QgsRubberBand *rb = new QgsRubberBand( this, geomType );
for ( const QgsGeometry &geom : qgsAsConst( geoms ) )
rb->addGeometry( geom, layer );

if ( geomType == QgsWkbTypes::LineGeometry || geomType == QgsWkbTypes::PointGeometry )
{
rb->setWidth( 2 );
rb->setSecondaryStrokeColor( QColor( 255, 255, 255 ) );
}
if ( geomType == QgsWkbTypes::PointGeometry )
rb->setIcon( QgsRubberBand::ICON_CIRCLE );

QColor startColor = color1;
if ( !startColor.isValid() )
{
if ( geomType == QgsWkbTypes::PolygonGeometry )
{
startColor = rb->fillColor();
}
else
{
startColor = rb->strokeColor();
}
startColor.setAlpha( 255 );
}
QColor endColor = color2;
if ( !endColor.isValid() )
{
endColor = startColor;
endColor.setAlpha( 0 );
}


QVariantAnimation *animation = new QVariantAnimation( this );
connect( animation, &QVariantAnimation::finished, this, [animation, rb]
{
animation->deleteLater();
delete rb;
} );
connect( animation, &QPropertyAnimation::valueChanged, this, [rb, geomType]( const QVariant & value )
{
QColor c = value.value<QColor>();
if ( geomType == QgsWkbTypes::PolygonGeometry )
{
rb->setFillColor( c );
}
else
{
rb->setStrokeColor( c );
QColor c = rb->secondaryStrokeColor();
c.setAlpha( c.alpha() );
rb->setSecondaryStrokeColor( c );
}
rb->update();
} );

animation->setDuration( duration * flashes );
animation->setStartValue( endColor );
double midStep = 0.2 / flashes;
for ( int i = 0; i < flashes; ++i )
{
double start = static_cast< double >( i ) / flashes;
animation->setKeyValueAt( start + midStep, startColor );
double end = static_cast< double >( i + 1 ) / flashes;
if ( !qgsDoubleNear( end, 1.0 ) )
animation->setKeyValueAt( end, endColor );
}
animation->setEndValue( endColor );
animation->start();
}

void QgsMapCanvas::keyPressEvent( QKeyEvent *e )
{
if ( mCanvasProperties->mouseButtonDown || mCanvasProperties->panSelectorDown )
Expand Down
13 changes: 13 additions & 0 deletions src/gui/qgsmapcanvas.h
Expand Up @@ -236,6 +236,19 @@ class GUI_EXPORT QgsMapCanvas : public QGraphicsView
//! Pan to the selected features of current (vector) layer keeping same extent.
void panToSelected( QgsVectorLayer *layer = nullptr );

/**
* Causes a set of features with matching \a ids from a vector \a layer to flash
* within the canvas.
*
* The \a startColor and \a endColor can be specified, along with the number of
* \a flashes and \a duration of each flash (in milliseconds).
*
* \since QGIS 3.0
*/
void flashFeatureIds( QgsVectorLayer *layer, const QgsFeatureIds &ids,
const QColor &startColor = QColor( 255, 0, 0, 255 ), const QColor &endColor = QColor( 255, 0, 0, 0 ),
int flashes = 3, int duration = 500 );

//! \brief Sets the map tool currently being used on the canvas
void setMapTool( QgsMapTool *mapTool );

Expand Down

0 comments on commit 3e9fa72

Please sign in to comment.