Skip to content

Commit edb30a2

Browse files
authoredFeb 14, 2017
Merge pull request #4134 from nyalldawson/save_task
[FEATURE] Background saving of vector layers
2 parents bd46b78 + 35bb5c6 commit edb30a2

18 files changed

+497
-91
lines changed
 

‎python/core/core.sip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@
149149
%Include qgsunittypes.sip
150150
%Include qgsvectordataprovider.sip
151151
%Include qgsvectorfilewriter.sip
152+
%Include qgsvectorfilewritertask.sip
152153
%Include qgsvectorlayer.sip
153154
%Include qgsvectorlayercache.sip
154155
%Include qgsvectorlayereditbuffer.sip

‎python/core/qgsfeedback.sip

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,15 @@ class QgsFeedback : QObject
3636
//! Tells whether the operation has been canceled already
3737
bool isCanceled() const;
3838

39+
void setProgress( double progress );
40+
41+
double progress() const;
42+
3943
signals:
4044
//! Internal routines can connect to this signal if they use event loop
4145
void canceled();
4246

47+
void progressChanged( double progress );
48+
49+
4350
};

‎python/core/qgstaskmanager.sip

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,7 @@ class QgsTask : QObject
7878
*/
7979
double progress() const;
8080

81-
/**
82-
* Notifies the task that it should terminate. Calling this is not guaranteed
83-
* to immediately end the task, rather it sets the isCanceled() flag which
84-
* task subclasses can check and terminate their operations at an appropriate
85-
* time. Any subtasks owned by this task will also be canceled.
86-
* @see isCanceled()
87-
*/
88-
void cancel();
81+
virtual void cancel();
8982

9083
/**
9184
* Places the task on hold. If the task in not queued
@@ -133,19 +126,9 @@ class QgsTask : QObject
133126
void addSubTask( QgsTask* subTask /Transfer/, const QgsTaskList& dependencies = QgsTaskList(),
134127
SubTaskDependency subTaskDependency = SubTaskIndependent );
135128

136-
/**
137-
* Sets a list of layer IDs on which the task depends. The task will automatically
138-
* be canceled if any of these layers are about to be removed.
139-
* @see dependentLayerIds()
140-
*/
141-
void setDependentLayers( const QStringList& dependentLayerIds );
129+
void setDependentLayers( const QList<QgsMapLayer*>& dependentLayers );
142130

143-
/**
144-
* Returns the list of layer IDs on which the task depends. The task will automatically
145-
* be canceled if any of these layers are about to be removed.
146-
* @see setDependentLayers()
147-
*/
148-
QStringList dependentLayerIds() const;
131+
QList< QgsMapLayer* > dependentLayers() const;
149132

150133
signals:
151134

@@ -324,19 +307,9 @@ class QgsTaskManager : QObject
324307
//! @note not available in Python bindings
325308
//QSet< long > dependencies( long taskId ) const;
326309

327-
/** Returns a list of layers on which as task is dependent. The task will automatically
328-
* be canceled if any of these layers are above to be removed.
329-
* @param taskId task ID
330-
* @returns list of layer IDs
331-
* @see tasksDependentOnLayer()
332-
*/
333-
QStringList dependentLayers( long taskId ) const;
310+
QList< QgsMapLayer* > dependentLayers( long taskId ) const;
334311

335-
/**
336-
* Returns a list of tasks which depend on a layer.
337-
* @see dependentLayers()
338-
*/
339-
QList< QgsTask* > tasksDependentOnLayer( const QString& layerId ) const;
312+
QList< QgsTask* > tasksDependentOnLayer( QgsMapLayer* layer ) const;
340313

341314
/** Returns a list of the active (queued or running) tasks.
342315
* @see countActiveTasks()

‎python/core/qgsvectorfilewriter.sip

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class QgsVectorFileWriter
9797
ErrProjection,
9898
ErrFeatureWriteFailed,
9999
ErrInvalidLayer,
100+
Canceled,
100101
};
101102

102103
enum SymbologyExport
@@ -323,6 +324,8 @@ class QgsVectorFileWriter
323324

324325
/** Field value converter */
325326
QgsVectorFileWriter::FieldValueConverter* fieldValueConverter;
327+
328+
QgsFeedback* feedback;
326329
};
327330

