Skip to content

Commit 93f714d

Browse files
committedApr 7, 2020
[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.
1 parent 78c86ef commit 93f714d

File tree

7 files changed

+559
-2
lines changed

7 files changed

+559
-2
lines changed
 
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/************************************************************************
2+
* This file has been generated automatically from *
3+
* *
4+
* src/core/qgsremappingproxyfeaturesink.h *
5+
* *
6+
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
7+
************************************************************************/
8+
9+
10+
11+
12+
class QgsRemappingSinkDefinition
13+
{
14+
%Docstring
15+
Defines the parameters used to remap features when creating a QgsRemappingProxyFeatureSink.
16+
17+
The definition includes parameters required to correctly map incoming features to the structure
18+
of the destination sink, e.g. information about how to create output field values and how to transform
19+
geometries to match the destination CRS.
20+
21+
.. versionadded:: 3.14
22+
%End
23+
24+
%TypeHeaderCode
25+
#include "qgsremappingproxyfeaturesink.h"
26+
%End
27+
public:
28+
29+
QMap< QString, QgsProperty > fieldMap() const;
30+
%Docstring
31+
Returns the field mapping, which defines how to map the values from incoming features to destination
32+
field values.
33+
34+
Field values are mapped using a QgsProperty source object, which allows either direct field value to field value
35+
mapping or use of QgsExpression expressions to transform values to the destination field.
36+
37+
.. seealso:: :py:func:`setFieldMap`
38+
39+
.. seealso:: :py:func:`addMappedField`
40+
%End
41+
42+
void setFieldMap( const QMap< QString, QgsProperty > &map );
43+
%Docstring
44+
Sets the field mapping, which defines how to map the values from incoming features to destination
45+
field values.
46+
47+
Field values are mapped using a QgsProperty source object, which allows either direct field value to field value
48+
mapping or use of QgsExpression expressions to transform values to the destination field.
49+
50+
.. seealso:: :py:func:`fieldMap`
51+
52+
.. seealso:: :py:func:`addMappedField`
53+
%End
54+
55+
void addMappedField( const QString &destinationField, const QgsProperty &property );
56+
%Docstring
57+
Adds a mapping for a destination field.
58+
59+
Field values are mapped using a QgsProperty source object, which allows either direct field value to field value
60+
mapping or use of QgsExpression expressions to transform values to the destination field.
61+
62+
.. seealso:: :py:func:`setFieldMap`
63+
64+
.. seealso:: :py:func:`fieldMap`
65+
%End
66+
67+
QgsCoordinateTransform transform() const;
68+
%Docstring
69+
Returns the transform used for reprojecting incoming features to the sink's destination CRS.
70+
71+
.. seealso:: :py:func:`setTransform`
72+
%End
73+
74+
void setTransform( const QgsCoordinateTransform &transform );
75+
%Docstring
76+
Sets the ``transform`` used for reprojecting incoming features to the sink's destination CRS.
77+
78+
.. seealso:: :py:func:`transform`
79+
%End
80+
81+
QgsWkbTypes::Type destinationWkbType() const;
82+
%Docstring
83+
Returns the WKB geometry type for the destination.
84+
85+
.. seealso:: :py:func:`setDestinationWkbType`
86+
%End
87+
88+
void setDestinationWkbType( QgsWkbTypes::Type type );
89+
%Docstring
90+
Sets the WKB geometry ``type`` for the destination.
91+
92+
.. seealso:: :py:func:`setDestinationWkbType`
93+
%End
94+
95+
QgsFields destinationFields() const;
96+
%Docstring
97+
Returns the fields for the destination sink.
98+
99+
.. seealso:: :py:func:`setDestinationFields`
100+
%End
101+
102+
void setDestinationFields( const QgsFields &fields );
103+
%Docstring
104+
Sets the ``fields`` for the destination sink.
105+
106+
.. seealso:: :py:func:`destinationFields`
107+
%End
108+
109+
};
110+
111+
112+
class QgsRemappingProxyFeatureSink : QgsFeatureSink
113+
{
114+
%Docstring
115+
A QgsFeatureSink which proxies incoming features to a destination feature sink, after applying
116+
transformations and field value mappings.
117+
118+
This sink allows for transformation of incoming features to match the requirements of storing
119+
in an existing destination layer, e.g. by reprojecting the features to the destination's CRS
120+
and by coercing geometries to the format required by the destination sink.
121+
122+
.. versionadded:: 3.14
123+
%End
124+
125+
%TypeHeaderCode
126+
#include "qgsremappingproxyfeaturesink.h"
127+
%End
128+
public:
129+
130+
QgsRemappingProxyFeatureSink( const QgsRemappingSinkDefinition &mappingDefinition, QgsFeatureSink *sink );
131+
%Docstring
132+
Constructor for QgsRemappingProxyFeatureSink, using the specified ``mappingDefinition``
133+
to manipulate features before sending them to the destination ``sink``.
134+
%End
135+
136+
void setExpressionContext( const QgsExpressionContext &context );
137+
%Docstring
138+
Sets the expression ``context`` to use when evaluating mapped field values.
139+
%End
140+
141+
QgsFeatureList remapFeature( const QgsFeature &feature ) const;
142+
%Docstring
143+
Remaps a ``feature`` to a set of features compatible with the destination sink.
144+
%End
145+
146+
virtual bool addFeature( QgsFeature &feature, QgsFeatureSink::Flags flags = 0 );
147+
148+
virtual bool addFeatures( QgsFeatureList &features, QgsFeatureSink::Flags flags = 0 );
149+
150+
virtual bool addFeatures( QgsFeatureIterator &iterator, QgsFeatureSink::Flags flags = 0 );
151+
152+
153+
QgsFeatureSink *destinationSink();
154+
%Docstring
155+
Returns the destination QgsFeatureSink which the proxy will forward features to.
156+
%End
157+
158+
};
159+
160+
161+
162+
163+
164+
/************************************************************************
165+
* This file has been generated automatically from *
166+
* *
167+
* src/core/qgsremappingproxyfeaturesink.h *
168+
* *
169+
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
170+
************************************************************************/

‎python/core/core_auto.sip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175
%Include auto_generated/qgsreadwritelocker.sip
176176
%Include auto_generated/qgsrelation.sip
177177
%Include auto_generated/qgsrelationcontext.sip
178+
%Include auto_generated/qgsremappingproxyfeaturesink.sip
178179
%Include auto_generated/qgsrelationmanager.sip
179180
%Include auto_generated/qgsrenderchecker.sip
180181
%Include auto_generated/qgsrendercontext.sip

‎src/core/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ SET(QGIS_CORE_SRCS
369369
qgsrelationcontext.cpp
370370
qgsweakrelation.cpp
371371
qgsrelationmanager.cpp
372+
qgsremappingproxyfeaturesink.cpp
372373
qgsrenderchecker.cpp
373374
qgsrendercontext.cpp
374375
qgsrunprocess.cpp
@@ -910,6 +911,7 @@ SET(QGIS_CORE_HDRS
910911
qgsreadwritelocker.h
911912
qgsrelation.h
912913
qgsrelationcontext.h
914+
qgsremappingproxyfeaturesink.h
913915
qgsweakrelation.h
914916
qgsrelationmanager.h
915917
qgsrenderchecker.h

‎src/core/geometry/qgsgeometry.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1313,12 +1313,20 @@ json QgsGeometry::asJsonObject( int precision ) const
13131313
QVector<QgsGeometry> QgsGeometry::coerceToType( const QgsWkbTypes::Type type ) const
13141314
{
13151315
QVector< QgsGeometry > res;
1316-
if ( wkbType() == type )
1316+
if ( isNull() )
1317+
return res;
1318+
1319+
if ( wkbType() == type || type == QgsWkbTypes::Unknown )
13171320
{
13181321
res << *this;
13191322
return res;
13201323
}
13211324

1325+
if ( type == QgsWkbTypes::NoGeometry )
1326+
{
1327+
return res;
1328+
}
1329+
13221330
QgsGeometry newGeom = *this;
13231331

13241332
// Curved -> straight
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/***************************************************************************
2+
qgsremappingproxyfeaturesink.cpp
3+
----------------------
4+
begin : April 2020
5+
copyright : (C) 2020 by Nyall Dawson
6+
email : nyall dot dawson at gmail dot com
7+
***************************************************************************/
8+
9+
/***************************************************************************
10+
* *
11+
* This program is free software; you can redistribute it and/or modify *
12+
* it under the terms of the GNU General Public License as published by *
13+
* the Free Software Foundation; either version 2 of the License, or *
14+
* (at your option) any later version. *
15+
* *
16+
***************************************************************************/
17+
18+
#include "qgsremappingproxyfeaturesink.h"
19+
#include "qgslogger.h"
20+
21+
QgsRemappingProxyFeatureSink::QgsRemappingProxyFeatureSink( const QgsRemappingSinkDefinition &mappingDefinition, QgsFeatureSink *sink )
22+
: QgsFeatureSink()
23+
, mDefinition( mappingDefinition )
24+
, mSink( sink )
25+
{}
26+
27+
void QgsRemappingProxyFeatureSink::setExpressionContext( const QgsExpressionContext &context )
28+
{
29+
mContext = context;
30+
}
31+
32+
QgsFeatureList QgsRemappingProxyFeatureSink::remapFeature( const QgsFeature &feature ) const
33+
{
34+
QgsFeatureList res;
35+
36+
mContext.setFeature( feature );
37+
38+
// remap fields first
39+
QgsFeature f;
40+
f.setFields( mDefinition.destinationFields(), true );
41+
QgsAttributes attributes;
42+
const QMap< QString, QgsProperty > fieldMap = mDefinition.fieldMap();
43+
for ( const QgsField &field : mDefinition.destinationFields() )
44+
{
45+
if ( fieldMap.contains( field.name() ) )
46+
{
47+
attributes.append( fieldMap.value( field.name() ).value( mContext ) );
48+
}
49+
else
50+
{
51+
attributes.append( QVariant() );
52+
}
53+
}
54+
f.setAttributes( attributes );
55+
56+
// make geometries compatible, and reproject if necessary
57+
if ( feature.hasGeometry() )
58+
{
59+
const QVector< QgsGeometry > geometries = feature.geometry().coerceToType( mDefinition.destinationWkbType() );
60+
if ( !geometries.isEmpty() )
61+
{
62+
res.reserve( geometries.size() );
63+
for ( const QgsGeometry &geometry : geometries )
64+
{
65+
QgsFeature featurePart = f;
66+
67+
QgsGeometry reproject = geometry;
68+
try
69+
{
70+
reproject.transform( mDefinition.transform() );
71+
featurePart.setGeometry( reproject );
72+
}
73+
catch ( QgsCsException & )
74+
{
75+
QgsLogger::warning( QObject::tr( "Error reprojecting feature geometry" ) );
76+
featurePart.clearGeometry();
77+
}
78+
res << featurePart;
79+
}
80+
}
81+
else
82+
{
83+
f.clearGeometry();
84+
res << f;
85+
}
86+
}
87+
else
88+
{
89+
res << f;
90+
}
91+
return res;
92+
}
93+
94+
bool QgsRemappingProxyFeatureSink::addFeature( QgsFeature &feature, QgsFeatureSink::Flags flags )
95+
{
96+
QgsFeatureList features = remapFeature( feature );
97+
return mSink->addFeatures( features, flags );
98+
}
99+
100+
bool QgsRemappingProxyFeatureSink::addFeatures( QgsFeatureList &features, QgsFeatureSink::Flags flags )
101+
{
102+
bool res = true;
103+
for ( QgsFeature &f : features )
104+
{
105+
res = addFeature( f, flags ) && res;
106+
}
107+
return res;
108+
}
109+
110+
bool QgsRemappingProxyFeatureSink::addFeatures( QgsFeatureIterator &iterator, QgsFeatureSink::Flags flags )
111+
{
112+
QgsFeature f;
113+
bool res = true;
114+
while ( iterator.nextFeature( f ) )
115+
{
116+
res = addFeature( f, flags ) && res;
117+
}
118+
return res;
119+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/***************************************************************************
2+
qgsremappingproxyfeaturesink.h
3+
----------------------
4+
begin : April 2020
5+
copyright : (C) 2020 by Nyall Dawson
6+
email : nyall dot dawson at gmail dot com
7+
***************************************************************************/
8+
9+
/***************************************************************************
10+
* *
11+
* This program is free software; you can redistribute it and/or modify *
12+
* it under the terms of the GNU General Public License as published by *
13+
* the Free Software Foundation; either version 2 of the License, or *
14+
* (at your option) any later version. *
15+
* *
16+
***************************************************************************/
17+
18+
#ifndef QGSREMAPPINGPROXYFEATURESINK_H
19+
#define QGSREMAPPINGPROXYFEATURESINK_H
20+
21+
#include "qgis_core.h"
22+
#include "qgis.h"
23+
#include "qgsfeaturesink.h"
24+
#include "qgsproperty.h"
25+
26+
/**
27+
* \class QgsRemappingSinkDefinition
28+
* \ingroup core
29+
* Defines the parameters used to remap features when creating a QgsRemappingProxyFeatureSink.
30+
*
31+
* The definition includes parameters required to correctly map incoming features to the structure
32+
* of the destination sink, e.g. information about how to create output field values and how to transform
33+
* geometries to match the destination CRS.
34+
*
35+
* \since QGIS 3.14
36+
*/
37+
class CORE_EXPORT QgsRemappingSinkDefinition
38+
{
39+
public:
40+
41+
/**
42+
* Returns the field mapping, which defines how to map the values from incoming features to destination
43+
* field values.
44+
*
45+
* Field values are mapped using a QgsProperty source object, which allows either direct field value to field value
46+
* mapping or use of QgsExpression expressions to transform values to the destination field.
47+
*
48+
* \see setFieldMap()
49+
* \see addMappedField()
50+
*/
51+
QMap< QString, QgsProperty > fieldMap() const { return mFieldMap; }
52+
53+
/**
54+
* Sets the field mapping, which defines how to map the values from incoming features to destination
55+
* field values.
56+
*
57+
* Field values are mapped using a QgsProperty source object, which allows either direct field value to field value
58+
* mapping or use of QgsExpression expressions to transform values to the destination field.
59+
*
60+
* \see fieldMap()
61+
* \see addMappedField()
62+
*/
63+
void setFieldMap( const QMap< QString, QgsProperty > &map ) { mFieldMap = map; }
64+
65+
/**
66+
* Adds a mapping for a destination field.
67+
*
68+
* Field values are mapped using a QgsProperty source object, which allows either direct field value to field value
69+
* mapping or use of QgsExpression expressions to transform values to the destination field.
70+
*
71+
* \see setFieldMap()
72+
* \see fieldMap()
73+
*/
74+
void addMappedField( const QString &destinationField, const QgsProperty &property ) { mFieldMap.insert( destinationField, property ); }
75+
76+
/**
77+
* Returns the transform used for reprojecting incoming features to the sink's destination CRS.
78+
*
79+
* \see setTransform()
80+
*/
81+
QgsCoordinateTransform transform() const { return mTransform; }
82+
83+
/**
84+
* Sets the \a transform used for reprojecting incoming features to the sink's destination CRS.
85+
*
86+
* \see transform()
87+
*/
88+
void setTransform( const QgsCoordinateTransform &transform ) { mTransform = transform; }
89+
90+
/**
91+
* Returns the WKB geometry type for the destination.
92+
*
93+
* \see setDestinationWkbType()
94+
*/
95+
QgsWkbTypes::Type destinationWkbType() const { return mDestinationWkbType; }
96+
97+
/**
98+
* Sets the WKB geometry \a type for the destination.
99+
*
100+
* \see setDestinationWkbType()
101+
*/
102+
void setDestinationWkbType( QgsWkbTypes::Type type ) { mDestinationWkbType = type; }
103+
104+
/**
105+
* Returns the fields for the destination sink.
106+
*
107+
* \see setDestinationFields()
108+
*/
109+
QgsFields destinationFields() const { return mDestinationFields; }
110+
111+
/**
112+
* Sets the \a fields for the destination sink.
113+
*
114+
* \see destinationFields()
115+
*/
116+
void setDestinationFields( const QgsFields &fields ) { mDestinationFields = fields; }
117+
118+
private:
119+
120+
QMap< QString, QgsProperty > mFieldMap;
121+
122+
QgsCoordinateTransform mTransform;
123+
124+
QgsWkbTypes::Type mDestinationWkbType = QgsWkbTypes::Unknown;
125+
126+
QgsFields mDestinationFields;
127+
128+
};
129+
130+
131+
/**
132+
* \class QgsRemappingProxyFeatureSink
133+
* \ingroup core
134+
* A QgsFeatureSink which proxies incoming features to a destination feature sink, after applying
135+
* transformations and field value mappings.
136+
*
137+
* This sink allows for transformation of incoming features to match the requirements of storing
138+
* in an existing destination layer, e.g. by reprojecting the features to the destination's CRS
139+
* and by coercing geometries to the format required by the destination sink.
140+
*
141+
* \since QGIS 3.14
142+
*/
143+
class CORE_EXPORT QgsRemappingProxyFeatureSink : public QgsFeatureSink
144+
{
145+
public:
146+
147+
/**
148+
* Constructor for QgsRemappingProxyFeatureSink, using the specified \a mappingDefinition
149+
* to manipulate features before sending them to the destination \a sink.
150+
*/
151+
QgsRemappingProxyFeatureSink( const QgsRemappingSinkDefinition &mappingDefinition, QgsFeatureSink *sink );
152+
153+
/**
154+
* Sets the expression \a context to use when evaluating mapped field values.
155+
*/
156+
void setExpressionContext( const QgsExpressionContext &context );
157+
158+
/**
159+
* Remaps a \a feature to a set of features compatible with the destination sink.
160+
*/
161+
QgsFeatureList remapFeature( const QgsFeature &feature ) const;
162+
163+
bool addFeature( QgsFeature &feature, QgsFeatureSink::Flags flags = nullptr ) override;
164+
bool addFeatures( QgsFeatureList &features, QgsFeatureSink::Flags flags = nullptr ) override;
165+
bool addFeatures( QgsFeatureIterator &iterator, QgsFeatureSink::Flags flags = nullptr ) override;
166+
167+
/**
168+
* Returns the destination QgsFeatureSink which the proxy will forward features to.
169+
*/
170+
QgsFeatureSink *destinationSink() { return mSink; }
171+
172+
private:
173+
174+
QgsRemappingSinkDefinition mDefinition;
175+
QgsFeatureSink *mSink = nullptr;
176+
mutable QgsExpressionContext mContext;
177+
};
178+
179+
#endif // QGSREMAPPINGPROXYFEATURESINK_H
180+
181+
182+
183+

‎tests/src/python/test_qgsfeaturesink.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,16 @@
2121
QgsField,
2222
QgsFields,
2323
QgsCoordinateReferenceSystem,
24-
QgsProxyFeatureSink)
24+
QgsProxyFeatureSink,
25+
QgsRemappingProxyFeatureSink,
26+
QgsRemappingSinkDefinition,
27+
QgsWkbTypes,
28+
QgsCoordinateTransform,
29+
QgsProject,
30+
QgsProperty,
31+
QgsExpressionContext,
32+
QgsExpressionContextScope
33+
)
2534
from qgis.PyQt.QtCore import QVariant
2635
from qgis.testing import start_app, unittest
2736
start_app()
@@ -94,6 +103,71 @@ def testProxyFeatureSink(self):
94103
self.assertEqual(store.features()[1]['fldtxt'], 'test2')
95104
self.assertEqual(store.features()[2]['fldtxt'], 'test3')
96105

106+
def testRemappingSink(self):
107+
"""
108+
Test remapping features
109+
"""
110+
fields = QgsFields()
111+
fields.append(QgsField('fldtxt', QVariant.String))
112+
fields.append(QgsField('fldint', QVariant.Int))
113+
fields.append(QgsField('fldtxt2', QVariant.String))
114+
115+
store = QgsFeatureStore(fields, QgsCoordinateReferenceSystem('EPSG:3857'))
116+
117+
mapping_def = QgsRemappingSinkDefinition()
118+
mapping_def.setDestinationWkbType(QgsWkbTypes.Point)
119+
self.assertEqual(mapping_def.destinationWkbType(), QgsWkbTypes.Point)
120+
mapping_def.setTransform(QgsCoordinateTransform(QgsCoordinateReferenceSystem('EPSG:4326'), QgsCoordinateReferenceSystem('EPSG:3857'), QgsProject.instance()))
121+
self.assertEqual(mapping_def.transform().sourceCrs().authid(), 'EPSG:4326')
122+
self.assertEqual(mapping_def.transform().destinationCrs().authid(), 'EPSG:3857')
123+
mapping_def.setDestinationFields(fields)
124+
self.assertEqual(mapping_def.destinationFields(), fields)
125+
mapping_def.addMappedField('fldtxt2', QgsProperty.fromField('fld1'))
126+
mapping_def.addMappedField('fldint', QgsProperty.fromExpression('@myval * fldint'))
127+
128+
self.assertEqual(mapping_def.fieldMap()['fldtxt2'].field(), 'fld1')
129+
self.assertEqual(mapping_def.fieldMap()['fldint'].expressionString(), '@myval * fldint')
130+
131+
proxy = QgsRemappingProxyFeatureSink(mapping_def, store)
132+
self.assertEqual(proxy.destinationSink(), store)
133+
134+
self.assertEqual(len(store), 0)
135+
136+
incoming_fields = QgsFields()
137+
incoming_fields.append(QgsField('fld1', QVariant.String))
138+
incoming_fields.append(QgsField('fldint', QVariant.Int))
139+
140+
context = QgsExpressionContext()
141+
scope = QgsExpressionContextScope()
142+
scope.setVariable('myval', 2)
143+
context.appendScope(scope)
144+
context.setFields(incoming_fields)
145+
proxy.setExpressionContext(context)
146+
147+
f = QgsFeature()
148+
f.setFields(incoming_fields)
149+
f.setAttributes(["test", 123])
150+
f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(1, 2)))
151+
self.assertTrue(proxy.addFeature(f))
152+
self.assertEqual(len(store), 1)
153+
self.assertEqual(store.features()[0].geometry().asWkt(1), 'Point (111319.5 222684.2)')
154+
self.assertEqual(store.features()[0].attributes(), [None, 246, 'test'])
155+
156+
f2 = QgsFeature()
157+
f2.setAttributes(["test2", 457])
158+
f2.setGeometry(QgsGeometry.fromWkt('LineString( 1 1, 2 2)'))
159+
f3 = QgsFeature()
160+
f3.setAttributes(["test3", 888])
161+
f3.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(3, 4)))
162+
self.assertTrue(proxy.addFeatures([f2, f3]))
163+
self.assertEqual(len(store), 4)
164+
self.assertEqual(store.features()[1].attributes(), [None, 914, 'test2'])
165+
self.assertEqual(store.features()[2].attributes(), [None, 914, 'test2'])
166+
self.assertEqual(store.features()[3].attributes(), [None, 1776, 'test3'])
167+
self.assertEqual(store.features()[1].geometry().asWkt(1), 'Point (111319.5 111325.1)')
168+
self.assertEqual(store.features()[2].geometry().asWkt(1), 'Point (222639 222684.2)')
169+
self.assertEqual(store.features()[3].geometry().asWkt(1), 'Point (333958.5 445640.1)')
170+
97171

98172
if __name__ == '__main__':
99173
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.