1 /***************************************************************************
2     offline_editing.cpp
3 
4     Offline Editing Plugin
5     a QGIS plugin
6      --------------------------------------
7     Date                 : 22-Jul-2010
8     Copyright            : (C) 2010 by Sourcepole
9     Email                : info at sourcepole.ch
10  ***************************************************************************
11  *                                                                         *
12  *   This program is free software; you can redistribute it and/or modify  *
13  *   it under the terms of the GNU General Public License as published by  *
14  *   the Free Software Foundation; either version 2 of the License, or     *
15  *   (at your option) any later version.                                   *
16  *                                                                         *
17  ***************************************************************************/
18 
19 
20 #include "qgsapplication.h"
21 #include "qgsdatasourceuri.h"
22 #include "qgsgeometry.h"
23 #include "qgslayertreegroup.h"
24 #include "qgslayertreelayer.h"
25 #include "qgsmaplayer.h"
26 #include "qgsofflineediting.h"
27 #include "qgsproject.h"
28 #include "qgsvectordataprovider.h"
29 #include "qgsvectorlayereditbuffer.h"
30 #include "qgsvectorlayerjoinbuffer.h"
31 #include "qgsspatialiteutils.h"
32 #include "qgsfeatureiterator.h"
33 #include "qgslogger.h"
34 #include "qgsvectorlayerutils.h"
35 #include "qgsrelationmanager.h"
36 #include "qgsmapthemecollection.h"
37 #include "qgslayertree.h"
38 #include "qgsogrutils.h"
39 #include "qgsvectorfilewriter.h"
40 #include "qgsvectorlayer.h"
41 #include "qgsproviderregistry.h"
42 #include "qgsprovidermetadata.h"
43 #include "qgsmaplayerstylemanager.h"
44 #include "qgsjsonutils.h"
45 #include "qgstransactiongroup.h"
46 
47 #include <QDir>
48 #include <QDomDocument>
49 #include <QDomNode>
50 #include <QFile>
51 #include <QRegularExpression>
52 
53 #include <ogr_srs_api.h>
54 
55 extern "C"
56 {
57 #include <sqlite3.h>
58 }
59 
60 #ifdef HAVE_SPATIALITE
61 extern "C"
62 {
63 #include <spatialite.h>
64 }
65 #endif
66 
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"
75 
QgsOfflineEditing()76 QgsOfflineEditing::QgsOfflineEditing()
77 {
78   connect( QgsProject::instance(), &QgsProject::layerWasAdded, this, &QgsOfflineEditing::setupLayer );
79 }
80 
81 /**
82  * convert current project to offline project
83  * returns offline project file path
84  *
85  * Workflow:
86  *
87  * - create a sqlite database at offlineDataPath
88  * - copy layers to Geopackage or SpatiaLite offline layers in the above-created database
89  * - replace remote layers' data source with offline layers from the database
90  * - mark those layers as offline
91  * - mark project as offline
92  */
convertToOfflineProject(const QString & offlineDataPath,const QString & offlineDbFile,const QStringList & layerIds,bool onlySelected,ContainerType containerType,const QString & layerNameSuffix)93 bool QgsOfflineEditing::convertToOfflineProject( const QString &offlineDataPath, const QString &offlineDbFile, const QStringList &layerIds, bool onlySelected, ContainerType containerType, const QString &layerNameSuffix )
94 {
95   if ( layerIds.isEmpty() )
96   {
97     return false;
98   }
99 
100   const QString dbPath = QDir( offlineDataPath ).absoluteFilePath( offlineDbFile );
101   if ( createOfflineDb( dbPath, containerType ) )
102   {
103     spatialite_database_unique_ptr database;
104     const int rc = database.open( dbPath );
105     if ( rc != SQLITE_OK )
106     {
107       showWarning( tr( "Could not open the SpatiaLite database" ) );
108     }
109     else
110     {
111       // create logging tables
112       createLoggingTables( database.get() );
113 
114       emit progressStarted();
115 
116       // copy selected vector layers to offline layer
117       for ( int i = 0; i < layerIds.count(); i++ )
118       {
119         emit layerProgressUpdated( i + 1, layerIds.count() );
120 
121         QgsMapLayer *layer = QgsProject::instance()->mapLayer( layerIds.at( i ) );
122         QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( layer );
123         if ( vl && vl->isValid() )
124         {
125           const QString origLayerId = vl->id();
126           convertToOfflineLayer( vl, database.get(), dbPath, onlySelected, containerType, layerNameSuffix );
127         }
128       }
129 
130       emit progressStopped();
131 
132       // save offline project
133       QString projectTitle = QgsProject::instance()->title();
134       if ( projectTitle.isEmpty() )
135       {
136         projectTitle = QFileInfo( QgsProject::instance()->fileName() ).fileName();
137       }
138       projectTitle += QLatin1String( " (offline)" );
139       QgsProject::instance()->setTitle( projectTitle );
140       QgsProject::instance()->writeEntry( PROJECT_ENTRY_SCOPE_OFFLINE, PROJECT_ENTRY_KEY_OFFLINE_DB_PATH, QgsProject::instance()->writePath( dbPath ) );
141 
142       return true;
143     }
144   }
145 
146   return false;
147 }
148 
isOfflineProject() const149 bool QgsOfflineEditing::isOfflineProject() const
150 {
151   return !QgsProject::instance()->readEntry( PROJECT_ENTRY_SCOPE_OFFLINE, PROJECT_ENTRY_KEY_OFFLINE_DB_PATH ).isEmpty();
152 }
153 
synchronize(bool useTransaction)154 void QgsOfflineEditing::synchronize( bool useTransaction )
155 {
156   // open logging db
157   const sqlite3_database_unique_ptr database = openLoggingDb();
158   if ( !database )
159   {
160     return;
161   }
162 
163   emit progressStarted();
164 
165   const QgsSnappingConfig snappingConfig = QgsProject::instance()->snappingConfig();
166 
167   // restore and sync remote layers
168   QMap<QString, QgsMapLayer *> mapLayers = QgsProject::instance()->mapLayers();
169   QMap<int, std::shared_ptr<QgsVectorLayer>> remoteLayersByOfflineId;
170   QMap<int, QgsVectorLayer *> offlineLayersByOfflineId;
171 
172   for ( QMap<QString, QgsMapLayer *>::iterator layer_it = mapLayers.begin() ; layer_it != mapLayers.end(); ++layer_it )
173   {
174     QgsVectorLayer *offlineLayer( qobject_cast<QgsVectorLayer *>( layer_it.value() ) );
175 
176     if ( !offlineLayer || !offlineLayer->isValid() )
177     {
178       QgsDebugMsgLevel( QStringLiteral( "Skipping offline layer %1 because it is an invalid layer" ).arg( layer_it.key() ), 4 );
179       continue;
180     }
181 
182     if ( !offlineLayer->customProperty( CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE, false ).toBool() )
183       continue;
184 
185     const QString remoteSource = offlineLayer->customProperty( CUSTOM_PROPERTY_REMOTE_SOURCE, "" ).toString();
186     const QString remoteProvider = offlineLayer->customProperty( CUSTOM_PROPERTY_REMOTE_PROVIDER, "" ).toString();
187     QString remoteName = offlineLayer->name();
188     const QString remoteNameSuffix = offlineLayer->customProperty( CUSTOM_PROPERTY_LAYERNAME_SUFFIX, " (offline)" ).toString();
189     if ( remoteName.endsWith( remoteNameSuffix ) )
190       remoteName.chop( remoteNameSuffix.size() );
191     const QgsVectorLayer::LayerOptions options { QgsProject::instance()->transformContext() };
192 
193     std::shared_ptr<QgsVectorLayer> remoteLayer = std::make_shared<QgsVectorLayer>( remoteSource, remoteName, remoteProvider, options );
194 
195     if ( ! remoteLayer->isValid() )
196     {
197       QgsDebugMsgLevel( QStringLiteral( "Skipping offline layer %1 because it failed to recreate its corresponding remote layer" ).arg( offlineLayer->id() ), 4 );
198       continue;
199     }
200 
201     // Rebuild WFS cache to get feature id<->GML fid mapping
202     if ( remoteLayer->providerType().contains( QLatin1String( "WFS" ), Qt::CaseInsensitive ) )
203     {
204       QgsFeatureIterator fit = remoteLayer->getFeatures();
205       QgsFeature f;
206       while ( fit.nextFeature( f ) )
207       {
208       }
209     }
210 
211     // TODO: only add remote layer if there are log entries?
212     // apply layer edit log
213     const QString sql = QStringLiteral( "SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( offlineLayer->id() );
214     const int layerId = sqlQueryInt( database.get(), sql, -1 );
215 
216     if ( layerId == -1 )
217     {
218       QgsDebugMsgLevel( QStringLiteral( "Skipping offline layer %1 because it failed to determine the offline editing layer id" ).arg( offlineLayer->id() ), 4 );
219       continue;
220     }
221 
222     remoteLayersByOfflineId.insert( layerId, remoteLayer );
223     offlineLayersByOfflineId.insert( layerId, offlineLayer );
224   }
225 
226   QgsDebugMsgLevel( QStringLiteral( "Found %1 offline layers in total" ).arg( offlineLayersByOfflineId.count() ), 4 );
227 
228   QMap<QPair<QString, QString>, std::shared_ptr<QgsTransactionGroup>> transactionGroups;
229   if ( useTransaction )
230   {
231     for ( const std::shared_ptr<QgsVectorLayer> &remoteLayer : std::as_const( remoteLayersByOfflineId ) )
232     {
233       const QString connectionString = QgsTransaction::connectionString( remoteLayer->source() );
234       const QPair<QString, QString> pair( remoteLayer->providerType(), connectionString );
235       std::shared_ptr<QgsTransactionGroup> transactionGroup = transactionGroups.value( pair );
236 
237       if ( !transactionGroup.get() )
238         transactionGroup = std::make_shared<QgsTransactionGroup>();
239 
240       if ( !transactionGroup->addLayer( remoteLayer.get() ) )
241       {
242         QgsDebugMsgLevel( QStringLiteral( "Failed to add a layer %1 into transaction group, will be modified without transaction" ).arg( remoteLayer->name() ), 4 );
243         continue;
244       }
245 
246       transactionGroups.insert( pair, transactionGroup );
247     }
248 
249     QgsDebugMsgLevel( QStringLiteral( "Created %1 transaction groups" ).arg( transactionGroups.count() ), 4 );
250   }
251 
252   const QList<int> offlineIds = remoteLayersByOfflineId.keys();
253   for ( int offlineLayerId : offlineIds )
254   {
255     std::shared_ptr<QgsVectorLayer> remoteLayer = remoteLayersByOfflineId.value( offlineLayerId );
256     QgsVectorLayer *offlineLayer = offlineLayersByOfflineId.value( offlineLayerId );
257 
258     // NOTE: if transaction is enabled, the layer might be already in editing mode
259     if ( !remoteLayer->startEditing() && !remoteLayer->isEditable() )
260     {
261       QgsDebugMsgLevel( QStringLiteral( "Failed to turn layer %1 into editing mode" ).arg( remoteLayer->name() ), 4 );
262       continue;
263     }
264 
265     // TODO: only get commitNos of this layer?
266     const int commitNo = getCommitNo( database.get() );
267     QgsDebugMsgLevel( QStringLiteral( "Found %1 commits" ).arg( commitNo ), 4 );
268 
269     for ( int i = 0; i < commitNo; i++ )
270     {
271       QgsDebugMsgLevel( QStringLiteral( "Apply commits chronologically from %1" ).arg( offlineLayer->name() ), 4 );
272       // apply commits chronologically
273       applyAttributesAdded( remoteLayer.get(), database.get(), offlineLayerId, i );
274       applyAttributeValueChanges( offlineLayer, remoteLayer.get(), database.get(), offlineLayerId, i );
275       applyGeometryChanges( remoteLayer.get(), database.get(), offlineLayerId, i );
276     }
277 
278     applyFeaturesAdded( offlineLayer, remoteLayer.get(), database.get(), offlineLayerId );
279     applyFeaturesRemoved( remoteLayer.get(), database.get(), offlineLayerId );
280   }
281 
282 
283   for ( int offlineLayerId : offlineIds )
284   {
285     std::shared_ptr<QgsVectorLayer> remoteLayer = remoteLayersByOfflineId[offlineLayerId];
286     QgsVectorLayer *offlineLayer = offlineLayersByOfflineId[offlineLayerId];
287 
288     if ( !remoteLayer->isEditable() )
289       continue;
290 
291     if ( remoteLayer->commitChanges() )
292     {
293       // update fid lookup
294       updateFidLookup( remoteLayer.get(), database.get(), offlineLayerId );
295 
296       QString sql;
297       // clear edit log for this layer
298       sql = QStringLiteral( "DELETE FROM 'log_added_attrs' WHERE \"layer_id\" = %1" ).arg( offlineLayerId );
299       sqlExec( database.get(), sql );
300       sql = QStringLiteral( "DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( offlineLayerId );
301       sqlExec( database.get(), sql );
302       sql = QStringLiteral( "DELETE FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( offlineLayerId );
303       sqlExec( database.get(), sql );
304       sql = QStringLiteral( "DELETE FROM 'log_feature_updates' WHERE \"layer_id\" = %1" ).arg( offlineLayerId );
305       sqlExec( database.get(), sql );
306       sql = QStringLiteral( "DELETE FROM 'log_geometry_updates' WHERE \"layer_id\" = %1" ).arg( offlineLayerId );
307       sqlExec( database.get(), sql );
308     }
309     else
310     {
311       showWarning( remoteLayer->commitErrors().join( QLatin1Char( '\n' ) ) );
312     }
313 
314     // Invalidate the connection to force a reload if the project is put offline
315     // again with the same path
316     offlineLayer->dataProvider()->invalidateConnections( QgsDataSourceUri( offlineLayer->source() ).database() );
317 
318     remoteLayer->reload(); //update with other changes
319     offlineLayer->setDataSource( remoteLayer->source(), remoteLayer->name(), remoteLayer->dataProvider()->name() );
320 
321     // remove offline layer properties
322     offlineLayer->removeCustomProperty( CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE );
323 
324     // remove original layer source and information
325     offlineLayer->removeCustomProperty( CUSTOM_PROPERTY_REMOTE_SOURCE );
326     offlineLayer->removeCustomProperty( CUSTOM_PROPERTY_REMOTE_PROVIDER );
327     offlineLayer->removeCustomProperty( CUSTOM_PROPERTY_ORIGINAL_LAYERID );
328     offlineLayer->removeCustomProperty( CUSTOM_PROPERTY_LAYERNAME_SUFFIX );
329 
330     // remove connected signals
331     disconnect( offlineLayer, &QgsVectorLayer::editingStarted, this, &QgsOfflineEditing::startListenFeatureChanges );
332     disconnect( offlineLayer, &QgsVectorLayer::editingStopped, this, &QgsOfflineEditing::stopListenFeatureChanges );
333 
334     //add constrainst of fields that use defaultValueClauses from provider on original
335     const QgsFields fields = remoteLayer->fields();
336     for ( const QgsField &field : fields )
337     {
338       if ( !remoteLayer->dataProvider()->defaultValueClause( remoteLayer->fields().fieldOriginIndex( remoteLayer->fields().indexOf( field.name() ) ) ).isEmpty() )
339       {
340         offlineLayer->setFieldConstraint( offlineLayer->fields().indexOf( field.name() ), QgsFieldConstraints::ConstraintNotNull );
341       }
342     }
343   }
344 
345   // disable offline project
346   const QString projectTitle = QgsProject::instance()->title().remove( QRegularExpression( " \\(offline\\)$" ) );
347   QgsProject::instance()->setTitle( projectTitle );
348   QgsProject::instance()->removeEntry( PROJECT_ENTRY_SCOPE_OFFLINE, PROJECT_ENTRY_KEY_OFFLINE_DB_PATH );
349   // reset commitNo
350   const QString sql = QStringLiteral( "UPDATE 'log_indices' SET 'last_index' = 0 WHERE \"name\" = 'commit_no'" );
351   sqlExec( database.get(), sql );
352   emit progressStopped();
353 }
354 
initializeSpatialMetadata(sqlite3 * sqlite_handle)355 void QgsOfflineEditing::initializeSpatialMetadata( sqlite3 *sqlite_handle )
356 {
357 #ifdef HAVE_SPATIALITE
358   // attempting to perform self-initialization for a newly created DB
359   if ( !sqlite_handle )
360     return;
361   // checking if this DB is really empty
362   char **results = nullptr;
363   int rows, columns;
364   int ret = sqlite3_get_table( sqlite_handle, "select count(*) from sqlite_master", &results, &rows, &columns, nullptr );
365   if ( ret != SQLITE_OK )
366     return;
367   int count = 0;
368   if ( rows >= 1 )
369   {
370     for ( int i = 1; i <= rows; i++ )
371       count = atoi( results[( i * columns ) + 0] );
372   }
373 
374   sqlite3_free_table( results );
375 
376   if ( count > 0 )
377     return;
378 
379   bool above41 = false;
380   ret = sqlite3_get_table( sqlite_handle, "select spatialite_version()", &results, &rows, &columns, nullptr );
381   if ( ret == SQLITE_OK && rows == 1 && columns == 1 )
382   {
383     const QString version = QString::fromUtf8( results[1] );
384 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
385     QStringList parts = version.split( ' ', QString::SkipEmptyParts );
386 #else
387     const QStringList parts = version.split( ' ', Qt::SkipEmptyParts );
388 #endif
389     if ( !parts.empty() )
390     {
391 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
392       QStringList verparts = parts.at( 0 ).split( '.', QString::SkipEmptyParts );
393 #else
394       const QStringList verparts = parts.at( 0 ).split( '.', Qt::SkipEmptyParts );
395 #endif
396       above41 = verparts.size() >= 2 && ( verparts.at( 0 ).toInt() > 4 || ( verparts.at( 0 ).toInt() == 4 && verparts.at( 1 ).toInt() >= 1 ) );
397     }
398   }
399 
400   sqlite3_free_table( results );
401 
402   // all right, it's empty: proceeding to initialize
403   char *errMsg = nullptr;
404   ret = sqlite3_exec( sqlite_handle, above41 ? "SELECT InitSpatialMetadata(1)" : "SELECT InitSpatialMetadata()", nullptr, nullptr, &errMsg );
405 
406   if ( ret != SQLITE_OK )
407   {
408     QString errCause = tr( "Unable to initialize SpatialMetadata:\n" );
409     errCause += QString::fromUtf8( errMsg );
410     showWarning( errCause );
411     sqlite3_free( errMsg );
412     return;
413   }
414   spatial_ref_sys_init( sqlite_handle, 0 );
415 #else
416   ( void )sqlite_handle;
417 #endif
418 }
419 
createOfflineDb(const QString & offlineDbPath,ContainerType containerType)420 bool QgsOfflineEditing::createOfflineDb( const QString &offlineDbPath, ContainerType containerType )
421 {
422   int ret;
423   char *errMsg = nullptr;
424   const QFile newDb( offlineDbPath );
425   if ( newDb.exists() )
426   {
427     QFile::remove( offlineDbPath );
428   }
429 
430   // see also QgsNewSpatialiteLayerDialog::createDb()
431 
432   const QFileInfo fullPath = QFileInfo( offlineDbPath );
433   const QDir path = fullPath.dir();
434 
435   // Must be sure there is destination directory ~/.qgis
436   QDir().mkpath( path.absolutePath() );
437 
438   // creating/opening the new database
439   const QString dbPath = newDb.fileName();
440 
441   // creating geopackage
442   switch ( containerType )
443   {
444     case GPKG:
445     {
446       OGRSFDriverH hGpkgDriver = OGRGetDriverByName( "GPKG" );
447       if ( !hGpkgDriver )
448       {
449         showWarning( tr( "Creation of database failed. GeoPackage driver not found." ) );
450         return false;
451       }
452 
453       const gdal::ogr_datasource_unique_ptr hDS( OGR_Dr_CreateDataSource( hGpkgDriver, dbPath.toUtf8().constData(), nullptr ) );
454       if ( !hDS )
455       {
456         showWarning( tr( "Creation of database failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
457         return false;
458       }
459       break;
460     }
461     case SpatiaLite:
462     {
463       break;
464     }
465   }
466 
467   spatialite_database_unique_ptr database;
468   ret = database.open_v2( dbPath, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr );
469   if ( ret )
470   {
471     // an error occurred
472     QString errCause = tr( "Could not create a new database\n" );
473     errCause += database.errorMessage();
474     showWarning( errCause );
475     return false;
476   }
477   // activating Foreign Key constraints
478   ret = sqlite3_exec( database.get(), "PRAGMA foreign_keys = 1", nullptr, nullptr, &errMsg );
479   if ( ret != SQLITE_OK )
480   {
481     showWarning( tr( "Unable to activate FOREIGN_KEY constraints" ) );
482     sqlite3_free( errMsg );
483     return false;
484   }
485   initializeSpatialMetadata( database.get() );
486   return true;
487 }
488 
createLoggingTables(sqlite3 * db)489 void QgsOfflineEditing::createLoggingTables( sqlite3 *db )
490 {
491   // indices
492   QString sql = QStringLiteral( "CREATE TABLE 'log_indices' ('name' TEXT, 'last_index' INTEGER)" );
493   sqlExec( db, sql );
494 
495   sql = QStringLiteral( "INSERT INTO 'log_indices' VALUES ('commit_no', 0)" );
496   sqlExec( db, sql );
497 
498   sql = QStringLiteral( "INSERT INTO 'log_indices' VALUES ('layer_id', 0)" );
499   sqlExec( db, sql );
500 
501   // layername <-> layer id
502   sql = QStringLiteral( "CREATE TABLE 'log_layer_ids' ('id' INTEGER, 'qgis_id' TEXT)" );
503   sqlExec( db, sql );
504 
505   // offline fid <-> remote fid
506   sql = QStringLiteral( "CREATE TABLE 'log_fids' ('layer_id' INTEGER, 'offline_fid' INTEGER, 'remote_fid' INTEGER, 'remote_pk' TEXT)" );
507   sqlExec( db, sql );
508 
509   // added attributes
510   sql = QStringLiteral( "CREATE TABLE 'log_added_attrs' ('layer_id' INTEGER, 'commit_no' INTEGER, " );
511   sql += QLatin1String( "'name' TEXT, 'type' INTEGER, 'length' INTEGER, 'precision' INTEGER, 'comment' TEXT)" );
512   sqlExec( db, sql );
513 
514   // added features
515   sql = QStringLiteral( "CREATE TABLE 'log_added_features' ('layer_id' INTEGER, 'fid' INTEGER)" );
516   sqlExec( db, sql );
517 
518   // removed features
519   sql = QStringLiteral( "CREATE TABLE 'log_removed_features' ('layer_id' INTEGER, 'fid' INTEGER)" );
520   sqlExec( db, sql );
521 
522   // feature updates
523   sql = QStringLiteral( "CREATE TABLE 'log_feature_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'attr' INTEGER, 'value' TEXT)" );
524   sqlExec( db, sql );
525 
526   // geometry updates
527   sql = QStringLiteral( "CREATE TABLE 'log_geometry_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'geom_wkt' TEXT)" );
528   sqlExec( db, sql );
529 
530   /* TODO: other logging tables
531     - attr delete (not supported by SpatiaLite provider)
532   */
533 }
534 
convertToOfflineLayer(QgsVectorLayer * layer,sqlite3 * db,const QString & offlineDbPath,bool onlySelected,ContainerType containerType,const QString & layerNameSuffix)535 void QgsOfflineEditing::convertToOfflineLayer( QgsVectorLayer *layer, sqlite3 *db, const QString &offlineDbPath, bool onlySelected, ContainerType containerType, const QString &layerNameSuffix )
536 {
537   if ( !layer || !layer->isValid() )
538   {
539     QgsDebugMsgLevel( QStringLiteral( "Layer %1 is invalid and cannot be copied" ).arg( layer ? layer->id() : QStringLiteral( "<UNKNOWN>" ) ), 4 );
540     return;
541   }
542 
543   const QString tableName = layer->id();
544   QgsDebugMsgLevel( QStringLiteral( "Creating offline table %1 ..." ).arg( tableName ), 4 );
545 
546   // new layer
547   std::unique_ptr<QgsVectorLayer> newLayer;
548 
549   switch ( containerType )
550   {
551     case SpatiaLite:
552     {
553 #ifdef HAVE_SPATIALITE
554       // create table
555       QString sql = QStringLiteral( "CREATE TABLE '%1' (" ).arg( tableName );
556       QString delim;
557       const QgsFields providerFields = layer->dataProvider()->fields();
558       for ( const auto &field : providerFields )
559       {
560         QString dataType;
561         const QVariant::Type type = field.type();
562         if ( type == QVariant::Int || type == QVariant::LongLong )
563         {
564           dataType = QStringLiteral( "INTEGER" );
565         }
566         else if ( type == QVariant::Double )
567         {
568           dataType = QStringLiteral( "REAL" );
569         }
570         else if ( type == QVariant::String )
571         {
572           dataType = QStringLiteral( "TEXT" );
573         }
574         else if ( type == QVariant::StringList  || type == QVariant::List )
575         {
576           dataType = QStringLiteral( "TEXT" );
577           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() ) );
578         }
579         else
580         {
581           showWarning( tr( "%1: Unknown data type %2. Not using type affinity for the field." ).arg( field.name(), QVariant::typeToName( type ) ) );
582         }
583 
584         sql += delim + QStringLiteral( "'%1' %2" ).arg( field.name(), dataType );
585         delim = ',';
586       }
587       sql += ')';
588 
589       int rc = sqlExec( db, sql );
590 
591       // add geometry column
592       if ( layer->isSpatial() )
593       {
594         const QgsWkbTypes::Type sourceWkbType = layer->wkbType();
595 
596         QString geomType;
597         switch ( QgsWkbTypes::flatType( sourceWkbType ) )
598         {
599           case QgsWkbTypes::Point:
600             geomType = QStringLiteral( "POINT" );
601             break;
602           case QgsWkbTypes::MultiPoint:
603             geomType = QStringLiteral( "MULTIPOINT" );
604             break;
605           case QgsWkbTypes::LineString:
606             geomType = QStringLiteral( "LINESTRING" );
607             break;
608           case QgsWkbTypes::MultiLineString:
609             geomType = QStringLiteral( "MULTILINESTRING" );
610             break;
611           case QgsWkbTypes::Polygon:
612             geomType = QStringLiteral( "POLYGON" );
613             break;
614           case QgsWkbTypes::MultiPolygon:
615             geomType = QStringLiteral( "MULTIPOLYGON" );
616             break;
617           default:
618             showWarning( tr( "Layer %1 has unsupported geometry type %2." ).arg( layer->name(), QgsWkbTypes::displayString( layer->wkbType() ) ) );
619             break;
620         };
621 
622         QString zmInfo = QStringLiteral( "XY" );
623 
624         if ( QgsWkbTypes::hasZ( sourceWkbType ) )
625           zmInfo += 'Z';
626         if ( QgsWkbTypes::hasM( sourceWkbType ) )
627           zmInfo += 'M';
628 
629         QString epsgCode;
630 
631         if ( layer->crs().authid().startsWith( QLatin1String( "EPSG:" ), Qt::CaseInsensitive ) )
632         {
633           epsgCode = layer->crs().authid().mid( 5 );
634         }
635         else
636         {
637           epsgCode = '0';
638           showWarning( tr( "Layer %1 has unsupported Coordinate Reference System (%2)." ).arg( layer->name(), layer->crs().authid() ) );
639         }
640 
641         const QString sqlAddGeom = QStringLiteral( "SELECT AddGeometryColumn('%1', 'Geometry', %2, '%3', '%4')" )
642                                    .arg( tableName, epsgCode, geomType, zmInfo );
643 
644         // create spatial index
645         const QString sqlCreateIndex = QStringLiteral( "SELECT CreateSpatialIndex('%1', 'Geometry')" ).arg( tableName );
646 
647         if ( rc == SQLITE_OK )
648         {
649           rc = sqlExec( db, sqlAddGeom );
650           if ( rc == SQLITE_OK )
651           {
652             rc = sqlExec( db, sqlCreateIndex );
653           }
654         }
655       }
656 
657       if ( rc != SQLITE_OK )
658       {
659         showWarning( tr( "Filling SpatiaLite for layer %1 failed" ).arg( layer->name() ) );
660         return;
661       }
662 
663       // add new layer
664       const QString connectionString = QStringLiteral( "dbname='%1' table='%2'%3 sql=" )
665                                        .arg( offlineDbPath,
666                                              tableName, layer->isSpatial() ? "(Geometry)" : "" );
667       const QgsVectorLayer::LayerOptions options { QgsProject::instance()->transformContext() };
668       newLayer = std::make_unique<QgsVectorLayer>( connectionString,
669                  layer->name() + layerNameSuffix, QStringLiteral( "spatialite" ), options );
670       break;
671 
672 #else
673       showWarning( tr( "No Spatialite support available" ) );
674       return;
675 #endif
676     }
677 
678     case GPKG:
679     {
680       // Set options
681       char **options = nullptr;
682 
683       options = CSLSetNameValue( options, "OVERWRITE", "YES" );
684       options = CSLSetNameValue( options, "IDENTIFIER", tr( "%1 (offline)" ).arg( layer->id() ).toUtf8().constData() );
685       options = CSLSetNameValue( options, "DESCRIPTION", layer->dataComment().toUtf8().constData() );
686 
687       //the FID-name should not exist in the original data
688       const QString fidBase( QStringLiteral( "fid" ) );
689       QString fid = fidBase;
690       int counter = 1;
691       while ( layer->dataProvider()->fields().lookupField( fid ) >= 0 && counter < 10000 )
692       {
693         fid = fidBase + '_' + QString::number( counter );
694         counter++;
695       }
696       if ( counter == 10000 )
697       {
698         showWarning( tr( "Cannot make FID-name for GPKG " ) );
699         return;
700       }
701 
702       options = CSLSetNameValue( options, "FID", fid.toUtf8().constData() );
703 
704       if ( layer->isSpatial() )
705       {
706         options = CSLSetNameValue( options, "GEOMETRY_COLUMN", "geom" );
707         options = CSLSetNameValue( options, "SPATIAL_INDEX", "YES" );
708       }
709 
710       OGRSFDriverH hDriver = nullptr;
711       OGRSpatialReferenceH hSRS = QgsOgrUtils::crsToOGRSpatialReference( layer->crs() );
712       gdal::ogr_datasource_unique_ptr hDS( OGROpen( offlineDbPath.toUtf8().constData(), true, &hDriver ) );
713       OGRLayerH hLayer = OGR_DS_CreateLayer( hDS.get(), tableName.toUtf8().constData(), hSRS, static_cast<OGRwkbGeometryType>( layer->wkbType() ), options );
714       CSLDestroy( options );
715       if ( hSRS )
716         OSRRelease( hSRS );
717       if ( !hLayer )
718       {
719         showWarning( tr( "Creation of layer failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
720         return;
721       }
722 
723       const QgsFields providerFields = layer->dataProvider()->fields();
724       for ( const auto &field : providerFields )
725       {
726         const QString fieldName( field.name() );
727         const QVariant::Type type = field.type();
728         OGRFieldType ogrType( OFTString );
729         OGRFieldSubType ogrSubType = OFSTNone;
730         if ( type == QVariant::Int )
731           ogrType = OFTInteger;
732         else if ( type == QVariant::LongLong )
733           ogrType = OFTInteger64;
734         else if ( type == QVariant::Double )
735           ogrType = OFTReal;
736         else if ( type == QVariant::Time )
737           ogrType = OFTTime;
738         else if ( type == QVariant::Date )
739           ogrType = OFTDate;
740         else if ( type == QVariant::DateTime )
741           ogrType = OFTDateTime;
742         else if ( type == QVariant::Bool )
743         {
744           ogrType = OFTInteger;
745           ogrSubType = OFSTBoolean;
746         }
747         else if ( type == QVariant::StringList || type == QVariant::List )
748         {
749           ogrType = OFTString;
750           ogrSubType = OFSTJSON;
751           showWarning( tr( "Field '%1' from layer %2 has been converted from a list to a JSON-formatted string value." ).arg( fieldName, layer->name() ) );
752         }
753         else
754           ogrType = OFTString;
755 
756         const int ogrWidth = field.length();
757 
758         const gdal::ogr_field_def_unique_ptr fld( OGR_Fld_Create( fieldName.toUtf8().constData(), ogrType ) );
759         OGR_Fld_SetWidth( fld.get(), ogrWidth );
760         if ( ogrSubType != OFSTNone )
761           OGR_Fld_SetSubType( fld.get(), ogrSubType );
762 
763         if ( OGR_L_CreateField( hLayer, fld.get(), true ) != OGRERR_NONE )
764         {
765           showWarning( tr( "Creation of field %1 failed (OGR error: %2)" )
766                        .arg( fieldName, QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
767           return;
768         }
769       }
770 
771       // In GDAL >= 2.0, the driver implements a deferred creation strategy, so
772       // issue a command that will force table creation
773       CPLErrorReset();
774       OGR_L_ResetReading( hLayer );
775       if ( CPLGetLastErrorType() != CE_None )
776       {
777         const QString msg( tr( "Creation of layer failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
778         showWarning( msg );
779         return;
780       }
781       hDS.reset();
782 
783       const QString uri = QStringLiteral( "%1|layername=%2" ).arg( offlineDbPath,  tableName );
784       const QgsVectorLayer::LayerOptions layerOptions { QgsProject::instance()->transformContext() };
785       newLayer = std::make_unique<QgsVectorLayer>( uri, layer->name() + layerNameSuffix, QStringLiteral( "ogr" ), layerOptions );
786       break;
787     }
788   }
789 
790   if ( newLayer && newLayer->isValid() )
791   {
792 
793     // copy features
794     newLayer->startEditing();
795     QgsFeature f;
796 
797     QgsFeatureRequest req;
798 
799     if ( onlySelected )
800     {
801       const QgsFeatureIds selectedFids = layer->selectedFeatureIds();
802       if ( !selectedFids.isEmpty() )
803         req.setFilterFids( selectedFids );
804     }
805 
806     QgsFeatureIterator fit = layer->dataProvider()->getFeatures( req );
807 
808     if ( req.filterType() == QgsFeatureRequest::FilterFids )
809     {
810       emit progressModeSet( QgsOfflineEditing::CopyFeatures, layer->selectedFeatureIds().size() );
811     }
812     else
813     {
814       emit progressModeSet( QgsOfflineEditing::CopyFeatures, layer->dataProvider()->featureCount() );
815     }
816     long long featureCount = 1;
817     const int remotePkIdx = getLayerPkIdx( layer );
818 
819     QList<QgsFeatureId> remoteFeatureIds;
820     QStringList remoteFeaturePks;
821     while ( fit.nextFeature( f ) )
822     {
823       remoteFeatureIds << f.id();
824       remoteFeaturePks << ( remotePkIdx >= 0 ? f.attribute( remotePkIdx ).toString() : QString() );
825 
826       // NOTE: SpatiaLite provider ignores position of geometry column
827       // fill gap in QgsAttributeMap if geometry column is not last (WORKAROUND)
828       int column = 0;
829       const QgsAttributes attrs = f.attributes();
830       // on GPKG newAttrs has an addition FID attribute, so we have to add a dummy in the original set
831       QgsAttributes newAttrs( containerType == GPKG ? attrs.count() + 1 : attrs.count() );
832       for ( int it = 0; it < attrs.count(); ++it )
833       {
834         QVariant attr = attrs.at( it );
835         if ( layer->fields().at( it ).type() == QVariant::StringList || layer->fields().at( it ).type() == QVariant::List )
836         {
837           attr = QgsJsonUtils::encodeValue( attr );
838         }
839         newAttrs[column++] = attr;
840       }
841       f.setAttributes( newAttrs );
842 
843       newLayer->addFeature( f );
844 
845       emit progressUpdated( featureCount++ );
846     }
847     if ( newLayer->commitChanges() )
848     {
849       emit progressModeSet( QgsOfflineEditing::ProcessFeatures, layer->dataProvider()->featureCount() );
850       featureCount = 1;
851 
852       // update feature id lookup
853       const int layerId = getOrCreateLayerId( db, layer->id() );
854       QList<QgsFeatureId> offlineFeatureIds;
855 
856       QgsFeatureIterator fit = newLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setNoAttributes() );
857       while ( fit.nextFeature( f ) )
858       {
859         offlineFeatureIds << f.id();
860       }
861 
862       // NOTE: insert fids in this loop, as the db is locked during newLayer->nextFeature()
863       sqlExec( db, QStringLiteral( "BEGIN" ) );
864       const int remoteCount = remoteFeatureIds.size();
865       for ( int i = 0; i < remoteCount; i++ )
866       {
867         // Check if the online feature has been fetched (WFS download aborted for some reason)
868         if ( i < offlineFeatureIds.count() )
869         {
870           addFidLookup( db, layerId, offlineFeatureIds.at( i ), remoteFeatureIds.at( i ), remoteFeaturePks.at( i ) );
871         }
872         else
873         {
874           showWarning( tr( "Feature cannot be copied to the offline layer, please check if the online layer '%1' is still accessible." ).arg( layer->name() ) );
875           return;
876         }
877         emit progressUpdated( featureCount++ );
878       }
879       sqlExec( db, QStringLiteral( "COMMIT" ) );
880     }
881     else
882     {
883       showWarning( newLayer->commitErrors().join( QLatin1Char( '\n' ) ) );
884     }
885 
886     // mark as offline layer
887     layer->setCustomProperty( CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE, true );
888 
889     // store original layer source and information
890     layer->setCustomProperty( CUSTOM_PROPERTY_REMOTE_SOURCE, layer->source() );
891     layer->setCustomProperty( CUSTOM_PROPERTY_REMOTE_PROVIDER, layer->providerType() );
892     layer->setCustomProperty( CUSTOM_PROPERTY_ORIGINAL_LAYERID, layer->id() );
893     layer->setCustomProperty( CUSTOM_PROPERTY_LAYERNAME_SUFFIX, layerNameSuffix );
894 
895     //remove constrainst of fields that use defaultValueClauses from provider on original
896     const QgsFields fields = layer->fields();
897     QStringList notNullFieldNames;
898     for ( const QgsField &field : fields )
899     {
900       if ( !layer->dataProvider()->defaultValueClause( layer->fields().fieldOriginIndex( layer->fields().indexOf( field.name() ) ) ).isEmpty() )
901       {
902         notNullFieldNames << field.name();
903       }
904     }
905 
906     layer->setDataSource( newLayer->source(), newLayer->name(), newLayer->dataProvider()->name() );
907 
908     for ( const QgsField &field : fields ) //QString &fieldName : fieldsToRemoveConstraint )
909     {
910       const int index = layer->fields().indexOf( field.name() );
911       if ( index > -1 )
912       {
913         // restore unique value constraints coming from original data provider
914         if ( field.constraints().constraints() & QgsFieldConstraints::ConstraintUnique )
915           layer->setFieldConstraint( index, QgsFieldConstraints::ConstraintUnique );
916 
917         // remove any undesired not null constraints coming from original data provider
918         if ( notNullFieldNames.contains( field.name() ) )
919         {
920           notNullFieldNames.removeAll( field.name() );
921           layer->removeFieldConstraint( index, QgsFieldConstraints::ConstraintNotNull );
922         }
923       }
924     }
925 
926     setupLayer( layer );
927   }
928   return;
929 }
930 
applyAttributesAdded(QgsVectorLayer * remoteLayer,sqlite3 * db,int layerId,int commitNo)931 void QgsOfflineEditing::applyAttributesAdded( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
932 {
933   Q_ASSERT( remoteLayer );
934 
935   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 );
936   QList<QgsField> fields = sqlQueryAttributesAdded( db, sql );
937 
938   const QgsVectorDataProvider *provider = remoteLayer->dataProvider();
939   const QList<QgsVectorDataProvider::NativeType> nativeTypes = provider->nativeTypes();
940 
941   // NOTE: uses last matching QVariant::Type of nativeTypes
942   QMap < QVariant::Type, QString /*typeName*/ > typeNameLookup;
943   for ( int i = 0; i < nativeTypes.size(); i++ )
944   {
945     const QgsVectorDataProvider::NativeType nativeType = nativeTypes.at( i );
946     typeNameLookup[ nativeType.mType ] = nativeType.mTypeName;
947   }
948 
949   emit progressModeSet( QgsOfflineEditing::AddFields, fields.size() );
950 
951   for ( int i = 0; i < fields.size(); i++ )
952   {
953     // lookup typename from layer provider
954     QgsField field = fields[i];
955     if ( typeNameLookup.contains( field.type() ) )
956     {
957       const QString typeName = typeNameLookup[ field.type()];
958       field.setTypeName( typeName );
959       remoteLayer->addAttribute( field );
960     }
961     else
962     {
963       showWarning( QStringLiteral( "Could not add attribute '%1' of type %2" ).arg( field.name() ).arg( field.type() ) );
964     }
965 
966     emit progressUpdated( i + 1 );
967   }
968 }
969 
applyFeaturesAdded(QgsVectorLayer * offlineLayer,QgsVectorLayer * remoteLayer,sqlite3 * db,int layerId)970 void QgsOfflineEditing::applyFeaturesAdded( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
971 {
972   Q_ASSERT( offlineLayer );
973   Q_ASSERT( remoteLayer );
974 
975   const QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
976   const QList<int> featureIdInts = sqlQueryInts( db, sql );
977   QgsFeatureIds newFeatureIds;
978   for ( const int id : featureIdInts )
979   {
980     newFeatureIds << id;
981   }
982 
983   QgsExpressionContext context = remoteLayer->createExpressionContext();
984 
985   // get new features from offline layer
986   QgsFeatureList features;
987   QgsFeatureIterator it = offlineLayer->getFeatures( QgsFeatureRequest().setFilterFids( newFeatureIds ) );
988   QgsFeature feature;
989   while ( it.nextFeature( feature ) )
990   {
991     features << feature;
992   }
993 
994   // copy features to remote layer
995   emit progressModeSet( QgsOfflineEditing::AddFeatures, features.size() );
996 
997   int i = 1;
998   const int newAttrsCount = remoteLayer->fields().count();
999   for ( QgsFeatureList::iterator it = features.begin(); it != features.end(); ++it )
1000   {
1001     // NOTE: SpatiaLite provider ignores position of geometry column
1002     // restore gap in QgsAttributeMap if geometry column is not last (WORKAROUND)
1003     const QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
1004     QgsAttributes newAttrs( newAttrsCount );
1005     const QgsAttributes attrs = it->attributes();
1006     for ( int it = 0; it < attrs.count(); ++it )
1007     {
1008       const int remoteAttributeIndex = attrLookup.value( it, -1 );
1009       // if virtual or non existing field
1010       if ( remoteAttributeIndex == -1 )
1011         continue;
1012       QVariant attr = attrs.at( it );
1013       if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::StringList )
1014       {
1015         if ( attr.type() == QVariant::StringList || attr.type() == QVariant::List )
1016         {
1017           attr = attr.toStringList();
1018         }
1019         else
1020         {
1021           attr = QgsJsonUtils::parseArray( attr.toString(), QVariant::String );
1022         }
1023       }
1024       else if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::List )
1025       {
1026         if ( attr.type() == QVariant::StringList || attr.type() == QVariant::List )
1027         {
1028           attr = attr.toList();
1029         }
1030         else
1031         {
1032           attr = QgsJsonUtils::parseArray( attr.toString(), remoteLayer->fields().at( remoteAttributeIndex ).subType() );
1033         }
1034       }
1035       newAttrs[ remoteAttributeIndex ] = attr;
1036     }
1037 
1038     // respect constraints and provider default values
1039     QgsFeature f = QgsVectorLayerUtils::createFeature( remoteLayer, it->geometry(), newAttrs.toMap(), &context );
1040     remoteLayer->addFeature( f );
1041 
1042     emit progressUpdated( i++ );
1043   }
1044 }
1045 
applyFeaturesRemoved(QgsVectorLayer * remoteLayer,sqlite3 * db,int layerId)1046 void QgsOfflineEditing::applyFeaturesRemoved( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
1047 {
1048   Q_ASSERT( remoteLayer );
1049 
1050   const QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( layerId );
1051   const QgsFeatureIds values = sqlQueryFeaturesRemoved( db, sql );
1052 
1053   emit progressModeSet( QgsOfflineEditing::RemoveFeatures, values.size() );
1054 
1055   int i = 1;
1056   for ( QgsFeatureIds::const_iterator it = values.constBegin(); it != values.constEnd(); ++it )
1057   {
1058     const QgsFeatureId fid = remoteFid( db, layerId, *it, remoteLayer );
1059     remoteLayer->deleteFeature( fid );
1060 
1061     emit progressUpdated( i++ );
1062   }
1063 }
1064 
applyAttributeValueChanges(QgsVectorLayer * offlineLayer,QgsVectorLayer * remoteLayer,sqlite3 * db,int layerId,int commitNo)1065 void QgsOfflineEditing::applyAttributeValueChanges( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
1066 {
1067   Q_ASSERT( offlineLayer );
1068   Q_ASSERT( remoteLayer );
1069 
1070   const QString sql = QStringLiteral( "SELECT \"fid\", \"attr\", \"value\" FROM 'log_feature_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2 " ).arg( layerId ).arg( commitNo );
1071   const AttributeValueChanges values = sqlQueryAttributeValueChanges( db, sql );
1072 
1073   emit progressModeSet( QgsOfflineEditing::UpdateFeatures, values.size() );
1074 
1075   QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
1076 
1077   for ( int i = 0; i < values.size(); i++ )
1078   {
1079     const QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid, remoteLayer );
1080     QgsDebugMsgLevel( QStringLiteral( "Offline changeAttributeValue %1 = %2" ).arg( attrLookup[ values.at( i ).attr ] ).arg( values.at( i ).value ), 4 );
1081 
1082     const int remoteAttributeIndex = attrLookup[ values.at( i ).attr ];
1083     QVariant attr = values.at( i ).value;
1084     if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::StringList )
1085     {
1086       attr = QgsJsonUtils::parseArray( attr.toString(), QVariant::String );
1087     }
1088     else if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::List )
1089     {
1090       attr = QgsJsonUtils::parseArray( attr.toString(), remoteLayer->fields().at( remoteAttributeIndex ).subType() );
1091     }
1092 
1093     remoteLayer->changeAttributeValue( fid, remoteAttributeIndex, attr );
1094 
1095     emit progressUpdated( i + 1 );
1096   }
1097 }
1098 
applyGeometryChanges(QgsVectorLayer * remoteLayer,sqlite3 * db,int layerId,int commitNo)1099 void QgsOfflineEditing::applyGeometryChanges( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
1100 {
1101   Q_ASSERT( remoteLayer );
1102 
1103   const QString sql = QStringLiteral( "SELECT \"fid\", \"geom_wkt\" FROM 'log_geometry_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2" ).arg( layerId ).arg( commitNo );
1104   const GeometryChanges values = sqlQueryGeometryChanges( db, sql );
1105 
1106   emit progressModeSet( QgsOfflineEditing::UpdateGeometries, values.size() );
1107 
1108   for ( int i = 0; i < values.size(); i++ )
1109   {
1110     const QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid, remoteLayer );
1111     QgsGeometry newGeom = QgsGeometry::fromWkt( values.at( i ).geom_wkt );
1112     remoteLayer->changeGeometry( fid, newGeom );
1113 
1114     emit progressUpdated( i + 1 );
1115   }
1116 }
1117 
updateFidLookup(QgsVectorLayer * remoteLayer,sqlite3 * db,int layerId)1118 void QgsOfflineEditing::updateFidLookup( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
1119 {
1120   Q_ASSERT( remoteLayer );
1121 
1122   // update fid lookup for added features
1123 
1124   // get remote added fids
1125   // NOTE: use QMap for sorted fids
1126   QMap < QgsFeatureId, QString > newRemoteFids;
1127   QgsFeature f;
1128 
1129   QgsFeatureIterator fit = remoteLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setNoAttributes() );
1130 
1131   emit progressModeSet( QgsOfflineEditing::ProcessFeatures, remoteLayer->featureCount() );
1132 
1133   const int remotePkIdx = getLayerPkIdx( remoteLayer );
1134 
1135   int i = 1;
1136   while ( fit.nextFeature( f ) )
1137   {
1138     if ( offlineFid( db, layerId, f.id() ) == -1 )
1139     {
1140       newRemoteFids[ f.id()] = remotePkIdx >= 0 ? f.attribute( remotePkIdx ).toString() : QString();
1141     }
1142 
1143     emit progressUpdated( i++ );
1144   }
1145 
1146   // get local added fids
1147   // NOTE: fids are sorted
1148   const QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
1149   const QList<int> newOfflineFids = sqlQueryInts( db, sql );
1150 
1151   if ( newRemoteFids.size() != newOfflineFids.size() )
1152   {
1153     //showWarning( QString( "Different number of new features on offline layer (%1) and remote layer (%2)" ).arg(newOfflineFids.size()).arg(newRemoteFids.size()) );
1154   }
1155   else
1156   {
1157     // add new fid lookups
1158     i = 0;
1159     sqlExec( db, QStringLiteral( "BEGIN" ) );
1160     for ( QMap<QgsFeatureId, QString>::const_iterator it = newRemoteFids.constBegin(); it != newRemoteFids.constEnd(); ++it )
1161     {
1162       addFidLookup( db, layerId, newOfflineFids.at( i++ ), it.key(), it.value() );
1163     }
1164     sqlExec( db, QStringLiteral( "COMMIT" ) );
1165   }
1166 }
1167 
1168 // NOTE: use this to map column indices in case the remote geometry column is not last
attributeLookup(QgsVectorLayer * offlineLayer,QgsVectorLayer * remoteLayer)1169 QMap<int, int> QgsOfflineEditing::attributeLookup( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer )
1170 {
1171   Q_ASSERT( offlineLayer );
1172   Q_ASSERT( remoteLayer );
1173 
1174   const QgsAttributeList &offlineAttrs = offlineLayer->attributeList();
1175 
1176   QMap < int /*offline attr*/, int /*remote attr*/ > attrLookup;
1177   // NOTE: though offlineAttrs can have new attributes not yet synced, we take the amount of offlineAttrs
1178   // because we anyway only add mapping for the fields existing in remoteLayer (this because it could contain fid on 0)
1179   for ( int i = 0; i < offlineAttrs.size(); i++ )
1180   {
1181     if ( remoteLayer->fields().lookupField( offlineLayer->fields().field( i ).name() ) >= 0 )
1182       attrLookup.insert( offlineAttrs.at( i ), remoteLayer->fields().indexOf( offlineLayer->fields().field( i ).name() ) );
1183   }
1184 
1185   return attrLookup;
1186 }
1187 
showWarning(const QString & message)1188 void QgsOfflineEditing::showWarning( const QString &message )
1189 {
1190   emit warning( tr( "Offline Editing Plugin" ), message );
1191 }
1192 
openLoggingDb()1193 sqlite3_database_unique_ptr QgsOfflineEditing::openLoggingDb()
1194 {
1195   sqlite3_database_unique_ptr database;
1196   const QString dbPath = QgsProject::instance()->readEntry( PROJECT_ENTRY_SCOPE_OFFLINE, PROJECT_ENTRY_KEY_OFFLINE_DB_PATH );
1197   if ( !dbPath.isEmpty() )
1198   {
1199     const QString absoluteDbPath = QgsProject::instance()->readPath( dbPath );
1200     const int rc = database.open( absoluteDbPath );
1201     if ( rc != SQLITE_OK )
1202     {
1203       QgsDebugMsg( QStringLiteral( "Could not open the SpatiaLite logging database" ) );
1204       showWarning( tr( "Could not open the SpatiaLite logging database" ) );
1205     }
1206   }
1207   else
1208   {
1209     QgsDebugMsg( QStringLiteral( "dbPath is empty!" ) );
1210   }
1211   return database;
1212 }
1213 
getOrCreateLayerId(sqlite3 * db,const QString & qgisLayerId)1214 int QgsOfflineEditing::getOrCreateLayerId( sqlite3 *db, const QString &qgisLayerId )
1215 {
1216   QString sql = QStringLiteral( "SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( qgisLayerId );
1217   int layerId = sqlQueryInt( db, sql, -1 );
1218   if ( layerId == -1 )
1219   {
1220     // next layer id
1221     sql = QStringLiteral( "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'layer_id'" );
1222     const int newLayerId = sqlQueryInt( db, sql, -1 );
1223 
1224     // insert layer
1225     sql = QStringLiteral( "INSERT INTO 'log_layer_ids' VALUES (%1, '%2')" ).arg( newLayerId ).arg( qgisLayerId );
1226     sqlExec( db, sql );
1227 
1228     // increase layer_id
1229     // TODO: use trigger for auto increment?
1230     sql = QStringLiteral( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'layer_id'" ).arg( newLayerId + 1 );
1231     sqlExec( db, sql );
1232 
1233     layerId = newLayerId;
1234   }
1235 
1236   return layerId;
1237 }
1238 
getCommitNo(sqlite3 * db)1239 int QgsOfflineEditing::getCommitNo( sqlite3 *db )
1240 {
1241   const QString sql = QStringLiteral( "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'commit_no'" );
1242   return sqlQueryInt( db, sql, -1 );
1243 }
1244 
increaseCommitNo(sqlite3 * db)1245 void QgsOfflineEditing::increaseCommitNo( sqlite3 *db )
1246 {
1247   const QString sql = QStringLiteral( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'commit_no'" ).arg( getCommitNo( db ) + 1 );
1248   sqlExec( db, sql );
1249 }
1250 
addFidLookup(sqlite3 * db,int layerId,QgsFeatureId offlineFid,QgsFeatureId remoteFid,const QString & remotePk)1251 void QgsOfflineEditing::addFidLookup( sqlite3 *db, int layerId, QgsFeatureId offlineFid, QgsFeatureId remoteFid, const QString &remotePk )
1252 {
1253   const QString sql = QStringLiteral( "INSERT INTO 'log_fids' VALUES ( %1, %2, %3, %4 )" ).arg( layerId ).arg( offlineFid ).arg( remoteFid ).arg( sqlEscape( remotePk ) );
1254   sqlExec( db, sql );
1255 }
1256 
remoteFid(sqlite3 * db,int layerId,QgsFeatureId offlineFid,QgsVectorLayer * remoteLayer)1257 QgsFeatureId QgsOfflineEditing::remoteFid( sqlite3 *db, int layerId, QgsFeatureId offlineFid, QgsVectorLayer *remoteLayer )
1258 {
1259   const int pkIdx = getLayerPkIdx( remoteLayer );
1260 
1261   if ( pkIdx == -1 )
1262   {
1263     const QString sql = QStringLiteral( "SELECT \"remote_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"offline_fid\" = %2" ).arg( layerId ).arg( offlineFid );
1264     return sqlQueryInt( db, sql, -1 );
1265   }
1266 
1267   const QString sql = QStringLiteral( "SELECT \"remote_pk\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"offline_fid\" = %2" ).arg( layerId ).arg( offlineFid );
1268   QString defaultValue;
1269   const QString pkValue = sqlQueryStr( db, sql, defaultValue );
1270 
1271   if ( pkValue.isNull() )
1272   {
1273     return -1;
1274   }
1275 
1276   const QString pkFieldName = remoteLayer->fields().at( pkIdx ).name();
1277   QgsFeatureIterator fit = remoteLayer->getFeatures( QStringLiteral( " %1 = %2 " ).arg( pkFieldName ).arg( sqlEscape( pkValue ) ) );
1278   QgsFeature f;
1279   while ( fit.nextFeature( f ) )
1280     return f.id();
1281 
1282   return -1;
1283 }
1284 
offlineFid(sqlite3 * db,int layerId,QgsFeatureId remoteFid)1285 QgsFeatureId QgsOfflineEditing::offlineFid( sqlite3 *db, int layerId, QgsFeatureId remoteFid )
1286 {
1287   const QString sql = QStringLiteral( "SELECT \"offline_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"remote_fid\" = %2" ).arg( layerId ).arg( remoteFid );
1288   return sqlQueryInt( db, sql, -1 );
1289 }
1290 
isAddedFeature(sqlite3 * db,int layerId,QgsFeatureId fid)1291 bool QgsOfflineEditing::isAddedFeature( sqlite3 *db, int layerId, QgsFeatureId fid )
1292 {
1293   const QString sql = QStringLiteral( "SELECT COUNT(\"fid\") FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( fid );
1294   return ( sqlQueryInt( db, sql, 0 ) > 0 );
1295 }
1296 
sqlExec(sqlite3 * db,const QString & sql)1297 int QgsOfflineEditing::sqlExec( sqlite3 *db, const QString &sql )
1298 {
1299   char *errmsg = nullptr;
1300   const int rc = sqlite3_exec( db, sql.toUtf8(), nullptr, nullptr, &errmsg );
1301   if ( rc != SQLITE_OK )
1302   {
1303     showWarning( errmsg );
1304   }
1305   return rc;
1306 }
1307 
sqlQueryStr(sqlite3 * db,const QString & sql,QString & defaultValue)1308 QString QgsOfflineEditing::sqlQueryStr( sqlite3 *db, const QString &sql, QString &defaultValue )
1309 {
1310   sqlite3_stmt *stmt = nullptr;
1311   if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1312   {
1313     showWarning( sqlite3_errmsg( db ) );
1314     return defaultValue;
1315   }
1316 
1317   QString value = defaultValue;
1318   const int ret = sqlite3_step( stmt );
1319   if ( ret == SQLITE_ROW )
1320   {
1321     value = QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 0 ) ) );
1322   }
1323   sqlite3_finalize( stmt );
1324 
1325   return value;
1326 }
1327 
sqlQueryInt(sqlite3 * db,const QString & sql,int defaultValue)1328 int QgsOfflineEditing::sqlQueryInt( sqlite3 *db, const QString &sql, int defaultValue )
1329 {
1330   sqlite3_stmt *stmt = nullptr;
1331   if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1332   {
1333     showWarning( sqlite3_errmsg( db ) );
1334     return defaultValue;
1335   }
1336 
1337   int value = defaultValue;
1338   const int ret = sqlite3_step( stmt );
1339   if ( ret == SQLITE_ROW )
1340   {
1341     value = sqlite3_column_int( stmt, 0 );
1342   }
1343   sqlite3_finalize( stmt );
1344 
1345   return value;
1346 }
1347 
sqlQueryInts(sqlite3 * db,const QString & sql)1348 QList<int> QgsOfflineEditing::sqlQueryInts( sqlite3 *db, const QString &sql )
1349 {
1350   QList<int> values;
1351 
1352   sqlite3_stmt *stmt = nullptr;
1353   if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1354   {
1355     showWarning( sqlite3_errmsg( db ) );
1356     return values;
1357   }
1358 
1359   int ret = sqlite3_step( stmt );
1360   while ( ret == SQLITE_ROW )
1361   {
1362     values << sqlite3_column_int( stmt, 0 );
1363 
1364     ret = sqlite3_step( stmt );
1365   }
1366   sqlite3_finalize( stmt );
1367 
1368   return values;
1369 }
1370 
sqlQueryAttributesAdded(sqlite3 * db,const QString & sql)1371 QList<QgsField> QgsOfflineEditing::sqlQueryAttributesAdded( sqlite3 *db, const QString &sql )
1372 {
1373   QList<QgsField> values;
1374 
1375   sqlite3_stmt *stmt = nullptr;
1376   if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1377   {
1378     showWarning( sqlite3_errmsg( db ) );
1379     return values;
1380   }
1381 
1382   int ret = sqlite3_step( stmt );
1383   while ( ret == SQLITE_ROW )
1384   {
1385     const QgsField field( QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 0 ) ) ),
1386                           static_cast< QVariant::Type >( sqlite3_column_int( stmt, 1 ) ),
1387                           QString(), // typeName
1388                           sqlite3_column_int( stmt, 2 ),
1389                           sqlite3_column_int( stmt, 3 ),
1390                           QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 4 ) ) ) );
1391     values << field;
1392 
1393     ret = sqlite3_step( stmt );
1394   }
1395   sqlite3_finalize( stmt );
1396 
1397   return values;
1398 }
1399 
sqlQueryFeaturesRemoved(sqlite3 * db,const QString & sql)1400 QgsFeatureIds QgsOfflineEditing::sqlQueryFeaturesRemoved( sqlite3 *db, const QString &sql )
1401 {
1402   QgsFeatureIds values;
1403 
1404   sqlite3_stmt *stmt = nullptr;
1405   if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1406   {
1407     showWarning( sqlite3_errmsg( db ) );
1408     return values;
1409   }
1410 
1411   int ret = sqlite3_step( stmt );
1412   while ( ret == SQLITE_ROW )
1413   {
1414     values << sqlite3_column_int( stmt, 0 );
1415 
1416     ret = sqlite3_step( stmt );
1417   }
1418   sqlite3_finalize( stmt );
1419 
1420   return values;
1421 }
1422 
sqlQueryAttributeValueChanges(sqlite3 * db,const QString & sql)1423 QgsOfflineEditing::AttributeValueChanges QgsOfflineEditing::sqlQueryAttributeValueChanges( sqlite3 *db, const QString &sql )
1424 {
1425   AttributeValueChanges values;
1426 
1427   sqlite3_stmt *stmt = nullptr;
1428   if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1429   {
1430     showWarning( sqlite3_errmsg( db ) );
1431     return values;
1432   }
1433 
1434   int ret = sqlite3_step( stmt );
1435   while ( ret == SQLITE_ROW )
1436   {
1437     AttributeValueChange change;
1438     change.fid = sqlite3_column_int( stmt, 0 );
1439     change.attr = sqlite3_column_int( stmt, 1 );
1440     change.value = QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 2 ) ) );
1441     values << change;
1442 
1443     ret = sqlite3_step( stmt );
1444   }
1445   sqlite3_finalize( stmt );
1446 
1447   return values;
1448 }
1449 
sqlQueryGeometryChanges(sqlite3 * db,const QString & sql)1450 QgsOfflineEditing::GeometryChanges QgsOfflineEditing::sqlQueryGeometryChanges( sqlite3 *db, const QString &sql )
1451 {
1452   GeometryChanges values;
1453 
1454   sqlite3_stmt *stmt = nullptr;
1455   if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1456   {
1457     showWarning( sqlite3_errmsg( db ) );
1458     return values;
1459   }
1460 
1461   int ret = sqlite3_step( stmt );
1462   while ( ret == SQLITE_ROW )
1463   {
1464     GeometryChange change;
1465     change.fid = sqlite3_column_int( stmt, 0 );
1466     change.geom_wkt = QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 1 ) ) );
1467     values << change;
1468 
1469     ret = sqlite3_step( stmt );
1470   }
1471   sqlite3_finalize( stmt );
1472 
1473   return values;
1474 }
1475 
committedAttributesAdded(const QString & qgisLayerId,const QList<QgsField> & addedAttributes)1476 void QgsOfflineEditing::committedAttributesAdded( const QString &qgisLayerId, const QList<QgsField> &addedAttributes )
1477 {
1478   const sqlite3_database_unique_ptr database = openLoggingDb();
1479   if ( !database )
1480     return;
1481 
1482   // insert log
1483   const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1484   const int commitNo = getCommitNo( database.get() );
1485 
1486   for ( const QgsField &field : addedAttributes )
1487   {
1488     const QString sql = QStringLiteral( "INSERT INTO 'log_added_attrs' VALUES ( %1, %2, '%3', %4, %5, %6, '%7' )" )
1489                         .arg( layerId )
1490                         .arg( commitNo )
1491                         .arg( field.name() )
1492                         .arg( field.type() )
1493                         .arg( field.length() )
1494                         .arg( field.precision() )
1495                         .arg( field.comment() );
1496     sqlExec( database.get(), sql );
1497   }
1498 
1499   increaseCommitNo( database.get() );
1500 }
1501 
committedFeaturesAdded(const QString & qgisLayerId,const QgsFeatureList & addedFeatures)1502 void QgsOfflineEditing::committedFeaturesAdded( const QString &qgisLayerId, const QgsFeatureList &addedFeatures )
1503 {
1504   const sqlite3_database_unique_ptr database = openLoggingDb();
1505   if ( !database )
1506     return;
1507 
1508   // insert log
1509   const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1510 
1511   // get new feature ids from db
1512   QgsMapLayer *layer = QgsProject::instance()->mapLayer( qgisLayerId );
1513   const QString dataSourceString = layer->source();
1514   const QgsDataSourceUri uri = QgsDataSourceUri( dataSourceString );
1515 
1516   const QString offlinePath = QgsProject::instance()->readPath( QgsProject::instance()->readEntry( PROJECT_ENTRY_SCOPE_OFFLINE, PROJECT_ENTRY_KEY_OFFLINE_DB_PATH ) );
1517   QString tableName;
1518 
1519   if ( !offlinePath.contains( ".gpkg" ) )
1520   {
1521     tableName = uri.table();
1522   }
1523   else
1524   {
1525     QgsProviderMetadata *ogrProviderMetaData = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "ogr" ) );
1526     const QVariantMap decodedUri = ogrProviderMetaData->decodeUri( dataSourceString );
1527     tableName = decodedUri.value( QStringLiteral( "layerName" ) ).toString();
1528     if ( tableName.isEmpty() )
1529     {
1530       showWarning( tr( "Could not deduce table name from data source %1." ).arg( dataSourceString ) );
1531     }
1532   }
1533 
1534   // only store feature ids
1535   const QString sql = QStringLiteral( "SELECT ROWID FROM '%1' ORDER BY ROWID DESC LIMIT %2" ).arg( tableName ).arg( addedFeatures.size() );
1536   const QList<int> newFeatureIds = sqlQueryInts( database.get(), sql );
1537   for ( int i = newFeatureIds.size() - 1; i >= 0; i-- )
1538   {
1539     const QString sql = QStringLiteral( "INSERT INTO 'log_added_features' VALUES ( %1, %2 )" )
1540                         .arg( layerId )
1541                         .arg( newFeatureIds.at( i ) );
1542     sqlExec( database.get(), sql );
1543   }
1544 }
1545 
committedFeaturesRemoved(const QString & qgisLayerId,const QgsFeatureIds & deletedFeatureIds)1546 void QgsOfflineEditing::committedFeaturesRemoved( const QString &qgisLayerId, const QgsFeatureIds &deletedFeatureIds )
1547 {
1548   const sqlite3_database_unique_ptr database = openLoggingDb();
1549   if ( !database )
1550     return;
1551 
1552   // insert log
1553   const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1554 
1555   for ( const QgsFeatureId id : deletedFeatureIds )
1556   {
1557     if ( isAddedFeature( database.get(), layerId, id ) )
1558     {
1559       // remove from added features log
1560       const QString sql = QStringLiteral( "DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( id );
1561       sqlExec( database.get(), sql );
1562     }
1563     else
1564     {
1565       const QString sql = QStringLiteral( "INSERT INTO 'log_removed_features' VALUES ( %1, %2)" )
1566                           .arg( layerId )
1567                           .arg( id );
1568       sqlExec( database.get(), sql );
1569     }
1570   }
1571 }
1572 
committedAttributeValuesChanges(const QString & qgisLayerId,const QgsChangedAttributesMap & changedAttrsMap)1573 void QgsOfflineEditing::committedAttributeValuesChanges( const QString &qgisLayerId, const QgsChangedAttributesMap &changedAttrsMap )
1574 {
1575   const sqlite3_database_unique_ptr database = openLoggingDb();
1576   if ( !database )
1577     return;
1578 
1579   // insert log
1580   const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1581   const int commitNo = getCommitNo( database.get() );
1582 
1583   for ( QgsChangedAttributesMap::const_iterator cit = changedAttrsMap.begin(); cit != changedAttrsMap.end(); ++cit )
1584   {
1585     const QgsFeatureId fid = cit.key();
1586     if ( isAddedFeature( database.get(), layerId, fid ) )
1587     {
1588       // skip added features
1589       continue;
1590     }
1591     const QgsAttributeMap attrMap = cit.value();
1592     for ( QgsAttributeMap::const_iterator it = attrMap.constBegin(); it != attrMap.constEnd(); ++it )
1593     {
1594       QString value = it.value().type() == QVariant::StringList || it.value().type() == QVariant::List ? QgsJsonUtils::encodeValue( it.value() ) : it.value().toString();
1595       value.replace( QLatin1String( "'" ), QLatin1String( "''" ) ); // escape quote
1596       const QString sql = QStringLiteral( "INSERT INTO 'log_feature_updates' VALUES ( %1, %2, %3, %4, '%5' )" )
1597                           .arg( layerId )
1598                           .arg( commitNo )
1599                           .arg( fid )
1600                           .arg( it.key() ) // attribute
1601                           .arg( value );
1602       sqlExec( database.get(), sql );
1603     }
1604   }
1605 
1606   increaseCommitNo( database.get() );
1607 }
1608 
committedGeometriesChanges(const QString & qgisLayerId,const QgsGeometryMap & changedGeometries)1609 void QgsOfflineEditing::committedGeometriesChanges( const QString &qgisLayerId, const QgsGeometryMap &changedGeometries )
1610 {
1611   const sqlite3_database_unique_ptr database = openLoggingDb();
1612   if ( !database )
1613     return;
1614 
1615   // insert log
1616   const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1617   const int commitNo = getCommitNo( database.get() );
1618 
1619   for ( QgsGeometryMap::const_iterator it = changedGeometries.begin(); it != changedGeometries.end(); ++it )
1620   {
1621     const QgsFeatureId fid = it.key();
1622     if ( isAddedFeature( database.get(), layerId, fid ) )
1623     {
1624       // skip added features
1625       continue;
1626     }
1627     const QgsGeometry geom = it.value();
1628     const QString sql = QStringLiteral( "INSERT INTO 'log_geometry_updates' VALUES ( %1, %2, %3, '%4' )" )
1629                         .arg( layerId )
1630                         .arg( commitNo )
1631                         .arg( fid )
1632                         .arg( geom.asWkt() );
1633     sqlExec( database.get(), sql );
1634 
1635     // TODO: use WKB instead of WKT?
1636   }
1637 
1638   increaseCommitNo( database.get() );
1639 }
1640 
startListenFeatureChanges()1641 void QgsOfflineEditing::startListenFeatureChanges()
1642 {
1643   QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1644 
1645   Q_ASSERT( vLayer );
1646 
1647   // enable logging, check if editBuffer is not null
1648   if ( vLayer->editBuffer() )
1649   {
1650     QgsVectorLayerEditBuffer *editBuffer = vLayer->editBuffer();
1651     connect( editBuffer, &QgsVectorLayerEditBuffer::committedAttributesAdded,
1652              this, &QgsOfflineEditing::committedAttributesAdded );
1653     connect( editBuffer, &QgsVectorLayerEditBuffer::committedAttributeValuesChanges,
1654              this, &QgsOfflineEditing::committedAttributeValuesChanges );
1655     connect( editBuffer, &QgsVectorLayerEditBuffer::committedGeometriesChanges,
1656              this, &QgsOfflineEditing::committedGeometriesChanges );
1657   }
1658   connect( vLayer, &QgsVectorLayer::committedFeaturesAdded,
1659            this, &QgsOfflineEditing::committedFeaturesAdded );
1660   connect( vLayer, &QgsVectorLayer::committedFeaturesRemoved,
1661            this, &QgsOfflineEditing::committedFeaturesRemoved );
1662 }
1663 
stopListenFeatureChanges()1664 void QgsOfflineEditing::stopListenFeatureChanges()
1665 {
1666   QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1667 
1668   Q_ASSERT( vLayer );
1669 
1670   // disable logging, check if editBuffer is not null
1671   if ( vLayer->editBuffer() )
1672   {
1673     QgsVectorLayerEditBuffer *editBuffer = vLayer->editBuffer();
1674     disconnect( editBuffer, &QgsVectorLayerEditBuffer::committedAttributesAdded,
1675                 this, &QgsOfflineEditing::committedAttributesAdded );
1676     disconnect( editBuffer, &QgsVectorLayerEditBuffer::committedAttributeValuesChanges,
1677                 this, &QgsOfflineEditing::committedAttributeValuesChanges );
1678     disconnect( editBuffer, &QgsVectorLayerEditBuffer::committedGeometriesChanges,
1679                 this, &QgsOfflineEditing::committedGeometriesChanges );
1680   }
1681   disconnect( vLayer, &QgsVectorLayer::committedFeaturesAdded,
1682               this, &QgsOfflineEditing::committedFeaturesAdded );
1683   disconnect( vLayer, &QgsVectorLayer::committedFeaturesRemoved,
1684               this, &QgsOfflineEditing::committedFeaturesRemoved );
1685 }
1686 
setupLayer(QgsMapLayer * layer)1687 void QgsOfflineEditing::setupLayer( QgsMapLayer *layer )
1688 {
1689   Q_ASSERT( layer );
1690 
1691   if ( QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( layer ) )
1692   {
1693     // detect offline layer
1694     if ( vLayer->customProperty( CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE, false ).toBool() )
1695     {
1696       connect( vLayer, &QgsVectorLayer::editingStarted, this, &QgsOfflineEditing::startListenFeatureChanges );
1697       connect( vLayer, &QgsVectorLayer::editingStopped, this, &QgsOfflineEditing::stopListenFeatureChanges );
1698     }
1699   }
1700 }
1701 
getLayerPkIdx(const QgsVectorLayer * layer) const1702 int QgsOfflineEditing::getLayerPkIdx( const QgsVectorLayer *layer ) const
1703 {
1704   const QList<int> pkAttrs = layer->primaryKeyAttributes();
1705   if ( pkAttrs.length() == 1 )
1706   {
1707     const QgsField pkField = layer->fields().at( pkAttrs[0] );
1708     const QVariant::Type pkType = pkField.type();
1709 
1710     if ( pkType == QVariant::String )
1711     {
1712       return pkAttrs[0];
1713     }
1714   }
1715 
1716   return -1;
1717 }
1718 
sqlEscape(QString value) const1719 QString QgsOfflineEditing::sqlEscape( QString value ) const
1720 {
1721   if ( value.isNull() )
1722     return QStringLiteral( "NULL" );
1723 
1724   value.replace( "'", "''" );
1725 
1726   return QStringLiteral( "'%1'" ).arg( value );
1727 }
1728