diff --git a/python/core/auto_generated/layertree/qgslayertreeutils.sip.in b/python/core/auto_generated/layertree/qgslayertreeutils.sip.in index bb2aeff2fb9..544ccf17e81 100644 --- a/python/core/auto_generated/layertree/qgslayertreeutils.sip.in +++ b/python/core/auto_generated/layertree/qgslayertreeutils.sip.in @@ -56,7 +56,14 @@ Returns true if any of the layers is modified static void removeInvalidLayers( QgsLayerTreeGroup *group ); %Docstring -Remove layer nodes that refer to invalid layers +Removes layer nodes that refer to invalid layers +%End + + static void storeOriginalLayersProperties( QgsLayerTreeGroup *group, const QDomDocument *doc ); +%Docstring +Stores in a layer's originalXmlProperties the layer properties information + +.. versionadded:: 3.6 %End static void replaceChildrenOfEmbeddedGroups( QgsLayerTreeGroup *group ); diff --git a/python/core/auto_generated/qgsmaplayer.sip.in b/python/core/auto_generated/qgsmaplayer.sip.in index e7c85b9b22f..e8ebda602da 100644 --- a/python/core/auto_generated/qgsmaplayer.sip.in +++ b/python/core/auto_generated/qgsmaplayer.sip.in @@ -180,7 +180,7 @@ Returns the display name of the layer. virtual QgsDataProvider *dataProvider(); %Docstring -Returns the layer's data provider. +Returns the layer's data provider, it may be null. %End @@ -966,6 +966,31 @@ Write just the symbology information for the layer into the document .. versionadded:: 2.16 %End + + virtual void setDataSource( const QString &dataSource, const QString &baseName, const QString &provider, const QgsDataProvider::ProviderOptions &options, bool loadDefaultStyleFlag = false ); +%Docstring +Updates the data source of the layer. The layer's renderer and legend will be preserved only +if the geometry type of the new data source matches the current geometry type of the layer. + +Subclasses should override this method: default implementation does nothing. + +:param dataSource: new layer data source +:param baseName: base name of the layer +:param provider: provider string +:param options: provider options +:param loadDefaultStyleFlag: set to true to reset the layer's style to the default for the + data source + +.. seealso:: :py:func:`dataSourceChanged` + +.. versionadded:: 3.6 +%End + + QString providerType() const; +%Docstring +Returns the provider type (provider key) for this layer +%End + QUndoStack *undoStack(); %Docstring Returns pointer to layer's undo stack @@ -1210,6 +1235,25 @@ Returns the message that should be notified by the provider to triggerRepaint Returns true if the refresh on provider nofification is enabled .. versionadded:: 3.0 +%End + + QString originalXmlProperties() const; +%Docstring +Returns the XML properties of the original layer as they were when the layer +was first read from the project file. In case of new layers this is normally empty. + +The storage format for the XML is qlr + +.. versionadded:: 3.6 +%End + + void setOriginalXmlProperties( const QString &originalXmlProperties ); +%Docstring +Sets the original XML properties for the layer to ``originalXmlProperties`` + +The storage format for the XML is qlr + +.. versionadded:: 3.6 %End public slots: @@ -1432,6 +1476,15 @@ Emitted when layer's flags have been modified. .. seealso:: :py:func:`flags` .. versionadded:: 3.4 +%End + + void dataSourceChanged(); +%Docstring +Emitted whenever the layer's data source has been changed. + +.. seealso:: :py:func:`setDataSource` + +.. versionadded:: 3.5 %End protected: @@ -1535,6 +1588,11 @@ Read style data common to all layer types .. versionadded:: 3.0 %End + void setProviderType( const QString &providerType ); +%Docstring +Sets the ``providerType`` (provider key) +%End + void appendError( const QgsErrorMessage &error ); %Docstring @@ -1563,6 +1621,8 @@ Checks whether a new set of dependencies will introduce a cycle %End + + }; QFlags operator|(QgsMapLayer::LayerFlag f1, QFlags f2); diff --git a/python/core/auto_generated/qgsmaplayerstore.sip.in b/python/core/auto_generated/qgsmaplayerstore.sip.in index b824da693ca..2fd179c674c 100644 --- a/python/core/auto_generated/qgsmaplayerstore.sip.in +++ b/python/core/auto_generated/qgsmaplayerstore.sip.in @@ -37,6 +37,13 @@ Constructor for QgsMapLayerStore. Returns the number of layers contained in the store. %End + int validCount() const; +%Docstring +Returns the number of valid layers contained in the store. + +.. versionadded:: 3.6 +%End + int __len__() const; %Docstring @@ -89,6 +96,19 @@ Returns a map of all layers by layer ID. .. seealso:: :py:func:`layers` %End + QMap validMapLayers() const; +%Docstring +Returns a map of all valid layers by layer ID. + +.. seealso:: :py:func:`mapLayer` + +.. seealso:: :py:func:`mapLayersByName` + +.. seealso:: :py:func:`layers` + +.. versionadded:: 3.6 +%End + QList addMapLayers( const QList &layers /Transfer/); @@ -104,7 +124,7 @@ The layersAdded() and layerWasAdded() signals will always be emitted. 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, + successfully. If a layer already exists in the store, it will not be part of the returned list. diff --git a/python/core/auto_generated/qgsproject.sip.in b/python/core/auto_generated/qgsproject.sip.in index 646a9611412..d5dc765bc07 100644 --- a/python/core/auto_generated/qgsproject.sip.in +++ b/python/core/auto_generated/qgsproject.sip.in @@ -700,6 +700,11 @@ Returns a pointer to the project's internal layer store. int count() const; %Docstring Returns the number of registered layers. +%End + + int validCount() const; +%Docstring +Returns the number of registered valid layers. %End QgsMapLayer *mapLayer( const QString &layerId ) const; @@ -728,10 +733,12 @@ Retrieve a list of matching registered layers by layer name. .. seealso:: :py:func:`mapLayers` %End - QMap mapLayers() const; + QMap mapLayers( const bool validOnly = false ) const; %Docstring Returns a map of all registered layers by layer ID. +:param validOnly: if set only valid layers will be returned + .. seealso:: :py:func:`mapLayer` .. seealso:: :py:func:`mapLayersByName` @@ -763,7 +770,7 @@ The legendLayersAdded() signal is emitted only if addToLegend is true. 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 registry, + successfully. If a layer or already exists in the registry, it will not be part of the returned QList. diff --git a/python/core/auto_generated/qgsrelation.sip.in b/python/core/auto_generated/qgsrelation.sip.in index a9e0fbf2189..5f68c5e623c 100644 --- a/python/core/auto_generated/qgsrelation.sip.in +++ b/python/core/auto_generated/qgsrelation.sip.in @@ -273,6 +273,7 @@ Returns a list of attributes used to form the referencing fields bool isValid() const; %Docstring Returns the validity of this relation. Don't use the information if it's not valid. +A relation is considered valid if both referenced and referencig layers are valid. :return: true if the relation is valid %End @@ -300,6 +301,14 @@ Gets the referenced field counterpart given a referencing field. Gets the referencing field counterpart given a referenced field. .. versionadded:: 3.0 +%End + + void updateRelationStatus(); +%Docstring +Updates the validity status of this relation. +Will be called internally whenever a member is changed. + +.. versionadded:: 3.6 %End }; diff --git a/python/core/auto_generated/qgsrelationmanager.sip.in b/python/core/auto_generated/qgsrelationmanager.sip.in index bcef6b4038b..3283d6ff27b 100644 --- a/python/core/auto_generated/qgsrelationmanager.sip.in +++ b/python/core/auto_generated/qgsrelationmanager.sip.in @@ -45,6 +45,8 @@ Gets access to the relations managed by this class. void addRelation( const QgsRelation &relation ); %Docstring Add a relation. +Invalid relations are added only if both referencing layer and referenced +layer exist. :param relation: The relation to add. %End @@ -134,6 +136,13 @@ This signal is emitted when the relations were loaded after reading a project Emitted when relations are added or removed to the manager. .. versionadded:: 2.5 +%End + + public slots: + + void updateRelationsStatus(); +%Docstring +Updates relations status %End }; diff --git a/python/core/auto_generated/qgsvectorlayer.sip.in b/python/core/auto_generated/qgsvectorlayer.sip.in index 9aca4f2f2f8..0cc3406ae14 100644 --- a/python/core/auto_generated/qgsvectorlayer.sip.in +++ b/python/core/auto_generated/qgsvectorlayer.sip.in @@ -736,11 +736,6 @@ Returns point, line or polygon %Docstring Returns the WKBType or WKBUnknown in case of error -%End - - QString providerType() const; -%Docstring -Returns the provider type for this layer %End virtual QgsCoordinateReferenceSystem sourceCrs() const ${SIP_FINAL}; @@ -988,7 +983,8 @@ if the geometry type of the new data source matches the current geometry type of .. deprecated:: Use version with ProviderOptions argument instead %End - void setDataSource( const QString &dataSource, const QString &baseName, const QString &provider, const QgsDataProvider::ProviderOptions &options, bool loadDefaultStyleFlag = false ); + virtual void setDataSource( const QString &dataSource, const QString &baseName, const QString &provider, const QgsDataProvider::ProviderOptions &options, bool loadDefaultStyleFlag = false ); + %Docstring Updates the data source of the layer. The layer's renderer and legend will be preserved only if the geometry type of the new data source matches the current geometry type of the layer. @@ -2326,15 +2322,6 @@ by the backend data provider). signals: - void dataSourceChanged(); -%Docstring -Emitted whenever the layer's data source has been changed. - -.. seealso:: :py:func:`setDataSource` - -.. versionadded:: 3.4 -%End - void selectionChanged( const QgsFeatureIds &selected, const QgsFeatureIds &deselected, bool clearAndSelect ); %Docstring This signal is emitted when selection was changed diff --git a/python/core/auto_generated/raster/qgsrasterlayer.sip.in b/python/core/auto_generated/raster/qgsrasterlayer.sip.in index f62f9a742a9..1d1e576e142 100644 --- a/python/core/auto_generated/raster/qgsrasterlayer.sip.in +++ b/python/core/auto_generated/raster/qgsrasterlayer.sip.in @@ -114,7 +114,7 @@ Constructor for LayerOptions. explicit QgsRasterLayer( const QString &uri, const QString &baseName = QString(), - const QString &providerKey = "gdal", + const QString &providerType = "gdal", const QgsRasterLayer::LayerOptions &options = QgsRasterLayer::LayerOptions() ); %Docstring This is the constructor for the RasterLayer class. @@ -192,6 +192,24 @@ Set the data provider. :param options: provider options .. versionadded:: 3.2 +%End + + virtual void setDataSource( const QString &dataSource, const QString &baseName, const QString &provider, const QgsDataProvider::ProviderOptions &options, bool loadDefaultStyleFlag = false ); + +%Docstring +Updates the data source of the layer. The layer's renderer and legend will be preserved only +if the geometry type of the new data source matches the current geometry type of the layer. + +:param dataSource: new layer data source +:param baseName: base name of the layer +:param provider: provider string +:param options: provider options +:param loadDefaultStyleFlag: set to true to reset the layer's style to the default for the + data source + +.. seealso:: :py:func:`dataSourceChanged` + +.. versionadded:: 3.6 %End LayerType rasterType(); diff --git a/python/gui/auto_generated/qgsdatasourceselectdialog.sip.in b/python/gui/auto_generated/qgsdatasourceselectdialog.sip.in index efe3b2a77a7..b478fa2d421 100644 --- a/python/gui/auto_generated/qgsdatasourceselectdialog.sip.in +++ b/python/gui/auto_generated/qgsdatasourceselectdialog.sip.in @@ -31,17 +31,22 @@ will return a (possibly invalid) QgsMimeDataUtils.Uri. %End public: - QgsDataSourceSelectDialog( bool setFilterByLayerType = false, + QgsDataSourceSelectDialog( QgsBrowserModel *browserModel = 0, + bool setFilterByLayerType = false, const QgsMapLayer::LayerType &layerType = QgsMapLayer::LayerType::VectorLayer, QWidget *parent = 0 ); %Docstring Constructs a QgsDataSourceSelectDialog, optionally filtering by layer type +:param browserModel: an existing browser model (typically from app), if null an instance will be created :param setFilterByLayerType: activates filtering by layer type :param layerType: sets the layer type filter, this is in effect only if filtering by layer type is also active :param parent: the object %End + + ~QgsDataSourceSelectDialog(); + void setLayerTypeFilter( QgsMapLayer::LayerType layerType ); %Docstring Sets layer type filter to ``layerType`` and activates the filtering diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 6803d32a05a..485cf52dcd4 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -101,6 +101,7 @@ #include "qgsgui.h" #include "qgsnative.h" +#include "qgsdatasourceselectdialog.h" #ifdef HAVE_OPENCL #include "qgsopenclutils.h" @@ -3941,7 +3942,8 @@ void QgisApp::initLayerTreeView() new QgsLayerTreeViewFilterIndicatorProvider( mLayerTreeView ); // gets parented to the layer view new QgsLayerTreeViewEmbeddedIndicatorProvider( mLayerTreeView ); // gets parented to the layer view new QgsLayerTreeViewMemoryIndicatorProvider( mLayerTreeView ); // gets parented to the layer view - new QgsLayerTreeViewBadLayerIndicatorProvider( mLayerTreeView ); // gets parented to the layer view + QgsLayerTreeViewBadLayerIndicatorProvider *badLayerIndicatorProvider = new QgsLayerTreeViewBadLayerIndicatorProvider( mLayerTreeView ); // gets parented to the layer view + connect( badLayerIndicatorProvider, &QgsLayerTreeViewBadLayerIndicatorProvider::requestChangeDataSource, this, &QgisApp::changeDataSource ); new QgsLayerTreeViewNonRemovableIndicatorProvider( mLayerTreeView ); // gets parented to the layer view setupLayerTreeViewFromSettings(); @@ -6935,6 +6937,62 @@ void QgisApp::refreshFeatureActions() updateDefaultFeatureAction( mFeatureActionMenu->activeAction() ); } +void QgisApp::changeDataSource( QgsMapLayer *layer ) +{ + // Get provider type + QString providerType( layer->providerType() ); + QgsMapLayer::LayerType layerType( layer->type() ); + + QgsDataSourceSelectDialog dlg( mBrowserModel, true, layerType ); + + if ( dlg.exec() == QDialog::Accepted ) + { + QgsMimeDataUtils::Uri uri( dlg.uri() ); + if ( uri.isValid() ) + { + bool layerIsValid( layer->isValid() ); + layer->setDataSource( uri.uri, layer->name(), uri.providerKey, QgsDataProvider::ProviderOptions() ); + // Re-apply style + if ( !( layerIsValid || layer->originalXmlProperties().isEmpty() ) ) + { + QgsReadWriteContext context; + context.setPathResolver( QgsProject::instance()->pathResolver() ); + context.setProjectTranslator( QgsProject::instance() ); + QString errorMsg; + QDomDocument doc; + if ( doc.setContent( layer->originalXmlProperties() ) ) + { + QDomNode layer_node( doc.firstChild( ) ); + if ( ! layer->readSymbology( layer_node, errorMsg, context ) ) + { + QgsDebugMsg( QStringLiteral( "Failed to restore original layer style from stored XML for layer %1: %2" ) + .arg( layer->name( ) ) + .arg( errorMsg ) ); + } + } + else + { + QgsDebugMsg( QStringLiteral( "Failed to create XML QDomDocument for layer %1: %2" ) + .arg( layer->name( ) ) + .arg( errorMsg ) ); + } + } + + // All the following code is necessary to refresh the layer + QgsLayerTreeModel *model = qobject_cast( mLayerTreeView->model() ); + if ( model ) + { + QgsLayerTreeLayer *tl( model->rootGroup()->findLayer( layer->id() ) ); + if ( tl && tl->itemVisibilityChecked() ) + { + tl->setItemVisibilityChecked( false ); + tl->setItemVisibilityChecked( true ); + } + } + } + } +} + void QgisApp::measure() { mMapCanvas->setMapTool( mMapTools.mMeasureDist ); diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index d27404cd906..575fa87eae8 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -942,6 +942,16 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow */ QgsBrowserModel *browserModel(); + /* + * Change data source for \a layer, a data source selection dialog + * will be opened and if accepted the data selected source will be + * applied. + * + * In case the layer was originally invalid and it had the original + * XML layer properties, the properties will be applied. + */ + void changeDataSource( QgsMapLayer *layer ); + /** * Add a raster layer directly without prompting user for location The caller must provide information compatible with the provider plugin @@ -971,9 +981,9 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow /** * \brief overloaded version of the private addLayer method that takes a list of * file names instead of prompting user with a dialog. - \param enc encoding type for the layer - \param dataSourceType type of ogr datasource - \returns true if successfully added layer + * \param enc encoding type for the layer + * \param dataSourceType type of ogr datasource + * \returns true if successfully added layer */ bool addVectorLayers( const QStringList &layerQStringList, const QString &enc, const QString &dataSourceType ); diff --git a/src/app/qgsapplayertreeviewmenuprovider.cpp b/src/app/qgsapplayertreeviewmenuprovider.cpp index 6ba4760118a..bba94d85cf0 100644 --- a/src/app/qgsapplayertreeviewmenuprovider.cpp +++ b/src/app/qgsapplayertreeviewmenuprovider.cpp @@ -199,6 +199,18 @@ QMenu *QgsAppLayerTreeViewMenuProvider::createContextMenu() menu->addSeparator(); + // change data source is only supported for vectors and rasters + if ( vlayer || rlayer ) + { + + QAction *a = new QAction( tr( "Change data source…" ), menu ); + connect( a, &QAction::triggered, [ = ] + { + QgisApp::instance()->changeDataSource( layer ); + } ); + menu->addAction( a ); + } + if ( vlayer ) { QAction *toggleEditingAction = QgisApp::instance()->actionToggleEditing(); @@ -210,7 +222,7 @@ QMenu *QgsAppLayerTreeViewMenuProvider::createContextMenu() QgisApp::instance(), [ = ] { QgisApp::instance()->attributeTable(); } ); // allow editing - int cap = vlayer->dataProvider()->capabilities(); + unsigned int cap = vlayer->dataProvider()->capabilities(); if ( cap & QgsVectorDataProvider::EditingCapabilities ) { if ( toggleEditingAction ) diff --git a/src/app/qgshandlebadlayers.cpp b/src/app/qgshandlebadlayers.cpp index 0f879bd6d03..2fbabb4bd82 100644 --- a/src/app/qgshandlebadlayers.cpp +++ b/src/app/qgshandlebadlayers.cpp @@ -26,6 +26,7 @@ #include "qgsproviderregistry.h" #include "qgsmessagebar.h" #include "qgssettings.h" +#include "qgslayertreeregistrybridge.h" #include #include @@ -33,6 +34,7 @@ #include #include #include +#include #include void QgsHandleBadLayersHandler::handleBadLayers( const QList &layers ) @@ -40,6 +42,10 @@ void QgsHandleBadLayersHandler::handleBadLayers( const QList &layers ) QApplication::setOverrideCursor( Qt::ArrowCursor ); QgsHandleBadLayers *dialog = new QgsHandleBadLayers( layers ); + dialog->buttonBox->button( QDialogButtonBox::Ignore )->setToolTip( tr( "Import all bad layers unmodified (you can fix them later)." ) ); + dialog->buttonBox->button( QDialogButtonBox::Apply )->setToolTip( tr( "Apply fixes to bad layers (remaining bad layers will be removed from the project)." ) ); + dialog->buttonBox->button( QDialogButtonBox::Discard )->setToolTip( tr( "Remove all bad layers from the project" ) ); + if ( dialog->layerCount() < layers.size() ) QgisApp::instance()->messageBar()->pushMessage( tr( "Handle bad layers" ), @@ -72,6 +78,8 @@ QgsHandleBadLayers::QgsHandleBadLayers( const QList &layers ) connect( mLayerList, &QTableWidget::itemSelectionChanged, this, &QgsHandleBadLayers::selectionChanged ); connect( mBrowseButton, &QAbstractButton::clicked, this, &QgsHandleBadLayers::browseClicked ); connect( buttonBox->button( QDialogButtonBox::Apply ), &QAbstractButton::clicked, this, &QgsHandleBadLayers::apply ); + connect( buttonBox->button( QDialogButtonBox::Ignore ), &QPushButton::clicked, this, &QgsHandleBadLayers::reject ); + connect( buttonBox->button( QDialogButtonBox::Discard ), &QPushButton::clicked, this, &QgsHandleBadLayers::accept ); mLayerList->clear(); mLayerList->setSortingEnabled( true ); @@ -340,6 +348,17 @@ void QgsHandleBadLayers::editAuthCfg() void QgsHandleBadLayers::apply() { + QgsProject::instance()->layerTreeRegistryBridge()->setEnabled( true ); + + QList toRemove; + for ( const auto &l : QgsProject::instance()->mapLayers( ) ) + { + if ( ! l->isValid() ) + toRemove << l; + } + + QgsProject::instance()->removeMapLayers( toRemove ); + for ( int i = 0; i < mLayerList->rowCount(); i++ ) { int idx = mLayerList->item( i, 0 )->data( Qt::UserRole ).toInt(); @@ -358,11 +377,15 @@ void QgsHandleBadLayers::apply() item->setForeground( QBrush( Qt::red ) ); } } + QgsProject::instance()->layerTreeRegistryBridge()->setEnabled( false ); + + if ( mLayerList->rowCount() == 0 ) + accept(); + } void QgsHandleBadLayers::accept() { - apply(); if ( mLayerList->rowCount() > 0 && QMessageBox::warning( this, @@ -375,28 +398,20 @@ void QgsHandleBadLayers::accept() { return; } + QList toRemove; + for ( const auto &l : QgsProject::instance()->mapLayers( ) ) + { + if ( ! l->isValid() ) + toRemove << l; + } + QgsProject::instance()->layerTreeRegistryBridge()->setEnabled( true ); + QgsProject::instance()->removeMapLayers( toRemove ); + QgsProject::instance()->layerTreeRegistryBridge()->setEnabled( false ); + mLayerList->clear(); QDialog::accept(); } -void QgsHandleBadLayers::reject() -{ - - if ( mLayerList->rowCount() > 0 && - QMessageBox::warning( this, - tr( "Unhandled layer will be lost." ), - tr( "There are still %n unhandled layer(s), that will be lost if you closed now.", - "unhandled layers", - mLayerList->rowCount() ), - QMessageBox::Ok | QMessageBox::Cancel, - QMessageBox::Cancel ) == QMessageBox::Cancel ) - { - return; - } - - QDialog::reject(); -} - int QgsHandleBadLayers::layerCount() { return mLayerList->rowCount(); diff --git a/src/app/qgshandlebadlayers.h b/src/app/qgshandlebadlayers.h index aa6eaa5f26f..ebc5899c77f 100644 --- a/src/app/qgshandlebadlayers.h +++ b/src/app/qgshandlebadlayers.h @@ -39,7 +39,7 @@ class QPushButton; class APP_EXPORT QgsHandleBadLayers : public QDialog - , private Ui::QgsHandleBadLayersBase + , public Ui::QgsHandleBadLayersBase { Q_OBJECT @@ -54,7 +54,6 @@ class APP_EXPORT QgsHandleBadLayers void editAuthCfg(); void apply(); void accept() override; - void reject() override; private: QPushButton *mBrowseButton = nullptr; diff --git a/src/app/qgslayertreeviewbadlayerindicator.cpp b/src/app/qgslayertreeviewbadlayerindicator.cpp index bc00d0dd84b..c5fe7a08ba6 100644 --- a/src/app/qgslayertreeviewbadlayerindicator.cpp +++ b/src/app/qgslayertreeviewbadlayerindicator.cpp @@ -20,7 +20,16 @@ #include "qgslayertreeutils.h" #include "qgslayertreemodel.h" #include "qgsvectorlayer.h" +#include "qgsrasterlayer.h" #include "qgisapp.h" +#include "qgsbrowsermodel.h" +#include "qgsbrowsertreeview.h" +#include "qgsbrowserproxymodel.h" + +#include +#include +#include +#include QgsLayerTreeViewBadLayerIndicatorProvider::QgsLayerTreeViewBadLayerIndicatorProvider( QgsLayerTreeView *view ) : QgsLayerTreeViewIndicatorProvider( view ) @@ -33,11 +42,12 @@ void QgsLayerTreeViewBadLayerIndicatorProvider::onIndicatorClicked( const QModel if ( !QgsLayerTree::isLayer( node ) ) return; - QgsVectorLayer *vlayer = qobject_cast( QgsLayerTree::toLayer( node )->layer() ); - if ( !vlayer ) + QgsMapLayer *layer = qobject_cast( QgsLayerTree::toLayer( node )->layer() ); + + if ( !layer ) return; - // TODO: open source select dialog + emit requestChangeDataSource( layer ); } QString QgsLayerTreeViewBadLayerIndicatorProvider::iconName( QgsMapLayer *layer ) @@ -49,8 +59,7 @@ QString QgsLayerTreeViewBadLayerIndicatorProvider::iconName( QgsMapLayer *layer QString QgsLayerTreeViewBadLayerIndicatorProvider::tooltipText( QgsMapLayer *layer ) { Q_UNUSED( layer ); - // TODO, click here to set a new data source. - return tr( "Bad layer!
Layer data source could not be found." ); + return tr( "Bad layer!
Layer data source could not be found. Click to set a new data source" ); } bool QgsLayerTreeViewBadLayerIndicatorProvider::acceptLayer( QgsMapLayer *layer ) diff --git a/src/app/qgslayertreeviewbadlayerindicator.h b/src/app/qgslayertreeviewbadlayerindicator.h index b973db1f12d..2a6a9c7d038 100644 --- a/src/app/qgslayertreeviewbadlayerindicator.h +++ b/src/app/qgslayertreeviewbadlayerindicator.h @@ -29,9 +29,18 @@ class QgsLayerTreeViewBadLayerIndicatorProvider : public QgsLayerTreeViewIndicat public: explicit QgsLayerTreeViewBadLayerIndicatorProvider( QgsLayerTreeView *view ); + signals: + + /** + * This signal is emitted when the user clicks on the bad layer indicator icon + * \param maplayer for change data source request + */ + void requestChangeDataSource( QgsMapLayer *maplayer ); + protected slots: void onIndicatorClicked( const QModelIndex &index ) override; + private: QString iconName( QgsMapLayer *layer ) override; QString tooltipText( QgsMapLayer *layer ) override; diff --git a/src/app/qgslayertreeviewindicatorprovider.cpp b/src/app/qgslayertreeviewindicatorprovider.cpp index cc21ff7bcdc..1a9bd05ccb4 100644 --- a/src/app/qgslayertreeviewindicatorprovider.cpp +++ b/src/app/qgslayertreeviewindicatorprovider.cpp @@ -20,6 +20,7 @@ #include "qgslayertreeutils.h" #include "qgslayertreeview.h" #include "qgsvectorlayer.h" +#include "qgsrasterlayer.h" #include "qgisapp.h" QgsLayerTreeViewIndicatorProvider::QgsLayerTreeViewIndicatorProvider( QgsLayerTreeView *view ) @@ -89,16 +90,20 @@ void QgsLayerTreeViewIndicatorProvider::onWillRemoveChildren( QgsLayerTreeNode * void QgsLayerTreeViewIndicatorProvider::onLayerLoaded() { + QgsLayerTreeLayer *layerNode = qobject_cast( sender() ); if ( !layerNode ) return; - if ( QgsVectorLayer *vlayer = qobject_cast( layerNode->layer() ) ) + if ( !( qobject_cast( layerNode->layer() ) || qobject_cast( layerNode->layer() ) ) ) + return; + + if ( QgsMapLayer *mapLayer = qobject_cast( layerNode->layer() ) ) { - if ( vlayer ) + if ( mapLayer ) { - connectSignals( vlayer ); - addOrRemoveIndicator( layerNode, vlayer ); + connectSignals( mapLayer ); + addOrRemoveIndicator( layerNode, mapLayer ); } } } @@ -123,18 +128,18 @@ void QgsLayerTreeViewIndicatorProvider::onLayerChanged() void QgsLayerTreeViewIndicatorProvider::connectSignals( QgsMapLayer *layer ) { - QgsVectorLayer *vlayer = qobject_cast( layer ); - if ( !vlayer ) + if ( !( qobject_cast( layer ) || qobject_cast( layer ) ) ) return; - connect( vlayer, &QgsVectorLayer::dataSourceChanged, this, &QgsLayerTreeViewIndicatorProvider::onLayerChanged ); + QgsMapLayer *mapLayer = qobject_cast( layer ); + connect( mapLayer, &QgsMapLayer::dataSourceChanged, this, &QgsLayerTreeViewIndicatorProvider::onLayerChanged ); } void QgsLayerTreeViewIndicatorProvider::disconnectSignals( QgsMapLayer *layer ) { - QgsVectorLayer *vlayer = qobject_cast( layer ); - if ( !vlayer ) + if ( !( qobject_cast( layer ) || qobject_cast( layer ) ) ) return; - disconnect( vlayer, &QgsVectorLayer::dataSourceChanged, this, &QgsLayerTreeViewIndicatorProvider::onLayerChanged ); + QgsMapLayer *mapLayer = qobject_cast( layer ); + disconnect( mapLayer, &QgsMapLayer::dataSourceChanged, this, &QgsLayerTreeViewIndicatorProvider::onLayerChanged ); } std::unique_ptr< QgsLayerTreeViewIndicator > QgsLayerTreeViewIndicatorProvider::newIndicator( QgsMapLayer *layer ) diff --git a/src/app/qgslayertreeviewindicatorprovider.h b/src/app/qgslayertreeviewindicatorprovider.h index e951d0a594a..df488c99c0f 100644 --- a/src/app/qgslayertreeviewindicatorprovider.h +++ b/src/app/qgslayertreeviewindicatorprovider.h @@ -38,8 +38,8 @@ class QgsMapLayer; * * Subclasses may override: * - onIndicatorClicked() default implementation does nothing - * - connectSignals() default implementation connects vector layers to dataSourceChanged() - * - disconnectSignals() default implementation disconnects vector layers from dataSourceChanged() + * - connectSignals() default implementation connects layers to dataSourceChanged() + * - disconnectSignals() default implementation disconnects layers from dataSourceChanged() */ class QgsLayerTreeViewIndicatorProvider : public QObject { @@ -51,9 +51,9 @@ class QgsLayerTreeViewIndicatorProvider : public QObject protected: // Subclasses MAY override: - //! Connect signals, default implementation connects vector layers to dataSourceChanged() + //! Connect signals, default implementation connects layers to dataSourceChanged() virtual void connectSignals( QgsMapLayer *layer ); - //! Disconnect signals, default implementation disconnects vector layers from dataSourceChanged() + //! Disconnect signals, default implementation disconnects layers from dataSourceChanged() virtual void disconnectSignals( QgsMapLayer *layer ); protected slots: diff --git a/src/app/qgsrelationmanagerdialog.cpp b/src/app/qgsrelationmanagerdialog.cpp index 4609308a6fd..e3a3df3aec0 100644 --- a/src/app/qgsrelationmanagerdialog.cpp +++ b/src/app/qgsrelationmanagerdialog.cpp @@ -49,6 +49,9 @@ void QgsRelationManagerDialog::setLayers( const QList< QgsVectorLayer * > &layer void QgsRelationManagerDialog::addRelation( const QgsRelation &rel ) { + if ( ! rel.isValid() ) + return; + mRelationsTable->setSortingEnabled( false ); int row = mRelationsTable->rowCount(); mRelationsTable->insertRow( row ); diff --git a/src/core/layertree/qgslayertreemodellegendnode.cpp b/src/core/layertree/qgslayertreemodellegendnode.cpp index 6826d90d9f9..4c314ead959 100644 --- a/src/core/layertree/qgslayertreemodellegendnode.cpp +++ b/src/core/layertree/qgslayertreemodellegendnode.cpp @@ -636,10 +636,13 @@ QImage QgsWmsLegendNode::getLegendGraphic() const QgsRasterLayer *layer = qobject_cast( mLayerNode->layer() ); const QgsLayerTreeModel *mod = model(); - if ( ! mod ) return mImage; + if ( ! mod ) + return mImage; const QgsMapSettings *ms = mod->legendFilterMapSettings(); QgsRasterDataProvider *prov = layer->dataProvider(); + if ( ! prov ) + return mImage; Q_ASSERT( ! mFetcher ); mFetcher.reset( prov->getLegendGraphicFetcher( ms ) ); diff --git a/src/core/layertree/qgslayertreeutils.cpp b/src/core/layertree/qgslayertreeutils.cpp index d3f0c7d927c..98b2f14d3b3 100644 --- a/src/core/layertree/qgslayertreeutils.cpp +++ b/src/core/layertree/qgslayertreeutils.cpp @@ -306,6 +306,40 @@ void QgsLayerTreeUtils::removeInvalidLayers( QgsLayerTreeGroup *group ) group->removeChildNode( node ); } +void QgsLayerTreeUtils::storeOriginalLayersProperties( QgsLayerTreeGroup *group, const QDomDocument *doc ) +{ + const QDomNodeList mlNodeList( doc->documentElement() + .firstChildElement( QStringLiteral( "projectlayers" ) ) + .elementsByTagName( QStringLiteral( "maplayer" ) ) ); + for ( QgsLayerTreeNode *node : group->children() ) + { + if ( QgsLayerTree::isLayer( node ) ) + { + QgsMapLayer *l( QgsLayerTree::toLayer( node )->layer() ); + if ( l ) + { + for ( int i = 0; i < mlNodeList.count(); i++ ) + { + QDomNode mlNode( mlNodeList.at( i ) ); + QString id( mlNode.firstChildElement( QStringLiteral( "id" ) ).firstChild().nodeValue() ); + if ( id == l->id() ) + { + QDomImplementation DomImplementation; + QDomDocumentType documentType = DomImplementation.createDocumentType( QStringLiteral( "qgis" ), QStringLiteral( "http://mrcc.com/qgis.dtd" ), QStringLiteral( "SYSTEM" ) ); + QDomDocument document( documentType ); + QDomElement element = mlNode.toElement(); + document.appendChild( element ); + QString str; + QTextStream stream( &str ); + document.save( stream, 4 /*indent*/ ); + l->setOriginalXmlProperties( str ); + } + } + } + } + } +} + QStringList QgsLayerTreeUtils::invisibleLayerList( QgsLayerTreeNode *node ) { QStringList list; diff --git a/src/core/layertree/qgslayertreeutils.h b/src/core/layertree/qgslayertreeutils.h index 33002612155..641b5888b13 100644 --- a/src/core/layertree/qgslayertreeutils.h +++ b/src/core/layertree/qgslayertreeutils.h @@ -19,6 +19,7 @@ #include #include #include +#include #include "qgis_core.h" class QDomElement; @@ -58,9 +59,15 @@ class CORE_EXPORT QgsLayerTreeUtils //! Returns true if any of the layers is modified static bool layersModified( const QList &layerNodes ); - //! Remove layer nodes that refer to invalid layers + //! Removes layer nodes that refer to invalid layers static void removeInvalidLayers( QgsLayerTreeGroup *group ); + /** + * Stores in a layer's originalXmlProperties the layer properties information + * \since 3.6 + */ + static void storeOriginalLayersProperties( QgsLayerTreeGroup *group, const QDomDocument *doc ); + //! Remove subtree of embedded groups and replaces it with a custom property embedded-visible-layers static void replaceChildrenOfEmbeddedGroups( QgsLayerTreeGroup *group ); diff --git a/src/core/qgsmaplayer.cpp b/src/core/qgsmaplayer.cpp index 54d6b78a06f..f452d40ad5e 100644 --- a/src/core/qgsmaplayer.cpp +++ b/src/core/qgsmaplayer.cpp @@ -283,12 +283,6 @@ bool QgsMapLayer::readLayerXml( const QDomElement &layerElement, QgsReadWriteCon QgsCoordinateReferenceSystem::setCustomCrsValidation( savedValidation ); mCRS = savedCRS; - // Abort if any error in layer, such as not found. - if ( layerError ) - { - return false; - } - // the internal name is just the data source basename //QFileInfo dataSourceFileInfo( mDataSource ); //internalName = dataSourceFileInfo.baseName(); @@ -387,7 +381,7 @@ bool QgsMapLayer::readLayerXml( const QDomElement &layerElement, QgsReadWriteCon QDomElement metadataElem = layerElement.firstChildElement( QStringLiteral( "resourceMetadata" ) ); mMetadata.readMetadataXml( metadataElem ); - return true; + return ! layerError; } // bool QgsMapLayer::readLayerXML @@ -1579,6 +1573,21 @@ bool QgsMapLayer::writeStyle( QDomNode &node, QDomDocument &doc, QString &errorM return false; } +void QgsMapLayer::setDataSource( const QString &dataSource, const QString &baseName, const QString &provider, const QgsDataProvider::ProviderOptions &options, bool loadDefaultStyleFlag ) +{ + Q_UNUSED( dataSource ); + Q_UNUSED( baseName ); + Q_UNUSED( provider ); + Q_UNUSED( options ); + Q_UNUSED( loadDefaultStyleFlag ); +} + + +QString QgsMapLayer::providerType() const +{ + return mProviderKey; +} + void QgsMapLayer::readCommonStyle( const QDomElement &layerElement, const QgsReadWriteContext &context, QgsMapLayer::StyleCategories categories ) { @@ -1824,6 +1833,21 @@ bool QgsMapLayer::isReadOnly() const return true; } +QString QgsMapLayer::originalXmlProperties() const +{ + return mOriginalXmlProperties; +} + +void QgsMapLayer::setOriginalXmlProperties( const QString &originalXmlProperties ) +{ + mOriginalXmlProperties = originalXmlProperties; +} + +void QgsMapLayer::setProviderType( const QString &providerType ) +{ + mProviderKey = providerType; +} + QSet QgsMapLayer::dependencies() const { return mDependencies; diff --git a/src/core/qgsmaplayer.h b/src/core/qgsmaplayer.h index 5071d6134d9..00f53ef5ed7 100644 --- a/src/core/qgsmaplayer.h +++ b/src/core/qgsmaplayer.h @@ -38,6 +38,7 @@ #include "qgslayermetadata.h" #include "qgsmaplayerstyle.h" #include "qgsreadwritecontext.h" +#include "qgsdataprovider.h" class QgsAbstract3DRenderer; class QgsDataProvider; @@ -231,12 +232,12 @@ class CORE_EXPORT QgsMapLayer : public QObject QString name() const; /** - * Returns the layer's data provider. + * Returns the layer's data provider, it may be null. */ virtual QgsDataProvider *dataProvider(); /** - * Returns the layer's data provider in a const-correct manner + * Returns the layer's data provider in a const-correct manner, it may be null. * \note not available in Python bindings */ virtual const QgsDataProvider *dataProvider() const SIP_SKIP; @@ -886,6 +887,29 @@ class CORE_EXPORT QgsMapLayer : public QObject virtual bool writeStyle( QDomNode &node, QDomDocument &doc, QString &errorMessage, const QgsReadWriteContext &context, StyleCategories categories = AllStyleCategories ) const; + + /** + * Updates the data source of the layer. The layer's renderer and legend will be preserved only + * if the geometry type of the new data source matches the current geometry type of the layer. + * + * Subclasses should override this method: default implementation does nothing. + * + * \param dataSource new layer data source + * \param baseName base name of the layer + * \param provider provider string + * \param options provider options + * \param loadDefaultStyleFlag set to true to reset the layer's style to the default for the + * data source + * \see dataSourceChanged() + * \since QGIS 3.6 + */ + virtual void setDataSource( const QString &dataSource, const QString &baseName, const QString &provider, const QgsDataProvider::ProviderOptions &options, bool loadDefaultStyleFlag = false ); + + /** + * Returns the provider type (provider key) for this layer + */ + QString providerType() const; + //! Returns pointer to layer's undo stack QUndoStack *undoStack(); @@ -1077,6 +1101,25 @@ class CORE_EXPORT QgsMapLayer : public QObject */ bool isRefreshOnNotifyEnabled() const { return mIsRefreshOnNofifyEnabled; } + /** + * Returns the XML properties of the original layer as they were when the layer + * was first read from the project file. In case of new layers this is normally empty. + * + * The storage format for the XML is qlr + * + * \since QGIS 3.6 + */ + QString originalXmlProperties() const; + + /** + * Sets the original XML properties for the layer to \a originalXmlProperties + * + * The storage format for the XML is qlr + * + * \since QGIS 3.6 + */ + void setOriginalXmlProperties( const QString &originalXmlProperties ); + public slots: /** @@ -1253,6 +1296,15 @@ class CORE_EXPORT QgsMapLayer : public QObject */ void flagsChanged(); + /** + * Emitted whenever the layer's data source has been changed. + * + * \see setDataSource() + * + * \since QGIS 3.5 + */ + void dataSourceChanged(); + private slots: void onNotifiedTriggerRepaint( const QString &message ); @@ -1340,6 +1392,9 @@ class CORE_EXPORT QgsMapLayer : public QObject void readCommonStyle( const QDomElement &layerElement, const QgsReadWriteContext &context, StyleCategories categories = AllStyleCategories ); + //! Sets the \a providerType (provider key) + void setProviderType( const QString &providerType ); + #ifndef SIP_RUN #if 0 //! Debugging member - invoked when a connect() is made to this object @@ -1400,6 +1455,10 @@ class CORE_EXPORT QgsMapLayer : public QObject bool mIsRefreshOnNofifyEnabled = false; QString mRefreshOnNofifyMessage; + //! Data provider key (name of the data provider) + QString mProviderKey; + + private: virtual QString baseURI( PropertyType type ) const; @@ -1465,6 +1524,13 @@ class CORE_EXPORT QgsMapLayer : public QObject //! Renderer for 3D views QgsAbstract3DRenderer *m3DRenderer = nullptr; + /** + * Stores the original XML properties of the layer when loaded from the project + * + * This information can be used to pass through the bad layers or to reset changes on a good layer + */ + QString mOriginalXmlProperties; + }; Q_DECLARE_METATYPE( QgsMapLayer * ) diff --git a/src/core/qgsmaplayerlegend.cpp b/src/core/qgsmaplayerlegend.cpp index b64a28e5516..24ad8f55449 100644 --- a/src/core/qgsmaplayerlegend.cpp +++ b/src/core/qgsmaplayerlegend.cpp @@ -307,7 +307,7 @@ QList QgsDefaultRasterLayerLegend::createLayerTre QList nodes; // temporary solution for WMS. Ideally should be done with a delegate. - if ( mLayer->dataProvider()->supportsLegendGraphic() ) + if ( mLayer->dataProvider() && mLayer->dataProvider()->supportsLegendGraphic() ) { nodes << new QgsWmsLegendNode( nodeLayer ); } diff --git a/src/core/qgsmaplayerstore.cpp b/src/core/qgsmaplayerstore.cpp index 93850e0522b..df17cbd6ed6 100644 --- a/src/core/qgsmaplayerstore.cpp +++ b/src/core/qgsmaplayerstore.cpp @@ -16,7 +16,9 @@ ***************************************************************************/ #include "qgsmaplayerstore.h" +#include "qgsmaplayer.h" #include "qgslogger.h" +#include QgsMapLayerStore::QgsMapLayerStore( QObject *parent ) : QObject( parent ) @@ -32,6 +34,18 @@ int QgsMapLayerStore::count() const return mMapLayers.size(); } +int QgsMapLayerStore::validCount() const +{ + int i = 0; + const QList cLayers = mMapLayers.values(); + for ( const auto l : cLayers ) + { + if ( l->isValid() ) + i++; + } + return i; +} + QgsMapLayer *QgsMapLayerStore::mapLayer( const QString &layerId ) const { return mMapLayers.value( layerId ); @@ -55,9 +69,9 @@ QList QgsMapLayerStore::addMapLayers( const QList QList myResultList; Q_FOREACH ( QgsMapLayer *myLayer, layers ) { - if ( !myLayer || !myLayer->isValid() ) + if ( !myLayer ) { - QgsDebugMsg( QStringLiteral( "Cannot add invalid layers" ) ); + QgsDebugMsg( QStringLiteral( "Cannot add null layers" ) ); continue; } //check the layer is not already registered! @@ -212,3 +226,14 @@ QMap QgsMapLayerStore::mapLayers() const { return mMapLayers; } + +QMap QgsMapLayerStore::validMapLayers() const +{ + QMap validLayers; + for ( const auto &id : mMapLayers.keys() ) + { + if ( mMapLayers[id]->isValid() ) + validLayers[id] = mMapLayers[id]; + } + return validLayers; +} diff --git a/src/core/qgsmaplayerstore.h b/src/core/qgsmaplayerstore.h index ff522e684af..46b2e932831 100644 --- a/src/core/qgsmaplayerstore.h +++ b/src/core/qgsmaplayerstore.h @@ -50,6 +50,12 @@ class CORE_EXPORT QgsMapLayerStore : public QObject */ int count() const; + /** + * Returns the number of valid layers contained in the store. + * \since QGIS 3.6 + */ + int validCount() const; + #ifdef SIP_RUN /** @@ -93,6 +99,15 @@ class CORE_EXPORT QgsMapLayerStore : public QObject */ QMap mapLayers() const; + /** + * Returns a map of all valid layers by layer ID. + * \see mapLayer() + * \see mapLayersByName() + * \see layers() + * \since QGIS 3.6 + */ + QMap validMapLayers() const; + #ifndef SIP_RUN /** @@ -135,7 +150,7 @@ class CORE_EXPORT QgsMapLayerStore : public QObject * 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, + * successfully. If a layer already exists in the store, * it will not be part of the returned list. * * \see addMapLayer() diff --git a/src/core/qgsproject.cpp b/src/core/qgsproject.cpp index 992c479e064..9a81df9f802 100644 --- a/src/core/qgsproject.cpp +++ b/src/core/qgsproject.cpp @@ -368,6 +368,20 @@ QgsProject::QgsProject( QObject *parent ) connect( mLayerStore.get(), &QgsMapLayerStore::layerWasAdded, this, &QgsProject::layerWasAdded ); if ( QgsApplication::instance() ) connect( QgsApplication::instance(), &QgsApplication::requestForTranslatableObjects, this, &QgsProject::registerTranslatableObjects ); + connect( mLayerStore.get(), static_cast & )>( &QgsMapLayerStore::layersWillBeRemoved ), + [ = ]( const QList &layers ) + { + for ( const auto &layer : layers ) + disconnect( layer, &QgsMapLayer::dataSourceChanged, mRelationManager, &QgsRelationManager::updateRelationsStatus ); + } + ); + connect( mLayerStore.get(), static_cast & )>( &QgsMapLayerStore::layersAdded ), + [ = ]( const QList &layers ) + { + for ( const auto &layer : layers ) + connect( layer, &QgsMapLayer::dataSourceChanged, mRelationManager, &QgsRelationManager::updateRelationsStatus ); + } + ); } @@ -926,31 +940,29 @@ bool QgsProject::addLayer( const QDomElement &layerElem, QList &broken if ( !mapLayer ) { QgsDebugMsg( QStringLiteral( "Unable to create layer" ) ); - return false; } Q_CHECK_PTR( mapLayer ); // NOLINT // have the layer restore state that is stored in Dom node - if ( mapLayer->readLayerXml( layerElem, context ) && mapLayer->isValid() ) + bool layerIsValid = mapLayer->readLayerXml( layerElem, context ) && mapLayer->isValid(); + QList newLayers; + newLayers << mapLayer; + if ( layerIsValid ) { emit readMapLayer( mapLayer, layerElem ); - - QList myLayers; - myLayers << mapLayer; - addMapLayers( myLayers ); - - return true; + addMapLayers( newLayers ); } else { - delete mapLayer; - + // It's a bad layer: do not add to legend (the user will decide if she wants to do so) + addMapLayers( newLayers, false ); + newLayers.first(); QgsDebugMsg( "Unable to load " + type + " layer" ); brokenNodes.push_back( layerElem ); - return false; } + return layerIsValid; } bool QgsProject::read( const QString &filename ) @@ -1293,8 +1305,10 @@ bool QgsProject::readProjectFile( const QString &filename ) } } - // make sure the are just valid layers - QgsLayerTreeUtils::removeInvalidLayers( mRootGroup ); + // After bad layer handling we might still have invalid layers, + // store them in case the user wanted to handle them later + // or wanted to pass them through when saving + QgsLayerTreeUtils::storeOriginalLayersProperties( mRootGroup, doc.get() ); mRootGroup->removeCustomProperty( QStringLiteral( "loading" ) ); @@ -1500,43 +1514,46 @@ void QgsProject::onMapLayersAdded( const QList &layers ) Q_FOREACH ( QgsMapLayer *layer, layers ) { - QgsVectorLayer *vlayer = qobject_cast( layer ); - if ( vlayer ) + if ( layer->isValid() ) { - if ( autoTransaction() ) + QgsVectorLayer *vlayer = qobject_cast( layer ); + if ( vlayer ) { - if ( QgsTransaction::supportsTransaction( vlayer ) ) + if ( autoTransaction() ) { - QString connString = QgsDataSourceUri( vlayer->source() ).connectionInfo(); - QString key = vlayer->providerType(); - - QgsTransactionGroup *tg = mTransactionGroups.value( qMakePair( key, connString ) ); - - if ( !tg ) + if ( QgsTransaction::supportsTransaction( vlayer ) ) { - tg = new QgsTransactionGroup(); - mTransactionGroups.insert( qMakePair( key, connString ), tg ); - tgChanged = true; + QString connString = QgsDataSourceUri( vlayer->source() ).connectionInfo(); + QString key = vlayer->providerType(); + + QgsTransactionGroup *tg = mTransactionGroups.value( qMakePair( key, connString ) ); + + if ( !tg ) + { + tg = new QgsTransactionGroup(); + mTransactionGroups.insert( qMakePair( key, connString ), tg ); + tgChanged = true; + } + tg->addLayer( vlayer ); } - tg->addLayer( vlayer ); } + vlayer->dataProvider()->setProviderProperty( QgsVectorDataProvider::EvaluateDefaultValues, evaluateDefaultValues() ); } - vlayer->dataProvider()->setProviderProperty( QgsVectorDataProvider::EvaluateDefaultValues, evaluateDefaultValues() ); - } - if ( tgChanged ) - emit transactionGroupsChanged(); + if ( tgChanged ) + emit transactionGroupsChanged(); - connect( layer, &QgsMapLayer::configChanged, this, [ = ] { setDirty(); } ); + connect( layer, &QgsMapLayer::configChanged, this, [ = ] { setDirty(); } ); - // check if we have to update connections for layers with dependencies - for ( QMap::iterator it = existingMaps.begin(); it != existingMaps.end(); it++ ) - { - QSet deps = it.value()->dependencies(); - if ( deps.contains( layer->id() ) ) + // check if we have to update connections for layers with dependencies + for ( QMap::iterator it = existingMaps.begin(); it != existingMaps.end(); it++ ) { - // reconnect to change signals - it.value()->setDependencies( deps ); + QSet deps = it.value()->dependencies(); + if ( deps.contains( layer->id() ) ) + { + // reconnect to change signals + it.value()->setDependencies( deps ); + } } } } @@ -1751,10 +1768,26 @@ bool QgsProject::writeProjectFile( const QString &filename ) QHash< QString, QPair< QString, bool> >::const_iterator emIt = mEmbeddedLayers.constFind( ml->id() ); if ( emIt == mEmbeddedLayers.constEnd() ) { - // general layer metadata - QDomElement maplayerElem = doc->createElement( QStringLiteral( "maplayer" ) ); - - ml->writeLayerXml( maplayerElem, *doc, context ); + QDomElement maplayerElem; + // If layer is not valid, let's try to restore saved properties from invalidLayerProperties + if ( ml->isValid() ) + { + // general layer metadata + maplayerElem = doc->createElement( QStringLiteral( "maplayer" ) ); + ml->writeLayerXml( maplayerElem, *doc, context ); + } + else if ( ! ml->originalXmlProperties().isEmpty() ) + { + QDomDocument document; + if ( document.setContent( ml->originalXmlProperties() ) ) + { + maplayerElem = document.firstChildElement(); + } + else + { + QgsDebugMsg( QStringLiteral( "Could not restore layer properties for layer %1" ).arg( ml->id() ) ); + } + } emit writeMapLayer( ml, maplayerElem, *doc ); @@ -2534,6 +2567,11 @@ int QgsProject::count() const return mLayerStore->count(); } +int QgsProject::validCount() const +{ + return mLayerStore->validCount(); +} + QgsMapLayer *QgsProject::mapLayer( const QString &layerId ) const { return mLayerStore->mapLayer( layerId ); @@ -2723,9 +2761,9 @@ void QgsProject::reloadAllLayers() } } -QMap QgsProject::mapLayers() const +QMap QgsProject::mapLayers( const bool validOnly ) const { - return mLayerStore->mapLayers(); + return validOnly ? mLayerStore->validMapLayers() : mLayerStore->mapLayers(); } QgsTransactionGroup *QgsProject::transactionGroup( const QString &providerKey, const QString &connString ) diff --git a/src/core/qgsproject.h b/src/core/qgsproject.h index 3cccbea5efd..36d3f0554ba 100644 --- a/src/core/qgsproject.h +++ b/src/core/qgsproject.h @@ -690,6 +690,9 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera //! Returns the number of registered layers. int count() const; + //! Returns the number of registered valid layers. + int validCount() const; + /** * Retrieve a pointer to a registered layer by layer ID. * \param layerId ID of layer to retrieve @@ -710,11 +713,13 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera /** * Returns a map of all registered layers by layer ID. + * + * \param validOnly if set only valid layers will be returned * \see mapLayer() * \see mapLayersByName() * \see layers() */ - QMap mapLayers() const; + QMap mapLayers( const bool validOnly = false ) const; /** * Returns true if the project comes from a zip archive, false otherwise. @@ -757,7 +762,7 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera * 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 registry, + * successfully. If a layer or already exists in the registry, * it will not be part of the returned QList. * * \note As a side-effect QgsProject is made dirty. diff --git a/src/core/qgsprojectbadlayerhandler.cpp b/src/core/qgsprojectbadlayerhandler.cpp index f025831b7b9..be86a98ee2a 100644 --- a/src/core/qgsprojectbadlayerhandler.cpp +++ b/src/core/qgsprojectbadlayerhandler.cpp @@ -22,7 +22,7 @@ void QgsProjectBadLayerHandler::handleBadLayers( const QList &layers ) { - QgsApplication::messageLog()->logMessage( QObject::tr( "%1 bad layers dismissed:" ).arg( layers.size() ) ); + QgsApplication::messageLog()->logMessage( QObject::tr( "%1 bad layers found:" ).arg( layers.size() ) ); Q_FOREACH ( const QDomNode &layer, layers ) { QgsApplication::messageLog()->logMessage( QObject::tr( " * %1" ).arg( dataSource( layer ) ) ); diff --git a/src/core/qgsrelation.cpp b/src/core/qgsrelation.cpp index b2c43c5bbea..273e27fbb74 100644 --- a/src/core/qgsrelation.cpp +++ b/src/core/qgsrelation.cpp @@ -325,7 +325,7 @@ QgsAttributeList QgsRelation::referencingFields() const bool QgsRelation::isValid() const { - return d->mValid && !d->mReferencingLayer.isNull() && !d->mReferencedLayer.isNull(); + return d->mValid && !d->mReferencingLayer.isNull() && !d->mReferencedLayer.isNull() && d->mReferencingLayer.data()->isValid() && d->mReferencedLayer.data()->isValid(); } bool QgsRelation::hasEqualDefinition( const QgsRelation &other ) const diff --git a/src/core/qgsrelation.h b/src/core/qgsrelation.h index 3df389b5c1d..9c5b1c26346 100644 --- a/src/core/qgsrelation.h +++ b/src/core/qgsrelation.h @@ -338,6 +338,7 @@ class CORE_EXPORT QgsRelation /** * Returns the validity of this relation. Don't use the information if it's not valid. + * A relation is considered valid if both referenced and referencig layers are valid. * * \returns true if the relation is valid */ @@ -366,14 +367,16 @@ class CORE_EXPORT QgsRelation */ Q_INVOKABLE QString resolveReferencingField( const QString &referencedField ) const; - private: - /** * Updates the validity status of this relation. * Will be called internally whenever a member is changed. + * + * \since QGIS 3.6 */ void updateRelationStatus(); + private: + mutable QExplicitlySharedDataPointer d; }; diff --git a/src/core/qgsrelationmanager.cpp b/src/core/qgsrelationmanager.cpp index a1bc3beb247..9e120990b38 100644 --- a/src/core/qgsrelationmanager.cpp +++ b/src/core/qgsrelationmanager.cpp @@ -50,7 +50,8 @@ QMap QgsRelationManager::relations() const void QgsRelationManager::addRelation( const QgsRelation &relation ) { - if ( !relation.isValid() ) + // Do not add relations to layers that do not exist + if ( !( relation.referencingLayer() && relation.referencedLayer() ) ) return; mRelations.insert( relation.id(), relation ); @@ -60,6 +61,16 @@ void QgsRelationManager::addRelation( const QgsRelation &relation ) emit changed(); } + +void QgsRelationManager::updateRelationsStatus() +{ + for ( auto relation : mRelations ) + { + relation.updateRelationStatus(); + } +} + + void QgsRelationManager::removeRelation( const QString &id ) { mRelations.remove( id ); diff --git a/src/core/qgsrelationmanager.h b/src/core/qgsrelationmanager.h index 9dfabf74c60..4e675e8f9a2 100644 --- a/src/core/qgsrelationmanager.h +++ b/src/core/qgsrelationmanager.h @@ -59,6 +59,8 @@ class CORE_EXPORT QgsRelationManager : public QObject /** * Add a relation. + * Invalid relations are added only if both referencing layer and referenced + * layer exist. * * \param relation The relation to add. */ @@ -141,6 +143,13 @@ class CORE_EXPORT QgsRelationManager : public QObject */ void changed(); + public slots: + + /** + * Updates relations status + */ + void updateRelationsStatus(); + private slots: void readProject( const QDomDocument &doc, QgsReadWriteContext &context ); void writeProject( QDomDocument &doc ); diff --git a/src/core/qgsvectorlayer.cpp b/src/core/qgsvectorlayer.cpp index 50d2afddab4..24173967f85 100644 --- a/src/core/qgsvectorlayer.cpp +++ b/src/core/qgsvectorlayer.cpp @@ -143,11 +143,13 @@ QgsVectorLayer::QgsVectorLayer( const QString &vectorLayerPath, const QString &providerKey, const LayerOptions &options ) : QgsMapLayer( VectorLayer, baseName, vectorLayerPath ) - , mProviderKey( providerKey ) , mAuxiliaryLayer( nullptr ) , mAuxiliaryLayerKey( QString() ) , mReadExtentFromXml( options.readExtentFromXml ) { + + setProviderType( providerKey ); + mGeometryOptions = qgis::make_unique(); mActions = new QgsActionManager( this ); mConditionalStyles = new QgsConditionalLayerStyles(); @@ -313,12 +315,6 @@ QString QgsVectorLayer::dataComment() const return QString(); } - -QString QgsVectorLayer::providerType() const -{ - return mProviderKey; -} - QgsCoordinateReferenceSystem QgsVectorLayer::sourceCrs() const { return crs(); @@ -599,16 +595,7 @@ QgsWkbTypes::GeometryType QgsVectorLayer::geometryType() const { QgsDebugMsgLevel( QStringLiteral( "invalid layer or pointer to mDataProvider is null" ), 3 ); } - - // We shouldn't get here, and if we have, other things are likely to - // go wrong. Code that uses the type() return value should be - // rewritten to cope with a value of Qgis::Unknown. To make this - // need known, the following message is printed every time we get - // here. - // AP: it looks like we almost always get here, since 2.x ... either we remove this - // warning of take care of the problems that may occur - QgsDebugMsg( QStringLiteral( "WARNING: This code should never be reached. Problems may occur..." ) ); - + QgsDebugMsgLevel( QStringLiteral( "Vector layer with unknown geometry type." ), 3 ); return QgsWkbTypes::UnknownGeometry; } @@ -1425,14 +1412,14 @@ bool QgsVectorLayer::readXml( const QDomNode &layer_node, QgsReadWriteContext &c QgsDataProvider::ProviderOptions options; if ( !setDataProvider( mProviderKey, options ) ) { - return false; + QgsDebugMsg( QStringLiteral( "Could not set data provider for layer %1" ).arg( publicSource() ) ); } QDomElement pkeyElem = pkeyNode.toElement(); if ( !pkeyElem.isNull() ) { QString encodingString = pkeyElem.attribute( QStringLiteral( "encoding" ) ); - if ( !encodingString.isEmpty() ) + if ( mDataProvider && !encodingString.isEmpty() ) { mDataProvider->setEncoding( encodingString ); } @@ -1591,6 +1578,7 @@ bool QgsVectorLayer::setDataProvider( QString const &provider, const QgsDataProv mDataProvider = qobject_cast( QgsProviderRegistry::instance()->createProvider( provider, dataSource, options ) ); if ( !mDataProvider ) { + mValid = false; QgsDebugMsgLevel( QStringLiteral( "Unable to get data provider" ), 2 ); return false; } @@ -1604,7 +1592,6 @@ bool QgsVectorLayer::setDataProvider( QString const &provider, const QgsDataProv if ( !mValid ) { QgsDebugMsgLevel( QStringLiteral( "Invalid provider plugin %1" ).arg( QString( mDataSource.toUtf8() ) ), 2 ); - return false; } if ( mDataProvider->capabilities() & QgsVectorDataProvider::ReadLayerMetadata ) diff --git a/src/core/qgsvectorlayer.h b/src/core/qgsvectorlayer.h index 6983aad6224..c42bd99ecbb 100644 --- a/src/core/qgsvectorlayer.h +++ b/src/core/qgsvectorlayer.h @@ -763,9 +763,6 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte //! Returns the WKBType or WKBUnknown in case of error QgsWkbTypes::Type wkbType() const FINAL; - //! Returns the provider type for this layer - QString providerType() const; - QgsCoordinateReferenceSystem sourceCrs() const FINAL; QString sourceName() const FINAL; @@ -987,7 +984,7 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte * \see dataSourceChanged() * \since QGIS 3.2 */ - void setDataSource( const QString &dataSource, const QString &baseName, const QString &provider, const QgsDataProvider::ProviderOptions &options, bool loadDefaultStyleFlag = false ); + void setDataSource( const QString &dataSource, const QString &baseName, const QString &provider, const QgsDataProvider::ProviderOptions &options, bool loadDefaultStyleFlag = false ) override; QString loadDefaultStyle( bool &resultFlag SIP_OUT ) FINAL; @@ -2135,15 +2132,6 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte signals: - /** - * Emitted whenever the layer's data source has been changed. - * - * \see setDataSource() - * - * \since QGIS 3.4 - */ - void dataSourceChanged(); - /** * This signal is emitted when selection was changed * @@ -2444,9 +2432,6 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte QString mMapTipTemplate; - //! Data provider key - QString mProviderKey; - //! The user-defined actions that are accessed from the Identify Results dialog box QgsActionManager *mActions = nullptr; diff --git a/src/core/raster/qgsrasterlayer.cpp b/src/core/raster/qgsrasterlayer.cpp index f81f35ba5cf..f5abde40f56 100644 --- a/src/core/raster/qgsrasterlayer.cpp +++ b/src/core/raster/qgsrasterlayer.cpp @@ -116,29 +116,14 @@ QgsRasterLayer::QgsRasterLayer( const QString &uri, // Constant that signals property not used. , QSTRING_NOT_SET( QStringLiteral( "Not Set" ) ) , TRSTRING_NOT_SET( tr( "Not Set" ) ) - , mProviderKey( providerKey ) { QgsDebugMsgLevel( QStringLiteral( "Entered" ), 4 ); - init(); + setProviderType( providerKey ); QgsDataProvider::ProviderOptions providerOptions; - setDataProvider( providerKey, providerOptions ); - if ( !mValid ) return; - // load default style - bool defaultLoadedFlag = false; - if ( mValid && options.loadDefaultStyle ) - { - loadDefaultStyle( defaultLoadedFlag ); - } - if ( !defaultLoadedFlag ) - { - setDefaultContrastEnhancement(); - } + setDataSource( uri, baseName, providerKey, providerOptions, options.loadDefaultStyle ); - // TODO: Connect signals from the dataprovider to the qgisapp - - emit statusChanged( tr( "QgsRasterLayer created" ) ); } // QgsRasterLayer ctor QgsRasterLayer::~QgsRasterLayer() @@ -797,7 +782,81 @@ void QgsRasterLayer::setDataProvider( QString const &provider, const QgsDataProv mValid = true; QgsDebugMsgLevel( QStringLiteral( "exiting." ), 4 ); -} // QgsRasterLayer::setDataProvider +} + +void QgsRasterLayer::setDataSource( const QString &dataSource, const QString &baseName, const QString &provider, const QgsDataProvider::ProviderOptions &options, bool loadDefaultStyleFlag ) +{ + + bool wasValid( isValid() ); + + QDomImplementation domImplementation; + QDomDocumentType documentType; + QDomDocument doc; + QString errorMsg; + QDomElement rootNode; + + // Store the original style + if ( wasValid && ! loadDefaultStyleFlag ) + { + documentType = domImplementation.createDocumentType( + QStringLiteral( "qgis" ), QStringLiteral( "http://mrcc.com/qgis.dtd" ), QStringLiteral( "SYSTEM" ) ); + doc = QDomDocument( documentType ); + rootNode = doc.createElement( QStringLiteral( "qgis" ) ); + rootNode.setAttribute( QStringLiteral( "version" ), Qgis::QGIS_VERSION ); + doc.appendChild( rootNode ); + QgsReadWriteContext writeContext; + if ( ! writeSymbology( rootNode, doc, errorMsg, writeContext ) ) + { + QgsDebugMsg( QStringLiteral( "Could not store symbology for layer %1: %2" ) + .arg( name() ) + .arg( errorMsg ) ); + } + } + + if ( mDataProvider ) + closeDataProvider(); + + init(); + + for ( int i = mPipe.size() - 1; i >= 0; --i ) + { + mPipe.remove( i ); + } + + mDataSource = dataSource; + mLayerName = baseName; + + setDataProvider( provider, options ); + + if ( mValid ) + { + // load default style + bool defaultLoadedFlag = false; + if ( loadDefaultStyleFlag ) + { + loadDefaultStyle( defaultLoadedFlag ); + } + else if ( wasValid && errorMsg.isEmpty() ) // Restore the style + { + QgsReadWriteContext readContext; + if ( ! readSymbology( rootNode, errorMsg, readContext ) ) + { + QgsDebugMsg( QStringLiteral( "Could not restore symbology for layer %1: %2" ) + .arg( name() ) + .arg( errorMsg ) ); + + } + } + + if ( !defaultLoadedFlag ) + { + setDefaultContrastEnhancement(); + } + emit statusChanged( tr( "QgsRasterLayer created" ) ); + } + emit dataSourceChanged(); + emit dataChanged(); +} void QgsRasterLayer::closeDataProvider() { @@ -1251,9 +1310,13 @@ QImage QgsRasterLayer::previewAsImage( QSize size, const QColor &bgColor, QImage { QImage myQImage( size, format ); + if ( ! isValid( ) ) + return QImage(); + myQImage.setColor( 0, bgColor.rgba() ); myQImage.fill( 0 ); //defaults to white, set to transparent for rendering on a map + QgsRasterViewPort *myRasterViewPort = new QgsRasterViewPort(); double myMapUnitsPerPixel; @@ -1474,7 +1537,12 @@ bool QgsRasterLayer::readXml( const QDomNode &layer_node, QgsReadWriteContext &c QgsDataProvider::ProviderOptions providerOptions; setDataProvider( mProviderKey, providerOptions ); - if ( !mValid ) return false; + + if ( ! mDataProvider ) + { + QgsDebugMsg( QStringLiteral( "Raster data provider could not be created for %1" ).arg( mDataSource ) ); + return false; + } QString error; bool res = readSymbology( layer_node, error, context ); diff --git a/src/core/raster/qgsrasterlayer.h b/src/core/raster/qgsrasterlayer.h index 6205041ee50..3dc448d83fe 100644 --- a/src/core/raster/qgsrasterlayer.h +++ b/src/core/raster/qgsrasterlayer.h @@ -199,7 +199,7 @@ class CORE_EXPORT QgsRasterLayer : public QgsMapLayer * */ explicit QgsRasterLayer( const QString &uri, const QString &baseName = QString(), - const QString &providerKey = "gdal", + const QString &providerType = "gdal", const QgsRasterLayer::LayerOptions &options = QgsRasterLayer::LayerOptions() ); ~QgsRasterLayer() override; @@ -259,6 +259,20 @@ class CORE_EXPORT QgsRasterLayer : public QgsMapLayer */ void setDataProvider( const QString &provider, const QgsDataProvider::ProviderOptions &options ); + /** + * Updates the data source of the layer. The layer's renderer and legend will be preserved only + * if the geometry type of the new data source matches the current geometry type of the layer. + * \param dataSource new layer data source + * \param baseName base name of the layer + * \param provider provider string + * \param options provider options + * \param loadDefaultStyleFlag set to true to reset the layer's style to the default for the + * data source + * \see dataSourceChanged() + * \since QGIS 3.6 + */ + void setDataSource( const QString &dataSource, const QString &baseName, const QString &provider, const QgsDataProvider::ProviderOptions &options, bool loadDefaultStyleFlag = false ) override; + /** * Returns the raster layer type (which is a read only property). */ @@ -453,9 +467,6 @@ class CORE_EXPORT QgsRasterLayer : public QgsMapLayer QgsRasterViewPort mLastViewPort; - //! [ data provider interface ] Data provider key - QString mProviderKey; - LayerType mRasterType; QgsRasterPipe mPipe; diff --git a/src/gui/qgsdatasourceselectdialog.cpp b/src/gui/qgsdatasourceselectdialog.cpp index ceec66a5b13..e81ef35b627 100644 --- a/src/gui/qgsdatasourceselectdialog.cpp +++ b/src/gui/qgsdatasourceselectdialog.cpp @@ -22,17 +22,30 @@ #include -QgsDataSourceSelectDialog::QgsDataSourceSelectDialog( bool setFilterByLayerType, - const QgsMapLayer::LayerType &layerType, - QWidget *parent ) +QgsDataSourceSelectDialog::QgsDataSourceSelectDialog( + QgsBrowserModel *browserModel, + bool setFilterByLayerType, + const QgsMapLayer::LayerType &layerType, + QWidget *parent ) : QDialog( parent ) { + if ( ! browserModel ) + { + mBrowserModel = qgis::make_unique(); + mOwnModel = true; + } + else + { + mBrowserModel.reset( browserModel ); + mOwnModel = false; + } + setupUi( this ); setWindowTitle( tr( "Select a Data Source" ) ); QgsGui::enableAutoGeometryRestore( this ); - mBrowserModel.initialize(); - mBrowserProxyModel.setBrowserModel( &mBrowserModel ); + mBrowserModel->initialize(); + mBrowserProxyModel.setBrowserModel( mBrowserModel.get() ); mBrowserTreeView->setHeaderHidden( true ); if ( setFilterByLayerType ) @@ -47,6 +60,12 @@ QgsDataSourceSelectDialog::QgsDataSourceSelectDialog( bool setFilterByLayerType, connect( mBrowserTreeView, &QgsBrowserTreeView::clicked, this, &QgsDataSourceSelectDialog::onLayerSelected ); } +QgsDataSourceSelectDialog::~QgsDataSourceSelectDialog() +{ + if ( ! mOwnModel ) + mBrowserModel.release(); +} + void QgsDataSourceSelectDialog::setLayerTypeFilter( QgsMapLayer::LayerType layerType ) { mBrowserProxyModel.setFilterByLayerType( true ); diff --git a/src/gui/qgsdatasourceselectdialog.h b/src/gui/qgsdatasourceselectdialog.h index 5c57b70d711..956b12614d0 100644 --- a/src/gui/qgsdatasourceselectdialog.h +++ b/src/gui/qgsdatasourceselectdialog.h @@ -50,14 +50,19 @@ class GUI_EXPORT QgsDataSourceSelectDialog: public QDialog, private Ui::QgsDataS /** * Constructs a QgsDataSourceSelectDialog, optionally filtering by layer type * + * \param browserModel an existing browser model (typically from app), if null an instance will be created * \param setFilterByLayerType activates filtering by layer type * \param layerType sets the layer type filter, this is in effect only if filtering by layer type is also active * \param parent the object */ - QgsDataSourceSelectDialog( bool setFilterByLayerType = false, + QgsDataSourceSelectDialog( QgsBrowserModel *browserModel = nullptr, + bool setFilterByLayerType = false, const QgsMapLayer::LayerType &layerType = QgsMapLayer::LayerType::VectorLayer, QWidget *parent = nullptr ); + + ~QgsDataSourceSelectDialog() override; + /** * Sets layer type filter to \a layerType and activates the filtering */ @@ -75,8 +80,9 @@ class GUI_EXPORT QgsDataSourceSelectDialog: public QDialog, private Ui::QgsDataS private: - QgsBrowserModel mBrowserModel; QgsBrowserProxyModel mBrowserProxyModel; + std::unique_ptr mBrowserModel; + bool mOwnModel = true; QgsMimeDataUtils::Uri mUri; }; diff --git a/src/gui/raster/qgsrastertransparencywidget.cpp b/src/gui/raster/qgsrastertransparencywidget.cpp index a120692063b..865efeb30af 100644 --- a/src/gui/raster/qgsrastertransparencywidget.cpp +++ b/src/gui/raster/qgsrastertransparencywidget.cpp @@ -70,6 +70,8 @@ QgsRasterTransparencyWidget::QgsRasterTransparencyWidget( QgsRasterLayer *layer, void QgsRasterTransparencyWidget::syncToLayer() { + if ( ! mRasterLayer->isValid() ) + return; QgsRasterDataProvider *provider = mRasterLayer->dataProvider(); QgsRasterRenderer *renderer = mRasterLayer->renderer(); if ( provider ) diff --git a/src/gui/raster/qgsrendererrasterpropertieswidget.cpp b/src/gui/raster/qgsrendererrasterpropertieswidget.cpp index 47640c40a64..f924d52f889 100644 --- a/src/gui/raster/qgsrendererrasterpropertieswidget.cpp +++ b/src/gui/raster/qgsrendererrasterpropertieswidget.cpp @@ -57,7 +57,8 @@ QgsRendererRasterPropertiesWidget::QgsRendererRasterPropertiesWidget( QgsMapLaye { mRasterLayer = qobject_cast( layer ); - if ( !mRasterLayer ) + + if ( !( mRasterLayer && mRasterLayer->isValid() ) ) return; setupUi( this ); @@ -126,6 +127,10 @@ void QgsRendererRasterPropertiesWidget::rendererChanged() void QgsRendererRasterPropertiesWidget::apply() { + + if ( ! mRasterLayer->isValid() ) + return; + mRasterLayer->brightnessFilter()->setBrightness( mSliderBrightness->value() ); mRasterLayer->brightnessFilter()->setContrast( mSliderContrast->value() ); diff --git a/src/providers/ogr/qgsogrprovider.cpp b/src/providers/ogr/qgsogrprovider.cpp index 9ab8b65a567..ec8bac38b17 100644 --- a/src/providers/ogr/qgsogrprovider.cpp +++ b/src/providers/ogr/qgsogrprovider.cpp @@ -892,7 +892,9 @@ QStringList QgsOgrProvider::_subLayers( bool withFeatureCount ) const void QgsOgrProvider::setEncoding( const QString &e ) { QgsSettings settings; - if ( ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) && settings.value( QStringLiteral( "qgis/ignoreShapeEncoding" ), true ).toBool() ) || !mOgrLayer->TestCapability( OLCStringsAsUTF8 ) ) + if ( ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) && + settings.value( QStringLiteral( "qgis/ignoreShapeEncoding" ), true ).toBool() ) || + ( mOgrLayer && !mOgrLayer->TestCapability( OLCStringsAsUTF8 ) ) ) { QgsVectorDataProvider::setEncoding( e ); } @@ -900,7 +902,6 @@ void QgsOgrProvider::setEncoding( const QString &e ) { QgsVectorDataProvider::setEncoding( QStringLiteral( "UTF-8" ) ); } - loadFields(); } diff --git a/src/ui/qgshandlebadlayersbase.ui b/src/ui/qgshandlebadlayersbase.ui index feab6a16958..a16fc04cd2e 100644 --- a/src/ui/qgshandlebadlayersbase.ui +++ b/src/ui/qgshandlebadlayersbase.ui @@ -27,45 +27,12 @@ Qt::Horizontal - QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok + QDialogButtonBox::Apply|QDialogButtonBox::Discard|QDialogButtonBox::Ignore - - - buttonBox - accepted() - QgsHandleBadLayersBase - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - QgsHandleBadLayersBase - reject() - - - 316 - 260 - - - 286 - 274 - - - - + diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index de110c46e42..b5d9bd0464d 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -55,6 +55,7 @@ ADD_PYTHON_TEST(PyQgsFieldComboBoxTest test_qgsfieldcombobox.py) ADD_PYTHON_TEST(PyQgsFieldFormattersTest test_qgsfieldformatters.py) ADD_PYTHON_TEST(PyQgsFillSymbolLayers test_qgsfillsymbollayers.py) ADD_PYTHON_TEST(PyQgsProject test_qgsproject.py) +ADD_PYTHON_TEST(PyQgsProjectBadLayers test_qgsprojectbadlayers.py) ADD_PYTHON_TEST(PyQgsFeatureIterator test_qgsfeatureiterator.py) ADD_PYTHON_TEST(PyQgsFeedback test_qgsfeedback.py) ADD_PYTHON_TEST(PyQgsFields test_qgsfields.py) diff --git a/tests/src/python/test_qgsmaplayerstore.py b/tests/src/python/test_qgsmaplayerstore.py index f1f20670291..363c9274d71 100644 --- a/tests/src/python/test_qgsmaplayerstore.py +++ b/tests/src/python/test_qgsmaplayerstore.py @@ -66,9 +66,11 @@ class TestQgsMapLayerStore(unittest.TestCase): """ 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) + vl = QgsVectorLayer("Point?field=x:string", 'test', "xxx") + self.assertEqual(store.addMapLayer(vl), vl) + self.assertEqual(len(store.mapLayersByName('test')), 1) + self.assertEqual(store.count(), 1) + self.assertEqual(store.validCount(), 0) def test_addMapLayerSignals(self): """ test that signals are correctly emitted when adding map layer""" @@ -120,12 +122,14 @@ class TestQgsMapLayerStore(unittest.TestCase): store.removeAllMapLayers() def test_addMapLayersInvalid(self): - """ test that invalid map layersd can't be added to store """ + """ test that invalid map layers can 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) + vl = QgsVectorLayer("Point?field=x:string", 'test', "xxx") + self.assertEqual(store.addMapLayers([vl]), [vl]) + self.assertEqual(len(store.mapLayersByName('test')), 1) + self.assertEqual(store.count(), 1) + self.assertEqual(store.validCount(), 0) def test_addMapLayersAlreadyAdded(self): """ test that already added layers can't be readded to store """ diff --git a/tests/src/python/test_qgsproject.py b/tests/src/python/test_qgsproject.py index ea717d89e82..43063ccf95c 100644 --- a/tests/src/python/test_qgsproject.py +++ b/tests/src/python/test_qgsproject.py @@ -249,12 +249,17 @@ class TestQgsProject(unittest.TestCase): QgsProject.instance().removeAllMapLayers() def test_addMapLayerInvalid(self): - """ test that invalid map layersd can't be added to registry """ + """ test that invalid map layers can 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) + vl = QgsVectorLayer("Point?field=x:string", 'test', "xxx") + self.assertEqual(QgsProject.instance().addMapLayer(vl), vl) + self.assertFalse(vl in QgsProject.instance().mapLayers(True).values()) + self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) + self.assertEqual(QgsProject.instance().count(), 1) + self.assertEqual(QgsProject.instance().validCount(), 0) + + self.assertEqual(len(QgsProject.instance().mapLayers(True)), 0) QgsProject.instance().removeAllMapLayers() @@ -313,12 +318,15 @@ class TestQgsProject(unittest.TestCase): QgsProject.instance().removeAllMapLayers() def test_addMapLayersInvalid(self): - """ test that invalid map layersd can't be added to registry """ + """ test that invalid map layers can 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) + vl = QgsVectorLayer("Point?field=x:string", 'test', "xxx") + self.assertEqual(QgsProject.instance().addMapLayers([vl]), [vl]) + self.assertFalse(vl in QgsProject.instance().mapLayers(True).values()) + self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1) + self.assertEqual(QgsProject.instance().count(), 1) + self.assertEqual(QgsProject.instance().validCount(), 0) QgsProject.instance().removeAllMapLayers() diff --git a/tests/src/python/test_qgsprojectbadlayers.py b/tests/src/python/test_qgsprojectbadlayers.py new file mode 100644 index 00000000000..75a4d0a33a6 --- /dev/null +++ b/tests/src/python/test_qgsprojectbadlayers.py @@ -0,0 +1,298 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsProject bad layers handling. + +.. 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. +""" +from builtins import chr +from builtins import range +__author__ = 'Alessandro Pasotti' +__date__ = '20/10/2018' +__copyright__ = 'Copyright 2018, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +import os +import filecmp + +import qgis # NOQA + +from qgis.core import (QgsProject, + QgsVectorLayer, + QgsCoordinateTransform, + QgsMapSettings, + QgsRasterLayer, + QgsMapLayer, + QgsRectangle, + QgsDataProvider, + QgsCoordinateReferenceSystem, + ) +from qgis.gui import (QgsLayerTreeMapCanvasBridge, + QgsMapCanvas) + +from qgis.PyQt.QtGui import QFont, QColor +from qgis.PyQt.QtTest import QSignalSpy +from qgis.PyQt.QtCore import QT_VERSION_STR, QTemporaryDir, QSize + +from qgis.testing import start_app, unittest +from utilities import (unitTestDataPath, renderMapToImage) +from shutil import copyfile + +app = start_app() +TEST_DATA_DIR = unitTestDataPath() + + +class TestQgsProjectBadLayers(unittest.TestCase): + + def setUp(self): + p = QgsProject.instance() + p.removeAllMapLayers() + + @classmethod + def getBaseMapSettings(cls): + """ + :rtype: QgsMapSettings + """ + ms = QgsMapSettings() + crs = QgsCoordinateReferenceSystem() + """:type: QgsCoordinateReferenceSystem""" + crs.createFromSrid(4326) + ms.setBackgroundColor(QColor(152, 219, 249)) + ms.setOutputSize(QSize(420, 280)) + ms.setOutputDpi(72) + ms.setFlag(QgsMapSettings.Antialiasing, True) + ms.setFlag(QgsMapSettings.UseAdvancedEffects, False) + ms.setFlag(QgsMapSettings.ForceVectorOutput, False) # no caching? + ms.setDestinationCrs(crs) + return ms + + def test_project_roundtrip(self): + """Tests that a project with bad layers can be saved and restored""" + + p = QgsProject.instance() + temp_dir = QTemporaryDir() + for ext in ('shp', 'dbf', 'shx', 'prj'): + copyfile(os.path.join(TEST_DATA_DIR, 'lines.%s' % ext), os.path.join(temp_dir.path(), 'lines.%s' % ext)) + copyfile(os.path.join(TEST_DATA_DIR, 'raster', 'band1_byte_ct_epsg4326.tif'), os.path.join(temp_dir.path(), 'band1_byte_ct_epsg4326.tif')) + copyfile(os.path.join(TEST_DATA_DIR, 'raster', 'band1_byte_ct_epsg4326.tif'), os.path.join(temp_dir.path(), 'band1_byte_ct_epsg4326_copy.tif')) + l = QgsVectorLayer(os.path.join(temp_dir.path(), 'lines.shp'), 'lines', 'ogr') + self.assertTrue(l.isValid()) + + rl = QgsRasterLayer(os.path.join(temp_dir.path(), 'band1_byte_ct_epsg4326.tif'), 'raster', 'gdal') + self.assertTrue(rl.isValid()) + rl_copy = QgsRasterLayer(os.path.join(temp_dir.path(), 'band1_byte_ct_epsg4326_copy.tif'), 'raster_copy', 'gdal') + self.assertTrue(rl_copy.isValid()) + self.assertTrue(p.addMapLayers([l, rl, rl_copy])) + + # Save project + project_path = os.path.join(temp_dir.path(), 'project.qgs') + self.assertTrue(p.write(project_path)) + + # Re-load the project, checking for the XML properties + self.assertTrue(p.read(project_path)) + vector = list(p.mapLayersByName('lines'))[0] + raster = list(p.mapLayersByName('raster'))[0] + raster_copy = list(p.mapLayersByName('raster_copy'))[0] + self.assertTrue(vector.originalXmlProperties() != '') + self.assertTrue(raster.originalXmlProperties() != '') + self.assertTrue(raster_copy.originalXmlProperties() != '') + # Test setter + raster.setOriginalXmlProperties('pippo') + self.assertEqual(raster.originalXmlProperties(), 'pippo') + + # Now create and invalid project: + bad_project_path = os.path.join(temp_dir.path(), 'project_bad.qgs') + with open(project_path, 'r') as infile: + with open(bad_project_path, 'w+') as outfile: + outfile.write(infile.read().replace('./lines.shp', './lines-BAD_SOURCE.shp').replace('band1_byte_ct_epsg4326_copy.tif', 'band1_byte_ct_epsg4326_copy-BAD_SOURCE.tif')) + + # Load the bad project + self.assertTrue(p.read(bad_project_path)) + # Check layer is invalid + vector = list(p.mapLayersByName('lines'))[0] + raster = list(p.mapLayersByName('raster'))[0] + raster_copy = list(p.mapLayersByName('raster_copy'))[0] + self.assertIsNotNone(vector.dataProvider()) + self.assertIsNotNone(raster.dataProvider()) + self.assertIsNotNone(raster_copy.dataProvider()) + self.assertFalse(vector.isValid()) + self.assertFalse(raster_copy.isValid()) + # Try a getFeatures + self.assertEqual([f for f in vector.getFeatures()], []) + self.assertTrue(raster.isValid()) + self.assertEqual(vector.providerType(), 'ogr') + + # Save the project + bad_project_path2 = os.path.join(temp_dir.path(), 'project_bad2.qgs') + p.write(bad_project_path2) + # Re-save the project, with fixed paths + good_project_path = os.path.join(temp_dir.path(), 'project_good.qgs') + with open(bad_project_path2, 'r') as infile: + with open(good_project_path, 'w+') as outfile: + outfile.write(infile.read().replace('./lines-BAD_SOURCE.shp', './lines.shp').replace('band1_byte_ct_epsg4326_copy-BAD_SOURCE.tif', 'band1_byte_ct_epsg4326_copy.tif')) + + # Load the good project + self.assertTrue(p.read(good_project_path)) + # Check layer is valid + vector = list(p.mapLayersByName('lines'))[0] + raster = list(p.mapLayersByName('raster'))[0] + raster_copy = list(p.mapLayersByName('raster_copy'))[0] + self.assertTrue(vector.isValid()) + self.assertTrue(raster.isValid()) + self.assertTrue(raster_copy.isValid()) + + def test_project_relations(self): + """Tests that a project with bad layers and relations can be saved with relations""" + + temp_dir = QTemporaryDir() + p = QgsProject.instance() + for ext in ('qgs', 'gpkg'): + copyfile(os.path.join(TEST_DATA_DIR, 'projects', 'relation_reference_test.%s' % ext), os.path.join(temp_dir.path(), 'relation_reference_test.%s' % ext)) + + # Load the good project + project_path = os.path.join(temp_dir.path(), 'relation_reference_test.qgs') + self.assertTrue(p.read(project_path)) + point_a = list(p.mapLayersByName('point_a'))[0] + point_b = list(p.mapLayersByName('point_b'))[0] + point_a_source = point_a.publicSource() + point_b_source = point_b.publicSource() + self.assertTrue(point_a.isValid()) + self.assertTrue(point_b.isValid()) + + # Check relations + def _check_relations(): + relation = list(p.relationManager().relations().values())[0] + self.assertTrue(relation.isValid()) + self.assertEqual(relation.referencedLayer().id(), point_b.id()) + self.assertEqual(relation.referencingLayer().id(), point_a.id()) + + _check_relations() + + # Now build a bad project + bad_project_path = os.path.join(temp_dir.path(), 'relation_reference_test_bad.qgs') + with open(project_path, 'r') as infile: + with open(bad_project_path, 'w+') as outfile: + outfile.write(infile.read().replace('./relation_reference_test.gpkg', './relation_reference_test-BAD_SOURCE.gpkg')) + + # Load the bad project + self.assertTrue(p.read(bad_project_path)) + point_a = list(p.mapLayersByName('point_a'))[0] + point_b = list(p.mapLayersByName('point_b'))[0] + self.assertFalse(point_a.isValid()) + self.assertFalse(point_b.isValid()) + + # This fails because relations are not valid anymore + with self.assertRaises(AssertionError): + _check_relations() + + # Changing data source, relations should be restored: + point_a.setDataSource(point_a_source, 'point_a', 'ogr') + point_b.setDataSource(point_b_source, 'point_b', 'ogr') + self.assertTrue(point_a.isValid()) + self.assertTrue(point_b.isValid()) + + # Check if relations were restored + _check_relations() + + # Reload the bad project + self.assertTrue(p.read(bad_project_path)) + point_a = list(p.mapLayersByName('point_a'))[0] + point_b = list(p.mapLayersByName('point_b'))[0] + self.assertFalse(point_a.isValid()) + self.assertFalse(point_b.isValid()) + + # This fails because relations are not valid anymore + with self.assertRaises(AssertionError): + _check_relations() + + # Save the bad project + bad_project_path2 = os.path.join(temp_dir.path(), 'relation_reference_test_bad2.qgs') + p.write(bad_project_path2) + + # Now fix the bad project + bad_project_path_fixed = os.path.join(temp_dir.path(), 'relation_reference_test_bad_fixed.qgs') + with open(bad_project_path2, 'r') as infile: + with open(bad_project_path_fixed, 'w+') as outfile: + outfile.write(infile.read().replace('./relation_reference_test-BAD_SOURCE.gpkg', './relation_reference_test.gpkg')) + + # Load the fixed project + self.assertTrue(p.read(bad_project_path_fixed)) + point_a = list(p.mapLayersByName('point_a'))[0] + point_b = list(p.mapLayersByName('point_b'))[0] + point_a_source = point_a.publicSource() + point_b_source = point_b.publicSource() + self.assertTrue(point_a.isValid()) + self.assertTrue(point_b.isValid()) + _check_relations() + + def testStyles(self): + """Test that styles for rasters and vectors are kept when setDataSource is called""" + + options = QgsDataProvider.ProviderOptions() + temp_dir = QTemporaryDir() + p = QgsProject.instance() + project_path = os.path.join(temp_dir.path(), 'good_layers_test.qgs') + copyfile(os.path.join(TEST_DATA_DIR, 'projects', 'good_layers_test.qgs'), project_path) + copyfile(os.path.join(TEST_DATA_DIR, 'projects', 'bad_layers_test.gpkg'), os.path.join(temp_dir.path(), 'bad_layers_test.gpkg')) + for f in ( + 'bad_layer_raster_test.tfw', + 'bad_layer_raster_test.tiff', + 'bad_layer_raster_test.tiff.aux.xml', + 'bad_layers_test.gpkg', + 'good_layers_test.qgs'): + copyfile(os.path.join(TEST_DATA_DIR, 'projects', f), os.path.join(temp_dir.path(), f)) + + p = QgsProject().instance() + self.assertTrue(p.read(project_path)) + self.assertEqual(p.count(), 3) + + ms = self.getBaseMapSettings() + point_a = list(p.mapLayersByName('point_a'))[0] + point_b = list(p.mapLayersByName('point_b'))[0] + raster = list(p.mapLayersByName('bad_layer_raster_test'))[0] + self.assertTrue(point_a.isValid()) + self.assertTrue(point_b.isValid()) + self.assertTrue(raster.isValid()) + ms.setExtent(QgsRectangle(2.81861, 41.98138, 2.81952, 41.9816)) + ms.setLayers([point_a, point_b, raster]) + image = renderMapToImage(ms) + print(os.path.join(temp_dir.path(), 'expected.png')) + self.assertTrue(image.save(os.path.join(temp_dir.path(), 'expected.png'), 'PNG')) + + point_a_source = point_a.publicSource() + point_b_source = point_b.publicSource() + raster_source = raster.publicSource() + point_a.setDataSource(point_a_source, point_a.name(), 'ogr', options) + point_b.setDataSource(point_b_source, point_b.name(), 'ogr', options) + raster.setDataSource(raster_source, raster.name(), 'gdal', options) + self.assertTrue(image.save(os.path.join(temp_dir.path(), 'actual.png'), 'PNG')) + + self.assertTrue(filecmp.cmp(os.path.join(temp_dir.path(), 'actual.png'), os.path.join(temp_dir.path(), 'expected.png')), False) + + # Now build a bad project + bad_project_path = os.path.join(temp_dir.path(), 'bad_layers_test.qgs') + with open(project_path, 'r') as infile: + with open(bad_project_path, 'w+') as outfile: + outfile.write(infile.read().replace('./bad_layers_test.', './bad_layers_test-BAD_SOURCE.').replace('bad_layer_raster_test.tiff', 'bad_layer_raster_test-BAD_SOURCE.tiff')) + + self.assertTrue(p.read(bad_project_path)) + self.assertEqual(p.count(), 3) + point_a = list(p.mapLayersByName('point_a'))[0] + point_b = list(p.mapLayersByName('point_b'))[0] + raster = list(p.mapLayersByName('bad_layer_raster_test'))[0] + self.assertFalse(point_a.isValid()) + self.assertFalse(point_b.isValid()) + self.assertFalse(raster.isValid()) + + point_a.setDataSource(point_a_source, point_a.name(), 'ogr', options) + point_b.setDataSource(point_b_source, point_b.name(), 'ogr', options) + raster.setDataSource(raster_source, raster.name(), 'gdal', options) + self.assertTrue(image.save(os.path.join(temp_dir.path(), 'actual_fixed.png'), 'PNG')) + + self.assertTrue(filecmp.cmp(os.path.join(temp_dir.path(), 'actual_fixed.png'), os.path.join(temp_dir.path(), 'expected.png')), False) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsrasterlayer.py b/tests/src/python/test_qgsrasterlayer.py index 126471d66a6..8102ea7bc35 100644 --- a/tests/src/python/test_qgsrasterlayer.py +++ b/tests/src/python/test_qgsrasterlayer.py @@ -16,16 +16,25 @@ __revision__ = '$Format:%H$' import qgis # NOQA import os +import filecmp -from qgis.PyQt.QtCore import QFileInfo -from qgis.PyQt.QtGui import QColor +from qgis.PyQt.QtCore import QSize, QFileInfo, Qt, QTemporaryDir + +from qgis.PyQt.QtGui import ( + QColor, + QImage, + QPainter, + QResizeEvent +) from qgis.PyQt.QtXml import QDomDocument + from qgis.core import (QgsRaster, QgsRasterLayer, QgsReadWriteContext, QgsColorRampShader, QgsContrastEnhancement, + QgsDataProvider, QgsProject, QgsMapSettings, QgsPointXY, @@ -40,6 +49,7 @@ from qgis.core import (QgsRaster, QgsGradientColorRamp) from utilities import unitTestDataPath from qgis.testing import start_app, unittest +from qgis.testing.mocked import get_iface # Convenience instances in case you may need them # not used in this test @@ -48,6 +58,14 @@ start_app() class TestQgsRasterLayer(unittest.TestCase): + def setUp(self): + self.iface = get_iface() + QgsProject.instance().removeAllMapLayers() + + self.iface.mapCanvas().viewport().resize(400, 400) + # For some reason the resizeEvent is not delivered, fake it + self.iface.mapCanvas().resizeEvent(QResizeEvent(QSize(400, 400), self.iface.mapCanvas().size())) + def testIdentify(self): myPath = os.path.join(unitTestDataPath(), 'landsat.tif') myFileInfo = QFileInfo(myPath) @@ -694,6 +712,35 @@ class TestQgsRasterLayer(unittest.TestCase): # compare xml documents self.assertEqual(layer_doc.toString(), clone_doc.toString()) + def testSetDataSource(self): + """Test change data source""" + + temp_dir = QTemporaryDir() + options = QgsDataProvider.ProviderOptions() + myPath = os.path.join(unitTestDataPath('raster'), + 'band1_float32_noct_epsg4326.tif') + myFileInfo = QFileInfo(myPath) + myBaseName = myFileInfo.baseName() + layer = QgsRasterLayer(myPath, myBaseName) + renderer = QgsSingleBandGrayRenderer(layer.dataProvider(), 2) + + image = layer.previewAsImage(QSize(400, 400)) + self.assertFalse(image.isNull()) + self.assertTrue(image.save(os.path.join(temp_dir.path(), 'expected.png'), "PNG")) + + layer.setDataSource(myPath.replace('4326.tif', '4326-BAD_SOURCE.tif'), 'bad_layer', 'gdal', options) + self.assertFalse(layer.isValid()) + image = layer.previewAsImage(QSize(400, 400)) + self.assertTrue(image.isNull()) + + layer.setDataSource(myPath.replace('4326-BAD_SOURCE.tif', '4326.tif'), 'bad_layer', 'gdal', options) + self.assertTrue(layer.isValid()) + image = layer.previewAsImage(QSize(400, 400)) + self.assertFalse(image.isNull()) + self.assertTrue(image.save(os.path.join(temp_dir.path(), 'actual.png'), "PNG")) + + self.assertTrue(filecmp.cmp(os.path.join(temp_dir.path(), 'actual.png'), os.path.join(temp_dir.path(), 'expected.png')), False) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgsrelation.py b/tests/src/python/test_qgsrelation.py index 7945ce0ca6d..b4f9907eef6 100644 --- a/tests/src/python/test_qgsrelation.py +++ b/tests/src/python/test_qgsrelation.py @@ -20,6 +20,7 @@ from qgis.core import (QgsVectorLayer, QgsGeometry, QgsPointXY, QgsAttributeEditorElement, + QgsAttributeEditorRelation, QgsProject ) from utilities import unitTestDataPath @@ -162,7 +163,10 @@ class TestQgsRelation(unittest.TestCase): def testValidRelationAfterChangingStyle(self): # load project myPath = os.path.join(unitTestDataPath(), 'relations.qgs') - QgsProject.instance().read(myPath) + p = QgsProject.instance() + self.assertTrue(p.read(myPath)) + for l in p.mapLayers().values(): + self.assertTrue(l.isValid()) # get referenced layer relations = QgsProject.instance().relationManager().relations() @@ -171,6 +175,7 @@ class TestQgsRelation(unittest.TestCase): # check that the relation is valid valid = False + self.assertEqual(len(referencedLayer.editFormConfig().tabs()[0].children()), 7) for tab in referencedLayer.editFormConfig().tabs(): for t in tab.children(): if (t.type() == QgsAttributeEditorElement.AeTypeRelation): @@ -180,6 +185,11 @@ class TestQgsRelation(unittest.TestCase): # update style referencedLayer.styleManager().setCurrentStyle("custom") + for l in p.mapLayers().values(): + self.assertTrue(l.isValid()) + + self.assertEqual(len(referencedLayer.editFormConfig().tabs()[0].children()), 7) + # check that the relation is still valid referencedLayer = relation.referencedLayer() valid = False diff --git a/tests/testdata/projects/bad_layer_raster_test.tfw b/tests/testdata/projects/bad_layer_raster_test.tfw new file mode 100644 index 00000000000..8124ff0648a --- /dev/null +++ b/tests/testdata/projects/bad_layer_raster_test.tfw @@ -0,0 +1,6 @@ +0.00000055284657534 +0 +0 +-0.00000055284657534 +2.8182844964232876 +41.98181469976164948 diff --git a/tests/testdata/projects/bad_layer_raster_test.tiff b/tests/testdata/projects/bad_layer_raster_test.tiff new file mode 100644 index 00000000000..0ed22e86a63 Binary files /dev/null and b/tests/testdata/projects/bad_layer_raster_test.tiff differ diff --git a/tests/testdata/projects/bad_layer_raster_test.tiff.aux.xml b/tests/testdata/projects/bad_layer_raster_test.tiff.aux.xml new file mode 100644 index 00000000000..eb56c82de54 --- /dev/null +++ b/tests/testdata/projects/bad_layer_raster_test.tiff.aux.xml @@ -0,0 +1,36 @@ + + + + + -0.498046875 + 255.498046875 + 256 + 0 + 0 + 13|18|50|12|7|13|16|15|14|20|6|13|13|8|9|58|31|15|21|16|15|25|51|20|19|22|16|47|30|20|29|32|22|38|24|32|32|39|35|47|70|68|52|35|121|46|42|60|53|37|39|44|49|83|52|63|54|67|77|73|61|72|56|76|63|73|72|76|99|69|93|90|80|85|92|110|100|106|112|109|100|99|92|113|137|151|136|129|182|156|152|146|126|147|162|163|195|157|145|202|219|164|180|175|213|187|206|193|195|214|211|208|192|211|191|925|454|420|397|455|428|333|414|313|402|378|392|418|364|400|427|420|453|444|477|403|447|513|438|537|510|527|530|544|570|546|619|530|595|651|597|660|738|714|668|876|828|20253|2012|1713|1589|1342|1401|1213|1242|1274|1335|1360|1307|1406|1363|1342|3316|1661|1674|1552|1719|1705|1667|1638|1449|1642|1468|1484|1550|1517|1605|1545|1617|1599|1683|2325|2934|3369|3918|4835|5909|7068|7857|148605|9058|10029|10103|9572|9936|10530|10904|11245|10726|10940|10999|11303|11583|12851|13863|15968|20827|1929455|12350|9950|9683|9843|10560|14977|666396|5951|5178|4871|4578|4364|4380|4142|4039|3754|3589|4403|3554|3073|2813|2513|2423|2209|2196|2167|2204|2215|2434|115706|1815|1722|1771|1920|2175|2521|3127|120133 + + + + 255 + 217.8975575084 + 0 + 16.402309158646 + + + + + 255 + 211.56457158206 + 0 + 19.453039894122 + + + + + 255 + 203.74267168054 + 8 + 21.479123268522 + + + diff --git a/tests/testdata/projects/bad_layers_test.gpkg b/tests/testdata/projects/bad_layers_test.gpkg new file mode 100644 index 00000000000..ed0a39349bb Binary files /dev/null and b/tests/testdata/projects/bad_layers_test.gpkg differ diff --git a/tests/testdata/projects/good_layers_test.qgs b/tests/testdata/projects/good_layers_test.qgs new file mode 100644 index 00000000000..3a3e1628763 --- /dev/null +++ b/tests/testdata/projects/good_layers_test.qgs @@ -0,0 +1,832 @@ + + + + + + + + + + +proj=longlat +datum=WGS84 +no_defs + 3452 + 4326 + EPSG:4326 + WGS 84 + longlat + WGS84 + true + + + + + + + + + + + + + + + point_a_e99cf1b1_e13e_44a8_b912_58505e7ac967 + point_b_d23a7df9_c9d6_4b48_9162_5fc1a7db2b96 + bad_layer_raster_test_18978e96_6781_4a5d_b0bc_474994ed231a + + + + + + + + + + + + + + + degrees + + 2.81828421961071651 + 41.9812628573046851 + 2.82010032075159867 + 41.9817810775760023 + + 0 + + + +proj=longlat +datum=WGS84 +no_defs + 3452 + 4326 + EPSG:4326 + WGS 84 + longlat + WGS84 + true + + + 0 + + + + + + + + + + + + + + + + + + + 2.81828421999999978 + 41.98122895881507333 + 2.82010032099999153 + 41.98181497618493552 + + bad_layer_raster_test_18978e96_6781_4a5d_b0bc_474994ed231a + ./bad_layer_raster_test.tiff + + + + bad_layer_raster_test + + + +proj=longlat +datum=WGS84 +no_defs + 3452 + 4326 + EPSG:4326 + WGS 84 + longlat + WGS84 + true + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + true + + + + + + + + + + + + + gdal + + + + + + + + + + + 1 + 1 + 1 + + + + + + + + + + + + MinMax + WholeRaster + Estimated + 0.02 + 0.98 + 2 + + + 0 + 255 + StretchToMinimumMaximum + + + + + + + 0 + + + + 2.81884431838989258 + 41.9814453125 + 2.81894969940185547 + 41.98154067993164063 + + point_a_e99cf1b1_e13e_44a8_b912_58505e7ac967 + ./bad_layers_test.gpkg|layername=point_a + + + + point_a + + + +proj=longlat +datum=WGS84 +no_defs + 3452 + 4326 + EPSG:4326 + WGS 84 + longlat + WGS84 + true + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + false + + + + + + + + + + + + + ogr + + + + + + + + + + + 1 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + generatedlayout + + + + + + + + + + + + fid + + + + + 2.81895375251770108 + 41.98152542114257813 + 2.81904959678649902 + 41.981597900390625 + + point_b_d23a7df9_c9d6_4b48_9162_5fc1a7db2b96 + ./bad_layers_test.gpkg|layername=point_b + + + + point_b + + + +proj=longlat +datum=WGS84 +no_defs + 3452 + 4326 + EPSG:4326 + WGS 84 + longlat + WGS84 + true + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + false + + + + + + + + + + + + + ogr + + + + + + + + + + + 1 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + generatedlayout + + + + + + + + + + + + + + + fid + + + + + + + + + + + + + + + + + + 5000 + false + + false + + + 8 + + conditions unknown + + true + 2 + MU + + 90 + + WGS84 + + + 1 + + + + false + + + None + false + false + false + + + + + + + + + + + 1 + true + + + + + + + + + + false + + + + false + + + false + + + true + 30 + true + 50 + 0 + 16 + false + false + false + + + m2 + meters + + + + + + false + + + + + + + + + qgisce_catalog_autoload + qgisce_template_version + + + true + 1.0 + + + + + 255 + 255 + 255 + 255 + 0 + 255 + 255 + + + + + + + + + + + + + + + + + + + + + + + Alessandro Pasotti + 2018-07-06T13:56:35 + + + + diff --git a/tests/testdata/projects/relation_reference_test.gpkg b/tests/testdata/projects/relation_reference_test.gpkg new file mode 100644 index 00000000000..128995114e0 Binary files /dev/null and b/tests/testdata/projects/relation_reference_test.gpkg differ diff --git a/tests/testdata/projects/relation_reference_test.qgs b/tests/testdata/projects/relation_reference_test.qgs new file mode 100644 index 00000000000..8726866a8d7 --- /dev/null +++ b/tests/testdata/projects/relation_reference_test.qgs @@ -0,0 +1,710 @@ + + + + + + + + + + +proj=utm +zone=30 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs + 2103 + 25830 + EPSG:25830 + ETRS89 / UTM zone 30N + utm + GRS80 + false + + + + + + + + + + + + point_a_e99cf1b1_e13e_44a8_b912_58505e7ac967 + point_b_d23a7df9_c9d6_4b48_9162_5fc1a7db2b96 + + + + + + + + + + + + + + + meters + + 982072.13989259675145149 + 4664077.95563993975520134 + 982298.28138825763016939 + 4664173.45214581862092018 + + 0 + + + +proj=utm +zone=30 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs + 2103 + 25830 + EPSG:25830 + ETRS89 / UTM zone 30N + utm + GRS80 + false + + + 0 + + + + + + + + + + + + + + + + 2.81884431838989258 + 41.9814453125 + 2.81894969940185547 + 41.98154067993164063 + + point_a_e99cf1b1_e13e_44a8_b912_58505e7ac967 + ./relation_reference_test.gpkg|layername=point_a + + + + point_a + + + +proj=longlat +datum=WGS84 +no_defs + 3452 + 4326 + EPSG:4326 + WGS 84 + longlat + WGS84 + true + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + false + + + + + + + + + + + + + ogr + + + + + + + + + + + 1 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + generatedlayout + + + + + + + + + + + + fid + + + + + 2.81895381989531746 + 41.9815272025749664 + 2.81904954416376663 + 41.98159776111032926 + + point_b_d23a7df9_c9d6_4b48_9162_5fc1a7db2b96 + ./relation_reference_test.gpkg|layername=point_b + + + + point_b + + + +proj=longlat +datum=WGS84 +no_defs + 3452 + 4326 + EPSG:4326 + WGS 84 + longlat + WGS84 + true + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + false + + + + + + + + + + + + + ogr + + + + + + + + + + + 1 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + generatedlayout + + + + + + + + + + + + + + + fid + + + + + + + + + false + None + + + + + + 2 + MU + true + + + false + + false + + + + + + + + m2 + meters + + 90 + false + + 8 + 5000 + + + + + + + 1 + + conditions unknown + + + + + false + + + + false + + GRS80 + + + + + + + + + + + + + + qgisce_catalog_autoload + qgisce_template_version + + + true + 1.0 + + + + + false + + false + + + 255 + 255 + 255 + 255 + 0 + 255 + 255 + + + 0 + 16 + false + false + true + 30 + 50 + false + true + + + + true + + 1 + + + + + false + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + Alessandro Pasotti + 2018-07-06T13:56:35 + + + +