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