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