328331
/** Writes a layer out to a vector file.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
class QgsVectorFileWriterTask : QgsTask
2+
{
3+
%TypeHeaderCode
4+
#include <qgsvectorfilewritertask.h>
5+
%End
6+
7+
public:
8+
9+
QgsVectorFileWriterTask( QgsVectorLayer* layer,
10+
const QString& fileName,
11+
const QgsVectorFileWriter::SaveVectorOptions& options );
12+
13+
virtual void cancel();
14+
15+
signals:
16+
17+
void writeComplete( const QString& newFilename );
18+
void errorOccurred( int error, const QString& errorMessage );
19+
20+
protected:
21+
22+
virtual bool run();
23+
virtual void finished( bool result );
24+
};
25+

‎src/app/qgisapp.cpp

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@
250250
#include "qgsvectorlayerjoininfo.h"
251251
#include "qgsvectorlayerutils.h"
252252
#include "qgshelp.h"
253+
#include "qgsvectorfilewritertask.h"
253254

254255
#include "qgssublayersdialog.h"
255256
#include "ogr/qgsopenvectorlayerdialog.h"
@@ -6477,12 +6478,6 @@ void QgisApp::saveAsVectorFileGeneral( QgsVectorLayer* vlayer, bool symbologyOpt
64776478
}
64786479
}
64796480

6480-
// ok if the file existed it should be deleted now so we can continue...
6481-
QApplication::setOverrideCursor( Qt::WaitCursor );
6482-
6483-
QgsVectorFileWriter::WriterError error;
6484-
QString errorMessage;
6485-
QString newFilename;
64866481
QgsRectangle filterExtent = dialog->filterExtent();
64876482
QgisAppFieldValueConverter converter( vlayer, dialog->attributesAsDisplayedValues() );
64886483
QgisAppFieldValueConverter* converterPtr = nullptr;
@@ -6510,32 +6505,41 @@ void QgisApp::saveAsVectorFileGeneral( QgsVectorLayer* vlayer, bool symbologyOpt
65106505
options.attributes = dialog->selectedAttributes();
65116506
options.fieldValueConverter = converterPtr;
65126507

6513-
error = QgsVectorFileWriter::writeAsVectorFormat(
6514-
vlayer, vectorFilename, options, &newFilename, &errorMessage );
6508+
bool addToCanvas = dialog->addToCanvas();
6509+
QString layerName = dialog->layername();
6510+
QgsVectorFileWriterTask* writerTask = new QgsVectorFileWriterTask( vlayer, vectorFilename, options );
65156511

6516-
QApplication::restoreOverrideCursor();
6517-
6518-
if ( error == QgsVectorFileWriter::NoError )
6512+
// when writer is successful:
6513+
connect( writerTask, &QgsVectorFileWriterTask::writeComplete, this, [this, addToCanvas, layerName, encoding, vectorFilename, vlayer]( const QString& newFilename )
65196514
{
6520-
if ( dialog->addToCanvas() )
6515+
if ( addToCanvas )
65216516
{
65226517
QString uri( newFilename );
6523-
if ( !dialog->layername().isEmpty() )
6524-
uri += "|layername=" + dialog->layername();
6525-
addVectorLayers( QStringList( uri ), encoding, QStringLiteral( "file" ) );
6518+
if ( !layerName.isEmpty() )
6519+
uri += "|layername=" + layerName;
6520+
this->addVectorLayers( QStringList( uri ), encoding, QStringLiteral( "file" ) );
65266521
}
6527-
emit layerSavedAs( vlayer, vectorFilename );
6528-
messageBar()->pushMessage( tr( "Saving done" ),
6529-
tr( "Export to vector file has been completed" ),
6530-
QgsMessageBar::INFO, messageTimeout() );
6522+
this->emit layerSavedAs( vlayer, vectorFilename );
6523+
this->messageBar()->pushMessage( tr( "Saving done" ),
6524+
tr( "Export to vector file has been completed" ),
6525+
QgsMessageBar::INFO, messageTimeout() );
65316526
}
6532-
else
6527+
);
6528+
6529+
// when an error occurs:
6530+
connect( writerTask, &QgsVectorFileWriterTask::errorOccurred, this, [=]( int error, const QString & errorMessage )
65336531
{
6534-
QgsMessageViewer *m = new QgsMessageViewer( nullptr );
6535-
m->setWindowTitle( tr( "Save error" ) );
6536-
m->setMessageAsPlainText( tr( "Export to vector file failed.\nError: %1" ).arg( errorMessage ) );
6537-
m->exec();
6532+
if ( error != QgsVectorFileWriter::Canceled )
6533+
{
6534+
QgsMessageViewer *m = new QgsMessageViewer( nullptr );
6535+
m->setWindowTitle( tr( "Save error" ) );
6536+
m->setMessageAsPlainText( tr( "Export to vector file failed.\nError: %1" ).arg( errorMessage ) );
6537+
m->exec();
6538+
}
65386539
}
6540+
);
6541+
6542+
QgsApplication::taskManager()->addTask( writerTask );
65396543
}
65406544

65416545
delete dialog;
@@ -8440,7 +8444,7 @@ void QgisApp::removeLayer()
84408444
QStringList activeTaskDescriptions;
84418445
Q_FOREACH ( QgsMapLayer * layer, mLayerTreeView->selectedLayers() )
84428446
{
8443-
QList< QgsTask* > tasks = QgsApplication::taskManager()->tasksDependentOnLayer( layer->id() );
8447+
QList< QgsTask* > tasks = QgsApplication::taskManager()->tasksDependentOnLayer( layer );
84448448
if ( !tasks.isEmpty() )
84458449
{
84468450
Q_FOREACH ( QgsTask* task, tasks )
@@ -9645,7 +9649,7 @@ bool QgisApp::checkTasksDependOnProject()
96459649

96469650
for ( ; layerIt != layers.constEnd(); ++layerIt )
96479651
{
9648-
QList< QgsTask* > tasks = QgsApplication::taskManager()->tasksDependentOnLayer( layerIt.key() );
9652+
QList< QgsTask* > tasks = QgsApplication::taskManager()->tasksDependentOnLayer( layerIt.value() );
96499653
if ( !tasks.isEmpty() )
96509654
{
96519655
Q_FOREACH ( QgsTask* task, tasks )

‎src/core/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ SET(QGIS_CORE_SRCS
237237
qgsunittypes.cpp
238238
qgsvectordataprovider.cpp
239239
qgsvectorfilewriter.cpp
240+
qgsvectorfilewritertask.cpp
240241
qgsvectorlayer.cpp
241242
qgsvectorlayercache.cpp
242243
qgsvectorlayerdiagramprovider.cpp
@@ -528,6 +529,7 @@ SET(QGIS_CORE_MOC_HDRS
528529
qgstransactiongroup.h
529530
qgsunittypes.h
530531
qgsvectordataprovider.h
532+
qgsvectorfilewritertask.h
531533
qgsvectorlayercache.h
532534
qgsvectorlayereditbuffer.h
533535
qgsvectorlayereditpassthrough.h

‎src/core/qgsfeedback.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,44 @@ class CORE_EXPORT QgsFeedback : public QObject
6161
//! Tells whether the operation has been canceled already
6262
bool isCanceled() const { return mCanceled; }
6363

64+
/**
65+
* Sets the current progress for the feedback object. The \a progress
66+
* argument is in percentage and valid values range from 0-100.
67+
* @see progress()
68+
* @see progressChanged()
69+
* @note added in QGIS 3.0
70+
*/
71+
void setProgress( double progress ) { mProgress = progress; emit progressChanged( mProgress ); }
72+
73+
/**
74+
* Returns the current progress reported by the feedback object. Depending on how the
75+
* feedback object is used progress reporting may not be supported. The returned value
76+
* is in percentage and ranges from 0-100.
77+
* @see setProgress()
78+
* @see progressChanged()
79+
* @note added in QGIS 3.0
80+
*/
81+
double progress() const { return mProgress; }
82+
6483
signals:
6584
//! Internal routines can connect to this signal if they use event loop
6685
void canceled();
6786

