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