Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[FEATURE][processing] Allow appending results to existing layers
When appending results, users are given a field mapping panel choice
to allow them to manually set how fields are mapped to the destination
layer's fields
  • Loading branch information
nyalldawson committed Apr 7, 2020
1 parent a3c7a05 commit 1e05545
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 0 deletions.
101 changes: 101 additions & 0 deletions src/gui/processing/qgsprocessingoutputdestinationwidget.cpp
Expand Up @@ -23,6 +23,10 @@
#include "qgsfileutils.h"
#include "qgsdatasourceuri.h"
#include "qgsencodingfiledialog.h"
#include "qgsdatasourceselectdialog.h"
#include "qgsprocessingcontext.h"
#include "qgsprocessingalgorithm.h"
#include "qgsfieldmappingwidget.h"
#include <QMenu>
#include <QFileDialog>
#include <QInputDialog>
Expand Down Expand Up @@ -78,6 +82,7 @@ bool QgsProcessingLayerOutputDestinationWidget::outputIsSkipped() const
void QgsProcessingLayerOutputDestinationWidget::setValue( const QVariant &value )
{
const bool prevSkip = outputIsSkipped();
mUseRemapping = false;
if ( !value.isValid() || ( value.type() == QVariant::String && value.toString().isEmpty() ) )
{
if ( mParameter->flags() & QgsProcessingParameterDefinition::FlagOptional )
Expand Down Expand Up @@ -108,6 +113,8 @@ void QgsProcessingLayerOutputDestinationWidget::setValue( const QVariant &value
if ( prev != QgsProcessingLayerOutputDestinationWidget::value() )
emit destinationChanged();
}
mUseRemapping = def.useRemapping();
mRemapDefinition = def.remappingDefinition();
mEncoding = def.createOptions.value( QStringLiteral( "fileEncoding" ) ).toString();
}
else
Expand Down Expand Up @@ -179,6 +186,8 @@ QVariant QgsProcessingLayerOutputDestinationWidget::value() const

QgsProcessingOutputLayerDefinition value( key );
value.createOptions.insert( QStringLiteral( "fileEncoding" ), mEncoding );
if ( mUseRemapping )
value.setRemappingDefinition( mRemapDefinition );
return value;
}

Expand Down Expand Up @@ -271,10 +280,28 @@ void QgsProcessingLayerOutputDestinationWidget::menuAboutToShow()
QAction *actionSaveToDatabase = new QAction( tr( "Save to Database Table…" ), this );
connect( actionSaveToDatabase, &QAction::triggered, this, &QgsProcessingLayerOutputDestinationWidget::saveToDatabase );
mMenu->addAction( actionSaveToDatabase );

if ( mParameter->algorithm() && dynamic_cast< const QgsProcessingParameterFeatureSink * >( mParameter )->supportsAppend() )
{
mMenu->addSeparator();
QAction *actionAppendToLayer = new QAction( tr( "Append to Layer…" ), this );
connect( actionAppendToLayer, &QAction::triggered, this, &QgsProcessingLayerOutputDestinationWidget::appendToLayer );
mMenu->addAction( actionAppendToLayer );
if ( mUseRemapping )
{
QAction *editMappingAction = new QAction( tr( "Edit Field Mapping…" ), this );
connect( editMappingAction, &QAction::triggered, this, [ = ]
{
setAppendDestination( value().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), mRemapDefinition.destinationFields() );
} );
mMenu->addAction( editMappingAction );
}
}
}

