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