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