Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #7369 from signedav/offline_editing
Offline Editing GeoPackage File Export
  • Loading branch information
m-kuhn committed Jul 13, 2018
2 parents e2a740b + e558044 commit 2699a42
Show file tree
Hide file tree
Showing 8 changed files with 535 additions and 213 deletions.
464 changes: 269 additions & 195 deletions src/core/qgsofflineediting.cpp

Large diffs are not rendered by default.

15 changes: 12 additions & 3 deletions src/core/qgsofflineediting.h
Expand Up @@ -49,6 +49,13 @@ class CORE_EXPORT QgsOfflineEditing : public QObject
UpdateGeometries
};

//! Type of offline database container file
enum ContainerType
{
SpatiaLite,
GPKG
};

QgsOfflineEditing();

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

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

private:
void initializeSpatialMetadata( sqlite3 *sqlite_handle );
bool createSpatialiteDB( const QString &offlineDbPath );
bool createOfflineDb( const QString &offlineDbPath, ContainerType containerType = SpatiaLite );
void createLoggingTables( sqlite3 *db );
QgsVectorLayer *copyVectorLayer( QgsVectorLayer *layer, sqlite3 *db, const QString &offlineDbPath, bool onlySelected );

QgsVectorLayer *copyVectorLayer( QgsVectorLayer *layer, sqlite3 *db, const QString &offlineDbPath, bool onlySelected, ContainerType containerType = SpatiaLite );

void applyAttributesAdded( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo );
void applyFeaturesAdded( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId );
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/offline_editing/offline_editing_plugin.cpp
Expand Up @@ -104,7 +104,7 @@ void QgsOfflineEditingPlugin::convertProject()
}

mProgressDialog->setTitle( tr( "Converting to Offline Project" ) );
if ( mOfflineEditing->convertToOfflineProject( myPluginGui->offlineDataPath(), myPluginGui->offlineDbFile(), selectedLayerIds, myPluginGui->onlySelected() ) )
if ( mOfflineEditing->convertToOfflineProject( myPluginGui->offlineDataPath(), myPluginGui->offlineDbFile(), selectedLayerIds, myPluginGui->onlySelected(), myPluginGui->dbContainerType() ) )
{
updateActions();
// Redraw, to make the offline layer visible
Expand Down
79 changes: 65 additions & 14 deletions src/plugins/offline_editing/offline_editing_plugin_gui.cpp
Expand Up @@ -106,7 +106,7 @@ QgsOfflineEditingPluginGui::QgsOfflineEditingPluginGui( QWidget *parent, Qt::Win

restoreState();

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

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

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

QgsOfflineEditingPluginGui::~QgsOfflineEditingPluginGui()
Expand Down Expand Up @@ -145,23 +146,58 @@ bool QgsOfflineEditingPluginGui::onlySelected() const
return mOnlySelectedCheckBox->checkState() == Qt::Checked;
}

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

if ( !fileName.isEmpty() )
void QgsOfflineEditingPluginGui::mBrowseButton_clicked()
{
switch ( dbContainerType() )
{
if ( !fileName.endsWith( QLatin1String( ".sqlite" ), Qt::CaseInsensitive ) )
case QgsOfflineEditing::GPKG:
{
//GeoPackage
QString fileName = QFileDialog::getSaveFileName( this,
tr( "Select target database for offline data" ),
QDir( mOfflineDataPath ).absoluteFilePath( mOfflineDbFile ),
tr( "GeoPackage" ) + " (*.gpkg);;"
+ tr( "All files" ) + " (*.*)" );

if ( !fileName.isEmpty() )
{
if ( !fileName.endsWith( QLatin1String( ".gpkg" ), Qt::CaseInsensitive ) )
{
fileName += QLatin1String( ".gpkg" );
}
mOfflineDbFile = QFileInfo( fileName ).fileName();
mOfflineDataPath = QFileInfo( fileName ).absolutePath();
mOfflineDataPathLineEdit->setText( fileName );
}
}
case QgsOfflineEditing::SpatiaLite:
{
fileName += QLatin1String( ".sqlite" );
//SpaciaLite
QString fileName = QFileDialog::getSaveFileName( this,
tr( "Select target database for offline data" ),
QDir( mOfflineDataPath ).absoluteFilePath( mOfflineDbFile ),
tr( "SpatiaLite DB" ) + " (*.sqlite);;"
+ tr( "All files" ) + " (*.*)" );

if ( !fileName.isEmpty() )
{
if ( !fileName.endsWith( QLatin1String( ".sqlite" ), Qt::CaseInsensitive ) )
{
fileName += QLatin1String( ".sqlite" );
}
mOfflineDbFile = QFileInfo( fileName ).fileName();
mOfflineDataPath = QFileInfo( fileName ).absolutePath();
mOfflineDataPathLineEdit->setText( fileName );
}
}
mOfflineDbFile = QFileInfo( fileName ).fileName();
mOfflineDataPath = QFileInfo( fileName ).absolutePath();
mOfflineDataPathLineEdit->setText( fileName );
}
}

Expand Down Expand Up @@ -216,9 +252,24 @@ void QgsOfflineEditingPluginGui::selectAll()
nodeLayer->setItemVisibilityChecked( true );
}


