Skip to content

Commit

Permalink
Rework layer dependencies
Browse files Browse the repository at this point in the history
A new class QgsMapLayerDependency allows to represent different kinds
of dependencies between layers.
  • Loading branch information
Hugo Mercier committed Aug 31, 2016
1 parent 1a5a7c5 commit 0749ba4
Show file tree
Hide file tree
Showing 19 changed files with 243 additions and 85 deletions.
1 change: 1 addition & 0 deletions ci/travis/linux/qt5/blacklist.txt
Expand Up @@ -9,6 +9,7 @@ PyQgsSipCoverage
PyQgsSpatialiteProvider
PyQgsVirtualLayerDefinition
PyQgsVirtualLayerProvider
PyQgsLayerDependencies
qgis_composermapgridtest
qgis_composerutils
ProcessingGrass7AlgorithmsImageryTest
Expand Down
1 change: 1 addition & 0 deletions python/core/core.sip
Expand Up @@ -80,6 +80,7 @@
%Include qgslogger.sip
%Include qgsmaphittest.sip
%Include qgsmaplayer.sip
%Include qgsmaplayerdependency.sip
%Include qgsmaplayerlegend.sip
%Include qgsmaplayermodel.sip
%Include qgsmaplayerproxymodel.sip
Expand Down
21 changes: 15 additions & 6 deletions python/core/qgsmaplayer.sip
Expand Up @@ -678,20 +678,29 @@ class QgsMapLayer : QObject

/**
* Sets the list of layers that may modify data/geometries of this layer when modified.
* @see dataDependencies
* @see dependencies()
*
* @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
* Sets the list of layers that may modify data/geometries of this layer when modified.
* @see dependencies()
*
* @param set of QgsMapLayerDependency. Only user-defined dependencies will be added
* @returns false if a dependency cycle has been detected (the change dependency set is not changed in that case)
*/
bool setDataDependencies( const QSet<QgsMapLayerDependency>& layers );

/**
* Gets the list of dependencies. This includes data dependencies set by the user (@see setDataDependencies)
* as well as dependencies given by the provider
*
* @returns IDs of the layers that this layer depends on
* @returns a set of QgsMapLayerDependency
*/
virtual QSet<QString> dataDependencies() const;
virtual QSet<QgsMapLayerDependency> dependencies() const;

signals:

Expand Down Expand Up @@ -785,5 +794,5 @@ class QgsMapLayer : QObject
void setError( const QgsError &error );

//! Checks if new change dependency candidates introduce a cycle
bool hasDataDependencyCycle( const QSet<QString>& layersIds ) const;
bool hasDataDependencyCycle( const QSet<QgsMapLayerDependency>& layersIds ) const;
};
36 changes: 36 additions & 0 deletions python/core/qgsmaplayerdependency.sip
@@ -0,0 +1,36 @@
class QgsMapLayerDependency
{
%TypeHeaderCode
#include "qgsmaplayerdependency.h"
%End
public:
//! Type of dependency
enum Type
{
PresenceDependency = 1, // The layer must be already present (in the registry) for this dependency to be resolved
DataDependency = 2 // The layer may be invalidated by data changes on another layer
};

//! Origin of the dependency
enum Origin
{
FromProvider = 0, // Dependency given by the provider, the user cannot change it
FromUser = 1 // Dependency given by the user
};

//! Standard constructor
QgsMapLayerDependency( QString layerId, Type type = DataDependency, Origin origin = FromUser );

//! Return the dependency type
Type type() const;

//! Return the dependency origin
Origin origin() const;

//! Return the ID of the layer this dependency depends on
QString layerId() const;

bool operator==( const QgsMapLayerDependency& other ) const;
};


2 changes: 1 addition & 1 deletion python/core/qgsvectordataprovider.sip
Expand Up @@ -368,7 +368,7 @@ class QgsVectorDataProvider : QgsDataProvider
/**
* Get the list of layer ids on which this layer depends. This in particular determines the order of layer loading.
*/
virtual QSet<QString> layerDependencies() const;
virtual QSet<QgsMapLayerDependency> dependencies() const;

