Skip to content
Permalink
Browse files

[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.
  • Loading branch information
Hugo Mercier
Hugo Mercier committed Aug 31, 2016
1 parent e6fd2e2 commit 1a5a7c59054e6197dac83cc64ce8fd15fd2b2cb6
@@ -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>
@@ -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>
@@ -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;
};
@@ -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
*
@@ -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() );

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

1 comment on commit 1a5a7c5

@nirvn

This comment has been minimized.

Copy link
Contributor

@nirvn nirvn commented on 1a5a7c5 Sep 4, 2016

@mhugo , nice feature. I've just tried out, and it seems to fail at one useful scenario: layer dependencies via symbology rendering. See this video for an example of what I mean (https://www.youtube.com/watch?v=UnxP8I3uqhA).

Basically, the "battleship" layer (i.e. the grid with colored blocks) is styled based on spatial intersection with geometries from another layer, called "hits", in which the user inputs points. Right now, the symbology automatically updates via a little hack (by setting the battleship's label mode to "obstacle"). But a proper solution would actually be to declare a dependency between the battleship layer and the hits layer.

Your feature is doing that, it only needs to insure that data added on a layer A triggers a symbology refresh of layer B with a dependency declared to layer A.

(ping @nyalldawson )

Please sign in to comment.
You can’t perform that action at this time.