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