signals:
/** Signals an error in this provider */
Expand Down
14 changes: 4 additions & 10 deletions python/core/qgsvectorlayer.sip
Expand Up @@ -421,12 +421,6 @@ class QgsVectorLayer : QgsMapLayer

const QList<QgsVectorJoinInfo> vectorJoins() const;

/**
* 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.
Expand All @@ -439,12 +433,12 @@ class QgsVectorLayer : QgsMapLayer
bool setDataDependencies( const QSet<QString>& layersIds );

/**
* Gets the list of layers that may modify data/geometries of this layer when modified.
* @see setDataDependencies
* Gets the list of dependencies. This includes data dependencies set by the user (@see setDataDependencies)
* as well as dependencies given by the provider
*
* @returns IDs of the layers that this layer depends on
* @returns a set of QgsMapLayerDependency
*/
QSet<QString> dataDependencies() const;
virtual QSet<QgsMapLayerDependency> dependencies() const;

/**
* Add a new field which is calculated by the expression specified
Expand Down
6 changes: 5 additions & 1 deletion src/app/qgsvectorlayerproperties.cpp
Expand Up @@ -302,7 +302,11 @@ QgsVectorLayerProperties::QgsVectorLayerProperties(

mLayersDependenciesTreeGroup->setVisible( Qt::Unchecked );

QSet<QString> dependencySources = mLayer->dataDependencies();
QSet<QString> dependencySources;
Q_FOREACH ( const QgsMapLayerDependency& dep, mLayer->dependencies() )
{
dependencySources << dep.layerId();
}
Q_FOREACH ( QgsLayerTreeLayer* layer, mLayersDependenciesTreeGroup->findLayers() )
{
layer->setVisible( dependencySources.contains( layer->layerId() ) ? Qt::Checked : Qt::Unchecked );
Expand Down
44 changes: 30 additions & 14 deletions src/core/qgsmaplayer.cpp
Expand Up @@ -1681,61 +1681,77 @@ void QgsMapLayer::setExtent( const QgsRectangle &r )
mExtent = r;
}

static QList<const QgsMapLayer*> _depOutEdges( const QgsMapLayer* vl, const QgsMapLayer* that, const QSet<QString>& layersIds )
static QList<const QgsMapLayer*> _depOutEdges( const QgsMapLayer* vl, const QgsMapLayer* that, const QSet<QgsMapLayerDependency>& layers )
{
QList<const QgsMapLayer*> lst;
if ( vl == that )
{
Q_FOREACH ( QString layerId, layersIds )
Q_FOREACH ( const QgsMapLayerDependency& dep, layers )
{
if ( const QgsMapLayer* l = QgsMapLayerRegistry::instance()->mapLayer( layerId ) )
if ( const QgsMapLayer* l = QgsMapLayerRegistry::instance()->mapLayer( dep.layerId() ) )
lst << l;
}
}
else
{
Q_FOREACH ( QString layerId, vl->dataDependencies() )
Q_FOREACH ( const QgsMapLayerDependency& dep, vl->dependencies() )
{
if ( const QgsMapLayer* l = QgsMapLayerRegistry::instance()->mapLayer( layerId ) )
if ( const QgsMapLayer* l = QgsMapLayerRegistry::instance()->mapLayer( dep.layerId() ) )
lst << l;
}
}
return lst;
}

static bool _depHasCycleDFS( const QgsMapLayer* n, QHash<const QgsMapLayer*, int>& mark, const QgsMapLayer* that, const QSet<QString>& layersIds )
static bool _depHasCycleDFS( const QgsMapLayer* n, QHash<const QgsMapLayer*, int>& mark, const QgsMapLayer* that, const QSet<QgsMapLayerDependency>& layers )
{
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 ) )
Q_FOREACH ( const QgsMapLayer* m, _depOutEdges( n, that, layers ) )
{
if ( _depHasCycleDFS( m, mark, that, layersIds ) )
if ( _depHasCycleDFS( m, mark, that, layers ) )
return true;
}
mark[n] = 2; // permanent
}
return false;
}

bool QgsMapLayer::hasDataDependencyCycle( const QSet<QString>& layersIds ) const
bool QgsMapLayer::hasDataDependencyCycle( const QSet<QgsMapLayerDependency>& layers ) const
{
QHash<const QgsMapLayer*, int> marks;
return _depHasCycleDFS( this, marks, this, layersIds );
return _depHasCycleDFS( this, marks, this, layers );
}

QSet<QgsMapLayerDependency> QgsMapLayer::dependencies() const
{
return mDataDependencies;
}

bool QgsMapLayer::setDataDependencies( const QSet<QString>& layersIds )
{
if ( hasDataDependencyCycle( layersIds ) )
QSet<QgsMapLayerDependency> deps;
Q_FOREACH ( QString layerId, layersIds )
{
deps << QgsMapLayerDependency( layerId );
}
if ( hasDataDependencyCycle( deps ) )
return false;

mDataDependencies = layersIds;
mDataDependencies = deps;

This comment has been minimized.

Copy link
@m-kuhn

m-kuhn Aug 31, 2016

Member

It would also be good to send a signal when this is set so the rest of the world can subscribe to notifications.

I always do setters like this:

if ( dependencies == mDependencies )
  return;

// additional checks

mDependencies = dependencies;
emit dependenciesChanged();

This comment has been minimized.

Copy link
@mhugo

mhugo Aug 31, 2016

I'll add that, thanks

return true;
}

QSet<QString> QgsMapLayer::dataDependencies() const
bool QgsMapLayer::setDataDependencies( const QSet<QgsMapLayerDependency>& layers )
{
return mDataDependencies;
QSet<QString> deps;
Q_FOREACH ( const QgsMapLayerDependency& dep, layers )
{
if ( dep.origin() == QgsMapLayerDependency::FromUser && dep.type() == QgsMapLayerDependency::DataDependency )
deps << dep.layerId();
}
return setDataDependencies( deps );
}
24 changes: 17 additions & 7 deletions src/core/qgsmaplayer.h
Expand Up @@ -32,6 +32,7 @@
#include "qgsrectangle.h"
#include "qgscoordinatereferencesystem.h"
#include "qgsrendercontext.h"
#include "qgsmaplayerdependency.h"

class QgsMapLayerLegend;
class QgsMapLayerRenderer;
Expand Down Expand Up @@ -700,20 +701,29 @@ class CORE_EXPORT QgsMapLayer : public QObject

/**
* Sets the list of layers that may modify data/geometries of this layer when modified.
* @see dataDependencies
* @see dependencies()
*
* @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
* Sets the list of layers that may modify data/geometries of this layer when modified.
* @see dependencies()
*
* @param layers set of QgsMapLayerDependency. Only user-defined dependencies will be added
* @returns false if a dependency cycle has been detected (the change dependency set is not changed in that case)
*/
bool setDataDependencies( const QSet<QgsMapLayerDependency>& layers );

