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