diff --git a/python/core/auto_generated/qgsremappingproxyfeaturesink.sip.in b/python/core/auto_generated/qgsremappingproxyfeaturesink.sip.in new file mode 100644 index 00000000000..45e1bb454ea --- /dev/null +++ b/python/core/auto_generated/qgsremappingproxyfeaturesink.sip.in @@ -0,0 +1,170 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgsremappingproxyfeaturesink.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsRemappingSinkDefinition +{ +%Docstring +Defines the parameters used to remap features when creating a QgsRemappingProxyFeatureSink. + +The definition includes parameters required to correctly map incoming features to the structure +of the destination sink, e.g. information about how to create output field values and how to transform +geometries to match the destination CRS. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsremappingproxyfeaturesink.h" +%End + public: + + QMap< QString, QgsProperty > fieldMap() const; +%Docstring +Returns the field mapping, which defines how to map the values from incoming features to destination +field values. + +Field values are mapped using a QgsProperty source object, which allows either direct field value to field value +mapping or use of QgsExpression expressions to transform values to the destination field. + +.. seealso:: :py:func:`setFieldMap` + +.. seealso:: :py:func:`addMappedField` +%End + + void setFieldMap( const QMap< QString, QgsProperty > &map ); +%Docstring +Sets the field mapping, which defines how to map the values from incoming features to destination +field values. + +Field values are mapped using a QgsProperty source object, which allows either direct field value to field value +mapping or use of QgsExpression expressions to transform values to the destination field. + +.. seealso:: :py:func:`fieldMap` + +.. seealso:: :py:func:`addMappedField` +%End + + void addMappedField( const QString &destinationField, const QgsProperty &property ); +%Docstring +Adds a mapping for a destination field. + +Field values are mapped using a QgsProperty source object, which allows either direct field value to field value +mapping or use of QgsExpression expressions to transform values to the destination field. + +.. seealso:: :py:func:`setFieldMap` + +.. seealso:: :py:func:`fieldMap` +%End + + QgsCoordinateTransform transform() const; +%Docstring +Returns the transform used for reprojecting incoming features to the sink's destination CRS. + +.. seealso:: :py:func:`setTransform` +%End + + void setTransform( const QgsCoordinateTransform &transform ); +%Docstring +Sets the ``transform`` used for reprojecting incoming features to the sink's destination CRS. + +.. seealso:: :py:func:`transform` +%End + + QgsWkbTypes::Type destinationWkbType() const; +%Docstring +Returns the WKB geometry type for the destination. + +.. seealso:: :py:func:`setDestinationWkbType` +%End + + void setDestinationWkbType( QgsWkbTypes::Type type ); +%Docstring +Sets the WKB geometry ``type`` for the destination. + +.. seealso:: :py:func:`setDestinationWkbType` +%End + + QgsFields destinationFields() const; +%Docstring +Returns the fields for the destination sink. + +.. seealso:: :py:func:`setDestinationFields` +%End + + void setDestinationFields( const QgsFields &fields ); +%Docstring +Sets the ``fields`` for the destination sink. + +.. seealso:: :py:func:`destinationFields` +%End + +}; + + +class QgsRemappingProxyFeatureSink : QgsFeatureSink +{ +%Docstring +A QgsFeatureSink which proxies incoming features to a destination feature sink, after applying +transformations and field value mappings. + +This sink allows for transformation of incoming features to match the requirements of storing +in an existing destination layer, e.g. by reprojecting the features to the destination's CRS +and by coercing geometries to the format required by the destination sink. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsremappingproxyfeaturesink.h" +%End + public: + + QgsRemappingProxyFeatureSink( const QgsRemappingSinkDefinition &mappingDefinition, QgsFeatureSink *sink ); +%Docstring +Constructor for QgsRemappingProxyFeatureSink, using the specified ``mappingDefinition`` +to manipulate features before sending them to the destination ``sink``. +%End + + void setExpressionContext( const QgsExpressionContext &context ); +%Docstring +Sets the expression ``context`` to use when evaluating mapped field values. +%End + + QgsFeatureList remapFeature( const QgsFeature &feature ) const; +%Docstring +Remaps a ``feature`` to a set of features compatible with the destination sink. +%End + + virtual bool addFeature( QgsFeature &feature, QgsFeatureSink::Flags flags = 0 ); + + virtual bool addFeatures( QgsFeatureList &features, QgsFeatureSink::Flags flags = 0 ); + + virtual bool addFeatures( QgsFeatureIterator &iterator, QgsFeatureSink::Flags flags = 0 ); + + + QgsFeatureSink *destinationSink(); +%Docstring +Returns the destination QgsFeatureSink which the proxy will forward features to. +%End + +}; + + + + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgsremappingproxyfeaturesink.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index bce3c9b0551..c9f6be29713 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -175,6 +175,7 @@ %Include auto_generated/qgsreadwritelocker.sip %Include auto_generated/qgsrelation.sip %Include auto_generated/qgsrelationcontext.sip +%Include auto_generated/qgsremappingproxyfeaturesink.sip %Include auto_generated/qgsrelationmanager.sip %Include auto_generated/qgsrenderchecker.sip %Include auto_generated/qgsrendercontext.sip diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 77228b40f56..8b664ce9d2f 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -369,6 +369,7 @@ SET(QGIS_CORE_SRCS qgsrelationcontext.cpp qgsweakrelation.cpp qgsrelationmanager.cpp + qgsremappingproxyfeaturesink.cpp qgsrenderchecker.cpp qgsrendercontext.cpp qgsrunprocess.cpp @@ -910,6 +911,7 @@ SET(QGIS_CORE_HDRS qgsreadwritelocker.h qgsrelation.h qgsrelationcontext.h + qgsremappingproxyfeaturesink.h qgsweakrelation.h qgsrelationmanager.h qgsrenderchecker.h diff --git a/src/core/geometry/qgsgeometry.cpp b/src/core/geometry/qgsgeometry.cpp index d6e1991dff8..ffa0fb02b13 100644 --- a/src/core/geometry/qgsgeometry.cpp +++ b/src/core/geometry/qgsgeometry.cpp @@ -1313,12 +1313,20 @@ json QgsGeometry::asJsonObject( int precision ) const QVector QgsGeometry::coerceToType( const QgsWkbTypes::Type type ) const { QVector< QgsGeometry > res; - if ( wkbType() == type ) + if ( isNull() ) + return res; + + if ( wkbType() == type || type == QgsWkbTypes::Unknown ) { res << *this; return res; } + if ( type == QgsWkbTypes::NoGeometry ) + { + return res; + } + QgsGeometry newGeom = *this; // Curved -> straight diff --git a/src/core/qgsremappingproxyfeaturesink.cpp b/src/core/qgsremappingproxyfeaturesink.cpp new file mode 100644 index 00000000000..b056084362a --- /dev/null +++ b/src/core/qgsremappingproxyfeaturesink.cpp @@ -0,0 +1,119 @@ +/*************************************************************************** + qgsremappingproxyfeaturesink.cpp + ---------------------- + begin : April 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsremappingproxyfeaturesink.h" +#include "qgslogger.h" + +QgsRemappingProxyFeatureSink::QgsRemappingProxyFeatureSink( const QgsRemappingSinkDefinition &mappingDefinition, QgsFeatureSink *sink ) + : QgsFeatureSink() + , mDefinition( mappingDefinition ) + , mSink( sink ) +{} + +void QgsRemappingProxyFeatureSink::setExpressionContext( const QgsExpressionContext &context ) +{ + mContext = context; +} + +QgsFeatureList QgsRemappingProxyFeatureSink::remapFeature( const QgsFeature &feature ) const +{ + QgsFeatureList res; + + mContext.setFeature( feature ); + + // remap fields first + QgsFeature f; + f.setFields( mDefinition.destinationFields(), true ); + QgsAttributes attributes; + const QMap< QString, QgsProperty > fieldMap = mDefinition.fieldMap(); + for ( const QgsField &field : mDefinition.destinationFields() ) + { + if ( fieldMap.contains( field.name() ) ) + { + attributes.append( fieldMap.value( field.name() ).value( mContext ) ); + } + else + { + attributes.append( QVariant() ); + } + } + f.setAttributes( attributes ); + + // make geometries compatible, and reproject if necessary + if ( feature.hasGeometry() ) + { + const QVector< QgsGeometry > geometries = feature.geometry().coerceToType( mDefinition.destinationWkbType() ); + if ( !geometries.isEmpty() ) + { + res.reserve( geometries.size() ); + for ( const QgsGeometry &geometry : geometries ) + { + QgsFeature featurePart = f; + + QgsGeometry reproject = geometry; + try + { + reproject.transform( mDefinition.transform() ); + featurePart.setGeometry( reproject ); + } + catch ( QgsCsException & ) + { + QgsLogger::warning( QObject::tr( "Error reprojecting feature geometry" ) ); + featurePart.clearGeometry(); + } + res << featurePart; + } + } + else + { + f.clearGeometry(); + res << f; + } + } + else + { + res << f; + } + return res; +} + +bool QgsRemappingProxyFeatureSink::addFeature( QgsFeature &feature, QgsFeatureSink::Flags flags ) +{ + QgsFeatureList features = remapFeature( feature ); + return mSink->addFeatures( features, flags ); +} + +bool QgsRemappingProxyFeatureSink::addFeatures( QgsFeatureList &features, QgsFeatureSink::Flags flags ) +{ + bool res = true; + for ( QgsFeature &f : features ) + { + res = addFeature( f, flags ) && res; + } + return res; +} + +bool QgsRemappingProxyFeatureSink::addFeatures( QgsFeatureIterator &iterator, QgsFeatureSink::Flags flags ) +{ + QgsFeature f; + bool res = true; + while ( iterator.nextFeature( f ) ) + { + res = addFeature( f, flags ) && res; + } + return res; +} diff --git a/src/core/qgsremappingproxyfeaturesink.h b/src/core/qgsremappingproxyfeaturesink.h new file mode 100644 index 00000000000..20c9563edc9 --- /dev/null +++ b/src/core/qgsremappingproxyfeaturesink.h @@ -0,0 +1,183 @@ +/*************************************************************************** + qgsremappingproxyfeaturesink.h + ---------------------- + begin : April 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSREMAPPINGPROXYFEATURESINK_H +#define QGSREMAPPINGPROXYFEATURESINK_H + +#include "qgis_core.h" +#include "qgis.h" +#include "qgsfeaturesink.h" +#include "qgsproperty.h" + +/** + * \class QgsRemappingSinkDefinition + * \ingroup core + * Defines the parameters used to remap features when creating a QgsRemappingProxyFeatureSink. + * + * The definition includes parameters required to correctly map incoming features to the structure + * of the destination sink, e.g. information about how to create output field values and how to transform + * geometries to match the destination CRS. + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsRemappingSinkDefinition +{ + public: + + /** + * Returns the field mapping, which defines how to map the values from incoming features to destination + * field values. + * + * Field values are mapped using a QgsProperty source object, which allows either direct field value to field value + * mapping or use of QgsExpression expressions to transform values to the destination field. + * + * \see setFieldMap() + * \see addMappedField() + */ + QMap< QString, QgsProperty > fieldMap() const { return mFieldMap; } + + /** + * Sets the field mapping, which defines how to map the values from incoming features to destination + * field values. + * + * Field values are mapped using a QgsProperty source object, which allows either direct field value to field value + * mapping or use of QgsExpression expressions to transform values to the destination field. + * + * \see fieldMap() + * \see addMappedField() + */ + void setFieldMap( const QMap< QString, QgsProperty > &map ) { mFieldMap = map; } + + /** + * Adds a mapping for a destination field. + * + * Field values are mapped using a QgsProperty source object, which allows either direct field value to field value + * mapping or use of QgsExpression expressions to transform values to the destination field. + * + * \see setFieldMap() + * \see fieldMap() + */ + void addMappedField( const QString &destinationField, const QgsProperty &property ) { mFieldMap.insert( destinationField, property ); } + + /** + * Returns the transform used for reprojecting incoming features to the sink's destination CRS. + * + * \see setTransform() + */ + QgsCoordinateTransform transform() const { return mTransform; } + + /** + * Sets the \a transform used for reprojecting incoming features to the sink's destination CRS. + * + * \see transform() + */ + void setTransform( const QgsCoordinateTransform &transform ) { mTransform = transform; } + + /** + * Returns the WKB geometry type for the destination. + * + * \see setDestinationWkbType() + */ + QgsWkbTypes::Type destinationWkbType() const { return mDestinationWkbType; } + + /** + * Sets the WKB geometry \a type for the destination. + * + * \see setDestinationWkbType() + */ + void setDestinationWkbType( QgsWkbTypes::Type type ) { mDestinationWkbType = type; } + + /** + * Returns the fields for the destination sink. + * + * \see setDestinationFields() + */ + QgsFields destinationFields() const { return mDestinationFields; } + + /** + * Sets the \a fields for the destination sink. + * + * \see destinationFields() + */ + void setDestinationFields( const QgsFields &fields ) { mDestinationFields = fields; } + + private: + + QMap< QString, QgsProperty > mFieldMap; + + QgsCoordinateTransform mTransform; + + QgsWkbTypes::Type mDestinationWkbType = QgsWkbTypes::Unknown; + + QgsFields mDestinationFields; + +}; + + +/** + * \class QgsRemappingProxyFeatureSink + * \ingroup core + * A QgsFeatureSink which proxies incoming features to a destination feature sink, after applying + * transformations and field value mappings. + * + * This sink allows for transformation of incoming features to match the requirements of storing + * in an existing destination layer, e.g. by reprojecting the features to the destination's CRS + * and by coercing geometries to the format required by the destination sink. + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsRemappingProxyFeatureSink : public QgsFeatureSink +{ + public: + + /** + * Constructor for QgsRemappingProxyFeatureSink, using the specified \a mappingDefinition + * to manipulate features before sending them to the destination \a sink. + */ + QgsRemappingProxyFeatureSink( const QgsRemappingSinkDefinition &mappingDefinition, QgsFeatureSink *sink ); + + /** + * Sets the expression \a context to use when evaluating mapped field values. + */ + void setExpressionContext( const QgsExpressionContext &context ); + + /** + * Remaps a \a feature to a set of features compatible with the destination sink. + */ + QgsFeatureList remapFeature( const QgsFeature &feature ) const; + + bool addFeature( QgsFeature &feature, QgsFeatureSink::Flags flags = nullptr ) override; + bool addFeatures( QgsFeatureList &features, QgsFeatureSink::Flags flags = nullptr ) override; + bool addFeatures( QgsFeatureIterator &iterator, QgsFeatureSink::Flags flags = nullptr ) override; + + /** + * Returns the destination QgsFeatureSink which the proxy will forward features to. + */ + QgsFeatureSink *destinationSink() { return mSink; } + + private: + + QgsRemappingSinkDefinition mDefinition; + QgsFeatureSink *mSink = nullptr; + mutable QgsExpressionContext mContext; +}; + +#endif // QGSREMAPPINGPROXYFEATURESINK_H + + + + diff --git a/tests/src/python/test_qgsfeaturesink.py b/tests/src/python/test_qgsfeaturesink.py index 924aec6e9b3..b35081b52b7 100644 --- a/tests/src/python/test_qgsfeaturesink.py +++ b/tests/src/python/test_qgsfeaturesink.py @@ -21,7 +21,16 @@ from qgis.core import (QgsFeatureStore, QgsField, QgsFields, QgsCoordinateReferenceSystem, - QgsProxyFeatureSink) + QgsProxyFeatureSink, + QgsRemappingProxyFeatureSink, + QgsRemappingSinkDefinition, + QgsWkbTypes, + QgsCoordinateTransform, + QgsProject, + QgsProperty, + QgsExpressionContext, + QgsExpressionContextScope + ) from qgis.PyQt.QtCore import QVariant from qgis.testing import start_app, unittest start_app() @@ -94,6 +103,71 @@ class TestQgsFeatureSink(unittest.TestCase): self.assertEqual(store.features()[1]['fldtxt'], 'test2') self.assertEqual(store.features()[2]['fldtxt'], 'test3') + def testRemappingSink(self): + """ + Test remapping features + """ + fields = QgsFields() + fields.append(QgsField('fldtxt', QVariant.String)) + fields.append(QgsField('fldint', QVariant.Int)) + fields.append(QgsField('fldtxt2', QVariant.String)) + + store = QgsFeatureStore(fields, QgsCoordinateReferenceSystem('EPSG:3857')) + + mapping_def = QgsRemappingSinkDefinition() + mapping_def.setDestinationWkbType(QgsWkbTypes.Point) + self.assertEqual(mapping_def.destinationWkbType(), QgsWkbTypes.Point) + mapping_def.setTransform(QgsCoordinateTransform(QgsCoordinateReferenceSystem('EPSG:4326'), QgsCoordinateReferenceSystem('EPSG:3857'), QgsProject.instance())) + self.assertEqual(mapping_def.transform().sourceCrs().authid(), 'EPSG:4326') + self.assertEqual(mapping_def.transform().destinationCrs().authid(), 'EPSG:3857') + mapping_def.setDestinationFields(fields) + self.assertEqual(mapping_def.destinationFields(), fields) + mapping_def.addMappedField('fldtxt2', QgsProperty.fromField('fld1')) + mapping_def.addMappedField('fldint', QgsProperty.fromExpression('@myval * fldint')) + + self.assertEqual(mapping_def.fieldMap()['fldtxt2'].field(), 'fld1') + self.assertEqual(mapping_def.fieldMap()['fldint'].expressionString(), '@myval * fldint') + + proxy = QgsRemappingProxyFeatureSink(mapping_def, store) + self.assertEqual(proxy.destinationSink(), store) + + self.assertEqual(len(store), 0) + + incoming_fields = QgsFields() + incoming_fields.append(QgsField('fld1', QVariant.String)) + incoming_fields.append(QgsField('fldint', QVariant.Int)) + + context = QgsExpressionContext() + scope = QgsExpressionContextScope() + scope.setVariable('myval', 2) + context.appendScope(scope) + context.setFields(incoming_fields) + proxy.setExpressionContext(context) + + f = QgsFeature() + f.setFields(incoming_fields) + f.setAttributes(["test", 123]) + f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(1, 2))) + self.assertTrue(proxy.addFeature(f)) + self.assertEqual(len(store), 1) + self.assertEqual(store.features()[0].geometry().asWkt(1), 'Point (111319.5 222684.2)') + self.assertEqual(store.features()[0].attributes(), [None, 246, 'test']) + + f2 = QgsFeature() + f2.setAttributes(["test2", 457]) + f2.setGeometry(QgsGeometry.fromWkt('LineString( 1 1, 2 2)')) + f3 = QgsFeature() + f3.setAttributes(["test3", 888]) + f3.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(3, 4))) + self.assertTrue(proxy.addFeatures([f2, f3])) + self.assertEqual(len(store), 4) + self.assertEqual(store.features()[1].attributes(), [None, 914, 'test2']) + self.assertEqual(store.features()[2].attributes(), [None, 914, 'test2']) + self.assertEqual(store.features()[3].attributes(), [None, 1776, 'test3']) + self.assertEqual(store.features()[1].geometry().asWkt(1), 'Point (111319.5 111325.1)') + self.assertEqual(store.features()[2].geometry().asWkt(1), 'Point (222639 222684.2)') + self.assertEqual(store.features()[3].geometry().asWkt(1), 'Point (333958.5 445640.1)') + if __name__ == '__main__': unittest.main()