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