Skip to content

Commit 2699a42

Browse files
authored
Merge pull request #7369 from signedav/offline_editing
Offline Editing GeoPackage File Export
2 parents e2a740b + e558044 commit 2699a42

File tree

8 files changed

+535
-213
lines changed

8 files changed

+535
-213
lines changed

src/core/qgsofflineediting.cpp

Lines changed: 269 additions & 195 deletions
Large diffs are not rendered by default.

src/core/qgsofflineediting.h

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ class CORE_EXPORT QgsOfflineEditing : public QObject
4949
UpdateGeometries
5050
};
5151

52+
//! Type of offline database container file
53+
enum ContainerType
54+
{
55+
SpatiaLite,
56+
GPKG
57+
};
58+
5259
QgsOfflineEditing();
5360

5461
/**
@@ -57,8 +64,9 @@ class CORE_EXPORT QgsOfflineEditing : public QObject
5764
* \param offlineDbFile Offline db file name
5865
* \param layerIds List of layer names to convert
5966
* \param onlySelected Only copy selected features from layers where a selection is present
67+
* \param containerType defines the SQLite file container type like SpatiaLite or GPKG
6068
*/
61-
bool convertToOfflineProject( const QString &offlineDataPath, const QString &offlineDbFile, const QStringList &layerIds, bool onlySelected = false );
69+
bool convertToOfflineProject( const QString &offlineDataPath, const QString &offlineDbFile, const QStringList &layerIds, bool onlySelected = false, ContainerType containerType = SpatiaLite );
6270

6371
//! Returns true if current project is offline
6472
bool isOfflineProject() const;
@@ -107,9 +115,10 @@ class CORE_EXPORT QgsOfflineEditing : public QObject
107115

108116
private:
109117
void initializeSpatialMetadata( sqlite3 *sqlite_handle );
110-
bool createSpatialiteDB( const QString &offlineDbPath );
118+
bool createOfflineDb( const QString &offlineDbPath, ContainerType containerType = SpatiaLite );
111119
void createLoggingTables( sqlite3 *db );
112-
QgsVectorLayer *copyVectorLayer( QgsVectorLayer *layer, sqlite3 *db, const QString &offlineDbPath, bool onlySelected );
120+
121+
QgsVectorLayer *copyVectorLayer( QgsVectorLayer *layer, sqlite3 *db, const QString &offlineDbPath, bool onlySelected, ContainerType containerType = SpatiaLite );
113122

114123
void applyAttributesAdded( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo );
115124
void applyFeaturesAdded( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId );

src/plugins/offline_editing/offline_editing_plugin.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ void QgsOfflineEditingPlugin::convertProject()
104104
}
105105

