QGIS API Documentation  3.2.0-Bonn (bc43194)
qgsofflineediting.cpp
Go to the documentation of this file.
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"
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 
39 #include <QDir>
40 #include <QDomDocument>
41 #include <QDomNode>
42 #include <QFile>
43 #include <QMessageBox>
44 
45 extern "C"
46 {
47 #include <sqlite3.h>
48 #include <spatialite.h>
49 }
50 
51 #define CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE "isOfflineEditable"
52 #define CUSTOM_PROPERTY_REMOTE_SOURCE "remoteSource"
53 #define CUSTOM_PROPERTY_REMOTE_PROVIDER "remoteProvider"
54 #define PROJECT_ENTRY_SCOPE_OFFLINE "OfflineEditingPlugin"
55 #define PROJECT_ENTRY_KEY_OFFLINE_DB_PATH "/OfflineDbPath"
56 
58 {
59  connect( QgsProject::instance(), &QgsProject::layerWasAdded, this, &QgsOfflineEditing::layerAdded );
60 }
61 
77 bool QgsOfflineEditing::convertToOfflineProject( const QString &offlineDataPath, const QString &offlineDbFile, const QStringList &layerIds, bool onlySelected )
78 {
79  if ( layerIds.isEmpty() )
80  {
81  return false;
82  }
83  QString dbPath = QDir( offlineDataPath ).absoluteFilePath( offlineDbFile );
84  if ( createSpatialiteDB( dbPath ) )
85  {
87  int rc = database.open( dbPath );
88  if ( rc != SQLITE_OK )
89  {
90  showWarning( tr( "Could not open the SpatiaLite database" ) );
91  }
92  else
93  {
94  // create logging tables
95  createLoggingTables( database.get() );
96 
97  emit progressStarted();
98 
99  QMap<QString, QgsVectorJoinList > joinInfoBuffer;
100  QMap<QString, QgsVectorLayer *> layerIdMapping;
101 
102  Q_FOREACH ( const QString &layerId, layerIds )
103  {
104  QgsMapLayer *layer = QgsProject::instance()->mapLayer( layerId );
105  QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( layer );
106  if ( !vl )
107  continue;
108  QgsVectorJoinList joins = vl->vectorJoins();
109 
110  // Layer names will be appended an _offline suffix
111  // Join fields are prefixed with the layer name and we do not want the
112  // field name to change so we stabilize the field name by defining a
113  // custom prefix with the layername without _offline suffix.
114  QgsVectorJoinList::iterator joinIt = joins.begin();
115  while ( joinIt != joins.end() )
116  {
117  if ( joinIt->prefix().isNull() )
118  {
119  QgsVectorLayer *vl = joinIt->joinLayer();
120 
121  if ( vl )
122  joinIt->setPrefix( vl->name() + '_' );
123  }
124  ++joinIt;
125  }
126  joinInfoBuffer.insert( vl->id(), joins );
127  }
128 
129  // copy selected vector layers to SpatiaLite
130  for ( int i = 0; i < layerIds.count(); i++ )
131  {
132  emit layerProgressUpdated( i + 1, layerIds.count() );
133 
134  QgsMapLayer *layer = QgsProject::instance()->mapLayer( layerIds.at( i ) );
135  QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( layer );
136  if ( vl )
137  {
138  QString origLayerId = vl->id();
139  QgsVectorLayer *newLayer = copyVectorLayer( vl, database.get(), dbPath, onlySelected );
140  if ( newLayer )
141  {
142  layerIdMapping.insert( origLayerId, newLayer );
143  // remove remote layer
145  QStringList() << origLayerId );
146  }
147  }
148  }
149 
150  // restore join info on new SpatiaLite layer
151  QMap<QString, QgsVectorJoinList >::ConstIterator it;
152  for ( it = joinInfoBuffer.constBegin(); it != joinInfoBuffer.constEnd(); ++it )
153  {
154  QgsVectorLayer *newLayer = layerIdMapping.value( it.key() );
155 
156  if ( newLayer )
157  {
158  Q_FOREACH ( QgsVectorLayerJoinInfo join, it.value() )
159  {
160  QgsVectorLayer *newJoinedLayer = layerIdMapping.value( join.joinLayerId() );
161  if ( newJoinedLayer )
162  {
163  // If the layer has been offline'd, update join information
164  join.setJoinLayer( newJoinedLayer );
165  }
166  newLayer->addJoin( join );
167  }
168  }
169  }
170 
171 
172  emit progressStopped();
173 
174  // save offline project
175  QString projectTitle = QgsProject::instance()->title();
176  if ( projectTitle.isEmpty() )
177  {
178  projectTitle = QFileInfo( QgsProject::instance()->fileName() ).fileName();
179  }
180  projectTitle += QLatin1String( " (offline)" );
181  QgsProject::instance()->setTitle( projectTitle );
182 
184 
185  return true;
186  }
187  }
188 
189  return false;
190 }
191 
193 {
195 }
196 
198 {
199  // open logging db
200  sqlite3_database_unique_ptr database = openLoggingDb();
201  if ( !database )
202  {
203  return;
204  }
205 
206  emit progressStarted();
207 
208  // restore and sync remote layers
209  QList<QgsMapLayer *> offlineLayers;
210  QMap<QString, QgsMapLayer *> mapLayers = QgsProject::instance()->mapLayers();
211  for ( QMap<QString, QgsMapLayer *>::iterator layer_it = mapLayers.begin() ; layer_it != mapLayers.end(); ++layer_it )
212  {
213  QgsMapLayer *layer = layer_it.value();
214  if ( layer->customProperty( CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE, false ).toBool() )
215  {
216  offlineLayers << layer;
217  }
218  }
219 
220  QgsDebugMsgLevel( QString( "Found %1 offline layers" ).arg( offlineLayers.count() ), 4 );
221  for ( int l = 0; l < offlineLayers.count(); l++ )
222  {
223  QgsMapLayer *layer = offlineLayers.at( l );
224 
225  emit layerProgressUpdated( l + 1, offlineLayers.count() );
226 
227  QString remoteSource = layer->customProperty( CUSTOM_PROPERTY_REMOTE_SOURCE, "" ).toString();
228  QString remoteProvider = layer->customProperty( CUSTOM_PROPERTY_REMOTE_PROVIDER, "" ).toString();
229  QString remoteName = layer->name();
230  remoteName.remove( QRegExp( " \\(offline\\)$" ) );
231 
232  QgsVectorLayer *remoteLayer = new QgsVectorLayer( remoteSource, remoteName, remoteProvider );
233  if ( remoteLayer->isValid() )
234  {
235  // Rebuild WFS cache to get feature id<->GML fid mapping
236  if ( remoteLayer->dataProvider()->name().contains( QLatin1String( "WFS" ), Qt::CaseInsensitive ) )
237  {
238  QgsFeatureIterator fit = remoteLayer->getFeatures();
239  QgsFeature f;
240  while ( fit.nextFeature( f ) )
241  {
242  }
243  }
244  // TODO: only add remote layer if there are log entries?
245 
246  QgsVectorLayer *offlineLayer = qobject_cast<QgsVectorLayer *>( layer );
247 
248  // register this layer with the central layers registry
249  QgsProject::instance()->addMapLayers( QList<QgsMapLayer *>() << remoteLayer, true );
250 
251  // copy style
252  copySymbology( offlineLayer, remoteLayer );
253  updateRelations( offlineLayer, remoteLayer );
254  updateMapThemes( offlineLayer, remoteLayer );
255  updateLayerOrder( offlineLayer, remoteLayer );
256 
257  // apply layer edit log
258  QString qgisLayerId = layer->id();
259  QString sql = QStringLiteral( "SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( qgisLayerId );
260  int layerId = sqlQueryInt( database.get(), sql, -1 );
261  if ( layerId != -1 )
262  {
263  remoteLayer->startEditing();
264 
265  // TODO: only get commitNos of this layer?
266  int commitNo = getCommitNo( database.get() );
267  QgsDebugMsgLevel( QString( "Found %1 commits" ).arg( commitNo ), 4 );
268  for ( int i = 0; i < commitNo; i++ )
269  {
270  QgsDebugMsgLevel( "Apply commits chronologically", 4 );
271  // apply commits chronologically
272  applyAttributesAdded( remoteLayer, database.get(), layerId, i );
273  applyAttributeValueChanges( offlineLayer, remoteLayer, database.get(), layerId, i );
274  applyGeometryChanges( remoteLayer, database.get(), layerId, i );
275  }
276 
277  applyFeaturesAdded( offlineLayer, remoteLayer, database.get(), layerId );
278  applyFeaturesRemoved( remoteLayer, database.get(), layerId );
279 
280  if ( remoteLayer->commitChanges() )
281  {
282  // update fid lookup
283  updateFidLookup( remoteLayer, database.get(), layerId );
284 
285  // clear edit log for this layer
286  sql = QStringLiteral( "DELETE FROM 'log_added_attrs' WHERE \"layer_id\" = %1" ).arg( layerId );
287  sqlExec( database.get(), sql );
288  sql = QStringLiteral( "DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
289  sqlExec( database.get(), sql );
290  sql = QStringLiteral( "DELETE FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( layerId );
291  sqlExec( database.get(), sql );
292  sql = QStringLiteral( "DELETE FROM 'log_feature_updates' WHERE \"layer_id\" = %1" ).arg( layerId );
293  sqlExec( database.get(), sql );
294  sql = QStringLiteral( "DELETE FROM 'log_geometry_updates' WHERE \"layer_id\" = %1" ).arg( layerId );
295  sqlExec( database.get(), sql );
296  }
297  else
298  {
299  showWarning( remoteLayer->commitErrors().join( QStringLiteral( "\n" ) ) );
300  }
301  }
302  else
303  {
304  QgsDebugMsg( "Could not find the layer id in the edit logs!" );
305  }
306  // Invalidate the connection to force a reload if the project is put offline
307  // again with the same path
308  offlineLayer->dataProvider()->invalidateConnections( QgsDataSourceUri( offlineLayer->source() ).database() );
309  // remove offline layer
310  QgsProject::instance()->removeMapLayers( QStringList() << qgisLayerId );
311 
312 
313  // disable offline project
314  QString projectTitle = QgsProject::instance()->title();
315  projectTitle.remove( QRegExp( " \\(offline\\)$" ) );
316  QgsProject::instance()->setTitle( projectTitle );
318  remoteLayer->reload(); //update with other changes
319  }
320  else
321  {
322  QgsDebugMsg( "Remote layer is not valid!" );
323  }
324  }
325 
326  // reset commitNo
327  QString sql = QStringLiteral( "UPDATE 'log_indices' SET 'last_index' = 0 WHERE \"name\" = 'commit_no'" );
328  sqlExec( database.get(), sql );
329 
330  emit progressStopped();
331 }
332 
333 void QgsOfflineEditing::initializeSpatialMetadata( sqlite3 *sqlite_handle )
334 {
335  // attempting to perform self-initialization for a newly created DB
336  if ( !sqlite_handle )
337  return;
338  // checking if this DB is really empty
339  char **results = nullptr;
340  int rows, columns;
341  int ret = sqlite3_get_table( sqlite_handle, "select count(*) from sqlite_master", &results, &rows, &columns, nullptr );
342  if ( ret != SQLITE_OK )
343  return;
344  int count = 0;
345  if ( rows >= 1 )
346  {
347  for ( int i = 1; i <= rows; i++ )
348  count = atoi( results[( i * columns ) + 0] );
349  }
350 
351  sqlite3_free_table( results );
352 
353  if ( count > 0 )
354  return;
355 
356  bool above41 = false;
357  ret = sqlite3_get_table( sqlite_handle, "select spatialite_version()", &results, &rows, &columns, nullptr );
358  if ( ret == SQLITE_OK && rows == 1 && columns == 1 )
359  {
360  QString version = QString::fromUtf8( results[1] );
361  QStringList parts = version.split( ' ', QString::SkipEmptyParts );
362  if ( !parts.empty() )
363  {
364  QStringList verparts = parts.at( 0 ).split( '.', QString::SkipEmptyParts );
365  above41 = verparts.size() >= 2 && ( verparts.at( 0 ).toInt() > 4 || ( verparts.at( 0 ).toInt() == 4 && verparts.at( 1 ).toInt() >= 1 ) );
366  }
367  }
368 
369  sqlite3_free_table( results );
370 
371  // all right, it's empty: proceeding to initialize
372  char *errMsg = nullptr;
373  ret = sqlite3_exec( sqlite_handle, above41 ? "SELECT InitSpatialMetadata(1)" : "SELECT InitSpatialMetadata()", nullptr, nullptr, &errMsg );
374 
375  if ( ret != SQLITE_OK )
376  {
377  QString errCause = tr( "Unable to initialize SpatialMetadata:\n" );
378  errCause += QString::fromUtf8( errMsg );
379  showWarning( errCause );
380  sqlite3_free( errMsg );
381  return;
382  }
383  spatial_ref_sys_init( sqlite_handle, 0 );
384 }
385 
386 bool QgsOfflineEditing::createSpatialiteDB( const QString &offlineDbPath )
387 {
388  int ret;
389  char *errMsg = nullptr;
390  QFile newDb( offlineDbPath );
391  if ( newDb.exists() )
392  {
393  QFile::remove( offlineDbPath );
394  }
395 
396  // see also QgsNewSpatialiteLayerDialog::createDb()
397 
398  QFileInfo fullPath = QFileInfo( offlineDbPath );
399  QDir path = fullPath.dir();
400 
401  // Must be sure there is destination directory ~/.qgis
402  QDir().mkpath( path.absolutePath() );
403 
404  // creating/opening the new database
405  QString dbPath = newDb.fileName();
407  ret = database.open_v2( dbPath, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr );
408  if ( ret )
409  {
410  // an error occurred
411  QString errCause = tr( "Could not create a new database\n" );
412  errCause += database.errorMessage();
413  showWarning( errCause );
414  return false;
415  }
416  // activating Foreign Key constraints
417  ret = sqlite3_exec( database.get(), "PRAGMA foreign_keys = 1", nullptr, nullptr, &errMsg );
418  if ( ret != SQLITE_OK )
419  {
420  showWarning( tr( "Unable to activate FOREIGN_KEY constraints" ) );
421  sqlite3_free( errMsg );
422  return false;
423  }
424  initializeSpatialMetadata( database.get() );
425  return true;
426 }
427 
428 void QgsOfflineEditing::createLoggingTables( sqlite3 *db )
429 {
430  // indices
431  QString sql = QStringLiteral( "CREATE TABLE 'log_indices' ('name' TEXT, 'last_index' INTEGER)" );
432  sqlExec( db, sql );
433 
434  sql = QStringLiteral( "INSERT INTO 'log_indices' VALUES ('commit_no', 0)" );
435  sqlExec( db, sql );
436 
437  sql = QStringLiteral( "INSERT INTO 'log_indices' VALUES ('layer_id', 0)" );
438  sqlExec( db, sql );
439 
440  // layername <-> layer id
441  sql = QStringLiteral( "CREATE TABLE 'log_layer_ids' ('id' INTEGER, 'qgis_id' TEXT)" );
442  sqlExec( db, sql );
443 
444  // offline fid <-> remote fid
445  sql = QStringLiteral( "CREATE TABLE 'log_fids' ('layer_id' INTEGER, 'offline_fid' INTEGER, 'remote_fid' INTEGER)" );
446  sqlExec( db, sql );
447 
448  // added attributes
449  sql = QStringLiteral( "CREATE TABLE 'log_added_attrs' ('layer_id' INTEGER, 'commit_no' INTEGER, " );
450  sql += QLatin1String( "'name' TEXT, 'type' INTEGER, 'length' INTEGER, 'precision' INTEGER, 'comment' TEXT)" );
451  sqlExec( db, sql );
452 
453  // added features
454  sql = QStringLiteral( "CREATE TABLE 'log_added_features' ('layer_id' INTEGER, 'fid' INTEGER)" );
455  sqlExec( db, sql );
456 
457  // removed features
458  sql = QStringLiteral( "CREATE TABLE 'log_removed_features' ('layer_id' INTEGER, 'fid' INTEGER)" );
459  sqlExec( db, sql );
460 
461  // feature updates
462  sql = QStringLiteral( "CREATE TABLE 'log_feature_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'attr' INTEGER, 'value' TEXT)" );
463  sqlExec( db, sql );
464 
465  // geometry updates
466  sql = QStringLiteral( "CREATE TABLE 'log_geometry_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'geom_wkt' TEXT)" );
467  sqlExec( db, sql );
468 
469  /* TODO: other logging tables
470  - attr delete (not supported by SpatiaLite provider)
471  */
472 }
473 
474 QgsVectorLayer *QgsOfflineEditing::copyVectorLayer( QgsVectorLayer *layer, sqlite3 *db, const QString &offlineDbPath, bool onlySelected )
475 {
476  if ( !layer )
477  return nullptr;
478 
479  QString tableName = layer->id();
480  QgsDebugMsgLevel( QString( "Creating offline table %1 ..." ).arg( tableName ), 4 );
481 
482  // create table
483  QString sql = QStringLiteral( "CREATE TABLE '%1' (" ).arg( tableName );
484  QString delim;
485  const QgsFields providerFields = layer->dataProvider()->fields();
486  for ( const auto &field : providerFields )
487  {
488  QString dataType;
489  QVariant::Type type = field.type();
490  if ( type == QVariant::Int || type == QVariant::LongLong )
491  {
492  dataType = QStringLiteral( "INTEGER" );
493  }
494  else if ( type == QVariant::Double )
495  {
496  dataType = QStringLiteral( "REAL" );
497  }
498  else if ( type == QVariant::String )
499  {
500  dataType = QStringLiteral( "TEXT" );
501  }
502  else
503  {
504  showWarning( tr( "%1: Unknown data type %2. Not using type affinity for the field." ).arg( field.name(), QVariant::typeToName( type ) ) );
505  }
506 
507  sql += delim + QStringLiteral( "'%1' %2" ).arg( field.name(), dataType );
508  delim = ',';
509  }
510  sql += ')';
511 
512  int rc = sqlExec( db, sql );
513 
514  // add geometry column
515  if ( layer->isSpatial() )
516  {
517  const QgsWkbTypes::Type sourceWkbType = layer->wkbType();
518 
519  QString geomType;
520  switch ( QgsWkbTypes::flatType( sourceWkbType ) )
521  {
522  case QgsWkbTypes::Point:
523  geomType = QStringLiteral( "POINT" );
524  break;
526  geomType = QStringLiteral( "MULTIPOINT" );
527  break;
529  geomType = QStringLiteral( "LINESTRING" );
530  break;
532  geomType = QStringLiteral( "MULTILINESTRING" );
533  break;
535  geomType = QStringLiteral( "POLYGON" );
536  break;
538  geomType = QStringLiteral( "MULTIPOLYGON" );
539  break;
540  default:
541  showWarning( tr( "Layer %1 has unsupported geometry type %2." ).arg( layer->name(), QgsWkbTypes::displayString( layer->wkbType() ) ) );
542  break;
543  };
544 
545  QString zmInfo = QStringLiteral( "XY" );
546 
547  if ( QgsWkbTypes::hasZ( sourceWkbType ) )
548  zmInfo += 'Z';
549  if ( QgsWkbTypes::hasM( sourceWkbType ) )
550  zmInfo += 'M';
551 
552  QString epsgCode;
553 
554  if ( layer->crs().authid().startsWith( QLatin1String( "EPSG:" ), Qt::CaseInsensitive ) )
555  {
556  epsgCode = layer->crs().authid().mid( 5 );
557  }
558  else
559  {
560  epsgCode = '0';
561  showWarning( tr( "Layer %1 has unsupported Coordinate Reference System (%2)." ).arg( layer->name(), layer->crs().authid() ) );
562  }
563 
564  QString sqlAddGeom = QStringLiteral( "SELECT AddGeometryColumn('%1', 'Geometry', %2, '%3', '%4')" )
565  .arg( tableName, epsgCode, geomType, zmInfo );
566 
567  // create spatial index
568  QString sqlCreateIndex = QStringLiteral( "SELECT CreateSpatialIndex('%1', 'Geometry')" ).arg( tableName );
569 
570  if ( rc == SQLITE_OK )
571  {
572  rc = sqlExec( db, sqlAddGeom );
573  if ( rc == SQLITE_OK )
574  {
575  rc = sqlExec( db, sqlCreateIndex );
576  }
577  }
578  }
579 
580  if ( rc == SQLITE_OK )
581  {
582  // add new layer
583  QString connectionString = QStringLiteral( "dbname='%1' table='%2'%3 sql=" )
584  .arg( offlineDbPath,
585  tableName, layer->isSpatial() ? "(Geometry)" : "" );
586  QgsVectorLayer *newLayer = new QgsVectorLayer( connectionString,
587  layer->name() + " (offline)", QStringLiteral( "spatialite" ) );
588  if ( newLayer->isValid() )
589  {
590 
591  // copy features
592  newLayer->startEditing();
593  QgsFeature f;
594 
595  QgsFeatureRequest req;
596 
597  if ( onlySelected )
598  {
599  QgsFeatureIds selectedFids = layer->selectedFeatureIds();
600  if ( !selectedFids.isEmpty() )
601  req.setFilterFids( selectedFids );
602  }
603 
604  QgsFeatureIterator fit = layer->dataProvider()->getFeatures( req );
605 
607  {
609  }
610  else
611  {
613  }
614  int featureCount = 1;
615 
616  QList<QgsFeatureId> remoteFeatureIds;
617  while ( fit.nextFeature( f ) )
618  {
619  remoteFeatureIds << f.id();
620 
621  // NOTE: SpatiaLite provider ignores position of geometry column
622  // fill gap in QgsAttributeMap if geometry column is not last (WORKAROUND)
623  int column = 0;
624  QgsAttributes attrs = f.attributes();
625  QgsAttributes newAttrs( attrs.count() );
626  for ( int it = 0; it < attrs.count(); ++it )
627  {
628  newAttrs[column++] = attrs.at( it );
629  }
630  f.setAttributes( newAttrs );
631 
632  newLayer->addFeature( f );
633 
634  emit progressUpdated( featureCount++ );
635  }
636  if ( newLayer->commitChanges() )
637  {
639  featureCount = 1;
640 
641  // update feature id lookup
642  int layerId = getOrCreateLayerId( db, newLayer->id() );
643  QList<QgsFeatureId> offlineFeatureIds;
644 
645  QgsFeatureIterator fit = newLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setSubsetOfAttributes( QgsAttributeList() ) );
646  while ( fit.nextFeature( f ) )
647  {
648  offlineFeatureIds << f.id();
649  }
650 
651  // NOTE: insert fids in this loop, as the db is locked during newLayer->nextFeature()
652  sqlExec( db, QStringLiteral( "BEGIN" ) );
653  int remoteCount = remoteFeatureIds.size();
654  for ( int i = 0; i < remoteCount; i++ )
655  {
656  // Check if the online feature has been fetched (WFS download aborted for some reason)
657  if ( i < offlineFeatureIds.count() )
658  {
659  addFidLookup( db, layerId, offlineFeatureIds.at( i ), remoteFeatureIds.at( i ) );
660  }
661  else
662  {
663  showWarning( tr( "Feature cannot be copied to the offline layer, please check if the online layer '%1' is still accessible." ).arg( layer->name() ) );
664  return nullptr;
665  }
666  emit progressUpdated( featureCount++ );
667  }
668  sqlExec( db, QStringLiteral( "COMMIT" ) );
669  }
670  else
671  {
672  showWarning( newLayer->commitErrors().join( QStringLiteral( "\n" ) ) );
673  }
674 
675  // mark as offline layer
677 
678  // store original layer source
681 
682  // register this layer with the central layers registry
684  QList<QgsMapLayer *>() << newLayer );
685 
686  // copy style
687  copySymbology( layer, newLayer );
688 
690  // Find the parent group of the original layer
691  QgsLayerTreeLayer *layerTreeLayer = layerTreeRoot->findLayer( layer->id() );
692  if ( layerTreeLayer )
693  {
694  QgsLayerTreeGroup *parentTreeGroup = qobject_cast<QgsLayerTreeGroup *>( layerTreeLayer->parent() );
695  if ( parentTreeGroup )
696  {
697  int index = parentTreeGroup->children().indexOf( layerTreeLayer );
698  // Move the new layer from the root group to the new group
699  QgsLayerTreeLayer *newLayerTreeLayer = layerTreeRoot->findLayer( newLayer->id() );
700  if ( newLayerTreeLayer )
701  {
702  QgsLayerTreeNode *newLayerTreeLayerClone = newLayerTreeLayer->clone();
703  QgsLayerTreeGroup *grp = qobject_cast<QgsLayerTreeGroup *>( newLayerTreeLayer->parent() );
704  parentTreeGroup->insertChildNode( index, newLayerTreeLayerClone );
705  if ( grp )
706  grp->removeChildNode( newLayerTreeLayer );
707  }
708  }
709  }
710 
711  updateRelations( layer, newLayer );
712  updateMapThemes( layer, newLayer );
713  updateLayerOrder( layer, newLayer );
714 
715 
716  }
717  return newLayer;
718  }
719  return nullptr;
720 }
721 
722 void QgsOfflineEditing::applyAttributesAdded( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
723 {
724  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 );
725  QList<QgsField> fields = sqlQueryAttributesAdded( db, sql );
726 
727  const QgsVectorDataProvider *provider = remoteLayer->dataProvider();
728  QList<QgsVectorDataProvider::NativeType> nativeTypes = provider->nativeTypes();
729 
730  // NOTE: uses last matching QVariant::Type of nativeTypes
731  QMap < QVariant::Type, QString /*typeName*/ > typeNameLookup;
732  for ( int i = 0; i < nativeTypes.size(); i++ )
733  {
734  QgsVectorDataProvider::NativeType nativeType = nativeTypes.at( i );
735  typeNameLookup[ nativeType.mType ] = nativeType.mTypeName;
736  }
737 
738  emit progressModeSet( QgsOfflineEditing::AddFields, fields.size() );
739 
740  for ( int i = 0; i < fields.size(); i++ )
741  {
742  // lookup typename from layer provider
743  QgsField field = fields[i];
744  if ( typeNameLookup.contains( field.type() ) )
745  {
746  QString typeName = typeNameLookup[ field.type()];
747  field.setTypeName( typeName );
748  remoteLayer->addAttribute( field );
749  }
750  else
751  {
752  showWarning( QStringLiteral( "Could not add attribute '%1' of type %2" ).arg( field.name() ).arg( field.type() ) );
753  }
754 
755  emit progressUpdated( i + 1 );
756  }
757 }
758 
759 void QgsOfflineEditing::applyFeaturesAdded( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
760 {
761  QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
762  QList<int> featureIdInts = sqlQueryInts( db, sql );
763  QgsFeatureIds newFeatureIds;
764  Q_FOREACH ( int id, featureIdInts )
765  {
766  newFeatureIds << id;
767  }
768 
769  QgsExpressionContext context = remoteLayer->createExpressionContext();
770 
771  // get new features from offline layer
772  QgsFeatureList features;
773  QgsFeatureIterator it = offlineLayer->getFeatures( QgsFeatureRequest().setFilterFids( newFeatureIds ) );
774  QgsFeature feature;
775  while ( it.nextFeature( feature ) )
776  {
777  features << feature;
778  }
779 
780  // copy features to remote layer
781  emit progressModeSet( QgsOfflineEditing::AddFeatures, features.size() );
782 
783  int i = 1;
784  int newAttrsCount = remoteLayer->fields().count();
785  for ( QgsFeatureList::iterator it = features.begin(); it != features.end(); ++it )
786  {
787  // NOTE: SpatiaLite provider ignores position of geometry column
788  // restore gap in QgsAttributeMap if geometry column is not last (WORKAROUND)
789  QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
790  QgsAttributes newAttrs( newAttrsCount );
791  QgsAttributes attrs = it->attributes();
792  for ( int it = 0; it < attrs.count(); ++it )
793  {
794  newAttrs[ attrLookup[ it ] ] = attrs.at( it );
795  }
796 
797  // respect constraints and provider default values
798  QgsFeature f = QgsVectorLayerUtils::createFeature( remoteLayer, it->geometry(), newAttrs.toMap(), &context );
799  remoteLayer->addFeature( f );
800 
801  emit progressUpdated( i++ );
802  }
803 }
804 
805 void QgsOfflineEditing::applyFeaturesRemoved( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
806 {
807  QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( layerId );
808  QgsFeatureIds values = sqlQueryFeaturesRemoved( db, sql );
809 
810  emit progressModeSet( QgsOfflineEditing::RemoveFeatures, values.size() );
811 
812  int i = 1;
813  for ( QgsFeatureIds::const_iterator it = values.constBegin(); it != values.constEnd(); ++it )
814  {
815  QgsFeatureId fid = remoteFid( db, layerId, *it );
816  remoteLayer->deleteFeature( fid );
817 
818  emit progressUpdated( i++ );
819  }
820 }
821 
822 void QgsOfflineEditing::applyAttributeValueChanges( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
823 {
824  QString sql = QStringLiteral( "SELECT \"fid\", \"attr\", \"value\" FROM 'log_feature_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2 " ).arg( layerId ).arg( commitNo );
825  AttributeValueChanges values = sqlQueryAttributeValueChanges( db, sql );
826 
827  emit progressModeSet( QgsOfflineEditing::UpdateFeatures, values.size() );
828 
829  QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
830 
831  for ( int i = 0; i < values.size(); i++ )
832  {
833  QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid );
834  QgsDebugMsgLevel( QString( "Offline changeAttributeValue %1 = %2" ).arg( QString( attrLookup[ values.at( i ).attr ] ), values.at( i ).value ), 4 );
835  remoteLayer->changeAttributeValue( fid, attrLookup[ values.at( i ).attr ], values.at( i ).value );
836 
837  emit progressUpdated( i + 1 );
838  }
839 }
840 
841 void QgsOfflineEditing::applyGeometryChanges( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
842 {
843  QString sql = QStringLiteral( "SELECT \"fid\", \"geom_wkt\" FROM 'log_geometry_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2" ).arg( layerId ).arg( commitNo );
844  GeometryChanges values = sqlQueryGeometryChanges( db, sql );
845 
847 
848  for ( int i = 0; i < values.size(); i++ )
849  {
850  QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid );
851  QgsGeometry newGeom = QgsGeometry::fromWkt( values.at( i ).geom_wkt );
852  remoteLayer->changeGeometry( fid, newGeom );
853 
854  emit progressUpdated( i + 1 );
855  }
856 }
857 
858 void QgsOfflineEditing::updateFidLookup( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
859 {
860  // update fid lookup for added features
861 
862  // get remote added fids
863  // NOTE: use QMap for sorted fids
864  QMap < QgsFeatureId, bool /*dummy*/ > newRemoteFids;
865  QgsFeature f;
866 
867  QgsFeatureIterator fit = remoteLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setSubsetOfAttributes( QgsAttributeList() ) );
868 
870 
871  int i = 1;
872  while ( fit.nextFeature( f ) )
873  {
874  if ( offlineFid( db, layerId, f.id() ) == -1 )
875  {
876  newRemoteFids[ f.id()] = true;
877  }
878 
879  emit progressUpdated( i++ );
880  }
881 
882  // get local added fids
883  // NOTE: fids are sorted
884  QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
885  QList<int> newOfflineFids = sqlQueryInts( db, sql );
886 
887  if ( newRemoteFids.size() != newOfflineFids.size() )
888  {
889  //showWarning( QString( "Different number of new features on offline layer (%1) and remote layer (%2)" ).arg(newOfflineFids.size()).arg(newRemoteFids.size()) );
890  }
891  else
892  {
893  // add new fid lookups
894  i = 0;
895  sqlExec( db, QStringLiteral( "BEGIN" ) );
896  for ( QMap<QgsFeatureId, bool>::const_iterator it = newRemoteFids.constBegin(); it != newRemoteFids.constEnd(); ++it )
897  {
898  addFidLookup( db, layerId, newOfflineFids.at( i++ ), it.key() );
899  }
900  sqlExec( db, QStringLiteral( "COMMIT" ) );
901  }
902 }
903 
904 void QgsOfflineEditing::copySymbology( QgsVectorLayer *sourceLayer, QgsVectorLayer *targetLayer )
905 {
906  QString error;
907  QDomDocument doc;
908  sourceLayer->exportNamedStyle( doc, error );
909 
910  if ( error.isEmpty() )
911  {
912  targetLayer->importNamedStyle( doc, error );
913  }
914  if ( !error.isEmpty() )
915  {
916  showWarning( error );
917  }
918 }
919 
920 void QgsOfflineEditing::updateRelations( QgsVectorLayer *sourceLayer, QgsVectorLayer *targetLayer )
921 {
923  QList<QgsRelation> relations;
924  relations = relationManager->referencedRelations( sourceLayer );
925 
926  Q_FOREACH ( QgsRelation relation, relations )
927  {
928  relationManager->removeRelation( relation );
929  relation.setReferencedLayer( targetLayer->id() );
930  relationManager->addRelation( relation );
931  }
932 
933  relations = relationManager->referencingRelations( sourceLayer );
934 
935  Q_FOREACH ( QgsRelation relation, relations )
936  {
937  relationManager->removeRelation( relation );
938  relation.setReferencingLayer( targetLayer->id() );
939  relationManager->addRelation( relation );
940  }
941 }
942 
943 void QgsOfflineEditing::updateMapThemes( QgsVectorLayer *sourceLayer, QgsVectorLayer *targetLayer )
944 {
946  QStringList mapThemeNames = mapThemeCollection->mapThemes();
947 
948  Q_FOREACH ( const QString &mapThemeName, mapThemeNames )
949  {
950  QgsMapThemeCollection::MapThemeRecord record = mapThemeCollection->mapThemeState( mapThemeName );
951 
952  Q_FOREACH ( QgsMapThemeCollection::MapThemeLayerRecord layerRecord, record.layerRecords() )
953  {
954  if ( layerRecord.layer() == sourceLayer )
955  {
956  layerRecord.setLayer( targetLayer );
957  record.removeLayerRecord( sourceLayer );
958  record.addLayerRecord( layerRecord );
959  }
960  }
961 
962  QgsProject::instance()->mapThemeCollection()->update( mapThemeName, record );
963  }
964 }
965 
966 void QgsOfflineEditing::updateLayerOrder( QgsVectorLayer *sourceLayer, QgsVectorLayer *targetLayer )
967 {
968  QList<QgsMapLayer *> layerOrder = QgsProject::instance()->layerTreeRoot()->customLayerOrder();
969 
970  auto iterator = layerOrder.begin();
971 
972  while ( iterator != layerOrder.end() )
973  {
974  if ( *iterator == targetLayer )
975  {
976  iterator = layerOrder.erase( iterator );
977  if ( iterator == layerOrder.end() )
978  break;
979  }
980 
981  if ( *iterator == sourceLayer )
982  {
983  *iterator = targetLayer;
984  }
985 
986  ++iterator;
987  }
988 
990 }
991 
992 // NOTE: use this to map column indices in case the remote geometry column is not last
993 QMap<int, int> QgsOfflineEditing::attributeLookup( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer )
994 {
995  const QgsAttributeList &offlineAttrs = offlineLayer->attributeList();
996  const QgsAttributeList &remoteAttrs = remoteLayer->attributeList();
997 
998  QMap < int /*offline attr*/, int /*remote attr*/ > attrLookup;
999  // NOTE: use size of remoteAttrs, as offlineAttrs can have new attributes not yet synced
1000  for ( int i = 0; i < remoteAttrs.size(); i++ )
1001  {
1002  attrLookup.insert( offlineAttrs.at( i ), remoteAttrs.at( i ) );
1003  }
1004 
1005  return attrLookup;
1006 }
1007 
1008 void QgsOfflineEditing::showWarning( const QString &message )
1009 {
1010  emit warning( tr( "Offline Editing Plugin" ), message );
1011 }
1012 
1013 sqlite3_database_unique_ptr QgsOfflineEditing::openLoggingDb()
1014 {
1015  sqlite3_database_unique_ptr database;
1017  if ( !dbPath.isEmpty() )
1018  {
1019  QString absoluteDbPath = QgsProject::instance()->readPath( dbPath );
1020  int rc = database.open( absoluteDbPath );
1021  if ( rc != SQLITE_OK )
1022  {
1023  QgsDebugMsg( "Could not open the SpatiaLite logging database" );
1024  showWarning( tr( "Could not open the SpatiaLite logging database" ) );
1025  }
1026  }
1027  else
1028  {
1029  QgsDebugMsg( "dbPath is empty!" );
1030  }
1031  return database;
1032 }
1033 
1034 int QgsOfflineEditing::getOrCreateLayerId( sqlite3 *db, const QString &qgisLayerId )
1035 {
1036  QString sql = QStringLiteral( "SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( qgisLayerId );
1037  int layerId = sqlQueryInt( db, sql, -1 );
1038  if ( layerId == -1 )
1039  {
1040  // next layer id
1041  sql = QStringLiteral( "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'layer_id'" );
1042  int newLayerId = sqlQueryInt( db, sql, -1 );
1043 
1044  // insert layer
1045  sql = QStringLiteral( "INSERT INTO 'log_layer_ids' VALUES (%1, '%2')" ).arg( newLayerId ).arg( qgisLayerId );
1046  sqlExec( db, sql );
1047 
1048  // increase layer_id
1049  // TODO: use trigger for auto increment?
1050  sql = QStringLiteral( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'layer_id'" ).arg( newLayerId + 1 );
1051  sqlExec( db, sql );
1052 
1053  layerId = newLayerId;
1054  }
1055 
1056  return layerId;
1057 }
1058 
1059 int QgsOfflineEditing::getCommitNo( sqlite3 *db )
1060 {
1061  QString sql = QStringLiteral( "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'commit_no'" );
1062  return sqlQueryInt( db, sql, -1 );
1063 }
1064 
1065 void QgsOfflineEditing::increaseCommitNo( sqlite3 *db )
1066 {
1067  QString sql = QStringLiteral( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'commit_no'" ).arg( getCommitNo( db ) + 1 );
1068  sqlExec( db, sql );
1069 }
1070 
1071 void QgsOfflineEditing::addFidLookup( sqlite3 *db, int layerId, QgsFeatureId offlineFid, QgsFeatureId remoteFid )
1072 {
1073  QString sql = QStringLiteral( "INSERT INTO 'log_fids' VALUES ( %1, %2, %3 )" ).arg( layerId ).arg( offlineFid ).arg( remoteFid );
1074  sqlExec( db, sql );
1075 }
1076 
1077 QgsFeatureId QgsOfflineEditing::remoteFid( sqlite3 *db, int layerId, QgsFeatureId offlineFid )
1078 {
1079  QString sql = QStringLiteral( "SELECT \"remote_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"offline_fid\" = %2" ).arg( layerId ).arg( offlineFid );
1080  return sqlQueryInt( db, sql, -1 );
1081 }
1082 
1083 QgsFeatureId QgsOfflineEditing::offlineFid( sqlite3 *db, int layerId, QgsFeatureId remoteFid )
1084 {
1085  QString sql = QStringLiteral( "SELECT \"offline_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"remote_fid\" = %2" ).arg( layerId ).arg( remoteFid );
1086  return sqlQueryInt( db, sql, -1 );
1087 }
1088 
1089 bool QgsOfflineEditing::isAddedFeature( sqlite3 *db, int layerId, QgsFeatureId fid )
1090 {
1091  QString sql = QStringLiteral( "SELECT COUNT(\"fid\") FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( fid );
1092  return ( sqlQueryInt( db, sql, 0 ) > 0 );
1093 }
1094 
1095 int QgsOfflineEditing::sqlExec( sqlite3 *db, const QString &sql )
1096 {
1097  char *errmsg = nullptr;
1098  int rc = sqlite3_exec( db, sql.toUtf8(), nullptr, nullptr, &errmsg );
1099  if ( rc != SQLITE_OK )
1100  {
1101  showWarning( errmsg );
1102  }
1103  return rc;
1104 }
1105 
1106 int QgsOfflineEditing::sqlQueryInt( sqlite3 *db, const QString &sql, int defaultValue )
1107 {
1108  sqlite3_stmt *stmt = nullptr;
1109  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1110  {
1111  showWarning( sqlite3_errmsg( db ) );
1112  return defaultValue;
1113  }
1114 
1115  int value = defaultValue;
1116  int ret = sqlite3_step( stmt );
1117  if ( ret == SQLITE_ROW )
1118  {
1119  value = sqlite3_column_int( stmt, 0 );
1120  }
1121  sqlite3_finalize( stmt );
1122 
1123  return value;
1124 }
1125 
1126 QList<int> QgsOfflineEditing::sqlQueryInts( sqlite3 *db, const QString &sql )
1127 {
1128  QList<int> values;
1129 
1130  sqlite3_stmt *stmt = nullptr;
1131  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1132  {
1133  showWarning( sqlite3_errmsg( db ) );
1134  return values;
1135  }
1136 
1137  int ret = sqlite3_step( stmt );
1138  while ( ret == SQLITE_ROW )
1139  {
1140  values << sqlite3_column_int( stmt, 0 );
1141 
1142  ret = sqlite3_step( stmt );
1143  }
1144  sqlite3_finalize( stmt );
1145 
1146  return values;
1147 }
1148 
1149 QList<QgsField> QgsOfflineEditing::sqlQueryAttributesAdded( sqlite3 *db, const QString &sql )
1150 {
1151  QList<QgsField> values;
1152 
1153  sqlite3_stmt *stmt = nullptr;
1154  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1155  {
1156  showWarning( sqlite3_errmsg( db ) );
1157  return values;
1158  }
1159 
1160  int ret = sqlite3_step( stmt );
1161  while ( ret == SQLITE_ROW )
1162  {
1163  QgsField field( QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 0 ) ) ),
1164  static_cast< QVariant::Type >( sqlite3_column_int( stmt, 1 ) ),
1165  QLatin1String( "" ), // typeName
1166  sqlite3_column_int( stmt, 2 ),
1167  sqlite3_column_int( stmt, 3 ),
1168  QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 4 ) ) ) );
1169  values << field;
1170 
1171  ret = sqlite3_step( stmt );
1172  }
1173  sqlite3_finalize( stmt );
1174 
1175  return values;
1176 }
1177 
1178 QgsFeatureIds QgsOfflineEditing::sqlQueryFeaturesRemoved( sqlite3 *db, const QString &sql )
1179 {
1180  QgsFeatureIds values;
1181 
1182  sqlite3_stmt *stmt = nullptr;
1183  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1184  {
1185  showWarning( sqlite3_errmsg( db ) );
1186  return values;
1187  }
1188 
1189  int ret = sqlite3_step( stmt );
1190  while ( ret == SQLITE_ROW )
1191  {
1192  values << sqlite3_column_int( stmt, 0 );
1193 
1194  ret = sqlite3_step( stmt );
1195  }
1196  sqlite3_finalize( stmt );
1197 
1198  return values;
1199 }
1200 
1201 QgsOfflineEditing::AttributeValueChanges QgsOfflineEditing::sqlQueryAttributeValueChanges( sqlite3 *db, const QString &sql )
1202 {
1203  AttributeValueChanges values;
1204 
1205  sqlite3_stmt *stmt = nullptr;
1206  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1207  {
1208  showWarning( sqlite3_errmsg( db ) );
1209  return values;
1210  }
1211 
1212  int ret = sqlite3_step( stmt );
1213  while ( ret == SQLITE_ROW )
1214  {
1215  AttributeValueChange change;
1216  change.fid = sqlite3_column_int( stmt, 0 );
1217  change.attr = sqlite3_column_int( stmt, 1 );
1218  change.value = QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 2 ) ) );
1219  values << change;
1220 
1221  ret = sqlite3_step( stmt );
1222  }
1223  sqlite3_finalize( stmt );
1224 
1225  return values;
1226 }
1227 
1228 QgsOfflineEditing::GeometryChanges QgsOfflineEditing::sqlQueryGeometryChanges( sqlite3 *db, const QString &sql )
1229 {
1230  GeometryChanges values;
1231 
1232  sqlite3_stmt *stmt = nullptr;
1233  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1234  {
1235  showWarning( sqlite3_errmsg( db ) );
1236  return values;
1237  }
1238 
1239  int ret = sqlite3_step( stmt );
1240  while ( ret == SQLITE_ROW )
1241  {
1242  GeometryChange change;
1243  change.fid = sqlite3_column_int( stmt, 0 );
1244  change.geom_wkt = QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 1 ) ) );
1245  values << change;
1246 
1247  ret = sqlite3_step( stmt );
1248  }
1249  sqlite3_finalize( stmt );
1250 
1251  return values;
1252 }
1253 
1254 void QgsOfflineEditing::committedAttributesAdded( const QString &qgisLayerId, const QList<QgsField> &addedAttributes )
1255 {
1256  sqlite3_database_unique_ptr database = openLoggingDb();
1257  if ( !database.get() )
1258  return;
1259 
1260  // insert log
1261  int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1262  int commitNo = getCommitNo( database.get() );
1263 
1264  for ( QList<QgsField>::const_iterator it = addedAttributes.begin(); it != addedAttributes.end(); ++it )
1265  {
1266  QgsField field = *it;
1267  QString sql = QStringLiteral( "INSERT INTO 'log_added_attrs' VALUES ( %1, %2, '%3', %4, %5, %6, '%7' )" )
1268  .arg( layerId )
1269  .arg( commitNo )
1270  .arg( field.name() )
1271  .arg( field.type() )
1272  .arg( field.length() )
1273  .arg( field.precision() )
1274  .arg( field.comment() );
1275  sqlExec( database.get(), sql );
1276  }
1277 
1278  increaseCommitNo( database.get() );
1279  sqlite3_close( database.get() );
1280 }
1281 
1282 void QgsOfflineEditing::committedFeaturesAdded( const QString &qgisLayerId, const QgsFeatureList &addedFeatures )
1283 {
1284  sqlite3_database_unique_ptr database = openLoggingDb();
1285  if ( !database.get() )
1286  return;
1287 
1288  // insert log
1289  int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1290 
1291  // get new feature ids from db
1292  QgsMapLayer *layer = QgsProject::instance()->mapLayer( qgisLayerId );
1293  QgsDataSourceUri uri = QgsDataSourceUri( layer->source() );
1294 
1295  // only store feature ids
1296  QString sql = QStringLiteral( "SELECT ROWID FROM '%1' ORDER BY ROWID DESC LIMIT %2" ).arg( uri.table() ).arg( addedFeatures.size() );
1297  QList<int> newFeatureIds = sqlQueryInts( database.get(), sql );
1298  for ( int i = newFeatureIds.size() - 1; i >= 0; i-- )
1299  {
1300  QString sql = QStringLiteral( "INSERT INTO 'log_added_features' VALUES ( %1, %2 )" )
1301  .arg( layerId )
1302  .arg( newFeatureIds.at( i ) );
1303  sqlExec( database.get(), sql );
1304  }
1305 
1306  sqlite3_close( database.get() );
1307 }
1308 
1309 void QgsOfflineEditing::committedFeaturesRemoved( const QString &qgisLayerId, const QgsFeatureIds &deletedFeatureIds )
1310 {
1311  sqlite3_database_unique_ptr database = openLoggingDb();
1312  if ( !database.get() )
1313  return;
1314 
1315  // insert log
1316  int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1317 
1318  for ( QgsFeatureIds::const_iterator it = deletedFeatureIds.begin(); it != deletedFeatureIds.end(); ++it )
1319  {
1320  if ( isAddedFeature( database.get(), layerId, *it ) )
1321  {
1322  // remove from added features log
1323  QString sql = QStringLiteral( "DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( *it );
1324  sqlExec( database.get(), sql );
1325  }
1326  else
1327  {
1328  QString sql = QStringLiteral( "INSERT INTO 'log_removed_features' VALUES ( %1, %2)" )
1329  .arg( layerId )
1330  .arg( *it );
1331  sqlExec( database.get(), sql );
1332  }
1333  }
1334 
1335  sqlite3_close( database.get() );
1336 }
1337 
1338 void QgsOfflineEditing::committedAttributeValuesChanges( const QString &qgisLayerId, const QgsChangedAttributesMap &changedAttrsMap )
1339 {
1340  sqlite3_database_unique_ptr database = openLoggingDb();
1341  if ( !database.get() )
1342  return;
1343 
1344  // insert log
1345  int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1346  int commitNo = getCommitNo( database.get() );
1347 
1348  for ( QgsChangedAttributesMap::const_iterator cit = changedAttrsMap.begin(); cit != changedAttrsMap.end(); ++cit )
1349  {
1350  QgsFeatureId fid = cit.key();
1351  if ( isAddedFeature( database.get(), layerId, fid ) )
1352  {
1353  // skip added features
1354  continue;
1355  }
1356  QgsAttributeMap attrMap = cit.value();
1357  for ( QgsAttributeMap::const_iterator it = attrMap.constBegin(); it != attrMap.constEnd(); ++it )
1358  {
1359  QString sql = QStringLiteral( "INSERT INTO 'log_feature_updates' VALUES ( %1, %2, %3, %4, '%5' )" )
1360  .arg( layerId )
1361  .arg( commitNo )
1362  .arg( fid )
1363  .arg( it.key() ) // attr
1364  .arg( it.value().toString() ); // value
1365  sqlExec( database.get(), sql );
1366  }
1367  }
1368 
1369  increaseCommitNo( database.get() );
1370  sqlite3_close( database.get() );
1371 }
1372 
1373 void QgsOfflineEditing::committedGeometriesChanges( const QString &qgisLayerId, const QgsGeometryMap &changedGeometries )
1374 {
1375  sqlite3_database_unique_ptr database = openLoggingDb();
1376  if ( !database.get() )
1377  return;
1378 
1379  // insert log
1380  int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1381  int commitNo = getCommitNo( database.get() );
1382 
1383  for ( QgsGeometryMap::const_iterator it = changedGeometries.begin(); it != changedGeometries.end(); ++it )
1384  {
1385  QgsFeatureId fid = it.key();
1386  if ( isAddedFeature( database.get(), layerId, fid ) )
1387  {
1388  // skip added features
1389  continue;
1390  }
1391  QgsGeometry geom = it.value();
1392  QString sql = QStringLiteral( "INSERT INTO 'log_geometry_updates' VALUES ( %1, %2, %3, '%4' )" )
1393  .arg( layerId )
1394  .arg( commitNo )
1395  .arg( fid )
1396  .arg( geom.asWkt() );
1397  sqlExec( database.get(), sql );
1398 
1399  // TODO: use WKB instead of WKT?
1400  }
1401 
1402  increaseCommitNo( database.get() );
1403  sqlite3_close( database.get() );
1404 }
1405 
1406 void QgsOfflineEditing::startListenFeatureChanges()
1407 {
1408  QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1409  // enable logging, check if editBuffer is not null
1410  if ( vLayer->editBuffer() )
1411  {
1412  QgsVectorLayerEditBuffer *editBuffer = vLayer->editBuffer();
1414  this, &QgsOfflineEditing::committedAttributesAdded );
1416  this, &QgsOfflineEditing::committedAttributeValuesChanges );
1418  this, &QgsOfflineEditing::committedGeometriesChanges );
1419  }
1420  connect( vLayer, &QgsVectorLayer::committedFeaturesAdded,
1421  this, &QgsOfflineEditing::committedFeaturesAdded );
1422  connect( vLayer, &QgsVectorLayer::committedFeaturesRemoved,
1423  this, &QgsOfflineEditing::committedFeaturesRemoved );
1424 }
1425 
1426 void QgsOfflineEditing::stopListenFeatureChanges()
1427 {
1428  QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1429  // disable logging, check if editBuffer is not null
1430  if ( vLayer->editBuffer() )
1431  {
1432  QgsVectorLayerEditBuffer *editBuffer = vLayer->editBuffer();
1433  disconnect( editBuffer, &QgsVectorLayerEditBuffer::committedAttributesAdded,
1434  this, &QgsOfflineEditing::committedAttributesAdded );
1436  this, &QgsOfflineEditing::committedAttributeValuesChanges );
1438  this, &QgsOfflineEditing::committedGeometriesChanges );
1439  }
1440  disconnect( vLayer, &QgsVectorLayer::committedFeaturesAdded,
1441  this, &QgsOfflineEditing::committedFeaturesAdded );
1442  disconnect( vLayer, &QgsVectorLayer::committedFeaturesRemoved,
1443  this, &QgsOfflineEditing::committedFeaturesRemoved );
1444 }
1445 
1446 void QgsOfflineEditing::layerAdded( QgsMapLayer *layer )
1447 {
1448  // detect offline layer
1449  if ( layer->customProperty( CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE, false ).toBool() )
1450  {
1451  QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( layer );
1452  connect( vLayer, &QgsVectorLayer::editingStarted, this, &QgsOfflineEditing::startListenFeatureChanges );
1453  connect( vLayer, &QgsVectorLayer::editingStopped, this, &QgsOfflineEditing::stopListenFeatureChanges );
1454  }
1455 }
1456 
1457 
Layer tree group node serves as a container for layers and further groups.
void setJoinLayer(QgsVectorLayer *layer)
Sets weak reference to the joined layer.
QgsFeatureId id
Definition: qgsfeature.h:71
Wrapper for iterator of features from vector data provider or vector layer.
QMap< QgsFeatureId, QgsGeometry > QgsGeometryMap
Definition: qgsfeature.h:537
void layerProgressUpdated(int layer, int numLayers)
Is emitted whenever a new layer is being processed.
int open(const QString &path)
Opens the database at the specified file path.
bool addJoin(const QgsVectorLayerJoinInfo &joinInfo)
Joins another vector layer to this layer.
int open_v2(const QString &path, int flags, const char *zVfs)
Opens the database at the specified file path.
QList< QgsMapLayer * > addMapLayers(const QList< QgsMapLayer *> &mapLayers, bool addToLegend=true, bool takeOwnership=true)
Add a list of layers to the map of loaded layers.
QgsMapLayer * layer() const
Returns map layer or null if the layer does not exist anymore.
Base class for all map layer types.
Definition: qgsmaplayer.h:61
void setLayer(QgsMapLayer *layer)
Sets the map layer for this record.
QString table() const
Returns the table.
static QgsFeature createFeature(QgsVectorLayer *layer, const QgsGeometry &geometry=QgsGeometry(), const QgsAttributeMap &attributes=QgsAttributeMap(), QgsExpressionContext *context=nullptr)
Creates a new feature ready for insertion into a layer.
Filter using feature IDs.
QString readEntry(const QString &scope, const QString &key, const QString &def=QString(), bool *ok=nullptr) const
QString name
Definition: qgsfield.h:57
int precision
Definition: qgsfield.h:54
#define PROJECT_ENTRY_KEY_OFFLINE_DB_PATH
bool deleteFeature(QgsFeatureId fid)
Deletes a feature from the layer (but does not commit it).
void committedAttributesAdded(const QString &layerId, const QList< QgsField > &addedAttributes)
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
QSet< QgsFeatureId > QgsFeatureIds
Definition: qgsfeature.h:544
QList< QgsRelation > referencingRelations(const QgsVectorLayer *layer=nullptr, int fieldIdx=-2) const
Gets all relations where the specified layer (and field) is the referencing part (i.e.
QgsMapThemeCollection::MapThemeRecord mapThemeState(const QString &name) const
Returns the recorded state of a map theme.
QList< QgsFeature > QgsFeatureList
Definition: qgsfeature.h:549
#define CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE
Individual map theme record of visible layers and styles.
bool commitChanges()
Attempts to commit to the underlying data provider any buffered changes made since the last to call t...
void setReferencingLayer(const QString &id)
Set the referencing (child) layer id.
void update(const QString &name, const QgsMapThemeCollection::MapThemeRecord &state)
Updates a map theme within the collection.
bool startEditing()
Makes the layer editable.
void setCustomProperty(const QString &key, const QVariant &value)
Set a custom property for layer.
QString comment
Definition: qgsfield.h:56
#define CUSTOM_PROPERTY_REMOTE_SOURCE
FilterType filterType() const
Returns the filter type which is currently set on this request.
Container of fields for a vector layer.
Definition: qgsfields.h:42
A geometry is the spatial representation of a feature.
Definition: qgsgeometry.h:104
void setAttributes(const QgsAttributes &attrs)
Sets the feature&#39;s attributes.
Definition: qgsfeature.cpp:127
void removeChildNode(QgsLayerTreeNode *node)
Remove a child node from this group.
QMap< QString, QgsMapLayer * > mapLayers() const
Returns a map of all registered layers by layer ID.
Individual record of a visible layer in a map theme record.
The feature class encapsulates a single feature including its id, geometry and a list of field/values...
Definition: qgsfeature.h:62
QList< QgsVectorDataProvider::NativeType > nativeTypes() const
Returns the names of the supported types.
bool isValid() const
Returns the status of the layer.
bool isOfflineProject() const
Returns true if current project is offline.
void committedFeaturesRemoved(const QString &layerId, const QgsFeatureIds &deletedFeatureIds)
This signal is emitted, when features are deleted from the provider.
int count() const
Returns number of items.
Definition: qgsfields.cpp:115
static bool hasZ(Type type)
Tests whether a WKB type contains the z-dimension.
Definition: qgswkbtypes.h:768
QList< QgsMapLayer * > customLayerOrder() const
The order in which layers will be rendered on the canvas.
virtual QString name() const =0
Returns a provider name.
bool convertToOfflineProject(const QString &offlineDataPath, const QString &offlineDbFile, const QStringList &layerIds, bool onlySelected=false)
Convert current project for offline editing.
void removeLayerRecord(QgsMapLayer *layer)
Removes a record for layer if present.
int length
Definition: qgsfield.h:53
bool writeEntry(const QString &scope, const QString &key, bool value)
Write a boolean entry to the project file.
void progressUpdated(int progress)
Emitted with the progress of the current mode.
void committedGeometriesChanges(const QString &layerId, const QgsGeometryMap &changedGeometries)
QgsVectorLayerEditBuffer * editBuffer()
Buffer with uncommitted editing operations. Only valid after editing has been turned on...
Type
The WKB type describes the number of dimensions a geometry has.
Definition: qgswkbtypes.h:67
const QList< QgsVectorLayerJoinInfo > vectorJoins() const
QString id() const
Returns the layer&#39;s unique ID, which is used to access this layer from QgsProject.
QList< QgsLayerTreeNode * > children()
Gets list of children of the node. Children are owned by the parent.
QgsMapThemeCollection mapThemeCollection
Definition: qgsproject.h:93
void progressStopped()
Emitted when the processing of all layers has finished.
const QgsFeatureIds & selectedFeatureIds() const
Returns a list of the selected features IDs in this layer.
QgsFields fields() const override
Returns the list of fields of this layer.
Unique pointer for spatialite databases, which automatically closes the database when the pointer goe...
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
QgsFields fields() const override=0
Returns the fields associated with this data provider.
long featureCount(const QString &legendKey) const
Number of features rendered with specified legend key.
void setTypeName(const QString &typeName)
Set the field type.
Definition: qgsfield.cpp:150
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
QgsLayerTreeNode * parent()
Gets pointer to the parent. If parent is a null pointer, the node is a root node. ...
void committedFeaturesAdded(const QString &layerId, const QgsFeatureList &addedFeatures)
This signal is emitted, when features are added to the provider.
QgsWkbTypes::Type wkbType() const override
Returns the WKBType or WKBUnknown in case of error.
Defines left outer join from our vector layer to some other vector layer.
QMap< int, QVariant > QgsAttributeMap
Definition: qgsattributes.h:39
void editingStopped()
Is emitted, when edited changes successfully have been written to the data provider.
This class wraps a request for features to a vector layer (or directly its vector data provider)...
bool isSpatial() const override
Returns true if this is a geometry layer and false in case of NoGeometry (table only) or UnknownGeome...
QgsCoordinateReferenceSystem crs() const
Returns the layer&#39;s spatial reference system.
This class is a base class for nodes in a layer tree.
bool removeEntry(const QString &scope, const QString &key)
Remove the given key.
QgsAttributeMap toMap() const
Returns a QgsAttributeMap of the attribute values.
QgsAttributeList attributeList() const
Returns list of attribute indexes.
QList< QgsRelation > referencedRelations(QgsVectorLayer *layer=nullptr) const
Gets all relations where this layer is the referenced part (i.e.
Encapsulate a field in an attribute table or data source.
Definition: qgsfield.h:48
virtual bool importNamedStyle(QDomDocument &doc, QString &errorMsg)
Import the properties of this layer from a QDomDocument.
QgsRelationManager relationManager
Definition: qgsproject.h:95
void editingStarted()
Is emitted, when editing on this layer has started.
int open(const QString &path)
Opens the database at the specified file path.
#define PROJECT_ENTRY_SCOPE_OFFLINE
void setReferencedLayer(const QString &id)
Set the referenced (parent) layer id.
struct sqlite3 sqlite3
QgsFeatureIterator getFeatures(const QgsFeatureRequest &request=QgsFeatureRequest()) const override
Query the layer for features specified in request.
void reload() override
Synchronises with changes in the datasource.
QgsExpressionContext createExpressionContext() const override
This method needs to be reimplemented in all classes which implement this interface and return an exp...
QString asWkt(int precision=17) const
Exports the geometry to WKT.
QStringList commitErrors() const
Returns a list containing any error messages generated when attempting to commit changes to the layer...
Unique pointer for sqlite3 databases, which automatically closes the database when the pointer goes o...
QgsLayerTree * layerTreeRoot() const
Returns pointer to the root (invisible) node of the project&#39;s layer tree.
static QgsGeometry fromWkt(const QString &wkt)
Creates a new geometry from a WKT string.
void warning(const QString &title, const QString &message)
Emitted when a warning needs to be displayed.
QgsFeatureRequest & setFilterFids(const QgsFeatureIds &fids)
Sets feature IDs that should be fetched.
void setCustomLayerOrder(const QList< QgsMapLayer *> &customLayerOrder)
The order in which layers will be rendered on the canvas.
void insertChildNode(int index, QgsLayerTreeNode *node)
Insert existing node at specified position.
QMap< QgsFeatureId, QgsAttributeMap > QgsChangedAttributesMap
Definition: qgsfeature.h:528
QgsFeatureIterator getFeatures(const QgsFeatureRequest &request=QgsFeatureRequest()) const override=0
Query the provider for features specified in request.
QString source() const
Returns the source for the layer.
long featureCount() const override=0
Number of features in the layer.
QList< QgsMapThemeCollection::MapThemeLayerRecord > layerRecords() const
Returns a list of records for all visible layer belonging to the theme.
void synchronize()
Synchronize to remote layers.
This class manages a set of relations between layers.
void committedAttributeValuesChanges(const QString &layerId, const QgsChangedAttributesMap &changedAttributesValues)
static QgsProject * instance()
Returns the QgsProject singleton instance.
Definition: qgsproject.cpp:391
void setTitle(const QString &title)
Sets the project&#39;s title.
Definition: qgsproject.cpp:400
QgsVectorDataProvider * dataProvider() override
Returns the layer&#39;s data provider.
static QString displayString(Type type)
Returns a display string type for a WKB type, e.g., the geometry name used in WKT geometry representa...
void progressModeSet(QgsOfflineEditing::ProgressMode mode, int maximum)
Is emitted when the mode for the progress of the current operation is set.
qint64 QgsFeatureId
Definition: qgsfeature.h:37
QgsMapLayer * mapLayer(const QString &layerId) const
Retrieve a pointer to a registered layer by layer ID.
void removeMapLayers(const QStringList &layerIds)
Remove a set of layers from the registry by layer ID.
QString name
Definition: qgsmaplayer.h:65
static bool hasM(Type type)
Tests whether a WKB type contains m values.
Definition: qgswkbtypes.h:818
bool changeAttributeValue(QgsFeatureId fid, int field, const QVariant &newValue, const QVariant &oldValue=QVariant(), bool skipDefaultValues=false)
Changes an attribute value for a feature (but does not immediately commit the changes).
void removeRelation(const QString &id)
Remove a relation.
Container class that allows storage of map themes consisting of visible map layers and layer styles...
QVariant customProperty(const QString &value, const QVariant &defaultValue=QVariant()) const
Read a custom property from layer.
QString title() const
Returns the project&#39;s title.
Definition: qgsproject.cpp:411
QList< int > QgsAttributeList
Definition: qgsfield.h:27
QgsLayerTreeLayer * findLayer(QgsMapLayer *layer) const
Find layer node representing the map layer.
QString providerType() const
Returns the provider type for this layer.
QList< QgsVectorLayerJoinInfo > QgsVectorJoinList
bool nextFeature(QgsFeature &f)
This is the base class for vector data providers.
Geometry is not required. It may still be returned if e.g. required for a filter condition.
Class for storing the component parts of a PostgreSQL/RDBMS datasource URI.
bool changeGeometry(QgsFeatureId fid, const QgsGeometry &geometry, bool skipDefaultValue=false)
Changes a feature&#39;s geometry within the layer&#39;s edit buffer (but does not immediately commit the chan...
virtual void invalidateConnections(const QString &connection)
Invalidate connections corresponding to specified name.
A vector of attributes.
Definition: qgsattributes.h:58
Represents a vector layer which manages a vector based data sets.
bool addAttribute(const QgsField &field)
Add an attribute field (but does not commit it) returns true if the field was added.
static Type flatType(Type type)
Returns the flat type for a WKB type.
Definition: qgswkbtypes.h:427
#define CUSTOM_PROPERTY_REMOTE_PROVIDER
void progressStarted()
The signal is emitted when the process has started.
QgsLayerTreeLayer * clone() const override
Create a copy of the node. Returns new instance.
bool addFeature(QgsFeature &feature, QgsFeatureSink::Flags flags=nullptr) override
Adds a single feature to the sink.
QVariant::Type type
Definition: qgsfield.h:55
void addLayerRecord(const QgsMapThemeCollection::MapThemeLayerRecord &record)
Add a new record for a layer.
QString authid() const
Returns the authority identifier for the CRS.
QgsAttributes attributes
Definition: qgsfeature.h:72
QString readPath(const QString &filename) const
Turn filename read from the project file to an absolute path.
void layerWasAdded(QgsMapLayer *layer)
Emitted when a layer was added to the registry.
Layer tree node points to a map layer.
QString joinLayerId() const
ID of the joined layer - may be used to resolve reference to the joined layer.
void addRelation(const QgsRelation &relation)
Add a relation.
QString errorMessage() const
Returns the most recent error message encountered by the database.
virtual void exportNamedStyle(QDomDocument &doc, QString &errorMsg) const
Export the properties of this layer as named style in a QDomDocument.