[Feature] Data dependencies between layers

This allows to declare data dependencies between layers. A data
dependency occurs when a data modification in a layer, not by direct
user manipulation may modify data of other layers.
This is the case for instance when geometry of a layer is updated by a
database trigger after modification of another layer's geometry.
This commit is contained in:
Hugo Mercier 2016-07-21 11:21:28 +02:00
parent e6fd2e2503
commit 1a5a7c5905
15 changed files with 773 additions and 198 deletions

View File

@ -665,6 +665,7 @@
<file>themes/default/mActionAddAfsLayer.svg</file>
<file>themes/default/mIconFormSelect.svg</file>
<file>themes/default/mActionMultiEdit.svg</file>
<file>themes/default/dependencies.svg</file>
</qresource>
<qresource prefix="/images/tips">
<file alias="symbol_levels.png">qgis_tips/symbol_levels.png</file>

View File

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
viewBox="0 0 16 16"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="sync_views.svg">
<defs
id="defs4">
<linearGradient
inkscape:collect="always"
id="linearGradient4158">
<stop
style="stop-color:#0000ff;stop-opacity:1"
offset="0"
id="stop4160" />
<stop
style="stop-color:#0000a9;stop-opacity:1"
offset="1"
id="stop4162" />
</linearGradient>
<marker
inkscape:stockid="Arrow2Mend"
orient="auto"
refY="0.0"
refX="0.0"
id="Arrow2Mend"
style="overflow:visible;"
inkscape:isstock="true">
<path
id="path4171"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
transform="scale(0.6) rotate(180) translate(0,0)" />
</marker>
<marker
inkscape:stockid="Arrow2Mend"
orient="auto"
refY="0"
refX="0"
id="Arrow2Mend-8"
style="overflow:visible"
inkscape:isstock="true">
<path
inkscape:connector-curvature="0"
id="path4171-2"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
transform="scale(-0.6,-0.6)" />
</marker>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient4158"
id="radialGradient4178"
cx="7.9999766"
cy="1040.8622"
fx="7.9999766"
fy="1040.8622"
r="6.9999766"
gradientTransform="matrix(1,0,0,0.49999621,0,520.43504)"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient4158"
id="radialGradient4178-3"
cx="7.9999766"
cy="1040.8622"
fx="7.9999766"
fy="1040.8622"
r="6.9999766"
gradientTransform="matrix(-1,0,0,0.49999621,16,527.43502)"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="18.5"
inkscape:cx="0.59568033"
inkscape:cy="10.721345"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:object-nodes="true"
inkscape:snap-smooth-nodes="true"
inkscape:window-width="1600"
inkscape:window-height="829"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1036.3622)">
<g
id="g4865">
<path
style="fill:url(#radialGradient4178);fill-opacity:1;fill-rule:evenodd;stroke:#0000a9;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1.5,1040.8622 0,1 8,0 0,2 5,-3 -5,-3 0,2 -8,0 z"
id="path4156"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
<path
style="fill:url(#radialGradient4178-3);fill-opacity:1;fill-rule:evenodd;stroke:#0000a9;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 14.5,1047.8622 0,1 -7.9999996,0 0,2 -5,-3 5,-3 0,2 7.9999996,0 z"
id="path4156-7"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -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<QString>& 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<QString> 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<QString>& layersIds ) const;
};

View File

