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