QGIS API Documentation 3.35.0-Master (728e5967df5)
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 );
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 ) );
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();
123 if ( projectTitle.isEmpty() )
124 {
125 projectTitle = QFileInfo( QgsProject::instance()->fileName() ).fileName();
126 }
127 projectTitle += QLatin1String( " (offline)" );
128 QgsProject::instance()->setTitle( projectTitle );
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();
155
156 // restore and sync remote layers
157 QMap<QString, QgsMapLayer *> mapLayers = QgsProject::instance()->mapLayers();
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() );
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\\)$" ) );
336 QgsProject::instance()->setTitle( projectTitle );
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#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
374 QStringList parts = version.split( ' ', QString::SkipEmptyParts );
375#else
376 const QStringList parts = version.split( ' ', Qt::SkipEmptyParts );
377#endif
378 if ( !parts.empty() )
379 {
380#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
381 QStringList verparts = parts.at( 0 ).split( '.', QString::SkipEmptyParts );
382#else
383 const QStringList verparts = parts.at( 0 ).split( '.', Qt::SkipEmptyParts );
384#endif
385 above41 = verparts.size() >= 2 && ( verparts.at( 0 ).toInt() > 4 || ( verparts.at( 0 ).toInt() == 4 && verparts.at( 1 ).toInt() >= 1 ) );
386 }
387 }
388
389 sqlite3_free_table( results );
390
391 // all right, it's empty: proceeding to initialize
392 char *errMsg = nullptr;
393 ret = sqlite3_exec( sqlite_handle, above41 ? "SELECT InitSpatialMetadata(1)" : "SELECT InitSpatialMetadata()", nullptr, nullptr, &errMsg );
394
395 if ( ret != SQLITE_OK )
396 {
397 QString errCause = tr( "Unable to initialize SpatialMetadata:\n" );
398 errCause += QString::fromUtf8( errMsg );
399 showWarning( errCause );
400 sqlite3_free( errMsg );
401 return;
402 }
403 spatial_ref_sys_init( sqlite_handle, 0 );
404#else
405 ( void )sqlite_handle;
406#endif
407}
408
409bool QgsOfflineEditing::createOfflineDb( const QString &offlineDbPath, ContainerType containerType )
410{
411 int ret;
412 char *errMsg = nullptr;
413 const QFile newDb( offlineDbPath );
414 if ( newDb.exists() )
415 {
416 QFile::remove( offlineDbPath );
417 }
418
419 // see also QgsNewSpatialiteLayerDialog::createDb()
420
421 const QFileInfo fullPath = QFileInfo( offlineDbPath );
422 const QDir path = fullPath.dir();
423
424 // Must be sure there is destination directory ~/.qgis
425 QDir().mkpath( path.absolutePath() );
426
427 // creating/opening the new database
428 const QString dbPath = newDb.fileName();
429
430 // creating geopackage
431 switch ( containerType )
432 {
433 case GPKG:
434 {
435 OGRSFDriverH hGpkgDriver = OGRGetDriverByName( "GPKG" );
436 if ( !hGpkgDriver )
437 {
438 showWarning( tr( "Creation of database failed. GeoPackage driver not found." ) );
439 return false;
440 }
441
442 const gdal::ogr_datasource_unique_ptr hDS( OGR_Dr_CreateDataSource( hGpkgDriver, dbPath.toUtf8().constData(), nullptr ) );
443 if ( !hDS )
444 {
445 showWarning( tr( "Creation of database failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
446 return false;
447 }
448 break;
449 }
450 case SpatiaLite:
451 {
452 break;
453 }
454 }
455
457 ret = database.open_v2( dbPath, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr );
458 if ( ret )
459 {
460 // an error occurred
461 QString errCause = tr( "Could not create a new database\n" );
462 errCause += database.errorMessage();
463 showWarning( errCause );
464 return false;
465 }
466 // activating Foreign Key constraints
467 ret = sqlite3_exec( database.get(), "PRAGMA foreign_keys = 1", nullptr, nullptr, &errMsg );
468 if ( ret != SQLITE_OK )
469 {
470 showWarning( tr( "Unable to activate FOREIGN_KEY constraints" ) );
471 sqlite3_free( errMsg );
472 return false;
473 }
474 initializeSpatialMetadata( database.get() );
475 return true;
476}
477
478void QgsOfflineEditing::createLoggingTables( sqlite3 *db )
479{
480 // indices
481 QString sql = QStringLiteral( "CREATE TABLE 'log_indices' ('name' TEXT, 'last_index' INTEGER)" );
482 sqlExec( db, sql );
483
484 sql = QStringLiteral( "INSERT INTO 'log_indices' VALUES ('commit_no', 0)" );
485 sqlExec( db, sql );
486
487 sql = QStringLiteral( "INSERT INTO 'log_indices' VALUES ('layer_id', 0)" );
488 sqlExec( db, sql );
489
490 // layername <-> layer id
491 sql = QStringLiteral( "CREATE TABLE 'log_layer_ids' ('id' INTEGER, 'qgis_id' TEXT)" );
492 sqlExec( db, sql );
493
494 // offline fid <-> remote fid
495 sql = QStringLiteral( "CREATE TABLE 'log_fids' ('layer_id' INTEGER, 'offline_fid' INTEGER, 'remote_fid' INTEGER, 'remote_pk' TEXT)" );
496 sqlExec( db, sql );
497
498 // added attributes
499 sql = QStringLiteral( "CREATE TABLE 'log_added_attrs' ('layer_id' INTEGER, 'commit_no' INTEGER, " );
500 sql += QLatin1String( "'name' TEXT, 'type' INTEGER, 'length' INTEGER, 'precision' INTEGER, 'comment' TEXT)" );
501 sqlExec( db, sql );
502
503 // added features
504 sql = QStringLiteral( "CREATE TABLE 'log_added_features' ('layer_id' INTEGER, 'fid' INTEGER)" );
505 sqlExec( db, sql );
506
507 // removed features
508 sql = QStringLiteral( "CREATE TABLE 'log_removed_features' ('layer_id' INTEGER, 'fid' INTEGER)" );
509 sqlExec( db, sql );
510
511 // feature updates
512 sql = QStringLiteral( "CREATE TABLE 'log_feature_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'attr' INTEGER, 'value' TEXT)" );
513 sqlExec( db, sql );
514
515 // geometry updates
516 sql = QStringLiteral( "CREATE TABLE 'log_geometry_updates' ('layer_id' INTEGER, 'commit_no' INTEGER, 'fid' INTEGER, 'geom_wkt' TEXT)" );
517 sqlExec( db, sql );
518
519 /* TODO: other logging tables
520 - attr delete (not supported by SpatiaLite provider)
521 */
522}
523
524void QgsOfflineEditing::convertToOfflineLayer( QgsVectorLayer *layer, sqlite3 *db, const QString &offlineDbPath, bool onlySelected, ContainerType containerType, const QString &layerNameSuffix )
525{
526 if ( !layer || !layer->isValid() )
527 {
528 QgsDebugMsgLevel( QStringLiteral( "Layer %1 is invalid and cannot be copied" ).arg( layer ? layer->id() : QStringLiteral( "<UNKNOWN>" ) ), 4 );
529 return;
530 }
531
532 const QString tableName = layer->id();
533 QgsDebugMsgLevel( QStringLiteral( "Creating offline table %1 ..." ).arg( tableName ), 4 );
534
535 // new layer
536 std::unique_ptr<QgsVectorLayer> newLayer;
537
538 switch ( containerType )
539 {
540 case SpatiaLite:
541 {
542#ifdef HAVE_SPATIALITE
543 // create table
544 QString sql = QStringLiteral( "CREATE TABLE '%1' (" ).arg( tableName );
545 QString delim;
546 const QgsFields providerFields = layer->dataProvider()->fields();
547 for ( const auto &field : providerFields )
548 {
549 QString dataType;
550 const QVariant::Type type = field.type();
551 if ( type == QVariant::Int || type == QVariant::LongLong )
552 {
553 dataType = QStringLiteral( "INTEGER" );
554 }
555 else if ( type == QVariant::Double )
556 {
557 dataType = QStringLiteral( "REAL" );
558 }
559 else if ( type == QVariant::String )
560 {
561 dataType = QStringLiteral( "TEXT" );
562 }
563 else if ( type == QVariant::StringList || type == QVariant::List )
564 {
565 dataType = QStringLiteral( "TEXT" );
566 showWarning( tr( "Field '%1' from layer %2 has been converted from a list to a string of comma-separated values." ).arg( field.name(), layer->name() ) );
567 }
568 else
569 {
570 showWarning( tr( "%1: Unknown data type %2. Not using type affinity for the field." ).arg( field.name(), QVariant::typeToName( type ) ) );
571 }
572
573 sql += delim + QStringLiteral( "'%1' %2" ).arg( field.name(), dataType );
574 delim = ',';
575 }
576 sql += ')';
577
578 int rc = sqlExec( db, sql );
579
580 // add geometry column
581 if ( layer->isSpatial() )
582 {
583 const Qgis::WkbType sourceWkbType = layer->wkbType();
584
585 QString geomType;
586 switch ( QgsWkbTypes::flatType( sourceWkbType ) )
587 {
589 geomType = QStringLiteral( "POINT" );
590 break;
592 geomType = QStringLiteral( "MULTIPOINT" );
593 break;
595 geomType = QStringLiteral( "LINESTRING" );
596 break;
598 geomType = QStringLiteral( "MULTILINESTRING" );
599 break;
601 geomType = QStringLiteral( "POLYGON" );
602 break;
604 geomType = QStringLiteral( "MULTIPOLYGON" );
605 break;
606 default:
607 showWarning( tr( "Layer %1 has unsupported geometry type %2." ).arg( layer->name(), QgsWkbTypes::displayString( layer->wkbType() ) ) );
608 break;
609 };
610
611 QString zmInfo = QStringLiteral( "XY" );
612
613 if ( QgsWkbTypes::hasZ( sourceWkbType ) )
614 zmInfo += 'Z';
615 if ( QgsWkbTypes::hasM( sourceWkbType ) )
616 zmInfo += 'M';
617
618 QString epsgCode;
619
620 if ( layer->crs().authid().startsWith( QLatin1String( "EPSG:" ), Qt::CaseInsensitive ) )
621 {
622 epsgCode = layer->crs().authid().mid( 5 );
623 }
624 else
625 {
626 epsgCode = '0';
627 showWarning( tr( "Layer %1 has unsupported Coordinate Reference System (%2)." ).arg( layer->name(), layer->crs().authid() ) );
628 }
629
630 const QString sqlAddGeom = QStringLiteral( "SELECT AddGeometryColumn('%1', 'Geometry', %2, '%3', '%4')" )
631 .arg( tableName, epsgCode, geomType, zmInfo );
632
633 // create spatial index
634 const QString sqlCreateIndex = QStringLiteral( "SELECT CreateSpatialIndex('%1', 'Geometry')" ).arg( tableName );
635
636 if ( rc == SQLITE_OK )
637 {
638 rc = sqlExec( db, sqlAddGeom );
639 if ( rc == SQLITE_OK )
640 {
641 rc = sqlExec( db, sqlCreateIndex );
642 }
643 }
644 }
645
646 if ( rc != SQLITE_OK )
647 {
648 showWarning( tr( "Filling SpatiaLite for layer %1 failed" ).arg( layer->name() ) );
649 return;
650 }
651
652 // add new layer
653 const QString connectionString = QStringLiteral( "dbname='%1' table='%2'%3 sql=" )
654 .arg( offlineDbPath,
655 tableName, layer->isSpatial() ? "(Geometry)" : "" );
657 newLayer = std::make_unique<QgsVectorLayer>( connectionString,
658 layer->name() + layerNameSuffix, QStringLiteral( "spatialite" ), options );
659 break;
660
661#else
662 showWarning( tr( "No Spatialite support available" ) );
663 return;
664#endif
665 }
666
667 case GPKG:
668 {
669 // Set options
670 char **options = nullptr;
671
672 options = CSLSetNameValue( options, "OVERWRITE", "YES" );
673 options = CSLSetNameValue( options, "IDENTIFIER", tr( "%1 (offline)" ).arg( layer->id() ).toUtf8().constData() );
674 options = CSLSetNameValue( options, "DESCRIPTION", layer->dataComment().toUtf8().constData() );
675
676 //the FID-name should not exist in the original data
677 const QString fidBase( QStringLiteral( "fid" ) );
678 QString fid = fidBase;
679 int counter = 1;
680 while ( layer->dataProvider()->fields().lookupField( fid ) >= 0 && counter < 10000 )
681 {
682 fid = fidBase + '_' + QString::number( counter );
683 counter++;
684 }
685 if ( counter == 10000 )
686 {
687 showWarning( tr( "Cannot make FID-name for GPKG " ) );
688 return;
689 }
690
691 options = CSLSetNameValue( options, "FID", fid.toUtf8().constData() );
692
693 if ( layer->isSpatial() )
694 {
695 options = CSLSetNameValue( options, "GEOMETRY_COLUMN", "geom" );
696 options = CSLSetNameValue( options, "SPATIAL_INDEX", "YES" );
697 }
698
699 OGRSFDriverH hDriver = nullptr;
701 gdal::ogr_datasource_unique_ptr hDS( OGROpen( offlineDbPath.toUtf8().constData(), true, &hDriver ) );
702 OGRLayerH hLayer = OGR_DS_CreateLayer( hDS.get(), tableName.toUtf8().constData(), hSRS, static_cast<OGRwkbGeometryType>( layer->wkbType() ), options );
703 CSLDestroy( options );
704 if ( hSRS )
705 OSRRelease( hSRS );
706 if ( !hLayer )
707 {
708 showWarning( tr( "Creation of layer failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
709 return;
710 }
711
712 const QgsFields providerFields = layer->dataProvider()->fields();
713 for ( const auto &field : providerFields )
714 {
715 const QString fieldName( field.name() );
716 const QVariant::Type type = field.type();
717 OGRFieldType ogrType( OFTString );
718 OGRFieldSubType ogrSubType = OFSTNone;
719 if ( type == QVariant::Int )
720 ogrType = OFTInteger;
721 else if ( type == QVariant::LongLong )
722 ogrType = OFTInteger64;
723 else if ( type == QVariant::Double )
724 ogrType = OFTReal;
725 else if ( type == QVariant::Time )
726 ogrType = OFTTime;
727 else if ( type == QVariant::Date )
728 ogrType = OFTDate;
729 else if ( type == QVariant::DateTime )
730 ogrType = OFTDateTime;
731 else if ( type == QVariant::Bool )
732 {
733 ogrType = OFTInteger;
734 ogrSubType = OFSTBoolean;
735 }
736 else if ( type == QVariant::StringList || type == QVariant::List )
737 {
738 ogrType = OFTString;
739 ogrSubType = OFSTJSON;
740 showWarning( tr( "Field '%1' from layer %2 has been converted from a list to a JSON-formatted string value." ).arg( fieldName, layer->name() ) );
741 }
742 else
743 ogrType = OFTString;
744
745 const int ogrWidth = field.length();
746
747 const gdal::ogr_field_def_unique_ptr fld( OGR_Fld_Create( fieldName.toUtf8().constData(), ogrType ) );
748 OGR_Fld_SetWidth( fld.get(), ogrWidth );
749 if ( ogrSubType != OFSTNone )
750 OGR_Fld_SetSubType( fld.get(), ogrSubType );
751
752 if ( OGR_L_CreateField( hLayer, fld.get(), true ) != OGRERR_NONE )
753 {
754 showWarning( tr( "Creation of field %1 failed (OGR error: %2)" )
755 .arg( fieldName, QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
756 return;
757 }
758 }
759
760 // In GDAL >= 2.0, the driver implements a deferred creation strategy, so
761 // issue a command that will force table creation
762 CPLErrorReset();
763 OGR_L_ResetReading( hLayer );
764 if ( CPLGetLastErrorType() != CE_None )
765 {
766 const QString msg( tr( "Creation of layer failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
767 showWarning( msg );
768 return;
769 }
770 hDS.reset();
771
772 const QString uri = QStringLiteral( "%1|layername=%2|option:QGIS_FORCE_WAL=ON" ).arg( offlineDbPath, tableName );
774 newLayer = std::make_unique<QgsVectorLayer>( uri, layer->name() + layerNameSuffix, QStringLiteral( "ogr" ), layerOptions );
775 break;
776 }
777 }
778
779 if ( newLayer && newLayer->isValid() )
780 {
781
782 // copy features
783 newLayer->startEditing();
784 QgsFeature f;
785
787
788 if ( onlySelected )
789 {
790 const QgsFeatureIds selectedFids = layer->selectedFeatureIds();
791 if ( !selectedFids.isEmpty() )
792 req.setFilterFids( selectedFids );
793 }
794
795 QgsFeatureIterator fit = layer->dataProvider()->getFeatures( req );
796
798 {
800 }
801 else
802 {
804 }
805 long long featureCount = 1;
806 const int remotePkIdx = getLayerPkIdx( layer );
807
808 QList<QgsFeatureId> remoteFeatureIds;
809 QStringList remoteFeaturePks;
810 while ( fit.nextFeature( f ) )
811 {
812 remoteFeatureIds << f.id();
813 remoteFeaturePks << ( remotePkIdx >= 0 ? f.attribute( remotePkIdx ).toString() : QString() );
814
815 // NOTE: SpatiaLite provider ignores position of geometry column
816 // fill gap in QgsAttributeMap if geometry column is not last (WORKAROUND)
817 int column = 0;
818 const QgsAttributes attrs = f.attributes();
819 // on GPKG newAttrs has an addition FID attribute, so we have to add a dummy in the original set
820 QgsAttributes newAttrs( containerType == GPKG ? attrs.count() + 1 : attrs.count() );
821 for ( int it = 0; it < attrs.count(); ++it )
822 {
823 const QVariant attr = attrs.at( it );
824 newAttrs[column++] = attr;
825 }
826 f.setAttributes( newAttrs );
827
828 newLayer->addFeature( f );
829
830 emit progressUpdated( featureCount++ );
831 }
832 if ( newLayer->commitChanges() )
833 {
835 featureCount = 1;
836
837 // update feature id lookup
838 const int layerId = getOrCreateLayerId( db, layer->id() );
839 QList<QgsFeatureId> offlineFeatureIds;
840
841 QgsFeatureIterator fit = newLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setNoAttributes() );
842 while ( fit.nextFeature( f ) )
843 {
844 offlineFeatureIds << f.id();
845 }
846
847 // NOTE: insert fids in this loop, as the db is locked during newLayer->nextFeature()
848 sqlExec( db, QStringLiteral( "BEGIN" ) );
849 const int remoteCount = remoteFeatureIds.size();
850 for ( int i = 0; i < remoteCount; i++ )
851 {
852 // Check if the online feature has been fetched (WFS download aborted for some reason)
853 if ( i < offlineFeatureIds.count() )
854 {
855 addFidLookup( db, layerId, offlineFeatureIds.at( i ), remoteFeatureIds.at( i ), remoteFeaturePks.at( i ) );
856 }
857 else
858 {
859 showWarning( tr( "Feature cannot be copied to the offline layer, please check if the online layer '%1' is still accessible." ).arg( layer->name() ) );
860 return;
861 }
862 emit progressUpdated( featureCount++ );
863 }
864 sqlExec( db, QStringLiteral( "COMMIT" ) );
865 }
866 else
867 {
868 showWarning( newLayer->commitErrors().join( QLatin1Char( '\n' ) ) );
869 }
870
871 // mark as offline layer
873
874 // store original layer source and information
878 layer->setCustomProperty( CUSTOM_PROPERTY_LAYERNAME_SUFFIX, layerNameSuffix );
879
880 //remove constrainst of fields that use defaultValueClauses from provider on original
881 const QgsFields fields = layer->fields();
882 QStringList notNullFieldNames;
883 for ( const QgsField &field : fields )
884 {
885 if ( !layer->dataProvider()->defaultValueClause( layer->fields().fieldOriginIndex( layer->fields().indexOf( field.name() ) ) ).isEmpty() )
886 {
887 notNullFieldNames << field.name();
888 }
889 }
890
891 layer->setDataSource( newLayer->source(), newLayer->name(), newLayer->dataProvider()->name() );
892
893 for ( const QgsField &field : fields ) //QString &fieldName : fieldsToRemoveConstraint )
894 {
895 const int index = layer->fields().indexOf( field.name() );
896 if ( index > -1 )
897 {
898 // restore unique value constraints coming from original data provider
899 if ( field.constraints().constraints() & QgsFieldConstraints::ConstraintUnique )
901
902 // remove any undesired not null constraints coming from original data provider
903 if ( notNullFieldNames.contains( field.name() ) )
904 {
905 notNullFieldNames.removeAll( field.name() );
907 }
908 }
909 }
910
911 setupLayer( layer );
912 }
913 return;
914}
915
916void QgsOfflineEditing::applyAttributesAdded( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
917{
918 Q_ASSERT( remoteLayer );
919
920 const QString sql = QStringLiteral( "SELECT \"name\", \"type\", \"length\", \"precision\", \"comment\" FROM 'log_added_attrs' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2" ).arg( layerId ).arg( commitNo );
921 QList<QgsField> fields = sqlQueryAttributesAdded( db, sql );
922
923 const QgsVectorDataProvider *provider = remoteLayer->dataProvider();
924 const QList<QgsVectorDataProvider::NativeType> nativeTypes = provider->nativeTypes();
925
926 // NOTE: uses last matching QVariant::Type of nativeTypes
927 QMap < QVariant::Type, QString /*typeName*/ > typeNameLookup;
928 for ( int i = 0; i < nativeTypes.size(); i++ )
929 {
930 const QgsVectorDataProvider::NativeType nativeType = nativeTypes.at( i );
931 typeNameLookup[ nativeType.mType ] = nativeType.mTypeName;
932 }
933
934 emit progressModeSet( QgsOfflineEditing::AddFields, fields.size() );
935
936 for ( int i = 0; i < fields.size(); i++ )
937 {
938 // lookup typename from layer provider
939 QgsField field = fields[i];
940 if ( typeNameLookup.contains( field.type() ) )
941 {
942 const QString typeName = typeNameLookup[ field.type()];
943 field.setTypeName( typeName );
944 remoteLayer->addAttribute( field );
945 }
946 else
947 {
948 showWarning( QStringLiteral( "Could not add attribute '%1' of type %2" ).arg( field.name() ).arg( field.type() ) );
949 }
950
951 emit progressUpdated( i + 1 );
952 }
953}
954
955void QgsOfflineEditing::applyFeaturesAdded( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
956{
957 Q_ASSERT( offlineLayer );
958 Q_ASSERT( remoteLayer );
959
960 const QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
961 const QList<int> featureIdInts = sqlQueryInts( db, sql );
962 QgsFeatureIds newFeatureIds;
963 for ( const int id : featureIdInts )
964 {
965 newFeatureIds << id;
966 }
967
968 QgsExpressionContext context = remoteLayer->createExpressionContext();
969
970 // get new features from offline layer
971 QgsFeatureList features;
972 QgsFeatureIterator it = offlineLayer->getFeatures( QgsFeatureRequest().setFilterFids( newFeatureIds ) );
973 QgsFeature feature;
974 while ( it.nextFeature( feature ) )
975 {
976 features << feature;
977 }
978
979 // copy features to remote layer
980 emit progressModeSet( QgsOfflineEditing::AddFeatures, features.size() );
981
982 int i = 1;
983 const int newAttrsCount = remoteLayer->fields().count();
984 for ( QgsFeatureList::iterator it = features.begin(); it != features.end(); ++it )
985 {
986 // NOTE: SpatiaLite provider ignores position of geometry column
987 // restore gap in QgsAttributeMap if geometry column is not last (WORKAROUND)
988 const QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
989 QgsAttributes newAttrs( newAttrsCount );
990 const QgsAttributes attrs = it->attributes();
991 for ( int it = 0; it < attrs.count(); ++it )
992 {
993 const int remoteAttributeIndex = attrLookup.value( it, -1 );
994 // if virtual or non existing field
995 if ( remoteAttributeIndex == -1 )
996 continue;
997 QVariant attr = attrs.at( it );
998 if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::StringList )
999 {
1000 if ( attr.type() == QVariant::StringList || attr.type() == QVariant::List )
1001 {
1002 attr = attr.toStringList();
1003 }
1004 else
1005 {
1006 attr = QgsJsonUtils::parseArray( attr.toString(), QVariant::String );
1007 }
1008 }
1009 else if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::List )
1010 {
1011 if ( attr.type() == QVariant::StringList || attr.type() == QVariant::List )
1012 {
1013 attr = attr.toList();
1014 }
1015 else
1016 {
1017 attr = QgsJsonUtils::parseArray( attr.toString(), remoteLayer->fields().at( remoteAttributeIndex ).subType() );
1018 }
1019 }
1020 newAttrs[ remoteAttributeIndex ] = attr;
1021 }
1022
1023 // respect constraints and provider default values
1024 QgsFeature f = QgsVectorLayerUtils::createFeature( remoteLayer, it->geometry(), newAttrs.toMap(), &context );
1025 remoteLayer->addFeature( f );
1026
1027 emit progressUpdated( i++ );
1028 }
1029}
1030
1031void QgsOfflineEditing::applyFeaturesRemoved( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
1032{
1033 Q_ASSERT( remoteLayer );
1034
1035 const QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( layerId );
1036 const QgsFeatureIds values = sqlQueryFeaturesRemoved( db, sql );
1037
1039
1040 int i = 1;
1041 for ( QgsFeatureIds::const_iterator it = values.constBegin(); it != values.constEnd(); ++it )
1042 {
1043 const QgsFeatureId fid = remoteFid( db, layerId, *it, remoteLayer );
1044 remoteLayer->deleteFeature( fid );
1045
1046 emit progressUpdated( i++ );
1047 }
1048}
1049
1050void QgsOfflineEditing::applyAttributeValueChanges( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
1051{
1052 Q_ASSERT( offlineLayer );
1053 Q_ASSERT( remoteLayer );
1054
1055 const QString sql = QStringLiteral( "SELECT \"fid\", \"attr\", \"value\" FROM 'log_feature_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2 " ).arg( layerId ).arg( commitNo );
1056 const AttributeValueChanges values = sqlQueryAttributeValueChanges( db, sql );
1057
1059
1060 QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
1061
1062 for ( int i = 0; i < values.size(); i++ )
1063 {
1064 const QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid, remoteLayer );
1065 QgsDebugMsgLevel( QStringLiteral( "Offline changeAttributeValue %1 = %2" ).arg( attrLookup[ values.at( i ).attr ] ).arg( values.at( i ).value ), 4 );
1066
1067 const int remoteAttributeIndex = attrLookup[ values.at( i ).attr ];
1068 QVariant attr = values.at( i ).value;
1069 if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::StringList )
1070 {
1071 attr = QgsJsonUtils::parseArray( attr.toString(), QVariant::String );
1072 }
1073 else if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::List )
1074 {
1075 attr = QgsJsonUtils::parseArray( attr.toString(), remoteLayer->fields().at( remoteAttributeIndex ).subType() );
1076 }
1077
1078 remoteLayer->changeAttributeValue( fid, remoteAttributeIndex, attr );
1079
1080 emit progressUpdated( i + 1 );
1081 }
1082}
1083
1084void QgsOfflineEditing::applyGeometryChanges( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
1085{
1086 Q_ASSERT( remoteLayer );
1087
1088 const QString sql = QStringLiteral( "SELECT \"fid\", \"geom_wkt\" FROM 'log_geometry_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2" ).arg( layerId ).arg( commitNo );
1089 const GeometryChanges values = sqlQueryGeometryChanges( db, sql );
1090
1092
1093 for ( int i = 0; i < values.size(); i++ )
1094 {
1095 const QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid, remoteLayer );
1096 QgsGeometry newGeom = QgsGeometry::fromWkt( values.at( i ).geom_wkt );
1097 remoteLayer->changeGeometry( fid, newGeom );
1098
1099 emit progressUpdated( i + 1 );
1100 }
1101}
1102
1103void QgsOfflineEditing::updateFidLookup( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
1104{
1105 Q_ASSERT( remoteLayer );
1106
1107 // update fid lookup for added features
1108
1109 // get remote added fids
1110 // NOTE: use QMap for sorted fids
1111 QMap < QgsFeatureId, QString > newRemoteFids;
1112 QgsFeature f;
1113
1114 QgsFeatureIterator fit = remoteLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setNoAttributes() );
1115
1117
1118 const int remotePkIdx = getLayerPkIdx( remoteLayer );
1119
1120 int i = 1;
1121 while ( fit.nextFeature( f ) )
1122 {
1123 if ( offlineFid( db, layerId, f.id() ) == -1 )
1124 {
1125 newRemoteFids[ f.id()] = remotePkIdx >= 0 ? f.attribute( remotePkIdx ).toString() : QString();
1126 }
1127
1128 emit progressUpdated( i++ );
1129 }
1130
1131 // get local added fids
1132 // NOTE: fids are sorted
1133 const QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
1134 const QList<int> newOfflineFids = sqlQueryInts( db, sql );
1135
1136 if ( newRemoteFids.size() != newOfflineFids.size() )
1137 {
1138 //showWarning( QString( "Different number of new features on offline layer (%1) and remote layer (%2)" ).arg(newOfflineFids.size()).arg(newRemoteFids.size()) );
1139 }
1140 else
1141 {
1142 // add new fid lookups
1143 i = 0;
1144 sqlExec( db, QStringLiteral( "BEGIN" ) );
1145 for ( QMap<QgsFeatureId, QString>::const_iterator it = newRemoteFids.constBegin(); it != newRemoteFids.constEnd(); ++it )
1146 {
1147 addFidLookup( db, layerId, newOfflineFids.at( i++ ), it.key(), it.value() );
1148 }
1149 sqlExec( db, QStringLiteral( "COMMIT" ) );
1150 }
1151}
1152
1153// NOTE: use this to map column indices in case the remote geometry column is not last
1154QMap<int, int> QgsOfflineEditing::attributeLookup( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer )
1155{
1156 Q_ASSERT( offlineLayer );
1157 Q_ASSERT( remoteLayer );
1158
1159 const QgsAttributeList &offlineAttrs = offlineLayer->attributeList();
1160
1161 QMap < int /*offline attr*/, int /*remote attr*/ > attrLookup;
1162 // NOTE: though offlineAttrs can have new attributes not yet synced, we take the amount of offlineAttrs
1163 // because we anyway only add mapping for the fields existing in remoteLayer (this because it could contain fid on 0)
1164 for ( int i = 0; i < offlineAttrs.size(); i++ )
1165 {
1166 if ( remoteLayer->fields().lookupField( offlineLayer->fields().field( i ).name() ) >= 0 )
1167 attrLookup.insert( offlineAttrs.at( i ), remoteLayer->fields().indexOf( offlineLayer->fields().field( i ).name() ) );
1168 }
1169
1170 return attrLookup;
1171}
1172
1173void QgsOfflineEditing::showWarning( const QString &message )
1174{
1175 emit warning( tr( "Offline Editing Plugin" ), message );
1176}
1177
1178sqlite3_database_unique_ptr QgsOfflineEditing::openLoggingDb()
1179{
1182 if ( !dbPath.isEmpty() )
1183 {
1184 const QString absoluteDbPath = QgsProject::instance()->readPath( dbPath );
1185 const int rc = database.open( absoluteDbPath );
1186 if ( rc != SQLITE_OK )
1187 {
1188 QgsDebugError( QStringLiteral( "Could not open the SpatiaLite logging database" ) );
1189 showWarning( tr( "Could not open the SpatiaLite logging database" ) );
1190 }
1191 }
1192 else
1193 {
1194 QgsDebugError( QStringLiteral( "dbPath is empty!" ) );
1195 }
1196 return database;
1197}
1198
1199int QgsOfflineEditing::getOrCreateLayerId( sqlite3 *db, const QString &qgisLayerId )
1200{
1201 QString sql = QStringLiteral( "SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( qgisLayerId );
1202 int layerId = sqlQueryInt( db, sql, -1 );
1203 if ( layerId == -1 )
1204 {
1205 // next layer id
1206 sql = QStringLiteral( "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'layer_id'" );
1207 const int newLayerId = sqlQueryInt( db, sql, -1 );
1208
1209 // insert layer
1210 sql = QStringLiteral( "INSERT INTO 'log_layer_ids' VALUES (%1, '%2')" ).arg( newLayerId ).arg( qgisLayerId );
1211 sqlExec( db, sql );
1212
1213 // increase layer_id
1214 // TODO: use trigger for auto increment?
1215 sql = QStringLiteral( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'layer_id'" ).arg( newLayerId + 1 );
1216 sqlExec( db, sql );
1217
1218 layerId = newLayerId;
1219 }
1220
1221 return layerId;
1222}
1223
1224int QgsOfflineEditing::getCommitNo( sqlite3 *db )
1225{
1226 const QString sql = QStringLiteral( "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'commit_no'" );
1227 return sqlQueryInt( db, sql, -1 );
1228}
1229
1230void QgsOfflineEditing::increaseCommitNo( sqlite3 *db )
1231{
1232 const QString sql = QStringLiteral( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'commit_no'" ).arg( getCommitNo( db ) + 1 );
1233 sqlExec( db, sql );
1234}
1235
1236void QgsOfflineEditing::addFidLookup( sqlite3 *db, int layerId, QgsFeatureId offlineFid, QgsFeatureId remoteFid, const QString &remotePk )
1237{
1238 const QString sql = QStringLiteral( "INSERT INTO 'log_fids' VALUES ( %1, %2, %3, %4 )" ).arg( layerId ).arg( offlineFid ).arg( remoteFid ).arg( sqlEscape( remotePk ) );
1239 sqlExec( db, sql );
1240}
1241
1242QgsFeatureId QgsOfflineEditing::remoteFid( sqlite3 *db, int layerId, QgsFeatureId offlineFid, QgsVectorLayer *remoteLayer )
1243{
1244 const int pkIdx = getLayerPkIdx( remoteLayer );
1245
1246 if ( pkIdx == -1 )
1247 {
1248 const QString sql = QStringLiteral( "SELECT \"remote_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"offline_fid\" = %2" ).arg( layerId ).arg( offlineFid );
1249 return sqlQueryInt( db, sql, -1 );
1250 }
1251
1252 const QString sql = QStringLiteral( "SELECT \"remote_pk\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"offline_fid\" = %2" ).arg( layerId ).arg( offlineFid );
1253 QString defaultValue;
1254 const QString pkValue = sqlQueryStr( db, sql, defaultValue );
1255
1256 if ( pkValue.isNull() )
1257 {
1258 return -1;
1259 }
1260
1261 const QString pkFieldName = remoteLayer->fields().at( pkIdx ).name();
1262 QgsFeatureIterator fit = remoteLayer->getFeatures( QStringLiteral( " %1 = %2 " ).arg( pkFieldName ).arg( sqlEscape( pkValue ) ) );
1263 QgsFeature f;
1264 while ( fit.nextFeature( f ) )
1265 return f.id();
1266
1267 return -1;
1268}
1269
1270QgsFeatureId QgsOfflineEditing::offlineFid( sqlite3 *db, int layerId, QgsFeatureId remoteFid )
1271{
1272 const QString sql = QStringLiteral( "SELECT \"offline_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"remote_fid\" = %2" ).arg( layerId ).arg( remoteFid );
1273 return sqlQueryInt( db, sql, -1 );
1274}
1275
1276bool QgsOfflineEditing::isAddedFeature( sqlite3 *db, int layerId, QgsFeatureId fid )
1277{
1278 const QString sql = QStringLiteral( "SELECT COUNT(\"fid\") FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( fid );
1279 return ( sqlQueryInt( db, sql, 0 ) > 0 );
1280}
1281
1282int QgsOfflineEditing::sqlExec( sqlite3 *db, const QString &sql )
1283{
1284 char *errmsg = nullptr;
1285 const int rc = sqlite3_exec( db, sql.toUtf8(), nullptr, nullptr, &errmsg );
1286 if ( rc != SQLITE_OK )
1287 {
1288 showWarning( errmsg );
1289 }
1290 return rc;
1291}
1292
1293QString QgsOfflineEditing::sqlQueryStr( sqlite3 *db, const QString &sql, QString &defaultValue )
1294{
1295 sqlite3_stmt *stmt = nullptr;
1296 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1297 {
1298 showWarning( sqlite3_errmsg( db ) );
1299 return defaultValue;
1300 }
1301
1302 QString value = defaultValue;
1303 const int ret = sqlite3_step( stmt );
1304 if ( ret == SQLITE_ROW )
1305 {
1306 value = QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 0 ) ) );
1307 }
1308 sqlite3_finalize( stmt );
1309
1310 return value;
1311}
1312
1313int QgsOfflineEditing::sqlQueryInt( sqlite3 *db, const QString &sql, int defaultValue )
1314{
1315 sqlite3_stmt *stmt = nullptr;
1316 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1317 {
1318 showWarning( sqlite3_errmsg( db ) );
1319 return defaultValue;
1320 }
1321
1322 int value = defaultValue;
1323 const int ret = sqlite3_step( stmt );
1324 if ( ret == SQLITE_ROW )
1325 {
1326 value = sqlite3_column_int( stmt, 0 );
1327 }
1328 sqlite3_finalize( stmt );
1329
1330 return value;
1331}
1332
1333QList<int> QgsOfflineEditing::sqlQueryInts( sqlite3 *db, const QString &sql )
1334{
1335 QList<int> values;
1336
1337 sqlite3_stmt *stmt = nullptr;
1338 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1339 {
1340 showWarning( sqlite3_errmsg( db ) );
1341 return values;
1342 }
1343
1344 int ret = sqlite3_step( stmt );
1345 while ( ret == SQLITE_ROW )
1346 {
1347 values << sqlite3_column_int( stmt, 0 );
1348
1349 ret = sqlite3_step( stmt );
1350 }
1351 sqlite3_finalize( stmt );
1352
1353 return values;
1354}
1355
1356QList<QgsField> QgsOfflineEditing::sqlQueryAttributesAdded( sqlite3 *db, const QString &sql )
1357{
1358 QList<QgsField> values;
1359
1360 sqlite3_stmt *stmt = nullptr;
1361 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1362 {
1363 showWarning( sqlite3_errmsg( db ) );
1364 return values;
1365 }
1366
1367 int ret = sqlite3_step( stmt );
1368 while ( ret == SQLITE_ROW )
1369 {
1370 const QgsField field( QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 0 ) ) ),
1371 static_cast< QVariant::Type >( sqlite3_column_int( stmt, 1 ) ),
1372 QString(), // typeName
1373 sqlite3_column_int( stmt, 2 ),
1374 sqlite3_column_int( stmt, 3 ),
1375 QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 4 ) ) ) );
1376 values << field;
1377
1378 ret = sqlite3_step( stmt );
1379 }
1380 sqlite3_finalize( stmt );
1381
1382 return values;
1383}
1384
1385QgsFeatureIds QgsOfflineEditing::sqlQueryFeaturesRemoved( sqlite3 *db, const QString &sql )
1386{
1387 QgsFeatureIds values;
1388
1389 sqlite3_stmt *stmt = nullptr;
1390 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1391 {
1392 showWarning( sqlite3_errmsg( db ) );
1393 return values;
1394 }
1395
1396 int ret = sqlite3_step( stmt );
1397 while ( ret == SQLITE_ROW )
1398 {
1399 values << sqlite3_column_int( stmt, 0 );
1400
1401 ret = sqlite3_step( stmt );
1402 }
1403 sqlite3_finalize( stmt );
1404
1405 return values;
1406}
1407
1408QgsOfflineEditing::AttributeValueChanges QgsOfflineEditing::sqlQueryAttributeValueChanges( sqlite3 *db, const QString &sql )
1409{
1410 AttributeValueChanges values;
1411
1412 sqlite3_stmt *stmt = nullptr;
1413 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1414 {
1415 showWarning( sqlite3_errmsg( db ) );
1416 return values;
1417 }
1418
1419 int ret = sqlite3_step( stmt );
1420 while ( ret == SQLITE_ROW )
1421 {
1422 AttributeValueChange change;
1423 change.fid = sqlite3_column_int( stmt, 0 );
1424 change.attr = sqlite3_column_int( stmt, 1 );
1425 change.value = QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 2 ) ) );
1426 values << change;
1427
1428 ret = sqlite3_step( stmt );
1429 }
1430 sqlite3_finalize( stmt );
1431
1432 return values;
1433}
1434
1435QgsOfflineEditing::GeometryChanges QgsOfflineEditing::sqlQueryGeometryChanges( sqlite3 *db, const QString &sql )
1436{
1437 GeometryChanges values;
1438
1439 sqlite3_stmt *stmt = nullptr;
1440 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1441 {
1442 showWarning( sqlite3_errmsg( db ) );
1443 return values;
1444 }
1445
1446 int ret = sqlite3_step( stmt );
1447 while ( ret == SQLITE_ROW )
1448 {
1449 GeometryChange change;
1450 change.fid = sqlite3_column_int( stmt, 0 );
1451 change.geom_wkt = QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 1 ) ) );
1452 values << change;
1453
1454 ret = sqlite3_step( stmt );
1455 }
1456 sqlite3_finalize( stmt );
1457
1458 return values;
1459}
1460
1461void QgsOfflineEditing::committedAttributesAdded( const QString &qgisLayerId, const QList<QgsField> &addedAttributes )
1462{
1463 const sqlite3_database_unique_ptr database = openLoggingDb();
1464 if ( !database )
1465 return;
1466
1467 // insert log
1468 const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1469 const int commitNo = getCommitNo( database.get() );
1470
1471 for ( const QgsField &field : addedAttributes )
1472 {
1473 const QString sql = QStringLiteral( "INSERT INTO 'log_added_attrs' VALUES ( %1, %2, '%3', %4, %5, %6, '%7' )" )
1474 .arg( layerId )
1475 .arg( commitNo )
1476 .arg( field.name() )
1477 .arg( field.type() )
1478 .arg( field.length() )
1479 .arg( field.precision() )
1480 .arg( field.comment() );
1481 sqlExec( database.get(), sql );
1482 }
1483
1484 increaseCommitNo( database.get() );
1485}
1486
1487void QgsOfflineEditing::committedFeaturesAdded( const QString &qgisLayerId, const QgsFeatureList &addedFeatures )
1488{
1489 const sqlite3_database_unique_ptr database = openLoggingDb();
1490 if ( !database )
1491 return;
1492
1493 // insert log
1494 const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1495
1496 // get new feature ids from db
1497 QgsMapLayer *layer = QgsProject::instance()->mapLayer( qgisLayerId );
1498 const QString dataSourceString = layer->source();
1499 const QgsDataSourceUri uri = QgsDataSourceUri( dataSourceString );
1500
1502 QString tableName;
1503
1504 if ( !offlinePath.contains( ".gpkg" ) )
1505 {
1506 tableName = uri.table();
1507 }
1508 else
1509 {
1510 QgsProviderMetadata *ogrProviderMetaData = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "ogr" ) );
1511 const QVariantMap decodedUri = ogrProviderMetaData->decodeUri( dataSourceString );
1512 tableName = decodedUri.value( QStringLiteral( "layerName" ) ).toString();
1513 if ( tableName.isEmpty() )
1514 {
1515 showWarning( tr( "Could not deduce table name from data source %1." ).arg( dataSourceString ) );
1516 }
1517 }
1518
1519 // only store feature ids
1520 const QString sql = QStringLiteral( "SELECT ROWID FROM '%1' ORDER BY ROWID DESC LIMIT %2" ).arg( tableName ).arg( addedFeatures.size() );
1521 const QList<int> newFeatureIds = sqlQueryInts( database.get(), sql );
1522 for ( int i = newFeatureIds.size() - 1; i >= 0; i-- )
1523 {
1524 const QString sql = QStringLiteral( "INSERT INTO 'log_added_features' VALUES ( %1, %2 )" )
1525 .arg( layerId )
1526 .arg( newFeatureIds.at( i ) );
1527 sqlExec( database.get(), sql );
1528 }
1529}
1530
1531void QgsOfflineEditing::committedFeaturesRemoved( const QString &qgisLayerId, const QgsFeatureIds &deletedFeatureIds )
1532{
1533 const sqlite3_database_unique_ptr database = openLoggingDb();
1534 if ( !database )
1535 return;
1536
1537 // insert log
1538 const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1539
1540 for ( const QgsFeatureId id : deletedFeatureIds )
1541 {
1542 if ( isAddedFeature( database.get(), layerId, id ) )
1543 {
1544 // remove from added features log
1545 const QString sql = QStringLiteral( "DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( id );
1546 sqlExec( database.get(), sql );
1547 }
1548 else
1549 {
1550 const QString sql = QStringLiteral( "INSERT INTO 'log_removed_features' VALUES ( %1, %2)" )
1551 .arg( layerId )
1552 .arg( id );
1553 sqlExec( database.get(), sql );
1554 }
1555 }
1556}
1557
1558void QgsOfflineEditing::committedAttributeValuesChanges( const QString &qgisLayerId, const QgsChangedAttributesMap &changedAttrsMap )
1559{
1560 const sqlite3_database_unique_ptr database = openLoggingDb();
1561 if ( !database )
1562 return;
1563
1564 // insert log
1565 const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1566 const int commitNo = getCommitNo( database.get() );
1567
1568 for ( QgsChangedAttributesMap::const_iterator cit = changedAttrsMap.begin(); cit != changedAttrsMap.end(); ++cit )
1569 {
1570 const QgsFeatureId fid = cit.key();
1571 if ( isAddedFeature( database.get(), layerId, fid ) )
1572 {
1573 // skip added features
1574 continue;
1575 }
1576 const QgsAttributeMap attrMap = cit.value();
1577 for ( QgsAttributeMap::const_iterator it = attrMap.constBegin(); it != attrMap.constEnd(); ++it )
1578 {
1579 QString value = it.value().type() == QVariant::StringList || it.value().type() == QVariant::List ? QgsJsonUtils::encodeValue( it.value() ) : it.value().toString();
1580 value.replace( QLatin1String( "'" ), QLatin1String( "''" ) ); // escape quote
1581 const QString sql = QStringLiteral( "INSERT INTO 'log_feature_updates' VALUES ( %1, %2, %3, %4, '%5' )" )
1582 .arg( layerId )
1583 .arg( commitNo )
1584 .arg( fid )
1585 .arg( it.key() ) // attribute
1586 .arg( value );
1587 sqlExec( database.get(), sql );
1588 }
1589 }
1590
1591 increaseCommitNo( database.get() );
1592}
1593
1594void QgsOfflineEditing::committedGeometriesChanges( const QString &qgisLayerId, const QgsGeometryMap &changedGeometries )
1595{
1596 const sqlite3_database_unique_ptr database = openLoggingDb();
1597 if ( !database )
1598 return;
1599
1600 // insert log
1601 const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1602 const int commitNo = getCommitNo( database.get() );
1603
1604 for ( QgsGeometryMap::const_iterator it = changedGeometries.begin(); it != changedGeometries.end(); ++it )
1605 {
1606 const QgsFeatureId fid = it.key();
1607 if ( isAddedFeature( database.get(), layerId, fid ) )
1608 {
1609 // skip added features
1610 continue;
1611 }
1612 const QgsGeometry geom = it.value();
1613 const QString sql = QStringLiteral( "INSERT INTO 'log_geometry_updates' VALUES ( %1, %2, %3, '%4' )" )
1614 .arg( layerId )
1615 .arg( commitNo )
1616 .arg( fid )
1617 .arg( geom.asWkt() );
1618 sqlExec( database.get(), sql );
1619
1620 // TODO: use WKB instead of WKT?
1621 }
1622
1623 increaseCommitNo( database.get() );
1624}
1625
1626void QgsOfflineEditing::startListenFeatureChanges()
1627{
1628 QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1629
1630 Q_ASSERT( vLayer );
1631
1632 // enable logging, check if editBuffer is not null
1633 if ( vLayer->editBuffer() )
1634 {
1635 QgsVectorLayerEditBuffer *editBuffer = vLayer->editBuffer();
1637 this, &QgsOfflineEditing::committedAttributesAdded );
1639 this, &QgsOfflineEditing::committedAttributeValuesChanges );
1641 this, &QgsOfflineEditing::committedGeometriesChanges );
1642 }
1644 this, &QgsOfflineEditing::committedFeaturesAdded );
1646 this, &QgsOfflineEditing::committedFeaturesRemoved );
1647}
1648
1649void QgsOfflineEditing::stopListenFeatureChanges()
1650{
1651 QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1652
1653 Q_ASSERT( vLayer );
1654
1655 // disable logging, check if editBuffer is not null
1656 if ( vLayer->editBuffer() )
1657 {
1658 QgsVectorLayerEditBuffer *editBuffer = vLayer->editBuffer();
1660 this, &QgsOfflineEditing::committedAttributesAdded );
1662 this, &QgsOfflineEditing::committedAttributeValuesChanges );
1664 this, &QgsOfflineEditing::committedGeometriesChanges );
1665 }
1666 disconnect( vLayer, &QgsVectorLayer::committedFeaturesAdded,
1667 this, &QgsOfflineEditing::committedFeaturesAdded );
1668 disconnect( vLayer, &QgsVectorLayer::committedFeaturesRemoved,
1669 this, &QgsOfflineEditing::committedFeaturesRemoved );
1670}
1671
1672void QgsOfflineEditing::setupLayer( QgsMapLayer *layer )
1673{
1674 Q_ASSERT( layer );
1675
1676 if ( QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( layer ) )
1677 {
1678 // detect offline layer
1679 if ( vLayer->customProperty( CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE, false ).toBool() )
1680 {
1681 connect( vLayer, &QgsVectorLayer::editingStarted, this, &QgsOfflineEditing::startListenFeatureChanges );
1682 connect( vLayer, &QgsVectorLayer::editingStopped, this, &QgsOfflineEditing::stopListenFeatureChanges );
1683 }
1684 }
1685}
1686
1687int QgsOfflineEditing::getLayerPkIdx( const QgsVectorLayer *layer ) const
1688{
1689 const QList<int> pkAttrs = layer->primaryKeyAttributes();
1690 if ( pkAttrs.length() == 1 )
1691 {
1692 const QgsField pkField = layer->fields().at( pkAttrs[0] );
1693 const QVariant::Type pkType = pkField.type();
1694
1695 if ( pkType == QVariant::String )
1696 {
1697 return pkAttrs[0];
1698 }
1699 }
1700
1701 return -1;
1702}
1703
1704QString QgsOfflineEditing::sqlEscape( QString value ) const
1705{
1706 if ( value.isNull() )
1707 return QStringLiteral( "NULL" );
1708
1709 value.replace( "'", "''" );
1710
1711 return QStringLiteral( "'%1'" ).arg( value );
1712}
WkbType
The WKB type describes the number of dimensions a geometry has.
Definition qgis.h:182
@ 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)
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.
@ NoGeometry
Geometry is not required. It may still be returned if e.g. required for a filter condition.
FilterType filterType() const
Returns the attribute/ID filter type which is currently set on this request.
@ FilterFids
Filter using feature IDs.
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
Definition qgsfeature.h:56
QgsAttributes attributes
Definition qgsfeature.h:65
QgsFeatureId id
Definition qgsfeature.h:64
void setAttributes(const QgsAttributes &attrs)
Sets the feature's attributes.
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
QString name
Definition qgsfield.h:62
int precision
Definition qgsfield.h:59
int length
Definition qgsfield.h:58
QVariant::Type type
Definition qgsfield.h:60
QVariant::Type subType() const
If the field is a collection, gets its element's type.
Definition qgsfield.cpp:145
QString comment
Definition qgsfield.h:61
void setTypeName(const QString &typeName)
Set the field type.
Definition qgsfield.cpp:231
Container of fields for a vector layer.
Definition qgsfields.h:45
int indexOf(const QString &fieldName) const
Gets the field index from the field name.
int count() const
Returns number of items.
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).
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 QgsGeometry fromWkt(const QString &wkt)
Creates a new geometry from a WKT string.
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, QVariant::Type type=QVariant::Invalid)
Parse a simple array (depth=1)
Base class for all map layer types.
Definition qgsmaplayer.h:75
QString name
Definition qgsmaplayer.h:78
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:81
QString id() const
Returns the layer's unique ID, which is used to access this layer from QgsProject.
Q_INVOKABLE void setCustomProperty(const QString &key, const QVariant &value)
Set a custom property for layer.
void setDataSource(const QString &dataSource, const QString &baseName, const QString &provider, bool loadDefaultStyleFlag=false)
Updates the data source of the 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.
void committedAttributeValuesChanges(const QString &layerId, const QgsChangedAttributesMap &changedAttributesValues)
void committedAttributesAdded(const QString &layerId, const QList< QgsField > &addedAttributes)
void committedGeometriesChanges(const QString &layerId, const QgsGeometryMap &changedGeometries)
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.
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.
bool deleteFeature(QgsFeatureId fid, DeleteContext *context=nullptr)
Deletes a feature from the layer (but does not commit it).
QgsFeatureIterator getFeatures(const QgsFeatureRequest &request=QgsFeatureRequest()) const FINAL
Queries the layer for features specified in request.
QgsFields fields() const FINAL
Returns the list of fields of this layer.
QgsAttributeList attributeList() const
Returns list of attribute indexes.
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 changeAttributeValue(QgsFeatureId fid, int field, const QVariant &newValue, const QVariant &oldValue=QVariant(), bool skipDefaultValues=false)
Changes an attribute value for a feature (but does not immediately commit the changes).
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
Definition qgsfeature.h:915
QMap< QgsFeatureId, QgsAttributeMap > QgsChangedAttributesMap
Definition qgsfeature.h:906
QList< QgsFeature > QgsFeatureList
Definition qgsfeature.h:920
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.