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