QGIS API Documentation  3.22.4-Białowieża (ce8e65e95e)
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 #include "qgstransactiongroup.h"
46 
47 #include <QDir>
48 #include <QDomDocument>
49 #include <QDomNode>
50 #include <QFile>
51 #include <QRegularExpression>
52 
53 #include <ogr_srs_api.h>
54 
55 extern "C"
56 {
57 #include <sqlite3.h>
58 }
59 
60 #ifdef HAVE_SPATIALITE
61 extern "C"
62 {
63 #include <spatialite.h>
64 }
65 #endif
66 
67 #define CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE "isOfflineEditable"
68 #define CUSTOM_PROPERTY_REMOTE_SOURCE "remoteSource"
69 #define CUSTOM_PROPERTY_REMOTE_PROVIDER "remoteProvider"
70 #define CUSTOM_SHOW_FEATURE_COUNT "showFeatureCount"
71 #define CUSTOM_PROPERTY_ORIGINAL_LAYERID "remoteLayerId"
72 #define CUSTOM_PROPERTY_LAYERNAME_SUFFIX "layerNameSuffix"
73 #define PROJECT_ENTRY_SCOPE_OFFLINE "OfflineEditingPlugin"
74 #define PROJECT_ENTRY_KEY_OFFLINE_DB_PATH "/OfflineDbPath"
75 
77 {
78  connect( QgsProject::instance(), &QgsProject::layerWasAdded, this, &QgsOfflineEditing::setupLayer );
79 }
80 
93 bool QgsOfflineEditing::convertToOfflineProject( const QString &offlineDataPath, const QString &offlineDbFile, const QStringList &layerIds, bool onlySelected, ContainerType containerType, const QString &layerNameSuffix )
94 {
95  if ( layerIds.isEmpty() )
96  {
97  return false;
98  }
99 
100  const QString dbPath = QDir( offlineDataPath ).absoluteFilePath( offlineDbFile );
101  if ( createOfflineDb( dbPath, containerType ) )
102  {
104  const int rc = database.open( dbPath );
105  if ( rc != SQLITE_OK )
106  {
107  showWarning( tr( "Could not open the SpatiaLite database" ) );
108  }
109  else
110  {
111  // create logging tables
112  createLoggingTables( database.get() );
113 
114  emit progressStarted();
115 
116  // copy selected vector layers to offline layer
117  for ( int i = 0; i < layerIds.count(); i++ )
118  {
119  emit layerProgressUpdated( i + 1, layerIds.count() );
120 
121  QgsMapLayer *layer = QgsProject::instance()->mapLayer( layerIds.at( i ) );
122  QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( layer );
123  if ( vl && vl->isValid() )
124  {
125  const QString origLayerId = vl->id();
126  convertToOfflineLayer( vl, database.get(), dbPath, onlySelected, containerType, layerNameSuffix );
127  }
128  }
129 
130  emit progressStopped();
131 
132  // save offline project
133  QString projectTitle = QgsProject::instance()->title();
134  if ( projectTitle.isEmpty() )
135  {
136  projectTitle = QFileInfo( QgsProject::instance()->fileName() ).fileName();
137  }
138  projectTitle += QLatin1String( " (offline)" );
139  QgsProject::instance()->setTitle( projectTitle );
141 
142  return true;
143  }
144  }
145 
146  return false;
147 }
148 
150 {
152 }
153 
154 void QgsOfflineEditing::synchronize( bool useTransaction )
155 {
156  // open logging db
157  const sqlite3_database_unique_ptr database = openLoggingDb();
158  if ( !database )
159  {
160  return;
161  }
162 
163  emit progressStarted();
164 
165  const QgsSnappingConfig snappingConfig = QgsProject::instance()->snappingConfig();
166 
167  // restore and sync remote layers
168  QMap<QString, QgsMapLayer *> mapLayers = QgsProject::instance()->mapLayers();
169  QMap<int, std::shared_ptr<QgsVectorLayer>> remoteLayersByOfflineId;
170  QMap<int, QgsVectorLayer *> offlineLayersByOfflineId;
171 
172  for ( QMap<QString, QgsMapLayer *>::iterator layer_it = mapLayers.begin() ; layer_it != mapLayers.end(); ++layer_it )
173  {
174  QgsVectorLayer *offlineLayer( qobject_cast<QgsVectorLayer *>( layer_it.value() ) );
175 
176  if ( !offlineLayer || !offlineLayer->isValid() )
177  {
178  QgsDebugMsgLevel( QStringLiteral( "Skipping offline layer %1 because it is an invalid layer" ).arg( layer_it.key() ), 4 );
179  continue;
180  }
181 
182  if ( !offlineLayer->customProperty( CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE, false ).toBool() )
183  continue;
184 
185  const QString remoteSource = offlineLayer->customProperty( CUSTOM_PROPERTY_REMOTE_SOURCE, "" ).toString();
186  const QString remoteProvider = offlineLayer->customProperty( CUSTOM_PROPERTY_REMOTE_PROVIDER, "" ).toString();
187  QString remoteName = offlineLayer->name();
188  const QString remoteNameSuffix = offlineLayer->customProperty( CUSTOM_PROPERTY_LAYERNAME_SUFFIX, " (offline)" ).toString();
189  if ( remoteName.endsWith( remoteNameSuffix ) )
190  remoteName.chop( remoteNameSuffix.size() );
192 
193  std::shared_ptr<QgsVectorLayer> remoteLayer = std::make_shared<QgsVectorLayer>( remoteSource, remoteName, remoteProvider, options );
194 
195  if ( ! remoteLayer->isValid() )
196  {
197  QgsDebugMsgLevel( QStringLiteral( "Skipping offline layer %1 because it failed to recreate its corresponding remote layer" ).arg( offlineLayer->id() ), 4 );
198  continue;
199  }
200 
201  // Rebuild WFS cache to get feature id<->GML fid mapping
202  if ( remoteLayer->providerType().contains( QLatin1String( "WFS" ), Qt::CaseInsensitive ) )
203  {
204  QgsFeatureIterator fit = remoteLayer->getFeatures();
205  QgsFeature f;
206  while ( fit.nextFeature( f ) )
207  {
208  }
209  }
210 
211  // TODO: only add remote layer if there are log entries?
212  // apply layer edit log
213  const QString sql = QStringLiteral( "SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( offlineLayer->id() );
214  const int layerId = sqlQueryInt( database.get(), sql, -1 );
215 
216  if ( layerId == -1 )
217  {
218  QgsDebugMsgLevel( QStringLiteral( "Skipping offline layer %1 because it failed to determine the offline editing layer id" ).arg( offlineLayer->id() ), 4 );
219  continue;
220  }
221 
222  remoteLayersByOfflineId.insert( layerId, remoteLayer );
223  offlineLayersByOfflineId.insert( layerId, offlineLayer );
224  }
225 
226  QgsDebugMsgLevel( QStringLiteral( "Found %1 offline layers in total" ).arg( offlineLayersByOfflineId.count() ), 4 );
227 
228  QMap<QPair<QString, QString>, std::shared_ptr<QgsTransactionGroup>> transactionGroups;
229  if ( useTransaction )
230  {
231  for ( const std::shared_ptr<QgsVectorLayer> &remoteLayer : std::as_const( remoteLayersByOfflineId ) )
232  {
233  const QString connectionString = QgsTransaction::connectionString( remoteLayer->source() );
234  const QPair<QString, QString> pair( remoteLayer->providerType(), connectionString );
235  std::shared_ptr<QgsTransactionGroup> transactionGroup = transactionGroups.value( pair );
236 
237  if ( !transactionGroup.get() )
238  transactionGroup = std::make_shared<QgsTransactionGroup>();
239 
240  if ( !transactionGroup->addLayer( remoteLayer.get() ) )
241  {
242  QgsDebugMsgLevel( QStringLiteral( "Failed to add a layer %1 into transaction group, will be modified without transaction" ).arg( remoteLayer->name() ), 4 );
243  continue;
244  }
245 
246  transactionGroups.insert( pair, transactionGroup );
247  }
248 
249  QgsDebugMsgLevel( QStringLiteral( "Created %1 transaction groups" ).arg( transactionGroups.count() ), 4 );
250  }
251 
252  const QList<int> offlineIds = remoteLayersByOfflineId.keys();
253  for ( int offlineLayerId : offlineIds )
254  {
255  std::shared_ptr<QgsVectorLayer> remoteLayer = remoteLayersByOfflineId.value( offlineLayerId );
256  QgsVectorLayer *offlineLayer = offlineLayersByOfflineId.value( offlineLayerId );
257 
258  // NOTE: if transaction is enabled, the layer might be already in editing mode
259  if ( !remoteLayer->startEditing() && !remoteLayer->isEditable() )
260  {
261  QgsDebugMsgLevel( QStringLiteral( "Failed to turn layer %1 into editing mode" ).arg( remoteLayer->name() ), 4 );
262  continue;
263  }
264 
265  // TODO: only get commitNos of this layer?
266  const int commitNo = getCommitNo( database.get() );
267  QgsDebugMsgLevel( QStringLiteral( "Found %1 commits" ).arg( commitNo ), 4 );
268 
269  for ( int i = 0; i < commitNo; i++ )
270  {
271  QgsDebugMsgLevel( QStringLiteral( "Apply commits chronologically from %1" ).arg( offlineLayer->name() ), 4 );
272  // apply commits chronologically
273  applyAttributesAdded( remoteLayer.get(), database.get(), offlineLayerId, i );
274  applyAttributeValueChanges( offlineLayer, remoteLayer.get(), database.get(), offlineLayerId, i );
275  applyGeometryChanges( remoteLayer.get(), database.get(), offlineLayerId, i );
276  }
277 
278  applyFeaturesAdded( offlineLayer, remoteLayer.get(), database.get(), offlineLayerId );
279  applyFeaturesRemoved( remoteLayer.get(), database.get(), offlineLayerId );
280  }
281 
282 
283  for ( int offlineLayerId : offlineIds )
284  {
285  std::shared_ptr<QgsVectorLayer> remoteLayer = remoteLayersByOfflineId[offlineLayerId];
286  QgsVectorLayer *offlineLayer = offlineLayersByOfflineId[offlineLayerId];
287 
288  if ( !remoteLayer->isEditable() )
289  continue;
290 
291  if ( remoteLayer->commitChanges() )
292  {
293  // update fid lookup
294  updateFidLookup( remoteLayer.get(), database.get(), offlineLayerId );
295 
296  QString sql;
297  // clear edit log for this layer
298  sql = QStringLiteral( "DELETE FROM 'log_added_attrs' WHERE \"layer_id\" = %1" ).arg( offlineLayerId );
299  sqlExec( database.get(), sql );
300  sql = QStringLiteral( "DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( offlineLayerId );
301  sqlExec( database.get(), sql );
302  sql = QStringLiteral( "DELETE FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( offlineLayerId );
303  sqlExec( database.get(), sql );
304  sql = QStringLiteral( "DELETE FROM 'log_feature_updates' WHERE \"layer_id\" = %1" ).arg( offlineLayerId );
305  sqlExec( database.get(), sql );
306  sql = QStringLiteral( "DELETE FROM 'log_geometry_updates' WHERE \"layer_id\" = %1" ).arg( offlineLayerId );
307  sqlExec( database.get(), sql );
308  }
309  else
310  {
311  showWarning( remoteLayer->commitErrors().join( QLatin1Char( '\n' ) ) );
312  }
313 
314  // Invalidate the connection to force a reload if the project is put offline
315  // again with the same path
316  offlineLayer->dataProvider()->invalidateConnections( QgsDataSourceUri( offlineLayer->source() ).database() );
317 
318  remoteLayer->reload(); //update with other changes
319  offlineLayer->setDataSource( remoteLayer->source(), remoteLayer->name(), remoteLayer->dataProvider()->name() );
320 
321  // remove offline layer properties
323 
324  // remove original layer source and information
329 
330  // remove connected signals
331  disconnect( offlineLayer, &QgsVectorLayer::editingStarted, this, &QgsOfflineEditing::startListenFeatureChanges );
332  disconnect( offlineLayer, &QgsVectorLayer::editingStopped, this, &QgsOfflineEditing::stopListenFeatureChanges );
333 
334  //add constrainst of fields that use defaultValueClauses from provider on original
335  const QgsFields fields = remoteLayer->fields();
336  for ( const QgsField &field : fields )
337  {
338  if ( !remoteLayer->dataProvider()->defaultValueClause( remoteLayer->fields().fieldOriginIndex( remoteLayer->fields().indexOf( field.name() ) ) ).isEmpty() )
339  {
340  offlineLayer->setFieldConstraint( offlineLayer->fields().indexOf( field.name() ), QgsFieldConstraints::ConstraintNotNull );
341  }
342  }
343  }
344 
345  // disable offline project
346  const QString projectTitle = QgsProject::instance()->title().remove( QRegularExpression( " \\(offline\\)$" ) );
347  QgsProject::instance()->setTitle( projectTitle );
349  // reset commitNo
350  const QString sql = QStringLiteral( "UPDATE 'log_indices' SET 'last_index' = 0 WHERE \"name\" = 'commit_no'" );
351  sqlExec( database.get(), sql );
352  emit progressStopped();
353 }
354 
355 void QgsOfflineEditing::initializeSpatialMetadata( sqlite3 *sqlite_handle )
356 {
357 #ifdef HAVE_SPATIALITE
358  // attempting to perform self-initialization for a newly created DB
359  if ( !sqlite_handle )
360  return;
361  // checking if this DB is really empty
362  char **results = nullptr;
363  int rows, columns;
364  int ret = sqlite3_get_table( sqlite_handle, "select count(*) from sqlite_master", &results, &rows, &columns, nullptr );
365  if ( ret != SQLITE_OK )
366  return;
367  int count = 0;
368  if ( rows >= 1 )
369  {
370  for ( int i = 1; i <= rows; i++ )
371  count = atoi( results[( i * columns ) + 0] );
372  }
373 
374  sqlite3_free_table( results );
375 
376  if ( count > 0 )
377  return;
378 
379  bool above41 = false;
380  ret = sqlite3_get_table( sqlite_handle, "select spatialite_version()", &results, &rows, &columns, nullptr );
381  if ( ret == SQLITE_OK && rows == 1 && columns == 1 )
382  {
383  const QString version = QString::fromUtf8( results[1] );
384 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
385  QStringList parts = version.split( ' ', QString::SkipEmptyParts );
386 #else
387  const QStringList parts = version.split( ' ', Qt::SkipEmptyParts );
388 #endif
389  if ( !parts.empty() )
390  {
391 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
392  QStringList verparts = parts.at( 0 ).split( '.', QString::SkipEmptyParts );
393 #else
394  const QStringList verparts = parts.at( 0 ).split( '.', Qt::SkipEmptyParts );
395 #endif
396  above41 = verparts.size() >= 2 && ( verparts.at( 0 ).toInt() > 4 || ( verparts.at( 0 ).toInt() == 4 && verparts.at( 1 ).toInt() >= 1 ) );
397  }
398  }
399 
400  sqlite3_free_table( results );
401 
402  // all right, it's empty: proceeding to initialize
403  char *errMsg = nullptr;
404  ret = sqlite3_exec( sqlite_handle, above41 ? "SELECT InitSpatialMetadata(1)" : "SELECT InitSpatialMetadata()", nullptr, nullptr, &errMsg );
405 
406  if ( ret != SQLITE_OK )
407  {
408  QString errCause = tr( "Unable to initialize SpatialMetadata:\n" );
409  errCause += QString::fromUtf8( errMsg );
410  showWarning( errCause );
411  sqlite3_free( errMsg );
412  return;
413  }
414  spatial_ref_sys_init( sqlite_handle, 0 );
415 #else
416  ( void )sqlite_handle;
417 #endif
418 }
419 
420 bool QgsOfflineEditing::createOfflineDb( const QString &offlineDbPath, ContainerType containerType )
421 {
422  int ret;
423  char *errMsg = nullptr;
424  const QFile newDb( offlineDbPath );
425  if ( newDb.exists() )
426  {
427  QFile::remove( offlineDbPath );
428  }
429 
430  // see also QgsNewSpatialiteLayerDialog::createDb()
431 
432  const QFileInfo fullPath = QFileInfo( offlineDbPath );
433  const QDir path = fullPath.dir();
434 
435  // Must be sure there is destination directory ~/.qgis
436  QDir().mkpath( path.absolutePath() );
437 
438  // creating/opening the new database
439  const QString dbPath = newDb.fileName();
440 
441  // creating geopackage
442  switch ( containerType )
443  {
444  case GPKG:
445  {
446  OGRSFDriverH hGpkgDriver = OGRGetDriverByName( "GPKG" );
447  if ( !hGpkgDriver )
448  {
449  showWarning( tr( "Creation of database failed. GeoPackage driver not found." ) );
450  return false;
451  }
452 
453  const gdal::ogr_datasource_unique_ptr hDS( OGR_Dr_CreateDataSource( hGpkgDriver, dbPath.toUtf8().constData(), nullptr ) );
454  if ( !hDS )
455  {
456  showWarning( tr( "Creation of database failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
457  return false;
458  }
459  break;
460  }
461  case SpatiaLite:
462  {
463  break;
464  }
465  }
466 
468  ret = database.open_v2( dbPath, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr );
469  if ( ret )
470  {
471  // an error occurred
472  QString errCause = tr( "Could not create a new database\n" );
473  errCause += database.errorMessage();
474  showWarning( errCause );
475  return false;
476  }
477  // activating Foreign Key constraints
478  ret = sqlite3_exec( database.get(), "PRAGMA foreign_keys = 1", nullptr, nullptr, &errMsg );
479  if ( ret != SQLITE_OK )
480  {
481  showWarning( tr( "Unable to activate FOREIGN_KEY constraints" ) );
482  sqlite3_free( errMsg );
483  return false;
484  }
485  initializeSpatialMetadata( database.get() );
486  return true;
487 }
488 
489 void QgsOfflineEditing::createLoggingTables( sqlite3 *db )
490 {
491  // indices
492  QString sql = QStringLiteral( "CREATE TABLE 'log_indices' ('name' TEXT, 'last_index' INTEGER)" );
493  sqlExec( db, sql );
494 
495  sql = QStringLiteral( "INSERT INTO 'log_indices' VALUES ('commit_no', 0)" );
496  sqlExec( db, sql );
497 
498  sql = QStringLiteral( "INSERT INTO 'log_indices' VALUES ('layer_id', 0)" );
499  sqlExec( db, sql );
500 
501  // layername <-> layer id
502  sql = QStringLiteral( "CREATE TABLE 'log_layer_ids' ('id' INTEGER, 'qgis_id' TEXT)" );
503  sqlExec( db, sql );
504 
505  // offline fid <-> remote fid
506  sql = QStringLiteral( "CREATE TABLE 'log_fids' ('layer_id' INTEGER, 'offline_fid' INTEGER, 'remote_fid' INTEGER, 'remote_pk' TEXT)" );
507  sqlExec( db, sql );
508 
509  // added attributes
510  sql = QStringLiteral( "CREATE TABLE 'log_added_attrs' ('layer_id' INTEGER, 'commit_no' INTEGER, " );
511  sql += QLatin1String( "'name' TEXT, 'type' INTEGER, 'length' INTEGER, 'precision' INTEGER, 'comment' TEXT)" );
512  sqlExec( db, sql );
513 
514  // added features
515  sql = QStringLiteral( "CREATE TABLE 'log_added_features' ('layer_id' INTEGER, 'fid' INTEGER)" );
516  sqlExec( db, sql );
517 
518  // removed features
519  sql = QStringLiteral( "CREATE TABLE 'log_removed_features' ('layer_id' INTEGER, 'fid' INTEGER)" );
520  sqlExec( db, sql );
521 
522  // feature updates
523  sql = QStringLiteral( "CREATE TABLE 'log_feature_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'attr' INTEGER, 'value' TEXT)" );
524  sqlExec( db, sql );
525 
526  // geometry updates
527  sql = QStringLiteral( "CREATE TABLE 'log_geometry_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'geom_wkt' TEXT)" );
528  sqlExec( db, sql );
529 
530  /* TODO: other logging tables
531  - attr delete (not supported by SpatiaLite provider)
532  */
533 }
534 
535 void QgsOfflineEditing::convertToOfflineLayer( QgsVectorLayer *layer, sqlite3 *db, const QString &offlineDbPath, bool onlySelected, ContainerType containerType, const QString &layerNameSuffix )
536 {
537  if ( !layer || !layer->isValid() )
538  {
539  QgsDebugMsgLevel( QStringLiteral( "Layer %1 is invalid and cannot be copied" ).arg( layer ? layer->id() : QStringLiteral( "<UNKNOWN>" ) ), 4 );
540  return;
541  }
542 
543  const QString tableName = layer->id();
544  QgsDebugMsgLevel( QStringLiteral( "Creating offline table %1 ..." ).arg( tableName ), 4 );
545 
546  // new layer
547  std::unique_ptr<QgsVectorLayer> newLayer;
548 
549  switch ( containerType )
550  {
551  case SpatiaLite:
552  {
553 #ifdef HAVE_SPATIALITE
554  // create table
555  QString sql = QStringLiteral( "CREATE TABLE '%1' (" ).arg( tableName );
556  QString delim;
557  const QgsFields providerFields = layer->dataProvider()->fields();
558  for ( const auto &field : providerFields )
559  {
560  QString dataType;
561  const QVariant::Type type = field.type();
562  if ( type == QVariant::Int || type == QVariant::LongLong )
563  {
564  dataType = QStringLiteral( "INTEGER" );
565  }
566  else if ( type == QVariant::Double )
567  {
568  dataType = QStringLiteral( "REAL" );
569  }
570  else if ( type == QVariant::String )
571  {
572  dataType = QStringLiteral( "TEXT" );
573  }
574  else if ( type == QVariant::StringList || type == QVariant::List )
575  {
576  dataType = QStringLiteral( "TEXT" );
577  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() ) );
578  }
579  else
580  {
581  showWarning( tr( "%1: Unknown data type %2. Not using type affinity for the field." ).arg( field.name(), QVariant::typeToName( type ) ) );
582  }
583 
584  sql += delim + QStringLiteral( "'%1' %2" ).arg( field.name(), dataType );
585  delim = ',';
586  }
587  sql += ')';
588 
589  int rc = sqlExec( db, sql );
590 
591  // add geometry column
592  if ( layer->isSpatial() )
593  {
594  const QgsWkbTypes::Type sourceWkbType = layer->wkbType();
595 
596  QString geomType;
597  switch ( QgsWkbTypes::flatType( sourceWkbType ) )
598  {
599  case QgsWkbTypes::Point:
600  geomType = QStringLiteral( "POINT" );
601  break;
603  geomType = QStringLiteral( "MULTIPOINT" );
604  break;
606  geomType = QStringLiteral( "LINESTRING" );
607  break;
609  geomType = QStringLiteral( "MULTILINESTRING" );
610  break;
612  geomType = QStringLiteral( "POLYGON" );
613  break;
615  geomType = QStringLiteral( "MULTIPOLYGON" );
616  break;
617  default:
618  showWarning( tr( "Layer %1 has unsupported geometry type %2." ).arg( layer->name(), QgsWkbTypes::displayString( layer->wkbType() ) ) );
619  break;
620  };
621 
622  QString zmInfo = QStringLiteral( "XY" );
623 
624  if ( QgsWkbTypes::hasZ( sourceWkbType ) )
625  zmInfo += 'Z';
626  if ( QgsWkbTypes::hasM( sourceWkbType ) )
627  zmInfo += 'M';
628 
629  QString epsgCode;
630 
631  if ( layer->crs().authid().startsWith( QLatin1String( "EPSG:" ), Qt::CaseInsensitive ) )
632  {
633  epsgCode = layer->crs().authid().mid( 5 );
634  }
635  else
636  {
637  epsgCode = '0';
638  showWarning( tr( "Layer %1 has unsupported Coordinate Reference System (%2)." ).arg( layer->name(), layer->crs().authid() ) );
639  }
640 
641  const QString sqlAddGeom = QStringLiteral( "SELECT AddGeometryColumn('%1', 'Geometry', %2, '%3', '%4')" )
642  .arg( tableName, epsgCode, geomType, zmInfo );
643 
644  // create spatial index
645  const QString sqlCreateIndex = QStringLiteral( "SELECT CreateSpatialIndex('%1', 'Geometry')" ).arg( tableName );
646 
647  if ( rc == SQLITE_OK )
648  {
649  rc = sqlExec( db, sqlAddGeom );
650  if ( rc == SQLITE_OK )
651  {
652  rc = sqlExec( db, sqlCreateIndex );
653  }
654  }
655  }
656 
657  if ( rc != SQLITE_OK )
658  {
659  showWarning( tr( "Filling SpatiaLite for layer %1 failed" ).arg( layer->name() ) );
660  return;
661  }
662 
663  // add new layer
664  const QString connectionString = QStringLiteral( "dbname='%1' table='%2'%3 sql=" )
665  .arg( offlineDbPath,
666  tableName, layer->isSpatial() ? "(Geometry)" : "" );
668  newLayer = std::make_unique<QgsVectorLayer>( connectionString,
669  layer->name() + layerNameSuffix, QStringLiteral( "spatialite" ), options );
670  break;
671 
672 #else
673  showWarning( tr( "No Spatialite support available" ) );
674  return;
675 #endif
676  }
677 
678  case GPKG:
679  {
680  // Set options
681  char **options = nullptr;
682 
683  options = CSLSetNameValue( options, "OVERWRITE", "YES" );
684  options = CSLSetNameValue( options, "IDENTIFIER", tr( "%1 (offline)" ).arg( layer->id() ).toUtf8().constData() );
685  options = CSLSetNameValue( options, "DESCRIPTION", layer->dataComment().toUtf8().constData() );
686 
687  //the FID-name should not exist in the original data
688  const QString fidBase( QStringLiteral( "fid" ) );
689  QString fid = fidBase;
690  int counter = 1;
691  while ( layer->dataProvider()->fields().lookupField( fid ) >= 0 && counter < 10000 )
692  {
693  fid = fidBase + '_' + QString::number( counter );
694  counter++;
695  }
696  if ( counter == 10000 )
697  {
698  showWarning( tr( "Cannot make FID-name for GPKG " ) );
699  return;
700  }
701 
702  options = CSLSetNameValue( options, "FID", fid.toUtf8().constData() );
703 
704  if ( layer->isSpatial() )
705  {
706  options = CSLSetNameValue( options, "GEOMETRY_COLUMN", "geom" );
707  options = CSLSetNameValue( options, "SPATIAL_INDEX", "YES" );
708  }
709 
710  OGRSFDriverH hDriver = nullptr;
712  gdal::ogr_datasource_unique_ptr hDS( OGROpen( offlineDbPath.toUtf8().constData(), true, &hDriver ) );
713  OGRLayerH hLayer = OGR_DS_CreateLayer( hDS.get(), tableName.toUtf8().constData(), hSRS, static_cast<OGRwkbGeometryType>( layer->wkbType() ), options );
714  CSLDestroy( options );
715  if ( hSRS )
716  OSRRelease( hSRS );
717  if ( !hLayer )
718  {
719  showWarning( tr( "Creation of layer failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
720  return;
721  }
722 
723  const QgsFields providerFields = layer->dataProvider()->fields();
724  for ( const auto &field : providerFields )
725  {
726  const QString fieldName( field.name() );
727  const QVariant::Type type = field.type();
728  OGRFieldType ogrType( OFTString );
729  OGRFieldSubType ogrSubType = OFSTNone;
730  if ( type == QVariant::Int )
731  ogrType = OFTInteger;
732  else if ( type == QVariant::LongLong )
733  ogrType = OFTInteger64;
734  else if ( type == QVariant::Double )
735  ogrType = OFTReal;
736  else if ( type == QVariant::Time )
737  ogrType = OFTTime;
738  else if ( type == QVariant::Date )
739  ogrType = OFTDate;
740  else if ( type == QVariant::DateTime )
741  ogrType = OFTDateTime;
742  else if ( type == QVariant::Bool )
743  {
744  ogrType = OFTInteger;
745  ogrSubType = OFSTBoolean;
746  }
747  else if ( type == QVariant::StringList || type == QVariant::List )
748  {
749  ogrType = OFTString;
750  ogrSubType = OFSTJSON;
751  showWarning( tr( "Field '%1' from layer %2 has been converted from a list to a JSON-formatted string value." ).arg( fieldName, layer->name() ) );
752  }
753  else
754  ogrType = OFTString;
755 
756  const int ogrWidth = field.length();
757 
758  const gdal::ogr_field_def_unique_ptr fld( OGR_Fld_Create( fieldName.toUtf8().constData(), ogrType ) );
759  OGR_Fld_SetWidth( fld.get(), ogrWidth );
760  if ( ogrSubType != OFSTNone )
761  OGR_Fld_SetSubType( fld.get(), ogrSubType );
762 
763  if ( OGR_L_CreateField( hLayer, fld.get(), true ) != OGRERR_NONE )
764  {
765  showWarning( tr( "Creation of field %1 failed (OGR error: %2)" )
766  .arg( fieldName, QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
767  return;
768  }
769  }
770 
771  // In GDAL >= 2.0, the driver implements a deferred creation strategy, so
772  // issue a command that will force table creation
773  CPLErrorReset();
774  OGR_L_ResetReading( hLayer );
775  if ( CPLGetLastErrorType() != CE_None )
776  {
777  const QString msg( tr( "Creation of layer failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
778  showWarning( msg );
779  return;
780  }
781  hDS.reset();
782 
783  const QString uri = QStringLiteral( "%1|layername=%2" ).arg( offlineDbPath, tableName );
785  newLayer = std::make_unique<QgsVectorLayer>( uri, layer->name() + layerNameSuffix, QStringLiteral( "ogr" ), layerOptions );
786  break;
787  }
788  }
789 
790  if ( newLayer && newLayer->isValid() )
791  {
792 
793  // copy features
794  newLayer->startEditing();
795  QgsFeature f;
796 
797  QgsFeatureRequest req;
798 
799  if ( onlySelected )
800  {
801  const QgsFeatureIds selectedFids = layer->selectedFeatureIds();
802  if ( !selectedFids.isEmpty() )
803  req.setFilterFids( selectedFids );
804  }
805 
806  QgsFeatureIterator fit = layer->dataProvider()->getFeatures( req );
807 
809  {
811  }
812  else
813  {
815  }
816  long long featureCount = 1;
817  const int remotePkIdx = getLayerPkIdx( layer );
818 
819  QList<QgsFeatureId> remoteFeatureIds;
820  QStringList remoteFeaturePks;
821  while ( fit.nextFeature( f ) )
822  {
823  remoteFeatureIds << f.id();
824  remoteFeaturePks << ( remotePkIdx >= 0 ? f.attribute( remotePkIdx ).toString() : QString() );
825 
826  // NOTE: SpatiaLite provider ignores position of geometry column
827  // fill gap in QgsAttributeMap if geometry column is not last (WORKAROUND)
828  int column = 0;
829  const QgsAttributes attrs = f.attributes();
830  // on GPKG newAttrs has an addition FID attribute, so we have to add a dummy in the original set
831  QgsAttributes newAttrs( containerType == GPKG ? attrs.count() + 1 : attrs.count() );
832  for ( int it = 0; it < attrs.count(); ++it )
833  {
834  QVariant attr = attrs.at( it );
835  if ( layer->fields().at( it ).type() == QVariant::StringList || layer->fields().at( it ).type() == QVariant::List )
836  {
837  attr = QgsJsonUtils::encodeValue( attr );
838  }
839  newAttrs[column++] = attr;
840  }
841  f.setAttributes( newAttrs );
842 
843  newLayer->addFeature( f );
844 
845  emit progressUpdated( featureCount++ );
846  }
847  if ( newLayer->commitChanges() )
848  {
850  featureCount = 1;
851 
852  // update feature id lookup
853  const int layerId = getOrCreateLayerId( db, layer->id() );
854  QList<QgsFeatureId> offlineFeatureIds;
855 
856  QgsFeatureIterator fit = newLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setNoAttributes() );
857  while ( fit.nextFeature( f ) )
858  {
859  offlineFeatureIds << f.id();
860  }
861 
862  // NOTE: insert fids in this loop, as the db is locked during newLayer->nextFeature()
863  sqlExec( db, QStringLiteral( "BEGIN" ) );
864  const int remoteCount = remoteFeatureIds.size();
865  for ( int i = 0; i < remoteCount; i++ )
866  {
867  // Check if the online feature has been fetched (WFS download aborted for some reason)
868  if ( i < offlineFeatureIds.count() )
869  {
870  addFidLookup( db, layerId, offlineFeatureIds.at( i ), remoteFeatureIds.at( i ), remoteFeaturePks.at( i ) );
871  }
872  else
873  {
874  showWarning( tr( "Feature cannot be copied to the offline layer, please check if the online layer '%1' is still accessible." ).arg( layer->name() ) );
875  return;
876  }
877  emit progressUpdated( featureCount++ );
878  }
879  sqlExec( db, QStringLiteral( "COMMIT" ) );
880  }
881  else
882  {
883  showWarning( newLayer->commitErrors().join( QLatin1Char( '\n' ) ) );
884  }
885 
886  // mark as offline layer
888 
889  // store original layer source and information
893  layer->setCustomProperty( CUSTOM_PROPERTY_LAYERNAME_SUFFIX, layerNameSuffix );
894 
895  //remove constrainst of fields that use defaultValueClauses from provider on original
896  const QgsFields fields = layer->fields();
897  QStringList notNullFieldNames;
898  for ( const QgsField &field : fields )
899  {
900  if ( !layer->dataProvider()->defaultValueClause( layer->fields().fieldOriginIndex( layer->fields().indexOf( field.name() ) ) ).isEmpty() )
901  {
902  notNullFieldNames << field.name();
903  }
904  }
905 
906  layer->setDataSource( newLayer->source(), newLayer->name(), newLayer->dataProvider()->name() );
907 
908  for ( const QgsField &field : fields ) //QString &fieldName : fieldsToRemoveConstraint )
909  {
910  const int index = layer->fields().indexOf( field.name() );
911  if ( index > -1 )
912  {
913  // restore unique value constraints coming from original data provider
916 
917  // remove any undesired not null constraints coming from original data provider
918  if ( notNullFieldNames.contains( field.name() ) )
919  {
920  notNullFieldNames.removeAll( field.name() );
922  }
923  }
924  }
925 
926  setupLayer( layer );
927  }
928  return;
929 }
930 
931 void QgsOfflineEditing::applyAttributesAdded( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
932 {
933  Q_ASSERT( remoteLayer );
934 
935  const 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 );
936  QList<QgsField> fields = sqlQueryAttributesAdded( db, sql );
937 
938  const QgsVectorDataProvider *provider = remoteLayer->dataProvider();
939  const QList<QgsVectorDataProvider::NativeType> nativeTypes = provider->nativeTypes();
940 
941  // NOTE: uses last matching QVariant::Type of nativeTypes
942  QMap < QVariant::Type, QString /*typeName*/ > typeNameLookup;
943  for ( int i = 0; i < nativeTypes.size(); i++ )
944  {
945  const QgsVectorDataProvider::NativeType nativeType = nativeTypes.at( i );
946  typeNameLookup[ nativeType.mType ] = nativeType.mTypeName;
947  }
948 
949  emit progressModeSet( QgsOfflineEditing::AddFields, fields.size() );
950 
951  for ( int i = 0; i < fields.size(); i++ )
952  {
953  // lookup typename from layer provider
954  QgsField field = fields[i];
955  if ( typeNameLookup.contains( field.type() ) )
956  {
957  const QString typeName = typeNameLookup[ field.type()];
959  remoteLayer->addAttribute( field );
960  }
961  else
962  {
963  showWarning( QStringLiteral( "Could not add attribute '%1' of type %2" ).arg( field.name() ).arg( field.type() ) );
964  }
965 
966  emit progressUpdated( i + 1 );
967  }
968 }
969 
970 void QgsOfflineEditing::applyFeaturesAdded( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
971 {
972  Q_ASSERT( offlineLayer );
973  Q_ASSERT( remoteLayer );
974 
975  const QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
976  const QList<int> featureIdInts = sqlQueryInts( db, sql );
977  QgsFeatureIds newFeatureIds;
978  for ( const int id : featureIdInts )
979  {
980  newFeatureIds << id;
981  }
982 
983  QgsExpressionContext context = remoteLayer->createExpressionContext();
984 
985  // get new features from offline layer
986  QgsFeatureList features;
987  QgsFeatureIterator it = offlineLayer->getFeatures( QgsFeatureRequest().setFilterFids( newFeatureIds ) );
988  QgsFeature feature;
989  while ( it.nextFeature( feature ) )
990  {
991  features << feature;
992  }
993 
994  // copy features to remote layer
995  emit progressModeSet( QgsOfflineEditing::AddFeatures, features.size() );
996 
997  int i = 1;
998  const int newAttrsCount = remoteLayer->fields().count();
999  for ( QgsFeatureList::iterator it = features.begin(); it != features.end(); ++it )
1000  {
1001  // NOTE: SpatiaLite provider ignores position of geometry column
1002  // restore gap in QgsAttributeMap if geometry column is not last (WORKAROUND)
1003  const QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
1004  QgsAttributes newAttrs( newAttrsCount );
1005  const QgsAttributes attrs = it->attributes();
1006  for ( int it = 0; it < attrs.count(); ++it )
1007  {
1008  const int remoteAttributeIndex = attrLookup.value( it, -1 );
1009  // if virtual or non existing field
1010  if ( remoteAttributeIndex == -1 )
1011  continue;
1012  QVariant attr = attrs.at( it );
1013  if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::StringList )
1014  {
1015  if ( attr.type() == QVariant::StringList || attr.type() == QVariant::List )
1016  {
1017  attr = attr.toStringList();
1018  }
1019  else
1020  {
1021  attr = QgsJsonUtils::parseArray( attr.toString(), QVariant::String );
1022  }
1023  }
1024  else if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::List )
1025  {
1026  if ( attr.type() == QVariant::StringList || attr.type() == QVariant::List )
1027  {
1028  attr = attr.toList();
1029  }
1030  else
1031  {
1032  attr = QgsJsonUtils::parseArray( attr.toString(), remoteLayer->fields().at( remoteAttributeIndex ).subType() );
1033  }
1034  }
1035  newAttrs[ remoteAttributeIndex ] = attr;
1036  }
1037 
1038  // respect constraints and provider default values
1039  QgsFeature f = QgsVectorLayerUtils::createFeature( remoteLayer, it->geometry(), newAttrs.toMap(), &context );
1040  remoteLayer->addFeature( f );
1041 
1042  emit progressUpdated( i++ );
1043  }
1044 }
1045 
1046 void QgsOfflineEditing::applyFeaturesRemoved( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
1047 {
1048  Q_ASSERT( remoteLayer );
1049 
1050  const QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( layerId );
1051  const QgsFeatureIds values = sqlQueryFeaturesRemoved( db, sql );
1052 
1053  emit progressModeSet( QgsOfflineEditing::RemoveFeatures, values.size() );
1054 
1055  int i = 1;
1056  for ( QgsFeatureIds::const_iterator it = values.constBegin(); it != values.constEnd(); ++it )
1057  {
1058  const QgsFeatureId fid = remoteFid( db, layerId, *it, remoteLayer );
1059  remoteLayer->deleteFeature( fid );
1060 
1061  emit progressUpdated( i++ );
1062  }
1063 }
1064 
1065 void QgsOfflineEditing::applyAttributeValueChanges( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
1066 {
1067  Q_ASSERT( offlineLayer );
1068  Q_ASSERT( remoteLayer );
1069 
1070  const QString sql = QStringLiteral( "SELECT \"fid\", \"attr\", \"value\" FROM 'log_feature_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2 " ).arg( layerId ).arg( commitNo );
1071  const AttributeValueChanges values = sqlQueryAttributeValueChanges( db, sql );
1072 
1073  emit progressModeSet( QgsOfflineEditing::UpdateFeatures, values.size() );
1074 
1075  QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
1076 
1077  for ( int i = 0; i < values.size(); i++ )
1078  {
1079  const QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid, remoteLayer );
1080  QgsDebugMsgLevel( QStringLiteral( "Offline changeAttributeValue %1 = %2" ).arg( attrLookup[ values.at( i ).attr ] ).arg( values.at( i ).value ), 4 );
1081 
1082  const int remoteAttributeIndex = attrLookup[ values.at( i ).attr ];
1083  QVariant attr = values.at( i ).value;
1084  if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::StringList )
1085  {
1086  attr = QgsJsonUtils::parseArray( attr.toString(), QVariant::String );
1087  }
1088  else if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::List )
1089  {
1090  attr = QgsJsonUtils::parseArray( attr.toString(), remoteLayer->fields().at( remoteAttributeIndex ).subType() );
1091  }
1092 
1093  remoteLayer->changeAttributeValue( fid, remoteAttributeIndex, attr );
1094 
1095  emit progressUpdated( i + 1 );
1096  }
1097 }
1098 
1099 void QgsOfflineEditing::applyGeometryChanges( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
1100 {
1101  Q_ASSERT( remoteLayer );
1102 
1103  const QString sql = QStringLiteral( "SELECT \"fid\", \"geom_wkt\" FROM 'log_geometry_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2" ).arg( layerId ).arg( commitNo );
1104  const GeometryChanges values = sqlQueryGeometryChanges( db, sql );
1105 
1107 
1108  for ( int i = 0; i < values.size(); i++ )
1109  {
1110  const QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid, remoteLayer );
1111  QgsGeometry newGeom = QgsGeometry::fromWkt( values.at( i ).geom_wkt );
1112  remoteLayer->changeGeometry( fid, newGeom );
1113 
1114  emit progressUpdated( i + 1 );
1115  }
1116 }
1117 
1118 void QgsOfflineEditing::updateFidLookup( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
1119 {
1120  Q_ASSERT( remoteLayer );
1121 
1122  // update fid lookup for added features
1123 
1124  // get remote added fids
1125  // NOTE: use QMap for sorted fids
1126  QMap < QgsFeatureId, QString > newRemoteFids;
1127  QgsFeature f;
1128 
1129  QgsFeatureIterator fit = remoteLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setNoAttributes() );
1130 
1132 
1133  const int remotePkIdx = getLayerPkIdx( remoteLayer );
1134 
1135  int i = 1;
1136  while ( fit.nextFeature( f ) )
1137  {
1138  if ( offlineFid( db, layerId, f.id() ) == -1 )
1139  {
1140  newRemoteFids[ f.id()] = remotePkIdx >= 0 ? f.attribute( remotePkIdx ).toString() : QString();
1141  }
1142 
1143  emit progressUpdated( i++ );
1144  }
1145 
1146  // get local added fids
1147  // NOTE: fids are sorted
1148  const QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
1149  const QList<int> newOfflineFids = sqlQueryInts( db, sql );
1150 
1151  if ( newRemoteFids.size() != newOfflineFids.size() )
1152  {
1153  //showWarning( QString( "Different number of new features on offline layer (%1) and remote layer (%2)" ).arg(newOfflineFids.size()).arg(newRemoteFids.size()) );
1154  }
1155  else
1156  {
1157  // add new fid lookups
1158  i = 0;
1159  sqlExec( db, QStringLiteral( "BEGIN" ) );
1160  for ( QMap<QgsFeatureId, QString>::const_iterator it = newRemoteFids.constBegin(); it != newRemoteFids.constEnd(); ++it )
1161  {
1162  addFidLookup( db, layerId, newOfflineFids.at( i++ ), it.key(), it.value() );
1163  }
1164  sqlExec( db, QStringLiteral( "COMMIT" ) );
1165  }
1166 }
1167 
1168 // NOTE: use this to map column indices in case the remote geometry column is not last
1169 QMap<int, int> QgsOfflineEditing::attributeLookup( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer )
1170 {
1171  Q_ASSERT( offlineLayer );
1172  Q_ASSERT( remoteLayer );
1173 
1174  const QgsAttributeList &offlineAttrs = offlineLayer->attributeList();
1175 
1176  QMap < int /*offline attr*/, int /*remote attr*/ > attrLookup;
1177  // NOTE: though offlineAttrs can have new attributes not yet synced, we take the amount of offlineAttrs
1178  // because we anyway only add mapping for the fields existing in remoteLayer (this because it could contain fid on 0)
1179  for ( int i = 0; i < offlineAttrs.size(); i++ )
1180  {
1181  if ( remoteLayer->fields().lookupField( offlineLayer->fields().field( i ).name() ) >= 0 )
1182  attrLookup.insert( offlineAttrs.at( i ), remoteLayer->fields().indexOf( offlineLayer->fields().field( i ).name() ) );
1183  }
1184 
1185  return attrLookup;
1186 }
1187 
1188 void QgsOfflineEditing::showWarning( const QString &message )
1189 {
1190  emit warning( tr( "Offline Editing Plugin" ), message );
1191 }
1192 
1193 sqlite3_database_unique_ptr QgsOfflineEditing::openLoggingDb()
1194 {
1195  sqlite3_database_unique_ptr database;
1197  if ( !dbPath.isEmpty() )
1198  {
1199  const QString absoluteDbPath = QgsProject::instance()->readPath( dbPath );
1200  const int rc = database.open( absoluteDbPath );
1201  if ( rc != SQLITE_OK )
1202  {
1203  QgsDebugMsg( QStringLiteral( "Could not open the SpatiaLite logging database" ) );
1204  showWarning( tr( "Could not open the SpatiaLite logging database" ) );
1205  }
1206  }
1207  else
1208  {
1209  QgsDebugMsg( QStringLiteral( "dbPath is empty!" ) );
1210  }
1211  return database;
1212 }
1213 
1214 int QgsOfflineEditing::getOrCreateLayerId( sqlite3 *db, const QString &qgisLayerId )
1215 {
1216  QString sql = QStringLiteral( "SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( qgisLayerId );
1217  int layerId = sqlQueryInt( db, sql, -1 );
1218  if ( layerId == -1 )
1219  {
1220  // next layer id
1221  sql = QStringLiteral( "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'layer_id'" );
1222  const int newLayerId = sqlQueryInt( db, sql, -1 );
1223 
1224  // insert layer
1225  sql = QStringLiteral( "INSERT INTO 'log_layer_ids' VALUES (%1, '%2')" ).arg( newLayerId ).arg( qgisLayerId );
1226  sqlExec( db, sql );
1227 
1228  // increase layer_id
1229  // TODO: use trigger for auto increment?
1230  sql = QStringLiteral( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'layer_id'" ).arg( newLayerId + 1 );
1231  sqlExec( db, sql );
1232 
1233  layerId = newLayerId;
1234  }
1235 
1236  return layerId;
1237 }
1238 
1239 int QgsOfflineEditing::getCommitNo( sqlite3 *db )
1240 {
1241  const QString sql = QStringLiteral( "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'commit_no'" );
1242  return sqlQueryInt( db, sql, -1 );
1243 }
1244 
1245 void QgsOfflineEditing::increaseCommitNo( sqlite3 *db )
1246 {
1247  const QString sql = QStringLiteral( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'commit_no'" ).arg( getCommitNo( db ) + 1 );
1248  sqlExec( db, sql );
1249 }
1250 
1251 void QgsOfflineEditing::addFidLookup( sqlite3 *db, int layerId, QgsFeatureId offlineFid, QgsFeatureId remoteFid, const QString &remotePk )
1252 {
1253  const QString sql = QStringLiteral( "INSERT INTO 'log_fids' VALUES ( %1, %2, %3, %4 )" ).arg( layerId ).arg( offlineFid ).arg( remoteFid ).arg( sqlEscape( remotePk ) );
1254  sqlExec( db, sql );
1255 }
1256 
1257 QgsFeatureId QgsOfflineEditing::remoteFid( sqlite3 *db, int layerId, QgsFeatureId offlineFid, QgsVectorLayer *remoteLayer )
1258 {
1259  const int pkIdx = getLayerPkIdx( remoteLayer );
1260 
1261  if ( pkIdx == -1 )
1262  {
1263  const QString sql = QStringLiteral( "SELECT \"remote_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"offline_fid\" = %2" ).arg( layerId ).arg( offlineFid );
1264  return sqlQueryInt( db, sql, -1 );
1265  }
1266 
1267  const QString sql = QStringLiteral( "SELECT \"remote_pk\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"offline_fid\" = %2" ).arg( layerId ).arg( offlineFid );
1268  QString defaultValue;
1269  const QString pkValue = sqlQueryStr( db, sql, defaultValue );
1270 
1271  if ( pkValue.isNull() )
1272  {
1273  return -1;
1274  }
1275 
1276  const QString pkFieldName = remoteLayer->fields().at( pkIdx ).name();
1277  QgsFeatureIterator fit = remoteLayer->getFeatures( QStringLiteral( " %1 = %2 " ).arg( pkFieldName ).arg( sqlEscape( pkValue ) ) );
1278  QgsFeature f;
1279  while ( fit.nextFeature( f ) )
1280  return f.id();
1281 
1282  return -1;
1283 }
1284 
1285 QgsFeatureId QgsOfflineEditing::offlineFid( sqlite3 *db, int layerId, QgsFeatureId remoteFid )
1286 {
1287  const QString sql = QStringLiteral( "SELECT \"offline_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"remote_fid\" = %2" ).arg( layerId ).arg( remoteFid );
1288  return sqlQueryInt( db, sql, -1 );
1289 }
1290 
1291 bool QgsOfflineEditing::isAddedFeature( sqlite3 *db, int layerId, QgsFeatureId fid )
1292 {
1293  const QString sql = QStringLiteral( "SELECT COUNT(\"fid\") FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( fid );
1294  return ( sqlQueryInt( db, sql, 0 ) > 0 );
1295 }
1296 
1297 int QgsOfflineEditing::sqlExec( sqlite3 *db, const QString &sql )
1298 {
1299  char *errmsg = nullptr;
1300  const int rc = sqlite3_exec( db, sql.toUtf8(), nullptr, nullptr, &errmsg );
1301  if ( rc != SQLITE_OK )
1302  {
1303  showWarning( errmsg );
1304  }
1305  return rc;
1306 }
1307 
1308 QString QgsOfflineEditing::sqlQueryStr( sqlite3 *db, const QString &sql, QString &defaultValue )
1309 {
1310  sqlite3_stmt *stmt = nullptr;
1311  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1312  {
1313  showWarning( sqlite3_errmsg( db ) );
1314  return defaultValue;
1315  }
1316 
1317  QString value = defaultValue;
1318  const int ret = sqlite3_step( stmt );
1319  if ( ret == SQLITE_ROW )
1320  {
1321  value = QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 0 ) ) );
1322  }
1323  sqlite3_finalize( stmt );
1324 
1325  return value;
1326 }
1327 
1328 int QgsOfflineEditing::sqlQueryInt( sqlite3 *db, const QString &sql, int defaultValue )
1329 {
1330  sqlite3_stmt *stmt = nullptr;
1331  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1332  {
1333  showWarning( sqlite3_errmsg( db ) );
1334  return defaultValue;
1335  }
1336 
1337  int value = defaultValue;
1338  const int ret = sqlite3_step( stmt );
1339  if ( ret == SQLITE_ROW )
1340  {
1341  value = sqlite3_column_int( stmt, 0 );
1342  }
1343  sqlite3_finalize( stmt );
1344 
1345  return value;
1346 }
1347 
1348 QList<int> QgsOfflineEditing::sqlQueryInts( sqlite3 *db, const QString &sql )
1349 {
1350  QList<int> values;
1351 
1352  sqlite3_stmt *stmt = nullptr;
1353  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1354  {
1355  showWarning( sqlite3_errmsg( db ) );
1356  return values;
1357  }
1358 
1359  int ret = sqlite3_step( stmt );
1360  while ( ret == SQLITE_ROW )
1361  {
1362  values << sqlite3_column_int( stmt, 0 );
1363 
1364  ret = sqlite3_step( stmt );
1365  }
1366  sqlite3_finalize( stmt );
1367 
1368  return values;
1369 }
1370 
1371 QList<QgsField> QgsOfflineEditing::sqlQueryAttributesAdded( sqlite3 *db, const QString &sql )
1372 {
1373  QList<QgsField> values;
1374 
1375  sqlite3_stmt *stmt = nullptr;
1376  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1377  {
1378  showWarning( sqlite3_errmsg( db ) );
1379  return values;
1380  }
1381 
1382  int ret = sqlite3_step( stmt );
1383  while ( ret == SQLITE_ROW )
1384  {
1385  const QgsField field( QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 0 ) ) ),
1386  static_cast< QVariant::Type >( sqlite3_column_int( stmt, 1 ) ),
1387  QString(), // typeName
1388  sqlite3_column_int( stmt, 2 ),
1389  sqlite3_column_int( stmt, 3 ),
1390  QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 4 ) ) ) );
1391  values << field;
1392 
1393  ret = sqlite3_step( stmt );
1394  }
1395  sqlite3_finalize( stmt );
1396 
1397  return values;
1398 }
1399 
1400 QgsFeatureIds QgsOfflineEditing::sqlQueryFeaturesRemoved( sqlite3 *db, const QString &sql )
1401 {
1402  QgsFeatureIds values;
1403 
1404  sqlite3_stmt *stmt = nullptr;
1405  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1406  {
1407  showWarning( sqlite3_errmsg( db ) );
1408  return values;
1409  }
1410 
1411  int ret = sqlite3_step( stmt );
1412  while ( ret == SQLITE_ROW )
1413  {
1414  values << sqlite3_column_int( stmt, 0 );
1415 
1416  ret = sqlite3_step( stmt );
1417  }
1418  sqlite3_finalize( stmt );
1419 
1420  return values;
1421 }
1422 
1423 QgsOfflineEditing::AttributeValueChanges QgsOfflineEditing::sqlQueryAttributeValueChanges( sqlite3 *db, const QString &sql )
1424 {
1425  AttributeValueChanges values;
1426 
1427  sqlite3_stmt *stmt = nullptr;
1428  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1429  {
1430  showWarning( sqlite3_errmsg( db ) );
1431  return values;
1432  }
1433 
1434  int ret = sqlite3_step( stmt );
1435  while ( ret == SQLITE_ROW )
1436  {
1437  AttributeValueChange change;
1438  change.fid = sqlite3_column_int( stmt, 0 );
1439  change.attr = sqlite3_column_int( stmt, 1 );
1440  change.value = QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 2 ) ) );
1441  values << change;
1442 
1443  ret = sqlite3_step( stmt );
1444  }
1445  sqlite3_finalize( stmt );
1446 
1447  return values;
1448 }
1449 
1450 QgsOfflineEditing::GeometryChanges QgsOfflineEditing::sqlQueryGeometryChanges( sqlite3 *db, const QString &sql )
1451 {
1452  GeometryChanges values;
1453 
1454  sqlite3_stmt *stmt = nullptr;
1455  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1456  {
1457  showWarning( sqlite3_errmsg( db ) );
1458  return values;
1459  }
1460 
1461  int ret = sqlite3_step( stmt );
1462  while ( ret == SQLITE_ROW )
1463  {
1464  GeometryChange change;
1465  change.fid = sqlite3_column_int( stmt, 0 );
1466  change.geom_wkt = QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 1 ) ) );
1467  values << change;
1468 
1469  ret = sqlite3_step( stmt );
1470  }
1471  sqlite3_finalize( stmt );
1472 
1473  return values;
1474 }
1475 
1476 void QgsOfflineEditing::committedAttributesAdded( const QString &qgisLayerId, const QList<QgsField> &addedAttributes )
1477 {
1478  const sqlite3_database_unique_ptr database = openLoggingDb();
1479  if ( !database )
1480  return;
1481 
1482  // insert log
1483  const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1484  const int commitNo = getCommitNo( database.get() );
1485 
1486  for ( const QgsField &field : addedAttributes )
1487  {
1488  const QString sql = QStringLiteral( "INSERT INTO 'log_added_attrs' VALUES ( %1, %2, '%3', %4, %5, %6, '%7' )" )
1489  .arg( layerId )
1490  .arg( commitNo )
1491  .arg( field.name() )
1492  .arg( field.type() )
1493  .arg( field.length() )
1494  .arg( field.precision() )
1495  .arg( field.comment() );
1496  sqlExec( database.get(), sql );
1497  }
1498 
1499  increaseCommitNo( database.get() );
1500 }
1501 
1502 void QgsOfflineEditing::committedFeaturesAdded( const QString &qgisLayerId, const QgsFeatureList &addedFeatures )
1503 {
1504  const sqlite3_database_unique_ptr database = openLoggingDb();
1505  if ( !database )
1506  return;
1507 
1508  // insert log
1509  const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1510 
1511  // get new feature ids from db
1512  QgsMapLayer *layer = QgsProject::instance()->mapLayer( qgisLayerId );
1513  const QString dataSourceString = layer->source();
1514  const QgsDataSourceUri uri = QgsDataSourceUri( dataSourceString );
1515 
1517  QString tableName;
1518 
1519  if ( !offlinePath.contains( ".gpkg" ) )
1520  {
1521  tableName = uri.table();
1522  }
1523  else
1524  {
1525  QgsProviderMetadata *ogrProviderMetaData = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "ogr" ) );
1526  const QVariantMap decodedUri = ogrProviderMetaData->decodeUri( dataSourceString );
1527  tableName = decodedUri.value( QStringLiteral( "layerName" ) ).toString();
1528  if ( tableName.isEmpty() )
1529  {
1530  showWarning( tr( "Could not deduce table name from data source %1." ).arg( dataSourceString ) );
1531  }
1532  }
1533 
1534  // only store feature ids
1535  const QString sql = QStringLiteral( "SELECT ROWID FROM '%1' ORDER BY ROWID DESC LIMIT %2" ).arg( tableName ).arg( addedFeatures.size() );
1536  const QList<int> newFeatureIds = sqlQueryInts( database.get(), sql );
1537  for ( int i = newFeatureIds.size() - 1; i >= 0; i-- )
1538  {
1539  const QString sql = QStringLiteral( "INSERT INTO 'log_added_features' VALUES ( %1, %2 )" )
1540  .arg( layerId )
1541  .arg( newFeatureIds.at( i ) );
1542  sqlExec( database.get(), sql );
1543  }
1544 }
1545 
1546 void QgsOfflineEditing::committedFeaturesRemoved( const QString &qgisLayerId, const QgsFeatureIds &deletedFeatureIds )
1547 {
1548  const sqlite3_database_unique_ptr database = openLoggingDb();
1549  if ( !database )
1550  return;
1551 
1552  // insert log
1553  const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1554 
1555  for ( const QgsFeatureId id : deletedFeatureIds )
1556  {
1557  if ( isAddedFeature( database.get(), layerId, id ) )
1558  {
1559  // remove from added features log
1560  const QString sql = QStringLiteral( "DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( id );
1561  sqlExec( database.get(), sql );
1562  }
1563  else
1564  {
1565  const QString sql = QStringLiteral( "INSERT INTO 'log_removed_features' VALUES ( %1, %2)" )
1566  .arg( layerId )
1567  .arg( id );
1568  sqlExec( database.get(), sql );
1569  }
1570  }
1571 }
1572 
1573 void QgsOfflineEditing::committedAttributeValuesChanges( const QString &qgisLayerId, const QgsChangedAttributesMap &changedAttrsMap )
1574 {
1575  const sqlite3_database_unique_ptr database = openLoggingDb();
1576  if ( !database )
1577  return;
1578 
1579  // insert log
1580  const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1581  const int commitNo = getCommitNo( database.get() );
1582 
1583  for ( QgsChangedAttributesMap::const_iterator cit = changedAttrsMap.begin(); cit != changedAttrsMap.end(); ++cit )
1584  {
1585  const QgsFeatureId fid = cit.key();
1586  if ( isAddedFeature( database.get(), layerId, fid ) )
1587  {
1588  // skip added features
1589  continue;
1590  }
1591  const QgsAttributeMap attrMap = cit.value();
1592  for ( QgsAttributeMap::const_iterator it = attrMap.constBegin(); it != attrMap.constEnd(); ++it )
1593  {
1594  QString value = it.value().type() == QVariant::StringList || it.value().type() == QVariant::List ? QgsJsonUtils::encodeValue( it.value() ) : it.value().toString();
1595  value.replace( QLatin1String( "'" ), QLatin1String( "''" ) ); // escape quote
1596  const QString sql = QStringLiteral( "INSERT INTO 'log_feature_updates' VALUES ( %1, %2, %3, %4, '%5' )" )
1597  .arg( layerId )
1598  .arg( commitNo )
1599  .arg( fid )
1600  .arg( it.key() ) // attribute
1601  .arg( value );
1602  sqlExec( database.get(), sql );
1603  }
1604  }
1605 
1606  increaseCommitNo( database.get() );
1607 }
1608 
1609 void QgsOfflineEditing::committedGeometriesChanges( const QString &qgisLayerId, const QgsGeometryMap &changedGeometries )
1610 {
1611  const sqlite3_database_unique_ptr database = openLoggingDb();
1612  if ( !database )
1613  return;
1614 
1615  // insert log
1616  const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1617  const int commitNo = getCommitNo( database.get() );
1618 
1619  for ( QgsGeometryMap::const_iterator it = changedGeometries.begin(); it != changedGeometries.end(); ++it )
1620  {
1621  const QgsFeatureId fid = it.key();
1622  if ( isAddedFeature( database.get(), layerId, fid ) )
1623  {
1624  // skip added features
1625  continue;
1626  }
1627  const QgsGeometry geom = it.value();
1628  const QString sql = QStringLiteral( "INSERT INTO 'log_geometry_updates' VALUES ( %1, %2, %3, '%4' )" )
1629  .arg( layerId )
1630  .arg( commitNo )
1631  .arg( fid )
1632  .arg( geom.asWkt() );
1633  sqlExec( database.get(), sql );
1634 
1635  // TODO: use WKB instead of WKT?
1636  }
1637 
1638  increaseCommitNo( database.get() );
1639 }
1640 
1641 void QgsOfflineEditing::startListenFeatureChanges()
1642 {
1643  QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1644 
1645  Q_ASSERT( vLayer );
1646 
1647  // enable logging, check if editBuffer is not null
1648  if ( vLayer->editBuffer() )
1649  {
1650  QgsVectorLayerEditBuffer *editBuffer = vLayer->editBuffer();
1652  this, &QgsOfflineEditing::committedAttributesAdded );
1654  this, &QgsOfflineEditing::committedAttributeValuesChanges );
1656  this, &QgsOfflineEditing::committedGeometriesChanges );
1657  }
1658  connect( vLayer, &QgsVectorLayer::committedFeaturesAdded,
1659  this, &QgsOfflineEditing::committedFeaturesAdded );
1660  connect( vLayer, &QgsVectorLayer::committedFeaturesRemoved,
1661  this, &QgsOfflineEditing::committedFeaturesRemoved );
1662 }
1663 
1664 void QgsOfflineEditing::stopListenFeatureChanges()
1665 {
1666  QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1667 
1668  Q_ASSERT( vLayer );
1669 
1670  // disable logging, check if editBuffer is not null
1671  if ( vLayer->editBuffer() )
1672  {
1673  QgsVectorLayerEditBuffer *editBuffer = vLayer->editBuffer();
1674  disconnect( editBuffer, &QgsVectorLayerEditBuffer::committedAttributesAdded,
1675  this, &QgsOfflineEditing::committedAttributesAdded );
1677  this, &QgsOfflineEditing::committedAttributeValuesChanges );
1679  this, &QgsOfflineEditing::committedGeometriesChanges );
1680  }
1681  disconnect( vLayer, &QgsVectorLayer::committedFeaturesAdded,
1682  this, &QgsOfflineEditing::committedFeaturesAdded );
1683  disconnect( vLayer, &QgsVectorLayer::committedFeaturesRemoved,
1684  this, &QgsOfflineEditing::committedFeaturesRemoved );
1685 }
1686 
1687 void QgsOfflineEditing::setupLayer( QgsMapLayer *layer )
1688 {
1689  Q_ASSERT( layer );
1690 
1691  if ( QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( layer ) )
1692  {
1693  // detect offline layer
1694  if ( vLayer->customProperty( CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE, false ).toBool() )
1695  {
1696  connect( vLayer, &QgsVectorLayer::editingStarted, this, &QgsOfflineEditing::startListenFeatureChanges );
1697  connect( vLayer, &QgsVectorLayer::editingStopped, this, &QgsOfflineEditing::stopListenFeatureChanges );
1698  }
1699  }
1700 }
1701 
1702 int QgsOfflineEditing::getLayerPkIdx( const QgsVectorLayer *layer ) const
1703 {
1704  const QList<int> pkAttrs = layer->primaryKeyAttributes();
1705  if ( pkAttrs.length() == 1 )
1706  {
1707  const QgsField pkField = layer->fields().at( pkAttrs[0] );
1708  const QVariant::Type pkType = pkField.type();
1709 
1710  if ( pkType == QVariant::String )
1711  {
1712  return pkAttrs[0];
1713  }
1714  }
1715 
1716  return -1;
1717 }
1718 
1719 QString QgsOfflineEditing::sqlEscape( QString value ) const
1720 {
1721  if ( value.isNull() )
1722  return QStringLiteral( "NULL" );
1723 
1724  value.replace( "'", "''" );
1725 
1726  return QStringLiteral( "'%1'" ).arg( value );
1727 }
A vector of attributes.
Definition: qgsattributes.h:58
QString authid() const
Returns the authority identifier for the 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 the 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 attribute/ID 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:153
QVariant attribute(const QString &name) const
Lookup attribute value by attribute name.
Definition: qgsfeature.cpp:320
Q_GADGET QgsFeatureId id
Definition: qgsfeature.h:64
@ ConstraintNotNull
Field may not be null.
@ ConstraintUnique
Field must have a unique value.
Q_GADGET Constraints constraints
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
QgsFieldConstraints constraints
Definition: qgsfield.h:63
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:125
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)
Base class for all map layer types.
Definition: qgsmaplayer.h:73
QString name
Definition: qgsmaplayer.h:76
void editingStopped()
Emitted when edited changes have been successfully written to the data provider.
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 removeCustomProperty(const QString &key)
Remove a custom property from layer.
void editingStarted()
Emitted when editing on this layer has started.
QgsCoordinateReferenceSystem crs
Definition: qgsmaplayer.h:79
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:81
void setDataSource(const QString &dataSource, const QString &baseName, const QString &provider, bool loadDefaultStyleFlag=false)
Updates the data source of the layer.
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 progressStopped()
Emitted when the processing of all layers has finished.
void synchronize(bool useTransaction=false)
Synchronize to remote layers.
ContainerType
Type of offline database container file.
void progressStarted()
Emitted when the process has started.
static OGRSpatialReferenceH crsToOGRSpatialReference(const QgsCoordinateReferenceSystem &crs)
Returns a OGRSpatialReferenceH corresponding to the specified crs object.
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 layerWasAdded(QgsMapLayer *layer)
Emitted when a layer was added to the registry.
QgsSnappingConfig snappingConfig
Definition: qgsproject.h:110
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.
QgsCoordinateTransformContext transformContext
Definition: qgsproject.h:107
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.
This is a container for configuration of the snapping of the project.
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)
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.
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...
void setFieldConstraint(int index, QgsFieldConstraints::Constraint constraint, QgsFieldConstraints::ConstraintStrength strength=QgsFieldConstraints::ConstraintStrengthHard)
Sets a constraint for a specified field index.
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 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.
QString dataComment() const
Returns a description for this layer as defined in the data provider.
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.
QgsAttributeList primaryKeyAttributes() const
Returns the list of attributes which make up the layer's primary keys.
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:1130
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:732
static bool hasZ(Type type) SIP_HOLDGIL
Tests whether a WKB type contains the z-dimension.
Definition: qgswkbtypes.h:1080
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:118
std::unique_ptr< std::remove_pointer< OGRFieldDefnH >::type, OGRFldDeleter > ogr_field_def_unique_ptr
Scoped OGR field definition.
Definition: qgsogrutils.h:128
QMap< int, QVariant > QgsAttributeMap
Definition: qgsattributes.h:38
struct sqlite3 sqlite3
void * OGRSpatialReferenceH
QMap< QgsFeatureId, QgsGeometry > QgsGeometryMap
Definition: qgsfeature.h:877
QMap< QgsFeatureId, QgsAttributeMap > QgsChangedAttributesMap
Definition: qgsfeature.h:868
QList< QgsFeature > QgsFeatureList
Definition: qgsfeature.h:882
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 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
const QString & typeName
Setting options for loading vector layers.