@ -422,10 +422,30 @@ class QgsVectorLayer : QgsMapLayer
const QList<QgsVectorJoinInfo> 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<QString> 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<QString>& 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<QString> dataDependencies() const;
/**
* Add a new field which is calculated by the expression specified
*

View File

@ -52,6 +52,7 @@
#include "qgsdatasourceuri.h"
#include "qgsrenderer.h"
#include "qgsexpressioncontext.h"
#include "layertree/qgslayertreelayer.h"
#include <QMessageBox>
#include <QDir>
@ -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<QString> 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<QString> 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() );

View File

@ -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<QgsLayerTreeGroup> mLayersDependenciesTreeGroup;
QScopedPointer<QgsLayerTreeModel> mLayersDependenciesTreeModel;
private slots:
void openPanel( QgsPanelWidget* panel );
};

View File

@ -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" );

View File

@ -1680,3 +1680,62 @@ void QgsMapLayer::setExtent( const QgsRectangle &r )
{
mExtent = r;
}
static QList<const QgsMapLayer*> _depOutEdges( const QgsMapLayer* vl, const QgsMapLayer* that, const QSet<QString>& layersIds )
{
QList<const QgsMapLayer*> 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<const QgsMapLayer*, int>& mark, const QgsMapLayer* that, const QSet<QString>& 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<QString>& layersIds ) const
{
QHash<const QgsMapLayer*, int> marks;
return _depHasCycleDFS( this, marks, this, layersIds );
}
bool QgsMapLayer::setDataDependencies( const QSet<QString>& layersIds )
{
if ( hasDataDependencyCycle( layersIds ) )
return false;
mDataDependencies = layersIds;
return true;
}
QSet<QString> QgsMapLayer::dataDependencies() const
{
return mDataDependencies;
}

View File

@ -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<QString>& 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<QString> 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<QString> mDataDependencies;
//! Checks whether a new set of data dependencies will introduce a cycle
bool hasDataDependencyCycle( const QSet<QString>& layersIds ) const;
private:
/**
* This method returns true by default but can be overwritten to specify

View File

@ -889,6 +889,12 @@ bool QgsProject::read()
mVisibilityPresetCollection.reset( new QgsMapThemeCollection() );
mVisibilityPresetCollection->readXml( *doc );
// reassign change dependencies now that all layers are loaded
QMap<QString, QgsMapLayer*> existingMaps = QgsMapLayerRegistry::instance()->mapLayers();
for ( QMap<QString, QgsMapLayer*>::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<QgsMapLayer*>& layers )
{
QMap<QString, QgsMapLayer*> existingMaps = QgsMapLayerRegistry::instance()->mapLayers();
Q_FOREACH ( QgsMapLayer* layer, layers )
{
QgsVectorLayer* vlayer = qobject_cast<QgsVectorLayer*>( layer );
@ -985,6 +993,17 @@ void QgsProject::onMapLayersAdded( const QList<QgsMapLayer*>& layers )
}
connect( layer, SIGNAL( configChanged() ), this, SLOT( setDirty() ) );
// check if we have to update connections for layers with dependencies
for ( QMap<QString, QgsMapLayer*>::iterator it = existingMaps.begin(); it != existingMaps.end(); it++ )
{
QSet<QString> deps = it.value()->dataDependencies();
if ( deps.contains( layer->id() ) )
{
// reconnect to change signals
it.value()->setDataDependencies( deps );
}
}
}
}

View File

@ -1456,6 +1456,17 @@ bool QgsVectorLayer::readXml( const QDomNode& layer_node )
}
updateFields();
QDomNode depsNode = layer_node.namedItem( "dataDependencies" );
QDomNodeList depsNodes = depsNode.childNodes();
QSet<QString> 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<QString> QgsVectorLayer::layerDependencies() const
}
return QSet<QString>();
}
QSet<QString> QgsVectorLayer::dataDependencies() const
{
return layerDependencies() + mDataDependencies;
}
bool QgsVectorLayer::setDataDependencies( const QSet<QString>& layersIds )
{
if ( hasDataDependencyCycle( layersIds ) )
return false;
QSet<QString> 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<QgsVectorLayer*>( 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<QgsVectorLayer*>( 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;
}

View File

@ -515,10 +515,30 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte
const QList<QgsVectorJoinInfo> 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<QString> 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<QString>& 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<QString> dataDependencies() const override;
/**
* Add a new field which is calculated by the expression specified
*

View File

@ -46,16 +46,7 @@
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
<item>
@ -231,6 +222,15 @@
<normaloff>:/images/themes/default/legend.svg</normaloff>:/images/themes/default/legend.svg</iconset>
</property>
</item>
<item>
<property name="text">
<string>Data dependencies</string>
</property>
<property name="icon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/dependencies.svg</normaloff>:/images/themes/default/dependencies.svg</iconset>
</property>
</item>
</widget>
</item>
</layout>
@ -249,16 +249,7 @@
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
<item>
@ -274,16 +265,7 @@
</property>
<widget class="QWidget" name="mOptsPage_General">
<layout class="QVBoxLayout" name="verticalLayout_14">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
<item>
@ -304,16 +286,7 @@
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_13">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
<item>
@ -335,16 +308,7 @@
<item row="5" column="0">
<widget class="QFrame" name="mDataSourceEncodingFrame">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
</layout>
@ -572,16 +536,7 @@
</widget>
<widget class="QWidget" name="mOptsPage_Style">
<layout class="QVBoxLayout" name="verticalLayout_11">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
<item>
@ -597,21 +552,12 @@
<rect>
<x>0</x>
<y>0</y>
<width>100</width>
<height>30</height>
<width>730</width>
<height>537</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_18">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
<item>
@ -641,16 +587,7 @@
</widget>
<widget class="QWidget" name="mOptsPage_Labels">
<layout class="QVBoxLayout" name="verticalLayout_12">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
<item>
@ -673,16 +610,7 @@
</widget>
<widget class="QWidget" name="mOptsPage_Fields">
<layout class="QVBoxLayout" name="verticalLayout_15">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
<item>
@ -698,21 +626,12 @@
<rect>
<x>0</x>
<y>0</y>
<width>100</width>
<height>30</height>
<width>730</width>
<height>537</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_20">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
<item>
@ -733,16 +652,7 @@
</widget>
<widget class="QWidget" name="mOptsPage_Rendering">
<layout class="QVBoxLayout" name="verticalLayout_19">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
<item>
@ -908,16 +818,7 @@
</widget>
<widget class="QWidget" name="mOptsPage_Display">
<layout class="QVBoxLayout" name="verticalLayout_25">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
<item>
@ -1003,16 +904,7 @@
</widget>
<widget class="QWidget" name="mOptsPage_Actions">
<layout class="QVBoxLayout" name="verticalLayout_16">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
<item>
@ -1028,21 +920,12 @@
<rect>
<x>0</x>
<y>0</y>
<width>100</width>
<height>30</height>
<width>730</width>
<height>537</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_21">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
<item>
@ -1069,16 +952,7 @@
</widget>
<widget class="QWidget" name="mOptsPage_Joins">
<layout class="QVBoxLayout" name="verticalLayout_17">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
<item>
@ -1099,16 +973,7 @@
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_23">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
<item>
@ -1206,16 +1071,7 @@
</widget>
<widget class="QWidget" name="mOptsPage_Diagrams">
<layout class="QVBoxLayout" name="verticalLayout_10">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
<item>
@ -1238,16 +1094,7 @@
</widget>
<widget class="QWidget" name="mOptsPage_Metadata">
<layout class="QVBoxLayout" name="verticalLayout_7">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
<item>
@ -1702,6 +1549,46 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="mOptsPage_DataDependencies">
<layout class="QVBoxLayout" name="verticalLayout_29">
<item>
<widget class="QgsCollapsibleGroupBox" name="groupBox_5">
<property name="title">
<string>Data dependencies</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_24">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Data of this layer may be updated by data change on one of these layers :</string>
</property>
</widget>
</item>
<item>
<widget class="QgsLayerTreeView" name="mLayersDependenciesTreeView">
<property name="enabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
@ -1723,16 +1610,7 @@
<enum>QFrame::Raised</enum>
</property>
<layout class="QGridLayout" name="gridLayout_btnbox">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
<item row="2" column="1" colspan="4">
@ -1795,6 +1673,9 @@
<extends>QFrame</extends>
<header>qgscodeeditorhtml.h</header>
<container>1</container>
<class>QgsLayerTreeView</class>
<extends>QTreeView</extends>
<header>qgslayertreeview.h</header>
</customwidget>
</customwidgets>
<tabstops>

View File

@ -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)

View File

@ -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()