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