Skip to content

Commit

Permalink
Server WFS: add srsName support to JSON output
Browse files Browse the repository at this point in the history
  • Loading branch information
elpaso authored and nyalldawson committed Dec 14, 2022
1 parent f59a8fb commit 5078512
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 2 deletions.
12 changes: 12 additions & 0 deletions python/core/auto_generated/qgsjsonutils.sip.in
Expand Up @@ -258,6 +258,18 @@ Returns a GeoJSON string representation of a list of features (feature collectio
%End


void setDestinationCrs( const QgsCoordinateReferenceSystem &destinationCrs );
%Docstring
Set the destination CRS for feature geometry transformation to ``destinationCrs``, this defaults to EPSG:4326
and it is only effective when the automatic geometry transformation is active (it is by default).

.. seealso:: :py:func:`setTransformGeometries`

.. seealso:: :py:func:`setSourceCrs`

.. versionadded:: 3.30
%End

};


Expand Down
11 changes: 10 additions & 1 deletion src/core/qgsjsonutils.cpp
Expand Up @@ -42,7 +42,10 @@ QgsJsonExporter::QgsJsonExporter( QgsVectorLayer *vectorLayer, int precision )
mCrs = vectorLayer->crs();
mTransform.setSourceCrs( mCrs );
}
mTransform.setDestinationCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) );

// Default 4326
mDestinationCrs = QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) );
mTransform.setDestinationCrs( mDestinationCrs );
}

void QgsJsonExporter::setVectorLayer( QgsVectorLayer *vectorLayer )
Expand Down Expand Up @@ -248,6 +251,12 @@ json QgsJsonExporter::exportFeaturesToJsonObject( const QgsFeatureList &features
return data;
}

void QgsJsonExporter::setDestinationCrs( const QgsCoordinateReferenceSystem &destinationCrs )
{
mDestinationCrs = destinationCrs;
mTransform.setDestinationCrs( mDestinationCrs );
}

//
// QgsJsonUtils
//
Expand Down
13 changes: 13 additions & 0 deletions src/core/qgsjsonutils.h
Expand Up @@ -241,6 +241,17 @@ class CORE_EXPORT QgsJsonExporter
*/
json exportFeaturesToJsonObject( const QgsFeatureList &features ) const SIP_SKIP;

/**
* Set the destination CRS for feature geometry transformation to \a destinationCrs, this defaults to EPSG:4326
* and it is only effective when the automatic geometry transformation is active (it is by default).
*
* \see setTransformGeometries()
* \see setSourceCrs()
*
* \since QGIS 3.30
*/
void setDestinationCrs( const QgsCoordinateReferenceSystem &destinationCrs );

private:

//! Maximum number of decimal places for geometry coordinates
Expand Down Expand Up @@ -274,6 +285,8 @@ class CORE_EXPORT QgsJsonExporter
bool mAttributeDisplayName = false;

bool mTransformGeometries = true;

QgsCoordinateReferenceSystem mDestinationCrs;
};

