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