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