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