void QgsOfflineEditingPluginGui::deSelectAll()
{
Q_FOREACH ( QgsLayerTreeLayer *nodeLayer, mLayerTree->layerTreeModel()->rootGroup()->findLayers() )
nodeLayer->setItemVisibilityChecked( false );
}

void QgsOfflineEditingPluginGui::datatypeChanged( int index )
{
if ( index == 0 )
{
//GeoPackage
mOfflineDbFile = QStringLiteral( "offline.gpkg" );
}
else
{
//SpatiaLite
mOfflineDbFile = QStringLiteral( "offline.sqlite" );
}
mOfflineDataPathLineEdit->setText( QDir( mOfflineDataPath ).absoluteFilePath( mOfflineDbFile ) );
}

3 changes: 3 additions & 0 deletions src/plugins/offline_editing/offline_editing_plugin_gui.h
Expand Up @@ -23,6 +23,7 @@

#include "ui_offline_editing_plugin_guibase.h"

#include "qgsofflineediting.h"
#include "qgslayertreemodel.h"

class QgsSelectLayerTreeModel : public QgsLayerTreeModel
Expand All @@ -46,11 +47,13 @@ class QgsOfflineEditingPluginGui : public QDialog, private Ui::QgsOfflineEditing
QString offlineDbFile();
QStringList selectedLayerIds();
bool onlySelected() const;
QgsOfflineEditing::ContainerType dbContainerType() const;

public slots:
//! Change the selection of layers in the list
void selectAll();
void deSelectAll();
void datatypeChanged( int index );

private:
void saveState();
Expand Down
37 changes: 37 additions & 0 deletions src/plugins/offline_editing/offline_editing_plugin_guibase.ui
Expand Up @@ -18,6 +18,43 @@
<normaloff>.</normaloff>.</iconset>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="selectDatatypeComboLabel">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Storage type</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="mSelectDatatypeCombo">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>GeoPackage</string>
</property>
</item>
<item>
<property name="text">
<string>SpatiaLite</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
Expand Down
1 change: 1 addition & 0 deletions tests/src/core/CMakeLists.txt
Expand Up @@ -197,6 +197,7 @@ SET(TESTS
testqgslayerdefinition.cpp
testqgssqliteutils.cpp
testqgsmimedatautils.cpp
testqgsofflineediting.cpp
)

IF(WITH_QTWEBKIT)
Expand Down
147 changes: 147 additions & 0 deletions tests/src/core/testqgsofflineediting.cpp
@@ -0,0 +1,147 @@
/***************************************************************************
testqgsofflineediting.cpp
---------------------
begin : 3.7.2018
copyright : (C) 2018 by david signer
email : david at opengis dot ch
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include <QObject>

#include <QString>
#include <QStringList>
#include <QApplication>
#include <QFileInfo>
#include <QDir>

#include "qgsunittypes.h"
#include "qgsofflineediting.h"
#include "qgstest.h"
#include "qgsvectorlayerref.h"

/**
* \ingroup UnitTests
*/
class TestQgsOfflineEditing : public QObject
{
Q_OBJECT

private:
QgsOfflineEditing *mOfflineEditing = nullptr;
QgsVectorLayer *mpLayer = nullptr;
QString offlineDataPath;
QString offlineDbFile;
QStringList layerIds;
long numberOfFeatures;
int numberOfFields;

private slots:
void initTestCase();// will be called before the first testfunction is executed.
void cleanupTestCase();// will be called after the last testfunction was executed.
void init(); // will be called before each testfunction is executed.
void cleanup(); // will be called after every testfunction.

void createSpatialiteAndSynchronizeBack();
void createGeopackageAndSynchronizeBack();
};

