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