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