Skip to content

Commit

Permalink
[FEATURE] Allow layers to be automatically refreshed at a specified i…
Browse files Browse the repository at this point in the history
…nterval

This allows users to set a timer interval in layer properties
for individual layers. These layers will be automatically refreshed
at a matching interval.

Canvas updates are deferred in order to avoid refreshing multiple
times if more than one layer has an auto update interval set.

Additionally, logic has been added to skip any auto redraws of
the canvas while the canvas is already being redrawn. This avoids
issues caused by setting a layer auto refresh to a shorter time than
is required to redraw the canvas.
  • Loading branch information
nyalldawson committed Feb 12, 2017
1 parent 9b9d49a commit 38f87a6
Show file tree
Hide file tree
Showing 14 changed files with 417 additions and 2 deletions.
7 changes: 7 additions & 0 deletions python/core/qgsmaplayer.sip
Expand Up @@ -603,6 +603,11 @@ class QgsMapLayer : QObject
*/
bool hasScaleBasedVisibility() const;

bool hasAutoRefreshEnabled() const;
int autoRefreshInterval() const;
void setAutoRefreshInterval( int interval );
void setAutoRefreshEnabled( bool enabled );

public slots:

/** Event handler for when a coordinate transform fails due to bad vertex error */
Expand Down Expand Up @@ -730,6 +735,8 @@ class QgsMapLayer : QObject
*/
void willBeDeleted();

void autoRefreshIntervalChanged( int interval );

protected:
/** Set the extent */
virtual void setExtent( const QgsRectangle &rect );
Expand Down
9 changes: 9 additions & 0 deletions src/app/qgsrasterlayerproperties.cpp
Expand Up @@ -139,6 +139,8 @@ QgsRasterLayerProperties::QgsRasterLayerProperties( QgsMapLayer* lyr, QgsMapCanv
// enable or disable Build Pyramids button depending on selection in pyramid list
connect( lbxPyramidResolutions, SIGNAL( itemSelectionChanged() ), this, SLOT( toggleBuildPyramidsButton() ) );

connect( mRefreshLayerCheckBox, &QCheckBox::toggled, mRefreshLayerIntervalSpinBox, &QDoubleSpinBox::setEnabled );

// set up the scale based layer visibility stuff....
mScaleRangeWidget->setMapCanvas( mMapCanvas );
chkUseScaleDependentRendering->setChecked( lyr->hasScaleBasedVisibility() );
Expand Down Expand Up @@ -713,6 +715,10 @@ void QgsRasterLayerProperties::sync()
leNoDataValue->insert( QLatin1String( "" ) );
}

mRefreshLayerCheckBox->setChecked( mRasterLayer->hasAutoRefreshEnabled() );
mRefreshLayerIntervalSpinBox->setEnabled( mRasterLayer->hasAutoRefreshEnabled() );
mRefreshLayerIntervalSpinBox->setValue( mRasterLayer->autoRefreshInterval() / 1000.0 );

populateTransparencyTable( mRasterLayer->renderer() );

QgsDebugMsg( "populate colormap tab" );
Expand Down Expand Up @@ -940,6 +946,9 @@ void QgsRasterLayerProperties::apply()
mRasterLayer->setMaximumScale( 1.0 / mScaleRangeWidget->minimumScale() );
mRasterLayer->setMinimumScale( 1.0 / mScaleRangeWidget->maximumScale() );

mRasterLayer->setAutoRefreshInterval( mRefreshLayerIntervalSpinBox->value() * 1000.0 );
mRasterLayer->setAutoRefreshEnabled( mRefreshLayerCheckBox->isChecked() );

//update the legend pixmap
// pixmapLegend->setPixmap( mRasterLayer->legendAsPixmap() );
// pixmapLegend->setScaledContents( true );
Expand Down
10 changes: 10 additions & 0 deletions src/app/qgsvectorlayerproperties.cpp
Expand Up @@ -322,6 +322,9 @@ QgsVectorLayerProperties::QgsVectorLayerProperties(
}

mLayersDependenciesTreeView->setModel( mLayersDependenciesTreeModel.get() );

connect( mRefreshLayerCheckBox, &QCheckBox::toggled, mRefreshLayerIntervalSpinBox, &QDoubleSpinBox::setEnabled );

} // QgsVectorLayerProperties ctor


Expand Down Expand Up @@ -446,6 +449,10 @@ void QgsVectorLayerProperties::syncToLayer()

mForceRasterCheckBox->setChecked( mLayer->renderer() && mLayer->renderer()->forceRasterRender() );