106106
mProgressDialog->setTitle( tr( "Converting to Offline Project" ) );
107-
if ( mOfflineEditing->convertToOfflineProject( myPluginGui->offlineDataPath(), myPluginGui->offlineDbFile(), selectedLayerIds, myPluginGui->onlySelected() ) )
107+
if ( mOfflineEditing->convertToOfflineProject( myPluginGui->offlineDataPath(), myPluginGui->offlineDbFile(), selectedLayerIds, myPluginGui->onlySelected(), myPluginGui->dbContainerType() ) )
108108
{
109109
updateActions();
110110
// Redraw, to make the offline layer visible

src/plugins/offline_editing/offline_editing_plugin_gui.cpp

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ QgsOfflineEditingPluginGui::QgsOfflineEditingPluginGui( QWidget *parent, Qt::Win
106106

107107
restoreState();
108108

109-
mOfflineDbFile = QStringLiteral( "offline.sqlite" );
109+
mOfflineDbFile = QStringLiteral( "offline.gpkg" );
110110
mOfflineDataPathLineEdit->setText( QDir( mOfflineDataPath ).absoluteFilePath( mOfflineDbFile ) );
111111

112112
QgsLayerTree *rootNode = QgsProject::instance()->layerTreeRoot()->clone();
@@ -116,6 +116,7 @@ QgsOfflineEditingPluginGui::QgsOfflineEditingPluginGui( QWidget *parent, Qt::Win
116116

117117
connect( mSelectAllButton, &QAbstractButton::clicked, this, &QgsOfflineEditingPluginGui::selectAll );
118118
connect( mDeselectAllButton, &QAbstractButton::clicked, this, &QgsOfflineEditingPluginGui::deSelectAll );
119+
connect( mSelectDatatypeCombo, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, &QgsOfflineEditingPluginGui::datatypeChanged );
119120
}
120121

121122
QgsOfflineEditingPluginGui::~QgsOfflineEditingPluginGui()
@@ -145,23 +146,58 @@ bool QgsOfflineEditingPluginGui::onlySelected() const
145146
return mOnlySelectedCheckBox->checkState() == Qt::Checked;
146147
}
147148

148-
void QgsOfflineEditingPluginGui::mBrowseButton_clicked()
149+
QgsOfflineEditing::ContainerType QgsOfflineEditingPluginGui::dbContainerType() const
149150
{
150-
QString fileName = QFileDialog::getSaveFileName( this,
151-
tr( "Select target database for offline data" ),
152-
QDir( mOfflineDataPath ).absoluteFilePath( mOfflineDbFile ),
153-
tr( "SpatiaLite DB" ) + " (*.sqlite);;"
154-
+ tr( "All files" ) + " (*.*)" );
151+
if ( mSelectDatatypeCombo->currentIndex() == 0 )
152+
return QgsOfflineEditing::GPKG;
153+
else
154+
return QgsOfflineEditing::SpatiaLite;
155+
}
155156

156-
if ( !fileName.isEmpty() )
157+
void QgsOfflineEditingPluginGui::mBrowseButton_clicked()
158+
{
159+
switch ( dbContainerType() )
157160
{
158-
if ( !fileName.endsWith( QLatin1String( ".sqlite" ), Qt::CaseInsensitive ) )
161+
case QgsOfflineEditing::GPKG:
162+
{
163+
//GeoPackage
164+
QString fileName = QFileDialog::getSaveFileName( this,
165+
tr( "Select target database for offline data" ),
166+
QDir( mOfflineDataPath ).absoluteFilePath( mOfflineDbFile ),
167+
tr( "GeoPackage" ) + " (*.gpkg);;"
168+
+ tr( "All files" ) + " (*.*)" );
169+
170+
if ( !fileName.isEmpty() )
171+
{
172+
if ( !fileName.endsWith( QLatin1String( ".gpkg" ), Qt::CaseInsensitive ) )
173+
{
174+
fileName += QLatin1String( ".gpkg" );
175+
}
176+
mOfflineDbFile = QFileInfo( fileName ).fileName();
177+
mOfflineDataPath = QFileInfo( fileName ).absolutePath();
178+
mOfflineDataPathLineEdit->setText( fileName );
179+
}
180+
}
181+
case QgsOfflineEditing::SpatiaLite:
159182
{
160-
fileName += QLatin1String( ".sqlite" );
183+
//SpaciaLite
184+
QString fileName = QFileDialog::getSaveFileName( this,
185+
tr( "Select target database for offline data" ),
186+
QDir( mOfflineDataPath ).absoluteFilePath( mOfflineDbFile ),
187+
tr( "SpatiaLite DB" ) + " (*.sqlite);;"
188+
+ tr( "All files" ) + " (*.*)" );
189+
190+
if ( !fileName.isEmpty() )
191+
{
192+
if ( !fileName.endsWith( QLatin1String( ".sqlite" ), Qt::CaseInsensitive ) )
193+
{
194+
fileName += QLatin1String( ".sqlite" );
195+
}
196+
mOfflineDbFile = QFileInfo( fileName ).fileName();
197+
mOfflineDataPath = QFileInfo( fileName ).absolutePath();
198+
mOfflineDataPathLineEdit->setText( fileName );
199+
}
161200
}
162-
mOfflineDbFile = QFileInfo( fileName ).fileName();
163-
mOfflineDataPath = QFileInfo( fileName ).absolutePath();
164-
mOfflineDataPathLineEdit->setText( fileName );
165201
}
166202
}
167203

@@ -216,9 +252,24 @@ void QgsOfflineEditingPluginGui::selectAll()
216252
nodeLayer->setItemVisibilityChecked( true );
217253
}
218254

219-
220255
void QgsOfflineEditingPluginGui::deSelectAll()
221256
{
222257
Q_FOREACH ( QgsLayerTreeLayer *nodeLayer, mLayerTree->layerTreeModel()->rootGroup()->findLayers() )
223258
nodeLayer->setItemVisibilityChecked( false );
224259
}
260+
261+
void QgsOfflineEditingPluginGui::datatypeChanged( int index )
262+
{
263+
if ( index == 0 )
264+
{
265+
//GeoPackage
266+
mOfflineDbFile = QStringLiteral( "offline.gpkg" );
267+
}
268+
else
269+
{
270+
//SpatiaLite
271+
mOfflineDbFile = QStringLiteral( "offline.sqlite" );
272+
}
273+
mOfflineDataPathLineEdit->setText( QDir( mOfflineDataPath ).absoluteFilePath( mOfflineDbFile ) );
274+
}
275+