void TestQgsOfflineEditing::initTestCase()
{
//
// Runs once before any tests are run
//
// init QGIS's paths - true means that all path will be inited from prefix
QgsApplication::init();
QgsApplication::initQgis();
QgsApplication::showSettings();

//OfflineEditing
mOfflineEditing = new QgsOfflineEditing();
offlineDataPath = ".";
}

void TestQgsOfflineEditing::cleanupTestCase()
{
QgsApplication::exitQgis();
}

void TestQgsOfflineEditing::init()
{
QString myFileName( TEST_DATA_DIR ); //defined in CmakeLists.txt
myFileName = myFileName + "/points.shp";
QFileInfo myMapFileInfo( myFileName );
mpLayer = new QgsVectorLayer( myMapFileInfo.filePath(),
myMapFileInfo.completeBaseName(), QStringLiteral( "ogr" ) );
QgsProject::instance()->addMapLayer( mpLayer );

numberOfFeatures = mpLayer->featureCount();
numberOfFields = mpLayer->fields().size();

layerIds.append( mpLayer->id() );
}

void TestQgsOfflineEditing::cleanup()
{
QgsProject::instance()->removeAllMapLayers();
layerIds.clear();
QDir dir( offlineDataPath );
dir.remove( offlineDbFile );
}

void TestQgsOfflineEditing::createSpatialiteAndSynchronizeBack()
{
offlineDbFile = "TestQgsOfflineEditing.sqlite";
QCOMPARE( mpLayer->name(), QStringLiteral( "points" ) );
QCOMPARE( mpLayer->featureCount(), numberOfFeatures );
QCOMPARE( mpLayer->fields().size(), numberOfFields );

//convert
mOfflineEditing->convertToOfflineProject( offlineDataPath, offlineDbFile, layerIds, false, QgsOfflineEditing::SpatiaLite );

mpLayer = qobject_cast<QgsVectorLayer *>( QgsProject::instance()->mapLayers().first() );
QCOMPARE( mpLayer->name(), QStringLiteral( "points (offline)" ) );
QCOMPARE( mpLayer->featureCount(), numberOfFeatures );

//synchronize back
mOfflineEditing->synchronize();

mpLayer = qobject_cast<QgsVectorLayer *>( QgsProject::instance()->mapLayers().first() );
QCOMPARE( mpLayer->name(), QStringLiteral( "points" ) );
QCOMPARE( mpLayer->featureCount(), numberOfFeatures );
QCOMPARE( mpLayer->fields().size(), numberOfFields );
}

void TestQgsOfflineEditing::createGeopackageAndSynchronizeBack()
{
offlineDbFile = "TestQgsOfflineEditing.gpkg";
QCOMPARE( mpLayer->name(), QStringLiteral( "points" ) );
QCOMPARE( mpLayer->featureCount(), numberOfFeatures );
QCOMPARE( mpLayer->fields().size(), numberOfFields );

//convert
mOfflineEditing->convertToOfflineProject( offlineDataPath, offlineDbFile, layerIds, false, QgsOfflineEditing::GPKG );

mpLayer = qobject_cast<QgsVectorLayer *>( QgsProject::instance()->mapLayers().first() );
QCOMPARE( mpLayer->name(), QStringLiteral( "points (offline)" ) );
QCOMPARE( mpLayer->featureCount(), numberOfFeatures );
//comparing with the number +1 because GPKG created an fid
QCOMPARE( mpLayer->fields().size(), numberOfFields + 1 );

//synchronize back
mOfflineEditing->synchronize();

mpLayer = qobject_cast<QgsVectorLayer *>( QgsProject::instance()->mapLayers().first() );
QCOMPARE( mpLayer->name(), QStringLiteral( "points" ) );
QCOMPARE( mpLayer->featureCount(), numberOfFeatures );
QCOMPARE( mpLayer->fields().size(), numberOfFields );
}

QGSTEST_MAIN( TestQgsOfflineEditing )
#include "testqgsofflineediting.moc"

0 comments on commit 2699a42

Please sign in to comment.