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