if ( mParameter->type() == QgsProcessingParameterFeatureSink::typeName() )
{
mMenu->addSeparator();
QAction *actionSetEncoding = new QAction( tr( "Change File Encoding (%1)…" ).arg( mEncoding ), this );
connect( actionSetEncoding, &QAction::triggered, this, &QgsProcessingLayerOutputDestinationWidget::selectEncoding );
mMenu->addAction( actionSetEncoding );
Expand All @@ -286,6 +313,7 @@ void QgsProcessingLayerOutputDestinationWidget::skipOutput()
leText->setPlaceholderText( tr( "[Skip output]" ) );
leText->clear();
mUseTemporary = false;
mUseRemapping = false;

emit skipOutputChanged( true );
emit destinationChanged();
Expand Down Expand Up @@ -313,6 +341,7 @@ void QgsProcessingLayerOutputDestinationWidget::saveToTemporary()
return;

mUseTemporary = true;
mUseRemapping = false;
if ( prevSkip )
emit skipOutputChanged( false );
emit destinationChanged();
Expand All @@ -331,6 +360,7 @@ void QgsProcessingLayerOutputDestinationWidget::selectDirectory()
leText->setText( QDir::toNativeSeparators( dirName ) );
settings.setValue( QStringLiteral( "/Processing/LastOutputPath" ), dirName );
mUseTemporary = false;
mUseRemapping = false;
emit skipOutputChanged( false );
emit destinationChanged();
}
Expand Down Expand Up @@ -377,6 +407,7 @@ void QgsProcessingLayerOutputDestinationWidget::selectFile()
if ( !filename.isEmpty() )
{
mUseTemporary = false;
mUseRemapping = false;
filename = QgsFileUtils::addExtensionFromFilter( filename, lastFilter );

leText->setText( filename );
Expand Down Expand Up @@ -406,6 +437,7 @@ void QgsProcessingLayerOutputDestinationWidget::saveToGeopackage()
return;

mUseTemporary = false;
mUseRemapping = false;

filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << QStringLiteral( "gpkg" ) );

Expand Down Expand Up @@ -446,6 +478,7 @@ void QgsProcessingLayerOutputDestinationWidget::saveToDatabase()
auto changed = [ = ]
{
mUseTemporary = false;
mUseRemapping = false;

QString geomColumn;
if ( const QgsProcessingParameterFeatureSink *sink = dynamic_cast< const QgsProcessingParameterFeatureSink * >( mParameter ) )
Expand Down Expand Up @@ -485,6 +518,72 @@ void QgsProcessingLayerOutputDestinationWidget::saveToDatabase()
}
}

void QgsProcessingLayerOutputDestinationWidget::appendToLayer()
{
if ( QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( this ) )
{
QgsDataSourceSelectWidget *widget = new QgsDataSourceSelectWidget( mBrowserModel, true, QgsMapLayerType::VectorLayer );
widget->setPanelTitle( tr( "Append \"%1\" to Layer" ).arg( mParameter->description() ) );

panel->openPanel( widget );

connect( widget, &QgsDataSourceSelectWidget::itemTriggered, this, [ = ]( const QgsMimeDataUtils::Uri & )
{
widget->acceptPanel();
} );
connect( widget, &QgsPanelWidget::panelAccepted, this, [ = ]()
{
if ( widget->uri().uri.isEmpty() )
setValue( QVariant() );
else
{
// get fields for destination
std::unique_ptr< QgsVectorLayer > dest = qgis::make_unique< QgsVectorLayer >( widget->uri().uri, QString(), widget->uri().providerKey );
if ( widget->uri().providerKey == QLatin1String( "ogr" ) )
setAppendDestination( widget->uri().uri, dest->fields() );
else
setAppendDestination( QgsProcessingUtils::encodeProviderKeyAndUri( widget->uri().providerKey, widget->uri().uri ), dest->fields() );
}
} );
}
}


void QgsProcessingLayerOutputDestinationWidget::setAppendDestination( const QString &uri, const QgsFields &destFields )
{
const QgsProcessingAlgorithm *alg = mParameter->algorithm();
QVariantMap props;
if ( mParametersGenerator )
props = mParametersGenerator->createProcessingParameters();
props.insert( mParameter->name(), uri );

const QgsProcessingAlgorithm::VectorProperties outputProps = alg->sinkProperties( mParameter->name(), props, *mContext, QMap<QString, QgsProcessingAlgorithm::VectorProperties >() );
if ( outputProps.availability == QgsProcessingAlgorithm::Available )
{
if ( QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( this ) )
{
// get mapping from fields output by algorithm to destination fields
QgsFieldMappingWidget *widget = new QgsFieldMappingWidget( nullptr, outputProps.fields, destFields );
widget->setPanelTitle( tr( "Append \"%1\" to Layer" ).arg( mParameter->description() ) );
if ( !mRemapDefinition.fieldMap().isEmpty() )
widget->setFieldPropertyMap( mRemapDefinition.fieldMap() );

panel->openPanel( widget );

connect( widget, &QgsPanelWidget::panelAccepted, this, [ = ]()
{
QgsProcessingOutputLayerDefinition def( uri );
QgsRemappingSinkDefinition remap;
remap.setSourceCrs( outputProps.crs );
remap.setFieldMap( widget->fieldPropertyMap() );
remap.setDestinationFields( destFields );
def.setRemappingDefinition( remap );
setValue( def );
} );
}
}
}

