From 9e184feaedaff9930a7336fe7367a3a48b544fc0 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 27 Jun 2017 15:24:48 +1000 Subject: [PATCH] Add method to evaluate parameters to compatible vector layers of a specified type --- .../processing/qgsprocessingparameters.sip | 16 ++++ python/core/processing/qgsprocessingutils.sip | 21 +++++ .../processing/qgsprocessingparameters.cpp | 44 +++++++++++ src/core/processing/qgsprocessingparameters.h | 16 ++++ src/core/processing/qgsprocessingutils.cpp | 36 +++++++++ src/core/processing/qgsprocessingutils.h | 21 +++++ tests/src/core/testqgsprocessing.cpp | 76 +++++++++++++++++++ 7 files changed, 230 insertions(+) diff --git a/python/core/processing/qgsprocessingparameters.sip b/python/core/processing/qgsprocessingparameters.sip index 42f408ad01e..4b6355977ef 100644 --- a/python/core/processing/qgsprocessingparameters.sip +++ b/python/core/processing/qgsprocessingparameters.sip @@ -472,6 +472,22 @@ class QgsProcessingParameters :rtype: QgsProcessingFeatureSource %End + static QString parameterAsCompatibleSourceLayerPath( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, + QgsProcessingContext &context, const QStringList &compatibleFormats, const QString &preferredFormat = QString( "shp" ), QgsProcessingFeedback *feedback = 0 ); +%Docstring + Evaluates the parameter with matching ``definition`` to a source vector layer file path of compatible format. + + If the parameter is evaluated to an existing layer, and that layer is not of the format listed in the + ``compatibleFormats`` argument, then the layer will first be exported to a compatible format + in a temporary location. The function will then return the path to that temporary file. + + ``compatibleFormats`` should consist entirely of lowercase file extensions, e.g. 'shp'. + + The ``preferredFormat`` argument is used to specify to desired file extension to use when a temporary + layer export is required. This defaults to shapefiles, because shapefiles are the future (don't believe the geopackage hype!). + :rtype: str +%End + static QgsMapLayer *parameterAsLayer( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, QgsProcessingContext &context ); %Docstring Evaluates the parameter with matching ``definition`` to a map layer. diff --git a/python/core/processing/qgsprocessingutils.sip b/python/core/processing/qgsprocessingutils.sip index 73c4c2e9298..4c206caab36 100644 --- a/python/core/processing/qgsprocessingutils.sip +++ b/python/core/processing/qgsprocessingutils.sip @@ -158,6 +158,27 @@ class QgsProcessingUtils :rtype: str %End + static QString convertToCompatibleFormat( const QgsVectorLayer *layer, + bool selectedFeaturesOnly, + const QString &baseName, + const QStringList &compatibleFormats, + const QString &preferredFormat, + QgsProcessingContext &context, + QgsProcessingFeedback *feedback ); +%Docstring + Converts a source vector ``layer`` to a file path to a vector layer of compatible format. + + If the specified ``layer`` is not of the format listed in the + ``compatibleFormats`` argument, then the layer will first be exported to a compatible format + in a temporary location using ``baseName``. The function will then return the path to that temporary file. + + ``compatibleFormats`` should consist entirely of lowercase file extensions, e.g. 'shp'. + + The ``preferredFormat`` argument is used to specify to desired file extension to use when a temporary + layer export is required. This defaults to shapefiles. + :rtype: str +%End + }; class QgsProcessingFeatureSource : QgsFeatureSource diff --git a/src/core/processing/qgsprocessingparameters.cpp b/src/core/processing/qgsprocessingparameters.cpp index 625880636d5..196e4862c45 100644 --- a/src/core/processing/qgsprocessingparameters.cpp +++ b/src/core/processing/qgsprocessingparameters.cpp @@ -21,6 +21,7 @@ #include "qgsvectorlayerfeatureiterator.h" #include "qgsprocessingoutputs.h" #include "qgssettings.h" +#include "qgsvectorfilewriter.h" bool QgsProcessingParameters::isDynamic( const QVariantMap ¶meters, const QString &name ) { @@ -327,6 +328,49 @@ QgsProcessingFeatureSource *QgsProcessingParameters::parameterAsSource( const Qg } } +QString QgsProcessingParameters::parameterAsCompatibleSourceLayerPath( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, QgsProcessingContext &context, const QStringList &compatibleFormats, const QString &preferredFormat, QgsProcessingFeedback *feedback ) +{ + if ( !definition ) + return QString(); + + QVariant val = parameters.value( definition->name() ); + + bool selectedFeaturesOnly = false; + if ( val.canConvert() ) + { + // input is a QgsProcessingFeatureSourceDefinition - get extra properties from it + QgsProcessingFeatureSourceDefinition fromVar = qvariant_cast( val ); + selectedFeaturesOnly = fromVar.selectedFeaturesOnly; + val = fromVar.source; + } + + QString layerRef; + if ( val.canConvert() ) + { + layerRef = val.value< QgsProperty >().valueAsString( context.expressionContext(), definition->defaultValue().toString() ); + } + else if ( !val.isValid() || val.toString().isEmpty() ) + { + // fall back to default + layerRef = definition->defaultValue().toString(); + } + else + { + layerRef = val.toString(); + } + + if ( layerRef.isEmpty() ) + return QString(); + + QgsVectorLayer *vl = qobject_cast< QgsVectorLayer *>( QgsProcessingUtils::mapLayerFromString( layerRef, context ) ); + if ( !vl ) + return QString(); + + return QgsProcessingUtils::convertToCompatibleFormat( vl, selectedFeaturesOnly, definition->name(), + compatibleFormats, preferredFormat, context, feedback ); +} + + QgsMapLayer *QgsProcessingParameters::parameterAsLayer( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, QgsProcessingContext &context ) { if ( !definition ) diff --git a/src/core/processing/qgsprocessingparameters.h b/src/core/processing/qgsprocessingparameters.h index 5f4cab2a4b2..bc428577bf0 100644 --- a/src/core/processing/qgsprocessingparameters.h +++ b/src/core/processing/qgsprocessingparameters.h @@ -32,6 +32,7 @@ class QgsFeatureSink; class QgsFeatureSource; class QgsProcessingFeatureSource; class QgsProcessingOutputDefinition; +class QgsProcessingFeedback; /** * \class QgsProcessingFeatureSourceDefinition @@ -508,6 +509,21 @@ class CORE_EXPORT QgsProcessingParameters */ static QgsProcessingFeatureSource *parameterAsSource( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, QgsProcessingContext &context ) SIP_FACTORY; + /** + * Evaluates the parameter with matching \a definition to a source vector layer file path of compatible format. + * + * If the parameter is evaluated to an existing layer, and that layer is not of the format listed in the + * \a compatibleFormats argument, then the layer will first be exported to a compatible format + * in a temporary location. The function will then return the path to that temporary file. + * + * \a compatibleFormats should consist entirely of lowercase file extensions, e.g. 'shp'. + * + * The \a preferredFormat argument is used to specify to desired file extension to use when a temporary + * layer export is required. This defaults to shapefiles, because shapefiles are the future (don't believe the geopackage hype!). + */ + static QString parameterAsCompatibleSourceLayerPath( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, + QgsProcessingContext &context, const QStringList &compatibleFormats, const QString &preferredFormat = QString( "shp" ), QgsProcessingFeedback *feedback = nullptr ); + /** * Evaluates the parameter with matching \a definition to a map layer. * diff --git a/src/core/processing/qgsprocessingutils.cpp b/src/core/processing/qgsprocessingutils.cpp index ded185b973d..4efaa880176 100644 --- a/src/core/processing/qgsprocessingutils.cpp +++ b/src/core/processing/qgsprocessingutils.cpp @@ -461,6 +461,42 @@ QString QgsProcessingUtils::formatHelpMapAsHtml( const QVariantMap &map, const Q return s; } +QString QgsProcessingUtils::convertToCompatibleFormat( const QgsVectorLayer *vl, bool selectedFeaturesOnly, const QString &baseName, const QStringList &compatibleFormats, const QString &preferredFormat, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + bool requiresTranslation = selectedFeaturesOnly; + if ( !selectedFeaturesOnly ) + { + QFileInfo fi( vl->source() ); + requiresTranslation = !compatibleFormats.contains( fi.suffix(), Qt::CaseInsensitive ); + } + + if ( requiresTranslation ) + { + QString temp = QgsProcessingUtils::generateTempFilename( baseName + '.' + preferredFormat ); + + QgsVectorFileWriter writer( temp, context.defaultEncoding(), + vl->fields(), vl->wkbType(), vl->crs(), QgsVectorFileWriter::driverForExtension( preferredFormat ) ); + QgsFeature f; + QgsFeatureIterator it; + if ( selectedFeaturesOnly ) + it = vl->getSelectedFeatures(); + else + it = vl->getFeatures(); + + while ( it.nextFeature( f ) ) + { + if ( feedback->isCanceled() ) + return QString(); + writer.addFeature( f, QgsFeatureSink::FastInsert ); + } + return temp; + } + else + { + return vl->source(); + } +} + // // QgsProcessingFeatureSource diff --git a/src/core/processing/qgsprocessingutils.h b/src/core/processing/qgsprocessingutils.h index 0e023bbf218..11f6a0c609d 100644 --- a/src/core/processing/qgsprocessingutils.h +++ b/src/core/processing/qgsprocessingutils.h @@ -28,6 +28,7 @@ class QgsProject; class QgsProcessingContext; class QgsMapLayerStore; +class QgsProcessingFeedback; #include #include @@ -189,6 +190,26 @@ class CORE_EXPORT QgsProcessingUtils */ static QString formatHelpMapAsHtml( const QVariantMap &map, const QgsProcessingAlgorithm *algorithm ); + /** + * Converts a source vector \a layer to a file path to a vector layer of compatible format. + * + * If the specified \a layer is not of the format listed in the + * \a compatibleFormats argument, then the layer will first be exported to a compatible format + * in a temporary location using \a baseName. The function will then return the path to that temporary file. + * + * \a compatibleFormats should consist entirely of lowercase file extensions, e.g. 'shp'. + * + * The \a preferredFormat argument is used to specify to desired file extension to use when a temporary + * layer export is required. This defaults to shapefiles. + */ + static QString convertToCompatibleFormat( const QgsVectorLayer *layer, + bool selectedFeaturesOnly, + const QString &baseName, + const QStringList &compatibleFormats, + const QString &preferredFormat, + QgsProcessingContext &context, + QgsProcessingFeedback *feedback ); + private: static bool canUseLayer( const QgsRasterLayer *layer ); diff --git a/tests/src/core/testqgsprocessing.cpp b/tests/src/core/testqgsprocessing.cpp index 60a6de1d293..c11d20fce78 100644 --- a/tests/src/core/testqgsprocessing.cpp +++ b/tests/src/core/testqgsprocessing.cpp @@ -333,6 +333,7 @@ class TestQgsProcessing: public QObject void modelExecution(); void modelAcceptableValues(); void tempUtils(); + void convertCompatible(); private: @@ -5039,5 +5040,80 @@ void TestQgsProcessing::tempUtils() QVERIFY( tempFile2.startsWith( tempFolder ) ); } +void TestQgsProcessing::convertCompatible() +{ + // start with a compatible shapefile + QString testDataDir = QStringLiteral( TEST_DATA_DIR ) + '/'; //defined in CmakeLists.txt + QString vector = testDataDir + "points.shp"; + QgsVectorLayer *layer = new QgsVectorLayer( vector, "vl" ); + QgsProject p; + p.addMapLayer( layer ); + + QgsProcessingContext context; + context.setProject( &p ); + QgsProcessingFeedback feedback; + QString out = QgsProcessingUtils::convertToCompatibleFormat( layer, false, QStringLiteral( "test" ), QStringList() << "shp", QString( "shp" ), context, &feedback ); + // layer should be returned unchanged - underlying source is compatible + QCOMPARE( out, layer->source() ); + + // don't include shp as compatible type + out = QgsProcessingUtils::convertToCompatibleFormat( layer, false, QStringLiteral( "test" ), QStringList() << "tab", QString( "tab" ), context, &feedback ); + QVERIFY( out != layer->source() ); + QVERIFY( out.endsWith( ".tab" ) ); + QVERIFY( out.startsWith( QgsProcessingUtils::tempFolder() ) ); + + // make sure all features are copied + QgsVectorLayer *t = new QgsVectorLayer( out, "vl2" ); + QCOMPARE( layer->featureCount(), t->featureCount() ); + QCOMPARE( layer->crs(), t->crs() ); + + // use a selection - this will require translation + QgsFeatureIds ids; + QgsFeature f; + QgsFeatureIterator it = layer->getFeatures(); + it.nextFeature( f ); + ids.insert( f.id() ); + it.nextFeature( f ); + ids.insert( f.id() ); + + layer->selectByIds( ids ); + out = QgsProcessingUtils::convertToCompatibleFormat( layer, true, QStringLiteral( "test" ), QStringList() << "tab", QString( "tab" ), context, &feedback ); + QVERIFY( out != layer->source() ); + QVERIFY( out.endsWith( ".tab" ) ); + QVERIFY( out.startsWith( QgsProcessingUtils::tempFolder() ) ); + delete t; + t = new QgsVectorLayer( out, "vl2" ); + QCOMPARE( t->featureCount(), static_cast< long >( ids.count() ) ); + + // using a selection but existing format - will still require translation + out = QgsProcessingUtils::convertToCompatibleFormat( layer, true, QStringLiteral( "test" ), QStringList() << "shp", QString( "shp" ), context, &feedback ); + QVERIFY( out != layer->source() ); + QVERIFY( out.endsWith( ".shp" ) ); + QVERIFY( out.startsWith( QgsProcessingUtils::tempFolder() ) ); + delete t; + t = new QgsVectorLayer( out, "vl2" ); + QCOMPARE( t->featureCount(), static_cast< long >( ids.count() ) ); + + + // also test evaluating parameter to compatible format + std::unique_ptr< QgsProcessingParameterDefinition > def( new QgsProcessingParameterFeatureSource( QStringLiteral( "source" ) ) ); + QVariantMap params; + params.insert( QStringLiteral( "source" ), QgsProcessingFeatureSourceDefinition( layer->id(), false ) ); + out = QgsProcessingParameters::parameterAsCompatibleSourceLayerPath( def.get(), params, context, QStringList() << "shp", QString( "shp" ), &feedback ); + QCOMPARE( out, layer->source() ); + + out = QgsProcessingParameters::parameterAsCompatibleSourceLayerPath( def.get(), params, context, QStringList() << "tab", QString( "tab" ), &feedback ); + QVERIFY( out != layer->source() ); + QVERIFY( out.endsWith( ".tab" ) ); + QVERIFY( out.startsWith( QgsProcessingUtils::tempFolder() ) ); + + // selected only, will force export + params.insert( QStringLiteral( "source" ), QgsProcessingFeatureSourceDefinition( layer->id(), true ) ); + out = QgsProcessingParameters::parameterAsCompatibleSourceLayerPath( def.get(), params, context, QStringList() << "shp", QString( "shp" ), &feedback ); + QVERIFY( out != layer->source() ); + QVERIFY( out.endsWith( ".shp" ) ); + QVERIFY( out.startsWith( QgsProcessingUtils::tempFolder() ) ); +} + QGSTEST_MAIN( TestQgsProcessing ) #include "testqgsprocessing.moc"