mRefreshLayerCheckBox->setChecked( mLayer->hasAutoRefreshEnabled() );
mRefreshLayerIntervalSpinBox->setEnabled( mLayer->hasAutoRefreshEnabled() );
mRefreshLayerIntervalSpinBox->setValue( mLayer->autoRefreshInterval() / 1000.0 );

// load appropriate symbology page (V1 or V2)
updateSymbologyPage();

Expand Down Expand Up @@ -583,6 +590,9 @@ void QgsVectorLayerProperties::apply()
if ( mLayer->renderer() )
mLayer->renderer()->setForceRasterRender( mForceRasterCheckBox->isChecked() );

mLayer->setAutoRefreshInterval( mRefreshLayerIntervalSpinBox->value() * 1000.0 );
mLayer->setAutoRefreshEnabled( mRefreshLayerCheckBox->isChecked() );

mOldJoins = mLayer->vectorJoins();

//save variables
Expand Down
41 changes: 41 additions & 0 deletions src/core/qgsmaplayer.cpp
Expand Up @@ -83,6 +83,7 @@ QgsMapLayer::QgsMapLayer( QgsMapLayer::LayerType type,
mScaleBasedVisibility = false;

connect( mStyleManager, &QgsMapLayerStyleManager::currentStyleChanged, this, &QgsMapLayer::styleChanged );
connect( &mRefreshTimer, &QTimer::timeout, this, [=] { triggerRepaint( true ); } );
}