87+
/**
88+
* Emitted when the feedback object reports a progress change. Depending on how the
89+
* feedback object is used progress reporting may not be supported. The \a progress
90+
* argument is in percentage and ranges from 0-100.
91+
* @note added in QGIS 3.0
92+
* @see setProgress()
93+
* @see progress()
94+
*/
95+
void progressChanged( double progress );
96+
6897
private:
6998
//! Whether the operation has been canceled already. False by default.
7099
bool mCanceled;
100+
101+
double mProgress = 0.0;
71102
};
72103

73104
#endif // QGSFEEDBACK_H

‎src/core/qgstaskmanager.cpp

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#include "qgstaskmanager.h"
1919
#include "qgsproject.h"
20+
#include "qgsmaplayerlistutils.h"
2021
#include <QtConcurrentRun>
2122

2223

@@ -136,9 +137,14 @@ void QgsTask::addSubTask( QgsTask* subTask, const QgsTaskList& dependencies,
136137
connect( subTask, &QgsTask::statusChanged, this, &QgsTask::subTaskStatusChanged );
137138
}
138139

139-
void QgsTask::setDependentLayers( const QStringList& dependentLayerIds )
140+
QList<QgsMapLayer*> QgsTask::dependentLayers() const
140141
{
141-
mDependentLayerIds = dependentLayerIds;
142+
return _qgis_listQPointerToRaw( mDependentLayers );
143+
}
144+
145+
void QgsTask::setDependentLayers( const QList< QgsMapLayer* >& dependentLayers )
146+
{
147+
mDependentLayers = _qgis_listRawToQPointer( dependentLayers );
142148
}
143149

144150
void QgsTask::subTaskStatusChanged( int status )
@@ -325,8 +331,8 @@ QgsTaskManager::QgsTaskManager( QObject* parent )
325331
, mTaskMutex( new QMutex( QMutex::Recursive ) )
326332
, mNextTaskId( 0 )
327333
{
328-
connect( QgsProject::instance(), SIGNAL( layersWillBeRemoved( QStringList ) ),
329-
this, SLOT( layersWillBeRemoved( QStringList ) ) );
334+
connect( QgsProject::instance(), static_cast < void ( QgsProject::* )( const QList< QgsMapLayer* >& ) > ( &QgsProject::layersWillBeRemoved ),
335+
this, &QgsTaskManager::layersWillBeRemoved );
330336
}
331337

