diff --git a/python/core/core.sip b/python/core/core.sip index 6c568f41e62..580ca629fdb 100644 --- a/python/core/core.sip +++ b/python/core/core.sip @@ -92,6 +92,7 @@ %Include qgsmaplayermodel.sip %Include qgsmaplayerproxymodel.sip %Include qgsmaplayerrenderer.sip +%Include qgsmaplayerstore.sip %Include qgsmaplayerstylemanager.sip %Include qgsmaprenderercache.sip %Include qgsmaprenderercustompainterjob.sip diff --git a/python/core/processing/qgsprocessingcontext.sip b/python/core/processing/qgsprocessingcontext.sip index 5d99c328b16..a554970843c 100644 --- a/python/core/processing/qgsprocessingcontext.sip +++ b/python/core/processing/qgsprocessingcontext.sip @@ -75,11 +75,11 @@ class QgsProcessingContext Sets the expression ``context``. %End - QgsProject &temporaryLayerStore(); + QgsMapLayerStore *temporaryLayerStore(); %Docstring - Returns a reference to the project used for storing temporary layers during + Returns a reference to the layer store used for storing temporary layers during algorithm execution. - :rtype: QgsProject + :rtype: QgsMapLayerStore %End QgsFeatureRequest::InvalidGeometryCheck invalidGeometryCheck() const; diff --git a/python/core/qgsmaplayerstore.sip b/python/core/qgsmaplayerstore.sip new file mode 100644 index 00000000000..ea3b27e2623 --- /dev/null +++ b/python/core/qgsmaplayerstore.sip @@ -0,0 +1,313 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgsmaplayerstore.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsMapLayerStore : QObject +{ +%Docstring + A storage object for map layers, in which the layers are owned by the + store and have their lifetime bound to the store. +.. versionadded:: 3.0 +%End + +%TypeHeaderCode +#include "qgsmaplayerstore.h" +%End + public: + + explicit QgsMapLayerStore( QObject *parent /TransferThis/ = 0 ); +%Docstring + Constructor for QgsMapLayerStore. +%End + + ~QgsMapLayerStore(); + + int count() const; +%Docstring + Returns the number of layers contained in the store. + :rtype: int +%End + + + int __len__() const; +%Docstring + Returns the number of layers contained in the store. + :rtype: int +%End +%MethodCode + sipRes = sipCpp->count(); +%End + + QgsMapLayer *mapLayer( const QString &id ) const; +%Docstring + Retrieve a pointer to a layer by layer ``id``. + \param id ID of layer to retrieve + :return: matching layer, or None if no matching layer found +.. seealso:: mapLayersByName() +.. seealso:: mapLayers() + :rtype: QgsMapLayer +%End + + QList mapLayersByName( const QString &name ) const; +%Docstring + Retrieve a list of matching layers by layer ``name``. + \param name name of layers to match + :return: list of matching layers +.. seealso:: mapLayer() +.. seealso:: mapLayers() + :rtype: list of QgsMapLayer +%End + + QMap mapLayers() const; +%Docstring + Returns a map of all layers by layer ID. +.. seealso:: mapLayer() +.. seealso:: mapLayersByName() +.. seealso:: layers() + :rtype: QMap +%End + + + QList addMapLayers( const QList &layers /Transfer/); + +%Docstring + \brief + Add a list of ``layers`` to the store. Ownership of the layers is transferred + to the store. + + The layersAdded() and layerWasAdded() signals will always be emitted. + + \param layers A list of layer which should be added to the store. + \param takeOwnership Ownership will be transferred to the layer store. + If you specify false here you have take care of deleting + the layers yourself. Not available in Python. + + :return: a list of the map layers that were added + successfully. If a layer is invalid, or already exists in the store, + it will not be part of the returned list. + +.. seealso:: addMapLayer() + :rtype: list of QgsMapLayer +%End + + QgsMapLayer *addMapLayer( QgsMapLayer *layer /Transfer/); + +%Docstring + \brief + Add a ``layer`` to the store. Ownership of the layer is transferred to the + store. + + The layersAdded() and layerWasAdded() signals will always be emitted. + If you are adding multiple layers at once, you should use + addMapLayers() instead. + + \param layer A layer to add to the store + \param takeOwnership Ownership will be transferred to the layer store. + If you specify false here you have take care of deleting + the layers yourself. Not available in Python. + + :return: None if unable to add layer, otherwise pointer to newly added layer + +.. seealso:: addMapLayers + +.. note:: + + Use addMapLayers() if adding more than one layer at a time. +.. seealso:: addMapLayers() + :rtype: QgsMapLayer +%End + + void removeMapLayers( const QStringList &layerIds ) /PyName=removeMapLayersById/; +%Docstring + \brief + Remove a set of layers from the store by layer ID. + + The specified layers will be removed from the store. + These layers will also be deleted. + + \param layerIds list of IDs of the layers to remove + +.. seealso:: takeMapLayer() +.. seealso:: removeMapLayer() +.. seealso:: removeAllMapLayers() +.. note:: + + available in Python bindings as removeMapLayersById. +%End + + void removeMapLayers( const QList &layers ); +%Docstring + \brief + Remove a set of ``layers`` from the store. + + The specified layers will be removed from the store. + These layers will also be deleted. + + \param layers A list of layers to remove. Null pointers are ignored. + +.. seealso:: takeMapLayer() +.. seealso:: removeMapLayer() +.. seealso:: removeAllMapLayers() +%End + + void removeMapLayer( const QString &id ); +%Docstring + \brief + Remove a layer from the store by layer ``id``. + + The specified layer will be removed from the store. The layer will also be deleted. + + \param id ID of the layer to remove + +.. seealso:: takeMapLayer() +.. seealso:: removeMapLayers() +.. seealso:: removeAllMapLayers() +%End + + void removeMapLayer( QgsMapLayer *layer ); +%Docstring + \brief + Remove a ``layer`` from the store. + + The specified layer will be removed from the store. The layer will also be deleted. + + \param layer The layer to remove. Null pointers are ignored. + +.. seealso:: takeMapLayer() +.. seealso:: removeMapLayers() +.. seealso:: removeAllMapLayers() +%End + + QgsMapLayer *takeMapLayer( QgsMapLayer *layer ) /TransferBack/; +%Docstring + Takes a ``layer`` from the store. If the layer was owned by the store, the + layer will be returned without deleting it. The caller takes ownership of + the layer and is responsible for deleting it. +.. seealso:: removeMapLayer() + :rtype: QgsMapLayer +%End + + void removeAllMapLayers(); +%Docstring + Removes all registered layers. These layers will also be deleted. + +.. note:: + + Calling this method will cause the removeAll() signal to + be emitted. +.. seealso:: removeMapLayer() +.. seealso:: removeMapLayers() +%End + + signals: + + void layersWillBeRemoved( const QStringList &layerIds ); +%Docstring + Emitted when one or more layers are about to be removed from the store. + + \param layerIds A list of IDs for the layers which are to be removed. +.. seealso:: layerWillBeRemoved() +.. seealso:: layersRemoved() +%End + + void layersWillBeRemoved( const QList &layers ); +%Docstring + Emitted when one or more layers are about to be removed from the store. + + \param layers A list of layers which are to be removed. +.. seealso:: layerWillBeRemoved() +.. seealso:: layersRemoved() +%End + + void layerWillBeRemoved( const QString &layerId ); +%Docstring + Emitted when a layer is about to be removed from the store. + + \param layerId The ID of the layer to be removed. + +.. note:: + + Consider using layersWillBeRemoved() instead. +.. seealso:: layersWillBeRemoved() +.. seealso:: layerRemoved() +%End + + void layerWillBeRemoved( QgsMapLayer *layer ); +%Docstring + Emitted when a layer is about to be removed from the store. + + \param layer The layer to be removed. + +.. note:: + + Consider using layersWillBeRemoved() instead. +.. seealso:: layersWillBeRemoved() +.. seealso:: layerRemoved() +%End + + void layersRemoved( const QStringList &layerIds ); +%Docstring + Emitted after one or more layers were removed from the store. + + \param layerIds A list of IDs of the layers which were removed. +.. seealso:: layersWillBeRemoved() +%End + + void layerRemoved( const QString &layerId ); +%Docstring + Emitted after a layer was removed from the store. + + \param layerId The ID of the layer removed. + +.. note:: + + Consider using layersRemoved() instead +.. seealso:: layerWillBeRemoved() +%End + + void allLayersRemoved(); +%Docstring + Emitted when all layers are removed, before layersWillBeRemoved() and + layerWillBeRemoved() signals are emitted. The layersWillBeRemoved() and + layerWillBeRemoved() signals will still be emitted following this signal. + You can use this signal to do easy (and fast) cleanup. +%End + + void layersAdded( const QList &layers ); +%Docstring + Emitted when one or more layers were added to the store. + + \param layers List of layers which have been added. + +.. seealso:: legendLayersAdded() +.. seealso:: layerWasAdded() +%End + + void layerWasAdded( QgsMapLayer *layer ); +%Docstring + Emitted when a ``layer`` was added to the store. + +.. note:: + + Consider using layersAdded() instead +.. seealso:: layersAdded() +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgsmaplayerstore.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/qgsproject.sip b/python/core/qgsproject.sip index 5f6312f54c2..cd598968c90 100644 --- a/python/core/qgsproject.sip +++ b/python/core/qgsproject.sip @@ -422,6 +422,8 @@ class QgsProject : QObject, QgsExpressionContextGenerator */ const QgsLabelingEngineSettings &labelingEngineSettings() const; + QgsMapLayerStore *layerStore(); + int count() const; QgsMapLayer *mapLayer( const QString &layerId ) const; diff --git a/python/plugins/processing/tools/vector.py b/python/plugins/processing/tools/vector.py index 293df7e488b..787c5f5623d 100644 --- a/python/plugins/processing/tools/vector.py +++ b/python/plugins/processing/tools/vector.py @@ -481,7 +481,7 @@ def createVectorWriter(destination, encoding, fields, geometryType, crs, context layer = QgsVectorLayer(uri, destination, 'memory') sink = layer.dataProvider() - context.temporaryLayerStore().addMapLayer(layer, False) + context.temporaryLayerStore().addMapLayer(layer) destination = layer.id() elif destination.startswith(POSTGIS_LAYER_PREFIX): uri = QgsDataSourceUri(destination[len(POSTGIS_LAYER_PREFIX):]) @@ -518,7 +518,7 @@ def createVectorWriter(destination, encoding, fields, geometryType, crs, context layer = QgsVectorLayer(uri.uri(), uri.table(), "postgres") sink = layer.dataProvider() - context.temporaryLayerStore().addMapLayer(layer, False) + context.temporaryLayerStore().addMapLayer(layer) elif destination.startswith(SPATIALITE_LAYER_PREFIX): uri = QgsDataSourceUri(destination[len(SPATIALITE_LAYER_PREFIX):]) try: @@ -548,7 +548,7 @@ def createVectorWriter(destination, encoding, fields, geometryType, crs, context layer = QgsVectorLayer(uri.uri(), uri.table(), "spatialite") sink = layer.dataProvider() - context.temporaryLayerStore().addMapLayer(layer, False) + context.temporaryLayerStore().addMapLayer(layer) else: formats = QgsVectorFileWriter.supportedFiltersAndFormats() OGRCodes = {} diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 96bf91d0eb0..80e1647a1ff 100755 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -178,6 +178,7 @@ SET(QGIS_CORE_SRCS qgsmaplayerlegend.cpp qgsmaplayermodel.cpp qgsmaplayerproxymodel.cpp + qgsmaplayerstore.cpp qgsmaplayerstylemanager.cpp qgsmaprenderercache.cpp qgsmaprenderercustompainterjob.cpp @@ -527,6 +528,7 @@ SET(QGIS_CORE_MOC_HDRS qgsmaplayerlegend.h qgsmaplayermodel.h qgsmaplayerproxymodel.h + qgsmaplayerstore.h qgsmaplayerstylemanager.h qgsmaprenderercache.h qgsmaprenderercustompainterjob.h diff --git a/src/core/processing/qgsprocessingcontext.h b/src/core/processing/qgsprocessingcontext.h index ad58cd41c16..d7f692d87c0 100644 --- a/src/core/processing/qgsprocessingcontext.h +++ b/src/core/processing/qgsprocessingcontext.h @@ -90,10 +90,10 @@ class CORE_EXPORT QgsProcessingContext void setExpressionContext( const QgsExpressionContext &context ) { mExpressionContext = context; } /** - * Returns a reference to the project used for storing temporary layers during + * Returns a reference to the layer store used for storing temporary layers during * algorithm execution. */ - QgsProject &temporaryLayerStore() { return tempProject; } + QgsMapLayerStore *temporaryLayerStore() { return &tempLayerStore; } /** * Returns the behavior used for checking invalid geometries in input layers. @@ -147,7 +147,7 @@ class CORE_EXPORT QgsProcessingContext QgsProcessingContext::Flags mFlags = 0; QPointer< QgsProject > mProject; //! Temporary project owned by the context, used for storing temporarily loaded map layers - QgsProject tempProject; + QgsMapLayerStore tempLayerStore; QgsExpressionContext mExpressionContext; QgsFeatureRequest::InvalidGeometryCheck mInvalidGeometryCheck = QgsFeatureRequest::GeometryNoCheck; std::function< void( const QgsFeature & ) > mInvalidGeometryCallback; diff --git a/src/core/processing/qgsprocessingutils.cpp b/src/core/processing/qgsprocessingutils.cpp index cda05ae8122..1f012af1381 100644 --- a/src/core/processing/qgsprocessingutils.cpp +++ b/src/core/processing/qgsprocessingutils.cpp @@ -85,12 +85,27 @@ QList QgsProcessingUtils::compatibleLayers( QgsProject *project, return layers; } -QgsMapLayer *QgsProcessingUtils::mapLayerFromProject( const QString &string, QgsProject *project ) +QgsMapLayer *QgsProcessingUtils::mapLayerFromStore( const QString &string, QgsMapLayerStore *store ) { - if ( string.isEmpty() ) + if ( !store || string.isEmpty() ) return nullptr; - QList< QgsMapLayer * > layers = compatibleLayers( project, false ); + QList< QgsMapLayer * > layers = store->mapLayers().values(); + + layers.erase( std::remove_if( layers.begin(), layers.end(), []( QgsMapLayer * layer ) + { + switch ( layer->type() ) + { + case QgsMapLayer::VectorLayer: + return !canUseLayer( qobject_cast< QgsVectorLayer * >( layer ) ); + case QgsMapLayer::RasterLayer: + return !canUseLayer( qobject_cast< QgsRasterLayer * >( layer ) ); + case QgsMapLayer::PluginLayer: + return true; + } + return true; + } ), layers.end() ); + Q_FOREACH ( QgsMapLayer *l, layers ) { if ( l->id() == string ) @@ -108,6 +123,7 @@ QgsMapLayer *QgsProcessingUtils::mapLayerFromProject( const QString &string, Qgs } return nullptr; } + ///@cond PRIVATE class ProjectionSettingRestorer { @@ -159,11 +175,15 @@ QgsMapLayer *QgsProcessingUtils::mapLayerFromString( const QString &string, QgsP return nullptr; // prefer project layers - QgsMapLayer *layer = mapLayerFromProject( string, context.project() ); - if ( layer ) - return layer; + QgsMapLayer *layer = nullptr; + if ( context.project() ) + { + QgsMapLayer *layer = mapLayerFromStore( string, context.project()->layerStore() ); + if ( layer ) + return layer; + } - layer = mapLayerFromProject( string, &context.temporaryLayerStore() ); + layer = mapLayerFromStore( string, context.temporaryLayerStore() ); if ( layer ) return layer; @@ -173,7 +193,7 @@ QgsMapLayer *QgsProcessingUtils::mapLayerFromString( const QString &string, QgsP layer = loadMapLayerFromString( string ); if ( layer ) { - context.temporaryLayerStore().addMapLayer( layer ); + context.temporaryLayerStore()->addMapLayer( layer ); return layer; } else diff --git a/src/core/processing/qgsprocessingutils.h b/src/core/processing/qgsprocessingutils.h index 72792d13b62..a26dfc5d941 100644 --- a/src/core/processing/qgsprocessingutils.h +++ b/src/core/processing/qgsprocessingutils.h @@ -27,6 +27,7 @@ class QgsProject; class QgsProcessingContext; +class QgsMapLayerStore; #include @@ -134,19 +135,19 @@ class CORE_EXPORT QgsProcessingUtils static bool canUseLayer( const QgsRasterLayer *layer ); static bool canUseLayer( const QgsVectorLayer *layer, - const QList< QgsWkbTypes::GeometryType > &geometryTypes ); + const QList< QgsWkbTypes::GeometryType > &geometryTypes = QList< QgsWkbTypes::GeometryType >() ); /** - * Interprets a \a string as a map layer from a project. + * Interprets a \a string as a map layer from a store. * - * This method attempts to match a string to a project map layer, using + * This method attempts to match a string to a store map layer, using * first the layer ID, then layer names, and finally layer source. * If the string matches a normalized version of any layer source - * for layers in the specified \a project, then those matching layers will be + * for layers in the specified \a store, then those matching layers will be * returned. * \see mapLayerFromString() */ - static QgsMapLayer *mapLayerFromProject( const QString &string, QgsProject *project ); + static QgsMapLayer *mapLayerFromStore( const QString &string, QgsMapLayerStore *store ); /** * Interprets a string as a map layer. The method will attempt to diff --git a/src/core/qgsmaplayerstore.cpp b/src/core/qgsmaplayerstore.cpp new file mode 100644 index 00000000000..01e489639cf --- /dev/null +++ b/src/core/qgsmaplayerstore.cpp @@ -0,0 +1,197 @@ +/*************************************************************************** + qgsmaplayerstore.cpp + -------------------- + begin : May 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail 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. * + * * + ***************************************************************************/ + +#include "qgsmaplayerstore.h" +#include "qgslogger.h" + +QgsMapLayerStore::QgsMapLayerStore( QObject *parent ) + : QObject( parent ) +{} + +QgsMapLayerStore::~QgsMapLayerStore() +{ + removeAllMapLayers(); +} + +int QgsMapLayerStore::count() const +{ + return mMapLayers.size(); +} + +QgsMapLayer *QgsMapLayerStore::mapLayer( const QString &layerId ) const +{ + return mMapLayers.value( layerId ); +} + +QList QgsMapLayerStore::mapLayersByName( const QString &layerName ) const +{ + QList myResultList; + Q_FOREACH ( QgsMapLayer *layer, mMapLayers ) + { + if ( layer->name() == layerName ) + { + myResultList << layer; + } + } + return myResultList; +} + +QList QgsMapLayerStore::addMapLayers( const QList &layers, bool takeOwnership ) +{ + QList myResultList; + Q_FOREACH ( QgsMapLayer *myLayer, layers ) + { + if ( !myLayer || !myLayer->isValid() ) + { + QgsDebugMsg( "Cannot add invalid layers" ); + continue; + } + //check the layer is not already registered! + if ( !mMapLayers.contains( myLayer->id() ) ) + { + mMapLayers[myLayer->id()] = myLayer; + myResultList << mMapLayers[myLayer->id()]; + if ( takeOwnership ) + { + myLayer->setParent( this ); + } + connect( myLayer, &QObject::destroyed, this, &QgsMapLayerStore::onMapLayerDeleted ); + emit layerWasAdded( myLayer ); + } + } + if ( !myResultList.isEmpty() ) + { + emit layersAdded( myResultList ); + } + return myResultList; +} + +QgsMapLayer * +QgsMapLayerStore::addMapLayer( QgsMapLayer *layer, bool takeOwnership ) +{ + QList addedLayers; + addedLayers = addMapLayers( QList() << layer, takeOwnership ); + return addedLayers.isEmpty() ? nullptr : addedLayers[0]; +} + +void QgsMapLayerStore::removeMapLayers( const QStringList &layerIds ) +{ + QList layers; + Q_FOREACH ( const QString &myId, layerIds ) + { + layers << mMapLayers.value( myId ); + } + + removeMapLayers( layers ); +} + +void QgsMapLayerStore::removeMapLayers( const QList &layers ) +{ + if ( layers.isEmpty() ) + return; + + QStringList layerIds; + QList layerList; + + Q_FOREACH ( QgsMapLayer *layer, layers ) + { + // check layer and the store contains it + if ( layer && mMapLayers.contains( layer->id() ) ) + { + layerIds << layer->id(); + layerList << layer; + } + } + + if ( layerIds.isEmpty() ) + return; + + emit layersWillBeRemoved( layerIds ); + emit layersWillBeRemoved( layerList ); + + Q_FOREACH ( QgsMapLayer *lyr, layerList ) + { + QString myId( lyr->id() ); + emit layerWillBeRemoved( myId ); + emit layerWillBeRemoved( lyr ); + mMapLayers.remove( myId ); + if ( lyr->parent() == this ) + { + delete lyr; + } + emit layerRemoved( myId ); + } + + emit layersRemoved( layerIds ); +} + +void QgsMapLayerStore::removeMapLayer( const QString &layerId ) +{ + removeMapLayers( QList() << mMapLayers.value( layerId ) ); +} + +void QgsMapLayerStore::removeMapLayer( QgsMapLayer *layer ) +{ + if ( layer ) + removeMapLayers( QList() << layer ); +} + +QgsMapLayer *QgsMapLayerStore::takeMapLayer( QgsMapLayer *layer ) +{ + if ( !layer ) + return nullptr; + + if ( mMapLayers.contains( layer->id() ) ) + { + emit layersWillBeRemoved( QStringList() << layer->id() ); + emit layersWillBeRemoved( QList() << layer ); + emit layerWillBeRemoved( layer->id() ); + emit layerWillBeRemoved( layer ); + + mMapLayers.remove( layer->id() ); + layer->setParent( nullptr ); + emit layerRemoved( layer->id() ); + emit layersRemoved( QStringList() << layer->id() ); + return layer; + } + return nullptr; //don't return layer - it wasn't owned and accordingly we aren't transferring ownership +} + +void QgsMapLayerStore::removeAllMapLayers() +{ + emit allLayersRemoved(); + // now let all observers know to clear themselves, + // and then consequently any of their map legends + removeMapLayers( mMapLayers.keys() ); + mMapLayers.clear(); +} + +void QgsMapLayerStore::onMapLayerDeleted( QObject *obj ) +{ + QString id = mMapLayers.key( static_cast( obj ) ); + + if ( !id.isNull() ) + { + QgsDebugMsg( QString( "Map layer deleted without unregistering! %1" ).arg( id ) ); + mMapLayers.remove( id ); + } +} + +QMap QgsMapLayerStore::mapLayers() const +{ + return mMapLayers; +} diff --git a/src/core/qgsmaplayerstore.h b/src/core/qgsmaplayerstore.h new file mode 100644 index 00000000000..d8d72656028 --- /dev/null +++ b/src/core/qgsmaplayerstore.h @@ -0,0 +1,337 @@ +/*************************************************************************** + qgsmaplayerstore.h + ------------------ + begin : May 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail 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 QGSMAPLAYERSTORE_H +#define QGSMAPLAYERSTORE_H + +#include "qgis_core.h" +#include "qgis.h" +#include "qgsmaplayer.h" +#include + +/** + * \class QgsMapLayerStore + * \ingroup core + * A storage object for map layers, in which the layers are owned by the + * store and have their lifetime bound to the store. + * \since QGIS 3.0 +*/ + +class CORE_EXPORT QgsMapLayerStore : public QObject +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsMapLayerStore. + */ + explicit QgsMapLayerStore( QObject *parent SIP_TRANSFERTHIS = nullptr ); + + ~QgsMapLayerStore(); + + /** + * Returns the number of layers contained in the store. + */ + int count() const; + +#ifdef SIP_RUN + + /** + * Returns the number of layers contained in the store. + */ + int __len__() const; + % MethodCode + sipRes = sipCpp->count(); + % End +#endif + + /** + * Retrieve a pointer to a layer by layer \a id. + * \param id ID of layer to retrieve + * \returns matching layer, or nullptr if no matching layer found + * \see mapLayersByName() + * \see mapLayers() + */ + QgsMapLayer *mapLayer( const QString &id ) const; + + /** + * Retrieve a list of matching layers by layer \a name. + * \param name name of layers to match + * \returns list of matching layers + * \see mapLayer() + * \see mapLayers() + */ + QList mapLayersByName( const QString &name ) const; + + /** + * Returns a map of all layers by layer ID. + * \see mapLayer() + * \see mapLayersByName() + * \see layers() + */ + QMap mapLayers() const; + +#ifndef SIP_RUN + + /** + * Returns a list of registered map layers with a specified layer type. + * + * Example: + * + * QVector vectorLayers = store->layers(); + * + * \note not available in Python bindings + * \see mapLayers() + */ + template + QVector layers() const + { + QVector layers; + QMap::const_iterator layerIt = mMapLayers.constBegin(); + for ( ; layerIt != mMapLayers.constEnd(); ++layerIt ) + { + T tLayer = qobject_cast( layerIt.value() ); + if ( tLayer ) + { + layers << tLayer; + } + } + return layers; + } +#endif + + /** + * \brief + * Add a list of \a layers to the store. Ownership of the layers is transferred + * to the store. + * + * The layersAdded() and layerWasAdded() signals will always be emitted. + * + * \param layers A list of layer which should be added to the store. + * \param takeOwnership Ownership will be transferred to the layer store. + * If you specify false here you have take care of deleting + * the layers yourself. Not available in Python. + * + * \returns a list of the map layers that were added + * successfully. If a layer is invalid, or already exists in the store, + * it will not be part of the returned list. + * + * \see addMapLayer() + */ + QList addMapLayers( const QList &layers SIP_TRANSFER, + bool takeOwnership SIP_PYARGREMOVE = true ); + + /** + * \brief + * Add a \a layer to the store. Ownership of the layer is transferred to the + * store. + * + * The layersAdded() and layerWasAdded() signals will always be emitted. + * If you are adding multiple layers at once, you should use + * addMapLayers() instead. + * + * \param layer A layer to add to the store + * \param takeOwnership Ownership will be transferred to the layer store. + * If you specify false here you have take care of deleting + * the layers yourself. Not available in Python. + * + * \returns nullptr if unable to add layer, otherwise pointer to newly added layer + * + * \see addMapLayers + * + * \note Use addMapLayers() if adding more than one layer at a time. + * \see addMapLayers() + */ + QgsMapLayer *addMapLayer( QgsMapLayer *layer SIP_TRANSFER, + bool takeOwnership SIP_PYARGREMOVE = true ); + + /** + * \brief + * Remove a set of layers from the store by layer ID. + * + * The specified layers will be removed from the store. + * These layers will also be deleted. + * + * \param layerIds list of IDs of the layers to remove + * + * \see takeMapLayer() + * \see removeMapLayer() + * \see removeAllMapLayers() + * \note available in Python bindings as removeMapLayersById. + */ + void removeMapLayers( const QStringList &layerIds ) SIP_PYNAME( removeMapLayersById ); + + /** + * \brief + * Remove a set of \a layers from the store. + * + * The specified layers will be removed from the store. + * These layers will also be deleted. + * + * \param layers A list of layers to remove. Null pointers are ignored. + * + * \see takeMapLayer() + * \see removeMapLayer() + * \see removeAllMapLayers() + */ + void removeMapLayers( const QList &layers ); + + /** + * \brief + * Remove a layer from the store by layer \a id. + * + * The specified layer will be removed from the store. The layer will also be deleted. + * + * \param id ID of the layer to remove + * + * \see takeMapLayer() + * \see removeMapLayers() + * \see removeAllMapLayers() + */ + void removeMapLayer( const QString &id ); + + /** + * \brief + * Remove a \a layer from the store. + * + * The specified layer will be removed from the store. The layer will also be deleted. + * + * \param layer The layer to remove. Null pointers are ignored. + * + * \see takeMapLayer() + * \see removeMapLayers() + * \see removeAllMapLayers() + */ + void removeMapLayer( QgsMapLayer *layer ); + + /** + * Takes a \a layer from the store. If the layer was owned by the store, the + * layer will be returned without deleting it. The caller takes ownership of + * the layer and is responsible for deleting it. + * \see removeMapLayer() + */ + QgsMapLayer *takeMapLayer( QgsMapLayer *layer ) SIP_TRANSFERBACK; + + /** + * Removes all registered layers. These layers will also be deleted. + * + * \note Calling this method will cause the removeAll() signal to + * be emitted. + * \see removeMapLayer() + * \see removeMapLayers() + */ + void removeAllMapLayers(); + + signals: + + /** + * Emitted when one or more layers are about to be removed from the store. + * + * \param layerIds A list of IDs for the layers which are to be removed. + * \see layerWillBeRemoved() + * \see layersRemoved() + */ + void layersWillBeRemoved( const QStringList &layerIds ); + + /** + * Emitted when one or more layers are about to be removed from the store. + * + * \param layers A list of layers which are to be removed. + * \see layerWillBeRemoved() + * \see layersRemoved() + */ + void layersWillBeRemoved( const QList &layers ); + + /** + * Emitted when a layer is about to be removed from the store. + * + * \param layerId The ID of the layer to be removed. + * + * \note Consider using layersWillBeRemoved() instead. + * \see layersWillBeRemoved() + * \see layerRemoved() + */ + void layerWillBeRemoved( const QString &layerId ); + + /** + * Emitted when a layer is about to be removed from the store. + * + * \param layer The layer to be removed. + * + * \note Consider using layersWillBeRemoved() instead. + * \see layersWillBeRemoved() + * \see layerRemoved() + */ + void layerWillBeRemoved( QgsMapLayer *layer ); + + /** + * Emitted after one or more layers were removed from the store. + * + * \param layerIds A list of IDs of the layers which were removed. + * \see layersWillBeRemoved() + */ + void layersRemoved( const QStringList &layerIds ); + + /** + * Emitted after a layer was removed from the store. + * + * \param layerId The ID of the layer removed. + * + * \note Consider using layersRemoved() instead + * \see layerWillBeRemoved() + */ + void layerRemoved( const QString &layerId ); + + /** + * Emitted when all layers are removed, before layersWillBeRemoved() and + * layerWillBeRemoved() signals are emitted. The layersWillBeRemoved() and + * layerWillBeRemoved() signals will still be emitted following this signal. + * You can use this signal to do easy (and fast) cleanup. + */ + void allLayersRemoved(); + + /** + * Emitted when one or more layers were added to the store. + * + * \param layers List of layers which have been added. + * + * \see legendLayersAdded() + * \see layerWasAdded() + */ + void layersAdded( const QList &layers ); + + /** + * Emitted when a \a layer was added to the store. + * + * \note Consider using layersAdded() instead + * \see layersAdded() + */ + void layerWasAdded( QgsMapLayer *layer ); + + private slots: + + void onMapLayerDeleted( QObject *obj ); + + private: + + QMap mMapLayers; + +}; + +#endif //QGSMAPLAYERSTORE_H diff --git a/src/core/qgsproject.cpp b/src/core/qgsproject.cpp index b3ad31ca1e6..3827b5e3187 100644 --- a/src/core/qgsproject.cpp +++ b/src/core/qgsproject.cpp @@ -46,6 +46,7 @@ #include "qgssettings.h" #include "qgsmaplayerlistutils.h" #include "qgslayoutmanager.h" +#include "qgsmaplayerstore.h" #include #include @@ -322,6 +323,7 @@ void removeKey_( const QString &scope, QgsProject::QgsProject( QObject *parent ) : QObject( parent ) + , mLayerStore( new QgsMapLayerStore( this ) ) , mBadLayerHandler( new QgsProjectBadLayerHandler() ) , mSnappingConfig( this ) , mRelationManager( new QgsRelationManager( this ) ) @@ -343,6 +345,21 @@ QgsProject::QgsProject( QObject *parent ) connect( this, &QgsProject::layersAdded, this, &QgsProject::onMapLayersAdded ); connect( this, &QgsProject::layersRemoved, this, [ = ] { cleanTransactionGroups(); } ); connect( this, static_cast < void ( QgsProject::* )( const QList & ) >( &QgsProject::layersWillBeRemoved ), this, &QgsProject::onMapLayersRemoved ); + + // proxy map layer store signals to this + connect( mLayerStore.get(), static_cast( &QgsMapLayerStore::layersWillBeRemoved ), + this, static_cast( &QgsProject::layersWillBeRemoved ) ); + connect( mLayerStore.get(), static_cast & )>( &QgsMapLayerStore::layersWillBeRemoved ), + this, static_cast & )>( &QgsProject::layersWillBeRemoved ) ); + connect( mLayerStore.get(), static_cast( &QgsMapLayerStore::layerWillBeRemoved ), + this, static_cast( &QgsProject::layerWillBeRemoved ) ); + connect( mLayerStore.get(), static_cast( &QgsMapLayerStore::layerWillBeRemoved ), + this, static_cast( &QgsProject::layerWillBeRemoved ) ); + connect( mLayerStore.get(), static_cast( &QgsMapLayerStore::layersRemoved ), this, &QgsProject::layersRemoved ); + connect( mLayerStore.get(), &QgsMapLayerStore::layerRemoved, this, &QgsProject::layerRemoved ); + connect( mLayerStore.get(), &QgsMapLayerStore::allLayersRemoved, this, &QgsProject::removeAll ); + connect( mLayerStore.get(), &QgsMapLayerStore::layersAdded, this, &QgsProject::layersAdded ); + connect( mLayerStore.get(), &QgsMapLayerStore::layerWasAdded, this, &QgsProject::layerWasAdded ); } @@ -884,7 +901,8 @@ bool QgsProject::read() // Resolve references to other vector layers // Needs to be done here once all dependent layers are loaded - for ( QMap::iterator it = mMapLayers.begin(); it != mMapLayers.end(); it++ ) + QMap layers = mLayerStore->mapLayers(); + for ( QMap::iterator it = layers.begin(); it != layers.end(); it++ ) { if ( QgsVectorLayer *vl = qobject_cast( it.value() ) ) vl->resolveReferences( this ); @@ -1049,6 +1067,16 @@ const QgsLabelingEngineSettings &QgsProject::labelingEngineSettings() const return *mLabelingEngineSettings; } +QgsMapLayerStore *QgsProject::layerStore() +{ + return mLayerStore.get(); +} + +const QgsMapLayerStore *QgsProject::layerStore() const +{ + return mLayerStore.get(); +} + QList QgsProject::avoidIntersectionsLayers() const { QList layers; @@ -2066,31 +2094,23 @@ QMap, QgsTransactionGroup *> QgsProject::transactionGrou // -// QgsMapLayerRegistry methods +// QgsMapLayerStore methods // int QgsProject::count() const { - return mMapLayers.size(); + return mLayerStore->count(); } QgsMapLayer *QgsProject::mapLayer( const QString &layerId ) const { - return mMapLayers.value( layerId ); + return mLayerStore->mapLayer( layerId ); } QList QgsProject::mapLayersByName( const QString &layerName ) const { - QList myResultList; - Q_FOREACH ( QgsMapLayer *layer, mMapLayers ) - { - if ( layer->name() == layerName ) - { - myResultList << layer; - } - } - return myResultList; + return mLayerStore->mapLayersByName( layerName ); } QList QgsProject::addMapLayers( @@ -2098,31 +2118,9 @@ QList QgsProject::addMapLayers( bool addToLegend, bool takeOwnership ) { - QList myResultList; - Q_FOREACH ( QgsMapLayer *myLayer, layers ) - { - if ( !myLayer || !myLayer->isValid() ) - { - QgsDebugMsg( "Cannot add invalid layers" ); - continue; - } - //check the layer is not already registered! - if ( !mMapLayers.contains( myLayer->id() ) ) - { - mMapLayers[myLayer->id()] = myLayer; - myResultList << mMapLayers[myLayer->id()]; - if ( takeOwnership ) - { - myLayer->setParent( this ); - } - connect( myLayer, &QObject::destroyed, this, &QgsProject::onMapLayerDeleted ); - emit layerWasAdded( myLayer ); - } - } + QList myResultList = mLayerStore->addMapLayers( layers, takeOwnership ); if ( !myResultList.isEmpty() ) { - emit layersAdded( myResultList ); - if ( addToLegend ) emit legendLayersAdded( myResultList ); } @@ -2141,116 +2139,47 @@ QgsProject::addMapLayer( QgsMapLayer *layer, void QgsProject::removeMapLayers( const QStringList &layerIds ) { - QList layers; - Q_FOREACH ( const QString &myId, layerIds ) - { - layers << mMapLayers.value( myId ); - } - - removeMapLayers( layers ); + mLayerStore->removeMapLayers( layerIds ); } void QgsProject::removeMapLayers( const QList &layers ) { - if ( layers.isEmpty() ) - return; - - QStringList layerIds; - QList layerList; - - Q_FOREACH ( QgsMapLayer *layer, layers ) - { - // check layer and the registry contains it - if ( layer && mMapLayers.contains( layer->id() ) ) - { - layerIds << layer->id(); - layerList << layer; - } - } - - if ( layerIds.isEmpty() ) - return; - - emit layersWillBeRemoved( layerIds ); - emit layersWillBeRemoved( layerList ); - - Q_FOREACH ( QgsMapLayer *lyr, layerList ) - { - QString myId( lyr->id() ); - emit layerWillBeRemoved( myId ); - emit layerWillBeRemoved( lyr ); - mMapLayers.remove( myId ); - if ( lyr->parent() == this ) - { - delete lyr; - } - emit layerRemoved( myId ); - } - - emit layersRemoved( layerIds ); + mLayerStore->removeMapLayers( layers ); } void QgsProject::removeMapLayer( const QString &layerId ) { - removeMapLayers( QList() << mMapLayers.value( layerId ) ); + mLayerStore->removeMapLayer( layerId ); } void QgsProject::removeMapLayer( QgsMapLayer *layer ) { - if ( layer ) - removeMapLayers( QList() << layer ); + mLayerStore->removeMapLayer( layer ); } QgsMapLayer *QgsProject::takeMapLayer( QgsMapLayer *layer ) { - if ( !layer ) - return nullptr; - - if ( mMapLayers.contains( layer->id() ) ) - { - emit layersWillBeRemoved( QStringList() << layer->id() ); - emit layersWillBeRemoved( QList() << layer ); - emit layerWillBeRemoved( layer->id() ); - emit layerWillBeRemoved( layer ); - - mMapLayers.remove( layer->id() ); - layer->setParent( nullptr ); - emit layerRemoved( layer->id() ); - emit layersRemoved( QStringList() << layer->id() ); - return layer; - } - return nullptr; //don't return layer - it wasn't owned and accordingly we aren't transferring ownership + return mLayerStore->takeMapLayer( layer ); } void QgsProject::removeAllMapLayers() { - emit removeAll(); - // now let all observers know to clear themselves, - // and then consequently any of their map legends - removeMapLayers( mMapLayers.keys() ); - mMapLayers.clear(); + mLayerStore->removeAllMapLayers(); } void QgsProject::reloadAllLayers() { - Q_FOREACH ( QgsMapLayer *layer, mMapLayers ) + QMap layers = mLayerStore->mapLayers(); + QMap::const_iterator it = layers.constBegin(); + for ( ; it != layers.constEnd(); ++it ) { - layer->reload(); - } -} - -void QgsProject::onMapLayerDeleted( QObject *obj ) -{ - QString id = mMapLayers.key( static_cast( obj ) ); - - if ( !id.isNull() ) - { - QgsDebugMsg( QString( "Map layer deleted without unregistering! %1" ).arg( id ) ); - mMapLayers.remove( id ); + it.value()->reload(); } } QMap QgsProject::mapLayers() const { - return mMapLayers; + return mLayerStore->mapLayers(); } + + diff --git a/src/core/qgsproject.h b/src/core/qgsproject.h index b6d0d1a1015..13d03439624 100644 --- a/src/core/qgsproject.h +++ b/src/core/qgsproject.h @@ -39,6 +39,7 @@ #include "qgscoordinatereferencesystem.h" #include "qgsprojectproperty.h" #include "qgsmaplayer.h" +#include "qgsmaplayerstore.h" class QFileInfo; class QDomDocument; @@ -529,6 +530,18 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera // Functionality from QgsMapLayerRegistry // + /** + * Returns a pointer to the project's internal layer store. + * /since QGIS 3.0 + */ + QgsMapLayerStore *layerStore(); + + /** + * Returns a pointer to the project's internal layer store. + * /since QGIS 3.0 + */ + SIP_SKIP const QgsMapLayerStore *layerStore() const; + //! Returns the number of registered layers. int count() const; @@ -568,17 +581,7 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera template SIP_SKIP QVector layers() const { - QVector layers; - QMap::const_iterator layerIt = mMapLayers.constBegin(); - for ( ; layerIt != mMapLayers.constEnd(); ++layerIt ) - { - T tLayer = qobject_cast( layerIt.value() ); - if ( tLayer ) - { - layers << tLayer; - } - } - return layers; + return mLayerStore->layers(); } /** @@ -969,8 +972,6 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera void onMapLayersRemoved( const QList &layers ); void cleanTransactionGroups( bool force = false ); - void onMapLayerDeleted( QObject *obj ); - private: static QgsProject *sProject; @@ -1003,7 +1004,7 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera //! \note not available in Python bindings void loadEmbeddedNodes( QgsLayerTreeGroup *group ) SIP_SKIP; - QMap mMapLayers; + std::unique_ptr< QgsMapLayerStore > mLayerStore; QString mErrorMessage; diff --git a/tests/src/core/testqgsprocessing.cpp b/tests/src/core/testqgsprocessing.cpp index 3520f95fb2a..76d5c122827 100644 --- a/tests/src/core/testqgsprocessing.cpp +++ b/tests/src/core/testqgsprocessing.cpp @@ -105,6 +105,7 @@ class TestQgsProcessing: public QObject void compatibleLayers(); void normalizeLayerSource(); void mapLayers(); + void mapLayerFromStore(); void mapLayerFromString(); void algorithm(); void features(); @@ -335,9 +336,37 @@ void TestQgsProcessing::normalizeLayerSource() void TestQgsProcessing::mapLayers() { - // test mapLayerFromProject + QString testDataDir = QStringLiteral( TEST_DATA_DIR ) + '/'; //defined in CmakeLists.txt + QString raster = testDataDir + "landsat.tif"; + QString vector = testDataDir + "points.shp"; - QgsProject p; + // test loadMapLayerFromString with raster + QgsMapLayer *l = QgsProcessingUtils::loadMapLayerFromString( raster ); + QVERIFY( l->isValid() ); + QCOMPARE( l->type(), QgsMapLayer::RasterLayer ); + delete l; + + //test with vector + l = QgsProcessingUtils::loadMapLayerFromString( vector ); + QVERIFY( l->isValid() ); + QCOMPARE( l->type(), QgsMapLayer::VectorLayer ); + delete l; + + l = QgsProcessingUtils::loadMapLayerFromString( QString() ); + QVERIFY( !l ); + l = QgsProcessingUtils::loadMapLayerFromString( QStringLiteral( "so much room for activities!" ) ); + QVERIFY( !l ); + l = QgsProcessingUtils::loadMapLayerFromString( testDataDir + "multipoint.shp" ); + QVERIFY( l->isValid() ); + QCOMPARE( l->type(), QgsMapLayer::VectorLayer ); + delete l; +} + +void TestQgsProcessing::mapLayerFromStore() +{ + // test mapLayerFromStore + + QgsMapLayerStore store; // add a bunch of layers to a project QString testDataDir = QStringLiteral( TEST_DATA_DIR ) + '/'; //defined in CmakeLists.txt @@ -352,33 +381,19 @@ void TestQgsProcessing::mapLayers() QgsVectorLayer *v1 = new QgsVectorLayer( "Polygon", "V4", "memory" ); QgsVectorLayer *v2 = new QgsVectorLayer( "Point", "v1", "memory" ); - p.addMapLayers( QList() << r1 << r2 << v1 << v2 ); + store.addMapLayers( QList() << r1 << r2 << v1 << v2 ); - QVERIFY( ! QgsProcessingUtils::mapLayerFromProject( QString(), nullptr ) ); - QVERIFY( ! QgsProcessingUtils::mapLayerFromProject( QStringLiteral( "v1" ), nullptr ) ); - QVERIFY( ! QgsProcessingUtils::mapLayerFromProject( QString(), &p ) ); - QCOMPARE( QgsProcessingUtils::mapLayerFromProject( raster1, &p ), r1 ); - QCOMPARE( QgsProcessingUtils::mapLayerFromProject( raster2, &p ), r2 ); - QCOMPARE( QgsProcessingUtils::mapLayerFromProject( "R1", &p ), r1 ); - QCOMPARE( QgsProcessingUtils::mapLayerFromProject( "ar2", &p ), r2 ); - QCOMPARE( QgsProcessingUtils::mapLayerFromProject( "V4", &p ), v1 ); - QCOMPARE( QgsProcessingUtils::mapLayerFromProject( "v1", &p ), v2 ); - QCOMPARE( QgsProcessingUtils::mapLayerFromProject( r1->id(), &p ), r1 ); - QCOMPARE( QgsProcessingUtils::mapLayerFromProject( v1->id(), &p ), v1 ); - - // test loadMapLayerFromString - QgsMapLayer *l = QgsProcessingUtils::loadMapLayerFromString( raster2 ); - QVERIFY( l->isValid() ); - QCOMPARE( l->type(), QgsMapLayer::RasterLayer ); - delete l; - l = QgsProcessingUtils::loadMapLayerFromString( QString() ); - QVERIFY( !l ); - l = QgsProcessingUtils::loadMapLayerFromString( QStringLiteral( "so much room for activities!" ) ); - QVERIFY( !l ); - l = QgsProcessingUtils::loadMapLayerFromString( testDataDir + "multipoint.shp" ); - QVERIFY( l->isValid() ); - QCOMPARE( l->type(), QgsMapLayer::VectorLayer ); - delete l; + QVERIFY( ! QgsProcessingUtils::mapLayerFromStore( QString(), nullptr ) ); + QVERIFY( ! QgsProcessingUtils::mapLayerFromStore( QStringLiteral( "v1" ), nullptr ) ); + QVERIFY( ! QgsProcessingUtils::mapLayerFromStore( QString(), &store ) ); + QCOMPARE( QgsProcessingUtils::mapLayerFromStore( raster1, &store ), r1 ); + QCOMPARE( QgsProcessingUtils::mapLayerFromStore( raster2, &store ), r2 ); + QCOMPARE( QgsProcessingUtils::mapLayerFromStore( "R1", &store ), r1 ); + QCOMPARE( QgsProcessingUtils::mapLayerFromStore( "ar2", &store ), r2 ); + QCOMPARE( QgsProcessingUtils::mapLayerFromStore( "V4", &store ), v1 ); + QCOMPARE( QgsProcessingUtils::mapLayerFromStore( "v1", &store ), v2 ); + QCOMPARE( QgsProcessingUtils::mapLayerFromStore( r1->id(), &store ), r1 ); + QCOMPARE( QgsProcessingUtils::mapLayerFromStore( v1->id(), &store ), v1 ); } void TestQgsProcessing::mapLayerFromString() @@ -423,7 +438,7 @@ void TestQgsProcessing::mapLayerFromString() // check that layers in context temporary store are used QgsVectorLayer *v5 = new QgsVectorLayer( "Polygon", "V5", "memory" ); QgsVectorLayer *v6 = new QgsVectorLayer( "Point", "v6", "memory" ); - c.temporaryLayerStore().addMapLayers( QList() << v5 << v6 ); + c.temporaryLayerStore()->addMapLayers( QList() << v5 << v6 ); QCOMPARE( QgsProcessingUtils::mapLayerFromString( "V5", c ), v5 ); QCOMPARE( QgsProcessingUtils::mapLayerFromString( "v6", c ), v6 ); QCOMPARE( QgsProcessingUtils::mapLayerFromString( v5->id(), c ), v5 ); @@ -440,7 +455,7 @@ void TestQgsProcessing::mapLayerFromString() QVERIFY( loadedLayer->isValid() ); QCOMPARE( loadedLayer->type(), QgsMapLayer::RasterLayer ); // should now be in temporary store - QCOMPARE( c.temporaryLayerStore().mapLayer( loadedLayer->id() ), loadedLayer ); + QCOMPARE( c.temporaryLayerStore()->mapLayer( loadedLayer->id() ), loadedLayer ); // since it's now in temporary store, should be accessible even if we deny loading new layers QCOMPARE( QgsProcessingUtils::mapLayerFromString( newRaster, c, false ), loadedLayer ); diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 0bc58b05a9d..a08f00025b9 100755 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -76,6 +76,7 @@ ADD_PYTHON_TEST(PyQgsMapCanvas test_qgsmapcanvas.py) ADD_PYTHON_TEST(PyQgsMapCanvasAnnotationItem test_qgsmapcanvasannotationitem.py) ADD_PYTHON_TEST(PyQgsMapLayer test_qgsmaplayer.py) ADD_PYTHON_TEST(PyQgsMapLayerModel test_qgsmaplayermodel.py) +ADD_PYTHON_TEST(PyQgsMapLayerStore test_qgsmaplayerstore.py) ADD_PYTHON_TEST(PyQgsMapRenderer test_qgsmaprenderer.py) ADD_PYTHON_TEST(PyQgsMapRendererCache test_qgsmaprenderercache.py) ADD_PYTHON_TEST(PyQgsMapThemeCollection test_qgsmapthemecollection.py) @@ -145,7 +146,6 @@ ADD_PYTHON_TEST(PyQgsVectorLayer test_qgsvectorlayer.py) ADD_PYTHON_TEST(PyQgsVectorLayerEditBuffer test_qgsvectorlayereditbuffer.py) ADD_PYTHON_TEST(PyQgsVectorLayerUtils test_qgsvectorlayerutils.py) ADD_PYTHON_TEST(PyQgsZonalStatistics test_qgszonalstatistics.py) -ADD_PYTHON_TEST(PyQgsMapLayerRegistry test_qgsmaplayerregistry.py) ADD_PYTHON_TEST(PyQgsVirtualLayerProvider test_provider_virtual.py) ADD_PYTHON_TEST(PyQgsVirtualLayerDefinition test_qgsvirtuallayerdefinition.py) ADD_PYTHON_TEST(PyQgsLayerDefinition test_qgslayerdefinition.py) diff --git a/tests/src/python/test_qgsmaplayerregistry.py b/tests/src/python/test_qgsmaplayerregistry.py deleted file mode 100644 index 26eb14730ae..00000000000 --- a/tests/src/python/test_qgsmaplayerregistry.py +++ /dev/null @@ -1,536 +0,0 @@ -# -*- coding: utf-8 -*- -"""QGIS Unit tests for QgsProject. - -.. 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__ = 'Alessandro Pasotti' -__date__ = '04/12/2015' -__copyright__ = 'Copyright 2015, The QGIS Project' -# This will get replaced with a git SHA1 when you do a git archive -__revision__ = '$Format:%H$' - -from qgis.core import QgsProject, QgsVectorLayer, QgsMapLayer -from qgis.testing import start_app, unittest -from qgis.PyQt.QtCore import QT_VERSION_STR -import sip - -try: - from qgis.PyQt.QtTest import QSignalSpy - use_signal_spy = True -except: - use_signal_spy = False - -start_app() - - -def createLayer(name): - return QgsVectorLayer("Point?field=x:string", name, "memory") - - -class TestQgsProjectMapLayers(unittest.TestCase): - - def setUp(self): - pass - - def testInstance(self): - """ test retrieving global instance """ - self.assertTrue(QgsProject.instance()) - - # register a layer to the singleton - QgsProject.instance().addMapLayer(createLayer('test')) - - # check that the same instance is returned - self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) - - QgsProject.instance().removeAllMapLayers() - - def test_addMapLayer(self): - """ test adding individual map layers to registry """ - QgsProject.instance().removeAllMapLayers() - - l1 = createLayer('test') - self.assertEqual(QgsProject.instance().addMapLayer(l1), l1) - self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) - self.assertEqual(QgsProject.instance().count(), 1) - - # adding a second layer should leave existing layers intact - l2 = createLayer('test2') - self.assertEqual(QgsProject.instance().addMapLayer(l2), l2) - self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) - self.assertEqual(len(QgsProject.instance().mapLayersByName('test2')), 1) - self.assertEqual(QgsProject.instance().count(), 2) - - QgsProject.instance().removeAllMapLayers() - - def test_addMapLayerAlreadyAdded(self): - """ test that already added layers can't be readded to registry """ - QgsProject.instance().removeAllMapLayers() - - l1 = createLayer('test') - QgsProject.instance().addMapLayer(l1) - self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) - self.assertEqual(QgsProject.instance().count(), 1) - self.assertEqual(QgsProject.instance().addMapLayer(l1), None) - self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) - self.assertEqual(QgsProject.instance().count(), 1) - - QgsProject.instance().removeAllMapLayers() - - def test_addMapLayerInvalid(self): - """ test that invalid map layersd can't be added to registry """ - QgsProject.instance().removeAllMapLayers() - - self.assertEqual(QgsProject.instance().addMapLayer(QgsVectorLayer("Point?field=x:string", 'test', "xxx")), None) - self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 0) - self.assertEqual(QgsProject.instance().count(), 0) - - QgsProject.instance().removeAllMapLayers() - - @unittest.skipIf(not use_signal_spy, "No QSignalSpy available") - def test_addMapLayerSignals(self): - """ test that signals are correctly emitted when adding map layer""" - - QgsProject.instance().removeAllMapLayers() - - layer_was_added_spy = QSignalSpy(QgsProject.instance().layerWasAdded) - layers_added_spy = QSignalSpy(QgsProject.instance().layersAdded) - legend_layers_added_spy = QSignalSpy(QgsProject.instance().legendLayersAdded) - - l1 = createLayer('test') - QgsProject.instance().addMapLayer(l1) - - # can't seem to actually test the data which was emitted, so best we can do is test - # the signal count - self.assertEqual(len(layer_was_added_spy), 1) - self.assertEqual(len(layers_added_spy), 1) - self.assertEqual(len(legend_layers_added_spy), 1) - - # layer not added to legend - QgsProject.instance().addMapLayer(createLayer('test2'), False) - self.assertEqual(len(layer_was_added_spy), 2) - self.assertEqual(len(layers_added_spy), 2) - self.assertEqual(len(legend_layers_added_spy), 1) - - # try readding a layer already in the registry - QgsProject.instance().addMapLayer(l1) - # should be no extra signals emitted - self.assertEqual(len(layer_was_added_spy), 2) - self.assertEqual(len(layers_added_spy), 2) - self.assertEqual(len(legend_layers_added_spy), 1) - - def test_addMapLayers(self): - """ test adding multiple map layers to registry """ - QgsProject.instance().removeAllMapLayers() - - l1 = createLayer('test') - l2 = createLayer('test2') - self.assertEqual(set(QgsProject.instance().addMapLayers([l1, l2])), set([l1, l2])) - self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) - self.assertEqual(len(QgsProject.instance().mapLayersByName('test2')), 1) - self.assertEqual(QgsProject.instance().count(), 2) - - # adding more layers should leave existing layers intact - l3 = createLayer('test3') - l4 = createLayer('test4') - self.assertEqual(set(QgsProject.instance().addMapLayers([l3, l4])), set([l3, l4])) - self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) - self.assertEqual(len(QgsProject.instance().mapLayersByName('test2')), 1) - self.assertEqual(len(QgsProject.instance().mapLayersByName('test3')), 1) - self.assertEqual(len(QgsProject.instance().mapLayersByName('test4')), 1) - self.assertEqual(QgsProject.instance().count(), 4) - - QgsProject.instance().removeAllMapLayers() - - def test_addMapLayersInvalid(self): - """ test that invalid map layersd can't be added to registry """ - QgsProject.instance().removeAllMapLayers() - - self.assertEqual(QgsProject.instance().addMapLayers([QgsVectorLayer("Point?field=x:string", 'test', "xxx")]), []) - self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 0) - self.assertEqual(QgsProject.instance().count(), 0) - - QgsProject.instance().removeAllMapLayers() - - def test_addMapLayersAlreadyAdded(self): - """ test that already added layers can't be readded to registry """ - QgsProject.instance().removeAllMapLayers() - - l1 = createLayer('test') - self.assertEqual(QgsProject.instance().addMapLayers([l1]), [l1]) - self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) - self.assertEqual(QgsProject.instance().count(), 1) - self.assertEqual(QgsProject.instance().addMapLayers([l1]), []) - self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) - self.assertEqual(QgsProject.instance().count(), 1) - - QgsProject.instance().removeAllMapLayers() - - @unittest.skipIf(not use_signal_spy, "No QSignalSpy available") - def test_addMapLayersSignals(self): - """ test that signals are correctly emitted when adding map layers""" - QgsProject.instance().removeAllMapLayers() - - layer_was_added_spy = QSignalSpy(QgsProject.instance().layerWasAdded) - layers_added_spy = QSignalSpy(QgsProject.instance().layersAdded) - legend_layers_added_spy = QSignalSpy(QgsProject.instance().legendLayersAdded) - - l1 = createLayer('test') - l2 = createLayer('test2') - QgsProject.instance().addMapLayers([l1, l2]) - - # can't seem to actually test the data which was emitted, so best we can do is test - # the signal count - self.assertEqual(len(layer_was_added_spy), 2) - self.assertEqual(len(layers_added_spy), 1) - self.assertEqual(len(legend_layers_added_spy), 1) - - # layer not added to legend - QgsProject.instance().addMapLayers([createLayer('test3'), createLayer('test4')], False) - self.assertEqual(len(layer_was_added_spy), 4) - self.assertEqual(len(layers_added_spy), 2) - self.assertEqual(len(legend_layers_added_spy), 1) - - # try readding a layer already in the registry - QgsProject.instance().addMapLayers([l1, l2]) - # should be no extra signals emitted - self.assertEqual(len(layer_was_added_spy), 4) - self.assertEqual(len(layers_added_spy), 2) - self.assertEqual(len(legend_layers_added_spy), 1) - - def test_mapLayerById(self): - """ test retrieving map layer by ID """ - QgsProject.instance().removeAllMapLayers() - - # test no crash with empty registry - self.assertEqual(QgsProject.instance().mapLayer('bad'), None) - self.assertEqual(QgsProject.instance().mapLayer(None), None) - - l1 = createLayer('test') - l2 = createLayer('test2') - - QgsProject.instance().addMapLayers([l1, l2]) - - self.assertEqual(QgsProject.instance().mapLayer('bad'), None) - self.assertEqual(QgsProject.instance().mapLayer(None), None) - self.assertEqual(QgsProject.instance().mapLayer(l1.id()), l1) - self.assertEqual(QgsProject.instance().mapLayer(l2.id()), l2) - - def test_mapLayersByName(self): - """ test retrieving map layer by name """ - QgsProject.instance().removeAllMapLayers() - - # test no crash with empty registry - self.assertEqual(QgsProject.instance().mapLayersByName('bad'), []) - self.assertEqual(QgsProject.instance().mapLayersByName(None), []) - - l1 = createLayer('test') - l2 = createLayer('test2') - - QgsProject.instance().addMapLayers([l1, l2]) - - self.assertEqual(QgsProject.instance().mapLayersByName('bad'), []) - self.assertEqual(QgsProject.instance().mapLayersByName(None), []) - self.assertEqual(QgsProject.instance().mapLayersByName('test'), [l1]) - self.assertEqual(QgsProject.instance().mapLayersByName('test2'), [l2]) - - #duplicate name - l3 = createLayer('test') - QgsProject.instance().addMapLayer(l3) - self.assertEqual(set(QgsProject.instance().mapLayersByName('test')), set([l1, l3])) - - def test_mapLayers(self): - """ test retrieving map layers list """ - QgsProject.instance().removeAllMapLayers() - - # test no crash with empty registry - self.assertEqual(QgsProject.instance().mapLayers(), {}) - - l1 = createLayer('test') - l2 = createLayer('test2') - - QgsProject.instance().addMapLayers([l1, l2]) - - self.assertEqual(QgsProject.instance().mapLayers(), {l1.id(): l1, l2.id(): l2}) - - def test_removeMapLayersById(self): - """ test removing map layers by ID """ - QgsProject.instance().removeAllMapLayers() - - # test no crash with empty registry - QgsProject.instance().removeMapLayers(['bad']) - QgsProject.instance().removeMapLayers([None]) - - l1 = createLayer('test') - l2 = createLayer('test2') - l3 = createLayer('test3') - - QgsProject.instance().addMapLayers([l1, l2, l3]) - self.assertEqual(QgsProject.instance().count(), 3) - - #remove bad layers - QgsProject.instance().removeMapLayers(['bad']) - self.assertEqual(QgsProject.instance().count(), 3) - QgsProject.instance().removeMapLayers([None]) - self.assertEqual(QgsProject.instance().count(), 3) - - # remove valid layers - l1_id = l1.id() - QgsProject.instance().removeMapLayers([l1_id]) - self.assertEqual(QgsProject.instance().count(), 2) - # double remove - QgsProject.instance().removeMapLayers([l1_id]) - self.assertEqual(QgsProject.instance().count(), 2) - - # test that layer has been deleted - self.assertTrue(sip.isdeleted(l1)) - - # remove multiple - QgsProject.instance().removeMapLayers([l2.id(), l3.id()]) - self.assertEqual(QgsProject.instance().count(), 0) - self.assertTrue(sip.isdeleted(l2)) - - # try removing a layer not in the registry - l4 = createLayer('test4') - QgsProject.instance().removeMapLayers([l4.id()]) - self.assertFalse(sip.isdeleted(l4)) - - # fails on qt5 due to removeMapLayers list type conversion - needs a PyName alias - # added to removeMapLayers for QGIS 3.0 - @unittest.expectedFailure(QT_VERSION_STR[0] == '5') - def test_removeMapLayersByLayer(self): - """ test removing map layers by layer""" - QgsProject.instance().removeAllMapLayers() - - # test no crash with empty registry - QgsProject.instance().removeMapLayers([None]) - - l1 = createLayer('test') - l2 = createLayer('test2') - l3 = createLayer('test3') - - QgsProject.instance().addMapLayers([l1, l2, l3]) - self.assertEqual(QgsProject.instance().count(), 3) - - #remove bad layers - QgsProject.instance().removeMapLayers([None]) - self.assertEqual(QgsProject.instance().count(), 3) - - # remove valid layers - QgsProject.instance().removeMapLayers([l1]) - self.assertEqual(QgsProject.instance().count(), 2) - - # test that layer has been deleted - self.assertTrue(sip.isdeleted(l1)) - - # remove multiple - QgsProject.instance().removeMapLayers([l2, l3]) - self.assertEqual(QgsProject.instance().count(), 0) - self.assertTrue(sip.isdeleted(l2)) - self.assertTrue(sip.isdeleted(l3)) - - def test_removeMapLayerById(self): - """ test removing a map layer by ID """ - QgsProject.instance().removeAllMapLayers() - - # test no crash with empty registry - QgsProject.instance().removeMapLayer('bad') - QgsProject.instance().removeMapLayer(None) - - l1 = createLayer('test') - l2 = createLayer('test2') - - QgsProject.instance().addMapLayers([l1, l2]) - self.assertEqual(QgsProject.instance().count(), 2) - - #remove bad layers - QgsProject.instance().removeMapLayer('bad') - self.assertEqual(QgsProject.instance().count(), 2) - QgsProject.instance().removeMapLayer(None) - self.assertEqual(QgsProject.instance().count(), 2) - - # remove valid layers - l1_id = l1.id() - QgsProject.instance().removeMapLayer(l1_id) - self.assertEqual(QgsProject.instance().count(), 1) - # double remove - QgsProject.instance().removeMapLayer(l1_id) - self.assertEqual(QgsProject.instance().count(), 1) - - # test that layer has been deleted - self.assertTrue(sip.isdeleted(l1)) - - # remove second layer - QgsProject.instance().removeMapLayer(l2.id()) - self.assertEqual(QgsProject.instance().count(), 0) - self.assertTrue(sip.isdeleted(l2)) - - # try removing a layer not in the registry - l3 = createLayer('test3') - QgsProject.instance().removeMapLayer(l3.id()) - self.assertFalse(sip.isdeleted(l3)) - - def test_removeMapLayerByLayer(self): - """ test removing a map layer by layer """ - QgsProject.instance().removeAllMapLayers() - - # test no crash with empty registry - QgsProject.instance().removeMapLayer('bad') - QgsProject.instance().removeMapLayer(None) - - l1 = createLayer('test') - l2 = createLayer('test2') - - QgsProject.instance().addMapLayers([l1, l2]) - self.assertEqual(QgsProject.instance().count(), 2) - - #remove bad layers - QgsProject.instance().removeMapLayer(None) - self.assertEqual(QgsProject.instance().count(), 2) - l3 = createLayer('test3') - QgsProject.instance().removeMapLayer(l3) - self.assertEqual(QgsProject.instance().count(), 2) - - # remove valid layers - QgsProject.instance().removeMapLayer(l1) - self.assertEqual(QgsProject.instance().count(), 1) - - # test that layer has been deleted - self.assertTrue(sip.isdeleted(l1)) - - # remove second layer - QgsProject.instance().removeMapLayer(l2) - self.assertEqual(QgsProject.instance().count(), 0) - self.assertTrue(sip.isdeleted(l2)) - - # try removing a layer not in the registry - l3 = createLayer('test3') - QgsProject.instance().removeMapLayer(l3) - self.assertFalse(sip.isdeleted(l3)) - - def test_removeAllMapLayers(self): - """ test removing all map layers from registry """ - QgsProject.instance().removeAllMapLayers() - l1 = createLayer('test') - l2 = createLayer('test2') - - QgsProject.instance().addMapLayers([l1, l2]) - self.assertEqual(QgsProject.instance().count(), 2) - QgsProject.instance().removeAllMapLayers() - self.assertEqual(QgsProject.instance().count(), 0) - self.assertEqual(QgsProject.instance().mapLayersByName('test'), []) - self.assertEqual(QgsProject.instance().mapLayersByName('test2'), []) - - @unittest.skipIf(not use_signal_spy, "No QSignalSpy available") - def test_addRemoveLayersSignals(self): - """ test that signals are correctly emitted when removing map layers""" - QgsProject.instance().removeAllMapLayers() - - layers_will_be_removed_spy = QSignalSpy(QgsProject.instance().layersWillBeRemoved) - layer_will_be_removed_spy_str = QSignalSpy(QgsProject.instance().layerWillBeRemoved[str]) - layer_will_be_removed_spy_layer = QSignalSpy(QgsProject.instance().layerWillBeRemoved[QgsMapLayer]) - layers_removed_spy = QSignalSpy(QgsProject.instance().layersRemoved) - layer_removed_spy = QSignalSpy(QgsProject.instance().layerRemoved) - remove_all_spy = QSignalSpy(QgsProject.instance().removeAll) - - l1 = createLayer('l1') - l2 = createLayer('l2') - l3 = createLayer('l3') - l4 = createLayer('l4') - QgsProject.instance().addMapLayers([l1, l2, l3, l4]) - - # remove 1 layer - QgsProject.instance().removeMapLayer(l1) - # can't seem to actually test the data which was emitted, so best we can do is test - # the signal count - self.assertEqual(len(layers_will_be_removed_spy), 1) - self.assertEqual(len(layer_will_be_removed_spy_str), 1) - self.assertEqual(len(layer_will_be_removed_spy_layer), 1) - self.assertEqual(len(layers_removed_spy), 1) - self.assertEqual(len(layer_removed_spy), 1) - self.assertEqual(len(remove_all_spy), 0) - self.assertEqual(QgsProject.instance().count(), 3) - - # remove 2 layers at once - QgsProject.instance().removeMapLayers([l2.id(), l3.id()]) - self.assertEqual(len(layers_will_be_removed_spy), 2) - self.assertEqual(len(layer_will_be_removed_spy_str), 3) - self.assertEqual(len(layer_will_be_removed_spy_layer), 3) - self.assertEqual(len(layers_removed_spy), 2) - self.assertEqual(len(layer_removed_spy), 3) - self.assertEqual(len(remove_all_spy), 0) - self.assertEqual(QgsProject.instance().count(), 1) - - # remove all - QgsProject.instance().removeAllMapLayers() - self.assertEqual(len(layers_will_be_removed_spy), 3) - self.assertEqual(len(layer_will_be_removed_spy_str), 4) - self.assertEqual(len(layer_will_be_removed_spy_layer), 4) - self.assertEqual(len(layers_removed_spy), 3) - self.assertEqual(len(layer_removed_spy), 4) - self.assertEqual(len(remove_all_spy), 1) - - #remove some layers which aren't in the registry - QgsProject.instance().removeMapLayers(['asdasd']) - self.assertEqual(len(layers_will_be_removed_spy), 3) - self.assertEqual(len(layer_will_be_removed_spy_str), 4) - self.assertEqual(len(layer_will_be_removed_spy_layer), 4) - self.assertEqual(len(layers_removed_spy), 3) - self.assertEqual(len(layer_removed_spy), 4) - self.assertEqual(len(remove_all_spy), 1) - - l5 = createLayer('test5') - QgsProject.instance().removeMapLayer(l5) - self.assertEqual(len(layers_will_be_removed_spy), 3) - self.assertEqual(len(layer_will_be_removed_spy_str), 4) - self.assertEqual(len(layer_will_be_removed_spy_layer), 4) - self.assertEqual(len(layers_removed_spy), 3) - self.assertEqual(len(layer_removed_spy), 4) - self.assertEqual(len(remove_all_spy), 1) - - def test_RemoveLayerShouldNotSegFault(self): - QgsProject.instance().removeAllMapLayers() - - reg = QgsProject.instance() - # Should not segfault - reg.removeMapLayers(['not_exists']) - reg.removeMapLayer('not_exists2') - - # check also that the removal of an unexistent layer does not insert a null layer - for k, layer in list(reg.mapLayers().items()): - assert(layer is not None) - - def testTakeLayer(self): - # test taking ownership of a layer from the project - l1 = createLayer('l1') - l2 = createLayer('l2') - p = QgsProject() - - # add one layer to project - p.addMapLayer(l1) - self.assertEqual(p.mapLayers(), {l1.id(): l1}) - self.assertEqual(l1.parent(), p) - - # try taking some layers which don't exist in project - self.assertFalse(p.takeMapLayer(None)) - self.assertFalse(p.takeMapLayer(l2)) - # but l2 should still exist.. - self.assertTrue(l2.isValid()) - - # take layer from project - self.assertEqual(p.takeMapLayer(l1), l1) - self.assertFalse(p.mapLayers()) # no layers left - # but l1 should still exist - self.assertTrue(l1.isValid()) - # layer should have no parent now - self.assertFalse(l1.parent()) - - # destroy project - p = None - self.assertTrue(l1.isValid()) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/src/python/test_qgsmaplayerstore.py b/tests/src/python/test_qgsmaplayerstore.py new file mode 100644 index 00000000000..7fa24e14d20 --- /dev/null +++ b/tests/src/python/test_qgsmaplayerstore.py @@ -0,0 +1,495 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsMapLayerStore. + +.. 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__ = 'Nyall Dawson' +__date__ = '2017-05' +__copyright__ = 'Copyright 2017, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +from qgis.core import QgsMapLayerStore, QgsVectorLayer, QgsMapLayer +from qgis.testing import start_app, unittest +from qgis.PyQt.QtCore import QT_VERSION_STR +import sip +from qgis.PyQt.QtTest import QSignalSpy + +start_app() + + +def createLayer(name): + return QgsVectorLayer("Point?field=x:string", name, "memory") + + +class TestQgsMapLayerStore(unittest.TestCase): + + def setUp(self): + pass + + def test_addMapLayer(self): + """ test adding individual map layers to store""" + store = QgsMapLayerStore() + + l1 = createLayer('test') + self.assertEqual(store.addMapLayer(l1), l1) + self.assertEqual(len(store.mapLayersByName('test')), 1) + self.assertEqual(store.count(), 1) + self.assertEqual(len(store), 1) + + # adding a second layer should leave existing layers intact + l2 = createLayer('test2') + self.assertEqual(store.addMapLayer(l2), l2) + self.assertEqual(len(store.mapLayersByName('test')), 1) + self.assertEqual(len(store.mapLayersByName('test2')), 1) + self.assertEqual(store.count(), 2) + self.assertEqual(len(store), 2) + + def test_addMapLayerAlreadyAdded(self): + """ test that already added layers can't be readded to store """ + store = QgsMapLayerStore() + + l1 = createLayer('test') + store.addMapLayer(l1) + self.assertEqual(len(store.mapLayersByName('test')), 1) + self.assertEqual(store.count(), 1) + self.assertEqual(store.addMapLayer(l1), None) + self.assertEqual(len(store.mapLayersByName('test')), 1) + self.assertEqual(store.count(), 1) + self.assertEqual(len(store), 1) + + def test_addMapLayerInvalid(self): + """ test that invalid map layers can't be added to store """ + store = QgsMapLayerStore() + + self.assertEqual(store.addMapLayer(QgsVectorLayer("Point?field=x:string", 'test', "xxx")), None) + self.assertEqual(len(store.mapLayersByName('test')), 0) + self.assertEqual(store.count(), 0) + + def test_addMapLayerSignals(self): + """ test that signals are correctly emitted when adding map layer""" + + store = QgsMapLayerStore() + + layer_was_added_spy = QSignalSpy(store.layerWasAdded) + layers_added_spy = QSignalSpy(store.layersAdded) + + l1 = createLayer('test') + store.addMapLayer(l1) + + # can't seem to actually test the data which was emitted, so best we can do is test + # the signal count + self.assertEqual(len(layer_was_added_spy), 1) + self.assertEqual(len(layers_added_spy), 1) + + store.addMapLayer(createLayer('test2')) + self.assertEqual(len(layer_was_added_spy), 2) + self.assertEqual(len(layers_added_spy), 2) + + # try readding a layer already in the store + store.addMapLayer(l1) + # should be no extra signals emitted + self.assertEqual(len(layer_was_added_spy), 2) + self.assertEqual(len(layers_added_spy), 2) + + def test_addMapLayers(self): + """ test adding multiple map layers to store """ + store = QgsMapLayerStore() + + l1 = createLayer('test') + l2 = createLayer('test2') + self.assertEqual(set(store.addMapLayers([l1, l2])), {l1, l2}) + self.assertEqual(len(store.mapLayersByName('test')), 1) + self.assertEqual(len(store.mapLayersByName('test2')), 1) + self.assertEqual(store.count(), 2) + + # adding more layers should leave existing layers intact + l3 = createLayer('test3') + l4 = createLayer('test4') + self.assertEqual(set(store.addMapLayers([l3, l4])), {l3, l4}) + self.assertEqual(len(store.mapLayersByName('test')), 1) + self.assertEqual(len(store.mapLayersByName('test2')), 1) + self.assertEqual(len(store.mapLayersByName('test3')), 1) + self.assertEqual(len(store.mapLayersByName('test4')), 1) + self.assertEqual(store.count(), 4) + + store.removeAllMapLayers() + + def test_addMapLayersInvalid(self): + """ test that invalid map layersd can't be added to store """ + store = QgsMapLayerStore() + + self.assertEqual(store.addMapLayers([QgsVectorLayer("Point?field=x:string", 'test', "xxx")]), []) + self.assertEqual(len(store.mapLayersByName('test')), 0) + self.assertEqual(store.count(), 0) + + def test_addMapLayersAlreadyAdded(self): + """ test that already added layers can't be readded to store """ + store = QgsMapLayerStore() + + l1 = createLayer('test') + self.assertEqual(store.addMapLayers([l1]), [l1]) + self.assertEqual(len(store.mapLayersByName('test')), 1) + self.assertEqual(store.count(), 1) + self.assertEqual(store.addMapLayers([l1]), []) + self.assertEqual(len(store.mapLayersByName('test')), 1) + self.assertEqual(store.count(), 1) + + def test_addMapLayersSignals(self): + """ test that signals are correctly emitted when adding map layers""" + store = QgsMapLayerStore() + + layer_was_added_spy = QSignalSpy(store.layerWasAdded) + layers_added_spy = QSignalSpy(store.layersAdded) + + l1 = createLayer('test') + l2 = createLayer('test2') + store.addMapLayers([l1, l2]) + + # can't seem to actually test the data which was emitted, so best we can do is test + # the signal count + self.assertEqual(len(layer_was_added_spy), 2) + self.assertEqual(len(layers_added_spy), 1) + + store.addMapLayers([createLayer('test3'), createLayer('test4')]) + self.assertEqual(len(layer_was_added_spy), 4) + self.assertEqual(len(layers_added_spy), 2) + + # try readding a layer already in the store + store.addMapLayers([l1, l2]) + # should be no extra signals emitted + self.assertEqual(len(layer_was_added_spy), 4) + self.assertEqual(len(layers_added_spy), 2) + + def test_mapLayerById(self): + """ test retrieving map layer by ID """ + store = QgsMapLayerStore() + + # test no crash with empty store + self.assertEqual(store.mapLayer('bad'), None) + self.assertEqual(store.mapLayer(None), None) + + l1 = createLayer('test') + l2 = createLayer('test2') + + store.addMapLayers([l1, l2]) + + self.assertEqual(store.mapLayer('bad'), None) + self.assertEqual(store.mapLayer(None), None) + self.assertEqual(store.mapLayer(l1.id()), l1) + self.assertEqual(store.mapLayer(l2.id()), l2) + + def test_mapLayersByName(self): + """ test retrieving map layer by name """ + store = QgsMapLayerStore() + + # test no crash with empty store + self.assertEqual(store.mapLayersByName('bad'), []) + self.assertEqual(store.mapLayersByName(None), []) + + l1 = createLayer('test') + l2 = createLayer('test2') + + store.addMapLayers([l1, l2]) + + self.assertEqual(store.mapLayersByName('bad'), []) + self.assertEqual(store.mapLayersByName(None), []) + self.assertEqual(store.mapLayersByName('test'), [l1]) + self.assertEqual(store.mapLayersByName('test2'), [l2]) + + #duplicate name + l3 = createLayer('test') + store.addMapLayer(l3) + self.assertEqual(set(store.mapLayersByName('test')), {l1, l3}) + + def test_mapLayers(self): + """ test retrieving map layers list """ + store = QgsMapLayerStore() + + # test no crash with empty store + self.assertEqual(store.mapLayers(), {}) + + l1 = createLayer('test') + l2 = createLayer('test2') + + store.addMapLayers([l1, l2]) + + self.assertEqual(store.mapLayers(), {l1.id(): l1, l2.id(): l2}) + + def test_removeMapLayersById(self): + """ test removing map layers by ID """ + store = QgsMapLayerStore() + + # test no crash with empty store + store.removeMapLayersById(['bad']) + store.removeMapLayersById([None]) + + l1 = createLayer('test') + l2 = createLayer('test2') + l3 = createLayer('test3') + + store.addMapLayers([l1, l2, l3]) + self.assertEqual(store.count(), 3) + + #remove bad layers + store.removeMapLayersById(['bad']) + self.assertEqual(store.count(), 3) + store.removeMapLayersById([None]) + self.assertEqual(store.count(), 3) + + # remove valid layers + l1_id = l1.id() + store.removeMapLayersById([l1_id]) + self.assertEqual(store.count(), 2) + # double remove + store.removeMapLayersById([l1_id]) + self.assertEqual(store.count(), 2) + + # test that layer has been deleted + self.assertTrue(sip.isdeleted(l1)) + + # remove multiple + store.removeMapLayersById([l2.id(), l3.id()]) + self.assertEqual(store.count(), 0) + self.assertTrue(sip.isdeleted(l2)) + + # try removing a layer not in the store + l4 = createLayer('test4') + store.removeMapLayersById([l4.id()]) + self.assertFalse(sip.isdeleted(l4)) + + def test_removeMapLayersByLayer(self): + """ test removing map layers by layer""" + store = QgsMapLayerStore() + + # test no crash with empty store + store.removeMapLayers([None]) + + l1 = createLayer('test') + l2 = createLayer('test2') + l3 = createLayer('test3') + + store.addMapLayers([l1, l2, l3]) + self.assertEqual(store.count(), 3) + + #remove bad layers + store.removeMapLayers([None]) + self.assertEqual(store.count(), 3) + + # remove valid layers + store.removeMapLayers([l1]) + self.assertEqual(store.count(), 2) + + # test that layer has been deleted + self.assertTrue(sip.isdeleted(l1)) + + # remove multiple + store.removeMapLayers([l2, l3]) + self.assertEqual(store.count(), 0) + self.assertTrue(sip.isdeleted(l2)) + self.assertTrue(sip.isdeleted(l3)) + + def test_removeMapLayerById(self): + """ test removing a map layer by ID """ + store = QgsMapLayerStore() + + # test no crash with empty store + store.removeMapLayer('bad') + store.removeMapLayer(None) + + l1 = createLayer('test') + l2 = createLayer('test2') + + store.addMapLayers([l1, l2]) + self.assertEqual(store.count(), 2) + + #remove bad layers + store.removeMapLayer('bad') + self.assertEqual(store.count(), 2) + store.removeMapLayer(None) + self.assertEqual(store.count(), 2) + + # remove valid layers + l1_id = l1.id() + store.removeMapLayer(l1_id) + self.assertEqual(store.count(), 1) + # double remove + store.removeMapLayer(l1_id) + self.assertEqual(store.count(), 1) + + # test that layer has been deleted + self.assertTrue(sip.isdeleted(l1)) + + # remove second layer + store.removeMapLayer(l2.id()) + self.assertEqual(store.count(), 0) + self.assertTrue(sip.isdeleted(l2)) + + # try removing a layer not in the store + l3 = createLayer('test3') + store.removeMapLayer(l3.id()) + self.assertFalse(sip.isdeleted(l3)) + + def test_removeMapLayerByLayer(self): + """ test removing a map layer by layer """ + store = QgsMapLayerStore() + + # test no crash with empty store + store.removeMapLayer('bad') + store.removeMapLayer(None) + + l1 = createLayer('test') + l2 = createLayer('test2') + + store.addMapLayers([l1, l2]) + self.assertEqual(store.count(), 2) + + #remove bad layers + store.removeMapLayer(None) + self.assertEqual(store.count(), 2) + l3 = createLayer('test3') + store.removeMapLayer(l3) + self.assertEqual(store.count(), 2) + + # remove valid layers + store.removeMapLayer(l1) + self.assertEqual(store.count(), 1) + + # test that layer has been deleted + self.assertTrue(sip.isdeleted(l1)) + + # remove second layer + store.removeMapLayer(l2) + self.assertEqual(store.count(), 0) + self.assertTrue(sip.isdeleted(l2)) + + # try removing a layer not in the store + l3 = createLayer('test3') + store.removeMapLayer(l3) + self.assertFalse(sip.isdeleted(l3)) + + def test_removeAllMapLayers(self): + """ test removing all map layers from store """ + store = QgsMapLayerStore() + l1 = createLayer('test') + l2 = createLayer('test2') + + store.addMapLayers([l1, l2]) + self.assertEqual(store.count(), 2) + store.removeAllMapLayers() + self.assertEqual(store.count(), 0) + self.assertEqual(store.mapLayersByName('test'), []) + self.assertEqual(store.mapLayersByName('test2'), []) + + def test_addRemoveLayersSignals(self): + """ test that signals are correctly emitted when removing map layers""" + store = QgsMapLayerStore() + + layers_will_be_removed_spy = QSignalSpy(store.layersWillBeRemoved) + layer_will_be_removed_spy_str = QSignalSpy(store.layerWillBeRemoved[str]) + layer_will_be_removed_spy_layer = QSignalSpy(store.layerWillBeRemoved[QgsMapLayer]) + layers_removed_spy = QSignalSpy(store.layersRemoved) + layer_removed_spy = QSignalSpy(store.layerRemoved) + remove_all_spy = QSignalSpy(store.allLayersRemoved) + + l1 = createLayer('l1') + l2 = createLayer('l2') + l3 = createLayer('l3') + l4 = createLayer('l4') + store.addMapLayers([l1, l2, l3, l4]) + + # remove 1 layer + store.removeMapLayer(l1) + # can't seem to actually test the data which was emitted, so best we can do is test + # the signal count + self.assertEqual(len(layers_will_be_removed_spy), 1) + self.assertEqual(len(layer_will_be_removed_spy_str), 1) + self.assertEqual(len(layer_will_be_removed_spy_layer), 1) + self.assertEqual(len(layers_removed_spy), 1) + self.assertEqual(len(layer_removed_spy), 1) + self.assertEqual(len(remove_all_spy), 0) + self.assertEqual(store.count(), 3) + + # remove 2 layers at once + store.removeMapLayersById([l2.id(), l3.id()]) + self.assertEqual(len(layers_will_be_removed_spy), 2) + self.assertEqual(len(layer_will_be_removed_spy_str), 3) + self.assertEqual(len(layer_will_be_removed_spy_layer), 3) + self.assertEqual(len(layers_removed_spy), 2) + self.assertEqual(len(layer_removed_spy), 3) + self.assertEqual(len(remove_all_spy), 0) + self.assertEqual(store.count(), 1) + + # remove all + store.removeAllMapLayers() + self.assertEqual(len(layers_will_be_removed_spy), 3) + self.assertEqual(len(layer_will_be_removed_spy_str), 4) + self.assertEqual(len(layer_will_be_removed_spy_layer), 4) + self.assertEqual(len(layers_removed_spy), 3) + self.assertEqual(len(layer_removed_spy), 4) + self.assertEqual(len(remove_all_spy), 1) + + #remove some layers which aren't in the store + store.removeMapLayersById(['asdasd']) + self.assertEqual(len(layers_will_be_removed_spy), 3) + self.assertEqual(len(layer_will_be_removed_spy_str), 4) + self.assertEqual(len(layer_will_be_removed_spy_layer), 4) + self.assertEqual(len(layers_removed_spy), 3) + self.assertEqual(len(layer_removed_spy), 4) + self.assertEqual(len(remove_all_spy), 1) + + l5 = createLayer('test5') + store.removeMapLayer(l5) + self.assertEqual(len(layers_will_be_removed_spy), 3) + self.assertEqual(len(layer_will_be_removed_spy_str), 4) + self.assertEqual(len(layer_will_be_removed_spy_layer), 4) + self.assertEqual(len(layers_removed_spy), 3) + self.assertEqual(len(layer_removed_spy), 4) + self.assertEqual(len(remove_all_spy), 1) + + def test_RemoveLayerShouldNotSegFault(self): + store = QgsMapLayerStore() + + # Should not segfault + store.removeMapLayersById(['not_exists']) + store.removeMapLayer('not_exists2') + + # check also that the removal of an unexistent layer does not insert a null layer + for k, layer in list(store.mapLayers().items()): + assert(layer is not None) + + def testTakeLayer(self): + # test taking ownership of a layer from the store + l1 = createLayer('l1') + l2 = createLayer('l2') + store = QgsMapLayerStore() + + # add one layer to store + store.addMapLayer(l1) + self.assertEqual(store.mapLayers(), {l1.id(): l1}) + self.assertEqual(l1.parent(), store) + + # try taking some layers which don't exist in store + self.assertFalse(store.takeMapLayer(None)) + self.assertFalse(store.takeMapLayer(l2)) + # but l2 should still exist.. + self.assertTrue(l2.isValid()) + + # take layer from store + self.assertEqual(store.takeMapLayer(l1), l1) + self.assertFalse(store.mapLayers()) # no layers left + # but l1 should still exist + self.assertTrue(l1.isValid()) + # layer should have no parent now + self.assertFalse(l1.parent()) + + # destroy store + store = None + self.assertTrue(l1.isValid()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsproject.py b/tests/src/python/test_qgsproject.py index 26f4fd16a83..b321290d9a3 100644 --- a/tests/src/python/test_qgsproject.py +++ b/tests/src/python/test_qgsproject.py @@ -22,9 +22,15 @@ from qgis.core import (QgsProject, QgsApplication, QgsUnitTypes, QgsCoordinateReferenceSystem, - QgsVectorLayer) + QgsVectorLayer, + QgsMapLayer) from qgis.gui import (QgsLayerTreeMapCanvasBridge, QgsMapCanvas) + +from qgis.PyQt.QtTest import QSignalSpy +from qgis.PyQt.QtCore import QT_VERSION_STR +import sip + from qgis.testing import start_app, unittest from utilities import (unitTestDataPath) @@ -32,6 +38,10 @@ app = start_app() TEST_DATA_DIR = unitTestDataPath() +def createLayer(name): + return QgsVectorLayer("Point?field=x:string", name, "memory") + + class TestQgsProject(unittest.TestCase): def __init__(self, methodName): @@ -181,6 +191,499 @@ class TestQgsProject(unittest.TestCase): expected = ['polys', 'lines'] self.assertEqual(sorted(layers_names), sorted(expected)) + def testInstance(self): + """ test retrieving global instance """ + self.assertTrue(QgsProject.instance()) + + # register a layer to the singleton + QgsProject.instance().addMapLayer(createLayer('test')) + + # check that the same instance is returned + self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) + + QgsProject.instance().removeAllMapLayers() + + def test_addMapLayer(self): + """ test adding individual map layers to registry """ + QgsProject.instance().removeAllMapLayers() + + l1 = createLayer('test') + self.assertEqual(QgsProject.instance().addMapLayer(l1), l1) + self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) + self.assertEqual(QgsProject.instance().count(), 1) + + # adding a second layer should leave existing layers intact + l2 = createLayer('test2') + self.assertEqual(QgsProject.instance().addMapLayer(l2), l2) + self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) + self.assertEqual(len(QgsProject.instance().mapLayersByName('test2')), 1) + self.assertEqual(QgsProject.instance().count(), 2) + + QgsProject.instance().removeAllMapLayers() + + def test_addMapLayerAlreadyAdded(self): + """ test that already added layers can't be readded to registry """ + QgsProject.instance().removeAllMapLayers() + + l1 = createLayer('test') + QgsProject.instance().addMapLayer(l1) + self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) + self.assertEqual(QgsProject.instance().count(), 1) + self.assertEqual(QgsProject.instance().addMapLayer(l1), None) + self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) + self.assertEqual(QgsProject.instance().count(), 1) + + QgsProject.instance().removeAllMapLayers() + + def test_addMapLayerInvalid(self): + """ test that invalid map layersd can't be added to registry """ + QgsProject.instance().removeAllMapLayers() + + self.assertEqual(QgsProject.instance().addMapLayer(QgsVectorLayer("Point?field=x:string", 'test', "xxx")), None) + self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 0) + self.assertEqual(QgsProject.instance().count(), 0) + + QgsProject.instance().removeAllMapLayers() + + def test_addMapLayerSignals(self): + """ test that signals are correctly emitted when adding map layer""" + + QgsProject.instance().removeAllMapLayers() + + layer_was_added_spy = QSignalSpy(QgsProject.instance().layerWasAdded) + layers_added_spy = QSignalSpy(QgsProject.instance().layersAdded) + legend_layers_added_spy = QSignalSpy(QgsProject.instance().legendLayersAdded) + + l1 = createLayer('test') + QgsProject.instance().addMapLayer(l1) + + # can't seem to actually test the data which was emitted, so best we can do is test + # the signal count + self.assertEqual(len(layer_was_added_spy), 1) + self.assertEqual(len(layers_added_spy), 1) + self.assertEqual(len(legend_layers_added_spy), 1) + + # layer not added to legend + QgsProject.instance().addMapLayer(createLayer('test2'), False) + self.assertEqual(len(layer_was_added_spy), 2) + self.assertEqual(len(layers_added_spy), 2) + self.assertEqual(len(legend_layers_added_spy), 1) + + # try readding a layer already in the registry + QgsProject.instance().addMapLayer(l1) + # should be no extra signals emitted + self.assertEqual(len(layer_was_added_spy), 2) + self.assertEqual(len(layers_added_spy), 2) + self.assertEqual(len(legend_layers_added_spy), 1) + + def test_addMapLayers(self): + """ test adding multiple map layers to registry """ + QgsProject.instance().removeAllMapLayers() + + l1 = createLayer('test') + l2 = createLayer('test2') + self.assertEqual(set(QgsProject.instance().addMapLayers([l1, l2])), set([l1, l2])) + self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) + self.assertEqual(len(QgsProject.instance().mapLayersByName('test2')), 1) + self.assertEqual(QgsProject.instance().count(), 2) + + # adding more layers should leave existing layers intact + l3 = createLayer('test3') + l4 = createLayer('test4') + self.assertEqual(set(QgsProject.instance().addMapLayers([l3, l4])), set([l3, l4])) + self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) + self.assertEqual(len(QgsProject.instance().mapLayersByName('test2')), 1) + self.assertEqual(len(QgsProject.instance().mapLayersByName('test3')), 1) + self.assertEqual(len(QgsProject.instance().mapLayersByName('test4')), 1) + self.assertEqual(QgsProject.instance().count(), 4) + + QgsProject.instance().removeAllMapLayers() + + def test_addMapLayersInvalid(self): + """ test that invalid map layersd can't be added to registry """ + QgsProject.instance().removeAllMapLayers() + + self.assertEqual(QgsProject.instance().addMapLayers([QgsVectorLayer("Point?field=x:string", 'test', "xxx")]), []) + self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 0) + self.assertEqual(QgsProject.instance().count(), 0) + + QgsProject.instance().removeAllMapLayers() + + def test_addMapLayersAlreadyAdded(self): + """ test that already added layers can't be readded to registry """ + QgsProject.instance().removeAllMapLayers() + + l1 = createLayer('test') + self.assertEqual(QgsProject.instance().addMapLayers([l1]), [l1]) + self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) + self.assertEqual(QgsProject.instance().count(), 1) + self.assertEqual(QgsProject.instance().addMapLayers([l1]), []) + self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) + self.assertEqual(QgsProject.instance().count(), 1) + + QgsProject.instance().removeAllMapLayers() + + def test_addMapLayersSignals(self): + """ test that signals are correctly emitted when adding map layers""" + QgsProject.instance().removeAllMapLayers() + + layer_was_added_spy = QSignalSpy(QgsProject.instance().layerWasAdded) + layers_added_spy = QSignalSpy(QgsProject.instance().layersAdded) + legend_layers_added_spy = QSignalSpy(QgsProject.instance().legendLayersAdded) + + l1 = createLayer('test') + l2 = createLayer('test2') + QgsProject.instance().addMapLayers([l1, l2]) + + # can't seem to actually test the data which was emitted, so best we can do is test + # the signal count + self.assertEqual(len(layer_was_added_spy), 2) + self.assertEqual(len(layers_added_spy), 1) + self.assertEqual(len(legend_layers_added_spy), 1) + + # layer not added to legend + QgsProject.instance().addMapLayers([createLayer('test3'), createLayer('test4')], False) + self.assertEqual(len(layer_was_added_spy), 4) + self.assertEqual(len(layers_added_spy), 2) + self.assertEqual(len(legend_layers_added_spy), 1) + + # try readding a layer already in the registry + QgsProject.instance().addMapLayers([l1, l2]) + # should be no extra signals emitted + self.assertEqual(len(layer_was_added_spy), 4) + self.assertEqual(len(layers_added_spy), 2) + self.assertEqual(len(legend_layers_added_spy), 1) + + def test_mapLayerById(self): + """ test retrieving map layer by ID """ + QgsProject.instance().removeAllMapLayers() + + # test no crash with empty registry + self.assertEqual(QgsProject.instance().mapLayer('bad'), None) + self.assertEqual(QgsProject.instance().mapLayer(None), None) + + l1 = createLayer('test') + l2 = createLayer('test2') + + QgsProject.instance().addMapLayers([l1, l2]) + + self.assertEqual(QgsProject.instance().mapLayer('bad'), None) + self.assertEqual(QgsProject.instance().mapLayer(None), None) + self.assertEqual(QgsProject.instance().mapLayer(l1.id()), l1) + self.assertEqual(QgsProject.instance().mapLayer(l2.id()), l2) + + def test_mapLayersByName(self): + """ test retrieving map layer by name """ + p = QgsProject() + + # test no crash with empty registry + self.assertEqual(p.mapLayersByName('bad'), []) + self.assertEqual(p.mapLayersByName(None), []) + + l1 = createLayer('test') + l2 = createLayer('test2') + + p.addMapLayers([l1, l2]) + + self.assertEqual(p.mapLayersByName('bad'), []) + self.assertEqual(p.mapLayersByName(None), []) + self.assertEqual(p.mapLayersByName('test'), [l1]) + self.assertEqual(p.mapLayersByName('test2'), [l2]) + + #duplicate name + l3 = createLayer('test') + p.addMapLayer(l3) + self.assertEqual(set(p.mapLayersByName('test')), set([l1, l3])) + + def test_mapLayers(self): + """ test retrieving map layers list """ + QgsProject.instance().removeAllMapLayers() + + # test no crash with empty registry + self.assertEqual(QgsProject.instance().mapLayers(), {}) + + l1 = createLayer('test') + l2 = createLayer('test2') + + QgsProject.instance().addMapLayers([l1, l2]) + + self.assertEqual(QgsProject.instance().mapLayers(), {l1.id(): l1, l2.id(): l2}) + + def test_removeMapLayersById(self): + """ test removing map layers by ID """ + QgsProject.instance().removeAllMapLayers() + + # test no crash with empty registry + QgsProject.instance().removeMapLayers(['bad']) + QgsProject.instance().removeMapLayers([None]) + + l1 = createLayer('test') + l2 = createLayer('test2') + l3 = createLayer('test3') + + QgsProject.instance().addMapLayers([l1, l2, l3]) + self.assertEqual(QgsProject.instance().count(), 3) + + #remove bad layers + QgsProject.instance().removeMapLayers(['bad']) + self.assertEqual(QgsProject.instance().count(), 3) + QgsProject.instance().removeMapLayers([None]) + self.assertEqual(QgsProject.instance().count(), 3) + + # remove valid layers + l1_id = l1.id() + QgsProject.instance().removeMapLayers([l1_id]) + self.assertEqual(QgsProject.instance().count(), 2) + # double remove + QgsProject.instance().removeMapLayers([l1_id]) + self.assertEqual(QgsProject.instance().count(), 2) + + # test that layer has been deleted + self.assertTrue(sip.isdeleted(l1)) + + # remove multiple + QgsProject.instance().removeMapLayers([l2.id(), l3.id()]) + self.assertEqual(QgsProject.instance().count(), 0) + self.assertTrue(sip.isdeleted(l2)) + + # try removing a layer not in the registry + l4 = createLayer('test4') + QgsProject.instance().removeMapLayers([l4.id()]) + self.assertFalse(sip.isdeleted(l4)) + + # fails on qt5 due to removeMapLayers list type conversion - needs a PyName alias + # added to removeMapLayers for QGIS 3.0 + @unittest.expectedFailure(QT_VERSION_STR[0] == '5') + def test_removeMapLayersByLayer(self): + """ test removing map layers by layer""" + QgsProject.instance().removeAllMapLayers() + + # test no crash with empty registry + QgsProject.instance().removeMapLayers([None]) + + l1 = createLayer('test') + l2 = createLayer('test2') + l3 = createLayer('test3') + + QgsProject.instance().addMapLayers([l1, l2, l3]) + self.assertEqual(QgsProject.instance().count(), 3) + + #remove bad layers + QgsProject.instance().removeMapLayers([None]) + self.assertEqual(QgsProject.instance().count(), 3) + + # remove valid layers + QgsProject.instance().removeMapLayers([l1]) + self.assertEqual(QgsProject.instance().count(), 2) + + # test that layer has been deleted + self.assertTrue(sip.isdeleted(l1)) + + # remove multiple + QgsProject.instance().removeMapLayers([l2, l3]) + self.assertEqual(QgsProject.instance().count(), 0) + self.assertTrue(sip.isdeleted(l2)) + self.assertTrue(sip.isdeleted(l3)) + + def test_removeMapLayerById(self): + """ test removing a map layer by ID """ + QgsProject.instance().removeAllMapLayers() + + # test no crash with empty registry + QgsProject.instance().removeMapLayer('bad') + QgsProject.instance().removeMapLayer(None) + + l1 = createLayer('test') + l2 = createLayer('test2') + + QgsProject.instance().addMapLayers([l1, l2]) + self.assertEqual(QgsProject.instance().count(), 2) + + #remove bad layers + QgsProject.instance().removeMapLayer('bad') + self.assertEqual(QgsProject.instance().count(), 2) + QgsProject.instance().removeMapLayer(None) + self.assertEqual(QgsProject.instance().count(), 2) + + # remove valid layers + l1_id = l1.id() + QgsProject.instance().removeMapLayer(l1_id) + self.assertEqual(QgsProject.instance().count(), 1) + # double remove + QgsProject.instance().removeMapLayer(l1_id) + self.assertEqual(QgsProject.instance().count(), 1) + + # test that layer has been deleted + self.assertTrue(sip.isdeleted(l1)) + + # remove second layer + QgsProject.instance().removeMapLayer(l2.id()) + self.assertEqual(QgsProject.instance().count(), 0) + self.assertTrue(sip.isdeleted(l2)) + + # try removing a layer not in the registry + l3 = createLayer('test3') + QgsProject.instance().removeMapLayer(l3.id()) + self.assertFalse(sip.isdeleted(l3)) + + def test_removeMapLayerByLayer(self): + """ test removing a map layer by layer """ + QgsProject.instance().removeAllMapLayers() + + # test no crash with empty registry + QgsProject.instance().removeMapLayer('bad') + QgsProject.instance().removeMapLayer(None) + + l1 = createLayer('test') + l2 = createLayer('test2') + + QgsProject.instance().addMapLayers([l1, l2]) + self.assertEqual(QgsProject.instance().count(), 2) + + #remove bad layers + QgsProject.instance().removeMapLayer(None) + self.assertEqual(QgsProject.instance().count(), 2) + l3 = createLayer('test3') + QgsProject.instance().removeMapLayer(l3) + self.assertEqual(QgsProject.instance().count(), 2) + + # remove valid layers + QgsProject.instance().removeMapLayer(l1) + self.assertEqual(QgsProject.instance().count(), 1) + + # test that layer has been deleted + self.assertTrue(sip.isdeleted(l1)) + + # remove second layer + QgsProject.instance().removeMapLayer(l2) + self.assertEqual(QgsProject.instance().count(), 0) + self.assertTrue(sip.isdeleted(l2)) + + # try removing a layer not in the registry + l3 = createLayer('test3') + QgsProject.instance().removeMapLayer(l3) + self.assertFalse(sip.isdeleted(l3)) + + def test_removeAllMapLayers(self): + """ test removing all map layers from registry """ + QgsProject.instance().removeAllMapLayers() + l1 = createLayer('test') + l2 = createLayer('test2') + + QgsProject.instance().addMapLayers([l1, l2]) + self.assertEqual(QgsProject.instance().count(), 2) + QgsProject.instance().removeAllMapLayers() + self.assertEqual(QgsProject.instance().count(), 0) + self.assertEqual(QgsProject.instance().mapLayersByName('test'), []) + self.assertEqual(QgsProject.instance().mapLayersByName('test2'), []) + + def test_addRemoveLayersSignals(self): + """ test that signals are correctly emitted when removing map layers""" + QgsProject.instance().removeAllMapLayers() + + layers_will_be_removed_spy = QSignalSpy(QgsProject.instance().layersWillBeRemoved) + layer_will_be_removed_spy_str = QSignalSpy(QgsProject.instance().layerWillBeRemoved[str]) + layer_will_be_removed_spy_layer = QSignalSpy(QgsProject.instance().layerWillBeRemoved[QgsMapLayer]) + layers_removed_spy = QSignalSpy(QgsProject.instance().layersRemoved) + layer_removed_spy = QSignalSpy(QgsProject.instance().layerRemoved) + remove_all_spy = QSignalSpy(QgsProject.instance().removeAll) + + l1 = createLayer('l1') + l2 = createLayer('l2') + l3 = createLayer('l3') + l4 = createLayer('l4') + QgsProject.instance().addMapLayers([l1, l2, l3, l4]) + + # remove 1 layer + QgsProject.instance().removeMapLayer(l1) + # can't seem to actually test the data which was emitted, so best we can do is test + # the signal count + self.assertEqual(len(layers_will_be_removed_spy), 1) + self.assertEqual(len(layer_will_be_removed_spy_str), 1) + self.assertEqual(len(layer_will_be_removed_spy_layer), 1) + self.assertEqual(len(layers_removed_spy), 1) + self.assertEqual(len(layer_removed_spy), 1) + self.assertEqual(len(remove_all_spy), 0) + self.assertEqual(QgsProject.instance().count(), 3) + + # remove 2 layers at once + QgsProject.instance().removeMapLayers([l2.id(), l3.id()]) + self.assertEqual(len(layers_will_be_removed_spy), 2) + self.assertEqual(len(layer_will_be_removed_spy_str), 3) + self.assertEqual(len(layer_will_be_removed_spy_layer), 3) + self.assertEqual(len(layers_removed_spy), 2) + self.assertEqual(len(layer_removed_spy), 3) + self.assertEqual(len(remove_all_spy), 0) + self.assertEqual(QgsProject.instance().count(), 1) + + # remove all + QgsProject.instance().removeAllMapLayers() + self.assertEqual(len(layers_will_be_removed_spy), 3) + self.assertEqual(len(layer_will_be_removed_spy_str), 4) + self.assertEqual(len(layer_will_be_removed_spy_layer), 4) + self.assertEqual(len(layers_removed_spy), 3) + self.assertEqual(len(layer_removed_spy), 4) + self.assertEqual(len(remove_all_spy), 1) + + #remove some layers which aren't in the registry + QgsProject.instance().removeMapLayers(['asdasd']) + self.assertEqual(len(layers_will_be_removed_spy), 3) + self.assertEqual(len(layer_will_be_removed_spy_str), 4) + self.assertEqual(len(layer_will_be_removed_spy_layer), 4) + self.assertEqual(len(layers_removed_spy), 3) + self.assertEqual(len(layer_removed_spy), 4) + self.assertEqual(len(remove_all_spy), 1) + + l5 = createLayer('test5') + QgsProject.instance().removeMapLayer(l5) + self.assertEqual(len(layers_will_be_removed_spy), 3) + self.assertEqual(len(layer_will_be_removed_spy_str), 4) + self.assertEqual(len(layer_will_be_removed_spy_layer), 4) + self.assertEqual(len(layers_removed_spy), 3) + self.assertEqual(len(layer_removed_spy), 4) + self.assertEqual(len(remove_all_spy), 1) + + def test_RemoveLayerShouldNotSegFault(self): + QgsProject.instance().removeAllMapLayers() + + reg = QgsProject.instance() + # Should not segfault + reg.removeMapLayers(['not_exists']) + reg.removeMapLayer('not_exists2') + + # check also that the removal of an unexistent layer does not insert a null layer + for k, layer in list(reg.mapLayers().items()): + assert(layer is not None) + + def testTakeLayer(self): + # test taking ownership of a layer from the project + l1 = createLayer('l1') + l2 = createLayer('l2') + p = QgsProject() + + # add one layer to project + p.addMapLayer(l1) + self.assertEqual(p.mapLayers(), {l1.id(): l1}) + self.assertEqual(l1.parent().parent(), p) + + # try taking some layers which don't exist in project + self.assertFalse(p.takeMapLayer(None)) + self.assertFalse(p.takeMapLayer(l2)) + # but l2 should still exist.. + self.assertTrue(l2.isValid()) + + # take layer from project + self.assertEqual(p.takeMapLayer(l1), l1) + self.assertFalse(p.mapLayers()) # no layers left + # but l1 should still exist + self.assertTrue(l1.isValid()) + # layer should have no parent now + self.assertFalse(l1.parent()) + + # destroy project + p = None + self.assertTrue(l1.isValid()) + if __name__ == '__main__': unittest.main()