This comment has been minimized.

Copy link
@m-kuhn

m-kuhn Aug 31, 2016

Member

Can the getters and setters be named analogue?
setDependencies() / dependencies()
or
setDataDependencies() / dataDependencies()

This comment has been minimized.

Copy link
@mhugo

mhugo Aug 31, 2016

It is named like this on purpose ... Layers dependencies are "read only" (given by the provider) and cannot be modified by the user

This comment has been minimized.

Copy link
@m-kuhn

m-kuhn Aug 31, 2016

Member

Shouldn't there also be a getter for dataDependencies() besides dependencies() then?

So manipulation can be:

d = l.dataDependencies()
d.append(new_dependency)
l.setDataDependencies(d)

This comment has been minimized.

Copy link
@mhugo

mhugo Aug 31, 2016

Right, it is better, thanks !


/**
* Gets the list of dependencies. This includes data dependencies set by the user (@see setDataDependencies)
* as well as dependencies given by the provider
*
* @returns IDs of the layers that this layer depends on
* @returns a set of QgsMapLayerDependency
*/
virtual QSet<QString> dataDependencies() const;
virtual QSet<QgsMapLayerDependency> dependencies() const;

signals:

Expand Down Expand Up @@ -854,10 +864,10 @@ class CORE_EXPORT QgsMapLayer : public QObject
QgsError mError;

