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