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 PROJECT_ENTRY_SCOPE_OFFLINE "OfflineEditingPlugin"
64 #define PROJECT_ENTRY_KEY_OFFLINE_DB_PATH "/OfflineDbPath"
89 if ( layerIds.isEmpty() )
94 QString dbPath = QDir( offlineDataPath ).absoluteFilePath( offlineDbFile );
95 if ( createOfflineDb( dbPath, containerType ) )
98 int rc = database.
open( dbPath );
99 if ( rc != SQLITE_OK )
101 showWarning( tr(
"Could not open the SpatiaLite database" ) );
106 createLoggingTables( database.get() );
110 QMap<QString, QgsVectorJoinList > joinInfoBuffer;
111 QMap<QString, QgsVectorLayer *> layerIdMapping;
113 for (
const QString &layerId : layerIds )
125 QgsVectorJoinList::iterator joinIt = joins.begin();
126 while ( joinIt != joins.end() )
128 if ( joinIt->prefix().isNull() )
133 joinIt->setPrefix( vl->
name() +
'_' );
137 joinInfoBuffer.insert( vl->
id(), joins );
143 for (
int i = 0; i < layerIds.count(); i++ )
151 QString origLayerId = vl->
id();
152 QgsVectorLayer *newLayer = copyVectorLayer( vl, database.get(), dbPath, onlySelected, containerType );
155 layerIdMapping.insert( origLayerId, newLayer );
158 snappingConfig.
removeLayers( QList<QgsMapLayer *>() << vl );
162 QStringList() << origLayerId );
170 QMap<QString, QgsVectorJoinList >::ConstIterator it;
171 for ( it = joinInfoBuffer.constBegin(); it != joinInfoBuffer.constEnd(); ++it )
177 const QList<QgsVectorLayerJoinInfo> joins = it.value();
180 QgsVectorLayer *newJoinedLayer = layerIdMapping.value( join.joinLayerId() );
181 if ( newJoinedLayer )
184 join.setJoinLayer( newJoinedLayer );
195 if ( projectTitle.isEmpty() )
199 projectTitle += QLatin1String(
" (offline)" );
230 QList<QgsMapLayer *> offlineLayers;
232 for ( QMap<QString, QgsMapLayer *>::iterator layer_it = mapLayers.begin() ; layer_it != mapLayers.end(); ++layer_it )
237 offlineLayers << layer;
241 QgsDebugMsgLevel( QStringLiteral(
"Found %1 offline layers" ).arg( offlineLayers.count() ), 4 );
242 for (
int l = 0; l < offlineLayers.count(); l++ )
250 QString remoteName = layer->
name();
251 remoteName.remove( QRegExp(
" \\(offline\\)$" ) );
257 if ( remoteLayer->
providerType().contains( QLatin1String(
"WFS" ), Qt::CaseInsensitive ) )
267 QgsVectorLayer *offlineLayer = qobject_cast<QgsVectorLayer *>( layer );
273 copySymbology( offlineLayer, remoteLayer );
274 updateRelations( offlineLayer, remoteLayer );
275 updateMapThemes( offlineLayer, remoteLayer );
276 updateLayerOrder( offlineLayer, remoteLayer );
280 snappingConfig.
removeLayers( QList<QgsMapLayer *>() << offlineLayer );
288 QString qgisLayerId = layer->
id();
289 QString sql = QStringLiteral(
"SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( qgisLayerId );
290 int layerId = sqlQueryInt( database.get(), sql, -1 );
296 int commitNo = getCommitNo( database.get() );
297 QgsDebugMsgLevel( QStringLiteral(
"Found %1 commits" ).arg( commitNo ), 4 );
298 for (
int i = 0; i < commitNo; i++ )
302 applyAttributesAdded( remoteLayer, database.get(), layerId, i );
303 applyAttributeValueChanges( offlineLayer, remoteLayer, database.get(), layerId, i );
304 applyGeometryChanges( remoteLayer, database.get(), layerId, i );
307 applyFeaturesAdded( offlineLayer, remoteLayer, database.get(), layerId );
308 applyFeaturesRemoved( remoteLayer, database.get(), layerId );
313 updateFidLookup( remoteLayer, database.get(), layerId );
316 sql = QStringLiteral(
"DELETE FROM 'log_added_attrs' WHERE \"layer_id\" = %1" ).arg( layerId );
317 sqlExec( database.get(), sql );
318 sql = QStringLiteral(
"DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
319 sqlExec( database.get(), sql );
320 sql = QStringLiteral(
"DELETE FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( layerId );
321 sqlExec( database.get(), sql );
322 sql = QStringLiteral(
"DELETE FROM 'log_feature_updates' WHERE \"layer_id\" = %1" ).arg( layerId );
323 sqlExec( database.get(), sql );
324 sql = QStringLiteral(
"DELETE FROM 'log_geometry_updates' WHERE \"layer_id\" = %1" ).arg( layerId );
325 sqlExec( database.get(), sql );
329 showWarning( remoteLayer->
commitErrors().join( QStringLiteral(
"\n" ) ) );
334 QgsDebugMsg( QStringLiteral(
"Could not find the layer id in the edit logs!" ) );
345 projectTitle.remove( QRegExp(
" \\(offline\\)$" ) );
352 QgsDebugMsg( QStringLiteral(
"Remote layer is not valid!" ) );
357 QString sql = QStringLiteral(
"UPDATE 'log_indices' SET 'last_index' = 0 WHERE \"name\" = 'commit_no'" );
358 sqlExec( database.get(), sql );
365 void QgsOfflineEditing::initializeSpatialMetadata(
sqlite3 *sqlite_handle )
368 if ( !sqlite_handle )
371 char **results =
nullptr;
373 int ret = sqlite3_get_table( sqlite_handle,
"select count(*) from sqlite_master", &results, &rows, &columns,
nullptr );
374 if ( ret != SQLITE_OK )
379 for (
int i = 1; i <= rows; i++ )
380 count = atoi( results[( i * columns ) + 0] );
383 sqlite3_free_table( results );
388 bool above41 =
false;
389 ret = sqlite3_get_table( sqlite_handle,
"select spatialite_version()", &results, &rows, &columns,
nullptr );
390 if ( ret == SQLITE_OK && rows == 1 && columns == 1 )
392 QString version = QString::fromUtf8( results[1] );
393 QStringList parts = version.split(
' ', QString::SkipEmptyParts );
394 if ( !parts.empty() )
396 QStringList verparts = parts.at( 0 ).split(
'.', QString::SkipEmptyParts );
397 above41 = verparts.size() >= 2 && ( verparts.at( 0 ).toInt() > 4 || ( verparts.at( 0 ).toInt() == 4 && verparts.at( 1 ).toInt() >= 1 ) );
401 sqlite3_free_table( results );
404 char *errMsg =
nullptr;
405 ret = sqlite3_exec( sqlite_handle, above41 ?
"SELECT InitSpatialMetadata(1)" :
"SELECT InitSpatialMetadata()",
nullptr,
nullptr, &errMsg );
407 if ( ret != SQLITE_OK )
409 QString errCause = tr(
"Unable to initialize SpatialMetadata:\n" );
410 errCause += QString::fromUtf8( errMsg );
411 showWarning( errCause );
412 sqlite3_free( errMsg );
415 spatial_ref_sys_init( sqlite_handle, 0 );
418 bool QgsOfflineEditing::createOfflineDb(
const QString &offlineDbPath, ContainerType containerType )
421 char *errMsg =
nullptr;
422 QFile newDb( offlineDbPath );
423 if ( newDb.exists() )
425 QFile::remove( offlineDbPath );
430 QFileInfo fullPath = QFileInfo( offlineDbPath );
431 QDir path = fullPath.dir();
434 QDir().mkpath( path.absolutePath() );
437 QString dbPath = newDb.fileName();
440 switch ( containerType )
444 OGRSFDriverH hGpkgDriver = OGRGetDriverByName(
"GPKG" );
447 showWarning( tr(
"Creation of database failed. GeoPackage driver not found." ) );
454 showWarning( tr(
"Creation of database failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
466 ret = database.
open_v2( dbPath, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
nullptr );
470 QString errCause = tr(
"Could not create a new database\n" );
472 showWarning( errCause );
476 ret = sqlite3_exec( database.get(),
"PRAGMA foreign_keys = 1",
nullptr,
nullptr, &errMsg );
477 if ( ret != SQLITE_OK )
479 showWarning( tr(
"Unable to activate FOREIGN_KEY constraints" ) );
480 sqlite3_free( errMsg );
483 initializeSpatialMetadata( database.get() );
487 void QgsOfflineEditing::createLoggingTables(
sqlite3 *db )
490 QString sql = QStringLiteral(
"CREATE TABLE 'log_indices' ('name' TEXT, 'last_index' INTEGER)" );
493 sql = QStringLiteral(
"INSERT INTO 'log_indices' VALUES ('commit_no', 0)" );
496 sql = QStringLiteral(
"INSERT INTO 'log_indices' VALUES ('layer_id', 0)" );
500 sql = QStringLiteral(
"CREATE TABLE 'log_layer_ids' ('id' INTEGER, 'qgis_id' TEXT)" );
504 sql = QStringLiteral(
"CREATE TABLE 'log_fids' ('layer_id' INTEGER, 'offline_fid' INTEGER, 'remote_fid' INTEGER)" );
508 sql = QStringLiteral(
"CREATE TABLE 'log_added_attrs' ('layer_id' INTEGER, 'commit_no' INTEGER, " );
509 sql += QLatin1String(
"'name' TEXT, 'type' INTEGER, 'length' INTEGER, 'precision' INTEGER, 'comment' TEXT)" );
513 sql = QStringLiteral(
"CREATE TABLE 'log_added_features' ('layer_id' INTEGER, 'fid' INTEGER)" );
517 sql = QStringLiteral(
"CREATE TABLE 'log_removed_features' ('layer_id' INTEGER, 'fid' INTEGER)" );
521 sql = QStringLiteral(
"CREATE TABLE 'log_feature_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'attr' INTEGER, 'value' TEXT)" );
525 sql = QStringLiteral(
"CREATE TABLE 'log_geometry_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'geom_wkt' TEXT)" );
538 QString tableName = layer->
id();
539 QgsDebugMsgLevel( QStringLiteral(
"Creating offline table %1 ..." ).arg( tableName ), 4 );
544 switch ( containerType )
549 QString sql = QStringLiteral(
"CREATE TABLE '%1' (" ).arg( tableName );
552 for (
const auto &field : providerFields )
555 QVariant::Type type = field.type();
556 if ( type == QVariant::Int || type == QVariant::LongLong )
558 dataType = QStringLiteral(
"INTEGER" );
560 else if ( type == QVariant::Double )
562 dataType = QStringLiteral(
"REAL" );
564 else if ( type == QVariant::String )
566 dataType = QStringLiteral(
"TEXT" );
570 showWarning( tr(
"%1: Unknown data type %2. Not using type affinity for the field." ).arg( field.name(), QVariant::typeToName( type ) ) );
573 sql += delim + QStringLiteral(
"'%1' %2" ).arg( field.name(), dataType );
578 int rc = sqlExec( db, sql );
589 geomType = QStringLiteral(
"POINT" );
592 geomType = QStringLiteral(
"MULTIPOINT" );
595 geomType = QStringLiteral(
"LINESTRING" );
598 geomType = QStringLiteral(
"MULTILINESTRING" );
601 geomType = QStringLiteral(
"POLYGON" );
604 geomType = QStringLiteral(
"MULTIPOLYGON" );
611 QString zmInfo = QStringLiteral(
"XY" );
620 if ( layer->
crs().
authid().startsWith( QLatin1String(
"EPSG:" ), Qt::CaseInsensitive ) )
622 epsgCode = layer->
crs().
authid().mid( 5 );
627 showWarning( tr(
"Layer %1 has unsupported Coordinate Reference System (%2)." ).arg( layer->
name(), layer->
crs().
authid() ) );
630 QString sqlAddGeom = QStringLiteral(
"SELECT AddGeometryColumn('%1', 'Geometry', %2, '%3', '%4')" )
631 .arg( tableName, epsgCode, geomType, zmInfo );
634 QString sqlCreateIndex = QStringLiteral(
"SELECT CreateSpatialIndex('%1', 'Geometry')" ).arg( tableName );
636 if ( rc == SQLITE_OK )
638 rc = sqlExec( db, sqlAddGeom );
639 if ( rc == SQLITE_OK )
641 rc = sqlExec( db, sqlCreateIndex );
646 if ( rc != SQLITE_OK )
648 showWarning( tr(
"Filling SpatiaLite for layer %1 failed" ).arg( layer->
name() ) );
653 QString connectionString = QStringLiteral(
"dbname='%1' table='%2'%3 sql=" )
655 tableName, layer->
isSpatial() ?
"(Geometry)" :
"" );
658 layer->
name() +
" (offline)", QStringLiteral(
"spatialite" ), options );
664 char **options =
nullptr;
666 options = CSLSetNameValue( options,
"OVERWRITE",
"YES" );
667 options = CSLSetNameValue( options,
"IDENTIFIER", tr(
"%1 (offline)" ).arg( layer->
id() ).toUtf8().constData() );
668 options = CSLSetNameValue( options,
"DESCRIPTION", layer->
dataComment().toUtf8().constData() );
671 QString fidBase( QStringLiteral(
"fid" ) );
672 QString fid = fidBase;
676 fid = fidBase +
'_' + QString::number( counter );
679 if ( counter == 10000 )
681 showWarning( tr(
"Cannot make FID-name for GPKG " ) );
685 options = CSLSetNameValue( options,
"FID", fid.toUtf8().constData() );
689 options = CSLSetNameValue( options,
"GEOMETRY_COLUMN",
"geom" );
690 options = CSLSetNameValue( options,
"SPATIAL_INDEX",
"YES" );
693 OGRSFDriverH hDriver =
nullptr;
696 OGRLayerH hLayer = OGR_DS_CreateLayer( hDS.get(), tableName.toUtf8().constData(), hSRS,
static_cast<OGRwkbGeometryType
>( layer->
wkbType() ), options );
697 CSLDestroy( options );
702 showWarning( tr(
"Creation of layer failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
707 for (
const auto &field : providerFields )
709 const QString fieldName( field.name() );
710 const QVariant::Type type = field.type();
711 OGRFieldType ogrType( OFTString );
712 OGRFieldSubType ogrSubType = OFSTNone;
713 if ( type == QVariant::Int )
714 ogrType = OFTInteger;
715 else if ( type == QVariant::LongLong )
716 ogrType = OFTInteger64;
717 else if ( type == QVariant::Double )
719 else if ( type == QVariant::Time )
721 else if ( type == QVariant::Date )
723 else if ( type == QVariant::DateTime )
724 ogrType = OFTDateTime;
725 else if ( type == QVariant::Bool )
727 ogrType = OFTInteger;
728 ogrSubType = OFSTBoolean;
733 int ogrWidth = field.length();
736 OGR_Fld_SetWidth( fld.get(), ogrWidth );
737 if ( ogrSubType != OFSTNone )
738 OGR_Fld_SetSubType( fld.get(), ogrSubType );
740 if ( OGR_L_CreateField( hLayer, fld.get(),
true ) != OGRERR_NONE )
742 showWarning( tr(
"Creation of field %1 failed (OGR error: %2)" )
743 .arg( fieldName, QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
751 OGR_L_ResetReading( hLayer );
752 if ( CPLGetLastErrorType() != CE_None )
754 QString msg( tr(
"Creation of layer failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
760 QString uri = QStringLiteral(
"%1|layername=%2" ).arg( offlineDbPath, tableName );
762 newLayer =
new QgsVectorLayer( uri, layer->
name() +
" (offline)", QStringLiteral(
"ogr" ), layerOptions );
779 if ( !selectedFids.isEmpty() )
793 int featureCount = 1;
795 QList<QgsFeatureId> remoteFeatureIds;
798 remoteFeatureIds << f.
id();
805 QgsAttributes newAttrs( containerType ==
GPKG ? attrs.count() + 1 : attrs.count() );
806 for (
int it = 0; it < attrs.count(); ++it )
808 newAttrs[column++] = attrs.at( it );
822 int layerId = getOrCreateLayerId( db, newLayer->
id() );
823 QList<QgsFeatureId> offlineFeatureIds;
828 offlineFeatureIds << f.
id();
832 sqlExec( db, QStringLiteral(
"BEGIN" ) );
833 int remoteCount = remoteFeatureIds.size();
834 for (
int i = 0; i < remoteCount; i++ )
837 if ( i < offlineFeatureIds.count() )
839 addFidLookup( db, layerId, offlineFeatureIds.at( i ), remoteFeatureIds.at( i ) );
843 showWarning( tr(
"Feature cannot be copied to the offline layer, please check if the online layer '%1' is still accessible." ).arg( layer->
name() ) );
848 sqlExec( db, QStringLiteral(
"COMMIT" ) );
852 showWarning( newLayer->
commitErrors().join( QStringLiteral(
"\n" ) ) );
867 QList<QgsMapLayer *>() << newLayer );
870 copySymbology( layer, newLayer );
873 const auto fields = layer->
fields();
874 for (
const QgsField &field : fields )
885 if ( layerTreeLayer )
888 if ( parentTreeGroup )
890 int index = parentTreeGroup->
children().indexOf( layerTreeLayer );
893 if ( newLayerTreeLayer )
907 updateRelations( layer, newLayer );
908 updateMapThemes( layer, newLayer );
909 updateLayerOrder( layer, newLayer );
917 void QgsOfflineEditing::applyAttributesAdded(
QgsVectorLayer *remoteLayer,
sqlite3 *db,
int layerId,
int commitNo )
919 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 );
920 QList<QgsField> fields = sqlQueryAttributesAdded( db, sql );
923 QList<QgsVectorDataProvider::NativeType> nativeTypes = provider->
nativeTypes();
926 QMap < QVariant::Type, QString > typeNameLookup;
927 for (
int i = 0; i < nativeTypes.size(); i++ )
935 for (
int i = 0; i < fields.size(); i++ )
939 if ( typeNameLookup.contains( field.
type() ) )
947 showWarning( QStringLiteral(
"Could not add attribute '%1' of type %2" ).arg( field.
name() ).arg( field.
type() ) );
956 QString sql = QStringLiteral(
"SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
957 const QList<int> featureIdInts = sqlQueryInts( db, sql );
959 for (
int id : featureIdInts )
980 for ( QgsFeatureList::iterator it = features.begin(); it != features.end(); ++it )
984 QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
987 for (
int it = 0; it < attrs.count(); ++it )
989 newAttrs[ attrLookup[ it ] ] = attrs.at( it );
1000 void QgsOfflineEditing::applyFeaturesRemoved(
QgsVectorLayer *remoteLayer,
sqlite3 *db,
int layerId )
1002 QString sql = QStringLiteral(
"SELECT \"fid\" FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( layerId );
1008 for ( QgsFeatureIds::const_iterator it = values.constBegin(); it != values.constEnd(); ++it )
1019 QString sql = QStringLiteral(
"SELECT \"fid\", \"attr\", \"value\" FROM 'log_feature_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2 " ).arg( layerId ).arg( commitNo );
1020 AttributeValueChanges values = sqlQueryAttributeValueChanges( db, sql );
1024 QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
1026 for (
int i = 0; i < values.size(); i++ )
1028 QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid );
1029 QgsDebugMsgLevel( QStringLiteral(
"Offline changeAttributeValue %1 = %2" ).arg( QString( attrLookup[ values.at( i ).attr ] ), values.at( i ).value ), 4 );
1030 remoteLayer->
changeAttributeValue( fid, attrLookup[ values.at( i ).attr ], values.at( i ).value );
1036 void QgsOfflineEditing::applyGeometryChanges(
QgsVectorLayer *remoteLayer,
sqlite3 *db,
int layerId,
int commitNo )
1038 QString sql = QStringLiteral(
"SELECT \"fid\", \"geom_wkt\" FROM 'log_geometry_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2" ).arg( layerId ).arg( commitNo );
1039 GeometryChanges values = sqlQueryGeometryChanges( db, sql );
1043 for (
int i = 0; i < values.size(); i++ )
1045 QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid );
1069 if ( offlineFid( db, layerId, f.
id() ) == -1 )
1071 newRemoteFids[ f.
id()] =
true;
1079 QString sql = QStringLiteral(
"SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
1080 QList<int> newOfflineFids = sqlQueryInts( db, sql );
1082 if ( newRemoteFids.size() != newOfflineFids.size() )
1090 sqlExec( db, QStringLiteral(
"BEGIN" ) );
1091 for ( QMap<QgsFeatureId, bool>::const_iterator it = newRemoteFids.constBegin(); it != newRemoteFids.constEnd(); ++it )
1093 addFidLookup( db, layerId, newOfflineFids.at( i++ ), it.key() );
1095 sqlExec( db, QStringLiteral(
"COMMIT" ) );
1109 if ( error.isEmpty() )
1113 if ( !error.isEmpty() )
1115 showWarning( error );
1122 const QList<QgsRelation> referencedRelations = relationManager->
referencedRelations( sourceLayer );
1124 for (
QgsRelation relation : referencedRelations )
1127 relation.setReferencedLayer( targetLayer->
id() );
1131 const QList<QgsRelation> referencingRelations = relationManager->
referencingRelations( sourceLayer );
1133 for (
QgsRelation relation : referencingRelations )
1136 relation.setReferencingLayer( targetLayer->
id() );
1144 const QStringList mapThemeNames = mapThemeCollection->
mapThemes();
1146 for (
const QString &mapThemeName : mapThemeNames )
1154 if ( layerRecord.layer() == sourceLayer )
1156 layerRecord.setLayer( targetLayer );
1170 auto iterator = layerOrder.begin();
1172 while ( iterator != layerOrder.end() )
1174 if ( *iterator == targetLayer )
1176 iterator = layerOrder.erase( iterator );
1177 if ( iterator == layerOrder.end() )
1181 if ( *iterator == sourceLayer )
1183 *iterator = targetLayer;
1197 QMap <
int ,
int > attrLookup;
1200 for (
int i = 0; i < offlineAttrs.size(); i++ )
1209 void QgsOfflineEditing::showWarning(
const QString &message )
1211 emit
warning( tr(
"Offline Editing Plugin" ), message );
1218 if ( !dbPath.isEmpty() )
1221 int rc = database.
open( absoluteDbPath );
1222 if ( rc != SQLITE_OK )
1224 QgsDebugMsg( QStringLiteral(
"Could not open the SpatiaLite logging database" ) );
1225 showWarning( tr(
"Could not open the SpatiaLite logging database" ) );
1230 QgsDebugMsg( QStringLiteral(
"dbPath is empty!" ) );
1235 int QgsOfflineEditing::getOrCreateLayerId(
sqlite3 *db,
const QString &qgisLayerId )
1237 QString sql = QStringLiteral(
"SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( qgisLayerId );
1238 int layerId = sqlQueryInt( db, sql, -1 );
1239 if ( layerId == -1 )
1242 sql = QStringLiteral(
"SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'layer_id'" );
1243 int newLayerId = sqlQueryInt( db, sql, -1 );
1246 sql = QStringLiteral(
"INSERT INTO 'log_layer_ids' VALUES (%1, '%2')" ).arg( newLayerId ).arg( qgisLayerId );
1251 sql = QStringLiteral(
"UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'layer_id'" ).arg( newLayerId + 1 );
1254 layerId = newLayerId;
1260 int QgsOfflineEditing::getCommitNo(
sqlite3 *db )
1262 QString sql = QStringLiteral(
"SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'commit_no'" );
1263 return sqlQueryInt( db, sql, -1 );
1266 void QgsOfflineEditing::increaseCommitNo(
sqlite3 *db )
1268 QString sql = QStringLiteral(
"UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'commit_no'" ).arg( getCommitNo( db ) + 1 );
1274 QString sql = QStringLiteral(
"INSERT INTO 'log_fids' VALUES ( %1, %2, %3 )" ).arg( layerId ).arg( offlineFid ).arg( remoteFid );
1280 QString sql = QStringLiteral(
"SELECT \"remote_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"offline_fid\" = %2" ).arg( layerId ).arg( offlineFid );
1281 return sqlQueryInt( db, sql, -1 );
1286 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 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 int rc = sqlite3_exec( db, sql.toUtf8(),
nullptr,
nullptr, &errmsg );
1300 if ( rc != SQLITE_OK )
1302 showWarning( errmsg );
1307 int QgsOfflineEditing::sqlQueryInt(
sqlite3 *db,
const QString &sql,
int 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 int value = defaultValue;
1317 int ret = sqlite3_step( stmt );
1318 if ( ret == SQLITE_ROW )
1320 value = sqlite3_column_int( stmt, 0 );
1322 sqlite3_finalize( stmt );
1327 QList<int> QgsOfflineEditing::sqlQueryInts(
sqlite3 *db,
const QString &sql )
1331 sqlite3_stmt *stmt =
nullptr;
1332 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt,
nullptr ) != SQLITE_OK )
1334 showWarning( sqlite3_errmsg( db ) );
1338 int ret = sqlite3_step( stmt );
1339 while ( ret == SQLITE_ROW )
1341 values << sqlite3_column_int( stmt, 0 );
1343 ret = sqlite3_step( stmt );
1345 sqlite3_finalize( stmt );
1350 QList<QgsField> QgsOfflineEditing::sqlQueryAttributesAdded(
sqlite3 *db,
const QString &sql )
1352 QList<QgsField> values;
1354 sqlite3_stmt *stmt =
nullptr;
1355 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt,
nullptr ) != SQLITE_OK )
1357 showWarning( sqlite3_errmsg( db ) );
1361 int ret = sqlite3_step( stmt );
1362 while ( ret == SQLITE_ROW )
1364 QgsField field( QString(
reinterpret_cast< const char *
>( sqlite3_column_text( stmt, 0 ) ) ),
1365 static_cast< QVariant::Type
>( sqlite3_column_int( stmt, 1 ) ),
1367 sqlite3_column_int( stmt, 2 ),
1368 sqlite3_column_int( stmt, 3 ),
1369 QString(
reinterpret_cast< const char *
>( sqlite3_column_text( stmt, 4 ) ) ) );
1372 ret = sqlite3_step( stmt );
1374 sqlite3_finalize( stmt );
1383 sqlite3_stmt *stmt =
nullptr;
1384 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt,
nullptr ) != SQLITE_OK )
1386 showWarning( sqlite3_errmsg( db ) );
1390 int ret = sqlite3_step( stmt );
1391 while ( ret == SQLITE_ROW )
1393 values << sqlite3_column_int( stmt, 0 );
1395 ret = sqlite3_step( stmt );
1397 sqlite3_finalize( stmt );
1402 QgsOfflineEditing::AttributeValueChanges QgsOfflineEditing::sqlQueryAttributeValueChanges(
sqlite3 *db,
const QString &sql )
1404 AttributeValueChanges values;
1406 sqlite3_stmt *stmt =
nullptr;
1407 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt,
nullptr ) != SQLITE_OK )
1409 showWarning( sqlite3_errmsg( db ) );
1413 int ret = sqlite3_step( stmt );
1414 while ( ret == SQLITE_ROW )
1416 AttributeValueChange change;
1417 change.fid = sqlite3_column_int( stmt, 0 );
1418 change.attr = sqlite3_column_int( stmt, 1 );
1419 change.value = QString(
reinterpret_cast< const char *
>( sqlite3_column_text( stmt, 2 ) ) );
1422 ret = sqlite3_step( stmt );
1424 sqlite3_finalize( stmt );
1429 QgsOfflineEditing::GeometryChanges QgsOfflineEditing::sqlQueryGeometryChanges(
sqlite3 *db,
const QString &sql )
1431 GeometryChanges values;
1433 sqlite3_stmt *stmt =
nullptr;
1434 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt,
nullptr ) != SQLITE_OK )
1436 showWarning( sqlite3_errmsg( db ) );
1440 int ret = sqlite3_step( stmt );
1441 while ( ret == SQLITE_ROW )
1443 GeometryChange change;
1444 change.fid = sqlite3_column_int( stmt, 0 );
1445 change.geom_wkt = QString(
reinterpret_cast< const char *
>( sqlite3_column_text( stmt, 1 ) ) );
1448 ret = sqlite3_step( stmt );
1450 sqlite3_finalize( stmt );
1455 void QgsOfflineEditing::committedAttributesAdded(
const QString &qgisLayerId,
const QList<QgsField> &addedAttributes )
1462 int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1463 int commitNo = getCommitNo( database.get() );
1465 for (
const QgsField &field : addedAttributes )
1467 QString sql = QStringLiteral(
"INSERT INTO 'log_added_attrs' VALUES ( %1, %2, '%3', %4, %5, %6, '%7' )" )
1470 .arg( field.
name() )
1471 .arg( field.
type() )
1475 sqlExec( database.get(), sql );
1478 increaseCommitNo( database.get() );
1481 void QgsOfflineEditing::committedFeaturesAdded(
const QString &qgisLayerId,
const QgsFeatureList &addedFeatures )
1488 int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1492 QString dataSourceString = layer->
source();
1498 if ( !offlinePath.contains(
".gpkg" ) )
1500 tableName = uri.
table();
1505 QVariantMap decodedUri = ogrProviderMetaData->
decodeUri( dataSourceString );
1506 tableName = decodedUri.value( QStringLiteral(
"layerName" ) ).toString();
1507 if ( tableName.isEmpty() )
1509 showWarning( tr(
"Could not deduce table name from data source %1." ).arg( dataSourceString ) );
1514 QString sql = QStringLiteral(
"SELECT ROWID FROM '%1' ORDER BY ROWID DESC LIMIT %2" ).arg( tableName ).arg( addedFeatures.size() );
1515 QList<int> newFeatureIds = sqlQueryInts( database.get(), sql );
1516 for (
int i = newFeatureIds.size() - 1; i >= 0; i-- )
1518 QString sql = QStringLiteral(
"INSERT INTO 'log_added_features' VALUES ( %1, %2 )" )
1520 .arg( newFeatureIds.at( i ) );
1521 sqlExec( database.get(), sql );
1525 void QgsOfflineEditing::committedFeaturesRemoved(
const QString &qgisLayerId,
const QgsFeatureIds &deletedFeatureIds )
1532 int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1536 if ( isAddedFeature( database.get(), layerId,
id ) )
1539 QString sql = QStringLiteral(
"DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg(
id );
1540 sqlExec( database.get(), sql );
1544 QString sql = QStringLiteral(
"INSERT INTO 'log_removed_features' VALUES ( %1, %2)" )
1547 sqlExec( database.get(), sql );
1552 void QgsOfflineEditing::committedAttributeValuesChanges(
const QString &qgisLayerId,
const QgsChangedAttributesMap &changedAttrsMap )
1559 int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1560 int commitNo = getCommitNo( database.get() );
1562 for ( QgsChangedAttributesMap::const_iterator cit = changedAttrsMap.begin(); cit != changedAttrsMap.end(); ++cit )
1565 if ( isAddedFeature( database.get(), layerId, fid ) )
1571 for ( QgsAttributeMap::const_iterator it = attrMap.constBegin(); it != attrMap.constEnd(); ++it )
1573 QString sql = QStringLiteral(
"INSERT INTO 'log_feature_updates' VALUES ( %1, %2, %3, %4, '%5' )" )
1578 .arg( it.value().toString() );
1579 sqlExec( database.get(), sql );
1583 increaseCommitNo( database.get() );
1586 void QgsOfflineEditing::committedGeometriesChanges(
const QString &qgisLayerId,
const QgsGeometryMap &changedGeometries )
1593 int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1594 int commitNo = getCommitNo( database.get() );
1596 for ( QgsGeometryMap::const_iterator it = changedGeometries.begin(); it != changedGeometries.end(); ++it )
1599 if ( isAddedFeature( database.get(), layerId, fid ) )
1605 QString sql = QStringLiteral(
"INSERT INTO 'log_geometry_updates' VALUES ( %1, %2, %3, '%4' )" )
1609 .arg( geom.
asWkt() );
1610 sqlExec( database.get(), sql );
1615 increaseCommitNo( database.get() );
1618 void QgsOfflineEditing::startListenFeatureChanges()
1620 QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1626 this, &QgsOfflineEditing::committedAttributesAdded );
1628 this, &QgsOfflineEditing::committedAttributeValuesChanges );
1630 this, &QgsOfflineEditing::committedGeometriesChanges );
1633 this, &QgsOfflineEditing::committedFeaturesAdded );
1635 this, &QgsOfflineEditing::committedFeaturesRemoved );
1638 void QgsOfflineEditing::stopListenFeatureChanges()
1640 QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1646 this, &QgsOfflineEditing::committedAttributesAdded );
1648 this, &QgsOfflineEditing::committedAttributeValuesChanges );
1650 this, &QgsOfflineEditing::committedGeometriesChanges );
1653 this, &QgsOfflineEditing::committedFeaturesAdded );
1655 this, &QgsOfflineEditing::committedFeaturesRemoved );
1658 void QgsOfflineEditing::layerAdded(
QgsMapLayer *layer )
1663 QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( layer );