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