46 #include <QDomDocument>
49 #include <QMessageBox>
51 #include <ogr_srs_api.h>
56 #include <spatialite.h>
59 #define CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE "isOfflineEditable"
60 #define CUSTOM_PROPERTY_REMOTE_SOURCE "remoteSource"
61 #define CUSTOM_PROPERTY_REMOTE_PROVIDER "remoteProvider"
62 #define CUSTOM_SHOW_FEATURE_COUNT "showFeatureCount"
63 #define CUSTOM_PROPERTY_ORIGINAL_LAYERID "remoteLayerId"
64 #define PROJECT_ENTRY_SCOPE_OFFLINE "OfflineEditingPlugin"
65 #define PROJECT_ENTRY_KEY_OFFLINE_DB_PATH "/OfflineDbPath"
90 if ( layerIds.isEmpty() )
95 QString dbPath = QDir( offlineDataPath ).absoluteFilePath( offlineDbFile );
96 if ( createOfflineDb( dbPath, containerType ) )
99 int rc = database.
open( dbPath );
100 if ( rc != SQLITE_OK )
102 showWarning( tr(
"Could not open the SpatiaLite database" ) );
107 createLoggingTables( database.get() );
111 QMap<QString, QgsVectorJoinList > joinInfoBuffer;
112 QMap<QString, QgsVectorLayer *> layerIdMapping;
114 for (
const QString &layerId : layerIds )
126 QgsVectorJoinList::iterator joinIt = joins.begin();
127 while ( joinIt != joins.end() )
129 if ( joinIt->prefix().isNull() )
134 joinIt->setPrefix( vl->
name() +
'_' );
138 joinInfoBuffer.insert( vl->
id(), joins );
144 for (
int i = 0; i < layerIds.count(); i++ )
152 QString origLayerId = vl->
id();
153 QgsVectorLayer *newLayer = copyVectorLayer( vl, database.get(), dbPath, onlySelected, containerType );
156 layerIdMapping.insert( origLayerId, newLayer );
159 snappingConfig.
removeLayers( QList<QgsMapLayer *>() << vl );
163 QStringList() << origLayerId );
171 QMap<QString, QgsVectorJoinList >::ConstIterator it;
172 for ( it = joinInfoBuffer.constBegin(); it != joinInfoBuffer.constEnd(); ++it )
178 const QList<QgsVectorLayerJoinInfo> joins = it.value();
181 QgsVectorLayer *newJoinedLayer = layerIdMapping.value( join.joinLayerId() );
182 if ( newJoinedLayer )
185 join.setJoinLayer( newJoinedLayer );
196 if ( projectTitle.isEmpty() )
200 projectTitle += QLatin1String(
" (offline)" );
231 QList<QgsMapLayer *> offlineLayers;
233 for ( QMap<QString, QgsMapLayer *>::iterator layer_it = mapLayers.begin() ; layer_it != mapLayers.end(); ++layer_it )
238 offlineLayers << layer;
242 QgsDebugMsgLevel( QStringLiteral(
"Found %1 offline layers" ).arg( offlineLayers.count() ), 4 );
243 for (
int l = 0; l < offlineLayers.count(); l++ )
251 QString remoteName = layer->
name();
252 remoteName.remove( QRegExp(
" \\(offline\\)$" ) );
258 if ( remoteLayer->
providerType().contains( QLatin1String(
"WFS" ), Qt::CaseInsensitive ) )
268 QgsVectorLayer *offlineLayer = qobject_cast<QgsVectorLayer *>( layer );
274 copySymbology( offlineLayer, remoteLayer );
275 updateRelations( offlineLayer, remoteLayer );
276 updateMapThemes( offlineLayer, remoteLayer );
277 updateLayerOrder( offlineLayer, remoteLayer );
281 snappingConfig.
removeLayers( QList<QgsMapLayer *>() << offlineLayer );
289 QString qgisLayerId = layer->
id();
290 QString sql = QStringLiteral(
"SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( qgisLayerId );
291 int layerId = sqlQueryInt( database.get(), sql, -1 );
297 int commitNo = getCommitNo( database.get() );
298 QgsDebugMsgLevel( QStringLiteral(
"Found %1 commits" ).arg( commitNo ), 4 );
299 for (
int i = 0; i < commitNo; i++ )
303 applyAttributesAdded( remoteLayer, database.get(), layerId, i );
304 applyAttributeValueChanges( offlineLayer, remoteLayer, database.get(), layerId, i );
305 applyGeometryChanges( remoteLayer, database.get(), layerId, i );
308 applyFeaturesAdded( offlineLayer, remoteLayer, database.get(), layerId );
309 applyFeaturesRemoved( remoteLayer, database.get(), layerId );
314 updateFidLookup( remoteLayer, database.get(), layerId );
317 sql = QStringLiteral(
"DELETE FROM 'log_added_attrs' WHERE \"layer_id\" = %1" ).arg( layerId );
318 sqlExec( database.get(), sql );
319 sql = QStringLiteral(
"DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
320 sqlExec( database.get(), sql );
321 sql = QStringLiteral(
"DELETE FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( layerId );
322 sqlExec( database.get(), sql );
323 sql = QStringLiteral(
"DELETE FROM 'log_feature_updates' WHERE \"layer_id\" = %1" ).arg( layerId );
324 sqlExec( database.get(), sql );
325 sql = QStringLiteral(
"DELETE FROM 'log_geometry_updates' WHERE \"layer_id\" = %1" ).arg( layerId );
326 sqlExec( database.get(), sql );
330 showWarning( remoteLayer->
commitErrors().join( QLatin1Char(
'\n' ) ) );
335 QgsDebugMsg( QStringLiteral(
"Could not find the layer id in the edit logs!" ) );
346 projectTitle.remove( QRegExp(
" \\(offline\\)$" ) );
353 QgsDebugMsg( QStringLiteral(
"Remote layer is not valid!" ) );
358 QString sql = QStringLiteral(
"UPDATE 'log_indices' SET 'last_index' = 0 WHERE \"name\" = 'commit_no'" );
359 sqlExec( database.get(), sql );
366 void QgsOfflineEditing::initializeSpatialMetadata(
sqlite3 *sqlite_handle )
369 if ( !sqlite_handle )
372 char **results =
nullptr;
374 int ret = sqlite3_get_table( sqlite_handle,
"select count(*) from sqlite_master", &results, &rows, &columns,
nullptr );
375 if ( ret != SQLITE_OK )
380 for (
int i = 1; i <= rows; i++ )
381 count = atoi( results[( i * columns ) + 0] );
384 sqlite3_free_table( results );
389 bool above41 =
false;
390 ret = sqlite3_get_table( sqlite_handle,
"select spatialite_version()", &results, &rows, &columns,
nullptr );
391 if ( ret == SQLITE_OK && rows == 1 && columns == 1 )
393 QString version = QString::fromUtf8( results[1] );
394 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
395 QStringList parts = version.split(
' ', QString::SkipEmptyParts );
397 QStringList parts = version.split(
' ', Qt::SkipEmptyParts );
399 if ( !parts.empty() )
401 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
402 QStringList verparts = parts.at( 0 ).split(
'.', QString::SkipEmptyParts );
404 QStringList verparts = parts.at( 0 ).split(
'.', Qt::SkipEmptyParts );
406 above41 = verparts.size() >= 2 && ( verparts.at( 0 ).toInt() > 4 || ( verparts.at( 0 ).toInt() == 4 && verparts.at( 1 ).toInt() >= 1 ) );
410 sqlite3_free_table( results );
413 char *errMsg =
nullptr;
414 ret = sqlite3_exec( sqlite_handle, above41 ?
"SELECT InitSpatialMetadata(1)" :
"SELECT InitSpatialMetadata()",
nullptr,
nullptr, &errMsg );
416 if ( ret != SQLITE_OK )
418 QString errCause = tr(
"Unable to initialize SpatialMetadata:\n" );
419 errCause += QString::fromUtf8( errMsg );
420 showWarning( errCause );
421 sqlite3_free( errMsg );
424 spatial_ref_sys_init( sqlite_handle, 0 );
427 bool QgsOfflineEditing::createOfflineDb(
const QString &offlineDbPath, ContainerType containerType )
430 char *errMsg =
nullptr;
431 QFile newDb( offlineDbPath );
432 if ( newDb.exists() )
434 QFile::remove( offlineDbPath );
439 QFileInfo fullPath = QFileInfo( offlineDbPath );
440 QDir path = fullPath.dir();
443 QDir().mkpath( path.absolutePath() );
446 QString dbPath = newDb.fileName();
449 switch ( containerType )
453 OGRSFDriverH hGpkgDriver = OGRGetDriverByName(
"GPKG" );
456 showWarning( tr(
"Creation of database failed. GeoPackage driver not found." ) );
463 showWarning( tr(
"Creation of database failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
475 ret = database.
open_v2( dbPath, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
nullptr );
479 QString errCause = tr(
"Could not create a new database\n" );
481 showWarning( errCause );
485 ret = sqlite3_exec( database.get(),
"PRAGMA foreign_keys = 1",
nullptr,
nullptr, &errMsg );
486 if ( ret != SQLITE_OK )
488 showWarning( tr(
"Unable to activate FOREIGN_KEY constraints" ) );
489 sqlite3_free( errMsg );
492 initializeSpatialMetadata( database.get() );
496 void QgsOfflineEditing::createLoggingTables(
sqlite3 *db )
499 QString sql = QStringLiteral(
"CREATE TABLE 'log_indices' ('name' TEXT, 'last_index' INTEGER)" );
502 sql = QStringLiteral(
"INSERT INTO 'log_indices' VALUES ('commit_no', 0)" );
505 sql = QStringLiteral(
"INSERT INTO 'log_indices' VALUES ('layer_id', 0)" );
509 sql = QStringLiteral(
"CREATE TABLE 'log_layer_ids' ('id' INTEGER, 'qgis_id' TEXT)" );
513 sql = QStringLiteral(
"CREATE TABLE 'log_fids' ('layer_id' INTEGER, 'offline_fid' INTEGER, 'remote_fid' INTEGER)" );
517 sql = QStringLiteral(
"CREATE TABLE 'log_added_attrs' ('layer_id' INTEGER, 'commit_no' INTEGER, " );
518 sql += QLatin1String(
"'name' TEXT, 'type' INTEGER, 'length' INTEGER, 'precision' INTEGER, 'comment' TEXT)" );
522 sql = QStringLiteral(
"CREATE TABLE 'log_added_features' ('layer_id' INTEGER, 'fid' INTEGER)" );
526 sql = QStringLiteral(
"CREATE TABLE 'log_removed_features' ('layer_id' INTEGER, 'fid' INTEGER)" );
530 sql = QStringLiteral(
"CREATE TABLE 'log_feature_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'attr' INTEGER, 'value' TEXT)" );
534 sql = QStringLiteral(
"CREATE TABLE 'log_geometry_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'geom_wkt' TEXT)" );
547 QString tableName = layer->
id();
548 QgsDebugMsgLevel( QStringLiteral(
"Creating offline table %1 ..." ).arg( tableName ), 4 );
553 switch ( containerType )
558 QString sql = QStringLiteral(
"CREATE TABLE '%1' (" ).arg( tableName );
561 for (
const auto &
field : providerFields )
565 if ( type == QVariant::Int || type == QVariant::LongLong )
567 dataType = QStringLiteral(
"INTEGER" );
569 else if ( type == QVariant::Double )
571 dataType = QStringLiteral(
"REAL" );
573 else if ( type == QVariant::String )
575 dataType = QStringLiteral(
"TEXT" );
579 showWarning( tr(
"%1: Unknown data type %2. Not using type affinity for the field." ).arg(
field.
name(), QVariant::typeToName( type ) ) );
582 sql += delim + QStringLiteral(
"'%1' %2" ).arg(
field.
name(), dataType );
587 int rc = sqlExec( db, sql );
598 geomType = QStringLiteral(
"POINT" );
601 geomType = QStringLiteral(
"MULTIPOINT" );
604 geomType = QStringLiteral(
"LINESTRING" );
607 geomType = QStringLiteral(
"MULTILINESTRING" );
610 geomType = QStringLiteral(
"POLYGON" );
613 geomType = QStringLiteral(
"MULTIPOLYGON" );
620 QString zmInfo = QStringLiteral(
"XY" );
629 if ( layer->
crs().
authid().startsWith( QLatin1String(
"EPSG:" ), Qt::CaseInsensitive ) )
631 epsgCode = layer->
crs().
authid().mid( 5 );
636 showWarning( tr(
"Layer %1 has unsupported Coordinate Reference System (%2)." ).arg( layer->
name(), layer->
crs().
authid() ) );
639 QString sqlAddGeom = QStringLiteral(
"SELECT AddGeometryColumn('%1', 'Geometry', %2, '%3', '%4')" )
640 .arg( tableName, epsgCode, geomType, zmInfo );
643 QString sqlCreateIndex = QStringLiteral(
"SELECT CreateSpatialIndex('%1', 'Geometry')" ).arg( tableName );
645 if ( rc == SQLITE_OK )
647 rc = sqlExec( db, sqlAddGeom );
648 if ( rc == SQLITE_OK )
650 rc = sqlExec( db, sqlCreateIndex );
655 if ( rc != SQLITE_OK )
657 showWarning( tr(
"Filling SpatiaLite for layer %1 failed" ).arg( layer->
name() ) );
662 QString connectionString = QStringLiteral(
"dbname='%1' table='%2'%3 sql=" )
664 tableName, layer->
isSpatial() ?
"(Geometry)" :
"" );
667 layer->
name() +
" (offline)", QStringLiteral(
"spatialite" ), options );
673 char **options =
nullptr;
675 options = CSLSetNameValue( options,
"OVERWRITE",
"YES" );
676 options = CSLSetNameValue( options,
"IDENTIFIER", tr(
"%1 (offline)" ).arg( layer->
id() ).toUtf8().constData() );
677 options = CSLSetNameValue( options,
"DESCRIPTION", layer->
dataComment().toUtf8().constData() );
680 QString fidBase( QStringLiteral(
"fid" ) );
681 QString fid = fidBase;
685 fid = fidBase +
'_' + QString::number( counter );
688 if ( counter == 10000 )
690 showWarning( tr(
"Cannot make FID-name for GPKG " ) );
694 options = CSLSetNameValue( options,
"FID", fid.toUtf8().constData() );
698 options = CSLSetNameValue( options,
"GEOMETRY_COLUMN",
"geom" );
699 options = CSLSetNameValue( options,
"SPATIAL_INDEX",
"YES" );
702 OGRSFDriverH hDriver =
nullptr;
705 OGRLayerH hLayer = OGR_DS_CreateLayer( hDS.get(), tableName.toUtf8().constData(), hSRS,
static_cast<OGRwkbGeometryType
>( layer->
wkbType() ), options );
706 CSLDestroy( options );
711 showWarning( tr(
"Creation of layer failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
716 for (
const auto &
field : providerFields )
719 const QVariant::Type type =
field.
type();
720 OGRFieldType ogrType( OFTString );
721 OGRFieldSubType ogrSubType = OFSTNone;
722 if ( type == QVariant::Int )
723 ogrType = OFTInteger;
724 else if ( type == QVariant::LongLong )
725 ogrType = OFTInteger64;
726 else if ( type == QVariant::Double )
728 else if ( type == QVariant::Time )
730 else if ( type == QVariant::Date )
732 else if ( type == QVariant::DateTime )
733 ogrType = OFTDateTime;
734 else if ( type == QVariant::Bool )
736 ogrType = OFTInteger;
737 ogrSubType = OFSTBoolean;
745 OGR_Fld_SetWidth( fld.get(), ogrWidth );
746 if ( ogrSubType != OFSTNone )
747 OGR_Fld_SetSubType( fld.get(), ogrSubType );
749 if ( OGR_L_CreateField( hLayer, fld.get(),
true ) != OGRERR_NONE )
751 showWarning( tr(
"Creation of field %1 failed (OGR error: %2)" )
752 .arg( fieldName, QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
760 OGR_L_ResetReading( hLayer );
761 if ( CPLGetLastErrorType() != CE_None )
763 QString msg( tr(
"Creation of layer failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
769 QString uri = QStringLiteral(
"%1|layername=%2" ).arg( offlineDbPath, tableName );
771 newLayer =
new QgsVectorLayer( uri, layer->
name() +
" (offline)", QStringLiteral(
"ogr" ), layerOptions );
788 if ( !selectedFids.isEmpty() )
802 int featureCount = 1;
804 QList<QgsFeatureId> remoteFeatureIds;
807 remoteFeatureIds << f.
id();
814 QgsAttributes newAttrs( containerType ==
GPKG ? attrs.count() + 1 : attrs.count() );
815 for (
int it = 0; it < attrs.count(); ++it )
817 newAttrs[column++] = attrs.at( it );
831 int layerId = getOrCreateLayerId( db, newLayer->
id() );
832 QList<QgsFeatureId> offlineFeatureIds;
837 offlineFeatureIds << f.
id();
841 sqlExec( db, QStringLiteral(
"BEGIN" ) );
842 int remoteCount = remoteFeatureIds.size();
843 for (
int i = 0; i < remoteCount; i++ )
846 if ( i < offlineFeatureIds.count() )
848 addFidLookup( db, layerId, offlineFeatureIds.at( i ), remoteFeatureIds.at( i ) );
852 showWarning( tr(
"Feature cannot be copied to the offline layer, please check if the online layer '%1' is still accessible." ).arg( layer->
name() ) );
857 sqlExec( db, QStringLiteral(
"COMMIT" ) );
861 showWarning( newLayer->
commitErrors().join( QLatin1Char(
'\n' ) ) );
877 QList<QgsMapLayer *>() << newLayer );
880 copySymbology( layer, newLayer );
883 const auto fields = layer->
fields();
895 if ( layerTreeLayer )
898 if ( parentTreeGroup )
900 int index = parentTreeGroup->
children().indexOf( layerTreeLayer );
903 if ( newLayerTreeLayer )
917 updateRelations( layer, newLayer );
918 updateMapThemes( layer, newLayer );
919 updateLayerOrder( layer, newLayer );
927 void QgsOfflineEditing::applyAttributesAdded(
QgsVectorLayer *remoteLayer,
sqlite3 *db,
int layerId,
int commitNo )
929 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 );
930 QList<QgsField> fields = sqlQueryAttributesAdded( db, sql );
933 QList<QgsVectorDataProvider::NativeType> nativeTypes = provider->
nativeTypes();
936 QMap < QVariant::Type, QString > typeNameLookup;
937 for (
int i = 0; i < nativeTypes.size(); i++ )
945 for (
int i = 0; i < fields.size(); i++ )
949 if ( typeNameLookup.contains(
field.
type() ) )
957 showWarning( QStringLiteral(
"Could not add attribute '%1' of type %2" ).arg(
field.
name() ).arg(
field.
type() ) );
966 QString sql = QStringLiteral(
"SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
967 const QList<int> featureIdInts = sqlQueryInts( db, sql );
969 for (
int id : featureIdInts )
990 for ( QgsFeatureList::iterator it = features.begin(); it != features.end(); ++it )
994 QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
997 for (
int it = 0; it < attrs.count(); ++it )
999 newAttrs[ attrLookup[ it ] ] = attrs.at( it );
1010 void QgsOfflineEditing::applyFeaturesRemoved(
QgsVectorLayer *remoteLayer,
sqlite3 *db,
int layerId )
1012 QString sql = QStringLiteral(
"SELECT \"fid\" FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( layerId );
1018 for ( QgsFeatureIds::const_iterator it = values.constBegin(); it != values.constEnd(); ++it )
1029 QString sql = QStringLiteral(
"SELECT \"fid\", \"attr\", \"value\" FROM 'log_feature_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2 " ).arg( layerId ).arg( commitNo );
1030 AttributeValueChanges values = sqlQueryAttributeValueChanges( db, sql );
1034 QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
1036 for (
int i = 0; i < values.size(); i++ )
1038 QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid );
1039 QgsDebugMsgLevel( QStringLiteral(
"Offline changeAttributeValue %1 = %2" ).arg( QString( attrLookup[ values.at( i ).attr ] ), values.at( i ).value ), 4 );
1040 remoteLayer->
changeAttributeValue( fid, attrLookup[ values.at( i ).attr ], values.at( i ).value );
1046 void QgsOfflineEditing::applyGeometryChanges(
QgsVectorLayer *remoteLayer,
sqlite3 *db,
int layerId,
int commitNo )
1048 QString sql = QStringLiteral(
"SELECT \"fid\", \"geom_wkt\" FROM 'log_geometry_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2" ).arg( layerId ).arg( commitNo );
1049 GeometryChanges values = sqlQueryGeometryChanges( db, sql );
1053 for (
int i = 0; i < values.size(); i++ )
1055 QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid );
1079 if ( offlineFid( db, layerId, f.
id() ) == -1 )
1081 newRemoteFids[ f.
id()] =
true;
1089 QString sql = QStringLiteral(
"SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
1090 QList<int> newOfflineFids = sqlQueryInts( db, sql );
1092 if ( newRemoteFids.size() != newOfflineFids.size() )
1100 sqlExec( db, QStringLiteral(
"BEGIN" ) );
1101 for ( QMap<QgsFeatureId, bool>::const_iterator it = newRemoteFids.constBegin(); it != newRemoteFids.constEnd(); ++it )
1103 addFidLookup( db, layerId, newOfflineFids.at( i++ ), it.key() );
1105 sqlExec( db, QStringLiteral(
"COMMIT" ) );
1119 if ( error.isEmpty() )
1123 if ( !error.isEmpty() )
1125 showWarning( error );
1132 const QList<QgsRelation> referencedRelations = relationManager->
referencedRelations( sourceLayer );
1134 for (
QgsRelation relation : referencedRelations )
1137 relation.setReferencedLayer( targetLayer->
id() );
1141 const QList<QgsRelation> referencingRelations = relationManager->
referencingRelations( sourceLayer );
1143 for (
QgsRelation relation : referencingRelations )
1146 relation.setReferencingLayer( targetLayer->
id() );
1154 const QStringList mapThemeNames = mapThemeCollection->
mapThemes();
1156 for (
const QString &mapThemeName : mapThemeNames )
1164 if ( layerRecord.layer() == sourceLayer )
1166 layerRecord.setLayer( targetLayer );
1180 auto iterator = layerOrder.begin();
1182 while ( iterator != layerOrder.end() )
1184 if ( *iterator == targetLayer )
1186 iterator = layerOrder.erase( iterator );
1187 if ( iterator == layerOrder.end() )
1191 if ( *iterator == sourceLayer )
1193 *iterator = targetLayer;
1207 QMap <
int ,
int > attrLookup;
1210 for (
int i = 0; i < offlineAttrs.size(); i++ )
1219 void QgsOfflineEditing::showWarning(
const QString &message )
1221 emit
warning( tr(
"Offline Editing Plugin" ), message );
1228 if ( !dbPath.isEmpty() )
1231 int rc = database.
open( absoluteDbPath );
1232 if ( rc != SQLITE_OK )
1234 QgsDebugMsg( QStringLiteral(
"Could not open the SpatiaLite logging database" ) );
1235 showWarning( tr(
"Could not open the SpatiaLite logging database" ) );
1240 QgsDebugMsg( QStringLiteral(
"dbPath is empty!" ) );
1245 int QgsOfflineEditing::getOrCreateLayerId(
sqlite3 *db,
const QString &qgisLayerId )
1247 QString sql = QStringLiteral(
"SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( qgisLayerId );
1248 int layerId = sqlQueryInt( db, sql, -1 );
1249 if ( layerId == -1 )
1252 sql = QStringLiteral(
"SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'layer_id'" );
1253 int newLayerId = sqlQueryInt( db, sql, -1 );
1256 sql = QStringLiteral(
"INSERT INTO 'log_layer_ids' VALUES (%1, '%2')" ).arg( newLayerId ).arg( qgisLayerId );
1261 sql = QStringLiteral(
"UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'layer_id'" ).arg( newLayerId + 1 );
1264 layerId = newLayerId;
1270 int QgsOfflineEditing::getCommitNo(
sqlite3 *db )
1272 QString sql = QStringLiteral(
"SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'commit_no'" );
1273 return sqlQueryInt( db, sql, -1 );
1276 void QgsOfflineEditing::increaseCommitNo(
sqlite3 *db )
1278 QString sql = QStringLiteral(
"UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'commit_no'" ).arg( getCommitNo( db ) + 1 );
1284 QString sql = QStringLiteral(
"INSERT INTO 'log_fids' VALUES ( %1, %2, %3 )" ).arg( layerId ).arg( offlineFid ).arg( remoteFid );
1290 QString sql = QStringLiteral(
"SELECT \"remote_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"offline_fid\" = %2" ).arg( layerId ).arg( offlineFid );
1291 return sqlQueryInt( db, sql, -1 );
1296 QString sql = QStringLiteral(
"SELECT \"offline_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"remote_fid\" = %2" ).arg( layerId ).arg( remoteFid );
1297 return sqlQueryInt( db, sql, -1 );
1302 QString sql = QStringLiteral(
"SELECT COUNT(\"fid\") FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( fid );
1303 return ( sqlQueryInt( db, sql, 0 ) > 0 );
1306 int QgsOfflineEditing::sqlExec(
sqlite3 *db,
const QString &sql )
1308 char *errmsg =
nullptr;
1309 int rc = sqlite3_exec( db, sql.toUtf8(),
nullptr,
nullptr, &errmsg );
1310 if ( rc != SQLITE_OK )
1312 showWarning( errmsg );
1317 int QgsOfflineEditing::sqlQueryInt(
sqlite3 *db,
const QString &sql,
int defaultValue )
1319 sqlite3_stmt *stmt =
nullptr;
1320 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt,
nullptr ) != SQLITE_OK )
1322 showWarning( sqlite3_errmsg( db ) );
1323 return defaultValue;
1326 int value = defaultValue;
1327 int ret = sqlite3_step( stmt );
1328 if ( ret == SQLITE_ROW )
1330 value = sqlite3_column_int( stmt, 0 );
1332 sqlite3_finalize( stmt );
1337 QList<int> QgsOfflineEditing::sqlQueryInts(
sqlite3 *db,
const QString &sql )
1341 sqlite3_stmt *stmt =
nullptr;
1342 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt,
nullptr ) != SQLITE_OK )
1344 showWarning( sqlite3_errmsg( db ) );
1348 int ret = sqlite3_step( stmt );
1349 while ( ret == SQLITE_ROW )
1351 values << sqlite3_column_int( stmt, 0 );
1353 ret = sqlite3_step( stmt );
1355 sqlite3_finalize( stmt );
1360 QList<QgsField> QgsOfflineEditing::sqlQueryAttributesAdded(
sqlite3 *db,
const QString &sql )
1362 QList<QgsField> values;
1364 sqlite3_stmt *stmt =
nullptr;
1365 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt,
nullptr ) != SQLITE_OK )
1367 showWarning( sqlite3_errmsg( db ) );
1371 int ret = sqlite3_step( stmt );
1372 while ( ret == SQLITE_ROW )
1374 QgsField field( QString(
reinterpret_cast< const char *
>( sqlite3_column_text( stmt, 0 ) ) ),
1375 static_cast< QVariant::Type
>( sqlite3_column_int( stmt, 1 ) ),
1377 sqlite3_column_int( stmt, 2 ),
1378 sqlite3_column_int( stmt, 3 ),
1379 QString(
reinterpret_cast< const char *
>( sqlite3_column_text( stmt, 4 ) ) ) );
1382 ret = sqlite3_step( stmt );
1384 sqlite3_finalize( stmt );
1393 sqlite3_stmt *stmt =
nullptr;
1394 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt,
nullptr ) != SQLITE_OK )
1396 showWarning( sqlite3_errmsg( db ) );
1400 int ret = sqlite3_step( stmt );
1401 while ( ret == SQLITE_ROW )
1403 values << sqlite3_column_int( stmt, 0 );
1405 ret = sqlite3_step( stmt );
1407 sqlite3_finalize( stmt );
1412 QgsOfflineEditing::AttributeValueChanges QgsOfflineEditing::sqlQueryAttributeValueChanges(
sqlite3 *db,
const QString &sql )
1414 AttributeValueChanges values;
1416 sqlite3_stmt *stmt =
nullptr;
1417 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt,
nullptr ) != SQLITE_OK )
1419 showWarning( sqlite3_errmsg( db ) );
1423 int ret = sqlite3_step( stmt );
1424 while ( ret == SQLITE_ROW )
1426 AttributeValueChange change;
1427 change.fid = sqlite3_column_int( stmt, 0 );
1428 change.attr = sqlite3_column_int( stmt, 1 );
1429 change.value = QString(
reinterpret_cast< const char *
>( sqlite3_column_text( stmt, 2 ) ) );
1432 ret = sqlite3_step( stmt );
1434 sqlite3_finalize( stmt );
1439 QgsOfflineEditing::GeometryChanges QgsOfflineEditing::sqlQueryGeometryChanges(
sqlite3 *db,
const QString &sql )
1441 GeometryChanges values;
1443 sqlite3_stmt *stmt =
nullptr;
1444 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt,
nullptr ) != SQLITE_OK )
1446 showWarning( sqlite3_errmsg( db ) );
1450 int ret = sqlite3_step( stmt );
1451 while ( ret == SQLITE_ROW )
1453 GeometryChange change;
1454 change.fid = sqlite3_column_int( stmt, 0 );
1455 change.geom_wkt = QString(
reinterpret_cast< const char *
>( sqlite3_column_text( stmt, 1 ) ) );
1458 ret = sqlite3_step( stmt );
1460 sqlite3_finalize( stmt );
1465 void QgsOfflineEditing::committedAttributesAdded(
const QString &qgisLayerId,
const QList<QgsField> &addedAttributes )
1472 int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1473 int commitNo = getCommitNo( database.get() );
1477 QString sql = QStringLiteral(
"INSERT INTO 'log_added_attrs' VALUES ( %1, %2, '%3', %4, %5, %6, '%7' )" )
1485 sqlExec( database.get(), sql );
1488 increaseCommitNo( database.get() );
1491 void QgsOfflineEditing::committedFeaturesAdded(
const QString &qgisLayerId,
const QgsFeatureList &addedFeatures )
1498 int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1502 QString dataSourceString = layer->
source();
1508 if ( !offlinePath.contains(
".gpkg" ) )
1510 tableName = uri.
table();
1515 QVariantMap decodedUri = ogrProviderMetaData->
decodeUri( dataSourceString );
1516 tableName = decodedUri.value( QStringLiteral(
"layerName" ) ).toString();
1517 if ( tableName.isEmpty() )
1519 showWarning( tr(
"Could not deduce table name from data source %1." ).arg( dataSourceString ) );
1524 QString sql = QStringLiteral(
"SELECT ROWID FROM '%1' ORDER BY ROWID DESC LIMIT %2" ).arg( tableName ).arg( addedFeatures.size() );
1525 QList<int> newFeatureIds = sqlQueryInts( database.get(), sql );
1526 for (
int i = newFeatureIds.size() - 1; i >= 0; i-- )
1528 QString sql = QStringLiteral(
"INSERT INTO 'log_added_features' VALUES ( %1, %2 )" )
1530 .arg( newFeatureIds.at( i ) );
1531 sqlExec( database.get(), sql );
1535 void QgsOfflineEditing::committedFeaturesRemoved(
const QString &qgisLayerId,
const QgsFeatureIds &deletedFeatureIds )
1542 int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1546 if ( isAddedFeature( database.get(), layerId,
id ) )
1549 QString sql = QStringLiteral(
"DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg(
id );
1550 sqlExec( database.get(), sql );
1554 QString sql = QStringLiteral(
"INSERT INTO 'log_removed_features' VALUES ( %1, %2)" )
1557 sqlExec( database.get(), sql );
1562 void QgsOfflineEditing::committedAttributeValuesChanges(
const QString &qgisLayerId,
const QgsChangedAttributesMap &changedAttrsMap )
1569 int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1570 int commitNo = getCommitNo( database.get() );
1572 for ( QgsChangedAttributesMap::const_iterator cit = changedAttrsMap.begin(); cit != changedAttrsMap.end(); ++cit )
1575 if ( isAddedFeature( database.get(), layerId, fid ) )
1581 for ( QgsAttributeMap::const_iterator it = attrMap.constBegin(); it != attrMap.constEnd(); ++it )
1583 QString sql = QStringLiteral(
"INSERT INTO 'log_feature_updates' VALUES ( %1, %2, %3, %4, '%5' )" )
1588 .arg( it.value().toString() );
1589 sqlExec( database.get(), sql );
1593 increaseCommitNo( database.get() );
1596 void QgsOfflineEditing::committedGeometriesChanges(
const QString &qgisLayerId,
const QgsGeometryMap &changedGeometries )
1603 int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1604 int commitNo = getCommitNo( database.get() );
1606 for ( QgsGeometryMap::const_iterator it = changedGeometries.begin(); it != changedGeometries.end(); ++it )
1609 if ( isAddedFeature( database.get(), layerId, fid ) )
1615 QString sql = QStringLiteral(
"INSERT INTO 'log_geometry_updates' VALUES ( %1, %2, %3, '%4' )" )
1619 .arg( geom.
asWkt() );
1620 sqlExec( database.get(), sql );
1625 increaseCommitNo( database.get() );
1628 void QgsOfflineEditing::startListenFeatureChanges()
1630 QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1636 this, &QgsOfflineEditing::committedAttributesAdded );
1638 this, &QgsOfflineEditing::committedAttributeValuesChanges );
1640 this, &QgsOfflineEditing::committedGeometriesChanges );
1643 this, &QgsOfflineEditing::committedFeaturesAdded );
1645 this, &QgsOfflineEditing::committedFeaturesRemoved );
1648 void QgsOfflineEditing::stopListenFeatureChanges()
1650 QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1656 this, &QgsOfflineEditing::committedAttributesAdded );
1658 this, &QgsOfflineEditing::committedAttributeValuesChanges );
1660 this, &QgsOfflineEditing::committedGeometriesChanges );
1663 this, &QgsOfflineEditing::committedFeaturesAdded );
1665 this, &QgsOfflineEditing::committedFeaturesRemoved );
1668 void QgsOfflineEditing::layerAdded(
QgsMapLayer *layer )
1673 QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( layer );