[FEATURE][API] Add new QgsFeatureSink subclass QgsRemappingProxyFeatureSink

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, by coercing geometries to the
format required by the destination sink, and by mapping field values from
the source to the destination.
This commit is contained in:
Nyall Dawson 2020-04-06 15:27:53 +10:00
parent 78c86ef6ca
commit 93f714d233
7 changed files with 559 additions and 2 deletions

View File

@ -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 *
************************************************************************/

View File

@ -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

View File

@ -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

View File

@ -1313,12 +1313,20 @@ json QgsGeometry::asJsonObject( int precision ) const
QVector<QgsGeometry> 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

View File

@ -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;
}

View File

@ -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

View File

@ -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()