Skip to content

Commit 38f87a6

Browse files
committedFeb 12, 2017
[FEATURE] Allow layers to be automatically refreshed at a specified interval
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.
1 parent 9b9d49a commit 38f87a6

14 files changed

+417
-2
lines changed
 

‎python/core/qgsmaplayer.sip

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,11 @@ class QgsMapLayer : QObject
603603
*/
604604
bool hasScaleBasedVisibility() const;
605605

606+
bool hasAutoRefreshEnabled() const;
607+
int autoRefreshInterval() const;
608+
void setAutoRefreshInterval( int interval );
609+
void setAutoRefreshEnabled( bool enabled );
610+
606611
public slots:
607612

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

738+
void autoRefreshIntervalChanged( int interval );
739+
733740
protected:
734741
/** Set the extent */
735742
virtual void setExtent( const QgsRectangle &rect );

‎src/app/qgsrasterlayerproperties.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ QgsRasterLayerProperties::QgsRasterLayerProperties( QgsMapLayer* lyr, QgsMapCanv
139139
// enable or disable Build Pyramids button depending on selection in pyramid list
140140
connect( lbxPyramidResolutions, SIGNAL( itemSelectionChanged() ), this, SLOT( toggleBuildPyramidsButton() ) );
141141

142+
connect( mRefreshLayerCheckBox, &QCheckBox::toggled, mRefreshLayerIntervalSpinBox, &QDoubleSpinBox::setEnabled );
143+
142144
// set up the scale based layer visibility stuff....
143145
mScaleRangeWidget->setMapCanvas( mMapCanvas );
144146
chkUseScaleDependentRendering->setChecked( lyr->hasScaleBasedVisibility() );
@@ -713,6 +715,10 @@ void QgsRasterLayerProperties::sync()
713715
leNoDataValue->insert( QLatin1String( "" ) );
714716
}
715717

718+
mRefreshLayerCheckBox->setChecked( mRasterLayer->hasAutoRefreshEnabled() );
719+
mRefreshLayerIntervalSpinBox->setEnabled( mRasterLayer->hasAutoRefreshEnabled() );
720+
mRefreshLayerIntervalSpinBox->setValue( mRasterLayer->autoRefreshInterval() / 1000.0 );
721+
716722
populateTransparencyTable( mRasterLayer->renderer() );
717723

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

949+
mRasterLayer->setAutoRefreshInterval( mRefreshLayerIntervalSpinBox->value() * 1000.0 );
950+
mRasterLayer->setAutoRefreshEnabled( mRefreshLayerCheckBox->isChecked() );
951+
943952
//update the legend pixmap
944953
// pixmapLegend->setPixmap( mRasterLayer->legendAsPixmap() );
945954
// pixmapLegend->setScaledContents( true );

‎src/app/qgsvectorlayerproperties.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,9 @@ QgsVectorLayerProperties::QgsVectorLayerProperties(
322322
}
323323

324324
mLayersDependenciesTreeView->setModel( mLayersDependenciesTreeModel.get() );
325+
326+
connect( mRefreshLayerCheckBox, &QCheckBox::toggled, mRefreshLayerIntervalSpinBox, &QDoubleSpinBox::setEnabled );
327+
325328
} // QgsVectorLayerProperties ctor
326329

327330

@@ -446,6 +449,10 @@ void QgsVectorLayerProperties::syncToLayer()
446449

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

452+
mRefreshLayerCheckBox->setChecked( mLayer->hasAutoRefreshEnabled() );
453+
mRefreshLayerIntervalSpinBox->setEnabled( mLayer->hasAutoRefreshEnabled() );
454+
mRefreshLayerIntervalSpinBox->setValue( mLayer->autoRefreshInterval() / 1000.0 );
455+
449456
// load appropriate symbology page (V1 or V2)
450457
updateSymbologyPage();
451458

@@ -583,6 +590,9 @@ void QgsVectorLayerProperties::apply()
583590
if ( mLayer->renderer() )
584591
mLayer->renderer()->setForceRasterRender( mForceRasterCheckBox->isChecked() );
585592