332338
QgsTaskManager::~QgsTaskManager()
@@ -376,8 +382,8 @@ long QgsTaskManager::addTaskPrivate( QgsTask* task, QgsTaskList dependencies, bo
376382
{
377383
mParentTasks << task;
378384
}
379-
if ( !task->dependentLayerIds().isEmpty() )
380-
mLayerDependencies.insert( taskId, task->dependentLayerIds() );
385+
if ( !task->dependentLayers().isEmpty() )
386+
mLayerDependencies.insert( taskId, _qgis_listRawToQPointer( task->dependentLayers() ) );
381387
mTaskMutex->unlock();
382388

383389
connect( task, &QgsTask::statusChanged, this, &QgsTaskManager::taskStatusChanged );
@@ -546,20 +552,20 @@ bool QgsTaskManager::hasCircularDependencies( long taskId ) const
546552
return !resolveDependencies( taskId, taskId, d );
547553
}
548554

549-
QStringList QgsTaskManager::dependentLayers( long taskId ) const
555+
QList<QgsMapLayer*> QgsTaskManager::dependentLayers( long taskId ) const
550556
{
551557
QMutexLocker ml( mTaskMutex );
552-
return mLayerDependencies.value( taskId, QStringList() );
558+
return _qgis_listQPointerToRaw( mLayerDependencies.value( taskId, QgsWeakMapLayerPointerList() ) );
553559
}
554560

