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