//! List of layers that may modify this layer on modification
QSet<QString> mDataDependencies;
QSet<QgsMapLayerDependency> mDataDependencies;

//! Checks whether a new set of data dependencies will introduce a cycle
bool hasDataDependencyCycle( const QSet<QString>& layersIds ) const;
bool hasDataDependencyCycle( const QSet<QgsMapLayerDependency>& layers ) const;

private:
/**
Expand Down
85 changes: 85 additions & 0 deletions src/core/qgsmaplayerdependency.h
@@ -0,0 +1,85 @@

/***************************************************************************
qgsmaplayerdependency.h - description
-------------------
begin : September 2016
copyright : (C) 2016 by Hugo Mercier
email : hugo dot mercier at oslandia dot com
***************************************************************************/

/***************************************************************************
* *
* 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. *
* *
***************************************************************************/

#ifndef QGSMAPLAYERDEPENDENCY_H
#define QGSMAPLAYERDEPENDENCY_H

#include <QString>

/** \ingroup core
* This class models dependencies with or between map layers.
* A dependency is defined by a layer ID, a type and an origin.
* The two combinations of type/origin that are currently supported are:
* - PresenceDependency && FromProvider: virtual layers for instance which may depend on other layers already loaded to work
* - DataDependency && FromUser: dependencies given by the user, mainly to represent database triggers
*
* @note added in QGIS 3.0
*/
class CORE_EXPORT QgsMapLayerDependency
{
public:
//! Type of dependency
enum Type
{
PresenceDependency = 1, //< The layer must be already present (in the registry) for this dependency to be resolved
DataDependency = 2 //< The layer may be invalidated by data changes on another layer
};

//! Origin of the dependency
enum Origin
{
FromProvider = 0, //< Dependency given by the provider, the user cannot change it
FromUser = 1 //< Dependency given by the user
};

//! Standard constructor
QgsMapLayerDependency( QString layerId, Type type = DataDependency, Origin origin = FromUser ) :
mType( type ),
mOrigin( origin ),
mLayerId( layerId )
{}

//! Return the dependency type
Type type() const { return mType; }

//! Return the dependency origin
Origin origin() const { return mOrigin; }

//! Return the ID of the layer this dependency depends on
QString layerId() const { return mLayerId; }

//! Comparison operator
bool operator==( const QgsMapLayerDependency& other ) const
{
return layerId() == other.layerId() && origin() == other.origin() && type() == other.type();
}
private:
Type mType;
Origin mOrigin;
QString mLayerId;
};

/**
* global qHash function for QgsMapLayerDependency, so that it can be used in a QSet
*/
inline uint qHash( const QgsMapLayerDependency& dep )
{
return qHash( dep.layerId() ) + dep.origin() + dep.type();
}

#endif
4 changes: 2 additions & 2 deletions src/core/qgsproject.cpp
Expand Up @@ -893,7 +893,7 @@ bool QgsProject::read()
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() );
it.value()->setDataDependencies( it.value()->dependencies() );
}

// read the project: used by map canvas and legend
Expand Down Expand Up @@ -997,7 +997,7 @@ void QgsProject::onMapLayersAdded( const QList<QgsMapLayer*>& layers )
// 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();
QSet<QgsMapLayerDependency> deps = it.value()->dependencies();
if ( deps.contains( layer->id() ) )
{
// reconnect to change signals
Expand Down

1 comment on commit 0749ba4

@m-kuhn
Copy link
Member

@m-kuhn m-kuhn commented on 0749ba4 Aug 31, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice feature, I'm sure I'll want to use it soon :)

Please sign in to comment.