QGIS API Documentation  3.12.1-BucureČ™ti (121cc00ff0)
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( QgsCoordinateReferenceSystem::WKT2_2018 ).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  OGRFieldSubType ogrSubType = OFSTNone;
709  if ( type == QVariant::Int )
710  ogrType = OFTInteger;
711  else if ( type == QVariant::LongLong )
712  ogrType = OFTInteger64;
713  else if ( type == QVariant::Double )
714  ogrType = OFTReal;
715  else if ( type == QVariant::Time )
716  ogrType = OFTTime;
717  else if ( type == QVariant::Date )
718  ogrType = OFTDate;
719  else if ( type == QVariant::DateTime )
720  ogrType = OFTDateTime;
721  else if ( type == QVariant::Bool )
722  {
723  ogrType = OFTInteger;
724  ogrSubType = OFSTBoolean;
725  }
726  else
727  ogrType = OFTString;
728 
729  int ogrWidth = field.length();
730 
731  gdal::ogr_field_def_unique_ptr fld( OGR_Fld_Create( fieldName.toUtf8().constData(), ogrType ) );
732  OGR_Fld_SetWidth( fld.get(), ogrWidth );
733  if ( ogrSubType != OFSTNone )
734  OGR_Fld_SetSubType( fld.get(), ogrSubType );
735 
736  if ( OGR_L_CreateField( hLayer, fld.get(), true ) != OGRERR_NONE )
737  {
738  showWarning( tr( "Creation of field %1 failed (OGR error: %2)" )
739  .arg( fieldName, QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
740  return nullptr;
741  }
742  }
743 
744  // In GDAL >= 2.0, the driver implements a deferred creation strategy, so
745  // issue a command that will force table creation
746  CPLErrorReset();
747  OGR_L_ResetReading( hLayer );
748  if ( CPLGetLastErrorType() != CE_None )
749  {
750  QString msg( tr( "Creation of layer failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
751  showWarning( msg );
752  return nullptr;
753  }
754  hDS.reset();
755 
756  QString uri = QStringLiteral( "%1|layername=%2" ).arg( offlineDbPath, tableName );
758  newLayer = new QgsVectorLayer( uri, layer->name() + " (offline)", QStringLiteral( "ogr" ), layerOptions );
759  break;
760  }
761  }
762 
763  if ( newLayer->isValid() )
764  {
765 
766  // copy features
767  newLayer->startEditing();
768  QgsFeature f;
769 
770  QgsFeatureRequest req;
771 
772  if ( onlySelected )
773  {
774  QgsFeatureIds selectedFids = layer->selectedFeatureIds();
775  if ( !selectedFids.isEmpty() )
776  req.setFilterFids( selectedFids );
777  }
778 
779  QgsFeatureIterator fit = layer->dataProvider()->getFeatures( req );
780 
782  {
784  }
785  else
786  {
788  }
789  int featureCount = 1;
790 
791  QList<QgsFeatureId> remoteFeatureIds;
792  while ( fit.nextFeature( f ) )
793  {
794  remoteFeatureIds << f.id();
795 
796  // NOTE: SpatiaLite provider ignores position of geometry column
797  // fill gap in QgsAttributeMap if geometry column is not last (WORKAROUND)
798  int column = 0;
799  QgsAttributes attrs = f.attributes();
800  // on GPKG newAttrs has an addition FID attribute, so we have to add a dummy in the original set
801  QgsAttributes newAttrs( containerType == GPKG ? attrs.count() + 1 : attrs.count() );
802  for ( int it = 0; it < attrs.count(); ++it )
803  {
804  newAttrs[column++] = attrs.at( it );
805  }
806  f.setAttributes( newAttrs );
807 
808  newLayer->addFeature( f );
809 
810  emit progressUpdated( featureCount++ );
811  }
812  if ( newLayer->commitChanges() )
813  {
815  featureCount = 1;
816 
817  // update feature id lookup
818  int layerId = getOrCreateLayerId( db, newLayer->id() );
819  QList<QgsFeatureId> offlineFeatureIds;
820 
821  QgsFeatureIterator fit = newLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setNoAttributes() );
822  while ( fit.nextFeature( f ) )
823  {
824  offlineFeatureIds << f.id();
825  }
826 
827  // NOTE: insert fids in this loop, as the db is locked during newLayer->nextFeature()
828  sqlExec( db, QStringLiteral( "BEGIN" ) );
829  int remoteCount = remoteFeatureIds.size();
830  for ( int i = 0; i < remoteCount; i++ )
831  {
832  // Check if the online feature has been fetched (WFS download aborted for some reason)
833  if ( i < offlineFeatureIds.count() )
834  {
835  addFidLookup( db, layerId, offlineFeatureIds.at( i ), remoteFeatureIds.at( i ) );
836  }
837  else
838  {
839  showWarning( tr( "Feature cannot be copied to the offline layer, please check if the online layer '%1' is still accessible." ).arg( layer->name() ) );
840  return nullptr;
841  }
842  emit progressUpdated( featureCount++ );
843  }
844  sqlExec( db, QStringLiteral( "COMMIT" ) );
845  }
846  else
847  {
848  showWarning( newLayer->commitErrors().join( QStringLiteral( "\n" ) ) );
849  }
850 
851  // mark as offline layer
853 
854  // store original layer source
857 
858  // register this layer with the central layers registry
860  QList<QgsMapLayer *>() << newLayer );
861 
862  // copy style
863  copySymbology( layer, newLayer );
864 
865  //remove constrainst of fields that use defaultValueClauses from provider on original
866  const auto fields = layer->fields();
867  for ( const QgsField &field : fields )
868  {
869  if ( !layer->dataProvider()->defaultValueClause( layer->fields().fieldOriginIndex( layer->fields().indexOf( field.name() ) ) ).isEmpty() )
870  {
871  newLayer->removeFieldConstraint( newLayer->fields().indexOf( field.name() ), QgsFieldConstraints::ConstraintNotNull );
872  }
873  }
874 
876  // Find the parent group of the original layer
877  QgsLayerTreeLayer *layerTreeLayer = layerTreeRoot->findLayer( layer->id() );
878  if ( layerTreeLayer )
879  {
880  QgsLayerTreeGroup *parentTreeGroup = qobject_cast<QgsLayerTreeGroup *>( layerTreeLayer->parent() );
881  if ( parentTreeGroup )
882  {
883  int index = parentTreeGroup->children().indexOf( layerTreeLayer );
884  // Move the new layer from the root group to the new group
885  QgsLayerTreeLayer *newLayerTreeLayer = layerTreeRoot->findLayer( newLayer->id() );
886  if ( newLayerTreeLayer )
887  {
888  QgsLayerTreeNode *newLayerTreeLayerClone = newLayerTreeLayer->clone();
889  //copy the showFeatureCount property to the new node
890  newLayerTreeLayerClone->setCustomProperty( CUSTOM_SHOW_FEATURE_COUNT, layerTreeLayer->customProperty( CUSTOM_SHOW_FEATURE_COUNT ) );
891  newLayerTreeLayerClone->setItemVisibilityChecked( layerTreeLayer->isVisible() );
892  QgsLayerTreeGroup *grp = qobject_cast<QgsLayerTreeGroup *>( newLayerTreeLayer->parent() );
893  parentTreeGroup->insertChildNode( index, newLayerTreeLayerClone );
894  if ( grp )
895  grp->removeChildNode( newLayerTreeLayer );
896  }
897  }
898  }
899 
900  updateRelations( layer, newLayer );
901  updateMapThemes( layer, newLayer );
902  updateLayerOrder( layer, newLayer );
903 
904 
905 
906  }
907  return newLayer;
908 }
909 
910 void QgsOfflineEditing::applyAttributesAdded( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
911 {
912  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 );
913  QList<QgsField> fields = sqlQueryAttributesAdded( db, sql );
914 
915  const QgsVectorDataProvider *provider = remoteLayer->dataProvider();
916  QList<QgsVectorDataProvider::NativeType> nativeTypes = provider->nativeTypes();
917 
918  // NOTE: uses last matching QVariant::Type of nativeTypes
919  QMap < QVariant::Type, QString /*typeName*/ > typeNameLookup;
920  for ( int i = 0; i < nativeTypes.size(); i++ )
921  {
922  QgsVectorDataProvider::NativeType nativeType = nativeTypes.at( i );
923  typeNameLookup[ nativeType.mType ] = nativeType.mTypeName;
924  }
925 
926  emit progressModeSet( QgsOfflineEditing::AddFields, fields.size() );
927 
928  for ( int i = 0; i < fields.size(); i++ )
929  {
930  // lookup typename from layer provider
931  QgsField field = fields[i];
932  if ( typeNameLookup.contains( field.type() ) )
933  {
934  QString typeName = typeNameLookup[ field.type()];
935  field.setTypeName( typeName );
936  remoteLayer->addAttribute( field );
937  }
938  else
939  {
940  showWarning( QStringLiteral( "Could not add attribute '%1' of type %2" ).arg( field.name() ).arg( field.type() ) );
941  }
942 
943  emit progressUpdated( i + 1 );
944  }
945 }
946 
947 void QgsOfflineEditing::applyFeaturesAdded( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
948 {
949  QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
950  const QList<int> featureIdInts = sqlQueryInts( db, sql );
951  QgsFeatureIds newFeatureIds;
952  for ( int id : featureIdInts )
953  {
954  newFeatureIds << id;
955  }
956 
957  QgsExpressionContext context = remoteLayer->createExpressionContext();
958 
959  // get new features from offline layer
960  QgsFeatureList features;
961  QgsFeatureIterator it = offlineLayer->getFeatures( QgsFeatureRequest().setFilterFids( newFeatureIds ) );
962  QgsFeature feature;
963  while ( it.nextFeature( feature ) )
964  {
965  features << feature;
966  }
967 
968  // copy features to remote layer
969  emit progressModeSet( QgsOfflineEditing::AddFeatures, features.size() );
970 
971  int i = 1;
972  int newAttrsCount = remoteLayer->fields().count();
973  for ( QgsFeatureList::iterator it = features.begin(); it != features.end(); ++it )
974  {
975  // NOTE: SpatiaLite provider ignores position of geometry column
976  // restore gap in QgsAttributeMap if geometry column is not last (WORKAROUND)
977  QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
978  QgsAttributes newAttrs( newAttrsCount );
979  QgsAttributes attrs = it->attributes();
980  for ( int it = 0; it < attrs.count(); ++it )
981  {
982  newAttrs[ attrLookup[ it ] ] = attrs.at( it );
983  }
984 
985  // respect constraints and provider default values
986  QgsFeature f = QgsVectorLayerUtils::createFeature( remoteLayer, it->geometry(), newAttrs.toMap(), &context );
987  remoteLayer->addFeature( f );
988 
989  emit progressUpdated( i++ );
990  }
991 }
992 
993 void QgsOfflineEditing::applyFeaturesRemoved( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
994 {
995  QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( layerId );
996  QgsFeatureIds values = sqlQueryFeaturesRemoved( db, sql );
997 
998  emit progressModeSet( QgsOfflineEditing::RemoveFeatures, values.size() );
999 
1000  int i = 1;
1001  for ( QgsFeatureIds::const_iterator it = values.constBegin(); it != values.constEnd(); ++it )
1002  {
1003  QgsFeatureId fid = remoteFid( db, layerId, *it );
1004  remoteLayer->deleteFeature( fid );
1005 
1006  emit progressUpdated( i++ );
1007  }
1008 }
1009 
1010 void QgsOfflineEditing::applyAttributeValueChanges( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
1011 {
1012  QString sql = QStringLiteral( "SELECT \"fid\", \"attr\", \"value\" FROM 'log_feature_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2 " ).arg( layerId ).arg( commitNo );
1013  AttributeValueChanges values = sqlQueryAttributeValueChanges( db, sql );
1014 
1015  emit progressModeSet( QgsOfflineEditing::UpdateFeatures, values.size() );
1016 
1017  QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
1018 
1019  for ( int i = 0; i < values.size(); i++ )
1020  {
1021  QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid );
1022  QgsDebugMsgLevel( QStringLiteral( "Offline changeAttributeValue %1 = %2" ).arg( QString( attrLookup[ values.at( i ).attr ] ), values.at( i ).value ), 4 );
1023  remoteLayer->changeAttributeValue( fid, attrLookup[ values.at( i ).attr ], values.at( i ).value );
1024 
1025  emit progressUpdated( i + 1 );
1026  }
1027 }
1028 
1029 void QgsOfflineEditing::applyGeometryChanges( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
1030 {
1031  QString sql = QStringLiteral( "SELECT \"fid\", \"geom_wkt\" FROM 'log_geometry_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2" ).arg( layerId ).arg( commitNo );
1032  GeometryChanges values = sqlQueryGeometryChanges( db, sql );
1033 
1035 
1036  for ( int i = 0; i < values.size(); i++ )
1037  {
1038  QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid );
1039  QgsGeometry newGeom = QgsGeometry::fromWkt( values.at( i ).geom_wkt );
1040  remoteLayer->changeGeometry( fid, newGeom );
1041 
1042  emit progressUpdated( i + 1 );
1043  }
1044 }
1045 
1046 void QgsOfflineEditing::updateFidLookup( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
1047 {
1048  // update fid lookup for added features
1049 
1050  // get remote added fids
1051  // NOTE: use QMap for sorted fids
1052  QMap < QgsFeatureId, bool /*dummy*/ > newRemoteFids;
1053  QgsFeature f;
1054 
1055  QgsFeatureIterator fit = remoteLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setNoAttributes() );
1056 
1058 
1059  int i = 1;
1060  while ( fit.nextFeature( f ) )
1061  {
1062  if ( offlineFid( db, layerId, f.id() ) == -1 )
1063  {
1064  newRemoteFids[ f.id()] = true;
1065  }
1066 
1067  emit progressUpdated( i++ );
1068  }
1069 
1070  // get local added fids
1071  // NOTE: fids are sorted
1072  QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
1073  QList<int> newOfflineFids = sqlQueryInts( db, sql );
1074 
1075  if ( newRemoteFids.size() != newOfflineFids.size() )
1076  {
1077  //showWarning( QString( "Different number of new features on offline layer (%1) and remote layer (%2)" ).arg(newOfflineFids.size()).arg(newRemoteFids.size()) );
1078  }
1079  else
1080  {
1081  // add new fid lookups
1082  i = 0;
1083  sqlExec( db, QStringLiteral( "BEGIN" ) );
1084  for ( QMap<QgsFeatureId, bool>::const_iterator it = newRemoteFids.constBegin(); it != newRemoteFids.constEnd(); ++it )
1085  {
1086  addFidLookup( db, layerId, newOfflineFids.at( i++ ), it.key() );
1087  }
1088  sqlExec( db, QStringLiteral( "COMMIT" ) );
1089  }
1090 }
1091 
1092 void QgsOfflineEditing::copySymbology( QgsVectorLayer *sourceLayer, QgsVectorLayer *targetLayer )
1093 {
1094  QString error;
1095  QDomDocument doc;
1096  QgsReadWriteContext context;
1097  QgsMapLayer::StyleCategories categories = static_cast<QgsMapLayer::StyleCategories>( QgsMapLayer::AllStyleCategories ) & ~QgsMapLayer::CustomProperties;
1098  sourceLayer->exportNamedStyle( doc, error, context, categories );
1099 
1100  if ( error.isEmpty() )
1101  {
1102  targetLayer->importNamedStyle( doc, error, categories );
1103  }
1104  if ( !error.isEmpty() )
1105  {
1106  showWarning( error );
1107  }
1108 }
1109 
1110 void QgsOfflineEditing::updateRelations( QgsVectorLayer *sourceLayer, QgsVectorLayer *targetLayer )
1111 {
1113  const QList<QgsRelation> referencedRelations = relationManager->referencedRelations( sourceLayer );
1114 
1115  for ( QgsRelation relation : referencedRelations )
1116  {
1117  relationManager->removeRelation( relation );
1118  relation.setReferencedLayer( targetLayer->id() );
1119  relationManager->addRelation( relation );
1120  }
1121 
1122  const QList<QgsRelation> referencingRelations = relationManager->referencingRelations( sourceLayer );
1123 
1124  for ( QgsRelation relation : referencingRelations )
1125  {
1126  relationManager->removeRelation( relation );
1127  relation.setReferencingLayer( targetLayer->id() );
1128  relationManager->addRelation( relation );
1129  }
1130 }
1131 
1132 void QgsOfflineEditing::updateMapThemes( QgsVectorLayer *sourceLayer, QgsVectorLayer *targetLayer )
1133 {
1135  const QStringList mapThemeNames = mapThemeCollection->mapThemes();
1136 
1137  for ( const QString &mapThemeName : mapThemeNames )
1138  {
1139  QgsMapThemeCollection::MapThemeRecord record = mapThemeCollection->mapThemeState( mapThemeName );
1140 
1141  const auto layerRecords = record.layerRecords();
1142 
1143  for ( QgsMapThemeCollection::MapThemeLayerRecord layerRecord : layerRecords )
1144  {
1145  if ( layerRecord.layer() == sourceLayer )
1146  {
1147  layerRecord.setLayer( targetLayer );
1148  record.removeLayerRecord( sourceLayer );
1149  record.addLayerRecord( layerRecord );
1150  }
1151  }
1152 
1153  QgsProject::instance()->mapThemeCollection()->update( mapThemeName, record );
1154  }
1155 }
1156 
1157 void QgsOfflineEditing::updateLayerOrder( QgsVectorLayer *sourceLayer, QgsVectorLayer *targetLayer )
1158 {
1159  QList<QgsMapLayer *> layerOrder = QgsProject::instance()->layerTreeRoot()->customLayerOrder();
1160 
1161  auto iterator = layerOrder.begin();
1162 
1163  while ( iterator != layerOrder.end() )
1164  {
1165  if ( *iterator == targetLayer )
1166  {
1167  iterator = layerOrder.erase( iterator );
1168  if ( iterator == layerOrder.end() )
1169  break;
1170  }
1171 
1172  if ( *iterator == sourceLayer )
1173  {
1174  *iterator = targetLayer;
1175  }
1176 
1177  ++iterator;
1178  }
1179 
1181 }
1182 
1183 // NOTE: use this to map column indices in case the remote geometry column is not last
1184 QMap<int, int> QgsOfflineEditing::attributeLookup( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer )
1185 {
1186  const QgsAttributeList &offlineAttrs = offlineLayer->attributeList();
1187 
1188  QMap < int /*offline attr*/, int /*remote attr*/ > attrLookup;
1189  // NOTE: though offlineAttrs can have new attributes not yet synced, we take the amount of offlineAttrs
1190  // because we anyway only add mapping for the fields existing in remoteLayer (this because it could contain fid on 0)
1191  for ( int i = 0; i < offlineAttrs.size(); i++ )
1192  {
1193  if ( remoteLayer->fields().lookupField( offlineLayer->fields().field( i ).name() ) >= 0 )
1194  attrLookup.insert( offlineAttrs.at( i ), remoteLayer->fields().indexOf( offlineLayer->fields().field( i ).name() ) );
1195  }
1196 
1197  return attrLookup;
1198 }
1199 
1200 void QgsOfflineEditing::showWarning( const QString &message )
1201 {
1202  emit warning( tr( "Offline Editing Plugin" ), message );
1203 }
1204 
1205 sqlite3_database_unique_ptr QgsOfflineEditing::openLoggingDb()
1206 {
1207  sqlite3_database_unique_ptr database;
1209  if ( !dbPath.isEmpty() )
1210  {
1211  QString absoluteDbPath = QgsProject::instance()->readPath( dbPath );
1212  int rc = database.open( absoluteDbPath );
1213  if ( rc != SQLITE_OK )
1214  {
1215  QgsDebugMsg( QStringLiteral( "Could not open the SpatiaLite logging database" ) );
1216  showWarning( tr( "Could not open the SpatiaLite logging database" ) );
1217  }
1218  }
1219  else
1220  {
1221  QgsDebugMsg( QStringLiteral( "dbPath is empty!" ) );
1222  }
1223  return database;
1224 }
1225 
1226 int QgsOfflineEditing::getOrCreateLayerId( sqlite3 *db, const QString &qgisLayerId )
1227 {
1228  QString sql = QStringLiteral( "SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( qgisLayerId );
1229  int layerId = sqlQueryInt( db, sql, -1 );
1230  if ( layerId == -1 )
1231  {
1232  // next layer id
1233  sql = QStringLiteral( "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'layer_id'" );
1234  int newLayerId = sqlQueryInt( db, sql, -1 );
1235 
1236  // insert layer
1237  sql = QStringLiteral( "INSERT INTO 'log_layer_ids' VALUES (%1, '%2')" ).arg( newLayerId ).arg( qgisLayerId );
1238  sqlExec( db, sql );
1239 
1240  // increase layer_id
1241  // TODO: use trigger for auto increment?
1242  sql = QStringLiteral( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'layer_id'" ).arg( newLayerId + 1 );
1243  sqlExec( db, sql );
1244 
1245  layerId = newLayerId;
1246  }
1247 
1248  return layerId;
1249 }
1250 
1251 int QgsOfflineEditing::getCommitNo( sqlite3 *db )
1252 {
1253  QString sql = QStringLiteral( "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'commit_no'" );
1254  return sqlQueryInt( db, sql, -1 );
1255 }
1256 
1257 void QgsOfflineEditing::increaseCommitNo( sqlite3 *db )
1258 {
1259  QString sql = QStringLiteral( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'commit_no'" ).arg( getCommitNo( db ) + 1 );
1260  sqlExec( db, sql );
1261 }
1262 
1263 void QgsOfflineEditing::addFidLookup( sqlite3 *db, int layerId, QgsFeatureId offlineFid, QgsFeatureId remoteFid )
1264 {
1265  QString sql = QStringLiteral( "INSERT INTO 'log_fids' VALUES ( %1, %2, %3 )" ).arg( layerId ).arg( offlineFid ).arg( remoteFid );
1266  sqlExec( db, sql );
1267 }
1268 
1269 QgsFeatureId QgsOfflineEditing::remoteFid( sqlite3 *db, int layerId, QgsFeatureId offlineFid )
1270 {
1271  QString sql = QStringLiteral( "SELECT \"remote_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"offline_fid\" = %2" ).arg( layerId ).arg( offlineFid );
1272  return sqlQueryInt( db, sql, -1 );
1273 }
1274 
1275 QgsFeatureId QgsOfflineEditing::offlineFid( sqlite3 *db, int layerId, QgsFeatureId remoteFid )
1276 {
1277  QString sql = QStringLiteral( "SELECT \"offline_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"remote_fid\" = %2" ).arg( layerId ).arg( remoteFid );
1278  return sqlQueryInt( db, sql, -1 );
1279 }
1280 
1281 bool QgsOfflineEditing::isAddedFeature( sqlite3 *db, int layerId, QgsFeatureId fid )
1282 {
1283  QString sql = QStringLiteral( "SELECT COUNT(\"fid\") FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( fid );
1284  return ( sqlQueryInt( db, sql, 0 ) > 0 );
1285 }
1286 
1287 int QgsOfflineEditing::sqlExec( sqlite3 *db, const QString &sql )
1288 {
1289  char *errmsg = nullptr;
1290  int rc = sqlite3_exec( db, sql.toUtf8(), nullptr, nullptr, &errmsg );
1291  if ( rc != SQLITE_OK )
1292  {
1293  showWarning( errmsg );
1294  }
1295  return rc;
1296 }
1297 
1298 int QgsOfflineEditing::sqlQueryInt( sqlite3 *db, const QString &sql, int defaultValue )
1299 {
1300  sqlite3_stmt *stmt = nullptr;
1301  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1302  {
1303  showWarning( sqlite3_errmsg( db ) );
1304  return defaultValue;
1305  }
1306 
1307  int value = defaultValue;
1308  int ret = sqlite3_step( stmt );
1309  if ( ret == SQLITE_ROW )
1310  {
1311  value = sqlite3_column_int( stmt, 0 );
1312  }
1313  sqlite3_finalize( stmt );
1314 
1315  return value;
1316 }
1317 
1318 QList<int> QgsOfflineEditing::sqlQueryInts( sqlite3 *db, const QString &sql )
1319 {
1320  QList<int> values;
1321 
1322  sqlite3_stmt *stmt = nullptr;
1323  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1324  {
1325  showWarning( sqlite3_errmsg( db ) );
1326  return values;
1327  }
1328 
1329  int ret = sqlite3_step( stmt );
1330  while ( ret == SQLITE_ROW )
1331  {
1332  values << sqlite3_column_int( stmt, 0 );
1333 
1334  ret = sqlite3_step( stmt );
1335  }
1336  sqlite3_finalize( stmt );
1337 
1338  return values;
1339 }
1340 
1341 QList<QgsField> QgsOfflineEditing::sqlQueryAttributesAdded( sqlite3 *db, const QString &sql )
1342 {
1343  QList<QgsField> values;
1344 
1345  sqlite3_stmt *stmt = nullptr;
1346  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1347  {
1348  showWarning( sqlite3_errmsg( db ) );
1349  return values;
1350  }
1351 
1352  int ret = sqlite3_step( stmt );
1353  while ( ret == SQLITE_ROW )
1354  {
1355  QgsField field( QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 0 ) ) ),
1356  static_cast< QVariant::Type >( sqlite3_column_int( stmt, 1 ) ),
1357  QString(), // typeName
1358  sqlite3_column_int( stmt, 2 ),
1359  sqlite3_column_int( stmt, 3 ),
1360  QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 4 ) ) ) );
1361  values << field;
1362 
1363  ret = sqlite3_step( stmt );
1364  }
1365  sqlite3_finalize( stmt );
1366 
1367  return values;
1368 }
1369 
1370 QgsFeatureIds QgsOfflineEditing::sqlQueryFeaturesRemoved( sqlite3 *db, const QString &sql )
1371 {
1372  QgsFeatureIds values;
1373 
1374  sqlite3_stmt *stmt = nullptr;
1375  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1376  {
1377  showWarning( sqlite3_errmsg( db ) );
1378  return values;
1379  }
1380 
1381  int ret = sqlite3_step( stmt );
1382  while ( ret == SQLITE_ROW )
1383  {
1384  values << sqlite3_column_int( stmt, 0 );
1385 
1386  ret = sqlite3_step( stmt );
1387  }
1388  sqlite3_finalize( stmt );
1389 
1390  return values;
1391 }
1392 
1393 QgsOfflineEditing::AttributeValueChanges QgsOfflineEditing::sqlQueryAttributeValueChanges( sqlite3 *db, const QString &sql )
1394 {
1395  AttributeValueChanges values;
1396 
1397  sqlite3_stmt *stmt = nullptr;
1398  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1399  {
1400  showWarning( sqlite3_errmsg( db ) );
1401  return values;
1402  }
1403 
1404  int ret = sqlite3_step( stmt );
1405  while ( ret == SQLITE_ROW )
1406  {
1407  AttributeValueChange change;
1408  change.fid = sqlite3_column_int( stmt, 0 );
1409  change.attr = sqlite3_column_int( stmt, 1 );
1410  change.value = QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 2 ) ) );
1411  values << change;
1412 
1413  ret = sqlite3_step( stmt );
1414  }
1415  sqlite3_finalize( stmt );
1416 
1417  return values;
1418 }
1419 
1420 QgsOfflineEditing::GeometryChanges QgsOfflineEditing::sqlQueryGeometryChanges( sqlite3 *db, const QString &sql )
1421 {
1422  GeometryChanges values;
1423 
1424  sqlite3_stmt *stmt = nullptr;
1425  if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1426  {
1427  showWarning( sqlite3_errmsg( db ) );
1428  return values;
1429  }
1430 
1431  int ret = sqlite3_step( stmt );
1432  while ( ret == SQLITE_ROW )
1433  {
1434  GeometryChange change;
1435  change.fid = sqlite3_column_int( stmt, 0 );
1436  change.geom_wkt = QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 1 ) ) );
1437  values << change;
1438 
1439  ret = sqlite3_step( stmt );
1440  }
1441  sqlite3_finalize( stmt );
1442 
1443  return values;
1444 }
1445 
1446 void QgsOfflineEditing::committedAttributesAdded( const QString &qgisLayerId, const QList<QgsField> &addedAttributes )
1447 {
1448  sqlite3_database_unique_ptr database = openLoggingDb();
1449  if ( !database )
1450  return;
1451 
1452  // insert log
1453  int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1454  int commitNo = getCommitNo( database.get() );
1455 
1456  for ( QList<QgsField>::const_iterator it = addedAttributes.begin(); it != addedAttributes.end(); ++it )
1457  {
1458  QgsField field = *it;
1459  QString sql = QStringLiteral( "INSERT INTO 'log_added_attrs' VALUES ( %1, %2, '%3', %4, %5, %6, '%7' )" )
1460  .arg( layerId )
1461  .arg( commitNo )
1462  .arg( field.name() )
1463  .arg( field.type() )
1464  .arg( field.length() )
1465  .arg( field.precision() )
1466  .arg( field.comment() );
1467  sqlExec( database.get(), sql );
1468  }
1469 
1470  increaseCommitNo( database.get() );
1471 }
1472 
1473 void QgsOfflineEditing::committedFeaturesAdded( const QString &qgisLayerId, const QgsFeatureList &addedFeatures )
1474 {
1475  sqlite3_database_unique_ptr database = openLoggingDb();
1476  if ( !database )
1477  return;
1478 
1479  // insert log
1480  int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1481 
1482  // get new feature ids from db
1483  QgsMapLayer *layer = QgsProject::instance()->mapLayer( qgisLayerId );
1484  QgsDataSourceUri uri = QgsDataSourceUri( layer->source() );
1485 
1487  QString tableName;
1488 
1489  if ( !offlinePath.contains( ".gpkg" ) )
1490  {
1491  tableName = uri.table();
1492  }
1493  else
1494  {
1495  tableName = uri.param( offlinePath + "|layername" );
1496  }
1497 
1498  // only store feature ids
1499  QString sql = QStringLiteral( "SELECT ROWID FROM '%1' ORDER BY ROWID DESC LIMIT %2" ).arg( tableName ).arg( addedFeatures.size() );
1500  QList<int> newFeatureIds = sqlQueryInts( database.get(), sql );
1501  for ( int i = newFeatureIds.size() - 1; i >= 0; i-- )
1502  {
1503  QString sql = QStringLiteral( "INSERT INTO 'log_added_features' VALUES ( %1, %2 )" )
1504  .arg( layerId )
1505  .arg( newFeatureIds.at( i ) );
1506  sqlExec( database.get(), sql );
1507  }
1508 }
1509 
1510 void QgsOfflineEditing::committedFeaturesRemoved( const QString &qgisLayerId, const QgsFeatureIds &deletedFeatureIds )
1511 {
1512  sqlite3_database_unique_ptr database = openLoggingDb();
1513  if ( !database )
1514  return;
1515 
1516  // insert log
1517  int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1518 
1519  for ( QgsFeatureIds::const_iterator it = deletedFeatureIds.begin(); it != deletedFeatureIds.end(); ++it )
1520  {
1521  if ( isAddedFeature( database.get(), layerId, *it ) )
1522  {
1523  // remove from added features log
1524  QString sql = QStringLiteral( "DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( *it );
1525  sqlExec( database.get(), sql );
1526  }
1527  else
1528  {
1529  QString sql = QStringLiteral( "INSERT INTO 'log_removed_features' VALUES ( %1, %2)" )
1530  .arg( layerId )
1531  .arg( *it );
1532  sqlExec( database.get(), sql );
1533  }
1534  }
1535 }
1536 
1537 void QgsOfflineEditing::committedAttributeValuesChanges( const QString &qgisLayerId, const QgsChangedAttributesMap &changedAttrsMap )
1538 {
1539  sqlite3_database_unique_ptr database = openLoggingDb();
1540  if ( !database )
1541  return;
1542 
1543  // insert log
1544  int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1545  int commitNo = getCommitNo( database.get() );
1546 
1547  for ( QgsChangedAttributesMap::const_iterator cit = changedAttrsMap.begin(); cit != changedAttrsMap.end(); ++cit )
1548  {
1549  QgsFeatureId fid = cit.key();
1550  if ( isAddedFeature( database.get(), layerId, fid ) )
1551  {
1552  // skip added features
1553  continue;
1554  }
1555  QgsAttributeMap attrMap = cit.value();
1556  for ( QgsAttributeMap::const_iterator it = attrMap.constBegin(); it != attrMap.constEnd(); ++it )
1557  {
1558  QString sql = QStringLiteral( "INSERT INTO 'log_feature_updates' VALUES ( %1, %2, %3, %4, '%5' )" )
1559  .arg( layerId )
1560  .arg( commitNo )
1561  .arg( fid )
1562  .arg( it.key() ) // attr
1563  .arg( it.value().toString() ); // value
1564  sqlExec( database.get(), sql );
1565  }
1566  }
1567 
1568  increaseCommitNo( database.get() );
1569 }
1570 
1571 void QgsOfflineEditing::committedGeometriesChanges( const QString &qgisLayerId, const QgsGeometryMap &changedGeometries )
1572 {
1573  sqlite3_database_unique_ptr database = openLoggingDb();
1574  if ( !database )
1575  return;
1576 
1577  // insert log
1578  int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1579  int commitNo = getCommitNo( database.get() );
1580 
1581  for ( QgsGeometryMap::const_iterator it = changedGeometries.begin(); it != changedGeometries.end(); ++it )
1582  {
1583  QgsFeatureId fid = it.key();
1584  if ( isAddedFeature( database.get(), layerId, fid ) )
1585  {
1586  // skip added features
1587  continue;
1588  }
1589  QgsGeometry geom = it.value();
1590  QString sql = QStringLiteral( "INSERT INTO 'log_geometry_updates' VALUES ( %1, %2, %3, '%4' )" )
1591  .arg( layerId )
1592  .arg( commitNo )
1593  .arg( fid )
1594  .arg( geom.asWkt() );
1595  sqlExec( database.get(), sql );
1596 
1597  // TODO: use WKB instead of WKT?
1598  }
1599 
1600  increaseCommitNo( database.get() );
1601 }
1602 
1603 void QgsOfflineEditing::startListenFeatureChanges()
1604 {
1605  QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1606  // enable logging, check if editBuffer is not null
1607  if ( vLayer->editBuffer() )
1608  {
1609  QgsVectorLayerEditBuffer *editBuffer = vLayer->editBuffer();
1611  this, &QgsOfflineEditing::committedAttributesAdded );
1613  this, &QgsOfflineEditing::committedAttributeValuesChanges );
1615  this, &QgsOfflineEditing::committedGeometriesChanges );
1616  }
1617  connect( vLayer, &QgsVectorLayer::committedFeaturesAdded,
1618  this, &QgsOfflineEditing::committedFeaturesAdded );
1619  connect( vLayer, &QgsVectorLayer::committedFeaturesRemoved,
1620  this, &QgsOfflineEditing::committedFeaturesRemoved );
1621 }
1622 
1623 void QgsOfflineEditing::stopListenFeatureChanges()
1624 {
1625  QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1626  // disable logging, check if editBuffer is not null
1627  if ( vLayer->editBuffer() )
1628  {
1629  QgsVectorLayerEditBuffer *editBuffer = vLayer->editBuffer();
1630  disconnect( editBuffer, &QgsVectorLayerEditBuffer::committedAttributesAdded,
1631  this, &QgsOfflineEditing::committedAttributesAdded );
1633  this, &QgsOfflineEditing::committedAttributeValuesChanges );
1635  this, &QgsOfflineEditing::committedGeometriesChanges );
1636  }
1637  disconnect( vLayer, &QgsVectorLayer::committedFeaturesAdded,
1638  this, &QgsOfflineEditing::committedFeaturesAdded );
1639  disconnect( vLayer, &QgsVectorLayer::committedFeaturesRemoved,
1640  this, &QgsOfflineEditing::committedFeaturesRemoved );
1641 }
1642 
1643 void QgsOfflineEditing::layerAdded( QgsMapLayer *layer )
1644 {
1645  // detect offline layer
1646  if ( layer->customProperty( CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE, false ).toBool() )
1647  {
1648  QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( layer );
1649  connect( vLayer, &QgsVectorLayer::editingStarted, this, &QgsOfflineEditing::startListenFeatureChanges );
1650  connect( vLayer, &QgsVectorLayer::editingStopped, this, &QgsOfflineEditing::stopListenFeatureChanges );
1651  }
1652 }
1653 
1654 
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:59
int precision
Definition: qgsfield.h:56
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:956
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.
QString comment
Definition: qgsfield.h:58
#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 length
Definition: qgsfield.h:55
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
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:100
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.
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:167
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.
Encapsulate a field in an attribute table or data source.
Definition: qgsfield.h:49
QgsRelationManager relationManager
Definition: qgsproject.h:102
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:98
#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:450
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:459
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...
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:471
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:101
#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: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.