593+
mLayer->setAutoRefreshInterval( mRefreshLayerIntervalSpinBox->value() * 1000.0 );
594+
mLayer->setAutoRefreshEnabled( mRefreshLayerCheckBox->isChecked() );
595+
586596
mOldJoins = mLayer->vectorJoins();
587597

588598
//save variables

‎src/core/qgsmaplayer.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ QgsMapLayer::QgsMapLayer( QgsMapLayer::LayerType type,
8383
mScaleBasedVisibility = false;
8484

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

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

424+
setAutoRefreshInterval( layerElement.attribute( QStringLiteral( "autoRefreshTime" ), 0 ).toInt() );
425+
setAutoRefreshEnabled( layerElement.attribute( QStringLiteral( "autoRefreshEnabled" ), QStringLiteral( "0" ) ).toInt() );
426+
423427
QDomNode extentNode = layerElement.namedItem( QStringLiteral( "extent" ) );
424428
if ( !extentNode.isNull() )
425429
{
@@ -537,6 +541,9 @@ bool QgsMapLayer::writeLayerXml( QDomElement& layerElement, QDomDocument& docume
537541
layerElement.appendChild( QgsXmlUtils::writeRectangle( mExtent, document ) );
538542
}
539543

544+
layerElement.setAttribute( QStringLiteral( "autoRefreshTime" ), QString::number( mRefreshTimer.interval() ) );
545+
layerElement.setAttribute( QStringLiteral( "autoRefreshEnabled" ), mRefreshTimer.isActive() ? 1 : 0 );
546+
540547
// ID
541548
QDomElement layerId = document.createElement( QStringLiteral( "id" ) );
542549
QDomText layerIdText = document.createTextNode( id() );
@@ -857,6 +864,40 @@ bool QgsMapLayer::hasScaleBasedVisibility() const
857864
return mScaleBasedVisibility;
858865
}
859866

867+
bool QgsMapLayer::hasAutoRefreshEnabled() const
868+
{
869+
return mRefreshTimer.isActive();
870+
}
871+
872+
int QgsMapLayer::autoRefreshInterval() const
873+
{
874+
return mRefreshTimer.interval();
875+
}
876+
877+
void QgsMapLayer::setAutoRefreshInterval( int interval )
878+
{
879+
if ( interval <= 0 )
880+
{
881+
mRefreshTimer.stop();
882+
mRefreshTimer.setInterval( 0 );
883+
}
884+
else
885+
{
886+
mRefreshTimer.setInterval( interval );
887+
}
888+
emit autoRefreshIntervalChanged( mRefreshTimer.isActive() ? mRefreshTimer.interval() : 0 );
889+
}
890+
891+
void QgsMapLayer::setAutoRefreshEnabled( bool enabled )
892+
{
893+
if ( !enabled )
894+
mRefreshTimer.stop();
895+
else if ( mRefreshTimer.interval() > 0 )
896+
mRefreshTimer.start();
897+
898+
emit autoRefreshIntervalChanged( mRefreshTimer.isActive() ? mRefreshTimer.interval() : 0 );
899+
}
900+
860901
void QgsMapLayer::setMinimumScale( double scale )
861902
{
862903
mMinScale = scale;

‎src/core/qgsmaplayer.h

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class CORE_EXPORT QgsMapLayer : public QObject
5353
Q_OBJECT
5454

5555
Q_PROPERTY( QString name READ name WRITE setName NOTIFY nameChanged )
56+
Q_PROPERTY( int autoRefreshInterval READ autoRefreshInterval WRITE setAutoRefreshInterval NOTIFY autoRefreshIntervalChanged )
5657

5758
public:
5859

@@ -646,6 +647,44 @@ class CORE_EXPORT QgsMapLayer : public QObject
646647
*/
647648
bool hasScaleBasedVisibility() const;
648649

650+
/**
651+
* Returns true if auto refresh is enabled for the layer.
652+
* @note added in QGIS 3.0
653+
* @see autoRefreshInterval()
654+
* @see setAutoRefreshEnabled()
655+
*/
656+
bool hasAutoRefreshEnabled() const;
657+
658+
/**
659+
* Returns the auto refresh interval (in milliseconds). Note that
660+
* auto refresh is only active when hasAutoRefreshEnabled() is true.
661+
* @note added in QGIS 3.0
662+
* @see autoRefreshEnabled()
663+
* @see setAutoRefreshInterval()
664+
*/
665+
int autoRefreshInterval() const;
666+
667+
/**
668+
* Sets the auto refresh interval (in milliseconds) for the layer. This
669+
* will cause the layer to be automatically redrawn on a matching interval.
670+
* Note that auto refresh must be enabled by calling setAutoRefreshEnabled().
671+
*
672+
* Note that auto refresh triggers deferred repaints of the layer. Any map
673+
* canvas must be refreshed separately in order to view the refreshed layer.
674+
* @note added in QGIS 3.0
675+
* @see autoRefreshInterval()
676+
* @see setAutoRefreshEnabled()
677+
*/
678+
void setAutoRefreshInterval( int interval );
679+
680+
/**
681+
* Sets whether auto refresh is enabled for the layer.
682+
* @note added in QGIS 3.0
683+
* @see hasAutoRefreshEnabled()
684+
* @see setAutoRefreshInterval()
685+
*/
686+
void setAutoRefreshEnabled( bool enabled );
687+
649688
public slots:
650689

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

828+
/**
829+
* Emitted when the auto refresh interval changes.
830+
* @see setAutoRefreshInterval()
831+
* @note added in QGIS 3.0
832+
*/
833+
void autoRefreshIntervalChanged( int interval );
834+
789835
protected:
790836
//! Set the extent
791837
virtual void setExtent( const QgsRectangle &rect );
@@ -920,6 +966,10 @@ class CORE_EXPORT QgsMapLayer : public QObject
920966

921967
//! Manager of multiple styles available for a layer (may be null)
922968
QgsMapLayerStyleManager* mStyleManager;
969+
970+
//! Timer for triggering automatic refreshes of the layer
971+
QTimer mRefreshTimer;
972+
923973
};
924974

925975
Q_DECLARE_METATYPE( QgsMapLayer* )

‎src/core/qgsvectorlayer.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ QgsVectorLayer::QgsVectorLayer( const QString& vectorLayerPath,
164164
setDataSource( vectorLayerPath, baseName, providerKey, loadDefaultStyleFlag );
165165
}
166166

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

170170
// Default simplify drawing settings

‎src/gui/qgsmapcanvas.cpp

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ QgsMapCanvas::QgsMapCanvas( QWidget * parent )
176176
QPixmap zoomPixmap = QPixmap(( const char ** )( zoom_in ) );
177177
mZoomCursor = QCursor( zoomPixmap, 7, 7 );
178178

179+
connect( &mAutoRefreshTimer, &QTimer::timeout, this, &QgsMapCanvas::autoRefreshTriggered );
180+
179181
setInteractive( false );
180182

181183
refresh();
@@ -297,6 +299,7 @@ void QgsMapCanvas::setLayers( const QList<QgsMapLayer*>& layers )
297299
{
298300
disconnect( layer, &QgsMapLayer::repaintRequested, this, &QgsMapCanvas::layerRepaintRequested );
299301
disconnect( layer, &QgsMapLayer::crsChanged, this, &QgsMapCanvas::layerCrsChange );
302+
disconnect( layer, &QgsMapLayer::autoRefreshIntervalChanged, this, &QgsMapCanvas::updateAutoRefreshTimer );
300303
if ( QgsVectorLayer* vlayer = qobject_cast<QgsVectorLayer *>( layer ) )
301304
{
302305
disconnect( vlayer, &QgsVectorLayer::selectionChanged, this, &QgsMapCanvas::selectionChangedSlot );
@@ -309,17 +312,18 @@ void QgsMapCanvas::setLayers( const QList<QgsMapLayer*>& layers )
309312
{
310313
connect( layer, &QgsMapLayer::repaintRequested, this, &QgsMapCanvas::layerRepaintRequested );
311314
connect( layer, &QgsMapLayer::crsChanged, this, &QgsMapCanvas::layerCrsChange );
315+
connect( layer, &QgsMapLayer::autoRefreshIntervalChanged, this, &QgsMapCanvas::updateAutoRefreshTimer );
312316
if ( QgsVectorLayer* vlayer = qobject_cast<QgsVectorLayer *>( layer ) )
313317
{
314318
connect( vlayer, &QgsVectorLayer::selectionChanged, this, &QgsMapCanvas::selectionChangedSlot );
315319
}
316320
}
317-
318321
updateDatumTransformEntries();
319322

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

326+
updateAutoRefreshTimer();
323327
refresh();
324328
}
325329

@@ -1674,7 +1678,40 @@ void QgsMapCanvas::layerRepaintRequested( bool deferred )
16741678
refresh();
16751679
}
16761680

1681+
void QgsMapCanvas::autoRefreshTriggered()
1682+
{
1683+
if ( mJob )
1684+
{
1685+
// canvas is currently being redrawn, so we skip this auto refresh
1686+
// otherwise we could get stuck in the situation where an auto refresh is triggered
1687+
// too often to allow the canvas to ever finish rendering
1688+
return;
1689+
}
16771690

1691+
refresh();
1692+
}
1693+
1694+
void QgsMapCanvas::updateAutoRefreshTimer()
1695+
{
1696+
// min auto refresh interval stores the smallest interval between layer auto refreshes. We automatically
1697+
// trigger a map refresh on this minimum interval
1698+
int minAutoRefreshInterval = -1;
1699+
Q_FOREACH ( QgsMapLayer* layer, mSettings.layers() )
1700+
{
1701+
if ( layer->hasAutoRefreshEnabled() && layer->autoRefreshInterval() > 0 )
1702+
minAutoRefreshInterval = minAutoRefreshInterval > 0 ? qMin( layer->autoRefreshInterval(), minAutoRefreshInterval ) : layer->autoRefreshInterval();
1703+
}
1704+
1705+
if ( minAutoRefreshInterval > 0 )
1706+
{
1707+
mAutoRefreshTimer.setInterval( minAutoRefreshInterval );
1708+
mAutoRefreshTimer.start();
1709+
}
1710+
else
1711+
{
1712+
mAutoRefreshTimer.stop();
1713+
}
1714+
}
16781715

16791716
QgsMapTool* QgsMapCanvas::mapTool()
16801717
{

‎src/gui/qgsmapcanvas.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,10 @@ class GUI_EXPORT QgsMapCanvas : public QGraphicsView
616616

617617
void layerRepaintRequested( bool deferred );
618618

619+
void autoRefreshTriggered();
620+
621+
void updateAutoRefreshTimer();
622+
619623
private:
620624
/// this class is non-copyable
621625

@@ -706,6 +710,8 @@ class GUI_EXPORT QgsMapCanvas : public QGraphicsView
706710

707711
QCursor mZoomCursor;
708712

713+
QTimer mAutoRefreshTimer;
714+
709715
//! Force a resize of the map canvas item
710716
//! @note added in 2.16
711717
void updateMapSize();

‎src/ui/qgsrasterlayerpropertiesbase.ui

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,52 @@
420420
</layout>
421421
</widget>
422422
</item>
423+
<item>
424+
<layout class="QHBoxLayout" name="horizontalLayout_3">
425+
<item>
426+
<widget class="QCheckBox" name="mRefreshLayerCheckBox">
427+
<property name="text">
428+
<string>Refresh layer at interval (seconds)</string>
429+
</property>
430+
</widget>
431+
</item>
432+
<item>
433+
<widget class="QDoubleSpinBox" name="mRefreshLayerIntervalSpinBox">
434+
<property name="toolTip">
435+
<string>Higher values result in more simplification</string>
436+
</property>
437+
<property name="decimals">
438+
<number>2</number>
439+
</property>
440+
<property name="minimum">
441+
<double>0.000000000000000</double>
442+
</property>
443+
<property name="maximum">
444+
<double>100000000000000000000.000000000000000</double>
445+
</property>
446+
<property name="singleStep">
447+
<double>5.000000000000000</double>
448+
</property>
449+
<property name="value">
450+
<double>10.000000000000000</double>
451+
</property>
452+
</widget>
453+
</item>
454+
<item>
455+
<spacer name="horizontalSpacer_6">
456+
<property name="orientation">
457+
<enum>Qt::Horizontal</enum>
458+
</property>
459+
<property name="sizeHint" stdset="0">
460+
<size>
461+
<width>40</width>
462+
<height>20</height>
463+
</size>
464+
</property>
465+
</spacer>
466+
</item>
467+
</layout>
468+
</item>
423469
<item>
424470
<spacer name="verticalSpacer_3">
425471
<property name="orientation">
@@ -2242,6 +2288,8 @@ p, li { white-space: pre-wrap; }
22422288
<tabstop>grpSRS</tabstop>
22432289
<tabstop>mCrsSelector</tabstop>
22442290
<tabstop>chkUseScaleDependentRendering</tabstop>
2291+
<tabstop>mRefreshLayerCheckBox</tabstop>
2292+
<tabstop>mRefreshLayerIntervalSpinBox</tabstop>
22452293
<tabstop>scrollArea</tabstop>
22462294
<tabstop>mBandRenderingGrpBx</tabstop>
22472295
<tabstop>mRenderTypeComboBox</tabstop>

‎src/ui/qgsvectorlayerpropertiesbase.ui

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,52 @@
906906
</property>
907907
</widget>
908908
</item>
909+
<item>
910+
<layout class="QHBoxLayout" name="horizontalLayout_3">
911+
<item>
912+
<widget class="QCheckBox" name="mRefreshLayerCheckBox">
913+
<property name="text">
914+
<string>Refresh layer at interval (seconds)</string>
915+
</property>
916+
</widget>
917+
</item>
918+
<item>
919+
<widget class="QDoubleSpinBox" name="mRefreshLayerIntervalSpinBox">
920+
<property name="toolTip">
921+
<string>Higher values result in more simplification</string>
922+
</property>
923+
<property name="decimals">
924+
<number>2</number>
925+
</property>
926+
<property name="minimum">
927+
<double>0.000000000000000</double>
928+
</property>
929+
<property name="maximum">
930+
<double>100000000000000000000.000000000000000</double>
931+
</property>
932+
<property name="singleStep">
933+
<double>5.000000000000000</double>
934+
</property>
935+
<property name="value">
936+
<double>10.000000000000000</double>
937+
</property>
938+
</widget>
939+
</item>
940+
<item>
941+
<spacer name="horizontalSpacer_4">
942+
<property name="orientation">
943+
<enum>Qt::Horizontal</enum>
944+
</property>
945+
<property name="sizeHint" stdset="0">
946+
<size>
947+
<width>40</width>
948+
<height>20</height>
949+
</size>
950+
</property>
951+
</spacer>
952+
</item>
953+
</layout>
954+
</item>
909955
<item>
910956
<spacer name="verticalSpacer_6">
911957
<property name="orientation">
@@ -1901,6 +1947,8 @@
19011947
<tabstop>mSimplifyDrawingAtProvider</tabstop>
19021948
<tabstop>mSimplifyMaximumScaleComboBox</tabstop>
19031949
<tabstop>mForceRasterCheckBox</tabstop>
1950+
<tabstop>mRefreshLayerCheckBox</tabstop>
1951+
<tabstop>mRefreshLayerIntervalSpinBox</tabstop>
19041952
<tabstop>mDisplayExpressionWidget</tabstop>
19051953
<tabstop>mMapTipExpressionFieldWidget</tabstop>
19061954
<tabstop>mInsertExpressionButton</tabstop>

‎tests/src/python/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ ADD_PYTHON_TEST(PyQgsJSONUtils test_qgsjsonutils.py)
6565
ADD_PYTHON_TEST(PyQgsLineSymbolLayers test_qgslinesymbollayers.py)
6666
ADD_PYTHON_TEST(PyQgsMapCanvas test_qgsmapcanvas.py)
6767
ADD_PYTHON_TEST(PyQgsMapCanvasAnnotationItem test_qgsmapcanvasannotationitem.py)
68+
ADD_PYTHON_TEST(PyQgsMapLayer test_qgsmaplayer.py)
6869
ADD_PYTHON_TEST(PyQgsMapLayerModel test_qgsmaplayermodel.py)
6970
ADD_PYTHON_TEST(PyQgsMapRenderer test_qgsmaprenderer.py)
7071
ADD_PYTHON_TEST(PyQgsMapRendererCache test_qgsmaprenderercache.py)

‎tests/src/python/test_qgsmapcanvas.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,70 @@ def testDeferredUpdate(self):
9090
# now we expect the canvas check to fail (since they'll be a new polygon rendered over it)
9191
self.assertFalse(self.canvasImageCheck('empty_canvas', 'empty_canvas', canvas))
9292

93+
def testRefreshOnTimer(self):
94+
""" test that map canvas refreshes with auto refreshing layers """
95+
canvas = QgsMapCanvas()
96+
canvas.setDestinationCrs(QgsCoordinateReferenceSystem(4326))
97+
canvas.setFrameStyle(0)
98+
canvas.resize(600, 400)
99+
self.assertEqual(canvas.width(), 600)
100+
self.assertEqual(canvas.height(), 400)
101+
102+
layer = QgsVectorLayer("Polygon?crs=epsg:4326&field=fldtxt:string",
103+
"layer", "memory")
104+
105+
canvas.setLayers([layer])
106+
canvas.setExtent(QgsRectangle(10, 30, 20, 35))
107+
canvas.show()
108+
109+
# need to wait until first redraw can occur (note that we first need to wait till drawing starts!)
110+
while not canvas.isDrawing():
111+
app.processEvents()
112+
while canvas.isDrawing():
113+
app.processEvents()
114+
115+
self.assertTrue(self.canvasImageCheck('empty_canvas', 'empty_canvas', canvas))
116+
117+
# add polygon to layer
118+
f = QgsFeature()
119+
f.setGeometry(QgsGeometry.fromRect(QgsRectangle(5, 25, 25, 45)))
120+
self.assertTrue(layer.dataProvider().addFeatures([f]))
121+
122+
# set auto refresh on layer
123+
layer.setAutoRefreshInterval(100)
124+
layer.setAutoRefreshEnabled(True)
125+
126+
timeout = time.time() + 1
127+
# expect canvas to auto refresh...
128+
while not canvas.isDrawing():
129+
app.processEvents()
130+
self.assertTrue(time.time() < timeout)
131+
while canvas.isDrawing():
132+
app.processEvents()
133+
self.assertTrue(time.time() < timeout)
134+
135+
# add a polygon to layer
136+
f = QgsFeature()
137+
f.setGeometry(QgsGeometry.fromRect(QgsRectangle(5, 25, 25, 45)))
138+
self.assertTrue(layer.dataProvider().addFeatures([f]))
139+
# wait for canvas auto refresh
140+
while not canvas.isDrawing():
141+
app.processEvents()
142+
self.assertTrue(time.time() < timeout)
143+
while canvas.isDrawing():
144+
app.processEvents()
145+
self.assertTrue(time.time() < timeout)
146+
147+
# now canvas should look different...
148+
self.assertFalse(self.canvasImageCheck('empty_canvas', 'empty_canvas', canvas))
149+
150+
# switch off auto refresh
151+
layer.setAutoRefreshEnabled(False)
152+
timeout = time.time() + 0.5
153+
while time.time() < timeout:
154+
# messy, but only way to check that canvas redraw doesn't occur
155+
self.assertFalse(canvas.isDrawing())
156+
93157
def canvasImageCheck(self, name, reference_image, canvas):
94158
self.report += "<h2>Render {}</h2>\n".format(name)
95159
temp_dir = QDir.tempPath() + '/'

‎tests/src/python/test_qgsmaplayer.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# -*- coding: utf-8 -*-
2+
"""QGIS Unit tests for QgsMapLayer
3+
4+
.. note:: This program is free software; you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation; either version 2 of the License, or
7+
(at your option) any later version.
8+
"""
9+
__author__ = 'Nyall Dawson'
10+
__date__ = '1/02/2017'
11+
__copyright__ = 'Copyright 2017, The QGIS Project'
12+
# This will get replaced with a git SHA1 when you do a git archive
13+
__revision__ = '$Format:%H$'
14+
15+
import qgis # NOQA
16+
17+
from qgis.core import (QgsMapRendererCache,
18+
QgsRectangle,
19+
QgsVectorLayer,
20+
QgsProject)
21+
from qgis.testing import start_app, unittest
22+
from qgis.PyQt.QtXml import (QDomDocument, QDomElement)
23+
24+
start_app()
25+
26+
27+
class TestQgsMapLayer(unittest.TestCase):
28+
29+
def copyLayerViaXmlReadWrite(self, source, dest):
30+
# write to xml
31+
doc = QDomDocument("testdoc")
32+
elem = doc.createElement("maplayer")
33+
self.assertTrue(source.writeLayerXml(elem, doc))
34+
self.assertTrue(dest.readLayerXml(elem), QgsProject.instance())
35+
36+
def testGettersSetters(self):
37+
# test auto refresh getters/setters
38+
layer = QgsVectorLayer("Point?field=fldtxt:string",
39+
"layer", "memory")
40+
self.assertFalse(layer.hasAutoRefreshEnabled())
41+
self.assertEqual(layer.autoRefreshInterval(), 0)
42+
layer.setAutoRefreshInterval(5)
43+
self.assertFalse(layer.hasAutoRefreshEnabled())
44+
self.assertEqual(layer.autoRefreshInterval(), 5)
45+
layer.setAutoRefreshEnabled(True)
46+
self.assertTrue(layer.hasAutoRefreshEnabled())
47+
self.assertEqual(layer.autoRefreshInterval(), 5)
48+
layer.setAutoRefreshInterval(0) # should disable auto refresh
49+
self.assertFalse(layer.hasAutoRefreshEnabled())
50+
self.assertEqual(layer.autoRefreshInterval(), 0)
51+
52+
def testSaveRestoreAutoRefresh(self):
53+
""" test saving/restoring auto refresh to xml """
54+
layer = QgsVectorLayer("Point?field=fldtxt:string",
55+
"layer", "memory")
56+
layer2 = QgsVectorLayer("Point?field=fldtxt:string",
57+
"layer", "memory")
58+
self.copyLayerViaXmlReadWrite(layer, layer2)
59+
self.assertFalse(layer2.hasAutoRefreshEnabled())
60+
self.assertEqual(layer2.autoRefreshInterval(), 0)
61+
62+
layer.setAutoRefreshInterval(56)
63+
self.copyLayerViaXmlReadWrite(layer, layer2)
64+
self.assertFalse(layer2.hasAutoRefreshEnabled())
65+
self.assertEqual(layer2.autoRefreshInterval(), 56)
66+
67+
layer.setAutoRefreshEnabled(True)
68+
self.copyLayerViaXmlReadWrite(layer, layer2)
69+
self.assertTrue(layer2.hasAutoRefreshEnabled())
70+
self.assertEqual(layer2.autoRefreshInterval(), 56)
71+
72+
73+
if __name__ == '__main__':
74+
unittest.main()

‎tests/src/python/test_qgsmaprenderercache.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
QgsVectorLayer,
2020
QgsProject)
2121
from qgis.testing import start_app, unittest
22+
from qgis.PyQt.QtCore import QCoreApplication
2223
from qgis.PyQt.QtGui import QImage
24+
from time import sleep
2325
start_app()
2426

2527

@@ -245,6 +247,24 @@ def testLayerRemoval(self):
245247
self.assertFalse(cache.hasCacheImage('depends3'))
246248
self.assertTrue(cache.hasCacheImage('no depends'))
247249

250+
def testClearOnLayerAutoRefresh(self):
251+
""" test that cache is cleared when layer auto refresh is triggered """
252+
cache = QgsMapRendererCache()
253+
layer1 = QgsVectorLayer("Point?field=fldtxt:string",
254+
"layer1", "memory")
255+
im = QImage(200, 200, QImage.Format_RGB32)
256+
cache.setCacheImage('l1', im, [layer1])
257+
self.assertTrue(cache.hasCacheImage('l1'))
258+
259+
layer1.setAutoRefreshInterval(100)
260+
layer1.setAutoRefreshEnabled(True)
261+
self.assertTrue(cache.hasCacheImage('l1'))
262+
263+
# wait a second...
264+
sleep(1)
265+
QCoreApplication.processEvents()
266+
# cache should be cleared
267+
self.assertFalse(cache.hasCacheImage('l1'))
248268

249269
if __name__ == '__main__':
250270
unittest.main()

2 commit comments

Comments
 (2)

blazek commented on Jun 29, 2017

@blazek
Member

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 commented on Jun 30, 2017

@nyalldawson
CollaboratorAuthor

@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.