/**
Expand Down
12 changes: 11 additions & 1 deletion src/server/services/wfs/qgswfsgetfeature.cpp
Expand Up @@ -1064,7 +1064,8 @@ namespace QgsWfs
else
query.addQueryItem( QStringLiteral( "VERSION" ), QStringLiteral( "1.0.0" ) );

for ( auto param : query.queryItems() )
const auto constItems { query.queryItems() };
for ( const auto &param : std::as_const( constItems ) )
{
if ( sParamFilter.contains( param.first.toUpper() ) )
query.removeAllQueryItems( param.first );
Expand Down Expand Up @@ -1283,6 +1284,15 @@ namespace QgsWfs
fcString += QLatin1String( " " );
else
fcString += QLatin1String( " ," );

const QgsCoordinateReferenceSystem destinationCrs { params.srsName.isEmpty( ) ? QStringLiteral( "EPSG:4326" ) : params.srsName };
if ( ! destinationCrs.isValid() )
{
throw QgsRequestNotWellFormedException( QStringLiteral( "srsName error: '%1' is not valid." ).arg( params.srsName ) );
}

mJsonExporter.setDestinationCrs( destinationCrs );
mJsonExporter.setTransformGeometries( true );
mJsonExporter.setSourceCrs( params.crs );
mJsonExporter.setIncludeGeometry( false );
mJsonExporter.setIncludeAttributes( !params.attributeIndexes.isEmpty() );
Expand Down
27 changes: 27 additions & 0 deletions tests/src/core/testqgsjsonutils.cpp
Expand Up @@ -44,6 +44,7 @@ class TestQgsJsonUtils : public QObject
void testExportAttributesJson_data();
void testExportAttributesJson();
void testExportFeatureJson();
void testExportFeatureJsonCrs();
void testExportGeomToJson();
};

Expand Down Expand Up @@ -248,6 +249,32 @@ void TestQgsJsonUtils::testExportFeatureJson()

}

void TestQgsJsonUtils::testExportFeatureJsonCrs()
{
QgsVectorLayer vl { QStringLiteral( "Polygon?field=fldtxt:string&field=fldint:integer&field=flddbl:double" ), QStringLiteral( "mem" ), QStringLiteral( "memory" ) };
QgsFeature feature { vl.fields() };
feature.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POLYGON((1.12 1.34,5.45 1.12,5.34 5.33,1.56 5.2,1.12 1.34),(2 2, 3 2, 3 3, 2 3,2 2))" ) ) );
feature.setAttributes( QgsAttributes() << QStringLiteral( "a value" ) << 1 << 2.0 );

QgsJsonExporter exporterPrecision { &vl, 1 };
exporterPrecision.setDestinationCrs( QgsCoordinateReferenceSystem( "EPSG:3857" ) );


const auto expectedJsonPrecision { QStringLiteral( "{\"bbox\":[124677.8,124685.8,606691.2,594190.5],\"geometry\":"
"{\"coordinates\":[[[124677.8,149181.7],[606691.2,124685.8],[594446.1,594190.5],[173658.4,579657.7],"
"[124677.8,149181.7]],[[222639.0,222684.2],[333958.5,222684.2],[333958.5,334111.2],[222639.0,334111.2],"
"[222639.0,222684.2]]],\"type\":\"Polygon\"},\"id\":123,\"properties\":{\"flddbl\":2.0,\"fldint\":1,"
"\"fldtxt\":\"a value\"},\"type\":\"Feature\"}" ) };

feature.setId( 123 );
const auto jPrecision( exporterPrecision.exportFeatureToJsonObject( feature ) );
qDebug() << QString::fromStdString( jPrecision.dump() );
QCOMPARE( QString::fromStdString( jPrecision.dump() ), expectedJsonPrecision );
const auto jsonPrecision { exporterPrecision.exportFeature( feature ) };
QCOMPARE( jsonPrecision, expectedJsonPrecision );

}

void TestQgsJsonUtils::testExportGeomToJson()
{
const QMap<QString, QString> testWkts
Expand Down
99 changes: 99 additions & 0 deletions tests/src/python/test_qgsserver_wfs.py
Expand Up @@ -19,6 +19,7 @@
# Needed on Qt 5 so that the serialization of XML is consistent among all executions
os.environ['QT_HASH_SEED'] = '1'

import json
import re
import urllib.request
import urllib.parse
Expand Down Expand Up @@ -721,6 +722,104 @@ def test_getFeatureFeatureIdJson(self):
+ "&SRSNAME=EPSG:4326&TYPENAME=testlayer&FEATUREID=testlayer.0",
'wfs_getFeature_1_0_0_featureid_0_json')

def test_getFeatureFeatureJsonCrs(self):
"""Test issue GH #25171, geojson srsName"""

project = QgsProject()
layer = QgsVectorLayer("Point?crs=epsg:3857&field=fldint:integer",
"layer", "memory")
project.addMapLayers([layer])
project.writeEntry("WFSLayers", "/", [layer.id()])
f = QgsFeature(layer.fields())

f.setGeometry(QgsGeometry.fromWkt('point(807305 5592878)'))
f.setAttributes([123])
layer.dataProvider().addFeatures([f])
f = QgsFeature(layer.fields())
f.setGeometry(QgsGeometry.fromWkt('point(812191 5589555)'))
f.setAttributes([123])
layer.dataProvider().addFeatures([f])

query_string = "?" + "&".join(["%s=%s" % i for i in list({
"SERVICE": "WFS",
"REQUEST": "GetFeature",
"VERSION": "1.1.0",
"TYPENAME": "layer",
"SRSNAME": "EPSG:3857",
"outputFormat": "GeoJSON"
}.items())])

header, body = self._execute_request_project(query_string, project)
json.loads(body)
jdata = json.loads(body)
jdata['features'][0]['geometry']
jdata['features'][0]['geometry']['coordinates']
self.assertEqual(jdata['features'][0]['geometry']['coordinates'], [807305, 5592878])

query_string = "?" + "&".join(["%s=%s" % i for i in list({
"SERVICE": "WFS",
"REQUEST": "GetFeature",
"VERSION": "1.1.0",
"TYPENAME": "layer",
"SRSNAME": "EPSG:4326",
"outputFormat": "GeoJSON"
}.items())])

header, body = self._execute_request_project(query_string, project)
json.loads(body)
jdata = json.loads(body)
jdata['features'][0]['geometry']
jdata['features'][0]['geometry']['coordinates']
self.assertEqual([int(i) for i in jdata['features'][0]['geometry']['coordinates']], [7, 44])

query_string = "?" + "&".join(["%s=%s" % i for i in list({
"SERVICE": "WFS",
"REQUEST": "GetFeature",
"VERSION": "1.1.0",
"TYPENAME": "layer",
"outputFormat": "GeoJSON"
}.items())])

header, body = self._execute_request_project(query_string, project)
json.loads(body)
jdata = json.loads(body)
jdata['features'][0]['geometry']
jdata['features'][0]['geometry']['coordinates']
self.assertEqual([int(i) for i in jdata['features'][0]['geometry']['coordinates']], [7, 44])

query_string = "?" + "&".join(["%s=%s" % i for i in list({
"SERVICE": "WFS",
"REQUEST": "GetFeature",
"VERSION": "1.1.0",
"TYPENAME": "layer",
"SRSNAME": "EPSG:32632",
"outputFormat": "GeoJSON"
}.items())])

header, body = self._execute_request_project(query_string, project)
json.loads(body)
jdata = json.loads(body)
jdata['features'][0]['geometry']
jdata['features'][0]['geometry']['coordinates']
self.assertEqual([int(i) for i in jdata['features'][0]['geometry']['coordinates']], [361806, 4964192])

query_string = "?" + "&".join(["%s=%s" % i for i in list({
"SERVICE": "WFS",
"REQUEST": "GetFeature",
"VERSION": "1.1.0",
"TYPENAME": "layer",
"SRSNAME": "EPSG:3857",
"outputFormat": "GeoJSON",
"FEATUREID": "layer.2"
}.items())])

header, body = self._execute_request_project(query_string, project)
json.loads(body)
jdata = json.loads(body)
jdata['features'][0]['geometry']
jdata['features'][0]['geometry']['coordinates']
self.assertEqual([int(i) for i in jdata['features'][0]['geometry']['coordinates']], [812191, 5589555])

def test_insert_srsName(self):
"""Test srsName is respected when insering"""

Expand Down

0 comments on commit 5078512

Please sign in to comment.