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