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