src/plugins/offline_editing/offline_editing_plugin_gui.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
#include "ui_offline_editing_plugin_guibase.h"
2525

26+
#include "qgsofflineediting.h"
2627
#include "qgslayertreemodel.h"
2728

2829
class QgsSelectLayerTreeModel : public QgsLayerTreeModel
@@ -46,11 +47,13 @@ class QgsOfflineEditingPluginGui : public QDialog, private Ui::QgsOfflineEditing
4647
QString offlineDbFile();
4748
QStringList selectedLayerIds();
4849
bool onlySelected() const;
50+
QgsOfflineEditing::ContainerType dbContainerType() const;
4951

5052
public slots:
5153
//! Change the selection of layers in the list
5254
void selectAll();
5355
void deSelectAll();
56+
void datatypeChanged( int index );
5457

5558
private:
5659
void saveState();

src/plugins/offline_editing/offline_editing_plugin_guibase.ui

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,43 @@
1818
<normaloff>.</normaloff>.</iconset>
1919
</property>
2020
<layout class="QVBoxLayout" name="verticalLayout_2">
21+
<item>
22+
<layout class="QHBoxLayout" name="horizontalLayout_4">
23+
<item>
24+
<widget class="QLabel" name="selectDatatypeComboLabel">
25+
<property name="maximumSize">
26+
<size>
27+
<width>16777215</width>
28+
<height>16777215</height>
29+
</size>
30+
</property>
31+
<property name="text">
32+
<string>Storage type</string>
33+
</property>
34+
</widget>
35+
</item>
36+
<item>
37+
<widget class="QComboBox" name="mSelectDatatypeCombo">
38+
<property name="sizePolicy">
39+
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
40+
<horstretch>0</horstretch>
41+
<verstretch>0</verstretch>
42+
</sizepolicy>
43+
</property>
44+
<item>
45+
<property name="text">
46+
<string>GeoPackage</string>
47+
</property>
48+
</item>
49+
<item>
50+
<property name="text">
51+
<string>SpatiaLite</string>
52+
</property>
53+
</item>
54+
</widget>
55+
</item>
56+
</layout>
57+
</item>
2158
<item>
2259
<layout class="QHBoxLayout" name="horizontalLayout">
2360
<item>

tests/src/core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ SET(TESTS
197197
testqgslayerdefinition.cpp
198198
testqgssqliteutils.cpp
199199
testqgsmimedatautils.cpp
200+
testqgsofflineediting.cpp
200201
)
201202

