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