QGIS API Documentation  2.8.2-Wien
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Groups 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  // 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  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  // register this layer with the central layers registry
562  QList<QgsMapLayer *>() << newLayer );
563 
564  // copy style
565  bool hasLabels = layer->hasLabelsEnabled();
566  if ( !hasLabels )
567  {
568  // NOTE: copy symbology before adding the layer so it is displayed correctly
569  copySymbology( layer, newLayer );
570  }
571 
573  // Find the parent group of the original layer
574  QgsLayerTreeLayer* layerTreeLayer = layerTreeRoot->findLayer( layer->id() );
575  if ( layerTreeLayer )
576  {
577  QgsLayerTreeGroup* parentTreeGroup = qobject_cast<QgsLayerTreeGroup*>( layerTreeLayer->parent() );
578  if ( parentTreeGroup )
579  {
580  int index = parentTreeGroup->children().indexOf( layerTreeLayer );
581  // Move the new layer from the root group to the new group
582  QgsLayerTreeLayer* newLayerTreeLayer = layerTreeRoot->findLayer( newLayer->id() );
583  if ( newLayerTreeLayer )
584  {
585  QgsLayerTreeNode* newLayerTreeLayerClone = newLayerTreeLayer->clone();
586  QgsLayerTreeGroup* grp = qobject_cast<QgsLayerTreeGroup*>( newLayerTreeLayer->parent() );
587  parentTreeGroup->insertChildNode( index, newLayerTreeLayerClone );
588  if ( grp )
589  grp->removeChildNode( newLayerTreeLayer );
590  }
591  }
592  }
593 
594  if ( hasLabels )
595  {
596  // NOTE: copy symbology of layers with labels enabled after adding to project, as it will crash otherwise (WORKAROUND)
597  copySymbology( layer, newLayer );
598  }
599 
600  // copy features
601  newLayer->startEditing();
602  QgsFeature f;
603 
604  // NOTE: force feature recount for PostGIS layer, else only visible features are counted, before iterating over all features (WORKAROUND)
605  layer->setSubsetString( "" );
606 
607  QgsFeatureIterator fit = layer->dataProvider()->getFeatures();
608 
610  int featureCount = 1;
611 
612  QList<QgsFeatureId> remoteFeatureIds;
613  while ( fit.nextFeature( f ) )
614  {
615  remoteFeatureIds << f.id();
616 
617  // NOTE: Spatialite provider ignores position of geometry column
618  // fill gap in QgsAttributeMap if geometry column is not last (WORKAROUND)
619  int column = 0;
620  QgsAttributes attrs = f.attributes();
621  QgsAttributes newAttrs( attrs.count() );
622  for ( int it = 0; it < attrs.count(); ++it )
623  {
624  newAttrs[column++] = attrs[it];
625  }
626  f.setAttributes( newAttrs );
627 
628  newLayer->addFeature( f, false );
629 
630  emit progressUpdated( featureCount++ );
631  }
632  if ( newLayer->commitChanges() )
633  {
635  featureCount = 1;
636 
637  // update feature id lookup
638  int layerId = getOrCreateLayerId( db, newLayer->id() );
639  QList<QgsFeatureId> offlineFeatureIds;
640 
641  QgsFeatureIterator fit = newLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setSubsetOfAttributes( QgsAttributeList() ) );
642  while ( fit.nextFeature( f ) )
643  {
644  offlineFeatureIds << f.id();
645  }
646 
647  // NOTE: insert fids in this loop, as the db is locked during newLayer->nextFeature()
648  sqlExec( db, "BEGIN" );
649  int remoteCount = remoteFeatureIds.size();
650  for ( int i = 0; i < remoteCount; i++ )
651  {
652  addFidLookup( db, layerId, offlineFeatureIds.at( i ), remoteFeatureIds.at( remoteCount - ( i + 1 ) ) );
653  emit progressUpdated( featureCount++ );
654  }
655  sqlExec( db, "COMMIT" );
656  }
657  else
658  {
659  showWarning( newLayer->commitErrors().join( "\n" ) );
660  }
661 
662  // remove remote layer
664  QStringList() << layer->id() );
665  }
666  return newLayer;
667  }
668  return 0;
669 }
670 
671 void QgsOfflineEditing::applyAttributesAdded( QgsVectorLayer* remoteLayer, sqlite3* db, int layerId, int commitNo )
672 {
673  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 );
674  QList<QgsField> fields = sqlQueryAttributesAdded( db, sql );
675 
676  const QgsVectorDataProvider* provider = remoteLayer->dataProvider();
677  QList<QgsVectorDataProvider::NativeType> nativeTypes = provider->nativeTypes();
678 
679  // NOTE: uses last matching QVariant::Type of nativeTypes
680  QMap < QVariant::Type, QString /*typeName*/ > typeNameLookup;
681  for ( int i = 0; i < nativeTypes.size(); i++ )
682  {
683  QgsVectorDataProvider::NativeType nativeType = nativeTypes.at( i );
684  typeNameLookup[ nativeType.mType ] = nativeType.mTypeName;
685  }
686 
687  emit progressModeSet( QgsOfflineEditing::AddFields, fields.size() );
688 
689  for ( int i = 0; i < fields.size(); i++ )
690  {
691  // lookup typename from layer provider
692  QgsField field = fields[i];
693  if ( typeNameLookup.contains( field.type() ) )
694  {
695  QString typeName = typeNameLookup[ field.type()];
696  field.setTypeName( typeName );
697  remoteLayer->addAttribute( field );
698  }
699  else
700  {
701  showWarning( QString( "Could not add attribute '%1' of type %2" ).arg( field.name() ).arg( field.type() ) );
702  }
703 
704  emit progressUpdated( i + 1 );
705  }
706 }
707 
708 void QgsOfflineEditing::applyFeaturesAdded( QgsVectorLayer* offlineLayer, QgsVectorLayer* remoteLayer, sqlite3* db, int layerId )
709 {
710  QString sql = QString( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
711  QList<int> newFeatureIds = sqlQueryInts( db, sql );
712 
713  // get default value for each field
714  const QgsFields& remoteFlds = remoteLayer->pendingFields();
715  QVector<QVariant> defaultValues( remoteFlds.count() );
716  for ( int i = 0; i < remoteFlds.count(); ++i )
717  {
718  if ( remoteFlds.fieldOrigin( i ) == QgsFields::OriginProvider )
719  defaultValues[i] = remoteLayer->dataProvider()->defaultValue( remoteFlds.fieldOriginIndex( i ) );
720  }
721 
722  // get new features from offline layer
723  QgsFeatureList features;
724  for ( int i = 0; i < newFeatureIds.size(); i++ )
725  {
726  QgsFeature feature;
727  if ( offlineLayer->getFeatures( QgsFeatureRequest().setFilterFid( newFeatureIds.at( i ) ) ).nextFeature( feature ) )
728  {
729  features << feature;
730  }
731  }
732 
733  // copy features to remote layer
734  emit progressModeSet( QgsOfflineEditing::AddFeatures, features.size() );
735 
736  int i = 1;
737  int newAttrsCount = remoteLayer->pendingFields().count();
738  for ( QgsFeatureList::iterator it = features.begin(); it != features.end(); ++it )
739  {
740  QgsFeature f = *it;
741 
742  // NOTE: Spatialite provider ignores position of geometry column
743  // restore gap in QgsAttributeMap if geometry column is not last (WORKAROUND)
744  QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
745  QgsAttributes newAttrs( newAttrsCount );
746  QgsAttributes attrs = f.attributes();
747  for ( int it = 0; it < attrs.count(); ++it )
748  {
749  newAttrs[ attrLookup[ it ] ] = attrs[ it ];
750  }
751 
752  // try to use default value from the provider
753  // (important especially e.g. for postgis primary key generated from a sequence)
754  for ( int k = 0; k < newAttrs.count(); ++k )
755  {
756  if ( newAttrs[k].isNull() && !defaultValues[k].isNull() )
757  newAttrs[k] = defaultValues[k];
758  }
759 
760  f.setAttributes( newAttrs );
761 
762  remoteLayer->addFeature( f, false );
763 
764  emit progressUpdated( i++ );
765  }
766 }
767 
768 void QgsOfflineEditing::applyFeaturesRemoved( QgsVectorLayer* remoteLayer, sqlite3* db, int layerId )
769 {
770  QString sql = QString( "SELECT \"fid\" FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( layerId );
771  QgsFeatureIds values = sqlQueryFeaturesRemoved( db, sql );
772 
773  emit progressModeSet( QgsOfflineEditing::RemoveFeatures, values.size() );
774 
775  int i = 1;
776  for ( QgsFeatureIds::const_iterator it = values.begin(); it != values.end(); ++it )
777  {
778  QgsFeatureId fid = remoteFid( db, layerId, *it );
779  remoteLayer->deleteFeature( fid );
780 
781  emit progressUpdated( i++ );
782  }
783 }
784 
785 void QgsOfflineEditing::applyAttributeValueChanges( QgsVectorLayer* offlineLayer, QgsVectorLayer* remoteLayer, sqlite3* db, int layerId, int commitNo )
786 {
787  QString sql = QString( "SELECT \"fid\", \"attr\", \"value\" FROM 'log_feature_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2 " ).arg( layerId ).arg( commitNo );
788  AttributeValueChanges values = sqlQueryAttributeValueChanges( db, sql );
789 
790  emit progressModeSet( QgsOfflineEditing::UpdateFeatures, values.size() );
791 
792  QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
793 
794  for ( int i = 0; i < values.size(); i++ )
795  {
796  QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid );
797 
798  remoteLayer->changeAttributeValue( fid, attrLookup[ values.at( i ).attr ], values.at( i ).value );
799 
800  emit progressUpdated( i + 1 );
801  }
802 }
803 
804 void QgsOfflineEditing::applyGeometryChanges( QgsVectorLayer* remoteLayer, sqlite3* db, int layerId, int commitNo )
805 {
806  QString sql = QString( "SELECT \"fid\", \"geom_wkt\" FROM 'log_geometry_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2" ).arg( layerId ).arg( commitNo );
807  GeometryChanges values = sqlQueryGeometryChanges( db, sql );
808 
810 
811  for ( int i = 0; i < values.size(); i++ )
812  {
813  QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid );
814  remoteLayer->changeGeometry( fid, QgsGeometry::fromWkt( values.at( i ).geom_wkt ) );
815 
816  emit progressUpdated( i + 1 );
817  }
818 }
819 
820 void QgsOfflineEditing::updateFidLookup( QgsVectorLayer* remoteLayer, sqlite3* db, int layerId )
821 {
822  // update fid lookup for added features
823 
824  // get remote added fids
825  // NOTE: use QMap for sorted fids
826  QMap < QgsFeatureId, bool /*dummy*/ > newRemoteFids;
827  QgsFeature f;
828 
829  QgsFeatureIterator fit = remoteLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setSubsetOfAttributes( QgsAttributeList() ) );
830 
832 
833  int i = 1;
834  while ( fit.nextFeature( f ) )
835  {
836  if ( offlineFid( db, layerId, f.id() ) == -1 )
837  {
838  newRemoteFids[ f.id()] = true;
839  }
840 
841  emit progressUpdated( i++ );
842  }
843 
844  // get local added fids
845  // NOTE: fids are sorted
846  QString sql = QString( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
847  QList<int> newOfflineFids = sqlQueryInts( db, sql );
848 
849  if ( newRemoteFids.size() != newOfflineFids.size() )
850  {
851  //showWarning( QString( "Different number of new features on offline layer (%1) and remote layer (%2)" ).arg(newOfflineFids.size()).arg(newRemoteFids.size()) );
852  }
853  else
854  {
855  // add new fid lookups
856  i = 0;
857  sqlExec( db, "BEGIN" );
858  for ( QMap<QgsFeatureId, bool>::const_iterator it = newRemoteFids.begin(); it != newRemoteFids.end(); ++it )
859  {
860  addFidLookup( db, layerId, newOfflineFids.at( i++ ), it.key() );
861  }
862  sqlExec( db, "COMMIT" );
863  }
864 }
865 
866 void QgsOfflineEditing::copySymbology( QgsVectorLayer* sourceLayer, QgsVectorLayer* targetLayer )
867 {
868  QString error;
869  QDomDocument doc;
870  sourceLayer->exportNamedStyle( doc, error );
871 
872  if ( error.isEmpty() )
873  {
874  targetLayer->importNamedStyle( doc, error );
875  }
876  if ( !error.isEmpty() )
877  {
878  showWarning( error );
879  }
880 }
881 
882 // NOTE: use this to map column indices in case the remote geometry column is not last
883 QMap<int, int> QgsOfflineEditing::attributeLookup( QgsVectorLayer* offlineLayer, QgsVectorLayer* remoteLayer )
884 {
885  const QgsAttributeList& offlineAttrs = offlineLayer->pendingAllAttributesList();
886  const QgsAttributeList& remoteAttrs = remoteLayer->pendingAllAttributesList();
887 
888  QMap < int /*offline attr*/, int /*remote attr*/ > attrLookup;
889  // NOTE: use size of remoteAttrs, as offlineAttrs can have new attributes not yet synced
890  for ( int i = 0; i < remoteAttrs.size(); i++ )
891  {
892  attrLookup.insert( offlineAttrs.at( i ), remoteAttrs.at( i ) );
893  }
894 
895  return attrLookup;
896 }
897 
898 void QgsOfflineEditing::showWarning( const QString& message )
899 {
900  emit warning( tr( "Offline Editing Plugin" ), message );
901 }
902 
903 sqlite3* QgsOfflineEditing::openLoggingDb()
904 {
905  sqlite3* db = NULL;
907  if ( !dbPath.isEmpty() )
908  {
909  int rc = sqlite3_open( dbPath.toUtf8().constData(), &db );
910  if ( rc != SQLITE_OK )
911  {
912  showWarning( tr( "Could not open the spatialite logging database" ) );
913  sqlite3_close( db );
914  db = NULL;
915  }
916  }
917  return db;
918 }
919 
920 int QgsOfflineEditing::getOrCreateLayerId( sqlite3* db, const QString& qgisLayerId )
921 {
922  QString sql = QString( "SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( qgisLayerId );
923  int layerId = sqlQueryInt( db, sql, -1 );
924  if ( layerId == -1 )
925  {
926  // next layer id
927  sql = "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'layer_id'";
928  int newLayerId = sqlQueryInt( db, sql, -1 );
929 
930  // insert layer
931  sql = QString( "INSERT INTO 'log_layer_ids' VALUES (%1, '%2')" ).arg( newLayerId ).arg( qgisLayerId );
932  sqlExec( db, sql );
933 
934  // increase layer_id
935  // TODO: use trigger for auto increment?
936  sql = QString( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'layer_id'" ).arg( newLayerId + 1 );
937  sqlExec( db, sql );
938 
939  layerId = newLayerId;
940  }
941 
942  return layerId;
943 }
944 
945 int QgsOfflineEditing::getCommitNo( sqlite3* db )
946 {
947  QString sql = "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'commit_no'";
948  return sqlQueryInt( db, sql, -1 );
949 }
950 
951 void QgsOfflineEditing::increaseCommitNo( sqlite3* db )
952 {
953  QString sql = QString( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'commit_no'" ).arg( getCommitNo( db ) + 1 );
954  sqlExec( db, sql );
955 }
956 
957 void QgsOfflineEditing::addFidLookup( sqlite3* db, int layerId, QgsFeatureId offlineFid, QgsFeatureId remoteFid )
958 {
959  QString sql = QString( "INSERT INTO 'log_fids' VALUES ( %1, %2, %3 )" ).arg( layerId ).arg( offlineFid ).arg( remoteFid );
960  sqlExec( db, sql );
961 }
962 
963 QgsFeatureId QgsOfflineEditing::remoteFid( sqlite3* db, int layerId, QgsFeatureId offlineFid )
964 {
965  QString sql = QString( "SELECT \"remote_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"offline_fid\" = %2" ).arg( layerId ).arg( offlineFid );
966  return sqlQueryInt( db, sql, -1 );
967 }
968 
969 QgsFeatureId QgsOfflineEditing::offlineFid( sqlite3* db, int layerId, QgsFeatureId remoteFid )
970 {
971  QString sql = QString( "SELECT \"offline_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"remote_fid\" = %2" ).arg( layerId ).arg( remoteFid );
972  return sqlQueryInt( db, sql, -1 );
973 }
974 
975 bool QgsOfflineEditing::isAddedFeature( sqlite3* db, int layerId, QgsFeatureId fid )
976 {
977  QString sql = QString( "SELECT COUNT(\"fid\") FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( fid );
978  return ( sqlQueryInt( db, sql, 0 ) > 0 );
979 }
980 
981 int QgsOfflineEditing::sqlExec( sqlite3* db, const QString& sql )
982 {
983  char * errmsg;
984  int rc = sqlite3_exec( db, sql.toUtf8(), NULL, NULL, &errmsg );
985  if ( rc != SQLITE_OK )
986  {
987  showWarning( errmsg );
988  }
989  return rc;
990 }
991 
992 int QgsOfflineEditing::sqlQueryInt( sqlite3* db, const QString& sql, int defaultValue )
993 {
994  sqlite3_stmt* stmt = NULL;
995  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, NULL ) != SQLITE_OK )
996  {
997  showWarning( sqlite3_errmsg( db ) );
998  return defaultValue;
999  }
1000 
1001  int value = defaultValue;
1002  int ret = sqlite3_step( stmt );
1003  if ( ret == SQLITE_ROW )
1004  {
1005  value = sqlite3_column_int( stmt, 0 );
1006  }
1007  sqlite3_finalize( stmt );
1008 
1009  return value;
1010 }
1011 
1012 QList<int> QgsOfflineEditing::sqlQueryInts( sqlite3* db, const QString& sql )
1013 {
1014  QList<int> values;
1015 
1016  sqlite3_stmt* stmt = NULL;
1017  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, NULL ) != SQLITE_OK )
1018  {
1019  showWarning( sqlite3_errmsg( db ) );
1020  return values;
1021  }
1022 
1023  int ret = sqlite3_step( stmt );
1024  while ( ret == SQLITE_ROW )
1025  {
1026  values << sqlite3_column_int( stmt, 0 );
1027 
1028  ret = sqlite3_step( stmt );
1029  }
1030  sqlite3_finalize( stmt );
1031 
1032  return values;
1033 }
1034 
1035 QList<QgsField> QgsOfflineEditing::sqlQueryAttributesAdded( sqlite3* db, const QString& sql )
1036 {
1037  QList<QgsField> values;
1038 
1039  sqlite3_stmt* stmt = NULL;
1040  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, NULL ) != SQLITE_OK )
1041  {
1042  showWarning( sqlite3_errmsg( db ) );
1043  return values;
1044  }
1045 
1046  int ret = sqlite3_step( stmt );
1047  while ( ret == SQLITE_ROW )
1048  {
1049  QgsField field( QString(( const char* )sqlite3_column_text( stmt, 0 ) ),
1050  ( QVariant::Type )sqlite3_column_int( stmt, 1 ),
1051  "", // typeName
1052  sqlite3_column_int( stmt, 2 ),
1053  sqlite3_column_int( stmt, 3 ),
1054  QString(( const char* )sqlite3_column_text( stmt, 4 ) ) );
1055  values << field;
1056 
1057  ret = sqlite3_step( stmt );
1058  }
1059  sqlite3_finalize( stmt );
1060 
1061  return values;
1062 }
1063 
1064 QgsFeatureIds QgsOfflineEditing::sqlQueryFeaturesRemoved( sqlite3* db, const QString& sql )
1065 {
1066  QgsFeatureIds values;
1067 
1068  sqlite3_stmt* stmt = NULL;
1069  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, NULL ) != SQLITE_OK )
1070  {
1071  showWarning( sqlite3_errmsg( db ) );
1072  return values;
1073  }
1074 
1075  int ret = sqlite3_step( stmt );
1076  while ( ret == SQLITE_ROW )
1077  {
1078  values << sqlite3_column_int( stmt, 0 );
1079 
1080  ret = sqlite3_step( stmt );
1081  }
1082  sqlite3_finalize( stmt );
1083 
1084  return values;
1085 }
1086 
1087 QgsOfflineEditing::AttributeValueChanges QgsOfflineEditing::sqlQueryAttributeValueChanges( sqlite3* db, const QString& sql )
1088 {
1089  AttributeValueChanges values;
1090 
1091  sqlite3_stmt* stmt = NULL;
1092  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, NULL ) != SQLITE_OK )
1093  {
1094  showWarning( sqlite3_errmsg( db ) );
1095  return values;
1096  }
1097 
1098  int ret = sqlite3_step( stmt );
1099  while ( ret == SQLITE_ROW )
1100  {
1101  AttributeValueChange change;
1102  change.fid = sqlite3_column_int( stmt, 0 );
1103  change.attr = sqlite3_column_int( stmt, 1 );
1104  change.value = QString(( const char* )sqlite3_column_text( stmt, 2 ) );
1105  values << change;
1106 
1107  ret = sqlite3_step( stmt );
1108  }
1109  sqlite3_finalize( stmt );
1110 
1111  return values;
1112 }
1113 
1114 QgsOfflineEditing::GeometryChanges QgsOfflineEditing::sqlQueryGeometryChanges( sqlite3* db, const QString& sql )
1115 {
1116  GeometryChanges values;
1117 
1118  sqlite3_stmt* stmt = NULL;
1119  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, NULL ) != SQLITE_OK )
1120  {
1121  showWarning( sqlite3_errmsg( db ) );
1122  return values;
1123  }
1124 
1125  int ret = sqlite3_step( stmt );
1126  while ( ret == SQLITE_ROW )
1127  {
1128  GeometryChange change;
1129  change.fid = sqlite3_column_int( stmt, 0 );
1130  change.geom_wkt = QString(( const char* )sqlite3_column_text( stmt, 1 ) );
1131  values << change;
1132 
1133  ret = sqlite3_step( stmt );
1134  }
1135  sqlite3_finalize( stmt );
1136 
1137  return values;
1138 }
1139 
1140 void QgsOfflineEditing::committedAttributesAdded( const QString& qgisLayerId, const QList<QgsField>& addedAttributes )
1141 {
1142  sqlite3* db = openLoggingDb();
1143  if ( db == NULL )
1144  {
1145  return;
1146  }
1147 
1148  // insert log
1149  int layerId = getOrCreateLayerId( db, qgisLayerId );
1150  int commitNo = getCommitNo( db );
1151 
1152  for ( QList<QgsField>::const_iterator it = addedAttributes.begin(); it != addedAttributes.end(); ++it )
1153  {
1154  QgsField field = *it;
1155  QString sql = QString( "INSERT INTO 'log_added_attrs' VALUES ( %1, %2, '%3', %4, %5, %6, '%7' )" )
1156  .arg( layerId )
1157  .arg( commitNo )
1158  .arg( field.name() )
1159  .arg( field.type() )
1160  .arg( field.length() )
1161  .arg( field.precision() )
1162  .arg( field.comment() );
1163  sqlExec( db, sql );
1164  }
1165 
1166  increaseCommitNo( db );
1167  sqlite3_close( db );
1168 }
1169 
1170 void QgsOfflineEditing::committedFeaturesAdded( const QString& qgisLayerId, const QgsFeatureList& addedFeatures )
1171 {
1172  sqlite3* db = openLoggingDb();
1173  if ( db == NULL )
1174  {
1175  return;
1176  }
1177 
1178  // insert log
1179  int layerId = getOrCreateLayerId( db, qgisLayerId );
1180 
1181  // get new feature ids from db
1182  QgsMapLayer* layer = QgsMapLayerRegistry::instance()->mapLayer( qgisLayerId );
1183  QgsDataSourceURI uri = QgsDataSourceURI( layer->source() );
1184 
1185  // only store feature ids
1186  QString sql = QString( "SELECT ROWID FROM '%1' ORDER BY ROWID DESC LIMIT %2" ).arg( uri.table() ).arg( addedFeatures.size() );
1187  QList<int> newFeatureIds = sqlQueryInts( db, sql );
1188  for ( int i = newFeatureIds.size() - 1; i >= 0; i-- )
1189  {
1190  QString sql = QString( "INSERT INTO 'log_added_features' VALUES ( %1, %2 )" )
1191  .arg( layerId )
1192  .arg( newFeatureIds.at( i ) );
1193  sqlExec( db, sql );
1194  }
1195 
1196  sqlite3_close( db );
1197 }
1198 
1199 void QgsOfflineEditing::committedFeaturesRemoved( const QString& qgisLayerId, const QgsFeatureIds& deletedFeatureIds )
1200 {
1201  sqlite3* db = openLoggingDb();
1202  if ( db == NULL )
1203  {
1204  return;
1205  }
1206 
1207  // insert log
1208  int layerId = getOrCreateLayerId( db, qgisLayerId );
1209 
1210  for ( QgsFeatureIds::const_iterator it = deletedFeatureIds.begin(); it != deletedFeatureIds.end(); ++it )
1211  {
1212  if ( isAddedFeature( db, layerId, *it ) )
1213  {
1214  // remove from added features log
1215  QString sql = QString( "DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( *it );
1216  sqlExec( db, sql );
1217  }
1218  else
1219  {
1220  QString sql = QString( "INSERT INTO 'log_removed_features' VALUES ( %1, %2)" )
1221  .arg( layerId )
1222  .arg( *it );
1223  sqlExec( db, sql );
1224  }
1225  }
1226 
1227  sqlite3_close( db );
1228 }
1229 
1230 void QgsOfflineEditing::committedAttributeValuesChanges( const QString& qgisLayerId, const QgsChangedAttributesMap& changedAttrsMap )
1231 {
1232  sqlite3* db = openLoggingDb();
1233  if ( db == NULL )
1234  {
1235  return;
1236  }
1237 
1238  // insert log
1239  int layerId = getOrCreateLayerId( db, qgisLayerId );
1240  int commitNo = getCommitNo( db );
1241 
1242  for ( QgsChangedAttributesMap::const_iterator cit = changedAttrsMap.begin(); cit != changedAttrsMap.end(); ++cit )
1243  {
1244  QgsFeatureId fid = cit.key();
1245  if ( isAddedFeature( db, layerId, fid ) )
1246  {
1247  // skip added features
1248  continue;
1249  }
1250  QgsAttributeMap attrMap = cit.value();
1251  for ( QgsAttributeMap::const_iterator it = attrMap.begin(); it != attrMap.end(); ++it )
1252  {
1253  QString sql = QString( "INSERT INTO 'log_feature_updates' VALUES ( %1, %2, %3, %4, '%5' )" )
1254  .arg( layerId )
1255  .arg( commitNo )
1256  .arg( fid )
1257  .arg( it.key() ) // attr
1258  .arg( it.value().toString() ); // value
1259  sqlExec( db, sql );
1260  }
1261  }
1262 
1263  increaseCommitNo( db );
1264  sqlite3_close( db );
1265 }
1266 
1267 void QgsOfflineEditing::committedGeometriesChanges( const QString& qgisLayerId, const QgsGeometryMap& changedGeometries )
1268 {
1269  sqlite3* db = openLoggingDb();
1270  if ( db == NULL )
1271  {
1272  return;
1273  }
1274 
1275  // insert log
1276  int layerId = getOrCreateLayerId( db, qgisLayerId );
1277  int commitNo = getCommitNo( db );
1278 
1279  for ( QgsGeometryMap::const_iterator it = changedGeometries.begin(); it != changedGeometries.end(); ++it )
1280  {
1281  QgsFeatureId fid = it.key();
1282  if ( isAddedFeature( db, layerId, fid ) )
1283  {
1284  // skip added features
1285  continue;
1286  }
1287  QgsGeometry geom = it.value();
1288  QString sql = QString( "INSERT INTO 'log_geometry_updates' VALUES ( %1, %2, %3, '%4' )" )
1289  .arg( layerId )
1290  .arg( commitNo )
1291  .arg( fid )
1292  .arg( geom.exportToWkt() );
1293  sqlExec( db, sql );
1294 
1295  // TODO: use WKB instead of WKT?
1296  }
1297 
1298  increaseCommitNo( db );
1299  sqlite3_close( db );
1300 }
1301 
1302 void QgsOfflineEditing::startListenFeatureChanges()
1303 {
1304  QgsVectorLayer* vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1305  // enable logging
1306  connect( vLayer->editBuffer(), SIGNAL( committedAttributesAdded( const QString&, const QList<QgsField>& ) ),
1307  this, SLOT( committedAttributesAdded( const QString&, const QList<QgsField>& ) ) );
1308  connect( vLayer, SIGNAL( committedFeaturesAdded( const QString&, const QgsFeatureList& ) ),
1309  this, SLOT( committedFeaturesAdded( const QString&, const QgsFeatureList& ) ) );
1310  connect( vLayer, SIGNAL( committedFeaturesRemoved( const QString&, const QgsFeatureIds& ) ),
1311  this, SLOT( committedFeaturesRemoved( const QString&, const QgsFeatureIds& ) ) );
1312  connect( vLayer->editBuffer(), SIGNAL( committedAttributeValuesChanges( const QString&, const QgsChangedAttributesMap& ) ),
1313  this, SLOT( committedAttributeValuesChanges( const QString&, const QgsChangedAttributesMap& ) ) );
1314  connect( vLayer->editBuffer(), SIGNAL( committedGeometriesChanges( const QString&, const QgsGeometryMap& ) ),
1315  this, SLOT( committedGeometriesChanges( const QString&, const QgsGeometryMap& ) ) );
1316 }
1317 
1318 void QgsOfflineEditing::stopListenFeatureChanges()
1319 {
1320  QgsVectorLayer* vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1321  // disable logging
1322  disconnect( vLayer->editBuffer(), SIGNAL( committedAttributesAdded( const QString&, const QList<QgsField>& ) ),
1323  this, SLOT( committedAttributesAdded( const QString&, const QList<QgsField>& ) ) );
1324  disconnect( vLayer, SIGNAL( committedFeaturesAdded( const QString&, const QgsFeatureList& ) ),
1325  this, SLOT( committedFeaturesAdded( const QString&, const QgsFeatureList& ) ) );
1326  disconnect( vLayer, SIGNAL( committedFeaturesRemoved( const QString&, const QgsFeatureIds& ) ),
1327  this, SLOT( committedFeaturesRemoved( const QString&, const QgsFeatureIds& ) ) );
1328  disconnect( vLayer->editBuffer(), SIGNAL( committedAttributeValuesChanges( const QString&, const QgsChangedAttributesMap& ) ),
1329  this, SLOT( committedAttributeValuesChanges( const QString&, const QgsChangedAttributesMap& ) ) );
1330  disconnect( vLayer->editBuffer(), SIGNAL( committedGeometriesChanges( const QString&, const QgsGeometryMap& ) ),
1331  this, SLOT( committedGeometriesChanges( const QString&, const QgsGeometryMap& ) ) );
1332 }
1333 
1334 void QgsOfflineEditing::layerAdded( QgsMapLayer* layer )
1335 {
1336  // detect offline layer
1337  if ( layer->customProperty( CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE, false ).toBool() )
1338  {
1339  QgsVectorLayer* vLayer = qobject_cast<QgsVectorLayer *>( layer );
1340  connect( vLayer, SIGNAL( editingStarted() ), this, SLOT( startListenFeatureChanges() ) );
1341  connect( vLayer, SIGNAL( editingStopped() ), this, SLOT( stopListenFeatureChanges() ) );
1342  }
1343 }
1344 
1345