202203
IF(WITH_QTWEBKIT)
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/***************************************************************************
2+
testqgsofflineediting.cpp
3+
4+
---------------------
5+
begin : 3.7.2018
6+
copyright : (C) 2018 by david signer
7+
email : david at opengis dot ch
8+
***************************************************************************
9+
* *
10+
* This program is free software; you can redistribute it and/or modify *
11+
* it under the terms of the GNU General Public License as published by *
12+
* the Free Software Foundation; either version 2 of the License, or *
13+
* (at your option) any later version. *
14+
* *
15+
***************************************************************************/
16+
#include <QObject>
17+
18+
#include <QString>
19+
#include <QStringList>
20+
#include <QApplication>
21+
#include <QFileInfo>
22+
#include <QDir>
23+
24+
#include "qgsunittypes.h"
25+
#include "qgsofflineediting.h"
26+
#include "qgstest.h"
27+
#include "qgsvectorlayerref.h"
28+
29+
/**
30+
* \ingroup UnitTests
31+
*/
32+
class TestQgsOfflineEditing : public QObject
33+
{
34+
Q_OBJECT
35+
36+
private:
37+
QgsOfflineEditing *mOfflineEditing = nullptr;
38+
QgsVectorLayer *mpLayer = nullptr;
39+
QString offlineDataPath;
40+
QString offlineDbFile;
41+
QStringList layerIds;
42+
long numberOfFeatures;
43+
int numberOfFields;
44+
45+
private slots:
46+
void initTestCase();// will be called before the first testfunction is executed.
47+
void cleanupTestCase();// will be called after the last testfunction was executed.
48+
void init(); // will be called before each testfunction is executed.
49+
void cleanup(); // will be called after every testfunction.
50+
51+
void createSpatialiteAndSynchronizeBack();
52+
void createGeopackageAndSynchronizeBack();
53+
};
54+
55+
void TestQgsOfflineEditing::initTestCase()
56+
{
57+
//
58+
// Runs once before any tests are run
59+
//
60+
// init QGIS's paths - true means that all path will be inited from prefix
61+
QgsApplication::init();
62+
QgsApplication::initQgis();
63+
QgsApplication::showSettings();
64+
65+
//OfflineEditing
66+
mOfflineEditing = new QgsOfflineEditing();
67+
offlineDataPath = ".";
68+
}
69+
70+
void TestQgsOfflineEditing::cleanupTestCase()
71+
{
72+
QgsApplication::exitQgis();
73+
}
74+
75+
void TestQgsOfflineEditing::init()
76+
{
77+
QString myFileName( TEST_DATA_DIR ); //defined in CmakeLists.txt
78+
myFileName = myFileName + "/points.shp";
79+
QFileInfo myMapFileInfo( myFileName );
80+
mpLayer = new QgsVectorLayer( myMapFileInfo.filePath(),
81+
myMapFileInfo.completeBaseName(), QStringLiteral( "ogr" ) );
82+
QgsProject::instance()->addMapLayer( mpLayer );
83+
84+
numberOfFeatures = mpLayer->featureCount();
85+
numberOfFields = mpLayer->fields().size();
86+
87+
layerIds.append( mpLayer->id() );
88+
}
89+
90+
void TestQgsOfflineEditing::cleanup()
91+
{
92+
QgsProject::instance()->removeAllMapLayers();
93+
layerIds.clear();
94+
QDir dir( offlineDataPath );
95+
dir.remove( offlineDbFile );
96+
}
97+
98+
void TestQgsOfflineEditing::createSpatialiteAndSynchronizeBack()
99+
{
100+
offlineDbFile = "TestQgsOfflineEditing.sqlite";
101+
QCOMPARE( mpLayer->name(), QStringLiteral( "points" ) );
102+
QCOMPARE( mpLayer->featureCount(), numberOfFeatures );
103+
QCOMPARE( mpLayer->fields().size(), numberOfFields );
104+
105+
//convert
106+
mOfflineEditing->convertToOfflineProject( offlineDataPath, offlineDbFile, layerIds, false, QgsOfflineEditing::SpatiaLite );
107+
108+
mpLayer = qobject_cast<QgsVectorLayer *>( QgsProject::instance()->mapLayers().first() );
109+
QCOMPARE( mpLayer->name(), QStringLiteral( "points (offline)" ) );
110+
QCOMPARE( mpLayer->featureCount(), numberOfFeatures );
111+
112+
//synchronize back
113+
mOfflineEditing->synchronize();
114+
115+
mpLayer = qobject_cast<QgsVectorLayer *>( QgsProject::instance()->mapLayers().first() );
116+
QCOMPARE( mpLayer->name(), QStringLiteral( "points" ) );
117+
QCOMPARE( mpLayer->featureCount(), numberOfFeatures );
118+
QCOMPARE( mpLayer->fields().size(), numberOfFields );
119+
}
120+
121+
void TestQgsOfflineEditing::createGeopackageAndSynchronizeBack()
122+
{
123+
offlineDbFile = "TestQgsOfflineEditing.gpkg";
124+
QCOMPARE( mpLayer->name(), QStringLiteral( "points" ) );
125+
QCOMPARE( mpLayer->featureCount(), numberOfFeatures );
126+
QCOMPARE( mpLayer->fields().size(), numberOfFields );
127+
128+
//convert
129+
mOfflineEditing->convertToOfflineProject( offlineDataPath, offlineDbFile, layerIds, false, QgsOfflineEditing::GPKG );
130+
131+
mpLayer = qobject_cast<QgsVectorLayer *>( QgsProject::instance()->mapLayers().first() );
132+
QCOMPARE( mpLayer->name(), QStringLiteral( "points (offline)" ) );
133+
QCOMPARE( mpLayer->featureCount(), numberOfFeatures );
134+
//comparing with the number +1 because GPKG created an fid
135+
QCOMPARE( mpLayer->fields().size(), numberOfFields + 1 );
136+
137+
//synchronize back
138+
mOfflineEditing->synchronize();
139+
140+
mpLayer = qobject_cast<QgsVectorLayer *>( QgsProject::instance()->mapLayers().first() );
141+
QCOMPARE( mpLayer->name(), QStringLiteral( "points" ) );
142+
QCOMPARE( mpLayer->featureCount(), numberOfFeatures );
143+
QCOMPARE( mpLayer->fields().size(), numberOfFields );
144+
}
145+
146+
QGSTEST_MAIN( TestQgsOfflineEditing )
147+
#include "testqgsofflineediting.moc"

0 commit comments

Comments
 (0)