555-
QList<QgsTask*> QgsTaskManager::tasksDependentOnLayer( const QString& layerId ) const
561+
QList<QgsTask*> QgsTaskManager::tasksDependentOnLayer( QgsMapLayer* layer ) const
556562
{
557563
QMutexLocker ml( mTaskMutex );
558564
QList< QgsTask* > tasks;
559-
QMap< long, QStringList >::const_iterator layerIt = mLayerDependencies.constBegin();
565+
QMap< long, QgsWeakMapLayerPointerList >::const_iterator layerIt = mLayerDependencies.constBegin();
560566
for ( ; layerIt != mLayerDependencies.constEnd(); ++layerIt )
561567
{
562-
if ( layerIt.value().contains( layerId ) )
568+
if ( _qgis_listQPointerToRaw( layerIt.value() ).contains( layer ) )
563569
{
564570
QgsTask* layerTask = task( layerIt.key() );
565571
if ( layerTask )
@@ -648,21 +654,21 @@ void QgsTaskManager::taskStatusChanged( int status )
648654

649655
}
650656

651-
void QgsTaskManager::layersWillBeRemoved( const QStringList& layerIds )
657+
void QgsTaskManager::layersWillBeRemoved( const QList< QgsMapLayer* >& layers )
652658
{
653659
mTaskMutex->lock();
654660
// scan through layers to be removed
655-
QMap< long, QStringList > layerDependencies = mLayerDependencies;
661+
QMap< long, QgsWeakMapLayerPointerList > layerDependencies = mLayerDependencies;
656662
layerDependencies.detach();
657663
mTaskMutex->unlock();
658664

659-
Q_FOREACH ( const QString& layerId, layerIds )
665+
Q_FOREACH ( QgsMapLayer* layer, layers )
660666
{
661667
// scan through tasks with layer dependencies
662-
for ( QMap< long, QStringList >::const_iterator it = layerDependencies.constBegin();
668+
for ( QMap< long, QgsWeakMapLayerPointerList >::const_iterator it = layerDependencies.constBegin();
663669
it != layerDependencies.constEnd(); ++it )
664670
{
665-
if ( !it.value().contains( layerId ) )
671+
if ( !( _qgis_listQPointerToRaw( it.value() ).contains( layer ) ) )
666672
{
667673
//task not dependent on this layer
668674
continue;

‎src/core/qgstaskmanager.h

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include <QReadWriteLock>
2525

2626
#include "qgis_core.h"
27+
#include "qgsmaplayer.h"
2728

2829
class QgsTask;
2930
class QgsTaskRunnableWrapper;
@@ -116,9 +117,11 @@ class CORE_EXPORT QgsTask : public QObject
116117
* to immediately end the task, rather it sets the isCanceled() flag which
117118
* task subclasses can check and terminate their operations at an appropriate
118119
* time. Any subtasks owned by this task will also be canceled.
120+
* Derived classes must ensure that the base class implementation is called
121+
* from any overridden version.
119122
* @see isCanceled()
120123
*/
121-
void cancel();
124+
virtual void cancel();
122125

123126
/**
124127
* Places the task on hold. If the task in not queued
@@ -168,18 +171,18 @@ class CORE_EXPORT QgsTask : public QObject
168171
SubTaskDependency subTaskDependency = SubTaskIndependent );
169172

170173
/**
171-
* Sets a list of layer IDs on which the task depends. The task will automatically
174+
* Sets a list of layers on which the task depends. The task will automatically
172175
* be canceled if any of these layers are about to be removed.
173176
* @see dependentLayerIds()
174177
*/
175-
void setDependentLayers( const QStringList& dependentLayerIds );
178+
void setDependentLayers( const QList<QgsMapLayer*>& dependentLayers );
176179

177180
/**
178-
* Returns the list of layer IDs on which the task depends. The task will automatically
181+
* Returns the list of layers on which the task depends. The task will automatically
179182
* be canceled if any of these layers are about to be removed.
180183
* @see setDependentLayers()
181184
*/
182-
QStringList dependentLayerIds() const { return mDependentLayerIds; }
185+
QList< QgsMapLayer* > dependentLayers() const;
183186

184187
signals:
185188

@@ -294,7 +297,7 @@ class CORE_EXPORT QgsTask : public QObject
294297
};
295298
QList< SubTask > mSubTasks;
296299

297-
QStringList mDependentLayerIds;
300+
QgsWeakMapLayerPointerList mDependentLayers;
298301

299302

300303
/**
@@ -428,16 +431,16 @@ class CORE_EXPORT QgsTaskManager : public QObject
428431
/** Returns a list of layers on which as task is dependent. The task will automatically
429432
* be canceled if any of these layers are above to be removed.
430433
* @param taskId task ID
431-
* @returns list of layer IDs
434+
* @returns list of layers
432435
* @see tasksDependentOnLayer()
433436
*/
434-
QStringList dependentLayers( long taskId ) const;
437+
QList< QgsMapLayer* > dependentLayers( long taskId ) const;
435438

436439
/**
437440
* Returns a list of tasks which depend on a layer.
438441
* @see dependentLayers()
439442
*/
440-
QList< QgsTask* > tasksDependentOnLayer( const QString& layerId ) const;
443+
QList< QgsTask* > tasksDependentOnLayer( QgsMapLayer* layer ) const;
441444

442445
/** Returns a list of the active (queued or running) tasks.
443446
* @see countActiveTasks()
@@ -487,7 +490,7 @@ class CORE_EXPORT QgsTaskManager : public QObject
487490

488491
void taskProgressChanged( double progress );
489492
void taskStatusChanged( int status );
490-
void layersWillBeRemoved( const QStringList& layerIds );
493+
void layersWillBeRemoved( const QList<QgsMapLayer*>& layers );
491494

492495
private:
493496

@@ -504,7 +507,7 @@ class CORE_EXPORT QgsTaskManager : public QObject
504507

505508
QMap< long, TaskInfo > mTasks;
506509
QMap< long, QgsTaskList > mTaskDependencies;
507-
QMap< long, QStringList > mLayerDependencies;
510+
QMap< long, QgsWeakMapLayerPointerList > mLayerDependencies;
508511

509512
//! Tracks the next unique task ID
510513
long mNextTaskId;

‎src/core/qgsvectorfilewriter.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2424,8 +2424,29 @@ QgsVectorFileWriter::writeAsVectorFormat( QgsVectorLayer* layer,
24242424
writer->mFields = layer->fields();
24252425

24262426
// write all features
2427+
long saved = 0;
2428+
long total = options.onlySelectedFeatures ? layer->selectedFeatureCount() : layer->featureCount();
2429+
int lastProgressReport = 0;
24272430
while ( fit.nextFeature( fet ) )
24282431
{
2432+
if ( options.feedback && options.feedback->isCanceled() )
2433+
{
2434+
delete writer;
2435+
return Canceled;
2436+
}
2437+
2438+
saved++;
2439+
if ( options.feedback )
2440+
{
2441+
//avoid spamming progress reports
2442+
int newProgress = ( 100.0 * saved ) / total;
2443+
if ( newProgress < 100 && newProgress != lastProgressReport )
2444+
{
2445+
lastProgressReport = newProgress;
2446+
options.feedback->setProgress( lastProgressReport );
2447+
}
2448+
}
2449+
24292450
if ( shallTransform )
24302451
{
24312452
try
@@ -3085,3 +3106,4 @@ bool QgsVectorFileWriter::areThereNewFieldsToCreate( const QString& datasetName,
30853106
OGR_DS_Destroy( hDS );
30863107
return ret;
30873108
}
3109+

‎src/core/qgsvectorfilewriter.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121

2222
#include "qgis_core.h"
2323
#include "qgsfields.h"
24+
#include "qgsfeedback.h"
2425
#include "qgssymbol.h"
26+
#include "qgstaskmanager.h"
27+
#include "qgsvectorlayer.h"
2528
#include <ogr_api.h>
2629

2730
#include <QPair>
@@ -164,6 +167,7 @@ class CORE_EXPORT QgsVectorFileWriter
164167
ErrProjection,
165168
ErrFeatureWriteFailed,
166169
ErrInvalidLayer,
170+
Canceled, //!< Writing was interrupted by manual cancelation
167171
};
168172

169173
enum SymbologyExport
@@ -391,6 +395,9 @@ class CORE_EXPORT QgsVectorFileWriter
391395

392396
//! Field value converter
393397
FieldValueConverter* fieldValueConverter;
398+
399+
//! Optional feedback object allowing cancelation of layer save
400+
QgsFeedback* feedback = nullptr;
394401
};
395402

396403
/** Writes a layer out to a vector file.
@@ -616,4 +623,5 @@ class CORE_EXPORT QgsVectorFileWriter
616623

617624
Q_DECLARE_OPERATORS_FOR_FLAGS( QgsVectorFileWriter::EditionCapabilities )
618625

626+
619627
#endif

‎src/core/qgsvectorfilewritertask.cpp

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/***************************************************************************
2+
qgsvectorfilewritertask.cpp
3+
---------------------------
4+
begin : Feb 2017
5+
copyright : (C) 2017 by Nyall Dawson
6+
email : nyall dot dawson at gmail dot com
7+
***************************************************************************/
8+
9+
/***************************************************************************
10+
* *
11+
* This program is free software; you can redistribute it and/or modify *
12+
* it under the terms of the GNU General Public License as published by *
13+
* the Free Software Foundation; either version 2 of the License, or *
14+
* (at your option) any later version. *
15+
* *
16+
***************************************************************************/
17+
18+
#include "qgsvectorfilewritertask.h"
19+
20+
21+
QgsVectorFileWriterTask::QgsVectorFileWriterTask( QgsVectorLayer* layer, const QString& fileName, const QgsVectorFileWriter::SaveVectorOptions& options )
22+
: QgsTask( tr( "Saving %1 " ).arg( fileName ), QgsTask::CanCancel )
23+
, mLayer( layer )
24+
, mDestFileName( fileName )
25+
, mOptions( options )
26+
{
27+
if ( !mOptions.feedback )
28+
{
29+
mOwnedFeedback.reset( new QgsFeedback() );
30+
mOptions.feedback = mOwnedFeedback.get();
31+
}
32+
if ( mLayer )
33+
setDependentLayers( QList< QgsMapLayer* >() << mLayer );
34+
}
35+
36+
void QgsVectorFileWriterTask::cancel()
37+
{
38+
mOptions.feedback->cancel();
39+
QgsTask::cancel();
40+
}
41+
42+
bool QgsVectorFileWriterTask::run()
43+
{
44+
if ( !mLayer )
45+
return false;
46+
47+
connect( mOptions.feedback, &QgsFeedback::progressChanged, this, &QgsVectorFileWriterTask::setProgress );
48+
49+
mError = QgsVectorFileWriter::writeAsVectorFormat(
50+
mLayer, mDestFileName, mOptions, &mNewFilename, &mErrorMessage );
51+
return mError == QgsVectorFileWriter::NoError;
52+
}
53+
54+
void QgsVectorFileWriterTask::finished( bool result )
55+
{
56+
if ( result )
57+
emit writeComplete( mNewFilename );
58+
else
59+
emit errorOccurred( mError, mErrorMessage );
60+
}

‎src/core/qgsvectorfilewritertask.h

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/***************************************************************************
2+
qgsvectorfilewritertask.h
3+
-------------------------
4+
begin : Feb 2017
5+
copyright : (C) 2017 by Nyall Dawson
6+
email : nyall dot dawson at gmail dot com
7+
***************************************************************************/
8+
9+
/***************************************************************************
10+
* *
11+
* This program is free software; you can redistribute it and/or modify *
12+
* it under the terms of the GNU General Public License as published by *
13+
* the Free Software Foundation; either version 2 of the License, or *
14+
* (at your option) any later version. *
15+
* *
16+
***************************************************************************/
17+
18+
#ifndef QGSVECTORFILEWRITERTASK_H
19+
#define QGSVECTORFILEWRITERTASK_H
20+
21+
#include "qgis_core.h"
22+
#include "qgsvectorfilewriter.h"
23+
#include "qgstaskmanager.h"
24+
#include "qgsvectorlayer.h"
25+
26+
/**
27+
* \class QgsVectorFileWriterTask
28+
* \ingroup core
29+
* QgsTask task which performs a QgsVectorFileWriter layer saving operation as a background
30+
* task. This can be used to save a vector layer out to a file without blocking the
31+
* QGIS interface.
32+
* \note Added in QGIS 3.0
33+
*/
34+
class CORE_EXPORT QgsVectorFileWriterTask : public QgsTask
35+
{
36+
Q_OBJECT
37+
38+
public:
39+
40+
/**
41+
* Constructor for QgsVectorFileWriterTask. Takes a source \a layer, destination \a fileName
42+
* and save \a options.
43+
*/
44+
QgsVectorFileWriterTask( QgsVectorLayer* layer,
45+
const QString& fileName,
46+
const QgsVectorFileWriter::SaveVectorOptions& options );
47+
48+
virtual void cancel() override;
49+
50+
signals:
51+
52+
/**
53+
* Emitted when writing the layer is successfully completed. The \a newFilename
54+
* parameter indicates the file path for the written file.
55+
*/
56+
void writeComplete( const QString& newFilename );
57+
58+
/**
59+
* Emitted when an error occurs which prevented the file being written (or if
60+
* the task is canceled). The writing \a error and \a errorMessage will be reported.
61+
*/
62+
void errorOccurred( int error, const QString& errorMessage );
63+
64+
protected:
65+
66+
virtual bool run() override;
67+
virtual void finished( bool result ) override;
68+
69+
private:
70+
71+
QPointer< QgsVectorLayer > mLayer = nullptr;
72+
73+
QString mDestFileName;
74+
75+
std::unique_ptr< QgsFeedback > mOwnedFeedback;
76+
77+
QgsVectorFileWriter::WriterError mError = QgsVectorFileWriter::NoError;
78+
79+
QString mNewFilename;
80+
QString mErrorMessage;
81+
82+
QgsVectorFileWriter::SaveVectorOptions mOptions;
83+
};
84+
85+
#endif

‎tests/src/core/testqgstaskmanager.cpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,13 +1026,13 @@ void TestQgsTaskManager::layerDependencies()
10261026
//test that remove layers cancels all tasks which are dependent on them
10271027
TestTask* task = new TestTask();
10281028
task->hold();
1029-
task->setDependentLayers( QStringList() << layer2->id() << layer3->id() );
1030-
QCOMPARE( task->dependentLayerIds(), QStringList() << layer2->id() << layer3->id() );
1029+
task->setDependentLayers( QList< QgsMapLayer* >() << layer2 << layer3 );
1030+
QCOMPARE( task->dependentLayers(), QList< QgsMapLayer* >() << layer2 << layer3 );
10311031
long taskId = manager.addTask( task );
1032-
QCOMPARE( manager.dependentLayers( taskId ), QStringList() << layer2->id() << layer3->id() );
1033-
QVERIFY( manager.tasksDependentOnLayer( "xxx" ).isEmpty() );
1034-
QCOMPARE( manager.tasksDependentOnLayer( layer2->id() ), QList< QgsTask* >() << task );
1035-
QCOMPARE( manager.tasksDependentOnLayer( layer3->id() ), QList< QgsTask* >() << task );
1032+
QCOMPARE( manager.dependentLayers( taskId ), QList< QgsMapLayer* >() << layer2 << layer3 );
1033+
QVERIFY( manager.tasksDependentOnLayer( nullptr ).isEmpty() );
1034+
QCOMPARE( manager.tasksDependentOnLayer( layer2 ), QList< QgsTask* >() << task );
1035+
QCOMPARE( manager.tasksDependentOnLayer( layer3 ), QList< QgsTask* >() << task );
10361036

10371037
QCOMPARE( task->status(), QgsTask::OnHold );
10381038
//removing layer1 should have no effect

‎tests/src/python/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ ADD_PYTHON_TEST(PyQgsFieldFormattersTest test_qgsfieldformatters.py)
5050
ADD_PYTHON_TEST(PyQgsFillSymbolLayers test_qgsfillsymbollayers.py)
5151
ADD_PYTHON_TEST(PyQgsProject test_qgsproject.py)
5252
ADD_PYTHON_TEST(PyQgsFeatureIterator test_qgsfeatureiterator.py)
53+
ADD_PYTHON_TEST(PyQgsFeedback test_qgsfeedback.py)
5354
ADD_PYTHON_TEST(PyQgsField test_qgsfield.py)
5455
ADD_PYTHON_TEST(PyQgsFieldModel test_qgsfieldmodel.py)
5556
ADD_PYTHON_TEST(PyQgsFilterLineEdit test_qgsfilterlineedit.py)
@@ -127,6 +128,7 @@ ADD_PYTHON_TEST(PyQgsTreeWidgetItem test_qgstreewidgetitem.py)
127128
ADD_PYTHON_TEST(PyQgsUnitTypes test_qgsunittypes.py)
128129
ADD_PYTHON_TEST(PyQgsVectorColorRamp test_qgsvectorcolorramp.py)
129130
ADD_PYTHON_TEST(PyQgsVectorFileWriter test_qgsvectorfilewriter.py)
131+
ADD_PYTHON_TEST(PyQgsVectorFileWriterTask test_qgsvectorfilewritertask.py)
130132
ADD_PYTHON_TEST(PyQgsVectorLayer test_qgsvectorlayer.py)
131133
ADD_PYTHON_TEST(PyQgsVectorLayerEditBuffer test_qgsvectorlayereditbuffer.py)
132134
ADD_PYTHON_TEST(PyQgsVectorLayerUtils test_qgsvectorlayerutils.py)

‎tests/src/python/test_qgsfeedback.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# -*- coding: utf-8 -*-
2+
"""QGIS Unit tests for QgsFeedback.
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__ = '12/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+
import os
17+
18+
from qgis.core import (QgsFeedback)
19+
from qgis.PyQt.QtTest import QSignalSpy
20+
from qgis.testing import unittest
21+
22+
23+
class TestQgsFeedback(unittest.TestCase):
24+
25+
def testCancel(self):
26+
f = QgsFeedback()
27+
self.assertFalse(f.isCanceled())
28+
29+
cancel_spy = QSignalSpy(f.canceled)
30+
31+
f.cancel()
32+
self.assertTrue(f.isCanceled())
33+
self.assertEqual(len(cancel_spy), 1)
34+
35+
def testProgress(self):
36+
f = QgsFeedback()
37+
self.assertEqual(f.progress(), 0.0)
38+
39+
progress_spy = QSignalSpy(f.progressChanged)
40+
41+
f.setProgress(25)
42+
self.assertEqual(f.progress(), 25.0)
43+
self.assertEqual(len(progress_spy), 1)
44+
self.assertEqual(progress_spy[0][0], 25.0)
45+
46+
47+
if __name__ == '__main__':
48+
unittest.main()
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# -*- coding: utf-8 -*-
2+
"""QGIS Unit tests for QgsVectorFileWriterTask.
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__ = '12/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+
import os
17+
18+
from qgis.core import (
19+
QgsTask,
20+
QgsTaskManager,
21+
QgsApplication,
22+
QgsVectorLayer,
23+
QgsFeature,
24+
QgsGeometry,
25+
QgsPoint,
26+
QgsVectorFileWriter,
27+
QgsVectorFileWriterTask
28+
29+
)
30+
from qgis.PyQt.QtCore import (QCoreApplication,
31+
QDir)
32+
33+
from qgis.testing import start_app, unittest
34+
35+
start_app()
36+
37+
38+
def create_temp_filename(base_file):
39+
return os.path.join(str(QDir.tempPath()), base_file)
40+
41+
42+
class TestQgsVectorFileWriterTask(unittest.TestCase):
43+
44+
def setUp(self):
45+
self.success = False
46+
self.fail = False
47+
48+
def onSuccess(self):
49+
self.success = True
50+
51+
def onFail(self):
52+
self.fail = True
53+
54+
def createLayer(self):
55+
layer = QgsVectorLayer(
56+
('Point?crs=epsg:4326&field=name:string(20)&'
57+
'field=age:integer&field=size:double&index=yes'),
58+
'test',
59+
'memory')
60+
61+
self.assertIsNotNone(layer, 'Provider not initialized')
62+
provider = layer.dataProvider()
63+
self.assertIsNotNone(provider)
64+
65+
ft = QgsFeature()
66+
ft.setGeometry(QgsGeometry.fromPoint(QgsPoint(10, 10)))
67+
ft.setAttributes(['Johny', 20, 0.3])
68+
provider.addFeatures([ft])
69+
return layer
70+
71+
def testSuccess(self):
72+
"""test successfully writing a layer"""
73+
self.layer = self.createLayer()
74+
options = QgsVectorFileWriter.SaveVectorOptions()
75+
tmp = create_temp_filename('successlayer.shp')
76+
task = QgsVectorFileWriterTask(self.layer, tmp, options)
77+
78+
task.writeComplete.connect(self.onSuccess)
79+
task.errorOccurred.connect(self.onFail)
80+
81+
QgsApplication.taskManager().addTask(task)
82+
while not self.success and not self.fail:
83+
QCoreApplication.processEvents()
84+
85+
self.assertTrue(self.success)
86+
self.assertFalse(self.fail)
87+
88+
def testLayerRemovalBeforeRun(self):
89+
"""test behavior when layer is removed before task begins"""
90+
self.layer = self.createLayer()
91+
options = QgsVectorFileWriter.SaveVectorOptions()
92+
tmp = create_temp_filename('fail.shp')
93+
task = QgsVectorFileWriterTask(self.layer, tmp, options)
94+
95+
task.writeComplete.connect(self.onSuccess)
96+
task.errorOccurred.connect(self.onFail)
97+
98+
# remove layer
99+
self.layer = None
100+
101+
QgsApplication.taskManager().addTask(task)
102+
while not self.success and not self.fail:
103+
QCoreApplication.processEvents()
104+
105+
self.assertFalse(self.success)
106+
self.assertTrue(self.fail)
107+
108+
def testNoLayer(self):
109+
"""test that failure (and not crash) occurs when no layer set"""
110+
111+
options = QgsVectorFileWriter.SaveVectorOptions()
112+
tmp = create_temp_filename('fail.shp')
113+
task = QgsVectorFileWriterTask(None, tmp, options)
114+
task.writeComplete.connect(self.onSuccess)
115+
task.errorOccurred.connect(self.onFail)
116+
117+
QgsApplication.taskManager().addTask(task)
118+
while not self.success and not self.fail:
119+
QCoreApplication.processEvents()
120+
121+
self.assertFalse(self.success)
122+
self.assertTrue(self.fail)
123+
124+
125+
if __name__ == '__main__':
126+
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.