diff --git a/python/core/qgsjsonutils.sip b/python/core/qgsjsonutils.sip index d7ada41ad47f..3a256d917100 100644 --- a/python/core/qgsjsonutils.sip +++ b/python/core/qgsjsonutils.sip @@ -12,11 +12,10 @@ class QgsJSONExporter public: /** Constructor for QgsJSONExporter. + * @param vectorLayer associated vector layer (required for related attribute export) * @param precision maximum number of decimal places to use for geometry coordinates - * @param includeGeometry set to false to avoid including the geometry representation in the JSON output - * @param includeAttributes set to false to avoid including any attribute values in the JSON output */ - QgsJSONExporter( int precision = 17, bool includeGeometry = true, bool includeAttributes = true ); + QgsJSONExporter( const QgsVectorLayer* vectorLayer = nullptr, int precision = 17 ); /** Sets the maximum number of decimal places to use in geometry coordinates. * @param precision number of decimal places @@ -51,6 +50,30 @@ class QgsJSONExporter */ bool includeAttributes() const; + /** Sets whether to include attributes of features linked via references in the JSON exports. + * @param includeRelated set to true to include attributes for any related child features + * within the exported properties element. + * @note associated vector layer must be set with setVectorLayer() + * @see includeRelated() + */ + void setIncludeRelated( bool includeRelated ); + + /** Returns whether attributes of related (child) features will be included in the JSON exports. + * @see setIncludeRelated() + */ + bool includeRelated() const; + + /** Sets the associated vector layer (required for related attribute export). + * @param vectorLayer vector layer + * @see vectorLayer() + */ + void setVectorLayer( const QgsVectorLayer* vectorLayer ); + + /** Returns the associated vector layer, if set. + * @see setVectorLayer() + */ + QgsVectorLayer* vectorLayer() const; + /** Sets the list of attributes to include in the JSON exports. * @param attributes list of attribute indexes, or an empty list to include all * attributes @@ -138,4 +161,9 @@ class QgsJSONUtils * @returns encoded value */ static QString encodeValue( const QVariant& value ); + + /** Exports all attributes from a QgsFeature as a JSON map type. + * @param feature feature to export + */ + static QString exportAttributes( const QgsFeature& feature ); }; diff --git a/src/core/qgsjsonutils.cpp b/src/core/qgsjsonutils.cpp index fa43a83b258d..250266f25474 100644 --- a/src/core/qgsjsonutils.cpp +++ b/src/core/qgsjsonutils.cpp @@ -16,16 +16,32 @@ #include "qgsjsonutils.h" #include "qgsogrutils.h" #include "qgsgeometry.h" +#include "qgsvectorlayer.h" +#include "qgsmaplayerregistry.h" +#include "qgsrelation.h" +#include "qgsrelationmanager.h" +#include "qgsproject.h" - -QgsJSONExporter::QgsJSONExporter( int precision, bool includeGeometry, bool includeAttributes ) +QgsJSONExporter::QgsJSONExporter( const QgsVectorLayer* vectorLayer, int precision ) : mPrecision( precision ) - , mIncludeGeometry( includeGeometry ) - , mIncludeAttributes( includeAttributes ) + , mIncludeGeometry( true ) + , mIncludeAttributes( true ) + , mIncludeRelatedAttributes( false ) + , mLayerId( vectorLayer ? vectorLayer->id() : QString() ) { } +void QgsJSONExporter::setVectorLayer( const QgsVectorLayer* vectorLayer ) +{ + mLayerId = vectorLayer ? vectorLayer->id() : QString(); +} + +QgsVectorLayer *QgsJSONExporter::vectorLayer() const +{ + return qobject_cast< QgsVectorLayer* >( QgsMapLayerRegistry::instance()->mapLayer( mLayerId ) ); +} + QString QgsJSONExporter::exportFeature( const QgsFeature& feature, const QVariantMap& extraProperties, const QVariant& id ) const { @@ -37,7 +53,6 @@ QString QgsJSONExporter::exportFeature( const QgsFeature& feature, const QVarian { //read all attribute values from the feature - if ( mIncludeAttributes ) { const QgsFields* fields = feature.fields(); @@ -70,7 +85,43 @@ QString QgsJSONExporter::exportFeature( const QgsFeature& feature, const QVarian ++attributeCounter; } } + + // related attributes + QgsVectorLayer* vl = vectorLayer(); + if ( vl && mIncludeRelatedAttributes ) + { + QList< QgsRelation > relations = QgsProject::instance()->relationManager()->referencedRelations( vl ); + Q_FOREACH ( const QgsRelation& relation, relations ) + { + if ( attributeCounter > 0 ) + properties += ",\n"; + + QgsFeatureRequest req = relation.getRelatedFeaturesRequest( feature ); + req.setFlags( QgsFeatureRequest::NoGeometry ); + QgsVectorLayer* childLayer = relation.referencingLayer(); + QString relatedFeatureAttributes; + if ( childLayer ) + { + QgsFeatureIterator it = childLayer->getFeatures( req ); + QgsFeature relatedFet; + int relationFeatures = 0; + while ( it.nextFeature( relatedFet ) ) + { + if ( relationFeatures > 0 ) + relatedFeatureAttributes += ",\n"; + + relatedFeatureAttributes += QgsJSONUtils::exportAttributes( relatedFet ); + relationFeatures++; + } + } + relatedFeatureAttributes.prepend( '[' ).append( ']' ); + + properties += QString( " \"%1\":%2" ).arg( relation.name(), relatedFeatureAttributes ); + attributeCounter++; + } + } } + bool hasProperties = attributeCounter > 0; QString s = "{\n \"type\":\"Feature\",\n"; @@ -115,6 +166,7 @@ QString QgsJSONExporter::exportFeature( const QgsFeature& feature, const QVarian } + // // QgsJSONUtils // @@ -194,3 +246,19 @@ QString QgsJSONUtils::encodeValue( const QVariant &value ) return v.prepend( '"' ).append( '"' ); } } + +QString QgsJSONUtils::exportAttributes( const QgsFeature& feature ) +{ + const QgsFields* fields = feature.fields(); + QString attrs; + for ( int i = 0; i < fields->count(); ++i ) + { + if ( i > 0 ) + attrs += ",\n"; + + QVariant val = feature.attributes().at( i ); + attrs += encodeValue( fields->at( i ).name() ) + ':' + encodeValue( val ); + } + return attrs.prepend( '{' ).append( '}' ); +} + diff --git a/src/core/qgsjsonutils.h b/src/core/qgsjsonutils.h index 1c0b0808d570..bc9ddc8bf22a 100644 --- a/src/core/qgsjsonutils.h +++ b/src/core/qgsjsonutils.h @@ -19,6 +19,7 @@ #include "qgsfeature.h" class QTextCodec; +class QgsVectorLayer; /** \ingroup core * \class QgsJSONExporter @@ -31,11 +32,10 @@ class CORE_EXPORT QgsJSONExporter public: /** Constructor for QgsJSONExporter. + * @param vectorLayer associated vector layer (required for related attribute export) * @param precision maximum number of decimal places to use for geometry coordinates - * @param includeGeometry set to false to avoid including the geometry representation in the JSON output - * @param includeAttributes set to false to avoid including any attribute values in the JSON output */ - QgsJSONExporter( int precision = 17, bool includeGeometry = true, bool includeAttributes = true ); + QgsJSONExporter( const QgsVectorLayer* vectorLayer = nullptr, int precision = 17 ); /** Sets the maximum number of decimal places to use in geometry coordinates. * @param precision number of decimal places @@ -70,6 +70,30 @@ class CORE_EXPORT QgsJSONExporter */ bool includeAttributes() const { return mIncludeAttributes; } + /** Sets whether to include attributes of features linked via references in the JSON exports. + * @param includeRelated set to true to include attributes for any related child features + * within the exported properties element. + * @note associated vector layer must be set with setVectorLayer() + * @see includeRelated() + */ + void setIncludeRelated( bool includeRelated ) { mIncludeRelatedAttributes = includeRelated; } + + /** Returns whether attributes of related (child) features will be included in the JSON exports. + * @see setIncludeRelated() + */ + bool includeRelated() const { return mIncludeRelatedAttributes; } + + /** Sets the associated vector layer (required for related attribute export). + * @param vectorLayer vector layer + * @see vectorLayer() + */ + void setVectorLayer( const QgsVectorLayer* vectorLayer ); + + /** Returns the associated vector layer, if set. + * @see setVectorLayer() + */ + QgsVectorLayer* vectorLayer() const; + /** Sets the list of attributes to include in the JSON exports. * @param attributes list of attribute indexes, or an empty list to include all * attributes @@ -115,6 +139,7 @@ class CORE_EXPORT QgsJSONExporter const QVariantMap& extraProperties = QVariantMap(), const QVariant& id = QVariant() ) const; + private: //! Maximum number of decimal places for geometry coordinates @@ -133,6 +158,12 @@ class CORE_EXPORT QgsJSONExporter //! Whether to include attributes in JSON export bool mIncludeAttributes; + //! Whether to include attributes from related features in JSON export + bool mIncludeRelatedAttributes; + + //! Layer ID of associated vector layer. Required for related attribute export. + QString mLayerId; + }; /** \ingroup core @@ -171,6 +202,11 @@ class CORE_EXPORT QgsJSONUtils */ static QString encodeValue( const QVariant& value ); + /** Exports all attributes from a QgsFeature as a JSON map type. + * @param feature feature to export + */ + static QString exportAttributes( const QgsFeature& feature ); + }; #endif // QGSJSONUTILS_H diff --git a/tests/src/python/test_qgsjsonutils.py b/tests/src/python/test_qgsjsonutils.py index 543ca7a940cb..451630e21e70 100644 --- a/tests/src/python/test_qgsjsonutils.py +++ b/tests/src/python/test_qgsjsonutils.py @@ -15,7 +15,7 @@ import qgis # NOQA from qgis.testing import unittest, start_app -from qgis.core import QgsJSONUtils, QgsJSONExporter, QgsFeature, QgsField, QgsFields, QgsWKBTypes, QgsGeometry, QgsPointV2, QgsLineStringV2, NULL +from qgis.core import QgsJSONUtils, QgsJSONExporter, QgsProject, QgsMapLayerRegistry, QgsFeature, QgsField, QgsFields, QgsWKBTypes, QgsGeometry, QgsPointV2, QgsLineStringV2, NULL, QgsVectorLayer, QgsRelation from qgis.PyQt.QtCore import QVariant, QTextCodec start_app() @@ -105,6 +105,29 @@ def testEncodeValue(self): self.assertEqual(QgsJSONUtils.encodeValue({'key': 'value', 'key2': 5}), '{"key":"value",\n"key2":5}') self.assertEqual(QgsJSONUtils.encodeValue({'key': [1, 2, 3], 'key2': {'nested': 'nested\\result'}}), '{"key":[1,2,3],\n"key2":{"nested":"nested\\\\result"}}') + def testExportAttributes(self): + """ test exporting feature's attributes to JSON object """ + fields = QgsFields() + + # test empty attributes + feature = QgsFeature(fields, 5) + expected = "{}" + self.assertEqual(QgsJSONUtils.exportAttributes(feature), expected) + + # test feature with attributes + fields.append(QgsField("name", QVariant.String)) + fields.append(QgsField("cost", QVariant.Double)) + fields.append(QgsField("population", QVariant.Int)) + + feature = QgsFeature(fields, 5) + feature.setGeometry(QgsGeometry(QgsPointV2(5, 6))) + feature.setAttributes(['Valsier Peninsula', 6.8, 198]) + + expected = """{"name":"Valsier Peninsula", +"cost":6.8, +"population":198}""" + self.assertEqual(QgsJSONUtils.exportAttributes(feature), expected) + def testJSONExporter(self): """ test converting features to GeoJSON """ fields = QgsFields() @@ -332,16 +355,117 @@ def testJSONExporter(self): }""" self.assertEqual(exporter.exportFeature(feature, extraProperties={"extra": "val1", "extra2": {"nested_map": 5, "nested_map2": "val"}, "extra3": [1, 2, 3]}), expected) exporter.setIncludeGeometry(True) + + def testExportFeatureRelations(self): + """ Test exporting a feature with relations """ + + #parent layer + parent = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer&field=foreignkey:integer", + "parent", "memory") + pr = parent.dataProvider() + pf1 = QgsFeature() + pf1.setFields(parent.fields()) + pf1.setAttributes(["test1", 67, 123]) + pf2 = QgsFeature() + pf2.setFields(parent.fields()) + pf2.setAttributes(["test2", 68, 124]) + assert pr.addFeatures([pf1, pf2]) + + #child layer + child = QgsVectorLayer( + "Point?field=x:string&field=y:integer&field=z:integer", + "referencedlayer", "memory") + pr = child.dataProvider() + f1 = QgsFeature() + f1.setFields(child.fields()) + f1.setAttributes(["foo", 123, 321]) + f2 = QgsFeature() + f2.setFields(child.fields()) + f2.setAttributes(["bar", 123, 654]) + f3 = QgsFeature() + f3.setFields(child.fields()) + f3.setAttributes(["foobar", 124, 554]) + assert pr.addFeatures([f1, f2, f3]) + + QgsMapLayerRegistry.instance().addMapLayers([child, parent]) + + rel = QgsRelation() + rel.setRelationId('rel1') + rel.setRelationName('relation one') + rel.setReferencingLayer(child.id()) + rel.setReferencedLayer(parent.id()) + rel.addFieldPair('y', 'foreignkey') + + QgsProject.instance().relationManager().addRelation(rel) + + exporter = QgsJSONExporter() + + exporter.setVectorLayer(parent) + self.assertEqual(exporter.vectorLayer(), parent) + exporter.setIncludeRelated(True) + self.assertEqual(exporter.includeRelated(), True) + expected = """{ "type":"Feature", - "id":5, - "geometry": - {"type": "Point", "coordinates": [5, 6]}, + "id":0, "properties":{ - "extra":"val1", - "extra2":2 + "fldtxt":"test1", + "fldint":67, + "foreignkey":123, + "relation one":[{"x":"foo", +"y":123, +"z":321}, +{"x":"bar", +"y":123, +"z":654}] + } +}""" + self.assertEqual(exporter.exportFeature(pf1), expected) + + expected = """{ + "type":"Feature", + "id":0, + "properties":{ + "fldtxt":"test2", + "fldint":68, + "foreignkey":124, + "relation one":[{"x":"foobar", +"y":124, +"z":554}] + } +}""" + self.assertEqual(exporter.exportFeature(pf2), expected) + + # test excluding related attributes + exporter.setIncludeRelated(False) + self.assertEqual(exporter.includeRelated(), False) + + expected = """{ + "type":"Feature", + "id":0, + "properties":{ + "fldtxt":"test2", + "fldint":68, + "foreignkey":124 } }""" + self.assertEqual(exporter.exportFeature(pf2), expected) + + # test without vector layer set + exporter.setIncludeRelated(True) + exporter.setVectorLayer(None) + + expected = """{ + "type":"Feature", + "id":0, + "properties":{ + "fldtxt":"test2", + "fldint":68, + "foreignkey":124 + } +}""" + self.assertEqual(exporter.exportFeature(pf2), expected) + if __name__ == "__main__": unittest.main()