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