QGIS API Documentation  2.17.0-Master (0497e4a)
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 "qgsmaplayerregistry.h"
27 #include "qgsofflineediting.h"
28 #include "qgsproject.h"
29 #include "qgsvectordataprovider.h"
32 #include "qgsslconnect.h"
33 
34 #include <QDir>
35 #include <QDomDocument>
36 #include <QDomNode>
37 #include <QFile>
38 #include <QMessageBox>
39 
40 extern "C"
41 {
42 #include <sqlite3.h>
43 #include <spatialite.h>
44 }
45 
46 // TODO: DEBUG
47 #include <QDebug>
48 // END
49 
50 #define CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE "isOfflineEditable"
51 #define CUSTOM_PROPERTY_REMOTE_SOURCE "remoteSource"
52 #define CUSTOM_PROPERTY_REMOTE_PROVIDER "remoteProvider"
53 #define PROJECT_ENTRY_SCOPE_OFFLINE "OfflineEditingPlugin"
54 #define PROJECT_ENTRY_KEY_OFFLINE_DB_PATH "/OfflineDbPath"
55 
57 {
58  connect( QgsMapLayerRegistry::instance(), SIGNAL( layerWasAdded( QgsMapLayer* ) ), this, SLOT( layerAdded( QgsMapLayer* ) ) );
59 }
60 
62 {
63 }
64 
80 bool QgsOfflineEditing::convertToOfflineProject( const QString& offlineDataPath, const QString& offlineDbFile, const QStringList& layerIds, bool onlySelected )
81 {
82  if ( layerIds.isEmpty() )
83  {
84  return false;
85  }
86  QString dbPath = QDir( offlineDataPath ).absoluteFilePath( offlineDbFile );
87  if ( createSpatialiteDB( dbPath ) )
88  {
89  sqlite3* db;
90  int rc = QgsSLConnect::sqlite3_open( dbPath.toUtf8().constData(), &db );
91  if ( rc != SQLITE_OK )
92  {
93  showWarning( tr( "Could not open the spatialite database" ) );
94  }
95  else
96  {
97  // create logging tables
98  createLoggingTables( db );
99 
100  emit progressStarted();
101 
102  QMap<QString, QgsVectorJoinList > joinInfoBuffer;
103  QMap<QString, QgsVectorLayer*> layerIdMapping;
104 
105  Q_FOREACH ( const QString& layerId, layerIds )
106  {
107  QgsMapLayer* layer = QgsMapLayerRegistry::instance()->mapLayer( layerId );
108  QgsVectorLayer* vl = qobject_cast<QgsVectorLayer*>( layer );
109  if ( !vl )
110  continue;
111  QgsVectorJoinList joins = vl->vectorJoins();
112 
113  // Layer names will be appended an _offline suffix
114  // Join fields are prefixed with the layer name and we do not want the
115  // field name to change so we stabilize the field name by defining a
116  // custom prefix with the layername without _offline suffix.
117  QgsVectorJoinList::iterator joinIt = joins.begin();
118  while ( joinIt != joins.end() )
119  {
120  if ( joinIt->prefix.isNull() )
121  {
122  QgsVectorLayer* vl = qobject_cast<QgsVectorLayer*>( QgsMapLayerRegistry::instance()->mapLayer( joinIt->joinLayerId ) );
123 
124  if ( vl )
125  joinIt->prefix = vl->name() + '_';
126  }
127  ++joinIt;
128  }
129  joinInfoBuffer.insert( vl->id(), joins );
130  }
131 
132  // copy selected vector layers to SpatiaLite
133  for ( int i = 0; i < layerIds.count(); i++ )
134  {
135  emit layerProgressUpdated( i + 1, layerIds.count() );
136 
137  QgsMapLayer* layer = QgsMapLayerRegistry::instance()->mapLayer( layerIds.at( i ) );
138  QgsVectorLayer* vl = qobject_cast<QgsVectorLayer*>( layer );
139  if ( vl )
140  {
141  QString origLayerId = vl->id();
142  QgsVectorLayer* newLayer = copyVectorLayer( vl, db, dbPath, onlySelected );
143 
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
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 ( QgsVectorJoinInfo 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.joinLayerId = newJoinedLayer->id();
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 += " (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;
217  for ( QMap<QString, QgsMapLayer*>::iterator layer_it = mapLayers.begin() ; layer_it != mapLayers.end(); ++layer_it )
218  {
219  QgsMapLayer* layer = layer_it.value();
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( "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
256 
257  // copy style
258  copySymbology( offlineLayer, remoteLayer );
259 
260  // apply layer edit log
261  QString qgisLayerId = layer->id();
262  QString sql = QString( "SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( qgisLayerId );
263  int layerId = sqlQueryInt( db, sql, -1 );
264  if ( layerId != -1 )
265  {
266  remoteLayer->startEditing();
267 
268  // TODO: only get commitNos of this layer?
269  int commitNo = getCommitNo( db );
270  QgsDebugMsgLevel( QString( "Found %1 commits" ).arg( commitNo ), 4 );
271  for ( int i = 0; i < commitNo; i++ )
272  {
273  QgsDebugMsgLevel( "Apply commits chronologically", 4 );
274  // apply commits chronologically
275  applyAttributesAdded( remoteLayer, db, layerId, i );
276  applyAttributeValueChanges( offlineLayer, remoteLayer, db, layerId, i );
277  applyGeometryChanges( remoteLayer, db, layerId, i );
278  }
279 
280  applyFeaturesAdded( offlineLayer, remoteLayer, db, layerId );
281  applyFeaturesRemoved( remoteLayer, db, layerId );
282 
283  if ( remoteLayer->commitChanges() )
284  {
285  // update fid lookup
286  updateFidLookup( remoteLayer, db, layerId );
287 
288  // clear edit log for this layer
289  sql = QString( "DELETE FROM 'log_added_attrs' WHERE \"layer_id\" = %1" ).arg( layerId );
290  sqlExec( db, sql );
291  sql = QString( "DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
292  sqlExec( db, sql );
293  sql = QString( "DELETE FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( layerId );
294  sqlExec( db, sql );
295  sql = QString( "DELETE FROM 'log_feature_updates' WHERE \"layer_id\" = %1" ).arg( layerId );
296  sqlExec( db, sql );
297  sql = QString( "DELETE FROM 'log_geometry_updates' WHERE \"layer_id\" = %1" ).arg( layerId );
298  sqlExec( db, sql );
299 
300  // reset commitNo
301  QString sql = QString( "UPDATE 'log_indices' SET 'last_index' = 0 WHERE \"name\" = 'commit_no'" );
302  sqlExec( db, sql );
303  }
304  else
305  {
306  showWarning( remoteLayer->commitErrors().join( "\n" ) );
307  }
308  }
309  else
310  {
311  QgsDebugMsg( "Could not find the layer id in the edit logs!" );
312  }
313  // Invalidate the connection to force a reload if the project is put offline
314  // again with the same path
315  offlineLayer->dataProvider()->invalidateConnections( QgsDataSourceURI( offlineLayer->source() ).database() );
316  // remove offline layer
318 
319 
320  // disable offline project
321  QString projectTitle = QgsProject::instance()->title();
322  projectTitle.remove( QRegExp( " \\(offline\\)$" ) );
323  QgsProject::instance()->setTitle( projectTitle );
325  remoteLayer->reload(); //update with other changes
326  }
327  else
328  {
329  QgsDebugMsg( "Remote layer is not valid!" );
330  }
331  }
332 
333  emit progressStopped();
334 
335  sqlite3_close( db );
336 }
337 
338 void QgsOfflineEditing::initializeSpatialMetadata( sqlite3 *sqlite_handle )
339 {
340  // attempting to perform self-initialization for a newly created DB
341  if ( !sqlite_handle )
342  return;
343  // checking if this DB is really empty
344  char **results;
345  int rows, columns;
346  int ret = sqlite3_get_table( sqlite_handle, "select count(*) from sqlite_master", &results, &rows, &columns, nullptr );
347  if ( ret != SQLITE_OK )
348  return;
349  int count = 0;
350  if ( rows >= 1 )
351  {
352  for ( int i = 1; i <= rows; i++ )
353  count = atoi( results[( i * columns ) + 0] );
354  }
355 
356  sqlite3_free_table( results );
357 
358  if ( count > 0 )
359  return;
360 
361  bool above41 = false;
362  ret = sqlite3_get_table( sqlite_handle, "select spatialite_version()", &results, &rows, &columns, nullptr );
363  if ( ret == SQLITE_OK && rows == 1 && columns == 1 )
364  {
365  QString version = QString::fromUtf8( results[1] );
366  QStringList parts = version.split( ' ', QString::SkipEmptyParts );
367  if ( parts.size() >= 1 )
368  {
369  QStringList verparts = parts[0].split( '.', QString::SkipEmptyParts );
370  above41 = verparts.size() >= 2 && ( verparts[0].toInt() > 4 || ( verparts[0].toInt() == 4 && verparts[1].toInt() >= 1 ) );
371  }
372  }
373 
374  sqlite3_free_table( results );
375 
376  // all right, it's empty: proceding to initialize
377  char *errMsg = nullptr;
378  ret = sqlite3_exec( sqlite_handle, above41 ? "SELECT InitSpatialMetadata(1)" : "SELECT InitSpatialMetadata()", nullptr, nullptr, &errMsg );
379 
380  if ( ret != SQLITE_OK )
381  {
382  QString errCause = tr( "Unable to initialize SpatialMetadata:\n" );
383  errCause += QString::fromUtf8( errMsg );
384  showWarning( errCause );
385  sqlite3_free( errMsg );
386  return;
387  }
388  spatial_ref_sys_init( sqlite_handle, 0 );
389 }
390 
391 bool QgsOfflineEditing::createSpatialiteDB( const QString& offlineDbPath )
392 {
393  int ret;
394  sqlite3 *sqlite_handle;
395  char *errMsg = nullptr;
396  QFile newDb( offlineDbPath );
397  if ( newDb.exists() )
398  {
399  QFile::remove( offlineDbPath );
400  }
401 
402  // see also QgsNewSpatialiteLayerDialog::createDb()
403 
404  QFileInfo fullPath = QFileInfo( offlineDbPath );
405  QDir path = fullPath.dir();
406 
407  // Must be sure there is destination directory ~/.qgis
408  QDir().mkpath( path.absolutePath() );
409 
410  // creating/opening the new database
411  QString dbPath = newDb.fileName();
412  ret = QgsSLConnect::sqlite3_open_v2( dbPath.toUtf8().constData(), &sqlite_handle, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr );
413  if ( ret )
414  {
415  // an error occurred
416  QString errCause = tr( "Could not create a new database\n" );
417  errCause += QString::fromUtf8( sqlite3_errmsg( sqlite_handle ) );
418  sqlite3_close( sqlite_handle );
419  showWarning( errCause );
420  return false;
421  }
422  // activating Foreign Key constraints
423  ret = sqlite3_exec( sqlite_handle, "PRAGMA foreign_keys = 1", nullptr, nullptr, &errMsg );
424  if ( ret != SQLITE_OK )
425  {
426  showWarning( tr( "Unable to activate FOREIGN_KEY constraints" ) );
427  sqlite3_free( errMsg );
428  QgsSLConnect::sqlite3_close( sqlite_handle );
429  return false;
430  }
431  initializeSpatialMetadata( sqlite_handle );
432 
433  // all done: closing the DB connection
434  QgsSLConnect::sqlite3_close( sqlite_handle );
435 
436  return true;
437 }
438 
439 void QgsOfflineEditing::createLoggingTables( sqlite3* db )
440 {
441  // indices
442  QString sql = "CREATE TABLE 'log_indices' ('name' TEXT, 'last_index' INTEGER)";
443  sqlExec( db, sql );
444 
445  sql = "INSERT INTO 'log_indices' VALUES ('commit_no', 0)";
446  sqlExec( db, sql );
447 
448  sql = "INSERT INTO 'log_indices' VALUES ('layer_id', 0)";
449  sqlExec( db, sql );
450 
451  // layername <-> layer id
452  sql = "CREATE TABLE 'log_layer_ids' ('id' INTEGER, 'qgis_id' TEXT)";
453  sqlExec( db, sql );
454 
455  // offline fid <-> remote fid
456  sql = "CREATE TABLE 'log_fids' ('layer_id' INTEGER, 'offline_fid' INTEGER, 'remote_fid' INTEGER)";
457  sqlExec( db, sql );
458 
459  // added attributes
460  sql = "CREATE TABLE 'log_added_attrs' ('layer_id' INTEGER, 'commit_no' INTEGER, ";
461  sql += "'name' TEXT, 'type' INTEGER, 'length' INTEGER, 'precision' INTEGER, 'comment' TEXT)";
462  sqlExec( db, sql );
463 
464  // added features
465  sql = "CREATE TABLE 'log_added_features' ('layer_id' INTEGER, 'fid' INTEGER)";
466  sqlExec( db, sql );
467 
468  // removed features
469  sql = "CREATE TABLE 'log_removed_features' ('layer_id' INTEGER, 'fid' INTEGER)";
470  sqlExec( db, sql );
471 
472  // feature updates
473  sql = "CREATE TABLE 'log_feature_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'attr' INTEGER, 'value' TEXT)";
474  sqlExec( db, sql );
475 
476  // geometry updates
477  sql = "CREATE TABLE 'log_geometry_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'geom_wkt' TEXT)";
478  sqlExec( db, sql );
479 
480  /* TODO: other logging tables
481  - attr delete (not supported by SpatiaLite provider)
482  */
483 }
484 
485 QgsVectorLayer* QgsOfflineEditing::copyVectorLayer( QgsVectorLayer* layer, sqlite3* db, const QString& offlineDbPath , bool onlySelected )
486 {
487  if ( !layer )
488  return nullptr;
489 
490  QString tableName = layer->id();
491  QgsDebugMsgLevel( QString( "Creating offline table %1 ..." ).arg( tableName ), 4 );
492 
493  // create table
494  QString sql = QString( "CREATE TABLE '%1' (" ).arg( tableName );
495  QString delim = "";
496  Q_FOREACH ( const QgsField& field, layer->dataProvider()->fields() )
497  {
498  QString dataType = "";
499  QVariant::Type type = field.type();
500  if ( type == QVariant::Int || type == QVariant::LongLong )
501  {
502  dataType = "INTEGER";
503  }
504  else if ( type == QVariant::Double )
505  {
506  dataType = "REAL";
507  }
508  else if ( type == QVariant::String )
509  {
510  dataType = "TEXT";
511  }
512  else
513  {
514  showWarning( tr( "%1: Unknown data type %2. Not using type affinity for the field." ).arg( field.name(), QVariant::typeToName( type ) ) );
515  }
516 
517  sql += delim + QString( "'%1' %2" ).arg( field.name(), dataType );
518  delim = ',';
519  }
520  sql += ')';
521 
522  int rc = sqlExec( db, sql );
523 
524  // add geometry column
525  if ( layer->hasGeometryType() )
526  {
527  QString geomType = "";
528  switch ( layer->wkbType() )
529  {
530  case QGis::WKBPoint:
531  geomType = "POINT";
532  break;
533  case QGis::WKBMultiPoint:
534  geomType = "MULTIPOINT";
535  break;
536  case QGis::WKBLineString:
537  geomType = "LINESTRING";
538  break;
540  geomType = "MULTILINESTRING";
541  break;
542  case QGis::WKBPolygon:
543  geomType = "POLYGON";
544  break;
546  geomType = "MULTIPOLYGON";
547  break;
548  default:
549  showWarning( tr( "QGIS wkbType %1 not supported" ).arg( layer->wkbType() ) );
550  break;
551  };
552  QString sqlAddGeom = QString( "SELECT AddGeometryColumn('%1', 'Geometry', %2, '%3', 2)" )
553  .arg( tableName )
554  .arg( layer->crs().authid().startsWith( "EPSG:", Qt::CaseInsensitive ) ? layer->crs().authid().mid( 5 ).toLong() : 0 )
555  .arg( geomType );
556 
557  // create spatial index
558  QString sqlCreateIndex = QString( "SELECT CreateSpatialIndex('%1', 'Geometry')" ).arg( tableName );
559 
560  if ( rc == SQLITE_OK )
561  {
562  rc = sqlExec( db, sqlAddGeom );
563  if ( rc == SQLITE_OK )
564  {
565  rc = sqlExec( db, sqlCreateIndex );
566  }
567  }
568  }
569 
570  if ( rc == SQLITE_OK )
571  {
572  // add new layer
573  QString connectionString = QString( "dbname='%1' table='%2'%3 sql=" )
574  .arg( offlineDbPath,
575  tableName, layer->hasGeometryType() ? "(Geometry)" : "" );
576  QgsVectorLayer* newLayer = new QgsVectorLayer( connectionString,
577  layer->name() + " (offline)", "spatialite" );
578  if ( newLayer->isValid() )
579  {
580  // mark as offline layer
582 
583  // store original layer source
586 
587  // register this layer with the central layers registry
589  QList<QgsMapLayer *>() << newLayer );
590 
591  // copy style
593  bool hasLabels = layer->hasLabelsEnabled();
595  if ( !hasLabels )
596  {
597  // NOTE: copy symbology before adding the layer so it is displayed correctly
598  copySymbology( layer, newLayer );
599  }
600 
602  // Find the parent group of the original layer
603  QgsLayerTreeLayer* layerTreeLayer = layerTreeRoot->findLayer( layer->id() );
604  if ( layerTreeLayer )
605  {
606  QgsLayerTreeGroup* parentTreeGroup = qobject_cast<QgsLayerTreeGroup*>( layerTreeLayer->parent() );
607  if ( parentTreeGroup )
608  {
609  int index = parentTreeGroup->children().indexOf( layerTreeLayer );
610  // Move the new layer from the root group to the new group
611  QgsLayerTreeLayer* newLayerTreeLayer = layerTreeRoot->findLayer( newLayer->id() );
612  if ( newLayerTreeLayer )
613  {
614  QgsLayerTreeNode* newLayerTreeLayerClone = newLayerTreeLayer->clone();
615  QgsLayerTreeGroup* grp = qobject_cast<QgsLayerTreeGroup*>( newLayerTreeLayer->parent() );
616  parentTreeGroup->insertChildNode( index, newLayerTreeLayerClone );
617  if ( grp )
618  grp->removeChildNode( newLayerTreeLayer );
619  }
620  }
621  }
622 
623  if ( hasLabels )
624  {
625  // NOTE: copy symbology of layers with labels enabled after adding to project, as it will crash otherwise (WORKAROUND)
626  copySymbology( layer, newLayer );
627  }
628 
629  // copy features
630  newLayer->startEditing();
631  QgsFeature f;
632 
633  // NOTE: force feature recount for PostGIS layer, else only visible features are counted, before iterating over all features (WORKAROUND)
634  layer->setSubsetString( layer->subsetString() );
635 
636  QgsFeatureRequest req;
637 
638  if ( onlySelected )
639  {
640  QgsFeatureIds selectedFids = layer->selectedFeaturesIds();
641  if ( !selectedFids.isEmpty() )
642  req.setFilterFids( selectedFids );
643  }
644 
645  QgsFeatureIterator fit = layer->dataProvider()->getFeatures( req );
646 
648  {
650  }
651  else
652  {
654  }
655  int featureCount = 1;
656 
657  QList<QgsFeatureId> remoteFeatureIds;
658  while ( fit.nextFeature( f ) )
659  {
660  remoteFeatureIds << f.id();
661 
662  // NOTE: Spatialite provider ignores position of geometry column
663  // fill gap in QgsAttributeMap if geometry column is not last (WORKAROUND)
664  int column = 0;
665  QgsAttributes attrs = f.attributes();
666  QgsAttributes newAttrs( attrs.count() );
667  for ( int it = 0; it < attrs.count(); ++it )
668  {
669  newAttrs[column++] = attrs.at( it );
670  }
671  f.setAttributes( newAttrs );
672 
673  newLayer->addFeature( f, false );
674 
675  emit progressUpdated( featureCount++ );
676  }
677  if ( newLayer->commitChanges() )
678  {
680  featureCount = 1;
681 
682  // update feature id lookup
683  int layerId = getOrCreateLayerId( db, newLayer->id() );
684  QList<QgsFeatureId> offlineFeatureIds;
685 
686  QgsFeatureIterator fit = newLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setSubsetOfAttributes( QgsAttributeList() ) );
687  while ( fit.nextFeature( f ) )
688  {
689  offlineFeatureIds << f.id();
690  }
691 
692  // NOTE: insert fids in this loop, as the db is locked during newLayer->nextFeature()
693  sqlExec( db, "BEGIN" );
694  int remoteCount = remoteFeatureIds.size();
695  for ( int i = 0; i < remoteCount; i++ )
696  {
697  // Check if the online feature has been fetched (WFS download aborted for some reason)
698  if ( i < offlineFeatureIds.count() )
699  {
700  addFidLookup( db, layerId, offlineFeatureIds.at( i ), remoteFeatureIds.at( i ) );
701  }
702  else
703  {
704  showWarning( tr( "Feature cannot be copied to the offline layer, please check if the online layer '%1' is still accessible." ).arg( layer->name() ) );
705  return nullptr;
706  }
707  emit progressUpdated( featureCount++ );
708  }
709  sqlExec( db, "COMMIT" );
710  }
711  else
712  {
713  showWarning( newLayer->commitErrors().join( "\n" ) );
714  }
715  }
716  return newLayer;
717  }
718  return nullptr;
719 }
720 
721 void QgsOfflineEditing::applyAttributesAdded( QgsVectorLayer* remoteLayer, sqlite3* db, int layerId, int commitNo )
722 {
723  QString sql = QString( "SELECT \"name\", \"type\", \"length\", \"precision\", \"comment\" FROM 'log_added_attrs' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2" ).arg( layerId ).arg( commitNo );
724  QList<QgsField> fields = sqlQueryAttributesAdded( db, sql );
725 
726  const QgsVectorDataProvider* provider = remoteLayer->dataProvider();
727  QList<QgsVectorDataProvider::NativeType> nativeTypes = provider->nativeTypes();
728 
729  // NOTE: uses last matching QVariant::Type of nativeTypes
730  QMap < QVariant::Type, QString /*typeName*/ > typeNameLookup;
731  for ( int i = 0; i < nativeTypes.size(); i++ )
732  {
733  QgsVectorDataProvider::NativeType nativeType = nativeTypes.at( i );
734  typeNameLookup[ nativeType.mType ] = nativeType.mTypeName;
735  }
736 
738 
739  for ( int i = 0; i < fields.size(); i++ )
740  {
741  // lookup typename from layer provider
742  QgsField field = fields[i];
743  if ( typeNameLookup.contains( field.type() ) )
744  {
745  QString typeName = typeNameLookup[ field.type()];
746  field.setTypeName( typeName );
747  remoteLayer->addAttribute( field );
748  }
749  else
750  {
751  showWarning( QString( "Could not add attribute '%1' of type %2" ).arg( field.name() ).arg( field.type() ) );
752  }
753 
754  emit progressUpdated( i + 1 );
755  }
756 }
757 
758 void QgsOfflineEditing::applyFeaturesAdded( QgsVectorLayer* offlineLayer, QgsVectorLayer* remoteLayer, sqlite3* db, int layerId )
759 {
760  QString sql = QString( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
761  QList<int> newFeatureIds = sqlQueryInts( db, sql );
762 
763  QgsFields remoteFlds = remoteLayer->fields();
764 
765  QgsExpressionContext context;
768  << QgsExpressionContextUtils::layerScope( remoteLayer );
769 
770  // get new features from offline layer
771  QgsFeatureList features;
772  for ( int i = 0; i < newFeatureIds.size(); i++ )
773  {
774  QgsFeature feature;
775  if ( offlineLayer->getFeatures( QgsFeatureRequest().setFilterFid( newFeatureIds.at( i ) ) ).nextFeature( feature ) )
776  {
777  features << feature;
778  }
779  }
780 
781  // copy features to remote layer
783 
784  int i = 1;
785  int newAttrsCount = remoteLayer->fields().count();
786  for ( QgsFeatureList::iterator it = features.begin(); it != features.end(); ++it )
787  {
788  QgsFeature f = *it;
789 
790  // NOTE: Spatialite provider ignores position of geometry column
791  // restore gap in QgsAttributeMap if geometry column is not last (WORKAROUND)
792  QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
793  QgsAttributes newAttrs( newAttrsCount );
794  QgsAttributes attrs = f.attributes();
795  for ( int it = 0; it < attrs.count(); ++it )
796  {
797  newAttrs[ attrLookup[ it ] ] = attrs.at( it );
798  }
799 
800  // try to use default value from the provider
801  // (important especially e.g. for postgis primary key generated from a sequence)
802  for ( int k = 0; k < newAttrs.count(); ++k )
803  {
804  if ( !newAttrs.at( k ).isNull() )
805  continue;
806 
807  if ( !remoteLayer->defaultValueExpression( k ).isEmpty() )
808  newAttrs[k] = remoteLayer->defaultValue( k, f, &context );
809  else if ( remoteFlds.fieldOrigin( k ) == QgsFields::OriginProvider )
810  newAttrs[k] = remoteLayer->dataProvider()->defaultValue( remoteFlds.fieldOriginIndex( k ) );
811  }
812 
813  f.setAttributes( newAttrs );
814 
815  remoteLayer->addFeature( f, false );
816 
817  emit progressUpdated( i++ );
818  }
819 }
820 
821 void QgsOfflineEditing::applyFeaturesRemoved( QgsVectorLayer* remoteLayer, sqlite3* db, int layerId )
822 {
823  QString sql = QString( "SELECT \"fid\" FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( layerId );
824  QgsFeatureIds values = sqlQueryFeaturesRemoved( db, sql );
825 
827 
828  int i = 1;
829  for ( QgsFeatureIds::const_iterator it = values.begin(); it != values.end(); ++it )
830  {
831  QgsFeatureId fid = remoteFid( db, layerId, *it );
832  remoteLayer->deleteFeature( fid );
833 
834  emit progressUpdated( i++ );
835  }
836 }
837 
838 void QgsOfflineEditing::applyAttributeValueChanges( QgsVectorLayer* offlineLayer, QgsVectorLayer* remoteLayer, sqlite3* db, int layerId, int commitNo )
839 {
840  QString sql = QString( "SELECT \"fid\", \"attr\", \"value\" FROM 'log_feature_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2 " ).arg( layerId ).arg( commitNo );
841  AttributeValueChanges values = sqlQueryAttributeValueChanges( db, sql );
842 
844 
845  QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
846 
847  for ( int i = 0; i < values.size(); i++ )
848  {
849  QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid );
850  QgsDebugMsgLevel( QString( "Offline changeAttributeValue %1 = %2" ).arg( QString( attrLookup[ values.at( i ).attr ] ), values.at( i ).value ), 4 );
851  remoteLayer->changeAttributeValue( fid, attrLookup[ values.at( i ).attr ], values.at( i ).value );
852 
853  emit progressUpdated( i + 1 );
854  }
855 }
856 
857 void QgsOfflineEditing::applyGeometryChanges( QgsVectorLayer* remoteLayer, sqlite3* db, int layerId, int commitNo )
858 {
859  QString sql = QString( "SELECT \"fid\", \"geom_wkt\" FROM 'log_geometry_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2" ).arg( layerId ).arg( commitNo );
860  GeometryChanges values = sqlQueryGeometryChanges( db, sql );
861 
863 
864  for ( int i = 0; i < values.size(); i++ )
865  {
866  QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid );
867  remoteLayer->changeGeometry( fid, QgsGeometry::fromWkt( values.at( i ).geom_wkt ) );
868 
869  emit progressUpdated( i + 1 );
870  }
871 }
872 
873 void QgsOfflineEditing::updateFidLookup( QgsVectorLayer* remoteLayer, sqlite3* db, int layerId )
874 {
875  // update fid lookup for added features
876 
877  // get remote added fids
878  // NOTE: use QMap for sorted fids
879  QMap < QgsFeatureId, bool /*dummy*/ > newRemoteFids;
880  QgsFeature f;
881 
882  QgsFeatureIterator fit = remoteLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setSubsetOfAttributes( QgsAttributeList() ) );
883 
885 
886  int i = 1;
887  while ( fit.nextFeature( f ) )
888  {
889  if ( offlineFid( db, layerId, f.id() ) == -1 )
890  {
891  newRemoteFids[ f.id()] = true;
892  }
893 
894  emit progressUpdated( i++ );
895  }
896 
897  // get local added fids
898  // NOTE: fids are sorted
899  QString sql = QString( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
900  QList<int> newOfflineFids = sqlQueryInts( db, sql );
901 
902  if ( newRemoteFids.size() != newOfflineFids.size() )
903  {
904  //showWarning( QString( "Different number of new features on offline layer (%1) and remote layer (%2)" ).arg(newOfflineFids.size()).arg(newRemoteFids.size()) );
905  }
906  else
907  {
908  // add new fid lookups
909  i = 0;
910  sqlExec( db, "BEGIN" );
911  for ( QMap<QgsFeatureId, bool>::const_iterator it = newRemoteFids.begin(); it != newRemoteFids.end(); ++it )
912  {
913  addFidLookup( db, layerId, newOfflineFids.at( i++ ), it.key() );
914  }
915  sqlExec( db, "COMMIT" );
916  }
917 }
918 
919 void QgsOfflineEditing::copySymbology( QgsVectorLayer* sourceLayer, QgsVectorLayer* targetLayer )
920 {
921  QString error;
922  QDomDocument doc;
923  sourceLayer->exportNamedStyle( doc, error );
924 
925  if ( error.isEmpty() )
926  {
927  targetLayer->importNamedStyle( doc, error );
928  }
929  if ( !error.isEmpty() )
930  {
931  showWarning( error );
932  }
933 }
934 
935 // NOTE: use this to map column indices in case the remote geometry column is not last
936 QMap<int, int> QgsOfflineEditing::attributeLookup( QgsVectorLayer* offlineLayer, QgsVectorLayer* remoteLayer )
937 {
938  const QgsAttributeList& offlineAttrs = offlineLayer->attributeList();
939  const QgsAttributeList& remoteAttrs = remoteLayer->attributeList();
940 
941  QMap < int /*offline attr*/, int /*remote attr*/ > attrLookup;
942  // NOTE: use size of remoteAttrs, as offlineAttrs can have new attributes not yet synced
943  for ( int i = 0; i < remoteAttrs.size(); i++ )
944  {
945  attrLookup.insert( offlineAttrs.at( i ), remoteAttrs.at( i ) );
946  }
947 
948  return attrLookup;
949 }
950 
951 void QgsOfflineEditing::showWarning( const QString& message )
952 {
953  emit warning( tr( "Offline Editing Plugin" ), message );
954 }
955 
956 sqlite3* QgsOfflineEditing::openLoggingDb()
957 {
958  sqlite3* db = nullptr;
960  if ( !dbPath.isEmpty() )
961  {
962  QString absoluteDbPath = QgsProject::instance()->readPath( dbPath );
963  int rc = sqlite3_open( absoluteDbPath.toUtf8().constData(), &db );
964  if ( rc != SQLITE_OK )
965  {
966  QgsDebugMsg( "Could not open the spatialite logging database" );
967  showWarning( tr( "Could not open the spatialite logging database" ) );
968  sqlite3_close( db );
969  db = nullptr;
970  }
971  }
972  else
973  {
974  QgsDebugMsg( "dbPath is empty!" );
975  }
976  return db;
977 }
978 
979 int QgsOfflineEditing::getOrCreateLayerId( sqlite3* db, const QString& qgisLayerId )
980 {
981  QString sql = QString( "SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( qgisLayerId );
982  int layerId = sqlQueryInt( db, sql, -1 );
983  if ( layerId == -1 )
984  {
985  // next layer id
986  sql = "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'layer_id'";
987  int newLayerId = sqlQueryInt( db, sql, -1 );
988 
989  // insert layer
990  sql = QString( "INSERT INTO 'log_layer_ids' VALUES (%1, '%2')" ).arg( newLayerId ).arg( qgisLayerId );
991  sqlExec( db, sql );
992 
993  // increase layer_id
994  // TODO: use trigger for auto increment?
995  sql = QString( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'layer_id'" ).arg( newLayerId + 1 );
996  sqlExec( db, sql );
997 
998  layerId = newLayerId;
999  }
1000 
1001  return layerId;
1002 }
1003 
1004 int QgsOfflineEditing::getCommitNo( sqlite3* db )
1005 {
1006  QString sql = "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'commit_no'";
1007  return sqlQueryInt( db, sql, -1 );
1008 }
1009 
1010 void QgsOfflineEditing::increaseCommitNo( sqlite3* db )
1011 {
1012  QString sql = QString( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'commit_no'" ).arg( getCommitNo( db ) + 1 );
1013  sqlExec( db, sql );
1014 }
1015 
1016 void QgsOfflineEditing::addFidLookup( sqlite3* db, int layerId, QgsFeatureId offlineFid, QgsFeatureId remoteFid )
1017 {
1018  QString sql = QString( "INSERT INTO 'log_fids' VALUES ( %1, %2, %3 )" ).arg( layerId ).arg( offlineFid ).arg( remoteFid );
1019  sqlExec( db, sql );
1020 }
1021 
1022 QgsFeatureId QgsOfflineEditing::remoteFid( sqlite3* db, int layerId, QgsFeatureId offlineFid )
1023 {
1024  QString sql = QString( "SELECT \"remote_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"offline_fid\" = %2" ).arg( layerId ).arg( offlineFid );
1025  return sqlQueryInt( db, sql, -1 );
1026 }
1027 
1028 QgsFeatureId QgsOfflineEditing::offlineFid( sqlite3* db, int layerId, QgsFeatureId remoteFid )
1029 {
1030  QString sql = QString( "SELECT \"offline_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"remote_fid\" = %2" ).arg( layerId ).arg( remoteFid );
1031  return sqlQueryInt( db, sql, -1 );
1032 }
1033 
1034 bool QgsOfflineEditing::isAddedFeature( sqlite3* db, int layerId, QgsFeatureId fid )
1035 {
1036  QString sql = QString( "SELECT COUNT(\"fid\") FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( fid );
1037  return ( sqlQueryInt( db, sql, 0 ) > 0 );
1038 }
1039 
1040 int QgsOfflineEditing::sqlExec( sqlite3* db, const QString& sql )
1041 {
1042  char * errmsg;
1043  int rc = sqlite3_exec( db, sql.toUtf8(), nullptr, nullptr, &errmsg );
1044  if ( rc != SQLITE_OK )
1045  {
1046  showWarning( errmsg );
1047  }
1048  return rc;
1049 }
1050 
1051 int QgsOfflineEditing::sqlQueryInt( sqlite3* db, const QString& sql, int defaultValue )
1052 {
1053  sqlite3_stmt* stmt = nullptr;
1054  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1055  {
1056  showWarning( sqlite3_errmsg( db ) );
1057  return defaultValue;
1058  }
1059 
1060  int value = defaultValue;
1061  int ret = sqlite3_step( stmt );
1062  if ( ret == SQLITE_ROW )
1063  {
1064  value = sqlite3_column_int( stmt, 0 );
1065  }
1066  sqlite3_finalize( stmt );
1067 
1068  return value;
1069 }
1070 
1071 QList<int> QgsOfflineEditing::sqlQueryInts( sqlite3* db, const QString& sql )
1072 {
1073  QList<int> values;
1074 
1075  sqlite3_stmt* stmt = nullptr;
1076  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1077  {
1078  showWarning( sqlite3_errmsg( db ) );
1079  return values;
1080  }
1081 
1082  int ret = sqlite3_step( stmt );
1083  while ( ret == SQLITE_ROW )
1084  {
1085  values << sqlite3_column_int( stmt, 0 );
1086 
1087  ret = sqlite3_step( stmt );
1088  }
1089  sqlite3_finalize( stmt );
1090 
1091  return values;
1092 }
1093 
1094 QList<QgsField> QgsOfflineEditing::sqlQueryAttributesAdded( sqlite3* db, const QString& sql )
1095 {
1096  QList<QgsField> values;
1097 
1098  sqlite3_stmt* stmt = nullptr;
1099  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1100  {
1101  showWarning( sqlite3_errmsg( db ) );
1102  return values;
1103  }
1104 
1105  int ret = sqlite3_step( stmt );
1106  while ( ret == SQLITE_ROW )
1107  {
1108  QgsField field( QString( reinterpret_cast< const char* >( sqlite3_column_text( stmt, 0 ) ) ),
1109  static_cast< QVariant::Type >( sqlite3_column_int( stmt, 1 ) ),
1110  "", // typeName
1111  sqlite3_column_int( stmt, 2 ),
1112  sqlite3_column_int( stmt, 3 ),
1113  QString( reinterpret_cast< const char* >( sqlite3_column_text( stmt, 4 ) ) ) );
1114  values << field;
1115 
1116  ret = sqlite3_step( stmt );
1117  }
1118  sqlite3_finalize( stmt );
1119 
1120  return values;
1121 }
1122 
1123 QgsFeatureIds QgsOfflineEditing::sqlQueryFeaturesRemoved( sqlite3* db, const QString& sql )
1124 {
1125  QgsFeatureIds values;
1126 
1127  sqlite3_stmt* stmt = nullptr;
1128  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1129  {
1130  showWarning( sqlite3_errmsg( db ) );
1131  return values;
1132  }
1133 
1134  int ret = sqlite3_step( stmt );
1135  while ( ret == SQLITE_ROW )
1136  {
1137  values << sqlite3_column_int( stmt, 0 );
1138 
1139  ret = sqlite3_step( stmt );
1140  }
1141  sqlite3_finalize( stmt );
1142 
1143  return values;
1144 }
1145 
1146 QgsOfflineEditing::AttributeValueChanges QgsOfflineEditing::sqlQueryAttributeValueChanges( sqlite3* db, const QString& sql )
1147 {
1148  AttributeValueChanges values;
1149 
1150  sqlite3_stmt* stmt = nullptr;
1151  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1152  {
1153  showWarning( sqlite3_errmsg( db ) );
1154  return values;
1155  }
1156 
1157  int ret = sqlite3_step( stmt );
1158  while ( ret == SQLITE_ROW )
1159  {
1160  AttributeValueChange change;
1161  change.fid = sqlite3_column_int( stmt, 0 );
1162  change.attr = sqlite3_column_int( stmt, 1 );
1163  change.value = QString( reinterpret_cast< const char* >( sqlite3_column_text( stmt, 2 ) ) );
1164  values << change;
1165 
1166  ret = sqlite3_step( stmt );
1167  }
1168  sqlite3_finalize( stmt );
1169 
1170  return values;
1171 }
1172 
1173 QgsOfflineEditing::GeometryChanges QgsOfflineEditing::sqlQueryGeometryChanges( sqlite3* db, const QString& sql )
1174 {
1175  GeometryChanges values;
1176 
1177  sqlite3_stmt* stmt = nullptr;
1178  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1179  {
1180  showWarning( sqlite3_errmsg( db ) );
1181  return values;
1182  }
1183 
1184  int ret = sqlite3_step( stmt );
1185  while ( ret == SQLITE_ROW )
1186  {
1187  GeometryChange change;
1188  change.fid = sqlite3_column_int( stmt, 0 );
1189  change.geom_wkt = QString( reinterpret_cast< const char* >( sqlite3_column_text( stmt, 1 ) ) );
1190  values << change;
1191 
1192  ret = sqlite3_step( stmt );
1193  }
1194  sqlite3_finalize( stmt );
1195 
1196  return values;
1197 }
1198 
1199 void QgsOfflineEditing::committedAttributesAdded( const QString& qgisLayerId, const QList<QgsField>& addedAttributes )
1200 {
1201  sqlite3* db = openLoggingDb();
1202  if ( !db )
1203  return;
1204 
1205  // insert log
1206  int layerId = getOrCreateLayerId( db, qgisLayerId );
1207  int commitNo = getCommitNo( db );
1208 
1209  for ( QList<QgsField>::const_iterator it = addedAttributes.begin(); it != addedAttributes.end(); ++it )
1210  {
1211  QgsField field = *it;
1212  QString sql = QString( "INSERT INTO 'log_added_attrs' VALUES ( %1, %2, '%3', %4, %5, %6, '%7' )" )
1213  .arg( layerId )
1214  .arg( commitNo )
1215  .arg( field.name() )
1216  .arg( field.type() )
1217  .arg( field.length() )
1218  .arg( field.precision() )
1219  .arg( field.comment() );
1220  sqlExec( db, sql );
1221  }
1222 
1223  increaseCommitNo( db );
1224  sqlite3_close( db );
1225 }
1226 
1227 void QgsOfflineEditing::committedFeaturesAdded( const QString& qgisLayerId, const QgsFeatureList& addedFeatures )
1228 {
1229  sqlite3* db = openLoggingDb();
1230  if ( !db )
1231  return;
1232 
1233  // insert log
1234  int layerId = getOrCreateLayerId( db, qgisLayerId );
1235 
1236  // get new feature ids from db
1237  QgsMapLayer* layer = QgsMapLayerRegistry::instance()->mapLayer( qgisLayerId );
1238  QgsDataSourceURI uri = QgsDataSourceURI( layer->source() );
1239 
1240  // only store feature ids
1241  QString sql = QString( "SELECT ROWID FROM '%1' ORDER BY ROWID DESC LIMIT %2" ).arg( uri.table() ).arg( addedFeatures.size() );
1242  QList<int> newFeatureIds = sqlQueryInts( db, sql );
1243  for ( int i = newFeatureIds.size() - 1; i >= 0; i-- )
1244  {
1245  QString sql = QString( "INSERT INTO 'log_added_features' VALUES ( %1, %2 )" )
1246  .arg( layerId )
1247  .arg( newFeatureIds.at( i ) );
1248  sqlExec( db, sql );
1249  }
1250 
1251  sqlite3_close( db );
1252 }
1253 
1254 void QgsOfflineEditing::committedFeaturesRemoved( const QString& qgisLayerId, const QgsFeatureIds& deletedFeatureIds )
1255 {
1256  sqlite3* db = openLoggingDb();
1257  if ( !db )
1258  return;
1259 
1260  // insert log
1261  int layerId = getOrCreateLayerId( db, qgisLayerId );
1262 
1263  for ( QgsFeatureIds::const_iterator it = deletedFeatureIds.begin(); it != deletedFeatureIds.end(); ++it )
1264  {
1265  if ( isAddedFeature( db, layerId, *it ) )
1266  {
1267  // remove from added features log
1268  QString sql = QString( "DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( *it );
1269  sqlExec( db, sql );
1270  }
1271  else
1272  {
1273  QString sql = QString( "INSERT INTO 'log_removed_features' VALUES ( %1, %2)" )
1274  .arg( layerId )
1275  .arg( *it );
1276  sqlExec( db, sql );
1277  }
1278  }
1279 
1280  sqlite3_close( db );
1281 }
1282 
1283 void QgsOfflineEditing::committedAttributeValuesChanges( const QString& qgisLayerId, const QgsChangedAttributesMap& changedAttrsMap )
1284 {
1285  sqlite3* db = openLoggingDb();
1286  if ( !db )
1287  return;
1288 
1289  // insert log
1290  int layerId = getOrCreateLayerId( db, qgisLayerId );
1291  int commitNo = getCommitNo( db );
1292 
1293  for ( QgsChangedAttributesMap::const_iterator cit = changedAttrsMap.begin(); cit != changedAttrsMap.end(); ++cit )
1294  {
1295  QgsFeatureId fid = cit.key();
1296  if ( isAddedFeature( db, layerId, fid ) )
1297  {
1298  // skip added features
1299  continue;
1300  }
1301  QgsAttributeMap attrMap = cit.value();
1302  for ( QgsAttributeMap::const_iterator it = attrMap.begin(); it != attrMap.end(); ++it )
1303  {
1304  QString sql = QString( "INSERT INTO 'log_feature_updates' VALUES ( %1, %2, %3, %4, '%5' )" )
1305  .arg( layerId )
1306  .arg( commitNo )
1307  .arg( fid )
1308  .arg( it.key() ) // attr
1309  .arg( it.value().toString() ); // value
1310  sqlExec( db, sql );
1311  }
1312  }
1313 
1314  increaseCommitNo( db );
1315  sqlite3_close( db );
1316 }
1317 
1318 void QgsOfflineEditing::committedGeometriesChanges( const QString& qgisLayerId, const QgsGeometryMap& changedGeometries )
1319 {
1320  sqlite3* db = openLoggingDb();
1321  if ( !db )
1322  return;
1323 
1324  // insert log
1325  int layerId = getOrCreateLayerId( db, qgisLayerId );
1326  int commitNo = getCommitNo( db );
1327 
1328  for ( QgsGeometryMap::const_iterator it = changedGeometries.begin(); it != changedGeometries.end(); ++it )
1329  {
1330  QgsFeatureId fid = it.key();
1331  if ( isAddedFeature( db, layerId, fid ) )
1332  {
1333  // skip added features
1334  continue;
1335  }
1336  QgsGeometry geom = it.value();
1337  QString sql = QString( "INSERT INTO 'log_geometry_updates' VALUES ( %1, %2, %3, '%4' )" )
1338  .arg( layerId )
1339  .arg( commitNo )
1340  .arg( fid )
1341  .arg( geom.exportToWkt() );
1342  sqlExec( db, sql );
1343 
1344  // TODO: use WKB instead of WKT?
1345  }
1346 
1347  increaseCommitNo( db );
1348  sqlite3_close( db );
1349 }
1350 
1351 void QgsOfflineEditing::startListenFeatureChanges()
1352 {
1353  QgsVectorLayer* vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1354  // enable logging, check if editBuffer is not null
1355  if ( vLayer->editBuffer() )
1356  {
1357  connect( vLayer->editBuffer(), SIGNAL( committedAttributesAdded( const QString&, const QList<QgsField>& ) ),
1358  this, SLOT( committedAttributesAdded( const QString&, const QList<QgsField>& ) ) );
1359  connect( vLayer->editBuffer(), SIGNAL( committedAttributeValuesChanges( const QString&, const QgsChangedAttributesMap& ) ),
1360  this, SLOT( committedAttributeValuesChanges( const QString&, const QgsChangedAttributesMap& ) ) );
1361  connect( vLayer->editBuffer(), SIGNAL( committedGeometriesChanges( const QString&, const QgsGeometryMap& ) ),
1362  this, SLOT( committedGeometriesChanges( const QString&, const QgsGeometryMap& ) ) );
1363  }
1364  connect( vLayer, SIGNAL( committedFeaturesAdded( const QString&, const QgsFeatureList& ) ),
1365  this, SLOT( committedFeaturesAdded( const QString&, const QgsFeatureList& ) ) );
1366  connect( vLayer, SIGNAL( committedFeaturesRemoved( const QString&, const QgsFeatureIds& ) ),
1367  this, SLOT( committedFeaturesRemoved( const QString&, const QgsFeatureIds& ) ) );
1368 }
1369 
1370 void QgsOfflineEditing::stopListenFeatureChanges()
1371 {
1372  QgsVectorLayer* vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1373  // disable logging, check if editBuffer is not null
1374  if ( vLayer->editBuffer() )
1375  {
1376  disconnect( vLayer->editBuffer(), SIGNAL( committedAttributesAdded( const QString&, const QList<QgsField>& ) ),
1377  this, SLOT( committedAttributesAdded( const QString&, const QList<QgsField>& ) ) );
1378  disconnect( vLayer->editBuffer(), SIGNAL( committedAttributeValuesChanges( const QString&, const QgsChangedAttributesMap& ) ),
1379  this, SLOT( committedAttributeValuesChanges( const QString&, const QgsChangedAttributesMap& ) ) );
1380  disconnect( vLayer->editBuffer(), SIGNAL( committedGeometriesChanges( const QString&, const QgsGeometryMap& ) ),
1381  this, SLOT( committedGeometriesChanges( const QString&, const QgsGeometryMap& ) ) );
1382  }
1383  disconnect( vLayer, SIGNAL( committedFeaturesAdded( const QString&, const QgsFeatureList& ) ),
1384  this, SLOT( committedFeaturesAdded( const QString&, const QgsFeatureList& ) ) );
1385  disconnect( vLayer, SIGNAL( committedFeaturesRemoved( const QString&, const QgsFeatureIds& ) ),
1386  this, SLOT( committedFeaturesRemoved( const QString&, const QgsFeatureIds& ) ) );
1387 }
1388 
1389 void QgsOfflineEditing::layerAdded( QgsMapLayer* layer )
1390 {
1391  // detect offline layer
1393  {
1394  QgsVectorLayer* vLayer = qobject_cast<QgsVectorLayer *>( layer );
1395  connect( vLayer, SIGNAL( editingStarted() ), this, SLOT( startListenFeatureChanges() ) );
1396  connect( vLayer, SIGNAL( editingStopped() ), this, SLOT( stopListenFeatureChanges() ) );
1397  }
1398 }
1399 
1400 
virtual QString subsetString()
Get the string (typically sql) used to define a subset of the layer.
Layer tree group node serves as a container for layers and further groups.
Wrapper for iterator of features from vector data provider or vector layer.
void layerProgressUpdated(int layer, int numLayers)
Emit a signal that the next layer of numLayers has started processing.
static unsigned index
bool addJoin(const QgsVectorJoinInfo &joinInfo)
Joins another vector layer to this layer.
Base class for all map layer types.
Definition: qgsmaplayer.h:49
QGis::WkbType wkbType() const
Returns the WKBType or WKBUnknown in case of error.
Filter using feature IDs.
QgsAttributes attributes() const
Returns the feature&#39;s attributes.
Definition: qgsfeature.cpp:110
QString readEntry(const QString &scope, const QString &key, const QString &def=QString::null, bool *ok=nullptr) const
FieldOrigin fieldOrigin(int fieldIdx) const
Get field&#39;s origin (value from an enumeration)
Definition: qgsfield.cpp:448
QString name
Definition: qgsfield.h:52
int precision
Definition: qgsfield.h:50
#define PROJECT_ENTRY_KEY_OFFLINE_DB_PATH
bool remove()
bool deleteFeature(QgsFeatureId fid)
Delete a feature from the layer (but does not commit it)
#define QgsDebugMsg(str)
Definition: qgslogger.h:33
int size() const
QObject * sender() const
QgsMapLayer * mapLayer(const QString &theLayerId) const
Retrieve a pointer to a registered layer by layer ID.
QStringList split(const QString &sep, SplitBehavior behavior, Qt::CaseSensitivity cs) const
#define CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE
QgsFeatureIterator getFeatures(const QgsFeatureRequest &request=QgsFeatureRequest())
Query the provider for features specified in request.
bool commitChanges()
Attempts to commit any changes to disk.
bool startEditing()
Make layer editable.
const_iterator constBegin() const
const T & at(int i) const
void setCustomProperty(const QString &key, const QVariant &value)
Set a custom property for layer.
const QgsCoordinateReferenceSystem & crs() const
Returns layer&#39;s spatial reference system.
QString fileName() const
QString comment
Definition: qgsfield.h:51
#define Q_NOWARN_DEPRECATED_PUSH
Definition: qgis.h:515
#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: qgsfield.h:252
QMap< QString, QgsMapLayer * > mapLayers() const
Returns a map of all registered layers by layer ID.
A geometry is the spatial representation of a feature.
Definition: qgsgeometry.h:76
void setAttributes(const QgsAttributes &attrs)
Sets the feature&#39;s attributes.
Definition: qgsfeature.cpp:115
void removeChildNode(QgsLayerTreeNode *node)
Remove a child node from this group. The node will be deleted.
void removeMapLayers(const QStringList &theLayerIds)
Remove a set of layers from the registry by layer ID.
QgsLayerTreeGroup * layerTreeRoot() const
Return pointer to the root (invisible) node of the project&#39;s layer tree.
field comes from the underlying data provider of the vector layer (originIndex = index in provider&#39;s ...
Definition: qgsfield.h:259
QString join(const QString &separator) const
bool exists() const
The feature class encapsulates a single feature including its id, geometry and a list of field/values...
Definition: qgsfeature.h:187
QString & remove(int position, int n)
bool addFeature(QgsFeature &feature, bool alsoUpdateExtent=true)
Adds a feature.
const QList< QgsVectorJoinInfo > vectorJoins() const
bool isOfflineProject() const
Return true if current project is offline.
bool disconnect(const QObject *sender, const char *signal, const QObject *receiver, const char *method)
int count() const
Return number of items.
Definition: qgsfield.cpp:402
QString tr(const char *sourceText, const char *disambiguation, int n)
static int sqlite3_close(sqlite3 *)
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.
int size() const
QgsFields fields() const
Returns the list of fields of this layer.
int length
Definition: qgsfield.h:49
int indexOf(const T &value, int from) const
int fieldOriginIndex(int fieldIdx) const
Get field&#39;s origin index (its meaning is specific to each type of origin)
Definition: qgsfield.cpp:456
bool hasGeometryType() const
Returns true if this is a geometry layer and false in case of NoGeometry (table only) or UnknownGeome...
long featureCount(QgsSymbolV2 *symbol)
Number of features rendered with specified symbol.
static int sqlite3_open(const char *filename, sqlite3 **ppDb)
bool writeEntry(const QString &scope, const QString &key, bool value)
void progressUpdated(int progress)
Emit a signal with the progress of the current mode.
QgsVectorLayerEditBuffer * editBuffer()
Buffer with uncommitted editing operations. Only valid after editing has been turned on...
int count(const T &value) const
QString fromUtf8(const char *str, int size)
static QgsExpressionContextScope * globalScope()
Creates a new scope which contains variables and functions relating to the global QGIS context...
QString id() const
Get this layer&#39;s unique ID, this ID is used to access this layer from map layer registry.
void progressStopped()
Emit a signal that processing of all layers has finished.
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:34
QList< QgsMapLayer * > addMapLayers(const QList< QgsMapLayer *> &theMapLayers, bool addToLegend=true, bool takeOwnership=true)
Add a list of layers to the map of loaded layers.
QString fileName() const
Q_DECL_DEPRECATED void title(const QString &title)
Every project has an associated title string.
Definition: qgsproject.h:91
void setTypeName(const QString &typeName)
Set the field type.
Definition: qgsfield.cpp:143
static int sqlite3_open_v2(const char *filename, sqlite3 **ppDb, int flags, const char *zVfs)
bool isEmpty() const
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.
bool isEmpty() const
const_iterator constEnd() const
const char * constData() const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const
virtual long featureCount() const =0
Number of features in the layer.
This class wraps a request for features to a vector layer (or directly its vector data provider)...
QList< int > QgsAttributeList
This class is a base class for nodes in a layer tree.
virtual QgsFeatureIterator getFeatures(const QgsFeatureRequest &request=QgsFeatureRequest())=0
Query the provider for features specified in request.
bool removeEntry(const QString &scope, const QString &key)
Remove the given key.
QgsAttributeList attributeList() const
Returns list of attribute indexes.
bool changeGeometry(QgsFeatureId fid, QgsGeometry *geom)
Change feature&#39;s geometry.
QString exportToWkt(int precision=17) const
Exports the geometry to WKT.
QDir dir() const
Encapsulate a field in an attribute table or data source.
Definition: qgsfield.h:44
virtual bool importNamedStyle(QDomDocument &doc, QString &errorMsg)
Import the properties of this layer from a QDomDocument.
iterator end()
const QList< NativeType > & nativeTypes() const
Returns the names of the supported types.
QList< QgsLayerTreeNode * > children()
Get list of children of the node. Children are owned by the parent.
bool isValid()
Return the status of the layer.
#define PROJECT_ENTRY_SCOPE_OFFLINE
iterator begin()
Q_DECL_DEPRECATED bool changeAttributeValue(QgsFeatureId fid, int field, const QVariant &value, bool emitSignal)
Changes an attribute value (but does not commit it)
const QStringList & commitErrors()
iterator end()
QgsFeatureId id() const
Get the feature ID for this feature.
Definition: qgsfeature.cpp:65
Class for storing the component parts of a PostgreSQL/RDBMS datasource URI.
struct sqlite3 sqlite3
iterator begin()
bool contains(QChar ch, Qt::CaseSensitivity cs) const
virtual void reload() override
Synchronises with changes in the datasource.
QgsLayerTreeLayer * findLayer(const QString &layerId) const
Find layer node representing the map layer specified by its ID. Searches recursively the whole sub-tr...
QString defaultValueExpression(int index) const
Returns the expression used when calculating the default value for a field.
const char * typeToName(Type typ)
const QgsFeatureIds & selectedFeaturesIds() const
Return reference to identifiers of selected features.
#define Q_NOWARN_DEPRECATED_POP
Definition: qgis.h:516
const Key key(const T &value) const
long toLong(bool *ok, int base) const
virtual void exportNamedStyle(QDomDocument &doc, QString &errorMsg)
Export the properties of this layer as named style in a QDomDocument.
const T & at(int i) const
void warning(const QString &title, const QString &message)
Emitted when a warning needs to be displayed.
virtual const QgsFields & fields() const =0
Return a map of indexes with field names for this layer.
static QgsMapLayerRegistry * instance()
Returns the instance pointer, creating the object on the first call.
QString mid(int position, int n) const
QgsFeatureRequest & setFilterFids(const QgsFeatureIds &fids)
Set feature IDs that should be fetched.
virtual QVariant defaultValue(int fieldId)
Returns the default value for field specified by fieldId.
void insertChildNode(int index, QgsLayerTreeNode *node)
Insert existing node at specified position. The node must not have a parent yet. The node will be own...
QString source() const
Returns the source for the layer.
QString absolutePath() const
void synchronize()
Synchronize to remote layers.
iterator end()
static QgsProject * instance()
Returns the QgsProject singleton instance.
Definition: qgsproject.cpp:382
QString table() const
Returns the table.
int count(const T &value) const
void setTitle(const QString &title)
Sets the project&#39;s title.
Definition: qgsproject.cpp:391
QString absoluteFilePath(const QString &fileName) const
QStringList split(const QString &sep, const QString &str, bool allowEmptyEntries)
virtual bool setSubsetString(const QString &subset)
Set the string (typically sql) used to define a subset of the layer.
bool toBool() const
bool isEmpty() const
void progressModeSet(QgsOfflineEditing::ProgressMode mode, int maximum)
Emit a signal that sets the mode for the progress of the current operation.
QString readPath(QString filename) const
Turn filename read from the project file to an absolute path.
qint64 QgsFeatureId
Definition: qgsfeature.h:31
QString name
Read property of QString layerName.
Definition: qgsmaplayer.h:53
iterator insert(const Key &key, const T &value)
QVariant customProperty(const QString &value, const QVariant &defaultValue=QVariant()) const
Read a custom property from layer.
static QgsExpressionContextScope * projectScope()
Creates a new scope which contains variables and functions relating to the current QGIS project...
static QgsGeometry * fromWkt(const QString &wkt)
Creates a new geometry from a WKT string.
QgsVectorDataProvider * dataProvider()
Returns the data provider.
static QgsExpressionContextScope * layerScope(const QgsMapLayer *layer)
Creates a new scope which contains variables and functions relating to a QgsMapLayer.
QString providerType() const
Return the provider type for this layer.
bool nextFeature(QgsFeature &f)
This is the base class for vector data providers.
Q_DECL_DEPRECATED bool hasLabelsEnabled() const
Label is on.
Geometry is not required. It may still be returned if e.g. required for a filter condition.
virtual void invalidateConnections(const QString &connection)
Invalidate connections corresponding to specified name.
A vector of attributes.
Definition: qgsfeature.h:115
bool connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
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:97
bool addAttribute(const QgsField &field)
Add an attribute field (but does not commit it) returns true if the field was added.
QString arg(qlonglong a, int fieldWidth, int base, const QChar &fillChar) const
QString toString() const
#define CUSTOM_PROPERTY_REMOTE_PROVIDER
QString joinLayerId
Source layer.
void progressStarted()
Emit a signal that processing has started.
iterator begin()
virtual QgsLayerTreeLayer * clone() const override
Create a copy of the node. Returns new instance.
QString authid() const
Returns the authority identifier for the CRS, which includes both the authority (eg EPSG) and the CRS...
bool mkpath(const QString &dirPath) const
Layer tree node points to a map layer.
const T value(const Key &key) const
QVariant defaultValue(int index, const QgsFeature &feature=QgsFeature(), QgsExpressionContext *context=nullptr) const
Returns the calculated default value for the specified field index.
QByteArray toUtf8() const