Skip to content
Permalink
Browse files

[FEATURE] Allow layers to be automatically refreshed at a specified i…

…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 38f87a624e43c440a67fcfd79e63cefaafb6224c
@@ -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 */
@@ -730,6 +735,8 @@ class QgsMapLayer : QObject
*/
void willBeDeleted();

void autoRefreshIntervalChanged( int interval );

protected:
/** Set the extent */
virtual void setExtent( const QgsRectangle &rect );
@@ -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() );
@@ -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" );
@@ -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 );
@@ -322,6 +322,9 @@ QgsVectorLayerProperties::QgsVectorLayerProperties(
}

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

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

} // QgsVectorLayerProperties ctor


@@ -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();

@@ -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
@@ -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()
@@ -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() )
{
@@ -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() );
@@ -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;
@@ -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:

@@ -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
@@ -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 );
@@ -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* )
@@ -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
@@ -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();
@@ -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 );
@@ -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();
}

@@ -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()
{
@@ -616,6 +616,10 @@ class GUI_EXPORT QgsMapCanvas : public QGraphicsView

void layerRepaintRequested( bool deferred );

void autoRefreshTriggered();

void updateAutoRefreshTimer();

private:
/// this class is non-copyable

@@ -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();
@@ -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">
@@ -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>

2 comments on commit 38f87a6

@blazek

This comment has been minimized.

Copy link
Member

@blazek blazek replied Jun 29, 2017

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

This comment has been minimized.

Copy link
Collaborator Author

@nyalldawson nyalldawson replied Jun 30, 2017

@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.
You can’t perform that action at this time.