48 #include <QDomDocument>
51 #include <QRegularExpression>
53 #include <ogr_srs_api.h>
60 #ifdef HAVE_SPATIALITE
63 #include <spatialite.h>
67 #define CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE "isOfflineEditable"
68 #define CUSTOM_PROPERTY_REMOTE_SOURCE "remoteSource"
69 #define CUSTOM_PROPERTY_REMOTE_PROVIDER "remoteProvider"
70 #define CUSTOM_SHOW_FEATURE_COUNT "showFeatureCount"
71 #define CUSTOM_PROPERTY_ORIGINAL_LAYERID "remoteLayerId"
72 #define CUSTOM_PROPERTY_LAYERNAME_SUFFIX "layerNameSuffix"
73 #define PROJECT_ENTRY_SCOPE_OFFLINE "OfflineEditingPlugin"
74 #define PROJECT_ENTRY_KEY_OFFLINE_DB_PATH "/OfflineDbPath"
95 if ( layerIds.isEmpty() )
100 const QString dbPath = QDir( offlineDataPath ).absoluteFilePath( offlineDbFile );
101 if ( createOfflineDb( dbPath, containerType ) )
104 const int rc = database.
open( dbPath );
105 if ( rc != SQLITE_OK )
107 showWarning( tr(
"Could not open the SpatiaLite database" ) );
112 createLoggingTables( database.get() );
117 for (
int i = 0; i < layerIds.count(); i++ )
125 convertToOfflineLayer( vl, database.get(), dbPath, onlySelected, containerType, layerNameSuffix );
133 if ( projectTitle.isEmpty() )
137 projectTitle += QLatin1String(
" (offline)" );
168 QMap<int, std::shared_ptr<QgsVectorLayer>> remoteLayersByOfflineId;
169 QMap<int, QgsVectorLayer *> offlineLayersByOfflineId;
171 for ( QMap<QString, QgsMapLayer *>::iterator layer_it = mapLayers.begin() ; layer_it != mapLayers.end(); ++layer_it )
173 QgsVectorLayer *offlineLayer( qobject_cast<QgsVectorLayer *>( layer_it.value() ) );
175 if ( !offlineLayer || !offlineLayer->
isValid() )
177 QgsDebugMsgLevel( QStringLiteral(
"Skipping offline layer %1 because it is an invalid layer" ).arg( layer_it.key() ), 4 );
186 QString remoteName = offlineLayer->
name();
188 if ( remoteName.endsWith( remoteNameSuffix ) )
189 remoteName.chop( remoteNameSuffix.size() );
192 std::shared_ptr<QgsVectorLayer> remoteLayer = std::make_shared<QgsVectorLayer>( remoteSource, remoteName, remoteProvider, options );
194 if ( ! remoteLayer->isValid() )
196 QgsDebugMsgLevel( QStringLiteral(
"Skipping offline layer %1 because it failed to recreate its corresponding remote layer" ).arg( offlineLayer->
id() ), 4 );
201 if ( remoteLayer->providerType().contains( QLatin1String(
"WFS" ), Qt::CaseInsensitive ) )
212 const QString sql = QStringLiteral(
"SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( offlineLayer->
id() );
213 const int layerId = sqlQueryInt( database.get(), sql, -1 );
217 QgsDebugMsgLevel( QStringLiteral(
"Skipping offline layer %1 because it failed to determine the offline editing layer id" ).arg( offlineLayer->
id() ), 4 );
221 remoteLayersByOfflineId.insert( layerId, remoteLayer );
222 offlineLayersByOfflineId.insert( layerId, offlineLayer );
225 QgsDebugMsgLevel( QStringLiteral(
"Found %1 offline layers in total" ).arg( offlineLayersByOfflineId.count() ), 4 );
227 QMap<QPair<QString, QString>, std::shared_ptr<QgsTransactionGroup>> transactionGroups;
228 if ( useTransaction )
230 for (
const std::shared_ptr<QgsVectorLayer> &remoteLayer : std::as_const( remoteLayersByOfflineId ) )
233 const QPair<QString, QString> pair( remoteLayer->providerType(), connectionString );
234 std::shared_ptr<QgsTransactionGroup> transactionGroup = transactionGroups.value( pair );
236 if ( !transactionGroup.get() )
237 transactionGroup = std::make_shared<QgsTransactionGroup>();
239 if ( !transactionGroup->addLayer( remoteLayer.get() ) )
241 QgsDebugMsgLevel( QStringLiteral(
"Failed to add a layer %1 into transaction group, will be modified without transaction" ).arg( remoteLayer->name() ), 4 );
245 transactionGroups.insert( pair, transactionGroup );
248 QgsDebugMsgLevel( QStringLiteral(
"Created %1 transaction groups" ).arg( transactionGroups.count() ), 4 );
251 const QList<int> offlineIds = remoteLayersByOfflineId.keys();
252 for (
int offlineLayerId : offlineIds )
254 std::shared_ptr<QgsVectorLayer> remoteLayer = remoteLayersByOfflineId.value( offlineLayerId );
255 QgsVectorLayer *offlineLayer = offlineLayersByOfflineId.value( offlineLayerId );
258 if ( !remoteLayer->startEditing() && !remoteLayer->isEditable() )
260 QgsDebugMsgLevel( QStringLiteral(
"Failed to turn layer %1 into editing mode" ).arg( remoteLayer->name() ), 4 );
265 const int commitNo = getCommitNo( database.get() );
266 QgsDebugMsgLevel( QStringLiteral(
"Found %1 commits" ).arg( commitNo ), 4 );
268 for (
int i = 0; i < commitNo; i++ )
270 QgsDebugMsgLevel( QStringLiteral(
"Apply commits chronologically from %1" ).arg( offlineLayer->
name() ), 4 );
272 applyAttributesAdded( remoteLayer.get(), database.get(), offlineLayerId, i );
273 applyAttributeValueChanges( offlineLayer, remoteLayer.get(), database.get(), offlineLayerId, i );
274 applyGeometryChanges( remoteLayer.get(), database.get(), offlineLayerId, i );
277 applyFeaturesAdded( offlineLayer, remoteLayer.get(), database.get(), offlineLayerId );
278 applyFeaturesRemoved( remoteLayer.get(), database.get(), offlineLayerId );
282 for (
int offlineLayerId : offlineIds )
284 std::shared_ptr<QgsVectorLayer> remoteLayer = remoteLayersByOfflineId[offlineLayerId];
285 QgsVectorLayer *offlineLayer = offlineLayersByOfflineId[offlineLayerId];
287 if ( !remoteLayer->isEditable() )
290 if ( remoteLayer->commitChanges() )
293 updateFidLookup( remoteLayer.get(), database.get(), offlineLayerId );
297 sql = QStringLiteral(
"DELETE FROM 'log_added_attrs' WHERE \"layer_id\" = %1" ).arg( offlineLayerId );
298 sqlExec( database.get(), sql );
299 sql = QStringLiteral(
"DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( offlineLayerId );
300 sqlExec( database.get(), sql );
301 sql = QStringLiteral(
"DELETE FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( offlineLayerId );
302 sqlExec( database.get(), sql );
303 sql = QStringLiteral(
"DELETE FROM 'log_feature_updates' WHERE \"layer_id\" = %1" ).arg( offlineLayerId );
304 sqlExec( database.get(), sql );
305 sql = QStringLiteral(
"DELETE FROM 'log_geometry_updates' WHERE \"layer_id\" = %1" ).arg( offlineLayerId );
306 sqlExec( database.get(), sql );
310 showWarning( remoteLayer->commitErrors().join( QLatin1Char(
'\n' ) ) );
317 remoteLayer->reload();
318 offlineLayer->
setDataSource( remoteLayer->source(), remoteLayer->name(), remoteLayer->dataProvider()->name() );
334 const QgsFields fields = remoteLayer->fields();
337 if ( !remoteLayer->dataProvider()->defaultValueClause( remoteLayer->fields().fieldOriginIndex( remoteLayer->fields().indexOf(
field.
name() ) ) ).isEmpty() )
349 const QString sql = QStringLiteral(
"UPDATE 'log_indices' SET 'last_index' = 0 WHERE \"name\" = 'commit_no'" );
350 sqlExec( database.get(), sql );
354 void QgsOfflineEditing::initializeSpatialMetadata(
sqlite3 *sqlite_handle )
356 #ifdef HAVE_SPATIALITE
358 if ( !sqlite_handle )
361 char **results =
nullptr;
363 int ret = sqlite3_get_table( sqlite_handle,
"select count(*) from sqlite_master", &results, &rows, &columns,
nullptr );
364 if ( ret != SQLITE_OK )
369 for (
int i = 1; i <= rows; i++ )
370 count = atoi( results[( i * columns ) + 0] );
373 sqlite3_free_table( results );
378 bool above41 =
false;
379 ret = sqlite3_get_table( sqlite_handle,
"select spatialite_version()", &results, &rows, &columns,
nullptr );
380 if ( ret == SQLITE_OK && rows == 1 && columns == 1 )
382 const QString version = QString::fromUtf8( results[1] );
383 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
384 QStringList parts = version.split(
' ', QString::SkipEmptyParts );
386 const QStringList parts = version.split(
' ', Qt::SkipEmptyParts );
388 if ( !parts.empty() )
390 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
391 QStringList verparts = parts.at( 0 ).split(
'.', QString::SkipEmptyParts );
393 const QStringList verparts = parts.at( 0 ).split(
'.', Qt::SkipEmptyParts );
395 above41 = verparts.size() >= 2 && ( verparts.at( 0 ).toInt() > 4 || ( verparts.at( 0 ).toInt() == 4 && verparts.at( 1 ).toInt() >= 1 ) );
399 sqlite3_free_table( results );
402 char *errMsg =
nullptr;
403 ret = sqlite3_exec( sqlite_handle, above41 ?
"SELECT InitSpatialMetadata(1)" :
"SELECT InitSpatialMetadata()",
nullptr,
nullptr, &errMsg );
405 if ( ret != SQLITE_OK )
407 QString errCause = tr(
"Unable to initialize SpatialMetadata:\n" );
408 errCause += QString::fromUtf8( errMsg );
409 showWarning( errCause );
410 sqlite3_free( errMsg );
413 spatial_ref_sys_init( sqlite_handle, 0 );
415 ( void )sqlite_handle;
419 bool QgsOfflineEditing::createOfflineDb(
const QString &offlineDbPath, ContainerType containerType )
422 char *errMsg =
nullptr;
423 const QFile newDb( offlineDbPath );
424 if ( newDb.exists() )
426 QFile::remove( offlineDbPath );
431 const QFileInfo fullPath = QFileInfo( offlineDbPath );
432 const QDir path = fullPath.dir();
435 QDir().mkpath( path.absolutePath() );
438 const QString dbPath = newDb.fileName();
441 switch ( containerType )
445 OGRSFDriverH hGpkgDriver = OGRGetDriverByName(
"GPKG" );
448 showWarning( tr(
"Creation of database failed. GeoPackage driver not found." ) );
455 showWarning( tr(
"Creation of database failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
467 ret = database.
open_v2( dbPath, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
nullptr );
471 QString errCause = tr(
"Could not create a new database\n" );
473 showWarning( errCause );
477 ret = sqlite3_exec( database.get(),
"PRAGMA foreign_keys = 1",
nullptr,
nullptr, &errMsg );
478 if ( ret != SQLITE_OK )
480 showWarning( tr(
"Unable to activate FOREIGN_KEY constraints" ) );
481 sqlite3_free( errMsg );
484 initializeSpatialMetadata( database.get() );
488 void QgsOfflineEditing::createLoggingTables(
sqlite3 *db )
491 QString sql = QStringLiteral(
"CREATE TABLE 'log_indices' ('name' TEXT, 'last_index' INTEGER)" );
494 sql = QStringLiteral(
"INSERT INTO 'log_indices' VALUES ('commit_no', 0)" );
497 sql = QStringLiteral(
"INSERT INTO 'log_indices' VALUES ('layer_id', 0)" );
501 sql = QStringLiteral(
"CREATE TABLE 'log_layer_ids' ('id' INTEGER, 'qgis_id' TEXT)" );
505 sql = QStringLiteral(
"CREATE TABLE 'log_fids' ('layer_id' INTEGER, 'offline_fid' INTEGER, 'remote_fid' INTEGER, 'remote_pk' TEXT)" );
509 sql = QStringLiteral(
"CREATE TABLE 'log_added_attrs' ('layer_id' INTEGER, 'commit_no' INTEGER, " );
510 sql += QLatin1String(
"'name' TEXT, 'type' INTEGER, 'length' INTEGER, 'precision' INTEGER, 'comment' TEXT)" );
514 sql = QStringLiteral(
"CREATE TABLE 'log_added_features' ('layer_id' INTEGER, 'fid' INTEGER)" );
518 sql = QStringLiteral(
"CREATE TABLE 'log_removed_features' ('layer_id' INTEGER, 'fid' INTEGER)" );
522 sql = QStringLiteral(
"CREATE TABLE 'log_feature_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'attr' INTEGER, 'value' TEXT)" );
526 sql = QStringLiteral(
"CREATE TABLE 'log_geometry_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'geom_wkt' TEXT)" );
534 void QgsOfflineEditing::convertToOfflineLayer(
QgsVectorLayer *layer,
sqlite3 *db,
const QString &offlineDbPath,
bool onlySelected, ContainerType containerType,
const QString &layerNameSuffix )
536 if ( !layer || !layer->
isValid() )
538 QgsDebugMsgLevel( QStringLiteral(
"Layer %1 is invalid and cannot be copied" ).arg( layer ? layer->
id() : QStringLiteral(
"<UNKNOWN>" ) ), 4 );
542 const QString tableName = layer->
id();
543 QgsDebugMsgLevel( QStringLiteral(
"Creating offline table %1 ..." ).arg( tableName ), 4 );
546 std::unique_ptr<QgsVectorLayer> newLayer;
548 switch ( containerType )
552 #ifdef HAVE_SPATIALITE
554 QString sql = QStringLiteral(
"CREATE TABLE '%1' (" ).arg( tableName );
557 for (
const auto &
field : providerFields )
560 const QVariant::Type type =
field.
type();
561 if ( type == QVariant::Int || type == QVariant::LongLong )
563 dataType = QStringLiteral(
"INTEGER" );
565 else if ( type == QVariant::Double )
567 dataType = QStringLiteral(
"REAL" );
569 else if ( type == QVariant::String )
571 dataType = QStringLiteral(
"TEXT" );
573 else if ( type == QVariant::StringList || type == QVariant::List )
575 dataType = QStringLiteral(
"TEXT" );
576 showWarning( tr(
"Field '%1' from layer %2 has been converted from a list to a string of comma-separated values." ).arg(
field.
name(), layer->
name() ) );
580 showWarning( tr(
"%1: Unknown data type %2. Not using type affinity for the field." ).arg(
field.
name(), QVariant::typeToName( type ) ) );
583 sql += delim + QStringLiteral(
"'%1' %2" ).arg(
field.
name(), dataType );
588 int rc = sqlExec( db, sql );
599 geomType = QStringLiteral(
"POINT" );
602 geomType = QStringLiteral(
"MULTIPOINT" );
605 geomType = QStringLiteral(
"LINESTRING" );
608 geomType = QStringLiteral(
"MULTILINESTRING" );
611 geomType = QStringLiteral(
"POLYGON" );
614 geomType = QStringLiteral(
"MULTIPOLYGON" );
621 QString zmInfo = QStringLiteral(
"XY" );
630 if ( layer->
crs().
authid().startsWith( QLatin1String(
"EPSG:" ), Qt::CaseInsensitive ) )
632 epsgCode = layer->
crs().
authid().mid( 5 );
637 showWarning( tr(
"Layer %1 has unsupported Coordinate Reference System (%2)." ).arg( layer->
name(), layer->
crs().
authid() ) );
640 const QString sqlAddGeom = QStringLiteral(
"SELECT AddGeometryColumn('%1', 'Geometry', %2, '%3', '%4')" )
641 .arg( tableName, epsgCode, geomType, zmInfo );
644 const QString sqlCreateIndex = QStringLiteral(
"SELECT CreateSpatialIndex('%1', 'Geometry')" ).arg( tableName );
646 if ( rc == SQLITE_OK )
648 rc = sqlExec( db, sqlAddGeom );
649 if ( rc == SQLITE_OK )
651 rc = sqlExec( db, sqlCreateIndex );
656 if ( rc != SQLITE_OK )
658 showWarning( tr(
"Filling SpatiaLite for layer %1 failed" ).arg( layer->
name() ) );
663 const QString connectionString = QStringLiteral(
"dbname='%1' table='%2'%3 sql=" )
665 tableName, layer->
isSpatial() ?
"(Geometry)" :
"" );
667 newLayer = std::make_unique<QgsVectorLayer>( connectionString,
668 layer->
name() + layerNameSuffix, QStringLiteral(
"spatialite" ), options );
672 showWarning( tr(
"No Spatialite support available" ) );
680 char **options =
nullptr;
682 options = CSLSetNameValue( options,
"OVERWRITE",
"YES" );
683 options = CSLSetNameValue( options,
"IDENTIFIER", tr(
"%1 (offline)" ).arg( layer->
id() ).toUtf8().constData() );
684 options = CSLSetNameValue( options,
"DESCRIPTION", layer->
dataComment().toUtf8().constData() );
687 const QString fidBase( QStringLiteral(
"fid" ) );
688 QString fid = fidBase;
692 fid = fidBase +
'_' + QString::number( counter );
695 if ( counter == 10000 )
697 showWarning( tr(
"Cannot make FID-name for GPKG " ) );
701 options = CSLSetNameValue( options,
"FID", fid.toUtf8().constData() );
705 options = CSLSetNameValue( options,
"GEOMETRY_COLUMN",
"geom" );
706 options = CSLSetNameValue( options,
"SPATIAL_INDEX",
"YES" );
709 OGRSFDriverH hDriver =
nullptr;
712 OGRLayerH hLayer = OGR_DS_CreateLayer( hDS.get(), tableName.toUtf8().constData(), hSRS,
static_cast<OGRwkbGeometryType
>( layer->
wkbType() ), options );
713 CSLDestroy( options );
718 showWarning( tr(
"Creation of layer failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
723 for (
const auto &
field : providerFields )
726 const QVariant::Type type =
field.
type();
727 OGRFieldType ogrType( OFTString );
728 OGRFieldSubType ogrSubType = OFSTNone;
729 if ( type == QVariant::Int )
730 ogrType = OFTInteger;
731 else if ( type == QVariant::LongLong )
732 ogrType = OFTInteger64;
733 else if ( type == QVariant::Double )
735 else if ( type == QVariant::Time )
737 else if ( type == QVariant::Date )
739 else if ( type == QVariant::DateTime )
740 ogrType = OFTDateTime;
741 else if ( type == QVariant::Bool )
743 ogrType = OFTInteger;
744 ogrSubType = OFSTBoolean;
746 else if ( type == QVariant::StringList || type == QVariant::List )
749 ogrSubType = OFSTJSON;
750 showWarning( tr(
"Field '%1' from layer %2 has been converted from a list to a JSON-formatted string value." ).arg( fieldName, layer->
name() ) );
758 OGR_Fld_SetWidth( fld.get(), ogrWidth );
759 if ( ogrSubType != OFSTNone )
760 OGR_Fld_SetSubType( fld.get(), ogrSubType );
762 if ( OGR_L_CreateField( hLayer, fld.get(),
true ) != OGRERR_NONE )
764 showWarning( tr(
"Creation of field %1 failed (OGR error: %2)" )
765 .arg( fieldName, QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
773 OGR_L_ResetReading( hLayer );
774 if ( CPLGetLastErrorType() != CE_None )
776 const QString msg( tr(
"Creation of layer failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
782 const QString uri = QStringLiteral(
"%1|layername=%2|option:QGIS_FORCE_WAL=ON" ).arg( offlineDbPath, tableName );
784 newLayer = std::make_unique<QgsVectorLayer>( uri, layer->
name() + layerNameSuffix, QStringLiteral(
"ogr" ), layerOptions );
789 if ( newLayer && newLayer->isValid() )
793 newLayer->startEditing();
801 if ( !selectedFids.isEmpty() )
815 long long featureCount = 1;
816 const int remotePkIdx = getLayerPkIdx( layer );
818 QList<QgsFeatureId> remoteFeatureIds;
819 QStringList remoteFeaturePks;
822 remoteFeatureIds << f.
id();
823 remoteFeaturePks << ( remotePkIdx >= 0 ? f.
attribute( remotePkIdx ).toString() : QString() );
830 QgsAttributes newAttrs( containerType ==
GPKG ? attrs.count() + 1 : attrs.count() );
831 for (
int it = 0; it < attrs.count(); ++it )
833 QVariant attr = attrs.at( it );
838 newAttrs[column++] = attr;
842 newLayer->addFeature( f );
846 if ( newLayer->commitChanges() )
852 const int layerId = getOrCreateLayerId( db, layer->
id() );
853 QList<QgsFeatureId> offlineFeatureIds;
858 offlineFeatureIds << f.
id();
862 sqlExec( db, QStringLiteral(
"BEGIN" ) );
863 const int remoteCount = remoteFeatureIds.size();
864 for (
int i = 0; i < remoteCount; i++ )
867 if ( i < offlineFeatureIds.count() )
869 addFidLookup( db, layerId, offlineFeatureIds.at( i ), remoteFeatureIds.at( i ), remoteFeaturePks.at( i ) );
873 showWarning( tr(
"Feature cannot be copied to the offline layer, please check if the online layer '%1' is still accessible." ).arg( layer->
name() ) );
878 sqlExec( db, QStringLiteral(
"COMMIT" ) );
882 showWarning( newLayer->commitErrors().join( QLatin1Char(
'\n' ) ) );
896 QStringList notNullFieldNames;
905 layer->
setDataSource( newLayer->source(), newLayer->name(), newLayer->dataProvider()->name() );
917 if ( notNullFieldNames.contains(
field.
name() ) )
919 notNullFieldNames.removeAll(
field.
name() );
930 void QgsOfflineEditing::applyAttributesAdded(
QgsVectorLayer *remoteLayer,
sqlite3 *db,
int layerId,
int commitNo )
932 Q_ASSERT( remoteLayer );
934 const QString sql = QStringLiteral(
"SELECT \"name\", \"type\", \"length\", \"precision\", \"comment\" FROM 'log_added_attrs' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2" ).arg( layerId ).arg( commitNo );
935 QList<QgsField> fields = sqlQueryAttributesAdded( db, sql );
938 const QList<QgsVectorDataProvider::NativeType> nativeTypes = provider->
nativeTypes();
941 QMap < QVariant::Type, QString > typeNameLookup;
942 for (
int i = 0; i < nativeTypes.size(); i++ )
950 for (
int i = 0; i < fields.size(); i++ )
954 if ( typeNameLookup.contains(
field.
type() ) )
962 showWarning( QStringLiteral(
"Could not add attribute '%1' of type %2" ).arg(
field.
name() ).arg(
field.
type() ) );
971 Q_ASSERT( offlineLayer );
972 Q_ASSERT( remoteLayer );
974 const QString sql = QStringLiteral(
"SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
975 const QList<int> featureIdInts = sqlQueryInts( db, sql );
977 for (
const int id : featureIdInts )
997 const int newAttrsCount = remoteLayer->
fields().
count();
998 for ( QgsFeatureList::iterator it = features.begin(); it != features.end(); ++it )
1002 const QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
1005 for (
int it = 0; it < attrs.count(); ++it )
1007 const int remoteAttributeIndex = attrLookup.value( it, -1 );
1009 if ( remoteAttributeIndex == -1 )
1011 QVariant attr = attrs.at( it );
1012 if ( remoteLayer->
fields().
at( remoteAttributeIndex ).
type() == QVariant::StringList )
1014 if ( attr.type() == QVariant::StringList || attr.type() == QVariant::List )
1016 attr = attr.toStringList();
1023 else if ( remoteLayer->
fields().
at( remoteAttributeIndex ).
type() == QVariant::List )
1025 if ( attr.type() == QVariant::StringList || attr.type() == QVariant::List )
1027 attr = attr.toList();
1034 newAttrs[ remoteAttributeIndex ] = attr;
1045 void QgsOfflineEditing::applyFeaturesRemoved(
QgsVectorLayer *remoteLayer,
sqlite3 *db,
int layerId )
1047 Q_ASSERT( remoteLayer );
1049 const QString sql = QStringLiteral(
"SELECT \"fid\" FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( layerId );
1050 const QgsFeatureIds values = sqlQueryFeaturesRemoved( db, sql );
1055 for ( QgsFeatureIds::const_iterator it = values.constBegin(); it != values.constEnd(); ++it )
1057 const QgsFeatureId fid = remoteFid( db, layerId, *it, remoteLayer );
1066 Q_ASSERT( offlineLayer );
1067 Q_ASSERT( remoteLayer );
1069 const QString sql = QStringLiteral(
"SELECT \"fid\", \"attr\", \"value\" FROM 'log_feature_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2 " ).arg( layerId ).arg( commitNo );
1070 const AttributeValueChanges values = sqlQueryAttributeValueChanges( db, sql );
1074 QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
1076 for (
int i = 0; i < values.size(); i++ )
1078 const QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid, remoteLayer );
1079 QgsDebugMsgLevel( QStringLiteral(
"Offline changeAttributeValue %1 = %2" ).arg( attrLookup[ values.at( i ).attr ] ).arg( values.at( i ).value ), 4 );
1081 const int remoteAttributeIndex = attrLookup[ values.at( i ).attr ];
1082 QVariant attr = values.at( i ).value;
1083 if ( remoteLayer->
fields().
at( remoteAttributeIndex ).
type() == QVariant::StringList )
1087 else if ( remoteLayer->
fields().
at( remoteAttributeIndex ).
type() == QVariant::List )
1098 void QgsOfflineEditing::applyGeometryChanges(
QgsVectorLayer *remoteLayer,
sqlite3 *db,
int layerId,
int commitNo )
1100 Q_ASSERT( remoteLayer );
1102 const QString sql = QStringLiteral(
"SELECT \"fid\", \"geom_wkt\" FROM 'log_geometry_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2" ).arg( layerId ).arg( commitNo );
1103 const GeometryChanges values = sqlQueryGeometryChanges( db, sql );
1107 for (
int i = 0; i < values.size(); i++ )
1109 const QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid, remoteLayer );
1119 Q_ASSERT( remoteLayer );
1125 QMap < QgsFeatureId, QString > newRemoteFids;
1132 const int remotePkIdx = getLayerPkIdx( remoteLayer );
1137 if ( offlineFid( db, layerId, f.
id() ) == -1 )
1139 newRemoteFids[ f.
id()] = remotePkIdx >= 0 ? f.
attribute( remotePkIdx ).toString() : QString();
1147 const QString sql = QStringLiteral(
"SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
1148 const QList<int> newOfflineFids = sqlQueryInts( db, sql );
1150 if ( newRemoteFids.size() != newOfflineFids.size() )
1158 sqlExec( db, QStringLiteral(
"BEGIN" ) );
1159 for ( QMap<QgsFeatureId, QString>::const_iterator it = newRemoteFids.constBegin(); it != newRemoteFids.constEnd(); ++it )
1161 addFidLookup( db, layerId, newOfflineFids.at( i++ ), it.key(), it.value() );
1163 sqlExec( db, QStringLiteral(
"COMMIT" ) );
1170 Q_ASSERT( offlineLayer );
1171 Q_ASSERT( remoteLayer );
1175 QMap <
int ,
int > attrLookup;
1178 for (
int i = 0; i < offlineAttrs.size(); i++ )
1187 void QgsOfflineEditing::showWarning(
const QString &message )
1189 emit
warning( tr(
"Offline Editing Plugin" ), message );
1196 if ( !dbPath.isEmpty() )
1199 const int rc = database.
open( absoluteDbPath );
1200 if ( rc != SQLITE_OK )
1202 QgsDebugMsg( QStringLiteral(
"Could not open the SpatiaLite logging database" ) );
1203 showWarning( tr(
"Could not open the SpatiaLite logging database" ) );
1208 QgsDebugMsg( QStringLiteral(
"dbPath is empty!" ) );
1213 int QgsOfflineEditing::getOrCreateLayerId(
sqlite3 *db,
const QString &qgisLayerId )
1215 QString sql = QStringLiteral(
"SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( qgisLayerId );
1216 int layerId = sqlQueryInt( db, sql, -1 );
1217 if ( layerId == -1 )
1220 sql = QStringLiteral(
"SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'layer_id'" );
1221 const int newLayerId = sqlQueryInt( db, sql, -1 );
1224 sql = QStringLiteral(
"INSERT INTO 'log_layer_ids' VALUES (%1, '%2')" ).arg( newLayerId ).arg( qgisLayerId );
1229 sql = QStringLiteral(
"UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'layer_id'" ).arg( newLayerId + 1 );
1232 layerId = newLayerId;
1238 int QgsOfflineEditing::getCommitNo(
sqlite3 *db )
1240 const QString sql = QStringLiteral(
"SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'commit_no'" );
1241 return sqlQueryInt( db, sql, -1 );
1244 void QgsOfflineEditing::increaseCommitNo(
sqlite3 *db )
1246 const QString sql = QStringLiteral(
"UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'commit_no'" ).arg( getCommitNo( db ) + 1 );
1252 const QString sql = QStringLiteral(
"INSERT INTO 'log_fids' VALUES ( %1, %2, %3, %4 )" ).arg( layerId ).arg( offlineFid ).arg( remoteFid ).arg( sqlEscape( remotePk ) );
1258 const int pkIdx = getLayerPkIdx( remoteLayer );
1262 const QString sql = QStringLiteral(
"SELECT \"remote_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"offline_fid\" = %2" ).arg( layerId ).arg( offlineFid );
1263 return sqlQueryInt( db, sql, -1 );
1266 const QString sql = QStringLiteral(
"SELECT \"remote_pk\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"offline_fid\" = %2" ).arg( layerId ).arg( offlineFid );
1267 QString defaultValue;
1268 const QString pkValue = sqlQueryStr( db, sql, defaultValue );
1270 if ( pkValue.isNull() )
1275 const QString pkFieldName = remoteLayer->
fields().
at( pkIdx ).
name();
1286 const QString sql = QStringLiteral(
"SELECT \"offline_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"remote_fid\" = %2" ).arg( layerId ).arg( remoteFid );
1287 return sqlQueryInt( db, sql, -1 );
1292 const QString sql = QStringLiteral(
"SELECT COUNT(\"fid\") FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( fid );
1293 return ( sqlQueryInt( db, sql, 0 ) > 0 );
1296 int QgsOfflineEditing::sqlExec(
sqlite3 *db,
const QString &sql )
1298 char *errmsg =
nullptr;
1299 const int rc = sqlite3_exec( db, sql.toUtf8(),
nullptr,
nullptr, &errmsg );
1300 if ( rc != SQLITE_OK )
1302 showWarning( errmsg );
1307 QString QgsOfflineEditing::sqlQueryStr(
sqlite3 *db,
const QString &sql, QString &defaultValue )
1309 sqlite3_stmt *stmt =
nullptr;
1310 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt,
nullptr ) != SQLITE_OK )
1312 showWarning( sqlite3_errmsg( db ) );
1313 return defaultValue;
1316 QString value = defaultValue;
1317 const int ret = sqlite3_step( stmt );
1318 if ( ret == SQLITE_ROW )
1320 value = QString(
reinterpret_cast< const char *
>( sqlite3_column_text( stmt, 0 ) ) );
1322 sqlite3_finalize( stmt );
1327 int QgsOfflineEditing::sqlQueryInt(
sqlite3 *db,
const QString &sql,
int defaultValue )
1329 sqlite3_stmt *stmt =
nullptr;
1330 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt,
nullptr ) != SQLITE_OK )
1332 showWarning( sqlite3_errmsg( db ) );
1333 return defaultValue;
1336 int value = defaultValue;
1337 const int ret = sqlite3_step( stmt );
1338 if ( ret == SQLITE_ROW )
1340 value = sqlite3_column_int( stmt, 0 );
1342 sqlite3_finalize( stmt );
1347 QList<int> QgsOfflineEditing::sqlQueryInts(
sqlite3 *db,
const QString &sql )
1351 sqlite3_stmt *stmt =
nullptr;
1352 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt,
nullptr ) != SQLITE_OK )
1354 showWarning( sqlite3_errmsg( db ) );
1358 int ret = sqlite3_step( stmt );
1359 while ( ret == SQLITE_ROW )
1361 values << sqlite3_column_int( stmt, 0 );
1363 ret = sqlite3_step( stmt );
1365 sqlite3_finalize( stmt );
1370 QList<QgsField> QgsOfflineEditing::sqlQueryAttributesAdded(
sqlite3 *db,
const QString &sql )
1372 QList<QgsField> values;
1374 sqlite3_stmt *stmt =
nullptr;
1375 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt,
nullptr ) != SQLITE_OK )
1377 showWarning( sqlite3_errmsg( db ) );
1381 int ret = sqlite3_step( stmt );
1382 while ( ret == SQLITE_ROW )
1384 const QgsField field( QString(
reinterpret_cast< const char *
>( sqlite3_column_text( stmt, 0 ) ) ),
1385 static_cast< QVariant::Type
>( sqlite3_column_int( stmt, 1 ) ),
1387 sqlite3_column_int( stmt, 2 ),
1388 sqlite3_column_int( stmt, 3 ),
1389 QString(
reinterpret_cast< const char *
>( sqlite3_column_text( stmt, 4 ) ) ) );
1392 ret = sqlite3_step( stmt );
1394 sqlite3_finalize( stmt );
1403 sqlite3_stmt *stmt =
nullptr;
1404 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt,
nullptr ) != SQLITE_OK )
1406 showWarning( sqlite3_errmsg( db ) );
1410 int ret = sqlite3_step( stmt );
1411 while ( ret == SQLITE_ROW )
1413 values << sqlite3_column_int( stmt, 0 );
1415 ret = sqlite3_step( stmt );
1417 sqlite3_finalize( stmt );
1422 QgsOfflineEditing::AttributeValueChanges QgsOfflineEditing::sqlQueryAttributeValueChanges(
sqlite3 *db,
const QString &sql )
1424 AttributeValueChanges values;
1426 sqlite3_stmt *stmt =
nullptr;
1427 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt,
nullptr ) != SQLITE_OK )
1429 showWarning( sqlite3_errmsg( db ) );
1433 int ret = sqlite3_step( stmt );
1434 while ( ret == SQLITE_ROW )
1436 AttributeValueChange change;
1437 change.fid = sqlite3_column_int( stmt, 0 );
1438 change.attr = sqlite3_column_int( stmt, 1 );
1439 change.value = QString(
reinterpret_cast< const char *
>( sqlite3_column_text( stmt, 2 ) ) );
1442 ret = sqlite3_step( stmt );
1444 sqlite3_finalize( stmt );
1449 QgsOfflineEditing::GeometryChanges QgsOfflineEditing::sqlQueryGeometryChanges(
sqlite3 *db,
const QString &sql )
1451 GeometryChanges values;
1453 sqlite3_stmt *stmt =
nullptr;
1454 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt,
nullptr ) != SQLITE_OK )
1456 showWarning( sqlite3_errmsg( db ) );
1460 int ret = sqlite3_step( stmt );
1461 while ( ret == SQLITE_ROW )
1463 GeometryChange change;
1464 change.fid = sqlite3_column_int( stmt, 0 );
1465 change.geom_wkt = QString(
reinterpret_cast< const char *
>( sqlite3_column_text( stmt, 1 ) ) );
1468 ret = sqlite3_step( stmt );
1470 sqlite3_finalize( stmt );
1475 void QgsOfflineEditing::committedAttributesAdded(
const QString &qgisLayerId,
const QList<QgsField> &addedAttributes )
1482 const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1483 const int commitNo = getCommitNo( database.get() );
1487 const QString sql = QStringLiteral(
"INSERT INTO 'log_added_attrs' VALUES ( %1, %2, '%3', %4, %5, %6, '%7' )" )
1495 sqlExec( database.get(), sql );
1498 increaseCommitNo( database.get() );
1501 void QgsOfflineEditing::committedFeaturesAdded(
const QString &qgisLayerId,
const QgsFeatureList &addedFeatures )
1508 const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1512 const QString dataSourceString = layer->
source();
1518 if ( !offlinePath.contains(
".gpkg" ) )
1520 tableName = uri.
table();
1525 const QVariantMap decodedUri = ogrProviderMetaData->
decodeUri( dataSourceString );
1526 tableName = decodedUri.value( QStringLiteral(
"layerName" ) ).toString();
1527 if ( tableName.isEmpty() )
1529 showWarning( tr(
"Could not deduce table name from data source %1." ).arg( dataSourceString ) );
1534 const QString sql = QStringLiteral(
"SELECT ROWID FROM '%1' ORDER BY ROWID DESC LIMIT %2" ).arg( tableName ).arg( addedFeatures.size() );
1535 const QList<int> newFeatureIds = sqlQueryInts( database.get(), sql );
1536 for (
int i = newFeatureIds.size() - 1; i >= 0; i-- )
1538 const QString sql = QStringLiteral(
"INSERT INTO 'log_added_features' VALUES ( %1, %2 )" )
1540 .arg( newFeatureIds.at( i ) );
1541 sqlExec( database.get(), sql );
1545 void QgsOfflineEditing::committedFeaturesRemoved(
const QString &qgisLayerId,
const QgsFeatureIds &deletedFeatureIds )
1552 const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1556 if ( isAddedFeature( database.get(), layerId,
id ) )
1559 const QString sql = QStringLiteral(
"DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg(
id );
1560 sqlExec( database.get(), sql );
1564 const QString sql = QStringLiteral(
"INSERT INTO 'log_removed_features' VALUES ( %1, %2)" )
1567 sqlExec( database.get(), sql );
1572 void QgsOfflineEditing::committedAttributeValuesChanges(
const QString &qgisLayerId,
const QgsChangedAttributesMap &changedAttrsMap )
1579 const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1580 const int commitNo = getCommitNo( database.get() );
1582 for ( QgsChangedAttributesMap::const_iterator cit = changedAttrsMap.begin(); cit != changedAttrsMap.end(); ++cit )
1585 if ( isAddedFeature( database.get(), layerId, fid ) )
1591 for ( QgsAttributeMap::const_iterator it = attrMap.constBegin(); it != attrMap.constEnd(); ++it )
1593 QString value = it.value().type() == QVariant::StringList || it.value().type() == QVariant::List ?
QgsJsonUtils::encodeValue( it.value() ) : it.value().toString();
1594 value.replace( QLatin1String(
"'" ), QLatin1String(
"''" ) );
1595 const QString sql = QStringLiteral(
"INSERT INTO 'log_feature_updates' VALUES ( %1, %2, %3, %4, '%5' )" )
1601 sqlExec( database.get(), sql );
1605 increaseCommitNo( database.get() );
1608 void QgsOfflineEditing::committedGeometriesChanges(
const QString &qgisLayerId,
const QgsGeometryMap &changedGeometries )
1615 const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1616 const int commitNo = getCommitNo( database.get() );
1618 for ( QgsGeometryMap::const_iterator it = changedGeometries.begin(); it != changedGeometries.end(); ++it )
1621 if ( isAddedFeature( database.get(), layerId, fid ) )
1627 const QString sql = QStringLiteral(
"INSERT INTO 'log_geometry_updates' VALUES ( %1, %2, %3, '%4' )" )
1631 .arg( geom.
asWkt() );
1632 sqlExec( database.get(), sql );
1637 increaseCommitNo( database.get() );
1640 void QgsOfflineEditing::startListenFeatureChanges()
1642 QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1651 this, &QgsOfflineEditing::committedAttributesAdded );
1653 this, &QgsOfflineEditing::committedAttributeValuesChanges );
1655 this, &QgsOfflineEditing::committedGeometriesChanges );
1658 this, &QgsOfflineEditing::committedFeaturesAdded );
1660 this, &QgsOfflineEditing::committedFeaturesRemoved );
1663 void QgsOfflineEditing::stopListenFeatureChanges()
1665 QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1674 this, &QgsOfflineEditing::committedAttributesAdded );
1676 this, &QgsOfflineEditing::committedAttributeValuesChanges );
1678 this, &QgsOfflineEditing::committedGeometriesChanges );
1681 this, &QgsOfflineEditing::committedFeaturesAdded );
1683 this, &QgsOfflineEditing::committedFeaturesRemoved );
1686 void QgsOfflineEditing::setupLayer(
QgsMapLayer *layer )
1690 if (
QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( layer ) )
1701 int QgsOfflineEditing::getLayerPkIdx(
const QgsVectorLayer *layer )
const
1704 if ( pkAttrs.length() == 1 )
1707 const QVariant::Type pkType = pkField.
type();
1709 if ( pkType == QVariant::String )
1718 QString QgsOfflineEditing::sqlEscape( QString value )
const
1720 if ( value.isNull() )
1721 return QStringLiteral(
"NULL" );
1723 value.replace(
"'",
"''" );
1725 return QStringLiteral(
"'%1'" ).arg( value );