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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + 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
qgscodeeditorhtml.h
1 + QgsLayerTreeView + QTreeView +
qgslayertreeview.h
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()