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