QGIS API Documentation 3.34.0-Prizren (ffbdd678812)
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 QVariant attr = attrs.at( it );
824 if ( layer->fields().at( it ).type() == QVariant::StringList || layer->fields().at( it ).type() == QVariant::List )
825 {
826 attr = QgsJsonUtils::encodeValue( attr );
827 }
828 newAttrs[column++] = attr;
829 }
830 f.setAttributes( newAttrs );
831
832 newLayer->addFeature( f );
833
834 emit progressUpdated( featureCount++ );
835 }
836 if ( newLayer->commitChanges() )
837 {
839 featureCount = 1;
840
841 // update feature id lookup
842 const int layerId = getOrCreateLayerId( db, layer->id() );
843 QList<QgsFeatureId> offlineFeatureIds;
844
845 QgsFeatureIterator fit = newLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setNoAttributes() );
846 while ( fit.nextFeature( f ) )
847 {
848 offlineFeatureIds << f.id();
849 }
850
851 // NOTE: insert fids in this loop, as the db is locked during newLayer->nextFeature()
852 sqlExec( db, QStringLiteral( "BEGIN" ) );
853 const int remoteCount = remoteFeatureIds.size();
854 for ( int i = 0; i < remoteCount; i++ )
855 {
856 // Check if the online feature has been fetched (WFS download aborted for some reason)
857 if ( i < offlineFeatureIds.count() )
858 {
859 addFidLookup( db, layerId, offlineFeatureIds.at( i ), remoteFeatureIds.at( i ), remoteFeaturePks.at( i ) );
860 }
861 else
862 {
863 showWarning( tr( "Feature cannot be copied to the offline layer, please check if the online layer '%1' is still accessible." ).arg( layer->name() ) );
864 return;
865 }
866 emit progressUpdated( featureCount++ );
867 }
868 sqlExec( db, QStringLiteral( "COMMIT" ) );
869 }
870 else
871 {
872 showWarning( newLayer->commitErrors().join( QLatin1Char( '\n' ) ) );
873 }
874
875 // mark as offline layer
877
878 // store original layer source and information
882 layer->setCustomProperty( CUSTOM_PROPERTY_LAYERNAME_SUFFIX, layerNameSuffix );
883
884 //remove constrainst of fields that use defaultValueClauses from provider on original
885 const QgsFields fields = layer->fields();
886 QStringList notNullFieldNames;
887 for ( const QgsField &field : fields )
888 {
889 if ( !layer->dataProvider()->defaultValueClause( layer->fields().fieldOriginIndex( layer->fields().indexOf( field.name() ) ) ).isEmpty() )
890 {
891 notNullFieldNames << field.name();
892 }
893 }
894
895 layer->setDataSource( newLayer->source(), newLayer->name(), newLayer->dataProvider()->name() );
896
897 for ( const QgsField &field : fields ) //QString &fieldName : fieldsToRemoveConstraint )
898 {
899 const int index = layer->fields().indexOf( field.name() );
900 if ( index > -1 )
901 {
902 // restore unique value constraints coming from original data provider
903 if ( field.constraints().constraints() & QgsFieldConstraints::ConstraintUnique )
905
906 // remove any undesired not null constraints coming from original data provider
907 if ( notNullFieldNames.contains( field.name() ) )
908 {
909 notNullFieldNames.removeAll( field.name() );
911 }
912 }
913 }
914
915 setupLayer( layer );
916 }
917 return;
918}
919
920void QgsOfflineEditing::applyAttributesAdded( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
921{
922 Q_ASSERT( remoteLayer );
923
924 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 );
925 QList<QgsField> fields = sqlQueryAttributesAdded( db, sql );
926
927 const QgsVectorDataProvider *provider = remoteLayer->dataProvider();
928 const QList<QgsVectorDataProvider::NativeType> nativeTypes = provider->nativeTypes();
929
930 // NOTE: uses last matching QVariant::Type of nativeTypes
931 QMap < QVariant::Type, QString /*typeName*/ > typeNameLookup;
932 for ( int i = 0; i < nativeTypes.size(); i++ )
933 {
934 const QgsVectorDataProvider::NativeType nativeType = nativeTypes.at( i );
935 typeNameLookup[ nativeType.mType ] = nativeType.mTypeName;
936 }
937
938 emit progressModeSet( QgsOfflineEditing::AddFields, fields.size() );
939
940 for ( int i = 0; i < fields.size(); i++ )
941 {
942 // lookup typename from layer provider
943 QgsField field = fields[i];
944 if ( typeNameLookup.contains( field.type() ) )
945 {
946 const QString typeName = typeNameLookup[ field.type()];
947 field.setTypeName( typeName );
948 remoteLayer->addAttribute( field );
949 }
950 else
951 {
952 showWarning( QStringLiteral( "Could not add attribute '%1' of type %2" ).arg( field.name() ).arg( field.type() ) );
953 }
954
955 emit progressUpdated( i + 1 );
956 }
957}
958
959void QgsOfflineEditing::applyFeaturesAdded( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
960{
961 Q_ASSERT( offlineLayer );
962 Q_ASSERT( remoteLayer );
963
964 const QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
965 const QList<int> featureIdInts = sqlQueryInts( db, sql );
966 QgsFeatureIds newFeatureIds;
967 for ( const int id : featureIdInts )
968 {
969 newFeatureIds << id;
970 }
971
972 QgsExpressionContext context = remoteLayer->createExpressionContext();
973
974 // get new features from offline layer
975 QgsFeatureList features;
976 QgsFeatureIterator it = offlineLayer->getFeatures( QgsFeatureRequest().setFilterFids( newFeatureIds ) );
977 QgsFeature feature;
978 while ( it.nextFeature( feature ) )
979 {
980 features << feature;
981 }
982
983 // copy features to remote layer
984 emit progressModeSet( QgsOfflineEditing::AddFeatures, features.size() );
985
986 int i = 1;
987 const int newAttrsCount = remoteLayer->fields().count();
988 for ( QgsFeatureList::iterator it = features.begin(); it != features.end(); ++it )
989 {
990 // NOTE: SpatiaLite provider ignores position of geometry column
991 // restore gap in QgsAttributeMap if geometry column is not last (WORKAROUND)
992 const QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
993 QgsAttributes newAttrs( newAttrsCount );
994 const QgsAttributes attrs = it->attributes();
995 for ( int it = 0; it < attrs.count(); ++it )
996 {
997 const int remoteAttributeIndex = attrLookup.value( it, -1 );
998 // if virtual or non existing field
999 if ( remoteAttributeIndex == -1 )
1000 continue;
1001 QVariant attr = attrs.at( it );
1002 if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::StringList )
1003 {
1004 if ( attr.type() == QVariant::StringList || attr.type() == QVariant::List )
1005 {
1006 attr = attr.toStringList();
1007 }
1008 else
1009 {
1010 attr = QgsJsonUtils::parseArray( attr.toString(), QVariant::String );
1011 }
1012 }
1013 else if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::List )
1014 {
1015 if ( attr.type() == QVariant::StringList || attr.type() == QVariant::List )
1016 {
1017 attr = attr.toList();
1018 }
1019 else
1020 {
1021 attr = QgsJsonUtils::parseArray( attr.toString(), remoteLayer->fields().at( remoteAttributeIndex ).subType() );
1022 }
1023 }
1024 newAttrs[ remoteAttributeIndex ] = attr;
1025 }
1026
1027 // respect constraints and provider default values
1028 QgsFeature f = QgsVectorLayerUtils::createFeature( remoteLayer, it->geometry(), newAttrs.toMap(), &context );
1029 remoteLayer->addFeature( f );
1030
1031 emit progressUpdated( i++ );
1032 }
1033}
1034
1035void QgsOfflineEditing::applyFeaturesRemoved( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
1036{
1037 Q_ASSERT( remoteLayer );
1038
1039 const QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_removed_features' WHERE \"layer_id\" = %1" ).arg( layerId );
1040 const QgsFeatureIds values = sqlQueryFeaturesRemoved( db, sql );
1041
1043
1044 int i = 1;
1045 for ( QgsFeatureIds::const_iterator it = values.constBegin(); it != values.constEnd(); ++it )
1046 {
1047 const QgsFeatureId fid = remoteFid( db, layerId, *it, remoteLayer );
1048 remoteLayer->deleteFeature( fid );
1049
1050 emit progressUpdated( i++ );
1051 }
1052}
1053
1054void QgsOfflineEditing::applyAttributeValueChanges( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
1055{
1056 Q_ASSERT( offlineLayer );
1057 Q_ASSERT( remoteLayer );
1058
1059 const QString sql = QStringLiteral( "SELECT \"fid\", \"attr\", \"value\" FROM 'log_feature_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2 " ).arg( layerId ).arg( commitNo );
1060 const AttributeValueChanges values = sqlQueryAttributeValueChanges( db, sql );
1061
1063
1064 QMap<int, int> attrLookup = attributeLookup( offlineLayer, remoteLayer );
1065
1066 for ( int i = 0; i < values.size(); i++ )
1067 {
1068 const QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid, remoteLayer );
1069 QgsDebugMsgLevel( QStringLiteral( "Offline changeAttributeValue %1 = %2" ).arg( attrLookup[ values.at( i ).attr ] ).arg( values.at( i ).value ), 4 );
1070
1071 const int remoteAttributeIndex = attrLookup[ values.at( i ).attr ];
1072 QVariant attr = values.at( i ).value;
1073 if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::StringList )
1074 {
1075 attr = QgsJsonUtils::parseArray( attr.toString(), QVariant::String );
1076 }
1077 else if ( remoteLayer->fields().at( remoteAttributeIndex ).type() == QVariant::List )
1078 {
1079 attr = QgsJsonUtils::parseArray( attr.toString(), remoteLayer->fields().at( remoteAttributeIndex ).subType() );
1080 }
1081
1082 remoteLayer->changeAttributeValue( fid, remoteAttributeIndex, attr );
1083
1084 emit progressUpdated( i + 1 );
1085 }
1086}
1087
1088void QgsOfflineEditing::applyGeometryChanges( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId, int commitNo )
1089{
1090 Q_ASSERT( remoteLayer );
1091
1092 const QString sql = QStringLiteral( "SELECT \"fid\", \"geom_wkt\" FROM 'log_geometry_updates' WHERE \"layer_id\" = %1 AND \"commit_no\" = %2" ).arg( layerId ).arg( commitNo );
1093 const GeometryChanges values = sqlQueryGeometryChanges( db, sql );
1094
1096
1097 for ( int i = 0; i < values.size(); i++ )
1098 {
1099 const QgsFeatureId fid = remoteFid( db, layerId, values.at( i ).fid, remoteLayer );
1100 QgsGeometry newGeom = QgsGeometry::fromWkt( values.at( i ).geom_wkt );
1101 remoteLayer->changeGeometry( fid, newGeom );
1102
1103 emit progressUpdated( i + 1 );
1104 }
1105}
1106
1107void QgsOfflineEditing::updateFidLookup( QgsVectorLayer *remoteLayer, sqlite3 *db, int layerId )
1108{
1109 Q_ASSERT( remoteLayer );
1110
1111 // update fid lookup for added features
1112
1113 // get remote added fids
1114 // NOTE: use QMap for sorted fids
1115 QMap < QgsFeatureId, QString > newRemoteFids;
1116 QgsFeature f;
1117
1118 QgsFeatureIterator fit = remoteLayer->getFeatures( QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry ).setNoAttributes() );
1119
1121
1122 const int remotePkIdx = getLayerPkIdx( remoteLayer );
1123
1124 int i = 1;
1125 while ( fit.nextFeature( f ) )
1126 {
1127 if ( offlineFid( db, layerId, f.id() ) == -1 )
1128 {
1129 newRemoteFids[ f.id()] = remotePkIdx >= 0 ? f.attribute( remotePkIdx ).toString() : QString();
1130 }
1131
1132 emit progressUpdated( i++ );
1133 }
1134
1135 // get local added fids
1136 // NOTE: fids are sorted
1137 const QString sql = QStringLiteral( "SELECT \"fid\" FROM 'log_added_features' WHERE \"layer_id\" = %1" ).arg( layerId );
1138 const QList<int> newOfflineFids = sqlQueryInts( db, sql );
1139
1140 if ( newRemoteFids.size() != newOfflineFids.size() )
1141 {
1142 //showWarning( QString( "Different number of new features on offline layer (%1) and remote layer (%2)" ).arg(newOfflineFids.size()).arg(newRemoteFids.size()) );
1143 }
1144 else
1145 {
1146 // add new fid lookups
1147 i = 0;
1148 sqlExec( db, QStringLiteral( "BEGIN" ) );
1149 for ( QMap<QgsFeatureId, QString>::const_iterator it = newRemoteFids.constBegin(); it != newRemoteFids.constEnd(); ++it )
1150 {
1151 addFidLookup( db, layerId, newOfflineFids.at( i++ ), it.key(), it.value() );
1152 }
1153 sqlExec( db, QStringLiteral( "COMMIT" ) );
1154 }
1155}
1156
1157// NOTE: use this to map column indices in case the remote geometry column is not last
1158QMap<int, int> QgsOfflineEditing::attributeLookup( QgsVectorLayer *offlineLayer, QgsVectorLayer *remoteLayer )
1159{
1160 Q_ASSERT( offlineLayer );
1161 Q_ASSERT( remoteLayer );
1162
1163 const QgsAttributeList &offlineAttrs = offlineLayer->attributeList();
1164
1165 QMap < int /*offline attr*/, int /*remote attr*/ > attrLookup;
1166 // NOTE: though offlineAttrs can have new attributes not yet synced, we take the amount of offlineAttrs
1167 // because we anyway only add mapping for the fields existing in remoteLayer (this because it could contain fid on 0)
1168 for ( int i = 0; i < offlineAttrs.size(); i++ )
1169 {
1170 if ( remoteLayer->fields().lookupField( offlineLayer->fields().field( i ).name() ) >= 0 )
1171 attrLookup.insert( offlineAttrs.at( i ), remoteLayer->fields().indexOf( offlineLayer->fields().field( i ).name() ) );
1172 }
1173
1174 return attrLookup;
1175}
1176
1177void QgsOfflineEditing::showWarning( const QString &message )
1178{
1179 emit warning( tr( "Offline Editing Plugin" ), message );
1180}
1181
1182sqlite3_database_unique_ptr QgsOfflineEditing::openLoggingDb()
1183{
1186 if ( !dbPath.isEmpty() )
1187 {
1188 const QString absoluteDbPath = QgsProject::instance()->readPath( dbPath );
1189 const int rc = database.open( absoluteDbPath );
1190 if ( rc != SQLITE_OK )
1191 {
1192 QgsDebugError( QStringLiteral( "Could not open the SpatiaLite logging database" ) );
1193 showWarning( tr( "Could not open the SpatiaLite logging database" ) );
1194 }
1195 }
1196 else
1197 {
1198 QgsDebugError( QStringLiteral( "dbPath is empty!" ) );
1199 }
1200 return database;
1201}
1202
1203int QgsOfflineEditing::getOrCreateLayerId( sqlite3 *db, const QString &qgisLayerId )
1204{
1205 QString sql = QStringLiteral( "SELECT \"id\" FROM 'log_layer_ids' WHERE \"qgis_id\" = '%1'" ).arg( qgisLayerId );
1206 int layerId = sqlQueryInt( db, sql, -1 );
1207 if ( layerId == -1 )
1208 {
1209 // next layer id
1210 sql = QStringLiteral( "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'layer_id'" );
1211 const int newLayerId = sqlQueryInt( db, sql, -1 );
1212
1213 // insert layer
1214 sql = QStringLiteral( "INSERT INTO 'log_layer_ids' VALUES (%1, '%2')" ).arg( newLayerId ).arg( qgisLayerId );
1215 sqlExec( db, sql );
1216
1217 // increase layer_id
1218 // TODO: use trigger for auto increment?
1219 sql = QStringLiteral( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'layer_id'" ).arg( newLayerId + 1 );
1220 sqlExec( db, sql );
1221
1222 layerId = newLayerId;
1223 }
1224
1225 return layerId;
1226}
1227
1228int QgsOfflineEditing::getCommitNo( sqlite3 *db )
1229{
1230 const QString sql = QStringLiteral( "SELECT \"last_index\" FROM 'log_indices' WHERE \"name\" = 'commit_no'" );
1231 return sqlQueryInt( db, sql, -1 );
1232}
1233
1234void QgsOfflineEditing::increaseCommitNo( sqlite3 *db )
1235{
1236 const QString sql = QStringLiteral( "UPDATE 'log_indices' SET 'last_index' = %1 WHERE \"name\" = 'commit_no'" ).arg( getCommitNo( db ) + 1 );
1237 sqlExec( db, sql );
1238}
1239
1240void QgsOfflineEditing::addFidLookup( sqlite3 *db, int layerId, QgsFeatureId offlineFid, QgsFeatureId remoteFid, const QString &remotePk )
1241{
1242 const QString sql = QStringLiteral( "INSERT INTO 'log_fids' VALUES ( %1, %2, %3, %4 )" ).arg( layerId ).arg( offlineFid ).arg( remoteFid ).arg( sqlEscape( remotePk ) );
1243 sqlExec( db, sql );
1244}
1245
1246QgsFeatureId QgsOfflineEditing::remoteFid( sqlite3 *db, int layerId, QgsFeatureId offlineFid, QgsVectorLayer *remoteLayer )
1247{
1248 const int pkIdx = getLayerPkIdx( remoteLayer );
1249
1250 if ( pkIdx == -1 )
1251 {
1252 const QString sql = QStringLiteral( "SELECT \"remote_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"offline_fid\" = %2" ).arg( layerId ).arg( offlineFid );
1253 return sqlQueryInt( db, sql, -1 );
1254 }
1255
1256 const QString sql = QStringLiteral( "SELECT \"remote_pk\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"offline_fid\" = %2" ).arg( layerId ).arg( offlineFid );
1257 QString defaultValue;
1258 const QString pkValue = sqlQueryStr( db, sql, defaultValue );
1259
1260 if ( pkValue.isNull() )
1261 {
1262 return -1;
1263 }
1264
1265 const QString pkFieldName = remoteLayer->fields().at( pkIdx ).name();
1266 QgsFeatureIterator fit = remoteLayer->getFeatures( QStringLiteral( " %1 = %2 " ).arg( pkFieldName ).arg( sqlEscape( pkValue ) ) );
1267 QgsFeature f;
1268 while ( fit.nextFeature( f ) )
1269 return f.id();
1270
1271 return -1;
1272}
1273
1274QgsFeatureId QgsOfflineEditing::offlineFid( sqlite3 *db, int layerId, QgsFeatureId remoteFid )
1275{
1276 const QString sql = QStringLiteral( "SELECT \"offline_fid\" FROM 'log_fids' WHERE \"layer_id\" = %1 AND \"remote_fid\" = %2" ).arg( layerId ).arg( remoteFid );
1277 return sqlQueryInt( db, sql, -1 );
1278}
1279
1280bool QgsOfflineEditing::isAddedFeature( sqlite3 *db, int layerId, QgsFeatureId fid )
1281{
1282 const QString sql = QStringLiteral( "SELECT COUNT(\"fid\") FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( fid );
1283 return ( sqlQueryInt( db, sql, 0 ) > 0 );
1284}
1285
1286int QgsOfflineEditing::sqlExec( sqlite3 *db, const QString &sql )
1287{
1288 char *errmsg = nullptr;
1289 const int rc = sqlite3_exec( db, sql.toUtf8(), nullptr, nullptr, &errmsg );
1290 if ( rc != SQLITE_OK )
1291 {
1292 showWarning( errmsg );
1293 }
1294 return rc;
1295}
1296
1297QString QgsOfflineEditing::sqlQueryStr( sqlite3 *db, const QString &sql, QString &defaultValue )
1298{
1299 sqlite3_stmt *stmt = nullptr;
1300 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1301 {
1302 showWarning( sqlite3_errmsg( db ) );
1303 return defaultValue;
1304 }
1305
1306 QString value = defaultValue;
1307 const int ret = sqlite3_step( stmt );
1308 if ( ret == SQLITE_ROW )
1309 {
1310 value = QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 0 ) ) );
1311 }
1312 sqlite3_finalize( stmt );
1313
1314 return value;
1315}
1316
1317int QgsOfflineEditing::sqlQueryInt( sqlite3 *db, const QString &sql, int defaultValue )
1318{
1319 sqlite3_stmt *stmt = nullptr;
1320 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1321 {
1322 showWarning( sqlite3_errmsg( db ) );
1323 return defaultValue;
1324 }
1325
1326 int value = defaultValue;
1327 const int ret = sqlite3_step( stmt );
1328 if ( ret == SQLITE_ROW )
1329 {
1330 value = sqlite3_column_int( stmt, 0 );
1331 }
1332 sqlite3_finalize( stmt );
1333
1334 return value;
1335}
1336
1337QList<int> QgsOfflineEditing::sqlQueryInts( sqlite3 *db, const QString &sql )
1338{
1339 QList<int> values;
1340
1341 sqlite3_stmt *stmt = nullptr;
1342 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1343 {
1344 showWarning( sqlite3_errmsg( db ) );
1345 return values;
1346 }
1347
1348 int ret = sqlite3_step( stmt );
1349 while ( ret == SQLITE_ROW )
1350 {
1351 values << sqlite3_column_int( stmt, 0 );
1352
1353 ret = sqlite3_step( stmt );
1354 }
1355 sqlite3_finalize( stmt );
1356
1357 return values;
1358}
1359
1360QList<QgsField> QgsOfflineEditing::sqlQueryAttributesAdded( sqlite3 *db, const QString &sql )
1361{
1362 QList<QgsField> values;
1363
1364 sqlite3_stmt *stmt = nullptr;
1365 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1366 {
1367 showWarning( sqlite3_errmsg( db ) );
1368 return values;
1369 }
1370
1371 int ret = sqlite3_step( stmt );
1372 while ( ret == SQLITE_ROW )
1373 {
1374 const QgsField field( QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 0 ) ) ),
1375 static_cast< QVariant::Type >( sqlite3_column_int( stmt, 1 ) ),
1376 QString(), // typeName
1377 sqlite3_column_int( stmt, 2 ),
1378 sqlite3_column_int( stmt, 3 ),
1379 QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 4 ) ) ) );
1380 values << field;
1381
1382 ret = sqlite3_step( stmt );
1383 }
1384 sqlite3_finalize( stmt );
1385
1386 return values;
1387}
1388
1389QgsFeatureIds QgsOfflineEditing::sqlQueryFeaturesRemoved( sqlite3 *db, const QString &sql )
1390{
1391 QgsFeatureIds values;
1392
1393 sqlite3_stmt *stmt = nullptr;
1394 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1395 {
1396 showWarning( sqlite3_errmsg( db ) );
1397 return values;
1398 }
1399
1400 int ret = sqlite3_step( stmt );
1401 while ( ret == SQLITE_ROW )
1402 {
1403 values << sqlite3_column_int( stmt, 0 );
1404
1405 ret = sqlite3_step( stmt );
1406 }
1407 sqlite3_finalize( stmt );
1408
1409 return values;
1410}
1411
1412QgsOfflineEditing::AttributeValueChanges QgsOfflineEditing::sqlQueryAttributeValueChanges( sqlite3 *db, const QString &sql )
1413{
1414 AttributeValueChanges values;
1415
1416 sqlite3_stmt *stmt = nullptr;
1417 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1418 {
1419 showWarning( sqlite3_errmsg( db ) );
1420 return values;
1421 }
1422
1423 int ret = sqlite3_step( stmt );
1424 while ( ret == SQLITE_ROW )
1425 {
1426 AttributeValueChange change;
1427 change.fid = sqlite3_column_int( stmt, 0 );
1428 change.attr = sqlite3_column_int( stmt, 1 );
1429 change.value = QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 2 ) ) );
1430 values << change;
1431
1432 ret = sqlite3_step( stmt );
1433 }
1434 sqlite3_finalize( stmt );
1435
1436 return values;
1437}
1438
1439QgsOfflineEditing::GeometryChanges QgsOfflineEditing::sqlQueryGeometryChanges( sqlite3 *db, const QString &sql )
1440{
1441 GeometryChanges values;
1442
1443 sqlite3_stmt *stmt = nullptr;
1444 if ( sqlite3_prepare_v2( db, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK )
1445 {
1446 showWarning( sqlite3_errmsg( db ) );
1447 return values;
1448 }
1449
1450 int ret = sqlite3_step( stmt );
1451 while ( ret == SQLITE_ROW )
1452 {
1453 GeometryChange change;
1454 change.fid = sqlite3_column_int( stmt, 0 );
1455 change.geom_wkt = QString( reinterpret_cast< const char * >( sqlite3_column_text( stmt, 1 ) ) );
1456 values << change;
1457
1458 ret = sqlite3_step( stmt );
1459 }
1460 sqlite3_finalize( stmt );
1461
1462 return values;
1463}
1464
1465void QgsOfflineEditing::committedAttributesAdded( const QString &qgisLayerId, const QList<QgsField> &addedAttributes )
1466{
1467 const sqlite3_database_unique_ptr database = openLoggingDb();
1468 if ( !database )
1469 return;
1470
1471 // insert log
1472 const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1473 const int commitNo = getCommitNo( database.get() );
1474
1475 for ( const QgsField &field : addedAttributes )
1476 {
1477 const QString sql = QStringLiteral( "INSERT INTO 'log_added_attrs' VALUES ( %1, %2, '%3', %4, %5, %6, '%7' )" )
1478 .arg( layerId )
1479 .arg( commitNo )
1480 .arg( field.name() )
1481 .arg( field.type() )
1482 .arg( field.length() )
1483 .arg( field.precision() )
1484 .arg( field.comment() );
1485 sqlExec( database.get(), sql );
1486 }
1487
1488 increaseCommitNo( database.get() );
1489}
1490
1491void QgsOfflineEditing::committedFeaturesAdded( const QString &qgisLayerId, const QgsFeatureList &addedFeatures )
1492{
1493 const sqlite3_database_unique_ptr database = openLoggingDb();
1494 if ( !database )
1495 return;
1496
1497 // insert log
1498 const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1499
1500 // get new feature ids from db
1501 QgsMapLayer *layer = QgsProject::instance()->mapLayer( qgisLayerId );
1502 const QString dataSourceString = layer->source();
1503 const QgsDataSourceUri uri = QgsDataSourceUri( dataSourceString );
1504
1506 QString tableName;
1507
1508 if ( !offlinePath.contains( ".gpkg" ) )
1509 {
1510 tableName = uri.table();
1511 }
1512 else
1513 {
1514 QgsProviderMetadata *ogrProviderMetaData = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "ogr" ) );
1515 const QVariantMap decodedUri = ogrProviderMetaData->decodeUri( dataSourceString );
1516 tableName = decodedUri.value( QStringLiteral( "layerName" ) ).toString();
1517 if ( tableName.isEmpty() )
1518 {
1519 showWarning( tr( "Could not deduce table name from data source %1." ).arg( dataSourceString ) );
1520 }
1521 }
1522
1523 // only store feature ids
1524 const QString sql = QStringLiteral( "SELECT ROWID FROM '%1' ORDER BY ROWID DESC LIMIT %2" ).arg( tableName ).arg( addedFeatures.size() );
1525 const QList<int> newFeatureIds = sqlQueryInts( database.get(), sql );
1526 for ( int i = newFeatureIds.size() - 1; i >= 0; i-- )
1527 {
1528 const QString sql = QStringLiteral( "INSERT INTO 'log_added_features' VALUES ( %1, %2 )" )
1529 .arg( layerId )
1530 .arg( newFeatureIds.at( i ) );
1531 sqlExec( database.get(), sql );
1532 }
1533}
1534
1535void QgsOfflineEditing::committedFeaturesRemoved( const QString &qgisLayerId, const QgsFeatureIds &deletedFeatureIds )
1536{
1537 const sqlite3_database_unique_ptr database = openLoggingDb();
1538 if ( !database )
1539 return;
1540
1541 // insert log
1542 const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1543
1544 for ( const QgsFeatureId id : deletedFeatureIds )
1545 {
1546 if ( isAddedFeature( database.get(), layerId, id ) )
1547 {
1548 // remove from added features log
1549 const QString sql = QStringLiteral( "DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( id );
1550 sqlExec( database.get(), sql );
1551 }
1552 else
1553 {
1554 const QString sql = QStringLiteral( "INSERT INTO 'log_removed_features' VALUES ( %1, %2)" )
1555 .arg( layerId )
1556 .arg( id );
1557 sqlExec( database.get(), sql );
1558 }
1559 }
1560}
1561
1562void QgsOfflineEditing::committedAttributeValuesChanges( const QString &qgisLayerId, const QgsChangedAttributesMap &changedAttrsMap )
1563{
1564 const sqlite3_database_unique_ptr database = openLoggingDb();
1565 if ( !database )
1566 return;
1567
1568 // insert log
1569 const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1570 const int commitNo = getCommitNo( database.get() );
1571
1572 for ( QgsChangedAttributesMap::const_iterator cit = changedAttrsMap.begin(); cit != changedAttrsMap.end(); ++cit )
1573 {
1574 const QgsFeatureId fid = cit.key();
1575 if ( isAddedFeature( database.get(), layerId, fid ) )
1576 {
1577 // skip added features
1578 continue;
1579 }
1580 const QgsAttributeMap attrMap = cit.value();
1581 for ( QgsAttributeMap::const_iterator it = attrMap.constBegin(); it != attrMap.constEnd(); ++it )
1582 {
1583 QString value = it.value().type() == QVariant::StringList || it.value().type() == QVariant::List ? QgsJsonUtils::encodeValue( it.value() ) : it.value().toString();
1584 value.replace( QLatin1String( "'" ), QLatin1String( "''" ) ); // escape quote
1585 const QString sql = QStringLiteral( "INSERT INTO 'log_feature_updates' VALUES ( %1, %2, %3, %4, '%5' )" )
1586 .arg( layerId )
1587 .arg( commitNo )
1588 .arg( fid )
1589 .arg( it.key() ) // attribute
1590 .arg( value );
1591 sqlExec( database.get(), sql );
1592 }
1593 }
1594
1595 increaseCommitNo( database.get() );
1596}
1597
1598void QgsOfflineEditing::committedGeometriesChanges( const QString &qgisLayerId, const QgsGeometryMap &changedGeometries )
1599{
1600 const sqlite3_database_unique_ptr database = openLoggingDb();
1601 if ( !database )
1602 return;
1603
1604 // insert log
1605 const int layerId = getOrCreateLayerId( database.get(), qgisLayerId );
1606 const int commitNo = getCommitNo( database.get() );
1607
1608 for ( QgsGeometryMap::const_iterator it = changedGeometries.begin(); it != changedGeometries.end(); ++it )
1609 {
1610 const QgsFeatureId fid = it.key();
1611 if ( isAddedFeature( database.get(), layerId, fid ) )
1612 {
1613 // skip added features
1614 continue;
1615 }
1616 const QgsGeometry geom = it.value();
1617 const QString sql = QStringLiteral( "INSERT INTO 'log_geometry_updates' VALUES ( %1, %2, %3, '%4' )" )
1618 .arg( layerId )
1619 .arg( commitNo )
1620 .arg( fid )
1621 .arg( geom.asWkt() );
1622 sqlExec( database.get(), sql );
1623
1624 // TODO: use WKB instead of WKT?
1625 }
1626
1627 increaseCommitNo( database.get() );
1628}
1629
1630void QgsOfflineEditing::startListenFeatureChanges()
1631{
1632 QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1633
1634 Q_ASSERT( vLayer );
1635
1636 // enable logging, check if editBuffer is not null
1637 if ( vLayer->editBuffer() )
1638 {
1639 QgsVectorLayerEditBuffer *editBuffer = vLayer->editBuffer();
1641 this, &QgsOfflineEditing::committedAttributesAdded );
1643 this, &QgsOfflineEditing::committedAttributeValuesChanges );
1645 this, &QgsOfflineEditing::committedGeometriesChanges );
1646 }
1648 this, &QgsOfflineEditing::committedFeaturesAdded );
1650 this, &QgsOfflineEditing::committedFeaturesRemoved );
1651}
1652
1653void QgsOfflineEditing::stopListenFeatureChanges()
1654{
1655 QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( sender() );
1656
1657 Q_ASSERT( vLayer );
1658
1659 // disable logging, check if editBuffer is not null
1660 if ( vLayer->editBuffer() )
1661 {
1662 QgsVectorLayerEditBuffer *editBuffer = vLayer->editBuffer();
1664 this, &QgsOfflineEditing::committedAttributesAdded );
1666 this, &QgsOfflineEditing::committedAttributeValuesChanges );
1668 this, &QgsOfflineEditing::committedGeometriesChanges );
1669 }
1670 disconnect( vLayer, &QgsVectorLayer::committedFeaturesAdded,
1671 this, &QgsOfflineEditing::committedFeaturesAdded );
1672 disconnect( vLayer, &QgsVectorLayer::committedFeaturesRemoved,
1673 this, &QgsOfflineEditing::committedFeaturesRemoved );
1674}
1675
1676void QgsOfflineEditing::setupLayer( QgsMapLayer *layer )
1677{
1678 Q_ASSERT( layer );
1679
1680 if ( QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( layer ) )
1681 {
1682 // detect offline layer
1683 if ( vLayer->customProperty( CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE, false ).toBool() )
1684 {
1685 connect( vLayer, &QgsVectorLayer::editingStarted, this, &QgsOfflineEditing::startListenFeatureChanges );
1686 connect( vLayer, &QgsVectorLayer::editingStopped, this, &QgsOfflineEditing::stopListenFeatureChanges );
1687 }
1688 }
1689}
1690
1691int QgsOfflineEditing::getLayerPkIdx( const QgsVectorLayer *layer ) const
1692{
1693 const QList<int> pkAttrs = layer->primaryKeyAttributes();
1694 if ( pkAttrs.length() == 1 )
1695 {
1696 const QgsField pkField = layer->fields().at( pkAttrs[0] );
1697 const QVariant::Type pkType = pkField.type();
1698
1699 if ( pkType == QVariant::String )
1700 {
1701 return pkAttrs[0];
1702 }
1703 }
1704
1705 return -1;
1706}
1707
1708QString QgsOfflineEditing::sqlEscape( QString value ) const
1709{
1710 if ( value.isNull() )
1711 return QStringLiteral( "NULL" );
1712
1713 value.replace( "'", "''" );
1714
1715 return QStringLiteral( "'%1'" ).arg( value );
1716}
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:74
QString name
Definition qgsmaplayer.h:77
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:80
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.