QgsMapLayer::~QgsMapLayer()
Expand Down Expand Up @@ -420,6 +421,9 @@ bool QgsMapLayer::readLayerXml( const QDomElement& layerElement, const QgsProjec
setMinimumScale( layerElement.attribute( QStringLiteral( "minimumScale" ) ).toDouble() );
setMaximumScale( layerElement.attribute( QStringLiteral( "maximumScale" ) ).toDouble() );

setAutoRefreshInterval( layerElement.attribute( QStringLiteral( "autoRefreshTime" ), 0 ).toInt() );
setAutoRefreshEnabled( layerElement.attribute( QStringLiteral( "autoRefreshEnabled" ), QStringLiteral( "0" ) ).toInt() );

QDomNode extentNode = layerElement.namedItem( QStringLiteral( "extent" ) );
if ( !extentNode.isNull() )
{
Expand Down Expand Up @@ -537,6 +541,9 @@ bool QgsMapLayer::writeLayerXml( QDomElement& layerElement, QDomDocument& docume
layerElement.appendChild( QgsXmlUtils::writeRectangle( mExtent, document ) );
}

layerElement.setAttribute( QStringLiteral( "autoRefreshTime" ), QString::number( mRefreshTimer.interval() ) );
layerElement.setAttribute( QStringLiteral( "autoRefreshEnabled" ), mRefreshTimer.isActive() ? 1 : 0 );

// ID
QDomElement layerId = document.createElement( QStringLiteral( "id" ) );
QDomText layerIdText = document.createTextNode( id() );
Expand Down Expand Up @@ -857,6 +864,40 @@ bool QgsMapLayer::hasScaleBasedVisibility() const
return mScaleBasedVisibility;
}

bool QgsMapLayer::hasAutoRefreshEnabled() const
{
return mRefreshTimer.isActive();
}

int QgsMapLayer::autoRefreshInterval() const
{
return mRefreshTimer.interval();
}

void QgsMapLayer::setAutoRefreshInterval( int interval )
{
if ( interval <= 0 )
{
mRefreshTimer.stop();
mRefreshTimer.setInterval( 0 );
}
else
{
mRefreshTimer.setInterval( interval );
}
emit autoRefreshIntervalChanged( mRefreshTimer.isActive() ? mRefreshTimer.interval() : 0 );
}

void QgsMapLayer::setAutoRefreshEnabled( bool enabled )
{
if ( !enabled )
mRefreshTimer.stop();
else if ( mRefreshTimer.interval() > 0 )
mRefreshTimer.start();

emit autoRefreshIntervalChanged( mRefreshTimer.isActive() ? mRefreshTimer.interval() : 0 );
}

void QgsMapLayer::setMinimumScale( double scale )
{
mMinScale = scale;
Expand Down
50 changes: 50 additions & 0 deletions src/core/qgsmaplayer.h
Expand Up @@ -53,6 +53,7 @@ class CORE_EXPORT QgsMapLayer : public QObject
Q_OBJECT

Q_PROPERTY( QString name READ name WRITE setName NOTIFY nameChanged )
Q_PROPERTY( int autoRefreshInterval READ autoRefreshInterval WRITE setAutoRefreshInterval NOTIFY autoRefreshIntervalChanged )

public:

Expand Down Expand Up @@ -646,6 +647,44 @@ class CORE_EXPORT QgsMapLayer : public QObject
*/
bool hasScaleBasedVisibility() const;

/**
* Returns true if auto refresh is enabled for the layer.
* @note added in QGIS 3.0
* @see autoRefreshInterval()
* @see setAutoRefreshEnabled()
*/
bool hasAutoRefreshEnabled() const;

/**
* Returns the auto refresh interval (in milliseconds). Note that
* auto refresh is only active when hasAutoRefreshEnabled() is true.
* @note added in QGIS 3.0
* @see autoRefreshEnabled()
* @see setAutoRefreshInterval()
*/
int autoRefreshInterval() const;

/**
* Sets the auto refresh interval (in milliseconds) for the layer. This
* will cause the layer to be automatically redrawn on a matching interval.
* Note that auto refresh must be enabled by calling setAutoRefreshEnabled().
*
* Note that auto refresh triggers deferred repaints of the layer. Any map
* canvas must be refreshed separately in order to view the refreshed layer.
* @note added in QGIS 3.0
* @see autoRefreshInterval()
* @see setAutoRefreshEnabled()
*/
void setAutoRefreshInterval( int interval );

/**
* Sets whether auto refresh is enabled for the layer.
* @note added in QGIS 3.0
* @see hasAutoRefreshEnabled()
* @see setAutoRefreshInterval()
*/
void setAutoRefreshEnabled( bool enabled );

public slots:

//! Event handler for when a coordinate transform fails due to bad vertex error
Expand Down Expand Up @@ -786,6 +825,13 @@ class CORE_EXPORT QgsMapLayer : public QObject
*/
void willBeDeleted();

/**
* Emitted when the auto refresh interval changes.
* @see setAutoRefreshInterval()
* @note added in QGIS 3.0
*/
void autoRefreshIntervalChanged( int interval );

protected:
//! Set the extent
virtual void setExtent( const QgsRectangle &rect );
Expand Down Expand Up @@ -920,6 +966,10 @@ class CORE_EXPORT QgsMapLayer : public QObject

//! Manager of multiple styles available for a layer (may be null)
QgsMapLayerStyleManager* mStyleManager;

//! Timer for triggering automatic refreshes of the layer
QTimer mRefreshTimer;

};

Q_DECLARE_METATYPE( QgsMapLayer* )
Expand Down
2 changes: 1 addition & 1 deletion src/core/qgsvectorlayer.cpp
Expand Up @@ -164,7 +164,7 @@ QgsVectorLayer::QgsVectorLayer( const QString& vectorLayerPath,
setDataSource( vectorLayerPath, baseName, providerKey, loadDefaultStyleFlag );
}

connect( this, &QgsVectorLayer::selectionChanged, this, [=]{ emit repaintRequested(); } );
connect( this, &QgsVectorLayer::selectionChanged, this, [=] { emit repaintRequested(); } );
connect( QgsProject::instance()->relationManager(), &QgsRelationManager::relationsLoaded, this, &QgsVectorLayer::onRelationsLoaded );

// Default simplify drawing settings
Expand Down
39 changes: 38 additions & 1 deletion src/gui/qgsmapcanvas.cpp
Expand Up @@ -176,6 +176,8 @@ QgsMapCanvas::QgsMapCanvas( QWidget * parent )
QPixmap zoomPixmap = QPixmap(( const char ** )( zoom_in ) );
mZoomCursor = QCursor( zoomPixmap, 7, 7 );

connect( &mAutoRefreshTimer, &QTimer::timeout, this, &QgsMapCanvas::autoRefreshTriggered );

setInteractive( false );

refresh();
Expand Down Expand Up @@ -297,6 +299,7 @@ void QgsMapCanvas::setLayers( const QList<QgsMapLayer*>& layers )
{
disconnect( layer, &QgsMapLayer::repaintRequested, this, &QgsMapCanvas::layerRepaintRequested );
disconnect( layer, &QgsMapLayer::crsChanged, this, &QgsMapCanvas::layerCrsChange );
disconnect( layer, &QgsMapLayer::autoRefreshIntervalChanged, this, &QgsMapCanvas::updateAutoRefreshTimer );
if ( QgsVectorLayer* vlayer = qobject_cast<QgsVectorLayer *>( layer ) )
{
disconnect( vlayer, &QgsVectorLayer::selectionChanged, this, &QgsMapCanvas::selectionChangedSlot );
Expand All @@ -309,17 +312,18 @@ void QgsMapCanvas::setLayers( const QList<QgsMapLayer*>& layers )
{
connect( layer, &QgsMapLayer::repaintRequested, this, &QgsMapCanvas::layerRepaintRequested );
connect( layer, &QgsMapLayer::crsChanged, this, &QgsMapCanvas::layerCrsChange );
connect( layer, &QgsMapLayer::autoRefreshIntervalChanged, this, &QgsMapCanvas::updateAutoRefreshTimer );
if ( QgsVectorLayer* vlayer = qobject_cast<QgsVectorLayer *>( layer ) )
{
connect( vlayer, &QgsVectorLayer::selectionChanged, this, &QgsMapCanvas::selectionChangedSlot );
}
}

updateDatumTransformEntries();

QgsDebugMsg( "Layers have changed, refreshing" );
emit layersChanged();

updateAutoRefreshTimer();
refresh();
}

Expand Down Expand Up @@ -1674,7 +1678,40 @@ void QgsMapCanvas::layerRepaintRequested( bool deferred )
refresh();
}

void QgsMapCanvas::autoRefreshTriggered()
{
if ( mJob )
{
// canvas is currently being redrawn, so we skip this auto refresh
// otherwise we could get stuck in the situation where an auto refresh is triggered
// too often to allow the canvas to ever finish rendering
return;
}

refresh();
}

void QgsMapCanvas::updateAutoRefreshTimer()
{
// min auto refresh interval stores the smallest interval between layer auto refreshes. We automatically
// trigger a map refresh on this minimum interval
int minAutoRefreshInterval = -1;
Q_FOREACH ( QgsMapLayer* layer, mSettings.layers() )
{
if ( layer->hasAutoRefreshEnabled() && layer->autoRefreshInterval() > 0 )
minAutoRefreshInterval = minAutoRefreshInterval > 0 ? qMin( layer->autoRefreshInterval(), minAutoRefreshInterval ) : layer->autoRefreshInterval();
}

if ( minAutoRefreshInterval > 0 )
{
mAutoRefreshTimer.setInterval( minAutoRefreshInterval );
mAutoRefreshTimer.start();
}
else
{
mAutoRefreshTimer.stop();
}
}

QgsMapTool* QgsMapCanvas::mapTool()
{
Expand Down
6 changes: 6 additions & 0 deletions src/gui/qgsmapcanvas.h
Expand Up @@ -616,6 +616,10 @@ class GUI_EXPORT QgsMapCanvas : public QGraphicsView

void layerRepaintRequested( bool deferred );

void autoRefreshTriggered();

void updateAutoRefreshTimer();

private:
/// this class is non-copyable

Expand Down Expand Up @@ -706,6 +710,8 @@ class GUI_EXPORT QgsMapCanvas : public QGraphicsView

QCursor mZoomCursor;

QTimer mAutoRefreshTimer;

//! Force a resize of the map canvas item
//! @note added in 2.16
void updateMapSize();
Expand Down
48 changes: 48 additions & 0 deletions src/ui/qgsrasterlayerpropertiesbase.ui
Expand Up @@ -420,6 +420,52 @@
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QCheckBox" name="mRefreshLayerCheckBox">
<property name="text">
<string>Refresh layer at interval (seconds)</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="mRefreshLayerIntervalSpinBox">
<property name="toolTip">
<string>Higher values result in more simplification</string>
</property>
<property name="decimals">
<number>2</number>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>100000000000000000000.000000000000000</double>
</property>
<property name="singleStep">
<double>5.000000000000000</double>
</property>
<property name="value">
<double>10.000000000000000</double>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
Expand Down Expand Up @@ -2242,6 +2288,8 @@ p, li { white-space: pre-wrap; }
<tabstop>grpSRS</tabstop>
<tabstop>mCrsSelector</tabstop>
<tabstop>chkUseScaleDependentRendering</tabstop>
<tabstop>mRefreshLayerCheckBox</tabstop>
<tabstop>mRefreshLayerIntervalSpinBox</tabstop>
<tabstop>scrollArea</tabstop>
<tabstop>mBandRenderingGrpBx</tabstop>
<tabstop>mRenderTypeComboBox</tabstop>
Expand Down

2 comments on commit 38f87a6

@blazek
Copy link
Member

@blazek blazek commented on 38f87a6 Jun 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The QTimer can only be destroyed from its thread. QgsVectorLayer given to QgsVectorLayerExporterTask with ownership was deleted from QgsVectorLayerExporterTask::run() which is called in another thread. That was resulting in qWarning "Timers cannot be stopped from another thread" and QGIS crashing after a while with useless traceback.

I have fixed that moving delete mLayer to QgsVectorLayerExporterTask::finished() in 7b1932a.

There are maybe other places where a layer may be deleted from a thread?

@nyalldawson
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blazek Good catch! I had a look through and the rest of the tasks look safe, except for QgsMapRendererTask. That's fixed in 233a861

Please sign in to comment.