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