QGIS API Documentation  2.7.0-Master
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages
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 
33 #include <QDir>
34 #include <QDomDocument>
35 #include <QDomNode>
36 #include <QFile>
37 #include <QMessageBox>
38 
39 extern "C"
40 {
41 #include <sqlite3.h>
42 #include <spatialite.h>
43 }
44 
45 // TODO: DEBUG
46 #include <QDebug>
47 // END
48 
49 #define CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE "isOfflineEditable"
50 #define CUSTOM_PROPERTY_REMOTE_SOURCE "remoteSource"
51 #define CUSTOM_PROPERTY_REMOTE_PROVIDER "remoteProvider"
52 #define PROJECT_ENTRY_SCOPE_OFFLINE "OfflineEditingPlugin"
53 #define PROJECT_ENTRY_KEY_OFFLINE_DB_PATH "/OfflineDbPath"
54 
56 {
57  connect( QgsMapLayerRegistry::instance(), SIGNAL( layerWasAdded( QgsMapLayer* ) ), this, SLOT( layerAdded( QgsMapLayer* ) ) );
58 }
59 
61 {
62 }
63 
68 bool QgsOfflineEditing::convertToOfflineProject( const QString& offlineDataPath, const QString& offlineDbFile, const QStringList& layerIds )
69 {
70  if ( layerIds.isEmpty() )
71  {
72  return false;
73  }
74  QString dbPath = QDir( offlineDataPath ).absoluteFilePath( offlineDbFile );
75  if ( createSpatialiteDB( dbPath ) )
76  {
77  spatialite_init( 0 );
78  sqlite3* db;
79  int rc = 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 
159  sqlite3_close( db );
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  // copy style
237  copySymbology( offlineLayer, remoteLayer );
238 
239  // register this layer with the central layers registry
241  QList<QgsMapLayer *>() << remoteLayer, true );
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  spatialite_init( 0 );
384  ret = sqlite3_open_v2( dbPath.toUtf8().constData(), &sqlite_handle, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL );
385  if ( ret )
386  {
387  // an error occurred
388  QString errCause = tr( "Could not create a new database\n" );
389  errCause += QString::fromUtf8( sqlite3_errmsg( sqlite_handle ) );
390  sqlite3_close( sqlite_handle );
391  showWarning( errCause );
392  return false;
393  }
394  // activating Foreign Key constraints
395  ret = sqlite3_exec( sqlite_handle, "PRAGMA foreign_keys = 1", NULL, 0, &errMsg );
396  if ( ret != SQLITE_OK )
397  {
398  showWarning( tr( "Unable to activate FOREIGN_KEY constraints" ) );
399  sqlite3_free( errMsg );
400  sqlite3_close( sqlite_handle );
401  return false;
402  }
403  initializeSpatialMetadata( sqlite_handle );
404 
405  // all done: closing the DB connection
406  sqlite3_close( sqlite_handle );
407 
408  return true;
409 }
410 
411 void QgsOfflineEditing::createLoggingTables( sqlite3* db )
412 {
413  // indices
414  QString sql = "CREATE TABLE 'log_indices' ('name' TEXT, 'last_index' INTEGER)";
415  sqlExec( db, sql );
416 
417  sql = "INSERT INTO 'log_indices' VALUES ('commit_no', 0)";
418  sqlExec( db, sql );
419 
420  sql = "INSERT INTO 'log_indices' VALUES ('layer_id', 0)";
421  sqlExec( db, sql );
422 
423  // layername <-> layer id
424  sql = "CREATE TABLE 'log_layer_ids' ('id' INTEGER, 'qgis_id' TEXT)";
425  sqlExec( db, sql );
426 
427  // offline fid <-> remote fid
428  sql = "CREATE TABLE 'log_fids' ('layer_id' INTEGER, 'offline_fid' INTEGER, 'remote_fid' INTEGER)";
429  sqlExec( db, sql );
430 
431  // added attributes
432  sql = "CREATE TABLE 'log_added_attrs' ('layer_id' INTEGER, 'commit_no' INTEGER, ";
433  sql += "'name' TEXT, 'type' INTEGER, 'length' INTEGER, 'precision' INTEGER, 'comment' TEXT)";
434  sqlExec( db, sql );
435 
436  // added features
437  sql = "CREATE TABLE 'log_added_features' ('layer_id' INTEGER, 'fid' INTEGER)";
438  sqlExec( db, sql );
439 
440  // removed features
441  sql = "CREATE TABLE 'log_removed_features' ('layer_id' INTEGER, 'fid' INTEGER)";
442  sqlExec( db, sql );
443 
444  // feature updates
445  sql = "CREATE TABLE 'log_feature_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'attr' INTEGER, 'value' TEXT)";
446  sqlExec( db, sql );
447 
448  // geometry updates
449  sql = "CREATE TABLE 'log_geometry_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'geom_wkt' TEXT)";
450  sqlExec( db, sql );
451 
452  /* TODO: other logging tables
453  - attr delete (not supported by SpatiaLite provider)
454  */
455 }
456 
457 QgsVectorLayer* QgsOfflineEditing::copyVectorLayer( QgsVectorLayer* layer, sqlite3* db, const QString& offlineDbPath )
458 {
459  if ( layer == 0 )
460  {
461  return 0;
462  }
463 
464  QString tableName = layer->id();
465 
466  // create table
467  QString sql = QString( "CREATE TABLE '%1' (" ).arg( tableName );
468  QString delim = "";
469  const QgsFields& fields = layer->dataProvider()->fields();
470  for ( int idx = 0; idx < fields.count(); ++idx )
471  {
472  QString dataType = "";
473  QVariant::Type type = fields[idx].type();
474  if ( type == QVariant::Int || type == QVariant::LongLong )
475  {
476  dataType = "INTEGER";
477  }
478  else if ( type == QVariant::Double )
479  {
480  dataType = "REAL";
481  }
482  else if ( type == QVariant::String )
483  {
484  dataType = "TEXT";
485  }
486  else
487  {
488  showWarning( tr( "%1: Unknown data type %2. Not using type affinity for the field." ).arg( fields[idx].name() ).arg( QVariant::typeToName( type ) ) );
489  }
490 
491  sql += delim + QString( "'%1' %2" ).arg( fields[idx].name() ).arg( dataType );
492  delim = ",";
493  }
494  sql += ")";
495 
496  int rc = sqlExec( db, sql );
497 
498  // add geometry column
499  if ( layer->hasGeometryType() )
500  {
501  QString geomType = "";
502  switch ( layer->wkbType() )
503  {
504  case QGis::WKBPoint:
505  geomType = "POINT";
506  break;
507  case QGis::WKBMultiPoint:
508  geomType = "MULTIPOINT";
509  break;
510  case QGis::WKBLineString:
511  geomType = "LINESTRING";
512  break;
514  geomType = "MULTILINESTRING";
515  break;
516  case QGis::WKBPolygon:
517  geomType = "POLYGON";
518  break;
520  geomType = "MULTIPOLYGON";
521  break;
522  default:
523  showWarning( tr( "QGIS wkbType %1 not supported" ).arg( layer->wkbType() ) );
524  break;
525  };
526  QString sqlAddGeom = QString( "SELECT AddGeometryColumn('%1', 'Geometry', %2, '%3', 2)" )
527  .arg( tableName )
528  .arg( layer->crs().authid().startsWith( "EPSG:", Qt::CaseInsensitive ) ? layer->crs().authid().mid( 5 ).toLong() : 0 )
529  .arg( geomType );
530 
531  // create spatial index
532  QString sqlCreateIndex = QString( "SELECT CreateSpatialIndex('%1', 'Geometry')" ).arg( tableName );
533 
534  if ( rc == SQLITE_OK )
535  {
536  rc = sqlExec( db, sqlAddGeom );
537  if ( rc == SQLITE_OK )
538  {
539  rc = sqlExec( db, sqlCreateIndex );
540  }
541  }
542  }
543 
544  if ( rc == SQLITE_OK )
545  {
546  // add new layer
547  QgsVectorLayer* newLayer = new QgsVectorLayer( QString( "dbname='%1' table='%2'%3 sql=" )
548  .arg( offlineDbPath )
549  .arg( tableName ).arg( layer->hasGeometryType() ? "(Geometry)" : "" ),
550  layer->name() + " (offline)", "spatialite" );
551  if ( newLayer->isValid() )
552  {
553  // mark as offline layer
555 
556  // store original layer source
559 
560  // copy style
561  bool hasLabels = layer->hasLabelsEnabled();
562  if ( !hasLabels )
563  {
564  // NOTE: copy symbology before adding the layer so it is displayed correctly
565  copySymbology( layer, newLayer );
566  }
567 
568  // register this layer with the central layers registry
570  QList<QgsMapLayer *>() << newLayer );
571 
573  // Find the parent group of the original layer
574  QgsLayerTreeLayer* layerTreeLayer = layerTreeRoot->findLayer( layer->id() );
575  QgsLayerTreeGroup* parentTreeGroup = qobject_cast<QgsLayerTreeGroup*>( layerTreeLayer->parent() );
576  if ( parentTreeGroup )
577  {
578  int index = parentTreeGroup->children().indexOf( layerTreeLayer );
579  // Move the new layer from the root group to the new group
580  QgsLayerTreeLayer* newLayerTreeLayer = layerTreeRoot->findLayer( newLayer->id() );
581  QgsLayerTreeNode* newLayerTreeLayerClone = newLayerTreeLayer->clone();
582  QgsLayerTreeGroup* grp = qobject_cast<QgsLayerTreeGroup*>( newLayerTreeLayer->parent() );
583  parentTreeGroup->insertChildNode( index, newLayerTreeLayerClone );
584  if ( grp )
585  grp->removeChildNode( newLayerTreeLayer );
586  }
587 
588  if ( hasLabels )
589  {
590  // NOTE: copy symbology of layers with labels enabled after adding to project, as it will crash otherwise (WORKAROUND)
591  copySymbology( layer, newLayer );
592  }
593 
594  // copy features
595  newLayer->startEditing();
596  QgsFeature f;
597 
598  // NOTE: force feature recount for PostGIS layer, else only visible features are counted, before iterating over all features (WORKAROUND)
599  layer->setSubsetString( "" );
600 
601  QgsFeatureIterator fit = layer->dataProvider()->getFeatures();
602 
604  int featureCount = 1;
605 
606  QList<QgsFeatureId> remoteFeatureIds;
607  while ( fit.nextFeature( f ) )
608  {
609  remoteFeatureIds << f.id();
610 
611  // NOTE: Spatialite provider ignores position of geometry column
612  // fill gap in QgsAttributeMap if geometry column is not last (WORKAROUND)
613  int column = 0;
614  QgsAttributes attrs = f.attributes();
615  QgsAttributes newAttrs( attrs.count() );
616  for ( int it = 0; it < attrs.count(); ++it )
617  {
618  newAttrs[column++] = attrs[it];
619  }
620  f.setAttributes( newAttrs );
621 
622  newLayer->addFeature( f, false );
623 
624  emit progressUpdated( featureCount++ );
625  }
626  if ( newLayer->commitChanges() )
627  {
629  featureCount = 1;
630 
631  // update feature id lookup
632  int layerId = getOrCreateLayerId( db, newLayer->id() );
633  QList<QgsFeatureId> offlineFeatureIds;
634 
635  QgsFeatureIterator fit = newLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setSubsetOfAttributes( QgsAttributeList() ) );
636  while ( fit.nextFeature( f ) )
637  {
638  offlineFeatureIds << f.id();
639  }
640 
641  // NOTE: insert fids in this loop, as the db is locked during newLayer->nextFeature()
642  sqlExec( db, "BEGIN" );
643  int remoteCount = remoteFeatureIds.size();
644  for ( int i = 0; i < remoteCount; i++ )
645  {
646  addFidLookup( db, layerId, offlineFeatureIds.at( i ), remoteFeatureIds.at( remoteCount - ( i + 1 ) ) );
647  emit progressUpdated( featureCount++ );
648  }
649  sqlExec( db, "COMMIT" );
650  }
651  else
652  {
653  showWarning( newLayer->commitErrors().join( "\n" ) );
654  }
655 
656  // remove remote layer
658  QStringList() << layer->id() );
659  }
660  return newLayer;
661  }
662  return 0;
663 }
664 
665 void QgsOfflineEditing::applyAttributesAdded( QgsVectorLayer* remoteLayer, sqlite3* db, int layerId, int commitNo )
666 {
667  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 );
668  QList<QgsField> fields = sqlQueryAttributesAdded( db, sql );
669 
670  const QgsVectorDataProvider* provider = remoteLayer->dataProvider();
671  QList<QgsVectorDataProvider::NativeType> nativeTypes = provider->nativeTypes();
672 
673  // NOTE: uses last matching QVariant::Type of nativeTypes
674  QMap < QVariant::Type, QString /*typeName*/ > typeNameLookup;
675  for ( int i = 0; i < nativeTypes.size(); i++ )
676  {
677  QgsVectorDataProvider::NativeType nativeType = nativeTypes.at( i );
678  typeNameLookup[ nativeType.mType ] = nativeType.mTypeName;
679  }
680 
681  emit progressModeSet( QgsOfflineEditing::AddFields, fields.size() );
682 
683  for ( int i = 0; i < fields.size(); i++ )
684  {
685  // lookup typename from layer provider
686  QgsField field = fields[i];
687  if ( typeNameLookup.contains( field.type() ) )
688  {
689  QString typeName = typeNameLookup[ field.type()];
690  field.setTypeName( typeName );
691  remoteLayer->addAttribute( field );
692  }
693  else
694  {
695  showWarning( QString( "Could not add attribute '%1' of type %2" ).arg( field.name() ).arg( field.type() ) );
696  }
697 
698  emit progressUpdated( i + 1 );
699  }
700 }
701 
702 void QgsOfflineEditing::applyFeaturesAdded( QgsVectorLayer* offlineLayer, QgsVectorLayer* remoteLayer, sqlite3* db, int layerId )
703 {
704  QString sql = QString( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
705  QList<int> newFeatureIds = sqlQueryInts( db, sql );
706 
707  // get default value for each field
708  const QgsFields& remoteFlds = remoteLayer->pendingFields();
709  QVector<QVariant> defaultValues( remoteFlds.count() );
710  for ( int i = 0; i < remoteFlds.count(); ++i )
711  {
712  if ( remoteFlds.fieldOrigin( i ) == QgsFields::OriginProvider )
713  defaultValues[i] = remoteLayer->dataProvider()->defaultValue( remoteFlds.fieldOriginIndex( i ) );
714  }
715 
716  // get new features from offline layer
717  QgsFeatureList features;
718  for ( int i = 0; i < newFeatureIds.size(); i++ )
719  {
720  QgsFeature feature;
721  if ( offlineLayer->getFeatures( QgsFeatureRequest().setFilterFid( newFeatureIds.at( i ) ) ).nextFeature( feature ) )
722  {
723  features << feature;
724  }
725  }
726 
727  // copy features to remote layer
728  emit progressModeSet( QgsOfflineEditing::AddFeatures, features.size() );
729 
730  int i = 1;
731  int newAttrsCount = remoteLayer->pendingFields().count();
732  for ( QgsFeatureList::iterator it = features.begin(); it != features.end(); ++it )
733  {
734  QgsFeature f = *it;
735 
736  // NOTE: Spatialite provider ignores position of geometry column
737  // restore gap in QgsAttributeMap if geometry column is not last (WORKAROUND)
738  QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
739  QgsAttributes newAttrs( newAttrsCount );
740  QgsAttributes attrs = f.attributes();
741  for ( int it = 0; it < attrs.count(); ++it )
742  {
743  newAttrs[ attrLookup[ it ] ] = attrs[ it ];
744  }
745 
746  // try to use default value from the provider
747  // (important especially e.g. for postgis primary key generated from a sequence)
748  for ( int k = 0; k < newAttrs.count(); ++k )
749  {
750  if ( newAttrs[k].isNull() && !defaultValues[k].isNull() )
751  newAttrs[k] = defaultValues[k];
752  }
753 
754  f.setAttributes( newAttrs );
755 
756  remoteLayer->addFeature( f, false );
757 
758  emit progressUpdated( i++ );
759  }
760 }
761 
762 void QgsOfflineEditing::applyFeaturesRemoved( QgsVectorLayer* remoteLayer, sqlite3* db, int layerId )
763 {
764  QString sql = QString( "SELECT \"fid\" FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( layerId );
765  QgsFeatureIds values = sqlQueryFeaturesRemoved( db, sql );
766 
767  emit progressModeSet( QgsOfflineEditing::RemoveFeatures, values.size() );
768 
769  int i = 1;
770  for ( QgsFeatureIds::const_iterator it = values.begin(); it != values.end(); ++it )
771  {
772  QgsFeatureId fid = remoteFid( db, layerId, *it );
773  remoteLayer->deleteFeature( fid );
774 
775  emit progressUpdated( i++ );
776  }
777 }
778 
779 void QgsOfflineEditing::applyAttributeValueChanges( QgsVectorLayer* offlineLayer, QgsVectorLayer* remoteLayer, sqlite3* db, int layerId, int commitNo )
780 {
781  QString sql = QString( "SELECT \"fid\", \"attr\", \"value\" FROM 'log_feature_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2 " ).arg( layerId ).arg( commitNo );
782  AttributeValueChanges values = sqlQueryAttributeValueChanges( db, sql );
783 
784  emit progressModeSet( QgsOfflineEditing::UpdateFeatures, values.size() );
785 
786  QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
787 
788  for ( int i = 0; i < values.size(); i++ )
789  {
790  QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid );
791 
792  remoteLayer->changeAttributeValue( fid, attrLookup[ values.at( i ).attr ], values.at( i ).value );
793 
794  emit progressUpdated( i + 1 );
795  }
796 }
797 
798 void QgsOfflineEditing::applyGeometryChanges( QgsVectorLayer* remoteLayer, sqlite3* db, int layerId, int commitNo )
799 {
800  QString sql = QString( "SELECT \"fid\", \"geom_wkt\" FROM 'log_geometry_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2" ).arg( layerId ).arg( commitNo );
801  GeometryChanges values = sqlQueryGeometryChanges( db, sql );
802 
804 
805  for ( int i = 0; i < values.size(); i++ )
806  {
807  QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid );
808  remoteLayer->changeGeometry( fid, QgsGeometry::fromWkt( values.at( i ).geom_wkt ) );
809 
810  emit progressUpdated( i + 1 );
811  }
812 }
813 
814 void QgsOfflineEditing::updateFidLookup( QgsVectorLayer* remoteLayer, sqlite3* db, int layerId )
815 {
816  // update fid lookup for added features
817 
818  // get remote added fids
819  // NOTE: use QMap for sorted fids
820  QMap < QgsFeatureId, bool /*dummy*/ > newRemoteFids;
821  QgsFeature f;
822 
823  QgsFeatureIterator fit = remoteLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setSubsetOfAttributes( QgsAttributeList() ) );
824 
826 
827  int i = 1;
828  while ( fit.nextFeature( f ) )
829  {
830  if ( offlineFid( db, layerId, f.id() ) == -1 )
831  {
832  newRemoteFids[ f.id()] = true;
833  }
834 
835  emit progressUpdated( i++ );
836  }
837 
838  // get local added fids
839  // NOTE: fids are sorted
840  QString sql = QString( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
841  QList<int> newOfflineFids = sqlQueryInts( db, sql );
842 
843  if ( newRemoteFids.size() != newOfflineFids.size() )
844  {
845  //showWarning( QString( "Different number of new features on offline layer (%1) and remote layer (%2)" ).arg(newOfflineFids.size()).arg(newRemoteFids.size()) );
846  }
847  else
848  {
849  // add new fid lookups
850  i = 0;
851  sqlExec( db, "BEGIN" );
852  for ( QMap<QgsFeatureId, bool>::const_iterator it = newRemoteFids.begin(); it != newRemoteFids.end(); ++it )
853  {
854  addFidLookup( db, layerId, newOfflineFids.at( i++ ), it.key() );
855  }
856  sqlExec( db, "COMMIT" );
857  }
858 }
859 
860 void QgsOfflineEditing::copySymbology( QgsVectorLayer* sourceLayer, QgsVectorLayer* targetLayer )
861 {
862  QString error;
863  QDomDocument doc;
864  sourceLayer->exportNamedStyle( doc, error );
865 
866  if ( error.isEmpty() )
867  {
868  targetLayer->importNamedStyle( doc, error );
869  }
870  if ( !error.isEmpty() )
871  {
872  showWarning( error );
873  }
874 }
875 
876 // NOTE: use this to map column indices in case the remote geometry column is not last
877 QMap<int, int> QgsOfflineEditing::attributeLookup( QgsVectorLayer* offlineLayer, QgsVectorLayer* remoteLayer )
878 {
879  const QgsAttributeList& offlineAttrs = offlineLayer->pendingAllAttributesList();
880  const QgsAttributeList& remoteAttrs = remoteLayer->pendingAllAttributesList();
881 
882  QMap < int /*offline attr*/, int /*remote attr*/ > attrLookup;
883  // NOTE: use size of remoteAttrs, as offlineAttrs can have new attributes not yet synced
884  for ( int i = 0; i < remoteAttrs.size(); i++ )
885  {
886  attrLookup.insert( offlineAttrs.at( i ), remoteAttrs.at( i ) );
887  }
888 
889  return attrLookup;
890 }
891 
892 void QgsOfflineEditing::showWarning( const QString& message )
893 {
894  emit warning( tr( "Offline Editing Plugin" ), message );
895 }
896 
897 sqlite3* QgsOfflineEditing::openLoggingDb()
898 {
899  sqlite3* db = NULL;
901  if ( !dbPath.isEmpty() )
902  {
903  int rc = sqlite3_open( dbPath.toUtf8().constData(), &db );
904  if ( rc != SQLITE_OK )
905  {
906  showWarning( tr( "Could not open the spatialite logging database" ) );
907  sqlite3_close( db );
908  db = NULL;
909  }
910  }
911  return db;
912 }
913 
914 int QgsOfflineEditing::getOrCreateLayerId( sqlite3* db, const QString& qgisLayerId )
915 {
916  QString sql = QString( "SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( qgisLayerId );
917  int layerId = sqlQueryInt( db, sql, -1 );
918  if ( layerId == -1 )
919  {
920  // next layer id
921  sql = "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'layer_id'";
922  int newLayerId = sqlQueryInt( db, sql, -1 );
923 
924  // insert layer
925  sql = QString( "INSERT INTO 'log_layer_ids' VALUES (%1, '%2')" ).arg( newLayerId ).arg( qgisLayerId );
926  sqlExec( db, sql );
927 
928  // increase layer_id
929  // TODO: use trigger for auto increment?
930  sql = QString( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'layer_id'" ).arg( newLayerId + 1 );
931  sqlExec( db, sql );
932 
933  layerId = newLayerId;
934  }
935 
936  return layerId;
937 }
938 
939 int QgsOfflineEditing::getCommitNo( sqlite3* db )
940 {
941  QString sql = "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'commit_no'";
942  return sqlQueryInt( db, sql, -1 );
943 }
944 
945 void QgsOfflineEditing::increaseCommitNo( sqlite3* db )
946 {
947  QString sql = QString( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'commit_no'" ).arg( getCommitNo( db ) + 1 );
948  sqlExec( db, sql );
949 }
950 
951 void QgsOfflineEditing::addFidLookup( sqlite3* db, int layerId, QgsFeatureId offlineFid, QgsFeatureId remoteFid )
952 {
953  QString sql = QString( "INSERT INTO 'log_fids' VALUES ( %1, %2, %3 )" ).arg( layerId ).arg( offlineFid ).arg( remoteFid );
954  sqlExec( db, sql );
955 }
956 
957 QgsFeatureId QgsOfflineEditing::remoteFid( sqlite3* db, int layerId, QgsFeatureId offlineFid )
958 {
959  QString sql = QString( "SELECT \"remote_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"offline_fid\" = %2" ).arg( layerId ).arg( offlineFid );
960  return sqlQueryInt( db, sql, -1 );
961 }
962 
963 QgsFeatureId QgsOfflineEditing::offlineFid( sqlite3* db, int layerId, QgsFeatureId remoteFid )
964 {
965  QString sql = QString( "SELECT \"offline_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"remote_fid\" = %2" ).arg( layerId ).arg( remoteFid );
966  return sqlQueryInt( db, sql, -1 );
967 }
968 
969 bool QgsOfflineEditing::isAddedFeature( sqlite3* db, int layerId, QgsFeatureId fid )
970 {
971  QString sql = QString( "SELECT COUNT(\"fid\") FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( fid );
972  return ( sqlQueryInt( db, sql, 0 ) > 0 );
973 }
974 
975 int QgsOfflineEditing::sqlExec( sqlite3* db, const QString& sql )
976 {
977  char * errmsg;
978  int rc = sqlite3_exec( db, sql.toUtf8(), NULL, NULL, &errmsg );
979  if ( rc != SQLITE_OK )
980  {
981  showWarning( errmsg );
982  }
983  return rc;
984 }
985 
986 int QgsOfflineEditing::sqlQueryInt( sqlite3* db, const QString& sql, int defaultValue )
987 {
988  sqlite3_stmt* stmt = NULL;
989  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, NULL ) != SQLITE_OK )
990  {
991  showWarning( sqlite3_errmsg( db ) );
992  return defaultValue;
993  }
994 
995  int value = defaultValue;
996  int ret = sqlite3_step( stmt );
997  if ( ret == SQLITE_ROW )
998  {
999  value = sqlite3_column_int( stmt, 0 );
1000  }
1001  sqlite3_finalize( stmt );
1002 
1003  return value;
1004 }
1005 
1006 QList<int> QgsOfflineEditing::sqlQueryInts( sqlite3* db, const QString& sql )
1007 {
1008  QList<int> values;
1009 
1010  sqlite3_stmt* stmt = NULL;
1011  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, NULL ) != SQLITE_OK )
1012  {
1013  showWarning( sqlite3_errmsg( db ) );
1014  return values;
1015  }
1016 
1017  int ret = sqlite3_step( stmt );
1018  while ( ret == SQLITE_ROW )
1019  {
1020  values << sqlite3_column_int( stmt, 0 );
1021 
1022  ret = sqlite3_step( stmt );
1023  }
1024  sqlite3_finalize( stmt );
1025 
1026  return values;
1027 }
1028 
1029 QList<QgsField> QgsOfflineEditing::sqlQueryAttributesAdded( sqlite3* db, const QString& sql )
1030 {
1031  QList<QgsField> values;
1032 
1033  sqlite3_stmt* stmt = NULL;
1034  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, NULL ) != SQLITE_OK )
1035  {
1036  showWarning( sqlite3_errmsg( db ) );
1037  return values;
1038  }
1039 
1040  int ret = sqlite3_step( stmt );
1041  while ( ret == SQLITE_ROW )
1042  {
1043  QgsField field( QString(( const char* )sqlite3_column_text( stmt, 0 ) ),
1044  ( QVariant::Type )sqlite3_column_int( stmt, 1 ),
1045  "", // typeName
1046  sqlite3_column_int( stmt, 2 ),
1047  sqlite3_column_int( stmt, 3 ),
1048  QString(( const char* )sqlite3_column_text( stmt, 4 ) ) );
1049  values << field;
1050 
1051  ret = sqlite3_step( stmt );
1052  }
1053  sqlite3_finalize( stmt );
1054 
1055  return values;
1056 }
1057 
1058 QgsFeatureIds QgsOfflineEditing::sqlQueryFeaturesRemoved( sqlite3* db, const QString& sql )
1059 {
1060  QgsFeatureIds values;
1061 
1062  sqlite3_stmt* stmt = NULL;
1063  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, NULL ) != SQLITE_OK )
1064  {
1065  showWarning( sqlite3_errmsg( db ) );
1066  return values;
1067  }
1068 
1069  int ret = sqlite3_step( stmt );
1070  while ( ret == SQLITE_ROW )
1071  {
1072  values << sqlite3_column_int( stmt, 0 );
1073 
1074  ret = sqlite3_step( stmt );
1075  }
1076  sqlite3_finalize( stmt );
1077 
1078  return values;
1079 }
1080 
1081 QgsOfflineEditing::AttributeValueChanges QgsOfflineEditing::sqlQueryAttributeValueChanges( sqlite3* db, const QString& sql )
1082 {
1083  AttributeValueChanges values;
1084 
1085  sqlite3_stmt* stmt = NULL;
1086  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, NULL ) != SQLITE_OK )
1087  {
1088  showWarning( sqlite3_errmsg( db ) );
1089  return values;
1090  }
1091 
1092  int ret = sqlite3_step( stmt );
1093  while ( ret == SQLITE_ROW )
1094  {
1095  AttributeValueChange change;
1096  change.fid = sqlite3_column_int( stmt, 0 );
1097  change.attr = sqlite3_column_int( stmt, 1 );
1098  change.value = QString(( const char* )sqlite3_column_text( stmt, 2 ) );
1099  values << change;
1100 
1101  ret = sqlite3_step( stmt );
1102  }
1103  sqlite3_finalize( stmt );
1104 
1105  return values;
1106 }
1107 
1108 QgsOfflineEditing::GeometryChanges QgsOfflineEditing::sqlQueryGeometryChanges( sqlite3* db, const QString& sql )
1109 {
1110  GeometryChanges values;
1111 
1112  sqlite3_stmt* stmt = NULL;
1113  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, NULL ) != SQLITE_OK )
1114  {
1115  showWarning( sqlite3_errmsg( db ) );
1116  return values;
1117  }
1118 
1119  int ret = sqlite3_step( stmt );
1120  while ( ret == SQLITE_ROW )
1121  {
1122  GeometryChange change;
1123  change.fid = sqlite3_column_int( stmt, 0 );
1124  change.geom_wkt = QString(( const char* )sqlite3_column_text( stmt, 1 ) );
1125  values << change;
1126 
1127  ret = sqlite3_step( stmt );
1128  }
1129  sqlite3_finalize( stmt );
1130 
1131  return values;
1132 }
1133 
1134 void QgsOfflineEditing::committedAttributesAdded( const QString& qgisLayerId, const QList<QgsField>& addedAttributes )
1135 {
1136  sqlite3* db = openLoggingDb();
1137  if ( db == NULL )
1138  {
1139  return;
1140  }
1141 
1142  // insert log
1143  int layerId = getOrCreateLayerId( db, qgisLayerId );
1144  int commitNo = getCommitNo( db );
1145 
1146  for ( QList<QgsField>::const_iterator it = addedAttributes.begin(); it != addedAttributes.end(); ++it )
1147  {
1148  QgsField field = *it;
1149  QString sql = QString( "INSERT INTO 'log_added_attrs' VALUES ( %1, %2, '%3', %4, %5, %6, '%7' )" )
1150  .arg( layerId )
1151  .arg( commitNo )
1152  .arg( field.name() )
1153  .arg( field.type() )
1154  .arg( field.length() )
1155  .arg( field.precision() )
1156  .arg( field.comment() );
1157  sqlExec( db, sql );
1158  }
1159 
1160  increaseCommitNo( db );
1161  sqlite3_close( db );
1162 }
1163 
1164 void QgsOfflineEditing::committedFeaturesAdded( const QString& qgisLayerId, const QgsFeatureList& addedFeatures )
1165 {
1166  sqlite3* db = openLoggingDb();
1167  if ( db == NULL )
1168  {
1169  return;
1170  }
1171 
1172  // insert log
1173  int layerId = getOrCreateLayerId( db, qgisLayerId );
1174 
1175  // get new feature ids from db
1176  QgsMapLayer* layer = QgsMapLayerRegistry::instance()->mapLayer( qgisLayerId );
1177  QgsDataSourceURI uri = QgsDataSourceURI( layer->source() );
1178 
1179  // only store feature ids
1180  QString sql = QString( "SELECT ROWID FROM '%1' ORDER BY ROWID DESC LIMIT %2" ).arg( uri.table() ).arg( addedFeatures.size() );
1181  QList<int> newFeatureIds = sqlQueryInts( db, sql );
1182  for ( int i = newFeatureIds.size() - 1; i >= 0; i-- )
1183  {
1184  QString sql = QString( "INSERT INTO 'log_added_features' VALUES ( %1, %2 )" )
1185  .arg( layerId )
1186  .arg( newFeatureIds.at( i ) );
1187  sqlExec( db, sql );
1188  }
1189 
1190  sqlite3_close( db );
1191 }
1192 
1193 void QgsOfflineEditing::committedFeaturesRemoved( const QString& qgisLayerId, const QgsFeatureIds& deletedFeatureIds )
1194 {
1195  sqlite3* db = openLoggingDb();
1196  if ( db == NULL )
1197  {
1198  return;
1199  }
1200 
1201  // insert log
1202  int layerId = getOrCreateLayerId( db, qgisLayerId );
1203 
1204  for ( QgsFeatureIds::const_iterator it = deletedFeatureIds.begin(); it != deletedFeatureIds.end(); ++it )
1205  {
1206  if ( isAddedFeature( db, layerId, *it ) )
1207  {
1208  // remove from added features log
1209  QString sql = QString( "DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( *it );
1210  sqlExec( db, sql );
1211  }
1212  else
1213  {
1214  QString sql = QString( "INSERT INTO 'log_removed_features' VALUES ( %1, %2)" )
1215  .arg( layerId )
1216  .arg( *it );
1217  sqlExec( db, sql );
1218  }
1219  }
1220 
1221  sqlite3_close( db );
1222 }
1223 
1224 void QgsOfflineEditing::committedAttributeValuesChanges( const QString& qgisLayerId, const QgsChangedAttributesMap& changedAttrsMap )
1225 {
1226  sqlite3* db = openLoggingDb();
1227  if ( db == NULL )
1228  {
1229  return;
1230  }
1231 
1232  // insert log
1233  int layerId = getOrCreateLayerId( db, qgisLayerId );
1234  int commitNo = getCommitNo( db );
1235 
1236  for ( QgsChangedAttributesMap::const_iterator cit = changedAttrsMap.begin(); cit != changedAttrsMap.end(); ++cit )
1237  {
1238  QgsFeatureId fid = cit.key();
1239  if ( isAddedFeature( db, layerId, fid ) )
1240  {
1241  // skip added features
1242  continue;
1243  }
1244  QgsAttributeMap attrMap = cit.value();
1245  for ( QgsAttributeMap::const_iterator it = attrMap.begin(); it != attrMap.end(); ++it )
1246  {
1247  QString sql = QString( "INSERT INTO 'log_feature_updates' VALUES ( %1, %2, %3, %4, '%5' )" )
1248  .arg( layerId )
1249  .arg( commitNo )
1250  .arg( fid )
1251  .arg( it.key() ) // attr
1252  .arg( it.value().toString() ); // value
1253  sqlExec( db, sql );
1254  }
1255  }
1256 
1257  increaseCommitNo( db );
1258  sqlite3_close( db );
1259 }
1260 
1261 void QgsOfflineEditing::committedGeometriesChanges( const QString& qgisLayerId, const QgsGeometryMap& changedGeometries )
1262 {
1263  sqlite3* db = openLoggingDb();
1264  if ( db == NULL )
1265  {
1266  return;
1267  }
1268 
1269  // insert log
1270  int layerId = getOrCreateLayerId( db, qgisLayerId );
1271  int commitNo = getCommitNo( db );
1272 
1273  for ( QgsGeometryMap::const_iterator it = changedGeometries.begin(); it != changedGeometries.end(); ++it )
1274  {
1275  QgsFeatureId fid = it.key();
1276  if ( isAddedFeature( db, layerId, fid ) )
1277  {
1278  // skip added features
1279  continue;
1280  }
1281  QgsGeometry geom = it.value();
1282  QString sql = QString( "INSERT INTO 'log_geometry_updates' VALUES ( %1, %2, %3, '%4' )" )
1283  .arg( layerId )
1284  .arg( commitNo )
1285  .arg( fid )
1286  .arg( geom.exportToWkt() );
1287  sqlExec( db, sql );
1288 
1289  // TODO: use WKB instead of WKT?
1290  }
1291 
1292  increaseCommitNo( db );
1293  sqlite3_close( db );
1294 }
1295 
1296 void QgsOfflineEditing::startListenFeatureChanges()
1297 {
1298  QgsVectorLayer* vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1299  // enable logging
1300  connect( vLayer->editBuffer(), SIGNAL( committedAttributesAdded( const QString&, const QList<QgsField>& ) ),
1301  this, SLOT( committedAttributesAdded( const QString&, const QList<QgsField>& ) ) );
1302  connect( vLayer, SIGNAL( committedFeaturesAdded( const QString&, const QgsFeatureList& ) ),
1303  this, SLOT( committedFeaturesAdded( const QString&, const QgsFeatureList& ) ) );
1304  connect( vLayer, SIGNAL( committedFeaturesRemoved( const QString&, const QgsFeatureIds& ) ),
1305  this, SLOT( committedFeaturesRemoved( const QString&, const QgsFeatureIds& ) ) );
1306  connect( vLayer->editBuffer(), SIGNAL( committedAttributeValuesChanges( const QString&, const QgsChangedAttributesMap& ) ),
1307  this, SLOT( committedAttributeValuesChanges( const QString&, const QgsChangedAttributesMap& ) ) );
1308  connect( vLayer->editBuffer(), SIGNAL( committedGeometriesChanges( const QString&, const QgsGeometryMap& ) ),
1309  this, SLOT( committedGeometriesChanges( const QString&, const QgsGeometryMap& ) ) );
1310 }
1311 
1312 void QgsOfflineEditing::stopListenFeatureChanges()
1313 {
1314  QgsVectorLayer* vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1315  // disable logging
1316  disconnect( vLayer->editBuffer(), SIGNAL( committedAttributesAdded( const QString&, const QList<QgsField>& ) ),
1317  this, SLOT( committedAttributesAdded( const QString&, const QList<QgsField>& ) ) );
1318  disconnect( vLayer, SIGNAL( committedFeaturesAdded( const QString&, const QgsFeatureList& ) ),
1319  this, SLOT( committedFeaturesAdded( const QString&, const QgsFeatureList& ) ) );
1320  disconnect( vLayer, SIGNAL( committedFeaturesRemoved( const QString&, const QgsFeatureIds& ) ),
1321  this, SLOT( committedFeaturesRemoved( const QString&, const QgsFeatureIds& ) ) );
1322  disconnect( vLayer->editBuffer(), SIGNAL( committedAttributeValuesChanges( const QString&, const QgsChangedAttributesMap& ) ),
1323  this, SLOT( committedAttributeValuesChanges( const QString&, const QgsChangedAttributesMap& ) ) );
1324  disconnect( vLayer->editBuffer(), SIGNAL( committedGeometriesChanges( const QString&, const QgsGeometryMap& ) ),
1325  this, SLOT( committedGeometriesChanges( const QString&, const QgsGeometryMap& ) ) );
1326 }
1327 
1328 void QgsOfflineEditing::layerAdded( QgsMapLayer* layer )
1329 {
1330  // detect offline layer
1331  if ( layer->customProperty( CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE, false ).toBool() )
1332  {
1333  QgsVectorLayer* vLayer = qobject_cast<QgsVectorLayer *>( layer );
1334  connect( vLayer, SIGNAL( editingStarted() ), this, SLOT( startListenFeatureChanges() ) );
1335  connect( vLayer, SIGNAL( editingStopped() ), this, SLOT( stopListenFeatureChanges() ) );
1336  }
1337 }
1338 
1339 
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:59
const QList< QgsVectorJoinInfo > & vectorJoins() const
Wrapper for iterator of features from vector data provider or vector layer.
QMap< QgsFeatureId, QgsGeometry > QgsGeometryMap
Definition: qgsfeature.h:315
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
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:99
bool deleteFeature(QgsFeatureId fid)
delete a feature from the layer (but does not commit it)
QSet< QgsFeatureId > QgsFeatureIds
Definition: qgsfeature.h:317
QList< QgsFeature > QgsFeatureList
Definition: qgsfeature.h:322
#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:79
Container of fields for a vector layer.
Definition: qgsfield.h:172
void setAttributes(const QgsAttributes &attrs)
Definition: qgsfeature.h:144
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.
The feature class encapsulates a single feature including its id, geometry and a list of field/values...
Definition: qgsfeature.h:113
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.
field comes from the underlying data provider of the vector layer (originIndex = index in provider's ...
Definition: qgsfield.h:179
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.
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'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:142
This class is a base class for nodes in a layer tree.
QString id() const
Get this layer's unique ID, this ID is used to access this layer from map layer registry.
Definition: qgsmaplayer.cpp:98
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'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:312
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:371
QVector< QVariant > QgsAttributes
Definition: qgsfeature.h:100
void synchronize()
synchronize to remote layers
FieldOrigin fieldOrigin(int fieldIdx) const
Get field's origin (value from an enumeration)
Definition: qgsfield.h:234
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:362
QString table() const
int length() const
Gets the length of the field.
Definition: qgsfield.cpp:74
bool hasLabelsEnabled() const
Label is on.
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:84
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'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's layer tree.
Geometry is not required. It may still be returned if e.g. required for a filter condition.
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:64
#define tr(sourceText)