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