void QgsProcessingLayerOutputDestinationWidget::selectEncoding()
{
QgsEncodingSelectionDialog dialog( this, tr( "File encoding" ), mEncoding );
Expand All @@ -500,9 +599,11 @@ void QgsProcessingLayerOutputDestinationWidget::selectEncoding()
void QgsProcessingLayerOutputDestinationWidget::textChanged( const QString &text )
{
mUseTemporary = text.isEmpty();
mUseRemapping = false;
emit destinationChanged();
}


QString QgsProcessingLayerOutputDestinationWidget::mimeDataToPath( const QMimeData *data )
{
const QgsMimeDataUtils::UriList uriList = QgsMimeDataUtils::decodeUriList( data );
Expand Down
7 changes: 7 additions & 0 deletions src/gui/processing/qgsprocessingoutputdestinationwidget.h
Expand Up @@ -20,6 +20,7 @@
#include "qgis_gui.h"
#include "ui_qgsprocessingdestinationwidgetbase.h"
#include "qgsprocessingwidgetwrapper.h"
#include "qgsprocessingcontext.h"
#include <QWidget>

class QgsProcessingDestinationParameter;
Expand Down Expand Up @@ -115,11 +116,14 @@ class GUI_EXPORT QgsProcessingLayerOutputDestinationWidget : public QWidget, pri
void selectFile();
void saveToGeopackage();
void saveToDatabase();
void appendToLayer();
void selectEncoding();
void textChanged( const QString &text );

private:

void setAppendDestination( const QString &uri, const QgsFields &destFields );

QString mimeDataToPath( const QMimeData *data );

const QgsProcessingDestinationParameter *mParameter = nullptr;
Expand All @@ -132,6 +136,9 @@ class GUI_EXPORT QgsProcessingLayerOutputDestinationWidget : public QWidget, pri
QgsBrowserGuiModel *mBrowserModel = nullptr;
QCheckBox *mOpenAfterRunningCheck = nullptr;

QgsRemappingSinkDefinition mRemapDefinition;
bool mUseRemapping = false;

QgsProcessingContext *mContext = nullptr;

friend class TestProcessingGui;
Expand Down
22 changes: 22 additions & 0 deletions tests/src/gui/testprocessinggui.cpp
Expand Up @@ -7349,6 +7349,28 @@ void TestProcessingGui::testOutputDefinitionWidget()
panel3.setValue( QgsProcessing::TEMPORARY_OUTPUT );
QCOMPARE( skipSpy3.count(), 3 );
QCOMPARE( changedSpy3.count(), 3 );

// with remapping
def = QgsProcessingOutputLayerDefinition( QStringLiteral( "test.shp" ) );
QgsRemappingSinkDefinition remap;
QMap< QString, QgsProperty > fieldMap;
fieldMap.insert( QStringLiteral( "field1" ), QgsProperty::fromField( QStringLiteral( "source1" ) ) );
fieldMap.insert( QStringLiteral( "field2" ), QgsProperty::fromExpression( QStringLiteral( "source || source2" ) ) );
remap.setFieldMap( fieldMap );
def.setRemappingDefinition( remap );

panel3.setValue( def );
v = panel3.value();
QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() );
QVERIFY( v.value< QgsProcessingOutputLayerDefinition>().useRemapping() );
QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().remappingDefinition().fieldMap().size(), 2 );
QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().remappingDefinition().fieldMap().value( QStringLiteral( "field1" ) ), QgsProperty::fromField( QStringLiteral( "source1" ) ) );
QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().remappingDefinition().fieldMap().value( QStringLiteral( "field2" ) ), QgsProperty::fromExpression( QStringLiteral( "source || source2" ) ) );

panel3.setValue( QStringLiteral( "other.shp" ) );
v = panel3.value();
QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() );
QVERIFY( !v.value< QgsProcessingOutputLayerDefinition>().useRemapping() );
}

void TestProcessingGui::testOutputDefinitionWidgetVectorOut()
Expand Down

0 comments on commit 1e05545

Please sign in to comment.