QGIS API Documentation  3.20.0-Odense (decaadbb31)
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 "qgsspatialiteutils.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 #include "qgsogrutils.h"
39 #include "qgsvectorfilewriter.h"
40 #include "qgsvectorlayer.h"
41 #include "qgsproviderregistry.h"
42 #include "qgsprovidermetadata.h"
44 #include "qgsjsonutils.h"
45 
46 #include <QDir>
47 #include <QDomDocument>
48 #include <QDomNode>
49 #include <QFile>
50 #include <QRegularExpression>
51 
52 #include <ogr_srs_api.h>
53 
54 extern "C"
55 {
56 #include <sqlite3.h>
57 }
58 
59 #ifdef HAVE_SPATIALITE
60 extern "C"
61 {
62 #include <spatialite.h>
63 }
64 #endif
65 
66 #define CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE "isOfflineEditable"
67 #define CUSTOM_PROPERTY_REMOTE_SOURCE "remoteSource"
68 #define CUSTOM_PROPERTY_REMOTE_PROVIDER "remoteProvider"
69 #define CUSTOM_SHOW_FEATURE_COUNT "showFeatureCount"
70 #define CUSTOM_PROPERTY_ORIGINAL_LAYERID "remoteLayerId"
71 #define CUSTOM_PROPERTY_LAYERNAME_SUFFIX "layerNameSuffix"
72 #define PROJECT_ENTRY_SCOPE_OFFLINE "OfflineEditingPlugin"
73 #define PROJECT_ENTRY_KEY_OFFLINE_DB_PATH "/OfflineDbPath"
74 
76 {
77  connect( QgsProject::instance(), &QgsProject::layerWasAdded, this, &QgsOfflineEditing::layerAdded );
78 }
79 
96 bool QgsOfflineEditing::convertToOfflineProject( const QString &offlineDataPath, const QString &offlineDbFile, const QStringList &layerIds, bool onlySelected, ContainerType containerType, const QString &layerNameSuffix )
97 {
98  if ( layerIds.isEmpty() )
99  {
100  return false;
101  }
102 
103  QString dbPath = QDir( offlineDataPath ).absoluteFilePath( offlineDbFile );
104  if ( createOfflineDb( dbPath, containerType ) )
105  {
107  int rc = database.open( dbPath );
108  if ( rc != SQLITE_OK )
109  {
110  showWarning( tr( "Could not open the SpatiaLite database" ) );
111  }
112  else
113  {
114  // create logging tables
115  createLoggingTables( database.get() );
116 
117  emit progressStarted();
118 
119  QMap<QString, QgsVectorJoinList > joinInfoBuffer;
120  QMap<QString, QgsVectorLayer *> layerIdMapping;
121 
122  for ( const QString &layerId : layerIds )
123  {
124  QgsMapLayer *layer = QgsProject::instance()->mapLayer( layerId );
125  QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( layer );
126  if ( !vl || !vl->isValid() )
127  {
128  QgsDebugMsgLevel( QStringLiteral( "Layer %1 is invalid" ).arg( layerId ), 4 );
129  continue;
130  }
131  QgsVectorJoinList joins = vl->vectorJoins();
132 
133  // Layer names will be appended an _offline suffix
134  // Join fields are prefixed with the layer name and we do not want the
135  // field name to change so we stabilize the field name by defining a
136  // custom prefix with the layername without _offline suffix.
137  QgsVectorJoinList::iterator joinIt = joins.begin();
138  while ( joinIt != joins.end() )
139  {
140  if ( joinIt->prefix().isNull() )
141  {
142  QgsVectorLayer *vl = joinIt->joinLayer();
143 
144  if ( vl && vl->isValid() )
145  joinIt->setPrefix( vl->name() + '_' );
146  }
147  ++joinIt;
148  }
149  joinInfoBuffer.insert( vl->id(), joins );
150  }
151 
153 
154  // copy selected vector layers to offline layer
155  for ( int i = 0; i < layerIds.count(); i++ )
156  {
157  emit layerProgressUpdated( i + 1, layerIds.count() );
158 
159  QgsMapLayer *layer = QgsProject::instance()->mapLayer( layerIds.at( i ) );
160  QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( layer );
161  if ( vl && vl->isValid() )
162  {
163  QString origLayerId = vl->id();
164  QgsVectorLayer *newLayer = copyVectorLayer( vl, database.get(), dbPath, onlySelected, containerType, layerNameSuffix );
165  if ( newLayer && newLayer->isValid() )
166  {
167  layerIdMapping.insert( origLayerId, newLayer );
168  //append individual layer setting on snapping settings
169  snappingConfig.setIndividualLayerSettings( newLayer, snappingConfig.individualLayerSettings( vl ) );
170  snappingConfig.removeLayers( QList<QgsMapLayer *>() << vl );
171 
172  // remove remote layer
174  QStringList() << origLayerId );
175  }
176  }
177  }
178 
179  QgsProject::instance()->setSnappingConfig( snappingConfig );
180 
181  // restore join info on new offline layer
182  QMap<QString, QgsVectorJoinList >::ConstIterator it;
183  for ( it = joinInfoBuffer.constBegin(); it != joinInfoBuffer.constEnd(); ++it )
184  {
185  QgsVectorLayer *newLayer = layerIdMapping.value( it.key() );
186 
187  if ( newLayer && newLayer->isValid() )
188  {
189  const QList<QgsVectorLayerJoinInfo> joins = it.value();
190  for ( QgsVectorLayerJoinInfo join : joins )
191  {
192  QgsVectorLayer *newJoinedLayer = layerIdMapping.value( join.joinLayerId() );
193  if ( newJoinedLayer && newJoinedLayer->isValid() )
194  {
195  // If the layer has been offline'd, update join information
196  join.setJoinLayer( newJoinedLayer );
197  }
198  newLayer->addJoin( join );
199  }
200  }
201  }
202 
203  emit progressStopped();
204 
205  // save offline project
206  QString projectTitle = QgsProject::instance()->title();
207  if ( projectTitle.isEmpty() )
208  {
209  projectTitle = QFileInfo( QgsProject::instance()->fileName() ).fileName();
210  }
211  projectTitle += QLatin1String( " (offline)" );
212  QgsProject::instance()->setTitle( projectTitle );
213 
215 
216  return true;
217  }
218  }
219 
220  return false;
221 }
222 
224 {
226 }
227 
229 {
230  // open logging db
231  sqlite3_database_unique_ptr database = openLoggingDb();
232  if ( !database )
233  {
234  return;
235  }
236 
237  emit progressStarted();
238 
240 
241  // restore and sync remote layers
242  QList<QgsMapLayer *> offlineLayers;
243  QMap<QString, QgsMapLayer *> mapLayers = QgsProject::instance()->mapLayers();
244  for ( QMap<QString, QgsMapLayer *>::iterator layer_it = mapLayers.begin() ; layer_it != mapLayers.end(); ++layer_it )
245  {
246  QgsMapLayer *layer = layer_it.value();
247 
248  if ( layer->customProperty( CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE, false ).toBool() )
249  {
250  if ( !layer->isValid() )
251  {
252  QgsDebugMsgLevel( QStringLiteral( "Skipping offline layer %1 because it is an invalid layer" ).arg( layer->id() ), 4 );
253  continue;
254  }
255 
256  offlineLayers << layer;
257  }
258  }
259 
260  QgsDebugMsgLevel( QStringLiteral( "Found %1 offline layers" ).arg( offlineLayers.count() ), 4 );
261  for ( int l = 0; l < offlineLayers.count(); l++ )
262  {
263  QgsMapLayer *layer = offlineLayers.at( l );
264 
265  emit layerProgressUpdated( l + 1, offlineLayers.count() );
266 
267  QString remoteSource = layer->customProperty( CUSTOM_PROPERTY_REMOTE_SOURCE, "" ).toString();
268  QString remoteProvider = layer->customProperty( CUSTOM_PROPERTY_REMOTE_PROVIDER, "" ).toString();
269  QString remoteName = layer->name();
270  QString remoteNameSuffix = layer->customProperty( CUSTOM_PROPERTY_LAYERNAME_SUFFIX, " (offline)" ).toString();
271  if ( remoteName.endsWith( remoteNameSuffix ) )
272  remoteName.chop( remoteNameSuffix.size() );
274  QgsVectorLayer *remoteLayer = new QgsVectorLayer( remoteSource, remoteName, remoteProvider, options );
275  if ( remoteLayer->isValid() )
276  {
277  // Rebuild WFS cache to get feature id<->GML fid mapping
278  if ( remoteLayer->providerType().contains( QLatin1String( "WFS" ), Qt::CaseInsensitive ) )
279  {
280  QgsFeatureIterator fit = remoteLayer->getFeatures();
281  QgsFeature f;
282  while ( fit.nextFeature( f ) )
283  {
284  }
285  }
286  // TODO: only add remote layer if there are log entries?
287 
288  QgsVectorLayer *offlineLayer = qobject_cast<QgsVectorLayer *>( layer );
289 
290  if ( offlineLayer->isValid() )
291  {
292  // register this layer with the central layers registry
293  QgsProject::instance()->addMapLayers( QList<QgsMapLayer *>() << remoteLayer, true );
294 
295  // copy style
296  copySymbology( offlineLayer, remoteLayer );
297  updateRelations( offlineLayer, remoteLayer );
298  updateMapThemes( offlineLayer, remoteLayer );
299  updateLayerOrder( offlineLayer, remoteLayer );
300 
301  //append individual layer setting on snapping settings
302  snappingConfig.setIndividualLayerSettings( remoteLayer, snappingConfig.individualLayerSettings( offlineLayer ) );
303  snappingConfig.removeLayers( QList<QgsMapLayer *>() << offlineLayer );
304 
305  //set QgsLayerTreeNode properties back
306  QgsLayerTreeLayer *layerTreeLayer = QgsProject::instance()->layerTreeRoot()->findLayer( offlineLayer->id() );
307  QgsLayerTreeLayer *newLayerTreeLayer = QgsProject::instance()->layerTreeRoot()->findLayer( remoteLayer->id() );
309 
310  // apply layer edit log
311  QString qgisLayerId = layer->id();
312  QString sql = QStringLiteral( "SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( qgisLayerId );
313  int layerId = sqlQueryInt( database.get(), sql, -1 );
314  if ( layerId != -1 )
315  {
316  remoteLayer->startEditing();
317 
318  // TODO: only get commitNos of this layer?
319  int commitNo = getCommitNo( database.get() );
320  QgsDebugMsgLevel( QStringLiteral( "Found %1 commits" ).arg( commitNo ), 4 );
321  for ( int i = 0; i < commitNo; i++ )
322  {
323  QgsDebugMsgLevel( QStringLiteral( "Apply commits chronologically" ), 4 );
324  // apply commits chronologically
325  applyAttributesAdded( remoteLayer, database.get(), layerId, i );
326  applyAttributeValueChanges( offlineLayer, remoteLayer, database.get(), layerId, i );
327  applyGeometryChanges( remoteLayer, database.get(), layerId, i );
328  }
329 
330  applyFeaturesAdded( offlineLayer, remoteLayer, database.get(), layerId );
331  applyFeaturesRemoved( remoteLayer, database.get(), layerId );
332 
333  if ( remoteLayer->commitChanges() )
334  {
335  // update fid lookup
336  updateFidLookup( remoteLayer, database.get(), layerId );
337 
338  // clear edit log for this layer
339  sql = QStringLiteral( "DELETE FROM 'log_added_attrs' WHERE \"layer_id\" = %1" ).arg( layerId );
340  sqlExec( database.get(), sql );
341  sql = QStringLiteral( "DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
342  sqlExec( database.get(), sql );
343  sql = QStringLiteral( "DELETE FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( layerId );
344  sqlExec( database.get(), sql );
345  sql = QStringLiteral( "DELETE FROM 'log_feature_updates' WHERE \"layer_id\" = %1" ).arg( layerId );
346  sqlExec( database.get(), sql );
347  sql = QStringLiteral( "DELETE FROM 'log_geometry_updates' WHERE \"layer_id\" = %1" ).arg( layerId );
348  sqlExec( database.get(), sql );
349  }
350  else
351  {
352  showWarning( remoteLayer->commitErrors().join( QLatin1Char( '\n' ) ) );
353  }
354  }
355  else
356  {
357  QgsDebugMsg( QStringLiteral( "Could not find the layer id in the edit logs!" ) );
358  }
359  // Invalidate the connection to force a reload if the project is put offline
360  // again with the same path
361  offlineLayer->dataProvider()->invalidateConnections( QgsDataSourceUri( offlineLayer->source() ).database() );
362  // remove offline layer
363  QgsProject::instance()->removeMapLayers( QStringList() << qgisLayerId );
364 
365 
366  // disable offline project
367  QString projectTitle = QgsProject::instance()->title();
368  projectTitle.remove( QRegExp( " \\(offline\\)$" ) );
369  QgsProject::instance()->setTitle( projectTitle );
371  remoteLayer->reload(); //update with other changes
372  }
373  else
374  {
375  QgsDebugMsg( QStringLiteral( "Offline layer %1 is not valid!" ).arg( offlineLayer->id() ) );
376  }
377  }
378  else
379  {
380  QgsDebugMsg( QStringLiteral( "Remote layer %1 is not valid!" ).arg( remoteLayer->id() ) );
381  }
382  }
383 
384  // reset commitNo
385  QString sql = QStringLiteral( "UPDATE 'log_indices' SET 'last_index' = 0 WHERE \"name\" = 'commit_no'" );
386  sqlExec( database.get(), sql );
387 
388  QgsProject::instance()->setSnappingConfig( snappingConfig );
389 
390  emit progressStopped();
391 }
392 
393 void QgsOfflineEditing::initializeSpatialMetadata( sqlite3 *sqlite_handle )
394 {
395 #ifdef HAVE_SPATIALITE
396  // attempting to perform self-initialization for a newly created DB
397  if ( !sqlite_handle )
398  return;
399  // checking if this DB is really empty
400  char **results = nullptr;
401  int rows, columns;
402  int ret = sqlite3_get_table( sqlite_handle, "select count(*) from sqlite_master", &results, &rows, &columns, nullptr );
403  if ( ret != SQLITE_OK )
404  return;
405  int count = 0;
406  if ( rows >= 1 )
407  {
408  for ( int i = 1; i <= rows; i++ )
409  count = atoi( results[( i * columns ) + 0] );
410  }
411 
412  sqlite3_free_table( results );
413 
414  if ( count > 0 )
415  return;
416 
417  bool above41 = false;
418  ret = sqlite3_get_table( sqlite_handle, "select spatialite_version()", &results, &rows, &columns, nullptr );
419  if ( ret == SQLITE_OK && rows == 1 && columns == 1 )
420  {
421  QString version = QString::fromUtf8( results[1] );
422 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
423  QStringList parts = version.split( ' ', QString::SkipEmptyParts );
424 #else
425  QStringList parts = version.split( ' ', Qt::SkipEmptyParts );
426 #endif
427  if ( !parts.empty() )
428  {
429 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
430  QStringList verparts = parts.at( 0 ).split( '.', QString::SkipEmptyParts );
431 #else
432  QStringList verparts = parts.at( 0 ).split( '.', Qt::SkipEmptyParts );
433 #endif
434  above41 = verparts.size() >= 2 && ( verparts.at( 0 ).toInt() > 4 || ( verparts.at( 0 ).toInt() == 4 && verparts.at( 1 ).toInt() >= 1 ) );
435  }
436  }
437 
438  sqlite3_free_table( results );
439 
440  // all right, it's empty: proceeding to initialize
441  char *errMsg = nullptr;
442  ret = sqlite3_exec( sqlite_handle, above41 ? "SELECT InitSpatialMetadata(1)" : "SELECT InitSpatialMetadata()", nullptr, nullptr, &errMsg );
443 
444  if ( ret != SQLITE_OK )
445  {
446  QString errCause = tr( "Unable to initialize SpatialMetadata:\n" );
447  errCause += QString::fromUtf8( errMsg );
448  showWarning( errCause );
449  sqlite3_free( errMsg );
450  return;
451  }
452  spatial_ref_sys_init( sqlite_handle, 0 );
453 #else
454  ( void )sqlite_handle;
455 #endif
456 }
457 
458 bool QgsOfflineEditing::createOfflineDb( const QString &offlineDbPath, ContainerType containerType )
459 {
460  int ret;
461  char *errMsg = nullptr;
462  QFile newDb( offlineDbPath );
463  if ( newDb.exists() )
464  {
465  QFile::remove( offlineDbPath );
466  }
467 
468  // see also QgsNewSpatialiteLayerDialog::createDb()
469 
470  QFileInfo fullPath = QFileInfo( offlineDbPath );
471  QDir path = fullPath.dir();
472 
473  // Must be sure there is destination directory ~/.qgis
474  QDir().mkpath( path.absolutePath() );
475 
476  // creating/opening the new database
477  QString dbPath = newDb.fileName();
478 
479  // creating geopackage
480  switch ( containerType )
481  {
482  case GPKG:
483  {
484  OGRSFDriverH hGpkgDriver = OGRGetDriverByName( "GPKG" );
485  if ( !hGpkgDriver )
486  {
487  showWarning( tr( "Creation of database failed. GeoPackage driver not found." ) );
488  return false;
489  }
490 
491  gdal::ogr_datasource_unique_ptr hDS( OGR_Dr_CreateDataSource( hGpkgDriver, dbPath.toUtf8().constData(), nullptr ) );
492  if ( !hDS )
493  {
494  showWarning( tr( "Creation of database failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
495  return false;
496  }
497  break;
498  }
499  case SpatiaLite:
500  {
501  break;
502  }
503  }
504 
506  ret = database.open_v2( dbPath, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr );
507  if ( ret )
508  {
509  // an error occurred
510  QString errCause = tr( "Could not create a new database\n" );
511  errCause += database.errorMessage();
512  showWarning( errCause );
513  return false;
514  }
515  // activating Foreign Key constraints
516  ret = sqlite3_exec( database.get(), "PRAGMA foreign_keys = 1", nullptr, nullptr, &errMsg );
517  if ( ret != SQLITE_OK )
518  {
519  showWarning( tr( "Unable to activate FOREIGN_KEY constraints" ) );
520  sqlite3_free( errMsg );
521  return false;
522  }
523  initializeSpatialMetadata( database.get() );
524  return true;
525 }
526 
527 void QgsOfflineEditing::createLoggingTables( sqlite3 *db )
528 {
529  // indices
530  QString sql = QStringLiteral( "CREATE TABLE 'log_indices' ('name' TEXT, 'last_index' INTEGER)" );
531  sqlExec( db, sql );
532 
533  sql = QStringLiteral( "INSERT INTO 'log_indices' VALUES ('commit_no', 0)" );
534  sqlExec( db, sql );
535 
536  sql = QStringLiteral( "INSERT INTO 'log_indices' VALUES ('layer_id', 0)" );
537  sqlExec( db, sql );
538 
539  // layername <-> layer id
540  sql = QStringLiteral( "CREATE TABLE 'log_layer_ids' ('id' INTEGER, 'qgis_id' TEXT)" );
541  sqlExec( db, sql );
542 
543  // offline fid <-> remote fid
544  sql = QStringLiteral( "CREATE TABLE 'log_fids' ('layer_id' INTEGER, 'offline_fid' INTEGER, 'remote_fid' INTEGER)" );
545  sqlExec( db, sql );
546 
547  // added attributes
548  sql = QStringLiteral( "CREATE TABLE 'log_added_attrs' ('layer_id' INTEGER, 'commit_no' INTEGER, " );
549  sql += QLatin1String( "'name' TEXT, 'type' INTEGER, 'length' INTEGER, 'precision' INTEGER, 'comment' TEXT)" );
550  sqlExec( db, sql );
551 
552  // added features
553  sql = QStringLiteral( "CREATE TABLE 'log_added_features' ('layer_id' INTEGER, 'fid' INTEGER)" );
554  sqlExec( db, sql );
555 
556  // removed features
557  sql = QStringLiteral( "CREATE TABLE 'log_removed_features' ('layer_id' INTEGER, 'fid' INTEGER)" );
558  sqlExec( db, sql );
559 
560  // feature updates
561  sql = QStringLiteral( "CREATE TABLE 'log_feature_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'attr' INTEGER, 'value' TEXT)" );
562  sqlExec( db, sql );
563 
564  // geometry updates
565  sql = QStringLiteral( "CREATE TABLE 'log_geometry_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'geom_wkt' TEXT)" );
566  sqlExec( db, sql );
567 
568  /* TODO: other logging tables
569  - attr delete (not supported by SpatiaLite provider)
570  */
571 }
572 
573 QgsVectorLayer *QgsOfflineEditing::copyVectorLayer( QgsVectorLayer *layer, sqlite3 *db, const QString &offlineDbPath, bool onlySelected, ContainerType containerType, const QString &layerNameSuffix )
574 {
575  if ( !layer || !layer->isValid() )
576  {
577  QgsDebugMsgLevel( QStringLiteral( "Layer %1 is invalid and cannot be copied" ).arg( layer ? layer->id() : QStringLiteral( "<UNKNOWN>" ) ), 4 );
578  return nullptr;
579  }
580 
581  QString tableName = layer->id();
582  QgsDebugMsgLevel( QStringLiteral( "Creating offline table %1 ..." ).arg( tableName ), 4 );
583 
584  // new layer
585  QgsVectorLayer *newLayer = nullptr;
586 
587  switch ( containerType )
588  {
589  case SpatiaLite:
590  {
591 #ifdef HAVE_SPATIALITE
592  // create table
593  QString sql = QStringLiteral( "CREATE TABLE '%1' (" ).arg( tableName );
594  QString delim;
595  const QgsFields providerFields = layer->dataProvider()->fields();
596  for ( const auto &field : providerFields )
597  {
598  QString dataType;
599  QVariant::Type type = field.type();
600  if ( type == QVariant::Int || type == QVariant::LongLong )
601  {
602  dataType = QStringLiteral( "INTEGER" );
603  }
604  else if ( type == QVariant::Double )
605  {
606  dataType = QStringLiteral( "REAL" );
607  }
608  else if ( type == QVariant::String )
609  {
610  dataType = QStringLiteral( "TEXT" );
611  }
612  else if ( type == QVariant::StringList || type == QVariant::List )
613  {
614  dataType = QStringLiteral( "TEXT" );
615  showWarning( tr( "Field '%1' from layer %2 has been converted from a list to a string of comma-separated values." ).arg( field.name(), layer->name() ) );
616  }
617  else
618  {
619  showWarning( tr( "%1: Unknown data type %2. Not using type affinity for the field." ).arg( field.name(), QVariant::typeToName( type ) ) );
620  }
621 
622  sql += delim + QStringLiteral( "'%1' %2" ).arg( field.name(), dataType );
623  delim = ',';
624  }
625  sql += ')';
626 
627  int rc = sqlExec( db, sql );
628 
629  // add geometry column
630  if ( layer->isSpatial() )
631  {
632  const QgsWkbTypes::Type sourceWkbType = layer->wkbType();
633 
634  QString geomType;
635  switch ( QgsWkbTypes::flatType( sourceWkbType ) )
636  {
637  case QgsWkbTypes::Point:
638  geomType = QStringLiteral( "POINT" );
639  break;
641  geomType = QStringLiteral( "MULTIPOINT" );
642  break;
644  geomType = QStringLiteral( "LINESTRING" );
645  break;
647  geomType = QStringLiteral( "MULTILINESTRING" );
648  break;
650  geomType = QStringLiteral( "POLYGON" );
651  break;
653  geomType = QStringLiteral( "MULTIPOLYGON" );
654  break;
655  default:
656  showWarning( tr( "Layer %1 has unsupported geometry type %2." ).arg( layer->name(), QgsWkbTypes::displayString( layer->wkbType() ) ) );
657  break;
658  };
659 
660  QString zmInfo = QStringLiteral( "XY" );
661 
662  if ( QgsWkbTypes::hasZ( sourceWkbType ) )
663  zmInfo += 'Z';
664  if ( QgsWkbTypes::hasM( sourceWkbType ) )
665  zmInfo += 'M';
666 
667  QString epsgCode;
668 
669  if ( layer->crs().authid().startsWith( QLatin1String( "EPSG:" ), Qt::CaseInsensitive ) )
670  {
671  epsgCode = layer->crs().authid().mid( 5 );
672  }
673  else
674  {
675  epsgCode = '0';
676  showWarning( tr( "Layer %1 has unsupported Coordinate Reference System (%2)." ).arg( layer->name(), layer->crs().authid() ) );
677  }
678 
679  QString sqlAddGeom = QStringLiteral( "SELECT AddGeometryColumn('%1', 'Geometry', %2, '%3', '%4')" )
680  .arg( tableName, epsgCode, geomType, zmInfo );
681 
682  // create spatial index
683  QString sqlCreateIndex = QStringLiteral( "SELECT CreateSpatialIndex('%1', 'Geometry')" ).arg( tableName );
684 
685  if ( rc == SQLITE_OK )
686  {
687  rc = sqlExec( db, sqlAddGeom );
688  if ( rc == SQLITE_OK )
689  {
690  rc = sqlExec( db, sqlCreateIndex );
691  }
692  }
693  }
694 
695  if ( rc != SQLITE_OK )
696  {
697  showWarning( tr( "Filling SpatiaLite for layer %1 failed" ).arg( layer->name() ) );
698  return nullptr;
699  }
700 
701  // add new layer
702  QString connectionString = QStringLiteral( "dbname='%1' table='%2'%3 sql=" )
703  .arg( offlineDbPath,
704  tableName, layer->isSpatial() ? "(Geometry)" : "" );
706  newLayer = new QgsVectorLayer( connectionString,
707  layer->name() + layerNameSuffix, QStringLiteral( "spatialite" ), options );
708  break;
709 
710 #else
711  showWarning( tr( "No Spatialite support available" ) );
712  return nullptr;
713 #endif
714  }
715 
716  case GPKG:
717  {
718  // Set options
719  char **options = nullptr;
720 
721  options = CSLSetNameValue( options, "OVERWRITE", "YES" );
722  options = CSLSetNameValue( options, "IDENTIFIER", tr( "%1 (offline)" ).arg( layer->id() ).toUtf8().constData() );
723  options = CSLSetNameValue( options, "DESCRIPTION", layer->dataComment().toUtf8().constData() );
724 
725  //the FID-name should not exist in the original data
726  QString fidBase( QStringLiteral( "fid" ) );
727  QString fid = fidBase;
728  int counter = 1;
729  while ( layer->dataProvider()->fields().lookupField( fid ) >= 0 && counter < 10000 )
730  {
731  fid = fidBase + '_' + QString::number( counter );
732  counter++;
733  }
734  if ( counter == 10000 )
735  {
736  showWarning( tr( "Cannot make FID-name for GPKG " ) );
737  return nullptr;
738  }
739 
740  options = CSLSetNameValue( options, "FID", fid.toUtf8().constData() );
741 
742  if ( layer->isSpatial() )
743  {
744  options = CSLSetNameValue( options, "GEOMETRY_COLUMN", "geom" );
745  options = CSLSetNameValue( options, "SPATIAL_INDEX", "YES" );
746  }
747 
748  OGRSFDriverH hDriver = nullptr;
749  OGRSpatialReferenceH hSRS = OSRNewSpatialReference( layer->crs().toWkt( QgsCoordinateReferenceSystem::WKT_PREFERRED_GDAL ).toLocal8Bit().data() );
750  gdal::ogr_datasource_unique_ptr hDS( OGROpen( offlineDbPath.toUtf8().constData(), true, &hDriver ) );
751  OGRLayerH hLayer = OGR_DS_CreateLayer( hDS.get(), tableName.toUtf8().constData(), hSRS, static_cast<OGRwkbGeometryType>( layer->wkbType() ), options );
752  CSLDestroy( options );
753  if ( hSRS )
754  OSRRelease( hSRS );
755  if ( !hLayer )
756  {
757  showWarning( tr( "Creation of layer failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
758  return nullptr;
759  }
760 
761  const QgsFields providerFields = layer->dataProvider()->fields();
762  for ( const auto &field : providerFields )
763  {
764  const QString fieldName( field.name() );
765  const QVariant::Type type = field.type();
766  OGRFieldType ogrType( OFTString );
767  OGRFieldSubType ogrSubType = OFSTNone;
768  if ( type == QVariant::Int )
769  ogrType = OFTInteger;
770  else if ( type == QVariant::LongLong )
771  ogrType = OFTInteger64;
772  else if ( type == QVariant::Double )
773  ogrType = OFTReal;
774  else if ( type == QVariant::Time )
775  ogrType = OFTTime;
776  else if ( type == QVariant::Date )
777  ogrType = OFTDate;
778  else if ( type == QVariant::DateTime )
779  ogrType = OFTDateTime;
780  else if ( type == QVariant::Bool )
781  {
782  ogrType = OFTInteger;
783  ogrSubType = OFSTBoolean;
784  }
785  else if ( type == QVariant::StringList || type == QVariant::List )
786  {
787  ogrType = OFTString;
788  ogrSubType = OFSTJSON;
789  showWarning( tr( "Field '%1' from layer %2 has been converted from a list to a JSON-formatted string value." ).arg( fieldName, layer->name() ) );
790  }
791  else
792  ogrType = OFTString;
793 
794  int ogrWidth = field.length();
795 
796  gdal::ogr_field_def_unique_ptr fld( OGR_Fld_Create( fieldName.toUtf8().constData(), ogrType ) );
797  OGR_Fld_SetWidth( fld.get(), ogrWidth );
798  if ( ogrSubType != OFSTNone )
799  OGR_Fld_SetSubType( fld.get(), ogrSubType );
800 
801  if ( OGR_L_CreateField( hLayer, fld.get(), true ) != OGRERR_NONE )
802  {
803  showWarning( tr( "Creation of field %1 failed (OGR error: %2)" )
804  .arg( fieldName, QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
805  return nullptr;
806  }
807  }
808 
809  // In GDAL >= 2.0, the driver implements a deferred creation strategy, so
810  // issue a command that will force table creation
811  CPLErrorReset();
812  OGR_L_ResetReading( hLayer );
813  if ( CPLGetLastErrorType() != CE_None )
814  {
815  QString msg( tr( "Creation of layer failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
816  showWarning( msg );
817  return nullptr;
818  }
819  hDS.reset();
820 
821  QString uri = QStringLiteral( "%1|layername=%2" ).arg( offlineDbPath, tableName );
823  newLayer = new QgsVectorLayer( uri, layer->name() + layerNameSuffix, QStringLiteral( "ogr" ), layerOptions );
824  break;
825  }
826  }
827 
828  if ( newLayer && newLayer->isValid() )
829  {
830 
831  // copy features
832  newLayer->startEditing();
833  QgsFeature f;
834 
835  QgsFeatureRequest req;
836 
837  if ( onlySelected )
838  {
839  QgsFeatureIds selectedFids = layer->selectedFeatureIds();
840  if ( !selectedFids.isEmpty() )
841  req.setFilterFids( selectedFids );
842  }
843 
844  QgsFeatureIterator fit = layer->dataProvider()->getFeatures( req );
845 
847  {
849  }
850  else
851  {
853  }
854  long long featureCount = 1;
855 
856  QList<QgsFeatureId> remoteFeatureIds;
857  while ( fit.nextFeature( f ) )
858  {
859  remoteFeatureIds << f.id();
860 
861  // NOTE: SpatiaLite provider ignores position of geometry column
862  // fill gap in QgsAttributeMap if geometry column is not last (WORKAROUND)
863  int column = 0;
864  QgsAttributes attrs = f.attributes();
865  // on GPKG newAttrs has an addition FID attribute, so we have to add a dummy in the original set
866  QgsAttributes newAttrs( containerType == GPKG ? attrs.count() + 1 : attrs.count() );
867  for ( int it = 0; it < attrs.count(); ++it )
868  {
869  QVariant attr = attrs.at( it );
870  if ( layer->fields().at( it ).type() == QVariant::StringList || layer->fields().at( it ).type() == QVariant::List )
871  {
872  attr = QgsJsonUtils::encodeValue( attr );
873  }
874  newAttrs[column++] = attr;
875  }
876  f.setAttributes( newAttrs );
877 
878  newLayer->addFeature( f );
879 
880  emit progressUpdated( featureCount++ );
881  }
882  if ( newLayer->commitChanges() )
883  {
885  featureCount = 1;
886 
887  // update feature id lookup
888  int layerId = getOrCreateLayerId( db, newLayer->id() );
889  QList<QgsFeatureId> offlineFeatureIds;
890 
891  QgsFeatureIterator fit = newLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setNoAttributes() );
892  while ( fit.nextFeature( f ) )
893  {
894  offlineFeatureIds << f.id();
895  }
896 
897  // NOTE: insert fids in this loop, as the db is locked during newLayer->nextFeature()
898  sqlExec( db, QStringLiteral( "BEGIN" ) );
899  int remoteCount = remoteFeatureIds.size();
900  for ( int i = 0; i < remoteCount; i++ )
901  {
902  // Check if the online feature has been fetched (WFS download aborted for some reason)
903  if ( i < offlineFeatureIds.count() )
904  {
905  addFidLookup( db, layerId, offlineFeatureIds.at( i ), remoteFeatureIds.at( i ) );
906  }
907  else
908  {
909  showWarning( tr( "Feature cannot be copied to the offline layer, please check if the online layer '%1' is still accessible." ).arg( layer->name() ) );
910  return nullptr;
911  }
912  emit progressUpdated( featureCount++ );
913  }
914  sqlExec( db, QStringLiteral( "COMMIT" ) );
915  }
916  else
917  {
918  showWarning( newLayer->commitErrors().join( QLatin1Char( '\n' ) ) );
919  }
920 
921  // copy the custom properties from original layer
922  newLayer->setCustomProperties( layer->customProperties() );
923 
924  // mark as offline layer
926 
927  // store original layer source and information
931  newLayer->setCustomProperty( CUSTOM_PROPERTY_LAYERNAME_SUFFIX, layerNameSuffix );
932 
933  // register this layer with the central layers registry
935  QList<QgsMapLayer *>() << newLayer );
936 
937  // copy style
938  copySymbology( layer, newLayer );
939 
940  //remove constrainst of fields that use defaultValueClauses from provider on original
941  const auto fields = layer->fields();
942  for ( const QgsField &field : fields )
943  {
944  if ( !layer->dataProvider()->defaultValueClause( layer->fields().fieldOriginIndex( layer->fields().indexOf( field.name() ) ) ).isEmpty() )
945  {
947  }
948  }
949 
951  // Find the parent group of the original layer
952  QgsLayerTreeLayer *layerTreeLayer = layerTreeRoot->findLayer( layer->id() );
953  if ( layerTreeLayer )
954  {
955  QgsLayerTreeGroup *parentTreeGroup = qobject_cast<QgsLayerTreeGroup *>( layerTreeLayer->parent() );
956  if ( parentTreeGroup )
957  {
958  int index = parentTreeGroup->children().indexOf( layerTreeLayer );
959  // Move the new layer from the root group to the new group
960  QgsLayerTreeLayer *newLayerTreeLayer = layerTreeRoot->findLayer( newLayer->id() );
961  if ( newLayerTreeLayer )
962  {
963  QgsLayerTreeNode *newLayerTreeLayerClone = newLayerTreeLayer->clone();
964  //copy the showFeatureCount property to the new node
965  newLayerTreeLayerClone->setCustomProperty( CUSTOM_SHOW_FEATURE_COUNT, layerTreeLayer->customProperty( CUSTOM_SHOW_FEATURE_COUNT ) );
966  newLayerTreeLayerClone->setItemVisibilityChecked( layerTreeLayer->isVisible() );
967  QgsLayerTreeGroup *grp = qobject_cast<QgsLayerTreeGroup *>( newLayerTreeLayer->parent() );
968  parentTreeGroup->insertChildNode( index, newLayerTreeLayerClone );
969  if ( grp )
970  grp->removeChildNode( newLayerTreeLayer );
971  }
972  }
973  }
974 
975  updateRelations( layer, newLayer );
976  updateMapThemes( layer, newLayer );
977  updateLayerOrder( layer, newLayer );
978 
979 
980 
981  }
982  return newLayer;
983 }
984 
985 void QgsOfflineEditing::applyAttributesAdded( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
986 {
987  Q_ASSERT( remoteLayer );
988 
989  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 );
990  QList<QgsField> fields = sqlQueryAttributesAdded( db, sql );
991 
992  const QgsVectorDataProvider *provider = remoteLayer->dataProvider();
993  QList<QgsVectorDataProvider::NativeType> nativeTypes = provider->nativeTypes();
994 
995  // NOTE: uses last matching QVariant::Type of nativeTypes
996  QMap < QVariant::Type, QString /*typeName*/ > typeNameLookup;
997  for ( int i = 0; i < nativeTypes.size(); i++ )
998  {
999  QgsVectorDataProvider::NativeType nativeType = nativeTypes.at( i );
1000  typeNameLookup[ nativeType.mType ] = nativeType.mTypeName;
1001  }
1002 
1003  emit progressModeSet( QgsOfflineEditing::AddFields, fields.size() );
1004 
1005  for ( int i = 0; i < fields.size(); i++ )
1006  {
1007  // lookup typename from layer provider
1008  QgsField field = fields[i];
1009  if ( typeNameLookup.contains( field.type() ) )
1010  {
1011  QString typeName = typeNameLookup[ field.type()];
1013  remoteLayer->addAttribute( field );
1014  }
1015  else
1016  {
1017  showWarning( QStringLiteral( "Could not add attribute '%1' of type %2" ).arg( field.name() ).arg( field.type() ) );
1018  }
1019 
1020  emit progressUpdated( i + 1 );
1021  }
1022 }
1023 
1024 void QgsOfflineEditing::applyFeaturesAdded( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
1025 {
1026  Q_ASSERT( offlineLayer );
1027  Q_ASSERT( remoteLayer );
1028 
1029  QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
1030  const QList<int> featureIdInts = sqlQueryInts( db, sql );
1031  QgsFeatureIds newFeatureIds;
1032  for ( int id : featureIdInts )
1033  {
1034  newFeatureIds << id;
1035  }
1036 
1037  QgsExpressionContext context = remoteLayer->createExpressionContext();
1038 
1039  // get new features from offline layer
1040  QgsFeatureList features;
1041  QgsFeatureIterator it = offlineLayer->getFeatures( QgsFeatureRequest().setFilterFids( newFeatureIds ) );
1042  QgsFeature feature;
1043  while ( it.nextFeature( feature ) )
1044  {
1045  features << feature;
1046  }
1047 
1048  // copy features to remote layer
1049  emit progressModeSet( QgsOfflineEditing::AddFeatures, features.size() );
1050 
1051  int i = 1;
1052  int newAttrsCount = remoteLayer->fields().count();
1053  for ( QgsFeatureList::iterator it = features.begin(); it != features.end(); ++it )
1054  {
1055  // NOTE: SpatiaLite provider ignores position of geometry column
1056  // restore gap in QgsAttributeMap if geometry column is not last (WORKAROUND)
1057  QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
1058  QgsAttributes newAttrs( newAttrsCount );
1059  QgsAttributes attrs = it->attributes();
1060  for ( int it = 0; it < attrs.count(); ++it )
1061  {
1062  int remoteAttributeIndex = attrLookup[ it ];
1063  QVariant attr = attrs.at( it );
1064  if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::StringList )
1065  {
1066  if ( attr.type() == QVariant::StringList || attr.type() == QVariant::List )
1067  {
1068  attr = attr.toStringList();
1069  }
1070  else
1071  {
1072  attr = QgsJsonUtils::parseArray( attr.toString(), QVariant::String );
1073  }
1074  }
1075  else if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::List )
1076  {
1077  if ( attr.type() == QVariant::StringList || attr.type() == QVariant::List )
1078  {
1079  attr = attr.toList();
1080  }
1081  else
1082  {
1083  attr = QgsJsonUtils::parseArray( attr.toString(), remoteLayer->fields().at( remoteAttributeIndex ).subType() );
1084  }
1085  }
1086  newAttrs[ remoteAttributeIndex ] = attr;
1087  }
1088 
1089  // respect constraints and provider default values
1090  QgsFeature f = QgsVectorLayerUtils::createFeature( remoteLayer, it->geometry(), newAttrs.toMap(), &context );
1091  remoteLayer->addFeature( f );
1092 
1093  emit progressUpdated( i++ );
1094  }
1095 }
1096 
1097 void QgsOfflineEditing::applyFeaturesRemoved( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
1098 {
1099  Q_ASSERT( remoteLayer );
1100 
1101  QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( layerId );
1102  QgsFeatureIds values = sqlQueryFeaturesRemoved( db, sql );
1103 
1104  emit progressModeSet( QgsOfflineEditing::RemoveFeatures, values.size() );
1105 
1106  int i = 1;
1107  for ( QgsFeatureIds::const_iterator it = values.constBegin(); it != values.constEnd(); ++it )
1108  {
1109  QgsFeatureId fid = remoteFid( db, layerId, *it );
1110  remoteLayer->deleteFeature( fid );
1111 
1112  emit progressUpdated( i++ );
1113  }
1114 }
1115 
1116 void QgsOfflineEditing::applyAttributeValueChanges( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
1117 {
1118  Q_ASSERT( offlineLayer );
1119  Q_ASSERT( remoteLayer );
1120 
1121  QString sql = QStringLiteral( "SELECT \"fid\", \"attr\", \"value\" FROM 'log_feature_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2 " ).arg( layerId ).arg( commitNo );
1122  AttributeValueChanges values = sqlQueryAttributeValueChanges( db, sql );
1123 
1124  emit progressModeSet( QgsOfflineEditing::UpdateFeatures, values.size() );
1125 
1126  QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
1127 
1128  for ( int i = 0; i < values.size(); i++ )
1129  {
1130  QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid );
1131  QgsDebugMsgLevel( QStringLiteral( "Offline changeAttributeValue %1 = %2" ).arg( QString( attrLookup[ values.at( i ).attr ] ), values.at( i ).value ), 4 );
1132 
1133  int remoteAttributeIndex = attrLookup[ values.at( i ).attr ];
1134  QVariant attr = values.at( i ).value;
1135  if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::StringList )
1136  {
1137  attr = QgsJsonUtils::parseArray( attr.toString(), QVariant::String );
1138  }
1139  else if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::List )
1140  {
1141  attr = QgsJsonUtils::parseArray( attr.toString(), remoteLayer->fields().at( remoteAttributeIndex ).subType() );
1142  }
1143 
1144  remoteLayer->changeAttributeValue( fid, remoteAttributeIndex, attr );
1145 
1146  emit progressUpdated( i + 1 );
1147  }
1148 }
1149 
1150 void QgsOfflineEditing::applyGeometryChanges( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
1151 {
1152  Q_ASSERT( remoteLayer );
1153 
1154  QString sql = QStringLiteral( "SELECT \"fid\", \"geom_wkt\" FROM 'log_geometry_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2" ).arg( layerId ).arg( commitNo );
1155  GeometryChanges values = sqlQueryGeometryChanges( db, sql );
1156 
1158 
1159  for ( int i = 0; i < values.size(); i++ )
1160  {
1161  QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid );
1162  QgsGeometry newGeom = QgsGeometry::fromWkt( values.at( i ).geom_wkt );
1163  remoteLayer->changeGeometry( fid, newGeom );
1164 
1165  emit progressUpdated( i + 1 );
1166  }
1167 }
1168 
1169 void QgsOfflineEditing::updateFidLookup( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
1170 {
1171  Q_ASSERT( remoteLayer );
1172 
1173  // update fid lookup for added features
1174 
1175  // get remote added fids
1176  // NOTE: use QMap for sorted fids
1177  QMap < QgsFeatureId, bool /*dummy*/ > newRemoteFids;
1178  QgsFeature f;
1179 
1180  QgsFeatureIterator fit = remoteLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setNoAttributes() );
1181 
1183 
1184  int i = 1;
1185  while ( fit.nextFeature( f ) )
1186  {
1187  if ( offlineFid( db, layerId, f.id() ) == -1 )
1188  {
1189  newRemoteFids[ f.id()] = true;
1190  }
1191 
1192  emit progressUpdated( i++ );
1193  }
1194 
1195  // get local added fids
1196  // NOTE: fids are sorted
1197  QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
1198  QList<int> newOfflineFids = sqlQueryInts( db, sql );
1199 
1200  if ( newRemoteFids.size() != newOfflineFids.size() )
1201  {
1202  //showWarning( QString( "Different number of new features on offline layer (%1) and remote layer (%2)" ).arg(newOfflineFids.size()).arg(newRemoteFids.size()) );
1203  }
1204  else
1205  {
1206  // add new fid lookups
1207  i = 0;
1208  sqlExec( db, QStringLiteral( "BEGIN" ) );
1209  for ( QMap<QgsFeatureId, bool>::const_iterator it = newRemoteFids.constBegin(); it != newRemoteFids.constEnd(); ++it )
1210  {
1211  addFidLookup( db, layerId, newOfflineFids.at( i++ ), it.key() );
1212  }
1213  sqlExec( db, QStringLiteral( "COMMIT" ) );
1214  }
1215 }
1216 
1217 void QgsOfflineEditing::copySymbology( QgsVectorLayer *sourceLayer, QgsVectorLayer *targetLayer )
1218 {
1219  Q_ASSERT( sourceLayer );
1220  Q_ASSERT( targetLayer );
1221 
1222  targetLayer->styleManager()->copyStylesFrom( sourceLayer->styleManager() );
1223 
1224  QString error;
1225  QDomDocument doc;
1226  QgsReadWriteContext context;
1227  QgsMapLayer::StyleCategories categories = static_cast<QgsMapLayer::StyleCategories>( QgsMapLayer::AllStyleCategories ) & ~QgsMapLayer::CustomProperties;
1228  sourceLayer->exportNamedStyle( doc, error, context, categories );
1229 
1230  if ( error.isEmpty() )
1231  {
1232  targetLayer->importNamedStyle( doc, error, categories );
1233  }
1234  if ( !error.isEmpty() )
1235  {
1236  showWarning( error );
1237  }
1238 }
1239 
1240 void QgsOfflineEditing::updateRelations( QgsVectorLayer *sourceLayer, QgsVectorLayer *targetLayer )
1241 {
1242  Q_ASSERT( sourceLayer );
1243  Q_ASSERT( targetLayer );
1244 
1246  const QList<QgsRelation> referencedRelations = relationManager->referencedRelations( sourceLayer );
1247 
1248  for ( QgsRelation relation : referencedRelations )
1249  {
1250  relationManager->removeRelation( relation );
1251  relation.setReferencedLayer( targetLayer->id() );
1252  relationManager->addRelation( relation );
1253  }
1254 
1255  const QList<QgsRelation> referencingRelations = relationManager->referencingRelations( sourceLayer );
1256 
1257  for ( QgsRelation relation : referencingRelations )
1258  {
1259  relationManager->removeRelation( relation );
1260  relation.setReferencingLayer( targetLayer->id() );
1261  relationManager->addRelation( relation );
1262  }
1263 }
1264 
1265 void QgsOfflineEditing::updateMapThemes( QgsVectorLayer *sourceLayer, QgsVectorLayer *targetLayer )
1266 {
1267  Q_ASSERT( sourceLayer );
1268  Q_ASSERT( targetLayer );
1269 
1271  const QStringList mapThemeNames = mapThemeCollection->mapThemes();
1272 
1273  for ( const QString &mapThemeName : mapThemeNames )
1274  {
1275  QgsMapThemeCollection::MapThemeRecord record = mapThemeCollection->mapThemeState( mapThemeName );
1276 
1277  const auto layerRecords = record.layerRecords();
1278 
1279  for ( QgsMapThemeCollection::MapThemeLayerRecord layerRecord : layerRecords )
1280  {
1281  if ( layerRecord.layer() == sourceLayer )
1282  {
1283  layerRecord.setLayer( targetLayer );
1284  record.removeLayerRecord( sourceLayer );
1285  record.addLayerRecord( layerRecord );
1286  }
1287  }
1288 
1289  QgsProject::instance()->mapThemeCollection()->update( mapThemeName, record );
1290  }
1291 }
1292 
1293 void QgsOfflineEditing::updateLayerOrder( QgsVectorLayer *sourceLayer, QgsVectorLayer *targetLayer )
1294 {
1295  Q_ASSERT( sourceLayer );
1296  Q_ASSERT( targetLayer );
1297 
1298  QList<QgsMapLayer *> layerOrder = QgsProject::instance()->layerTreeRoot()->customLayerOrder();
1299 
1300  auto iterator = layerOrder.begin();
1301 
1302  while ( iterator != layerOrder.end() )
1303  {
1304  if ( *iterator == targetLayer )
1305  {
1306  iterator = layerOrder.erase( iterator );
1307  if ( iterator == layerOrder.end() )
1308  break;
1309  }
1310 
1311  if ( *iterator == sourceLayer )
1312  {
1313  *iterator = targetLayer;
1314  }
1315 
1316  ++iterator;
1317  }
1318 
1320 }
1321 
1322 // NOTE: use this to map column indices in case the remote geometry column is not last
1323 QMap<int, int> QgsOfflineEditing::attributeLookup( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer )
1324 {
1325  Q_ASSERT( offlineLayer );
1326  Q_ASSERT( remoteLayer );
1327 
1328  const QgsAttributeList &offlineAttrs = offlineLayer->attributeList();
1329 
1330  QMap < int /*offline attr*/, int /*remote attr*/ > attrLookup;
1331  // NOTE: though offlineAttrs can have new attributes not yet synced, we take the amount of offlineAttrs
1332  // because we anyway only add mapping for the fields existing in remoteLayer (this because it could contain fid on 0)
1333  for ( int i = 0; i < offlineAttrs.size(); i++ )
1334  {
1335  if ( remoteLayer->fields().lookupField( offlineLayer->fields().field( i ).name() ) >= 0 )
1336  attrLookup.insert( offlineAttrs.at( i ), remoteLayer->fields().indexOf( offlineLayer->fields().field( i ).name() ) );
1337  }
1338 
1339  return attrLookup;
1340 }
1341 
1342 void QgsOfflineEditing::showWarning( const QString &message )
1343 {
1344  emit warning( tr( "Offline Editing Plugin" ), message );
1345 }
1346 
1347 sqlite3_database_unique_ptr QgsOfflineEditing::openLoggingDb()
1348 {
1349  sqlite3_database_unique_ptr database;
1351  if ( !dbPath.isEmpty() )
1352  {
1353  QString absoluteDbPath = QgsProject::instance()->readPath( dbPath );
1354  int rc = database.open( absoluteDbPath );
1355  if ( rc != SQLITE_OK )
1356  {
1357  QgsDebugMsg( QStringLiteral( "Could not open the SpatiaLite logging database" ) );
1358  showWarning( tr( "Could not open the SpatiaLite logging database" ) );
1359  }
1360  }
1361  else
1362  {
1363  QgsDebugMsg( QStringLiteral( "dbPath is empty!" ) );
1364  }
1365  return database;
1366 }
1367 
1368 int QgsOfflineEditing::getOrCreateLayerId( sqlite3 *db, const QString &qgisLayerId )
1369 {
1370  QString sql = QStringLiteral( "SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( qgisLayerId );
1371  int layerId = sqlQueryInt( db, sql, -1 );
1372  if ( layerId == -1 )
1373  {
1374  // next layer id
1375  sql = QStringLiteral( "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'layer_id'" );
1376  int newLayerId = sqlQueryInt( db, sql, -1 );
1377 
1378  // insert layer
1379  sql = QStringLiteral( "INSERT INTO 'log_layer_ids' VALUES (%1, '%2')" ).arg( newLayerId ).arg( qgisLayerId );
1380  sqlExec( db, sql );
1381 
1382  // increase layer_id
1383  // TODO: use trigger for auto increment?
1384  sql = QStringLiteral( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'layer_id'" ).arg( newLayerId + 1 );
1385  sqlExec( db, sql );
1386 
1387  layerId = newLayerId;
1388  }
1389 
1390  return layerId;
1391 }
1392 
1393 int QgsOfflineEditing::getCommitNo( sqlite3 *db )
1394 {
1395  QString sql = QStringLiteral( "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'commit_no'" );
1396  return sqlQueryInt( db, sql, -1 );
1397 }
1398 
1399 void QgsOfflineEditing::increaseCommitNo( sqlite3 *db )
1400 {
1401  QString sql = QStringLiteral( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'commit_no'" ).arg( getCommitNo( db ) + 1 );
1402  sqlExec( db, sql );
1403 }
1404 
1405 void QgsOfflineEditing::addFidLookup( sqlite3 *db, int layerId, QgsFeatureId offlineFid, QgsFeatureId remoteFid )
1406 {
1407  QString sql = QStringLiteral( "INSERT INTO 'log_fids' VALUES ( %1, %2, %3 )" ).arg( layerId ).arg( offlineFid ).arg( remoteFid );
1408  sqlExec( db, sql );
1409 }
1410 
1411 QgsFeatureId QgsOfflineEditing::remoteFid( sqlite3 *db, int layerId, QgsFeatureId offlineFid )
1412 {
1413  QString sql = QStringLiteral( "SELECT \"remote_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"offline_fid\" = %2" ).arg( layerId ).arg( offlineFid );
1414  return sqlQueryInt( db, sql, -1 );
1415 }
1416 
1417 QgsFeatureId QgsOfflineEditing::offlineFid( sqlite3 *db, int layerId, QgsFeatureId remoteFid )
1418 {
1419  QString sql = QStringLiteral( "SELECT \"offline_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"remote_fid\" = %2" ).arg( layerId ).arg( remoteFid );
1420  return sqlQueryInt( db, sql, -1 );
1421 }
1422 
1423 bool QgsOfflineEditing::isAddedFeature( sqlite3 *db, int layerId, QgsFeatureId fid )
1424 {
1425  QString sql = QStringLiteral( "SELECT COUNT(\"fid\") FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( fid );
1426  return ( sqlQueryInt( db, sql, 0 ) > 0 );
1427 }
1428 
1429 int QgsOfflineEditing::sqlExec( sqlite3 *db, const QString &sql )
1430 {
1431  char *errmsg = nullptr;
1432  int rc = sqlite3_exec( db, sql.toUtf8(), nullptr, nullptr, &errmsg );
1433  if ( rc != SQLITE_OK )
1434  {
1435  showWarning( errmsg );
1436  }
1437  return rc;
1438 }
1439 
1440 int QgsOfflineEditing::sqlQueryInt( sqlite3 *db, const QString &sql, int defaultValue )
1441 {
1442  sqlite3_stmt *stmt = nullptr;
1443  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1444  {
1445  showWarning( sqlite3_errmsg( db ) );
1446  return defaultValue;
1447  }
1448 
1449  int value = defaultValue;
1450  int ret = sqlite3_step( stmt );
1451  if ( ret == SQLITE_ROW )
1452  {
1453  value = sqlite3_column_int( stmt, 0 );
1454  }
1455  sqlite3_finalize( stmt );
1456 
1457  return value;
1458 }
1459 
1460 QList<int> QgsOfflineEditing::sqlQueryInts( sqlite3 *db, const QString &sql )
1461 {
1462  QList<int> values;
1463 
1464  sqlite3_stmt *stmt = nullptr;
1465  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1466  {
1467  showWarning( sqlite3_errmsg( db ) );
1468  return values;
1469  }
1470 
1471  int ret = sqlite3_step( stmt );
1472  while ( ret == SQLITE_ROW )
1473  {
1474  values << sqlite3_column_int( stmt, 0 );
1475 
1476  ret = sqlite3_step( stmt );
1477  }
1478  sqlite3_finalize( stmt );
1479 
1480  return values;
1481 }
1482 
1483 QList<QgsField> QgsOfflineEditing::sqlQueryAttributesAdded( sqlite3 *db, const QString &sql )
1484 {
1485  QList<QgsField> values;
1486 
1487  sqlite3_stmt *stmt = nullptr;
1488  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1489  {
1490  showWarning( sqlite3_errmsg( db ) );
1491  return values;
1492  }
1493 
1494  int ret = sqlite3_step( stmt );
1495  while ( ret == SQLITE_ROW )
1496  {
1497  QgsField field( QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 0 ) ) ),
1498  static_cast< QVariant::Type >( sqlite3_column_int( stmt, 1 ) ),
1499  QString(), // typeName
1500  sqlite3_column_int( stmt, 2 ),
1501  sqlite3_column_int( stmt, 3 ),
1502  QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 4 ) ) ) );
1503  values << field;
1504 
1505  ret = sqlite3_step( stmt );
1506  }
1507  sqlite3_finalize( stmt );
1508 
1509  return values;
1510 }
1511 
1512 QgsFeatureIds QgsOfflineEditing::sqlQueryFeaturesRemoved( sqlite3 *db, const QString &sql )
1513 {
1514  QgsFeatureIds values;
1515 
1516  sqlite3_stmt *stmt = nullptr;
1517  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1518  {
1519  showWarning( sqlite3_errmsg( db ) );
1520  return values;
1521  }
1522 
1523  int ret = sqlite3_step( stmt );
1524  while ( ret == SQLITE_ROW )
1525  {
1526  values << sqlite3_column_int( stmt, 0 );
1527 
1528  ret = sqlite3_step( stmt );
1529  }
1530  sqlite3_finalize( stmt );
1531 
1532  return values;
1533 }
1534 
1535 QgsOfflineEditing::AttributeValueChanges QgsOfflineEditing::sqlQueryAttributeValueChanges( sqlite3 *db, const QString &sql )
1536 {
1537  AttributeValueChanges values;
1538 
1539  sqlite3_stmt *stmt = nullptr;
1540  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1541  {
1542  showWarning( sqlite3_errmsg( db ) );
1543  return values;
1544  }
1545 
1546  int ret = sqlite3_step( stmt );
1547  while ( ret == SQLITE_ROW )
1548  {
1549  AttributeValueChange change;
1550  change.fid = sqlite3_column_int( stmt, 0 );
1551  change.attr = sqlite3_column_int( stmt, 1 );
1552  change.value = QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 2 ) ) );
1553  values << change;
1554 
1555  ret = sqlite3_step( stmt );
1556  }
1557  sqlite3_finalize( stmt );
1558 
1559  return values;
1560 }
1561 
1562 QgsOfflineEditing::GeometryChanges QgsOfflineEditing::sqlQueryGeometryChanges( sqlite3 *db, const QString &sql )
1563 {
1564  GeometryChanges values;
1565 
1566  sqlite3_stmt *stmt = nullptr;
1567  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1568  {
1569  showWarning( sqlite3_errmsg( db ) );
1570  return values;
1571  }
1572 
1573  int ret = sqlite3_step( stmt );
1574  while ( ret == SQLITE_ROW )
1575  {
1576  GeometryChange change;
1577  change.fid = sqlite3_column_int( stmt, 0 );
1578  change.geom_wkt = QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 1 ) ) );
1579  values << change;
1580 
1581  ret = sqlite3_step( stmt );
1582  }
1583  sqlite3_finalize( stmt );
1584 
1585  return values;
1586 }
1587 
1588 void QgsOfflineEditing::committedAttributesAdded( const QString &qgisLayerId, const QList<QgsField> &addedAttributes )
1589 {
1590  sqlite3_database_unique_ptr database = openLoggingDb();
1591  if ( !database )
1592  return;
1593 
1594  // insert log
1595  int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1596  int commitNo = getCommitNo( database.get() );
1597 
1598  for ( const QgsField &field : addedAttributes )
1599  {
1600  QString sql = QStringLiteral( "INSERT INTO 'log_added_attrs' VALUES ( %1, %2, '%3', %4, %5, %6, '%7' )" )
1601  .arg( layerId )
1602  .arg( commitNo )
1603  .arg( field.name() )
1604  .arg( field.type() )
1605  .arg( field.length() )
1606  .arg( field.precision() )
1607  .arg( field.comment() );
1608  sqlExec( database.get(), sql );
1609  }
1610 
1611  increaseCommitNo( database.get() );
1612 }
1613 
1614 void QgsOfflineEditing::committedFeaturesAdded( const QString &qgisLayerId, const QgsFeatureList &addedFeatures )
1615 {
1616  sqlite3_database_unique_ptr database = openLoggingDb();
1617  if ( !database )
1618  return;
1619 
1620  // insert log
1621  int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1622 
1623  // get new feature ids from db
1624  QgsMapLayer *layer = QgsProject::instance()->mapLayer( qgisLayerId );
1625  QString dataSourceString = layer->source();
1626  QgsDataSourceUri uri = QgsDataSourceUri( dataSourceString );
1627 
1629  QString tableName;
1630 
1631  if ( !offlinePath.contains( ".gpkg" ) )
1632  {
1633  tableName = uri.table();
1634  }
1635  else
1636  {
1637  QgsProviderMetadata *ogrProviderMetaData = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "ogr" ) );
1638  QVariantMap decodedUri = ogrProviderMetaData->decodeUri( dataSourceString );
1639  tableName = decodedUri.value( QStringLiteral( "layerName" ) ).toString();
1640  if ( tableName.isEmpty() )
1641  {
1642  showWarning( tr( "Could not deduce table name from data source %1." ).arg( dataSourceString ) );
1643  }
1644  }
1645 
1646  // only store feature ids
1647  QString sql = QStringLiteral( "SELECT ROWID FROM '%1' ORDER BY ROWID DESC LIMIT %2" ).arg( tableName ).arg( addedFeatures.size() );
1648  QList<int> newFeatureIds = sqlQueryInts( database.get(), sql );
1649  for ( int i = newFeatureIds.size() - 1; i >= 0; i-- )
1650  {
1651  QString sql = QStringLiteral( "INSERT INTO 'log_added_features' VALUES ( %1, %2 )" )
1652  .arg( layerId )
1653  .arg( newFeatureIds.at( i ) );
1654  sqlExec( database.get(), sql );
1655  }
1656 }
1657 
1658 void QgsOfflineEditing::committedFeaturesRemoved( const QString &qgisLayerId, const QgsFeatureIds &deletedFeatureIds )
1659 {
1660  sqlite3_database_unique_ptr database = openLoggingDb();
1661  if ( !database )
1662  return;
1663 
1664  // insert log
1665  int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1666 
1667  for ( QgsFeatureId id : deletedFeatureIds )
1668  {
1669  if ( isAddedFeature( database.get(), layerId, id ) )
1670  {
1671  // remove from added features log
1672  QString sql = QStringLiteral( "DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( id );
1673  sqlExec( database.get(), sql );
1674  }
1675  else
1676  {
1677  QString sql = QStringLiteral( "INSERT INTO 'log_removed_features' VALUES ( %1, %2)" )
1678  .arg( layerId )
1679  .arg( id );
1680  sqlExec( database.get(), sql );
1681  }
1682  }
1683 }
1684 
1685 void QgsOfflineEditing::committedAttributeValuesChanges( const QString &qgisLayerId, const QgsChangedAttributesMap &changedAttrsMap )
1686 {
1687  sqlite3_database_unique_ptr database = openLoggingDb();
1688  if ( !database )
1689  return;
1690 
1691  // insert log
1692  int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1693  int commitNo = getCommitNo( database.get() );
1694 
1695  for ( QgsChangedAttributesMap::const_iterator cit = changedAttrsMap.begin(); cit != changedAttrsMap.end(); ++cit )
1696  {
1697  QgsFeatureId fid = cit.key();
1698  if ( isAddedFeature( database.get(), layerId, fid ) )
1699  {
1700  // skip added features
1701  continue;
1702  }
1703  QgsAttributeMap attrMap = cit.value();
1704  for ( QgsAttributeMap::const_iterator it = attrMap.constBegin(); it != attrMap.constEnd(); ++it )
1705  {
1706  QString value = it.value().type() == QVariant::StringList || it.value().type() == QVariant::List ? QgsJsonUtils::encodeValue( it.value() ) : it.value().toString();
1707  value.replace( QLatin1String( "'" ), QLatin1String( "''" ) ); // escape quote
1708  QString sql = QStringLiteral( "INSERT INTO 'log_feature_updates' VALUES ( %1, %2, %3, %4, '%5' )" )
1709  .arg( layerId )
1710  .arg( commitNo )
1711  .arg( fid )
1712  .arg( it.key() ) // attribute
1713  .arg( value );
1714  sqlExec( database.get(), sql );
1715  }
1716  }
1717 
1718  increaseCommitNo( database.get() );
1719 }
1720 
1721 void QgsOfflineEditing::committedGeometriesChanges( const QString &qgisLayerId, const QgsGeometryMap &changedGeometries )
1722 {
1723  sqlite3_database_unique_ptr database = openLoggingDb();
1724  if ( !database )
1725  return;
1726 
1727  // insert log
1728  int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1729  int commitNo = getCommitNo( database.get() );
1730 
1731  for ( QgsGeometryMap::const_iterator it = changedGeometries.begin(); it != changedGeometries.end(); ++it )
1732  {
1733  QgsFeatureId fid = it.key();
1734  if ( isAddedFeature( database.get(), layerId, fid ) )
1735  {
1736  // skip added features
1737  continue;
1738  }
1739  QgsGeometry geom = it.value();
1740  QString sql = QStringLiteral( "INSERT INTO 'log_geometry_updates' VALUES ( %1, %2, %3, '%4' )" )
1741  .arg( layerId )
1742  .arg( commitNo )
1743  .arg( fid )
1744  .arg( geom.asWkt() );
1745  sqlExec( database.get(), sql );
1746 
1747  // TODO: use WKB instead of WKT?
1748  }
1749 
1750  increaseCommitNo( database.get() );
1751 }
1752 
1753 void QgsOfflineEditing::startListenFeatureChanges()
1754 {
1755  QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1756 
1757  Q_ASSERT( vLayer );
1758 
1759  // enable logging, check if editBuffer is not null
1760  if ( vLayer->editBuffer() )
1761  {
1762  QgsVectorLayerEditBuffer *editBuffer = vLayer->editBuffer();
1764  this, &QgsOfflineEditing::committedAttributesAdded );
1766  this, &QgsOfflineEditing::committedAttributeValuesChanges );
1768  this, &QgsOfflineEditing::committedGeometriesChanges );
1769  }
1770  connect( vLayer, &QgsVectorLayer::committedFeaturesAdded,
1771  this, &QgsOfflineEditing::committedFeaturesAdded );
1772  connect( vLayer, &QgsVectorLayer::committedFeaturesRemoved,
1773  this, &QgsOfflineEditing::committedFeaturesRemoved );
1774 }
1775 
1776 void QgsOfflineEditing::stopListenFeatureChanges()
1777 {
1778  QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1779 
1780  Q_ASSERT( vLayer );
1781 
1782  // disable logging, check if editBuffer is not null
1783  if ( vLayer->editBuffer() )
1784  {
1785  QgsVectorLayerEditBuffer *editBuffer = vLayer->editBuffer();
1786  disconnect( editBuffer, &QgsVectorLayerEditBuffer::committedAttributesAdded,
1787  this, &QgsOfflineEditing::committedAttributesAdded );
1789  this, &QgsOfflineEditing::committedAttributeValuesChanges );
1791  this, &QgsOfflineEditing::committedGeometriesChanges );
1792  }
1793  disconnect( vLayer, &QgsVectorLayer::committedFeaturesAdded,
1794  this, &QgsOfflineEditing::committedFeaturesAdded );
1795  disconnect( vLayer, &QgsVectorLayer::committedFeaturesRemoved,
1796  this, &QgsOfflineEditing::committedFeaturesRemoved );
1797 }
1798 
1799 void QgsOfflineEditing::layerAdded( QgsMapLayer *layer )
1800 {
1801  Q_ASSERT( layer );
1802 
1803  // detect offline layer
1804  if ( layer->customProperty( CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE, false ).toBool() )
1805  {
1806  QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( layer );
1807 
1808  Q_ASSERT( vLayer );
1809 
1810  connect( vLayer, &QgsVectorLayer::editingStarted, this, &QgsOfflineEditing::startListenFeatureChanges );
1811  connect( vLayer, &QgsVectorLayer::editingStopped, this, &QgsOfflineEditing::stopListenFeatureChanges );
1812  }
1813 }
1814 
A vector of attributes.
Definition: qgsattributes.h:58
QString authid() const
Returns the authority identifier for the CRS.
@ WKT_PREFERRED_GDAL
Preferred format for conversion of CRS to WKT for use with the GDAL library.
QString toWkt(WktVariant variant=WKT1_GDAL, bool multiline=false, int indentationWidth=4) const
Returns a WKT representation of this CRS.
virtual void invalidateConnections(const QString &connection)
Invalidate connections corresponding to specified name.
Class for storing the component parts of a RDBMS data source URI (e.g.
QString table() const
Returns the table name stored in the URI.
QString database() const
Returns the database name stored in the URI.
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
Wrapper for iterator of features from vector data provider or vector layer.
bool nextFeature(QgsFeature &f)
This class wraps a request for features to a vector layer (or directly its vector data provider).
QgsFeatureRequest & setFilterFids(const QgsFeatureIds &fids)
Sets feature IDs that should be fetched.
@ NoGeometry
Geometry is not required. It may still be returned if e.g. required for a filter condition.
FilterType filterType() const
Returns the filter type which is currently set on this request.
@ FilterFids
Filter using feature IDs.
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
Definition: qgsfeature.h:56
QgsAttributes attributes
Definition: qgsfeature.h:65
void setAttributes(const QgsAttributes &attrs)
Sets the feature's attributes.
Definition: qgsfeature.cpp:135
Q_GADGET QgsFeatureId id
Definition: qgsfeature.h:64
@ ConstraintNotNull
Field may not be null.
Encapsulate a field in an attribute table or data source.
Definition: qgsfield.h:51
QString name
Definition: qgsfield.h:60
int precision
Definition: qgsfield.h:57
int length
Definition: qgsfield.h:56
QVariant::Type type
Definition: qgsfield.h:58
QVariant::Type subType() const
If the field is a collection, gets its element's type.
Definition: qgsfield.cpp:133
QString comment
Definition: qgsfield.h:59
void setTypeName(const QString &typeName)
Set the field type.
Definition: qgsfield.cpp:189
Container of fields for a vector layer.
Definition: qgsfields.h:45
int indexOf(const QString &fieldName) const
Gets the field index from the field name.
Definition: qgsfields.cpp:207
int count() const
Returns number of items.
Definition: qgsfields.cpp:133
QgsField field(int fieldIdx) const
Returns the field at particular index (must be in range 0..N-1).
Definition: qgsfields.cpp:168
QgsField at(int i) const
Returns the field at particular index (must be in range 0..N-1).
Definition: qgsfields.cpp:163
int fieldOriginIndex(int fieldIdx) const
Returns the field's origin index (its meaning is specific to each type of origin).
Definition: qgsfields.cpp:197
int lookupField(const QString &fieldName) const
Looks up field's index from the field name.
Definition: qgsfields.cpp:344
A geometry is the spatial representation of a feature.
Definition: qgsgeometry.h:124
static QgsGeometry fromWkt(const QString &wkt)
Creates a new geometry from a WKT string.
QString asWkt(int precision=17) const
Exports the geometry to WKT.
static Q_INVOKABLE QString encodeValue(const QVariant &value)
Encodes a value to a JSON string representation, adding appropriate quotations and escaping where req...
static Q_INVOKABLE QVariantList parseArray(const QString &json, QVariant::Type type=QVariant::Invalid)
Parse a simple array (depth=1)
Layer tree group node serves as a container for layers and further groups.
void insertChildNode(int index, QgsLayerTreeNode *node)
Insert existing node at specified position.
void removeChildNode(QgsLayerTreeNode *node)
Remove a child node from this group.
QgsLayerTreeLayer * findLayer(QgsMapLayer *layer) const
Find layer node representing the map layer.
Layer tree node points to a map layer.
QgsLayerTreeLayer * clone() const override
Create a copy of the node. Returns new instance.
This class is a base class for nodes in a layer tree.
bool isVisible() const
Returns whether a node is really visible (ie checked and all its ancestors checked as well)
void setCustomProperty(const QString &key, const QVariant &value)
Sets a custom property for the node. Properties are stored in a map and saved in project file.
QVariant customProperty(const QString &key, const QVariant &defaultValue=QVariant()) const
Read a custom property from layer. Properties are stored in a map and saved in project file.
QgsLayerTreeNode * parent()
Gets pointer to the parent. If parent is nullptr, the node is a root node.
QList< QgsLayerTreeNode * > children()
Gets list of children of the node. Children are owned by the parent.
void setItemVisibilityChecked(bool checked)
Check or uncheck a node (independently of its ancestors or children)
void setCustomLayerOrder(const QList< QgsMapLayer * > &customLayerOrder)
The order in which layers will be rendered on the canvas.
QList< QgsMapLayer * > customLayerOrder() const
The order in which layers will be rendered on the canvas.
void copyStylesFrom(QgsMapLayerStyleManager *other)
Copies all styles from other.
Base class for all map layer types.
Definition: qgsmaplayer.h:70
QString name
Definition: qgsmaplayer.h:73
virtual void exportNamedStyle(QDomDocument &doc, QString &errorMsg, const QgsReadWriteContext &context=QgsReadWriteContext(), QgsMapLayer::StyleCategories categories=QgsMapLayer::AllStyleCategories) const
Export the properties of this layer as named style in a QDomDocument.
virtual bool importNamedStyle(QDomDocument &doc, QString &errorMsg, QgsMapLayer::StyleCategories categories=QgsMapLayer::AllStyleCategories)
Import the properties of this layer from a QDomDocument.
QString source() const
Returns the source for the layer.
Q_INVOKABLE QVariant customProperty(const QString &value, const QVariant &defaultValue=QVariant()) const
Read a custom property from layer.
QString providerType() const
Returns the provider type (provider key) for this layer.
void setCustomProperties(const QgsObjectCustomProperties &properties)
Set custom properties for layer.
QgsCoordinateReferenceSystem crs
Definition: qgsmaplayer.h:76
QString id() const
Returns the layer's unique ID, which is used to access this layer from QgsProject.
Q_INVOKABLE void setCustomProperty(const QString &key, const QVariant &value)
Set a custom property for layer.
bool isValid
Definition: qgsmaplayer.h:78
QgsMapLayerStyleManager * styleManager() const
Gets access to the layer's style manager.
@ AllStyleCategories
Definition: qgsmaplayer.h:172
@ CustomProperties
Custom properties (by plugins for instance)
Definition: qgsmaplayer.h:165
const QgsObjectCustomProperties & customProperties() const
Read all custom properties from layer.
Individual record of a visible layer in a map theme record.
Individual map theme record of visible layers and styles.
void removeLayerRecord(QgsMapLayer *layer)
Removes a record for layer if present.
QList< QgsMapThemeCollection::MapThemeLayerRecord > layerRecords() const
Returns a list of records for all visible layer belonging to the theme.
void addLayerRecord(const QgsMapThemeCollection::MapThemeLayerRecord &record)
Add a new record for a layer.
Container class that allows storage of map themes consisting of visible map layers and layer styles.
QgsMapThemeCollection::MapThemeRecord mapThemeState(const QString &name) const
Returns the recorded state of a map theme.
void update(const QString &name, const QgsMapThemeCollection::MapThemeRecord &state)
Updates a map theme within the collection.
void progressModeSet(QgsOfflineEditing::ProgressMode mode, long long maximum)
Emitted when the mode for the progress of the current operation is set.
void progressUpdated(long long progress)
Emitted with the progress of the current mode.
void layerProgressUpdated(int layer, int numLayers)
Emitted whenever a new layer is being processed.
bool isOfflineProject() const
Returns true if current project is offline.
bool convertToOfflineProject(const QString &offlineDataPath, const QString &offlineDbFile, const QStringList &layerIds, bool onlySelected=false, ContainerType containerType=SpatiaLite, const QString &layerNameSuffix=QStringLiteral(" (offline)"))
Convert current project for offline editing.
void warning(const QString &title, const QString &message)
Emitted when a warning needs to be displayed.
void synchronize()
Synchronize to remote layers.
void progressStopped()
Emitted when the processing of all layers has finished.
ContainerType
Type of offline database container file.
void progressStarted()
Emitted when the process has started.
QgsRelationManager * relationManager
Definition: qgsproject.h:109
QList< QgsMapLayer * > addMapLayers(const QList< QgsMapLayer * > &mapLayers, bool addToLegend=true, bool takeOwnership=true)
Add a list of layers to the map of loaded layers.
QString title() const
Returns the project's title.
Definition: qgsproject.cpp:488
static QgsProject * instance()
Returns the QgsProject singleton instance.
Definition: qgsproject.cpp:467
Q_INVOKABLE QgsMapLayer * mapLayer(const QString &layerId) const
Retrieve a pointer to a registered layer by layer ID.
void setSnappingConfig(const QgsSnappingConfig &snappingConfig)
The snapping configuration for this project.
void layerWasAdded(QgsMapLayer *layer)
Emitted when a layer was added to the registry.
QgsSnappingConfig snappingConfig
Definition: qgsproject.h:108
void removeMapLayers(const QStringList &layerIds)
Remove a set of layers from the registry by layer ID.
QString readEntry(const QString &scope, const QString &key, const QString &def=QString(), bool *ok=nullptr) const
Reads a string from the specified scope and key.
QgsMapThemeCollection * mapThemeCollection
Definition: qgsproject.h:107
QgsCoordinateTransformContext transformContext
Definition: qgsproject.h:105
QgsLayerTree * layerTreeRoot() const
Returns pointer to the root (invisible) node of the project's layer tree.
void setTitle(const QString &title)
Sets the project's title.
Definition: qgsproject.cpp:476
bool writeEntry(const QString &scope, const QString &key, bool value)
Write a boolean value to the project file.
QString readPath(const QString &filename) const
Transforms a filename read from the project file to an absolute path.
QMap< QString, QgsMapLayer * > mapLayers(const bool validOnly=false) const
Returns a map of all registered layers by layer ID.
bool removeEntry(const QString &scope, const QString &key)
Remove the given key from the specified scope.
Holds data provider key, description, and associated shared library file or function pointer informat...
virtual QVariantMap decodeUri(const QString &uri) const
Breaks a provider data source URI into its component paths (e.g.
static QgsProviderRegistry * instance(const QString &pluginPath=QString())
Means of accessing canonical single instance.
QgsProviderMetadata * providerMetadata(const QString &providerKey) const
Returns metadata of the provider or nullptr if not found.
The class is used as a container of context for various read/write operations on other objects.
This class manages a set of relations between layers.
QList< QgsRelation > referencedRelations(const QgsVectorLayer *layer=nullptr) const
Gets all relations where this layer is the referenced part (i.e.
QList< QgsRelation > referencingRelations(const QgsVectorLayer *layer=nullptr, int fieldIdx=-2) const
Gets all relations where the specified layer (and field) is the referencing part (i....
void removeRelation(const QString &id)
Remove a relation.
void addRelation(const QgsRelation &relation)
Add a relation.
This is a container for configuration of the snapping of the project.
QHash< QgsVectorLayer *, QgsSnappingConfig::IndividualLayerSettings > individualLayerSettings() const
Returns individual snapping settings for all layers.
bool removeLayers(const QList< QgsMapLayer * > &layers)
Removes the specified layers from the individual layer configuration.
void setIndividualLayerSettings(QgsVectorLayer *vl, const QgsSnappingConfig::IndividualLayerSettings &individualLayerSettings)
Sets individual layer snappings settings (applied if mode is AdvancedConfiguration)
This is the base class for vector data providers.
long long featureCount() const override=0
Number of features in the layer.
QList< QgsVectorDataProvider::NativeType > nativeTypes() const
Returns the names of the supported types.
virtual QString defaultValueClause(int fieldIndex) const
Returns any default value clauses which are present at the provider for a specified field index.
QgsFields fields() const override=0
Returns the fields associated with this data provider.
QgsFeatureIterator getFeatures(const QgsFeatureRequest &request=QgsFeatureRequest()) const override=0
Query the provider for features specified in request.
void committedAttributeValuesChanges(const QString &layerId, const QgsChangedAttributesMap &changedAttributesValues)
void committedAttributesAdded(const QString &layerId, const QList< QgsField > &addedAttributes)
void committedGeometriesChanges(const QString &layerId, const QgsGeometryMap &changedGeometries)
Defines left outer join from our vector layer to some other vector layer.
static QgsFeature createFeature(const QgsVectorLayer *layer, const QgsGeometry &geometry=QgsGeometry(), const QgsAttributeMap &attributes=QgsAttributeMap(), QgsExpressionContext *context=nullptr)
Creates a new feature ready for insertion into a layer.
Represents a vector layer which manages a vector based data sets.
Q_INVOKABLE QgsWkbTypes::Type wkbType() const FINAL
Returns the WKBType or WKBUnknown in case of error.
void committedFeaturesAdded(const QString &layerId, const QgsFeatureList &addedFeatures)
Emitted when features are added to the provider.
void editingStopped()
Emitted when edited changes have been successfully written to the data provider.
Q_INVOKABLE bool startEditing()
Makes the layer editable.
bool addJoin(const QgsVectorLayerJoinInfo &joinInfo)
Joins another vector layer to this layer.
bool addAttribute(const QgsField &field)
Add an attribute field (but does not commit it) returns true if the field was added.
long long featureCount(const QString &legendKey) const
Number of features rendered with specified legend key.
bool isSpatial() const FINAL
Returns true if this is a geometry layer and false in case of NoGeometry (table only) or UnknownGeome...
bool deleteFeature(QgsFeatureId fid, DeleteContext *context=nullptr)
Deletes a feature from the layer (but does not commit it).
QgsFeatureIterator getFeatures(const QgsFeatureRequest &request=QgsFeatureRequest()) const FINAL
Queries the layer for features specified in request.
QgsFields fields() const FINAL
Returns the list of fields of this layer.
QgsAttributeList attributeList() const
Returns list of attribute indexes.
void removeFieldConstraint(int index, QgsFieldConstraints::Constraint constraint)
Removes a constraint for a specified field index.
void editingStarted()
Emitted when editing on this layer has started.
void committedFeaturesRemoved(const QString &layerId, const QgsFeatureIds &deletedFeatureIds)
Emitted when features are deleted from the provider.
QgsExpressionContext createExpressionContext() const FINAL
This method needs to be reimplemented in all classes which implement this interface and return an exp...
Q_INVOKABLE const QgsFeatureIds & selectedFeatureIds() const
Returns a list of the selected features IDs in this layer.
QStringList commitErrors() const
Returns a list containing any error messages generated when attempting to commit changes to the layer...
QString dataComment() const
Returns a description for this layer as defined in the data provider.
Q_INVOKABLE bool commitChanges(bool stopEditing=true)
Attempts to commit to the underlying data provider any buffered changes made since the last to call t...
QgsVectorDataProvider * dataProvider() FINAL
Returns the layer's data provider, it may be nullptr.
bool changeAttributeValue(QgsFeatureId fid, int field, const QVariant &newValue, const QVariant &oldValue=QVariant(), bool skipDefaultValues=false)
Changes an attribute value for a feature (but does not immediately commit the changes).
bool addFeature(QgsFeature &feature, QgsFeatureSink::Flags flags=QgsFeatureSink::Flags()) FINAL
Adds a single feature to the sink.
void reload() FINAL
Synchronises with changes in the datasource.
const QList< QgsVectorLayerJoinInfo > vectorJoins() const
Q_INVOKABLE QgsVectorLayerEditBuffer * editBuffer()
Buffer with uncommitted editing operations. Only valid after editing has been turned on.
bool changeGeometry(QgsFeatureId fid, QgsGeometry &geometry, bool skipDefaultValue=false)
Changes a feature's geometry within the layer's edit buffer (but does not immediately commit the chan...
static bool hasM(Type type) SIP_HOLDGIL
Tests whether a WKB type contains m values.
Definition: qgswkbtypes.h:1100
Type
The WKB type describes the number of dimensions a geometry has.
Definition: qgswkbtypes.h:70
static QString displayString(Type type) SIP_HOLDGIL
Returns a non-translated display string type for a WKB type, e.g., the geometry name used in WKT geom...
static Type flatType(Type type) SIP_HOLDGIL
Returns the flat type for a WKB type.
Definition: qgswkbtypes.h:702
static bool hasZ(Type type) SIP_HOLDGIL
Tests whether a WKB type contains the z-dimension.
Definition: qgswkbtypes.h:1050
Unique pointer for spatialite databases, which automatically closes the database when the pointer goe...
int open(const QString &path)
Opens the database at the specified file path.
int open_v2(const QString &path, int flags, const char *zVfs)
Opens the database at the specified file path.
QString errorMessage() const
Returns the most recent error message encountered by the database.
Unique pointer for sqlite3 databases, which automatically closes the database when the pointer goes o...
int open(const QString &path)
Opens the database at the specified file path.
std::unique_ptr< std::remove_pointer< OGRDataSourceH >::type, OGRDataSourceDeleter > ogr_datasource_unique_ptr
Scoped OGR data source.
Definition: qgsogrutils.h:116
std::unique_ptr< std::remove_pointer< OGRFieldDefnH >::type, OGRFldDeleter > ogr_field_def_unique_ptr
Scoped OGR field definition.
Definition: qgsogrutils.h:126
QMap< int, QVariant > QgsAttributeMap
Definition: qgsattributes.h:38
struct sqlite3 sqlite3
void * OGRSpatialReferenceH
QMap< QgsFeatureId, QgsGeometry > QgsGeometryMap
Definition: qgsfeature.h:731
QMap< QgsFeatureId, QgsAttributeMap > QgsChangedAttributesMap
Definition: qgsfeature.h:722
QList< QgsFeature > QgsFeatureList
Definition: qgsfeature.h:736
QSet< QgsFeatureId > QgsFeatureIds
Definition: qgsfeatureid.h:37
qint64 QgsFeatureId
64 bit feature ids negative numbers are used for uncommitted/newly added features
Definition: qgsfeatureid.h:28
QList< int > QgsAttributeList
Definition: qgsfield.h:26
const QgsField & field
Definition: qgsfield.h:463
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
#define CUSTOM_PROPERTY_ORIGINAL_LAYERID
#define CUSTOM_SHOW_FEATURE_COUNT
#define PROJECT_ENTRY_SCOPE_OFFLINE
#define CUSTOM_PROPERTY_REMOTE_PROVIDER
#define CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE
#define CUSTOM_PROPERTY_LAYERNAME_SUFFIX
#define CUSTOM_PROPERTY_REMOTE_SOURCE
#define PROJECT_ENTRY_KEY_OFFLINE_DB_PATH
QList< QgsVectorLayerJoinInfo > QgsVectorJoinList
const QString & typeName
Setting options for loading vector layers.