diff --git a/images/images.qrc b/images/images.qrc
index ecf3bb0a3bd..e71570a33fe 100644
--- a/images/images.qrc
+++ b/images/images.qrc
@@ -665,6 +665,7 @@
themes/default/mActionAddAfsLayer.svg
themes/default/mIconFormSelect.svg
themes/default/mActionMultiEdit.svg
+ themes/default/dependencies.svg
qgis_tips/symbol_levels.png
diff --git a/images/themes/default/dependencies.svg b/images/themes/default/dependencies.svg
new file mode 100644
index 00000000000..9a89b39261c
--- /dev/null
+++ b/images/themes/default/dependencies.svg
@@ -0,0 +1,151 @@
+
+
+
+
diff --git a/python/core/qgsmaplayer.sip b/python/core/qgsmaplayer.sip
index af4d8e91cb8..e78fe85fd20 100644
--- a/python/core/qgsmaplayer.sip
+++ b/python/core/qgsmaplayer.sip
@@ -676,6 +676,23 @@ class QgsMapLayer : QObject
*/
void emitStyleChanged();
+ /**
+ * Sets the list of layers that may modify data/geometries of this layer when modified.
+ * @see dataDependencies
+ *
+ * @param layersIds IDs of the layers that this layer depends on
+ * @returns false if a dependency cycle has been detected (the change dependency set is not changed in that case)
+ */
+ virtual bool setDataDependencies( const QSet& layersIds );
+
+ /**
+ * Gets the list of layers that may modify data/geometries of this layer when modified.
+ * @see setDataDependencies
+ *
+ * @returns IDs of the layers that this layer depends on
+ */
+ virtual QSet dataDependencies() const;
+
signals:
/** Emit a signal with status (e.g. to be caught by QgisApp and display a msg on status bar) */
@@ -766,4 +783,7 @@ class QgsMapLayer : QObject
void appendError( const QgsErrorMessage &error );
/** Set error message */
void setError( const QgsError &error );
+
+ //! Checks if new change dependency candidates introduce a cycle
+ bool hasDataDependencyCycle( const QSet& layersIds ) const;
};
diff --git a/python/core/qgsvectorlayer.sip b/python/core/qgsvectorlayer.sip
index d053680320d..b079a273195 100644
--- a/python/core/qgsvectorlayer.sip
+++ b/python/core/qgsvectorlayer.sip
@@ -422,10 +422,30 @@ class QgsVectorLayer : QgsMapLayer
const QList vectorJoins() const;
/**
- * Get the list of layer ids on which this layer depends. This in particular determines the order of layer loading.
+ * Gets the list of layer ids on which this layer depends, as returned by the provider.
+ * This in particular determines the order of layer loading.
*/
virtual QSet layerDependencies() const;
+ /**
+ * Sets the list of layers that may modify data/geometries of this layer when modified.
+ * This is meant mainly to declare database triggers between layers.
+ * When one of these layers is modified (feature added/deleted or geometry changed),
+ * dataChanged() will be emitted, allowing users of this layer to refresh / update it.
+ *
+ * @param layersIds IDs of the layers that this layer depends on
+ * @returns false if a dependency cycle has been detected (the change dependency set is not changed in that case)
+ */
+ bool setDataDependencies( const QSet& layersIds );
+
+ /**
+ * Gets the list of layers that may modify data/geometries of this layer when modified.
+ * @see setDataDependencies
+ *
+ * @returns IDs of the layers that this layer depends on
+ */
+ QSet dataDependencies() const;
+
/**
* Add a new field which is calculated by the expression specified
*
diff --git a/src/app/qgsvectorlayerproperties.cpp b/src/app/qgsvectorlayerproperties.cpp
index 9a19ba2b128..f1f6362bcac 100644
--- a/src/app/qgsvectorlayerproperties.cpp
+++ b/src/app/qgsvectorlayerproperties.cpp
@@ -52,6 +52,7 @@
#include "qgsdatasourceuri.h"
#include "qgsrenderer.h"
#include "qgsexpressioncontext.h"
+#include "layertree/qgslayertreelayer.h"
#include
#include
@@ -291,6 +292,23 @@ QgsVectorLayerProperties::QgsVectorLayerProperties(
QString title = QString( tr( "Layer Properties - %1" ) ).arg( mLayer->name() );
restoreOptionsBaseUi( title );
+
+ mLayersDependenciesTreeGroup.reset( QgsProject::instance()->layerTreeRoot()->clone() );
+ QgsLayerTreeLayer* layer = mLayersDependenciesTreeGroup->findLayer( mLayer->id() );
+ layer->parent()->takeChild( layer );
+ mLayersDependenciesTreeModel.reset( new QgsLayerTreeModel( mLayersDependenciesTreeGroup.data() ) );
+ // use visibility as selection
+ mLayersDependenciesTreeModel->setFlag( QgsLayerTreeModel::AllowNodeChangeVisibility );
+
+ mLayersDependenciesTreeGroup->setVisible( Qt::Unchecked );
+
+ QSet dependencySources = mLayer->dataDependencies();
+ Q_FOREACH ( QgsLayerTreeLayer* layer, mLayersDependenciesTreeGroup->findLayers() )
+ {
+ layer->setVisible( dependencySources.contains( layer->layerId() ) ? Qt::Checked : Qt::Unchecked );
+ }
+
+ mLayersDependenciesTreeView->setModel( mLayersDependenciesTreeModel.data() );
} // QgsVectorLayerProperties ctor
@@ -558,6 +576,18 @@ void QgsVectorLayerProperties::apply()
QgsExpressionContextUtils::setLayerVariables( mLayer, mVariableEditor->variablesInActiveScope() );
updateVariableEditor();
+ // save layer dependencies
+ QSet deps;
+ Q_FOREACH ( const QgsLayerTreeLayer* layer, mLayersDependenciesTreeGroup->findLayers() )
+ {
+ if ( layer->isVisible() )
+ deps << layer->layerId();
+ }
+ if ( ! mLayer->setDataDependencies( deps ) )
+ {
+ QMessageBox::warning( nullptr, tr( "Dependency cycle" ), tr( "This configuration introduces a cycle in data dependencies and will be ignored" ) );
+ }
+
// update symbology
emit refreshLegend( mLayer->id() );
diff --git a/src/app/qgsvectorlayerproperties.h b/src/app/qgsvectorlayerproperties.h
index 08b5dba9033..3dc282d5a58 100644
--- a/src/app/qgsvectorlayerproperties.h
+++ b/src/app/qgsvectorlayerproperties.h
@@ -25,6 +25,8 @@
#include "qgscontexthelp.h"
#include "qgsmaplayerstylemanager.h"
#include "qgsvectorlayer.h"
+#include "layertree/qgslayertreemodel.h"
+#include "layertree/qgslayertreegroup.h"
class QgsMapLayer;
@@ -193,6 +195,9 @@ class APP_EXPORT QgsVectorLayerProperties : public QgsOptionsDialogBase, private
QgsExpressionContext createExpressionContext() const override;
+ QScopedPointer mLayersDependenciesTreeGroup;
+ QScopedPointer mLayersDependenciesTreeModel;
+
private slots:
void openPanel( QgsPanelWidget* panel );
};
diff --git a/src/core/qgslayerdefinition.cpp b/src/core/qgslayerdefinition.cpp
index 97d2d4d267a..17e06ee4d7e 100644
--- a/src/core/qgslayerdefinition.cpp
+++ b/src/core/qgslayerdefinition.cpp
@@ -111,6 +111,22 @@ bool QgsLayerDefinition::loadLayerDefinition( QDomDocument doc, QgsLayerTreeGrou
joinNode.toElement().setAttribute( "joinLayerId", newid );
}
}
+
+ // change IDs of dependencies
+ QDomNodeList dataDeps = doc.elementsByTagName( "dataDependencies" );
+ for ( int i = 0; i < dataDeps.size(); i++ )
+ {
+ QDomNodeList layers = dataDeps.at( i ).childNodes();
+ for ( int j = 0; j < layers.size(); j++ )
+ {
+ QDomElement elt = layers.at( j ).toElement();
+ if ( elt.attribute( "id" ) == oldid )
+ {
+ elt.setAttribute( "id", newid );
+ }
+ }
+ }
+
}
QDomElement layerTreeElem = doc.documentElement().firstChildElement( "layer-tree-group" );
diff --git a/src/core/qgsmaplayer.cpp b/src/core/qgsmaplayer.cpp
index 76b30f0363f..819b217f8ab 100644
--- a/src/core/qgsmaplayer.cpp
+++ b/src/core/qgsmaplayer.cpp
@@ -1680,3 +1680,62 @@ void QgsMapLayer::setExtent( const QgsRectangle &r )
{
mExtent = r;
}
+
+static QList _depOutEdges( const QgsMapLayer* vl, const QgsMapLayer* that, const QSet& layersIds )
+{
+ QList lst;
+ if ( vl == that )
+ {
+ Q_FOREACH ( QString layerId, layersIds )
+ {
+ if ( const QgsMapLayer* l = QgsMapLayerRegistry::instance()->mapLayer( layerId ) )
+ lst << l;
+ }
+ }
+ else
+ {
+ Q_FOREACH ( QString layerId, vl->dataDependencies() )
+ {
+ if ( const QgsMapLayer* l = QgsMapLayerRegistry::instance()->mapLayer( layerId ) )
+ lst << l;
+ }
+ }
+ return lst;
+}
+
+static bool _depHasCycleDFS( const QgsMapLayer* n, QHash& mark, const QgsMapLayer* that, const QSet& layersIds )
+{
+ if ( mark.value( n ) == 1 ) // temporary
+ return true;
+ if ( mark.value( n ) == 0 ) // not visited
+ {
+ mark[n] = 1; // temporary
+ Q_FOREACH ( const QgsMapLayer* m, _depOutEdges( n, that, layersIds ) )
+ {
+ if ( _depHasCycleDFS( m, mark, that, layersIds ) )
+ return true;
+ }
+ mark[n] = 2; // permanent
+ }
+ return false;
+}
+
+bool QgsMapLayer::hasDataDependencyCycle( const QSet& layersIds ) const
+{
+ QHash marks;
+ return _depHasCycleDFS( this, marks, this, layersIds );
+}
+
+bool QgsMapLayer::setDataDependencies( const QSet& layersIds )
+{
+ if ( hasDataDependencyCycle( layersIds ) )
+ return false;
+
+ mDataDependencies = layersIds;
+ return true;
+}
+
+QSet QgsMapLayer::dataDependencies() const
+{
+ return mDataDependencies;
+}
diff --git a/src/core/qgsmaplayer.h b/src/core/qgsmaplayer.h
index 783a0f00419..3c9643a1c90 100644
--- a/src/core/qgsmaplayer.h
+++ b/src/core/qgsmaplayer.h
@@ -698,6 +698,23 @@ class CORE_EXPORT QgsMapLayer : public QObject
*/
void emitStyleChanged();
+ /**
+ * Sets the list of layers that may modify data/geometries of this layer when modified.
+ * @see dataDependencies
+ *
+ * @param layersIds IDs of the layers that this layer depends on
+ * @returns false if a dependency cycle has been detected (the change dependency set is not changed in that case)
+ */
+ virtual bool setDataDependencies( const QSet& layersIds );
+
+ /**
+ * Gets the list of layers that may modify data/geometries of this layer when modified.
+ * @see setDataDependencies
+ *
+ * @returns IDs of the layers that this layer depends on
+ */
+ virtual QSet dataDependencies() const;
+
signals:
/** Emit a signal with status (e.g. to be caught by QgisApp and display a msg on status bar) */
@@ -836,6 +853,12 @@ class CORE_EXPORT QgsMapLayer : public QObject
/** \brief Error */
QgsError mError;
+ //! List of layers that may modify this layer on modification
+ QSet mDataDependencies;
+
+ //! Checks whether a new set of data dependencies will introduce a cycle
+ bool hasDataDependencyCycle( const QSet& layersIds ) const;
+
private:
/**
* This method returns true by default but can be overwritten to specify
diff --git a/src/core/qgsproject.cpp b/src/core/qgsproject.cpp
index be413dec0a4..222e0179d06 100644
--- a/src/core/qgsproject.cpp
+++ b/src/core/qgsproject.cpp
@@ -889,6 +889,12 @@ bool QgsProject::read()
mVisibilityPresetCollection.reset( new QgsMapThemeCollection() );
mVisibilityPresetCollection->readXml( *doc );
+ // reassign change dependencies now that all layers are loaded
+ QMap existingMaps = QgsMapLayerRegistry::instance()->mapLayers();
+ for ( QMap::iterator it = existingMaps.begin(); it != existingMaps.end(); it++ )
+ {
+ it.value()->setDataDependencies( it.value()->dataDependencies() );
+ }
// read the project: used by map canvas and legend
emit readProject( *doc );
@@ -957,6 +963,8 @@ QgsExpressionContext QgsProject::createExpressionContext() const
void QgsProject::onMapLayersAdded( const QList& layers )
{
+ QMap existingMaps = QgsMapLayerRegistry::instance()->mapLayers();
+
Q_FOREACH ( QgsMapLayer* layer, layers )
{
QgsVectorLayer* vlayer = qobject_cast( layer );
@@ -985,6 +993,17 @@ void QgsProject::onMapLayersAdded( const QList& layers )
}
connect( layer, SIGNAL( configChanged() ), this, SLOT( setDirty() ) );
+
+ // check if we have to update connections for layers with dependencies
+ for ( QMap::iterator it = existingMaps.begin(); it != existingMaps.end(); it++ )
+ {
+ QSet deps = it.value()->dataDependencies();
+ if ( deps.contains( layer->id() ) )
+ {
+ // reconnect to change signals
+ it.value()->setDataDependencies( deps );
+ }
+ }
}
}
diff --git a/src/core/qgsvectorlayer.cpp b/src/core/qgsvectorlayer.cpp
index 0942c627b95..4c35e2a7f31 100644
--- a/src/core/qgsvectorlayer.cpp
+++ b/src/core/qgsvectorlayer.cpp
@@ -1456,6 +1456,17 @@ bool QgsVectorLayer::readXml( const QDomNode& layer_node )
}
updateFields();
+ QDomNode depsNode = layer_node.namedItem( "dataDependencies" );
+ QDomNodeList depsNodes = depsNode.childNodes();
+ QSet sources;
+ for ( int i = 0; i < depsNodes.count(); i++ )
+ {
+ QDomNode node = depsNodes.at( i );
+ QString source = depsNodes.at( i ).toElement().attribute( "id" );
+ sources << source;
+ }
+ setDataDependencies( sources );
+
setLegend( QgsMapLayerLegend::defaultVectorLegend( this ) );
return mValid; // should be true if read successfully
@@ -1647,6 +1658,19 @@ bool QgsVectorLayer::writeXml( QDomNode & layer_node,
}
layer_node.appendChild( defaultsElem );
+ // change dependencies
+ QDomElement dataDependenciesElement = document.createElement( "dataDependencies" );
+ Q_FOREACH ( QString layerId, dataDependencies() )
+ {
+ QDomElement depElem = document.createElement( "layer" );
+ depElem.setAttribute( "id", layerId );
+ dataDependenciesElement.appendChild( depElem );
+ }
+ layer_node.appendChild( dataDependenciesElement );
+
+ // save expression fields
+ mExpressionFieldBuffer->writeXml( layer_node, document );
+
writeStyleManager( layer_node, document );
// renderer specific settings
@@ -4069,3 +4093,49 @@ QSet QgsVectorLayer::layerDependencies() const
}
return QSet();
}
+
+QSet QgsVectorLayer::dataDependencies() const
+{
+ return layerDependencies() + mDataDependencies;
+}
+
+bool QgsVectorLayer::setDataDependencies( const QSet& layersIds )
+{
+ if ( hasDataDependencyCycle( layersIds ) )
+ return false;
+
+ QSet toAdd = layerDependencies() + layersIds - mDataDependencies;
+
+ // disconnect layers that are not present in the list of dependencies anymore
+ Q_FOREACH ( QString layerId, mDataDependencies )
+ {
+ QgsVectorLayer* lyr = static_cast( QgsMapLayerRegistry::instance()->mapLayer( layerId ) );
+ if ( lyr == nullptr )
+ continue;
+ disconnect( lyr, SIGNAL( featureAdded( QgsFeatureId ) ), this, SIGNAL( dataChanged() ) );
+ disconnect( lyr, SIGNAL( featureDeleted( QgsFeatureId ) ), this, SIGNAL( dataChanged() ) );
+ disconnect( lyr, SIGNAL( geometryChanged( QgsFeatureId, QgsGeometry& ) ), this, SIGNAL( dataChanged() ) );
+ disconnect( lyr, SIGNAL( dataChanged() ), this, SIGNAL( dataChanged() ) );
+ }
+
+ // assign new dependencies
+ mDataDependencies = layerDependencies() + layersIds;
+
+ // connect to new layers
+ Q_FOREACH ( QString layerId, mDataDependencies )
+ {
+ QgsVectorLayer* lyr = static_cast( QgsMapLayerRegistry::instance()->mapLayer( layerId ) );
+ if ( lyr == nullptr )
+ continue;
+ connect( lyr, SIGNAL( featureAdded( QgsFeatureId ) ), this, SIGNAL( dataChanged() ) );
+ connect( lyr, SIGNAL( featureDeleted( QgsFeatureId ) ), this, SIGNAL( dataChanged() ) );
+ connect( lyr, SIGNAL( geometryChanged( QgsFeatureId, QgsGeometry& ) ), this, SIGNAL( dataChanged() ) );
+ connect( lyr, SIGNAL( dataChanged() ), this, SIGNAL( dataChanged() ) );
+ }
+
+ // if new layers are present, emit a data change
+ if ( ! toAdd.isEmpty() )
+ emit dataChanged();
+
+ return true;
+}
diff --git a/src/core/qgsvectorlayer.h b/src/core/qgsvectorlayer.h
index 05594994b02..598df920d75 100644
--- a/src/core/qgsvectorlayer.h
+++ b/src/core/qgsvectorlayer.h
@@ -515,10 +515,30 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte
const QList vectorJoins() const;
/**
- * Get the list of layer ids on which this layer depends. This in particular determines the order of layer loading.
+ * Gets the list of layer ids on which this layer depends, as returned by the provider.
+ * This in particular determines the order of layer loading.
*/
virtual QSet layerDependencies() const;
+ /**
+ * Sets the list of layers that may modify data/geometries of this layer when modified.
+ * This is meant mainly to declare database triggers between layers.
+ * When one of these layers is modified (feature added/deleted or geometry changed),
+ * dataChanged() will be emitted, allowing users of this layer to refresh / update it.
+ *
+ * @param layersIds IDs of the layers that this layer depends on
+ * @returns false if a dependency cycle has been detected (the change dependency set is not changed in that case)
+ */
+ bool setDataDependencies( const QSet& layersIds ) override;
+
+ /**
+ * Gets the list of layers that may modify data/geometries of this layer when modified.
+ * @see setDataDependencies
+ *
+ * @returns IDs of the layers that this layer depends on
+ */
+ QSet dataDependencies() const override;
+
/**
* Add a new field which is calculated by the expression specified
*
diff --git a/src/ui/qgsvectorlayerpropertiesbase.ui b/src/ui/qgsvectorlayerpropertiesbase.ui
index 2716902285e..45aa566f6a6 100644
--- a/src/ui/qgsvectorlayerpropertiesbase.ui
+++ b/src/ui/qgsvectorlayerpropertiesbase.ui
@@ -46,16 +46,7 @@
QFrame::Raised
-
- 0
-
-
- 0
-
-
- 0
-
-
+
0
-
@@ -231,6 +222,15 @@
:/images/themes/default/legend.svg:/images/themes/default/legend.svg
+ -
+
+ Data dependencies
+
+
+
+ :/images/themes/default/dependencies.svg:/images/themes/default/dependencies.svg
+
+
@@ -249,16 +249,7 @@
QFrame::Raised
-
- 0
-
-
- 0
-
-
- 0
-
-
+
0
-
@@ -274,16 +265,7 @@
-
- 0
-
-
- 0
-
-
- 0
-
-
+
0
-
@@ -304,16 +286,7 @@
-
- 0
-
-
- 0
-
-
- 0
-
-
+
0
-
@@ -335,16 +308,7 @@
-
-
- 0
-
-
- 0
-
-
- 0
-
-
+
0
@@ -572,16 +536,7 @@
-
- 0
-
-
- 0
-
-
- 0
-
-
+
0
-
@@ -597,21 +552,12 @@
0
0
- 100
- 30
+ 730
+ 537
-
- 0
-
-
- 0
-
-
- 0
-
-
+
0
-
@@ -641,16 +587,7 @@
-
- 0
-
-
- 0
-
-
- 0
-
-
+
0
-
@@ -673,16 +610,7 @@
-
- 0
-
-
- 0
-
-
- 0
-
-
+
0
-
@@ -698,21 +626,12 @@
0
0
- 100
- 30
+ 730
+ 537
-
- 0
-
-
- 0
-
-
- 0
-
-
+
0
-
@@ -733,16 +652,7 @@
-
- 0
-
-
- 0
-
-
- 0
-
-
+
0
-
@@ -908,16 +818,7 @@
-
- 0
-
-
- 0
-
-
- 0
-
-
+
0
-
@@ -1003,16 +904,7 @@
-
- 0
-
-
- 0
-
-
- 0
-
-
+
0
-
@@ -1028,21 +920,12 @@
0
0
- 100
- 30
+ 730
+ 537
-
- 0
-
-
- 0
-
-
- 0
-
-
+
0
-
@@ -1069,16 +952,7 @@
-
- 0
-
-
- 0
-
-
- 0
-
-
+
0
-
@@ -1099,16 +973,7 @@
-
- 0
-
-
- 0
-
-
- 0
-
-
+
0
-
@@ -1206,16 +1071,7 @@
-
- 0
-
-
- 0
-
-
- 0
-
-
+
0
-
@@ -1238,16 +1094,7 @@
-
- 0
-
-
- 0
-
-
- 0
-
-
+
0
-
@@ -1702,6 +1549,46 @@
+
+
+ -
+
+
+ Data dependencies
+
+
+
-
+
+
+ Data of this layer may be updated by data change on one of these layers :
+
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
@@ -1723,16 +1610,7 @@
QFrame::Raised
-
- 0
-
-
- 0
-
-
- 0
-
-
+
0
-
@@ -1795,6 +1673,9 @@
QFrame
1
+ QgsLayerTreeView
+ QTreeView
+
diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt
index 52bed0b8a8f..7ab831c25f5 100644
--- a/tests/src/python/CMakeLists.txt
+++ b/tests/src/python/CMakeLists.txt
@@ -108,6 +108,7 @@ ADD_PYTHON_TEST(PyQgsLayerDefinition test_qgslayerdefinition.py)
ADD_PYTHON_TEST(PyQgsWFSProvider test_provider_wfs.py)
ADD_PYTHON_TEST(PyQgsWFSProviderGUI test_provider_wfs_gui.py)
ADD_PYTHON_TEST(PyQgsConsole test_console.py)
+ADD_PYTHON_TEST(PyQgsLayerDependencies test_layer_dependencies.py)
IF (NOT WIN32)
ADD_PYTHON_TEST(PyQgsLogger test_qgslogger.py)
diff --git a/tests/src/python/test_layer_dependencies.py b/tests/src/python/test_layer_dependencies.py
new file mode 100644
index 00000000000..0e822446b8a
--- /dev/null
+++ b/tests/src/python/test_layer_dependencies.py
@@ -0,0 +1,259 @@
+# -*- coding: utf-8 -*-
+"""QGIS Unit tests for QgsSnappingUtils (complement to C++-based tests)
+
+.. note:: This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+"""
+__author__ = 'Hugo Mercier'
+__date__ = '12/07/2016'
+__copyright__ = 'Copyright 2016, The QGIS Project'
+# This will get replaced with a git SHA1 when you do a git archive
+__revision__ = '$Format:%H$'
+
+import qgis # NOQA
+import os
+
+from qgis.core import (QgsMapLayerRegistry,
+ QgsVectorLayer,
+ QgsMapSettings,
+ QgsSnappingUtils,
+ QgsPointLocator,
+ QgsTolerance,
+ QgsRectangle,
+ QgsPoint,
+ QgsFeature,
+ QgsGeometry,
+ QgsProject,
+ QgsLayerDefinition
+ )
+
+from qgis.testing import start_app, unittest
+from utilities import unitTestDataPath
+
+from qgis.PyQt.QtCore import QSize, QPoint
+
+import tempfile
+
+try:
+ from pyspatialite import dbapi2 as sqlite3
+except ImportError:
+ print("You should install pyspatialite to run the tests")
+ raise ImportError
+
+# Convenience instances in case you may need them
+start_app()
+
+
+class TestLayerDependencies(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ """Run before all tests"""
+
+ # create a temp spatialite db with a trigger
+ fo = tempfile.NamedTemporaryFile()
+ fn = fo.name
+ fo.close()
+ cls.fn = fn
+ con = sqlite3.connect(fn)
+ cur = con.cursor()
+ cur.execute("SELECT InitSpatialMetadata(1)")
+ cur.execute("create table node(id integer primary key autoincrement);")
+ cur.execute("select AddGeometryColumn('node', 'geom', 4326, 'POINT');")
+ cur.execute("create table section(id integer primary key autoincrement, node1 integer, node2 integer);")
+ cur.execute("select AddGeometryColumn('section', 'geom', 4326, 'LINESTRING');")
+ cur.execute("create trigger add_nodes after insert on section begin insert into node (geom) values (st_startpoint(NEW.geom)); insert into node (geom) values (st_endpoint(NEW.geom)); end;")
+ cur.execute("insert into node (geom) values (geomfromtext('point(0 0)', 4326));")
+ cur.execute("insert into node (geom) values (geomfromtext('point(1 0)', 4326));")
+ cur.execute("create table node2(id integer primary key autoincrement);")
+ cur.execute("select AddGeometryColumn('node2', 'geom', 4326, 'POINT');")
+ cur.execute("create trigger add_nodes2 after insert on node begin insert into node2 (geom) values (st_translate(NEW.geom, 0.2, 0, 0)); end;")
+ con.commit()
+ con.close()
+
+ cls.pointsLayer = QgsVectorLayer("dbname='%s' table=\"node\" (geom) sql=" % fn, "points", "spatialite")
+ assert (cls.pointsLayer.isValid())
+ cls.linesLayer = QgsVectorLayer("dbname='%s' table=\"section\" (geom) sql=" % fn, "lines", "spatialite")
+ assert (cls.linesLayer.isValid())
+ cls.pointsLayer2 = QgsVectorLayer("dbname='%s' table=\"node2\" (geom) sql=" % fn, "_points2", "spatialite")
+ assert (cls.pointsLayer2.isValid())
+ QgsMapLayerRegistry.instance().addMapLayers([cls.pointsLayer, cls.linesLayer, cls.pointsLayer2])
+
+ # save the project file
+ fo = tempfile.NamedTemporaryFile()
+ fn = fo.name
+ fo.close()
+ cls.projectFile = fn
+ QgsProject.instance().setFileName(cls.projectFile)
+ QgsProject.instance().write()
+
+ @classmethod
+ def tearDownClass(cls):
+ """Run after all tests"""
+ pass
+
+ def setUp(self):
+ """Run before each test."""
+ pass
+
+ def tearDown(self):
+ """Run after each test."""
+ pass
+
+ def test_resetSnappingIndex(self):
+ self.pointsLayer.setDataDependencies([])
+ self.linesLayer.setDataDependencies([])
+ self.pointsLayer2.setDataDependencies([])
+
+ ms = QgsMapSettings()
+ ms.setOutputSize(QSize(100, 100))
+ ms.setExtent(QgsRectangle(0, 0, 1, 1))
+ self.assertTrue(ms.hasValidSettings())
+
+ u = QgsSnappingUtils()
+ u.setMapSettings(ms)
+ u.setSnapToMapMode(QgsSnappingUtils.SnapAdvanced)
+ layers = [QgsSnappingUtils.LayerConfig(self.pointsLayer, QgsPointLocator.Vertex, 20, QgsTolerance.Pixels)]
+ u.setLayers(layers)
+
+ m = u.snapToMap(QPoint(95, 100))
+ self.assertTrue(m.isValid())
+ self.assertTrue(m.hasVertex())
+ self.assertEqual(m.point(), QgsPoint(1, 0))
+
+ f = QgsFeature(self.linesLayer.fields())
+ f.setFeatureId(1)
+ geom = QgsGeometry.fromWkt("LINESTRING(0 0,1 1)")
+ f.setGeometry(geom)
+ self.linesLayer.startEditing()
+ self.linesLayer.addFeatures([f])
+ self.linesLayer.commitChanges()
+
+ l1 = len([f for f in self.pointsLayer.getFeatures()])
+ self.assertEqual(l1, 4)
+ m = u.snapToMap(QPoint(95, 0))
+ # snapping not updated
+ self.assertEqual(m.isValid(), False)
+
+ # set layer dependencies
+ self.pointsLayer.setDataDependencies([self.linesLayer.id()])
+ # add another line
+ f = QgsFeature(self.linesLayer.fields())
+ f.setFeatureId(2)
+ geom = QgsGeometry.fromWkt("LINESTRING(0 0,0.5 0.5)")
+ f.setGeometry(geom)
+ self.linesLayer.startEditing()
+ self.linesLayer.addFeatures([f])
+ self.linesLayer.commitChanges()
+ # check the snapped point is ok
+ m = u.snapToMap(QPoint(45, 50))
+ self.assertTrue(m.isValid())
+ self.assertTrue(m.hasVertex())
+ self.assertEqual(m.point(), QgsPoint(0.5, 0.5))
+ self.pointsLayer.setDataDependencies([])
+
+ # test chained layer dependencies A -> B -> C
+ layers = [QgsSnappingUtils.LayerConfig(self.pointsLayer, QgsPointLocator.Vertex, 20, QgsTolerance.Pixels),
+ QgsSnappingUtils.LayerConfig(self.pointsLayer2, QgsPointLocator.Vertex, 20, QgsTolerance.Pixels)
+ ]
+ u.setLayers(layers)
+ self.pointsLayer.setDataDependencies([self.linesLayer.id()])
+ self.pointsLayer2.setDataDependencies([self.pointsLayer.id()])
+ # add another line
+ f = QgsFeature(self.linesLayer.fields())
+ f.setFeatureId(3)
+ geom = QgsGeometry.fromWkt("LINESTRING(0 0.2,0.5 0.8)")
+ f.setGeometry(geom)
+ self.linesLayer.startEditing()
+ self.linesLayer.addFeatures([f])
+ self.linesLayer.commitChanges()
+ # check the second snapped point is ok
+ m = u.snapToMap(QPoint(75, 100 - 80))
+ self.assertTrue(m.isValid())
+ self.assertTrue(m.hasVertex())
+ self.assertEqual(m.point(), QgsPoint(0.7, 0.8))
+ self.pointsLayer.setDataDependencies([])
+ self.pointsLayer2.setDataDependencies([])
+
+ def test_cycleDetection(self):
+ self.assertTrue(self.pointsLayer.setDataDependencies([self.linesLayer.id()]))
+ self.assertFalse(self.linesLayer.setDataDependencies([self.pointsLayer.id()]))
+ self.pointsLayer.setDataDependencies([])
+ self.linesLayer.setDataDependencies([])
+
+ def test_layerDefinitionRewriteId(self):
+ tmpfile = os.path.join(tempfile.tempdir, "test.qlr")
+
+ ltr = QgsProject.instance().layerTreeRoot()
+
+ self.pointsLayer.setDataDependencies([self.linesLayer.id()])
+
+ QgsLayerDefinition.exportLayerDefinition(tmpfile, [ltr])
+
+ grp = ltr.addGroup("imported")
+ QgsLayerDefinition.loadLayerDefinition(tmpfile, grp)
+
+ newPointsLayer = None
+ newLinesLayer = None
+ for l in grp.findLayers():
+ if l.layerId().startswith('points'):
+ newPointsLayer = l.layer()
+ elif l.layerId().startswith('lines'):
+ newLinesLayer = l.layer()
+ self.assertFalse(newPointsLayer is None)
+ self.assertFalse(newLinesLayer is None)
+ self.assertTrue(newLinesLayer.id() in newPointsLayer.dataDependencies())
+
+ self.pointsLayer.setDataDependencies([])
+
+ def test_signalConnection(self):
+ # remove all layers
+ QgsMapLayerRegistry.instance().removeAllMapLayers()
+ # set dependencies and add back layers
+ self.pointsLayer = QgsVectorLayer("dbname='%s' table=\"node\" (geom) sql=" % self.fn, "points", "spatialite")
+ assert (self.pointsLayer.isValid())
+ self.linesLayer = QgsVectorLayer("dbname='%s' table=\"section\" (geom) sql=" % self.fn, "lines", "spatialite")
+ assert (self.linesLayer.isValid())
+ self.pointsLayer2 = QgsVectorLayer("dbname='%s' table=\"node2\" (geom) sql=" % self.fn, "_points2", "spatialite")
+ assert (self.pointsLayer2.isValid())
+ self.pointsLayer.setDataDependencies([self.linesLayer.id()])
+ self.pointsLayer2.setDataDependencies([self.pointsLayer.id()])
+ # this should update connections between layers
+ QgsMapLayerRegistry.instance().addMapLayers([self.pointsLayer])
+ QgsMapLayerRegistry.instance().addMapLayers([self.linesLayer])
+ QgsMapLayerRegistry.instance().addMapLayers([self.pointsLayer2])
+
+ ms = QgsMapSettings()
+ ms.setOutputSize(QSize(100, 100))
+ ms.setExtent(QgsRectangle(0, 0, 1, 1))
+ self.assertTrue(ms.hasValidSettings())
+
+ u = QgsSnappingUtils()
+ u.setMapSettings(ms)
+ u.setSnapToMapMode(QgsSnappingUtils.SnapAdvanced)
+ layers = [QgsSnappingUtils.LayerConfig(self.pointsLayer, QgsPointLocator.Vertex, 20, QgsTolerance.Pixels),
+ QgsSnappingUtils.LayerConfig(self.pointsLayer2, QgsPointLocator.Vertex, 20, QgsTolerance.Pixels)
+ ]
+ u.setLayers(layers)
+ # add another line
+ f = QgsFeature(self.linesLayer.fields())
+ f.setFeatureId(4)
+ geom = QgsGeometry.fromWkt("LINESTRING(0.5 0.2,0.6 0)")
+ f.setGeometry(geom)
+ self.linesLayer.startEditing()
+ self.linesLayer.addFeatures([f])
+ self.linesLayer.commitChanges()
+ # check the second snapped point is ok
+ m = u.snapToMap(QPoint(75, 100 - 0))
+ self.assertTrue(m.isValid())
+ self.assertTrue(m.hasVertex())
+ self.assertEqual(m.point(), QgsPoint(0.8, 0.0))
+
+ self.pointsLayer.setDataDependencies([])
+ self.pointsLayer2.setDataDependencies([])
+
+
+if __name__ == '__main__':
+ unittest.main()