Skip to content

Commit 1d1a30e

Browse files
committed
Transform extra geoms to the same CRS as main geom
1 parent ef9aadb commit 1d1a30e

8 files changed

Lines changed: 385 additions & 123 deletions

src/server/services/wfs3/qgswfs3handlers.cpp

Lines changed: 226 additions & 96 deletions
Large diffs are not rendered by default.

src/server/services/wfs3/qgswfs3handlers.h

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#ifndef QGS_WFS3_HANDLERS_H
1919
#define QGS_WFS3_HANDLERS_H
2020

21+
#include "qgscoordinatereferencesystem.h"
2122
#include "qgsfields.h"
2223
#include "qgsserverogcapihandler.h"
2324

@@ -110,6 +111,12 @@ class QgsWfs3AbstractItemsHandler : public QgsServerOgcApiHandler
110111
*/
111112
static QUrlQuery removeOffsetAndLimit( const QUrlQuery &urlQuery, bool removeProfile = false );
112113

114+
/**
115+
* Returns TRUE if the given \a crs is published in the WFS service in the given \a apiContext, FALSE otherwise.
116+
*/
117+
static bool crsIsPublished( const QgsCoordinateReferenceSystem &crs, const QgsServerApiContext &context );
118+
119+
113120
protected:
114121
/**
115122
* Referenced layer information from ValueRelation and RelationReference widgets
@@ -130,18 +137,18 @@ class QgsWfs3AbstractItemsHandler : public QgsServerOgcApiHandler
130137
* The returned map has as key the index of the field in the \a mapLayer that contains the reference to the other layer, and as value a ReferencedLayerInfo struct with the information about the referenced layer.
131138
* Unpublished fields or referenced layers are not returned as well as relations using compound primary keys (i.e. referencing multiple fields) or referencing fields that are not a primary key of the referenced layer.
132139
*/
133-
QMap<int, ReferencedLayerInfo> gatherReferencedLayerInfo( const QgsVectorLayer *mapLayer, const QgsServerApiContext &apiContext ) const;
140+
QMap<int, ReferencedLayerInfo> gatherReferencedLayerInfo( const QgsVectorLayer *mapLayer, const QgsServerApiContext &context ) const;
134141

135142
/**
136143
* Returns the name used by OAPIF to identify the collection
137144
* \throw QgsServerApiImproperlyConfiguredException if referenced layer is not found
138145
*/
139-
QString referencedLayerIdentifier( const QgsVectorLayer *mapLayer, int fieldIdx, const QgsServerApiContext &apiContext, QString *referencedLayerTitle = nullptr ) const;
146+
QString referencedLayerIdentifier( const QgsVectorLayer *mapLayer, int fieldIdx, const QgsServerApiContext &context, QString *referencedLayerTitle = nullptr ) const;
140147

141148
/**
142149
* Creates the link to the referenced feature to be set in the referencing feature JSON
143150
*/
144-
json relatedFeatureReference( const QVariant &referencedFeatureValue, const ReferencedLayerInfo &referencedInfo, QgsServerOgcApi::Profile relAs, const QgsServerApiContext &apiContext ) const;
151+
json relatedFeatureReference( const QVariant &referencedFeatureValue, const ReferencedLayerInfo &referencedInfo, QgsServerOgcApi::Profile relAs, const QgsServerApiContext &context ) const;
145152

146153
/**
147154
* Returns the URI to the feature(s) given the collection ID and the field values, it may return a
@@ -154,7 +161,7 @@ class QgsWfs3AbstractItemsHandler : public QgsServerOgcApiHandler
154161
* to the items endpoint with filters on the field values will be returned.
155162
*/
156163
static QString uri(
157-
const QString &collectionId, const QMap<QString, QVariant> &fieldValueMap, const QgsServerApiContext &apiContext, QgsServerOgcApi::ContentType contentType = QgsServerOgcApi::ContentType::JSON
164+
const QString &collectionId, const QMap<QString, QVariant> &fieldValueMap, const QgsServerApiContext &context, QgsServerOgcApi::ContentType contentType = QgsServerOgcApi::ContentType::JSON
158165
);
159166
};
160167

@@ -289,7 +296,7 @@ class QgsWfs3CollectionsItemsHandler : public QgsWfs3AbstractItemsHandler
289296
{
290297
public:
291298
QgsWfs3CollectionsItemsHandler();
292-
void handleRequest( const QgsServerApiContext &apiContext ) const override;
299+
void handleRequest( const QgsServerApiContext &context ) const override;
293300
QRegularExpression path() const override { return QRegularExpression( R"re(/collections/(?<collectionId>[^/]+)/items(\.geojson|\.json|\.html|\.fgb|/)?$)re" ); }
294301
std::string operationId() const override { return "getFeatures"; }
295302
std::string summary() const override { return "Retrieve features of feature collection {collectionId}."; }
@@ -321,10 +328,10 @@ class QgsWfs3CollectionsItemsHandler : public QgsWfs3AbstractItemsHandler
321328
const QList<QgsServerQueryStringParameter> fieldParameters( const QgsVectorLayer *mapLayer, const QgsServerApiContext &context ) const;
322329

323330
// Json output
324-
void writeJsonOutput( const QgsVectorLayer *mapLayer, QgsFeatureRequest &featureRequest, const QgsServerApiContext &apiContext, const ExportContext &exportContext ) const;
331+
void writeJsonOutput( const QgsVectorLayer *mapLayer, QgsFeatureRequest &featureRequest, const QgsServerApiContext &context, const ExportContext &exportContext ) const;
325332

326333
// FlatGeobuf output
327-
void writeFlatGeobufOutput( const QgsVectorLayer *mapLayer, QgsFeatureRequest &featureRequest, const QgsServerApiContext &apiContext, const ExportContext &exportContext ) const;
334+
void writeFlatGeobufOutput( const QgsVectorLayer *mapLayer, QgsFeatureRequest &featureRequest, const QgsServerApiContext &context, const ExportContext &exportContext ) const;
328335
};
329336

330337
/**
@@ -364,6 +371,7 @@ class QgsWfs3CollectionsFeatureHandler : public QgsWfs3AbstractItemsHandler
364371
QStringList tags() const override { return { u"Features"_s }; }
365372
QgsServerOgcApi::Rel linkType() const override { return QgsServerOgcApi::Rel::data; }
366373
json schema( const QgsServerApiContext &context ) const override;
374+
QList<QgsServerQueryStringParameter> parameters( const QgsServerApiContext &apiContext ) const override;
367375
};
368376

369377

tests/src/python/test_qgsserver_ogcapi_schema.py

Lines changed: 106 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
from qgis.testing import unittest
6969
from test_qgsserver import QgsServerTestBase
7070
from test_qgsserver_api import QgsServerAPITestBase
71-
from utilities import unitTestDataPath
71+
from utilities import compareWkt, unitTestDataPath
7272

7373

7474
class EnumProvider(PyProvider):
@@ -106,15 +106,15 @@ def setUpClass(cls):
106106
# Set to True in child classes to re-generate reference files for this class
107107
regeregenerate_api_reference = True
108108

109-
def _getJsonResponse(self, url, project):
109+
def _getJsonResponse(self, url, project, expected_status=200):
110110
request = QgsBufferServerRequest(url)
111111
response = QgsBufferServerResponse()
112112
server = QgsServer()
113113
server.handleRequest(request, response, project)
114114
self.assertEqual(
115115
response.statusCode(),
116-
200,
117-
f"Request failed with status {response.statusCode()} and message: {bytes(response.body()).decode('utf8')} for URL: {url}",
116+
expected_status,
117+
f"Request status mismatch {response.statusCode()} and message: {bytes(response.body()).decode('utf8')} for URL: {url}",
118118
)
119119
response_str = bytes(response.body()).decode("utf8")
120120
j = json.loads(response_str)
@@ -563,7 +563,7 @@ def test_relation(self):
563563
},
564564
)
565565

566-
def test_extra_geometry(self):
566+
def test_extra_geometry_and_crs(self):
567567

568568
if "QGIS_PGTEST_DB" in os.environ:
569569
dbconn = os.environ["QGIS_PGTEST_DB"]
@@ -584,12 +584,14 @@ def test_extra_geometry(self):
584584
conn.executeSql(
585585
"SELECT AddGeometryColumn('qgis_test', 'TwoGeoms', 'geom1', 4326, 'POINT', 2 )"
586586
)
587+
588+
# NOTE: geom2 is the QGIS layer primary geometry in 3857
587589
conn.executeSql(
588-
"SELECT AddGeometryColumn('qgis_test', 'TwoGeoms', 'geom2', 4326, 'POINT', 2 )"
590+
"SELECT AddGeometryColumn('qgis_test', 'TwoGeoms', 'geom2', 3857, 'POINT', 2 )"
589591
)
590592

591593
conn.executeSql(
592-
"INSERT INTO \"qgis_test\".\"TwoGeoms\" (geom1, geom2) VALUES (ST_GeomFromText('POINT(0 0)', 4326), ST_GeomFromText('POINT(1 1)', 4326))"
594+
"INSERT INTO \"qgis_test\".\"TwoGeoms\" (geom1, geom2) VALUES (ST_GeomFromText('POINT(1 1)', 4326), ST_GeomFromText('POINT(111319 111325)', 3857))"
593595
)
594596

595597
layer_source = conn.tableUri("qgis_test", "TwoGeoms") + " (geom2) sql="
@@ -611,11 +613,105 @@ def test_extra_geometry(self):
611613
{"format": "geometry-any", "type": "string", "x-ogc-propertySeq": 1},
612614
)
613615

614-
# Get data and check geom1 is correctly exposed as WKT
616+
# Default 4326 CRS, check geom1 is correctly exposed as WKT
617+
j = self._getJsonResponse(
618+
"http://server.qgis.org/wfs3/collections/TwoGeoms/items.json", project
619+
)
620+
self.assertEqual(j["features"][0]["properties"]["geom1"].upper(), "POINT (1 1)")
621+
622+
# Explicitly ask for 4326, check geom1 is correctly exposed as WKT
623+
encoded_crs = parse.quote("http://www.opengis.net/def/crs/EPSG/0/4326", safe="")
624+
j = self._getJsonResponse(
625+
f"http://server.qgis.org/wfs3/collections/TwoGeoms/items.json?crs={encoded_crs}",
626+
project,
627+
)
628+
self.assertEqual(j["features"][0]["properties"]["geom1"].upper(), "POINT (1 1)")
629+
630+
# 3857 and check geom1 is correctly exposed as transformed WKT
631+
encoded_crs = parse.quote("http://www.opengis.net/def/crs/EPSG/0/3857", safe="")
632+
j = self._getJsonResponse(
633+
f"http://server.qgis.org/wfs3/collections/TwoGeoms/items.json?crs={encoded_crs}",
634+
project,
635+
)
636+
self.assertTrue(
637+
compareWkt(
638+
j["features"][0]["properties"]["geom1"].upper(),
639+
"POINT (111319 111325)",
640+
tol=1,
641+
)
642+
)
643+
644+
# Test single feature retrieval with 3857 and check geom1 is correctly exposed as transformed WKT
615645
j = self._getJsonResponse(
616-
"http://server.qgis.org/wfs3/collections/TwoGeoms/items?f=json", project
646+
f"http://server.qgis.org/wfs3/collections/TwoGeoms/items/1.json?crs={encoded_crs}",
647+
project,
648+
)
649+
650+
self.assertTrue(
651+
compareWkt(
652+
j["properties"]["geom1"].upper(),
653+
"POINT (111319 111325)",
654+
tol=1,
655+
)
656+
)
657+
658+
# Check main geometry is still correctly exposed as GeoJSON 3857
659+
self.assertAlmostEqual(j["geometry"]["coordinates"][0], 111319, delta=0.1)
660+
self.assertAlmostEqual(j["geometry"]["coordinates"][1], 111325, delta=0.1)
661+
662+
# Check both are transformed to http://www.opengis.net/def/crs/EPSG/0/3395
663+
encoded_crs = parse.quote("http://www.opengis.net/def/crs/EPSG/0/3395", safe="")
664+
665+
# Error because CRS 3395 is not published
666+
j = self._getJsonResponse(
667+
f"http://server.qgis.org/wfs3/collections/TwoGeoms/items.json?crs={encoded_crs}",
668+
project,
669+
expected_status=400,
670+
)
671+
672+
self.assertEqual(j[0]["code"], "Bad request error")
673+
self.assertTrue(j[0]["description"].startswith("Argument 'crs' is not valid."))
674+
675+
# Publish 3395
676+
project.writeEntry("WMSCrsList", "/", ["EPSG:3395"])
677+
678+
j = self._getJsonResponse(
679+
f"http://server.qgis.org/wfs3/collections/TwoGeoms/items.json?crs={encoded_crs}",
680+
project,
681+
expected_status=200,
682+
)
683+
684+
self.assertTrue(
685+
compareWkt(
686+
j["features"][0]["properties"]["geom1"].upper(),
687+
"POINT (111319 110579)",
688+
tol=1,
689+
)
690+
)
691+
self.assertAlmostEqual(
692+
j["features"][0]["geometry"]["coordinates"][0], 111319.0, delta=0.1
693+
)
694+
self.assertAlmostEqual(
695+
j["features"][0]["geometry"]["coordinates"][1], 110579.8, delta=0.1
696+
)
697+
698+
# Same check for single feature retrieval
699+
j = self._getJsonResponse(
700+
f"http://server.qgis.org/wfs3/collections/TwoGeoms/items/1.json?crs={encoded_crs}",
701+
project,
702+
)
703+
self.assertTrue(
704+
compareWkt(
705+
j["properties"]["geom1"].upper(),
706+
"POINT (111319 110579)",
707+
tol=1,
708+
)
709+
)
710+
711+
self.assertAlmostEqual(j["geometry"]["coordinates"][0], 111319, delta=0.1)
712+
self.assertAlmostEqual(
713+
j["geometry"]["coordinates"][1], 110579.823312, delta=0.1
617714
)
618-
self.assertEqual(j["features"][0]["properties"]["geom1"].upper(), "POINT (0 0)")
619715

620716

621717
if __name__ == "__main__":

tests/testdata/qgis_server/api/test_wfs3_collection_layer1_with_short_name.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,16 @@ Content-Type: application/json
5555
"title": "A Layer1 with a short name items as FLATGEOBUF",
5656
"type": "application/flatgeobuf"
5757
},
58+
{
59+
"href": "http://server.qgis.org/wfs3/collections/layer1_with_short_name/schema.json",
60+
"rel": "http://www.opengis.net/def/rel/ogc/1.0/schema",
61+
"title": "Schema of features in 'A Layer1 with a short name' as SCHEMA_JSON",
62+
"type": "application/schema+json"
63+
},
5864
{
5965
"href": "http://server.qgis.org/wfs3/collections/layer1_with_short_name/schema.html",
60-
"rel": "http://www.opengis.net/def/rel/ogc/0.0/schema",
61-
"title": "Schema of features in 'A Layer1 with a short name'",
66+
"rel": "http://www.opengis.net/def/rel/ogc/1.0/schema",
67+
"title": "Schema of features in 'A Layer1 with a short name' as HTML",
6268
"type": "text/html"
6369
}
6470
],

tests/testdata/qgis_server/api/test_wfs3_collection_points_timefilters.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,16 @@ Content-Type: application/json
6060
"title": "points items as FLATGEOBUF",
6161
"type": "application/flatgeobuf"
6262
},
63+
{
64+
"href": "http://server.qgis.org/wfs3/collections/points/schema.json",
65+
"rel": "http://www.opengis.net/def/rel/ogc/1.0/schema",
66+
"title": "Schema of features in 'points' as SCHEMA_JSON",
67+
"type": "application/schema+json"
68+
},
6369
{
6470
"href": "http://server.qgis.org/wfs3/collections/points/schema.html",
65-
"rel": "http://www.opengis.net/def/rel/ogc/0.0/schema",
66-
"title": "Schema of features in 'points'",
71+
"rel": "http://www.opengis.net/def/rel/ogc/1.0/schema",
72+
"title": "Schema of features in 'points' as HTML",
6773
"type": "text/html"
6874
}
6975
],

tests/testdata/qgis_server/api/test_wfs3_collection_testlayer_èé.html

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333

3434
<link rel="items" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/items.fgb" title="A test wfs vector layer èé items as FLATGEOBUF" type="application/flatgeobuf">
3535

36-
<link rel="http://www.opengis.net/def/rel/ogc/0.0/schema" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/schema.html" title="Schema of features in 'A test wfs vector layer èé'" type="text/html">
36+
<link rel="http://www.opengis.net/def/rel/ogc/1.0/schema" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/schema.json" title="Schema of features in 'A test wfs vector layer èé' as SCHEMA_JSON" type="application/schema+json">
37+
38+
<link rel="http://www.opengis.net/def/rel/ogc/1.0/schema" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/schema.html" title="Schema of features in 'A test wfs vector layer èé' as HTML" type="text/html">
3739

3840

3941

@@ -83,6 +85,8 @@ <h1><a title="View items of: A test wfs vector layer èé" href="
8385
8486
8587
88+
89+
8690
">A test wfs vector layer èé</a></h1>
8791
<h3>Available CRSs</h3>
8892
<ul>
@@ -129,7 +133,11 @@ <h3>Links</h3>
129133

130134

131135

132-
<li><a rel="http://www.opengis.net/def/rel/ogc/0.0/schema" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/schema.html">Schema of features in 'A test wfs vector layer èé'</a></li>
136+
<li><a rel="http://www.opengis.net/def/rel/ogc/1.0/schema" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/schema.json">Schema of features in 'A test wfs vector layer èé' as SCHEMA_JSON</a></li>
137+
138+
139+
140+
<li><a rel="http://www.opengis.net/def/rel/ogc/1.0/schema" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/schema.html">Schema of features in 'A test wfs vector layer èé' as HTML</a></li>
133141

134142

135143
</ul>

tests/testdata/qgis_server/api/test_wfs3_collection_testlayer_èé.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,16 @@ Content-Type: application/json
5555
"title": "A test wfs vector layer èé items as FLATGEOBUF",
5656
"type": "application/flatgeobuf"
5757
},
58+
{
59+
"href": "http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/schema.json",
60+
"rel": "http://www.opengis.net/def/rel/ogc/1.0/schema",
61+
"title": "Schema of features in 'A test wfs vector layer èé' as SCHEMA_JSON",
62+
"type": "application/schema+json"
63+
},
5864
{
5965
"href": "http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/schema.html",
60-
"rel": "http://www.opengis.net/def/rel/ogc/0.0/schema",
61-
"title": "Schema of features in 'A test wfs vector layer èé'",
66+
"rel": "http://www.opengis.net/def/rel/ogc/1.0/schema",
67+
"title": "Schema of features in 'A test wfs vector layer èé' as HTML",
6268
"type": "text/html"
6369
}
6470
],

tests/testdata/qgis_server/api/test_wfs3_conformance.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ Content-Type: application/json
22

33
{
44
"conformsTo": [
5-
"http://www.opengis.net/spec/ogcapi-common-1/1.0/req/landing-pagehttp://www.opengis.net/spec/ogcapi-common-1/1.0/req/oas30",
5+
"http://www.opengis.net/spec/ogcapi-common-1/1.0/req/landing-page",
6+
"http://www.opengis.net/spec/ogcapi-common-1/1.0/req/oas30",
67
"http://www.opengis.net/spec/ogcapi-common-1/1.0/req/html",
7-
"http://www.opengis.net/spec/ogcapi-common-1/1.0/req/jsonhttp://www.opengis.net/spec/ogcapi-common-3/1.0/conf/schemas",
8+
"http://www.opengis.net/spec/ogcapi-common-1/1.0/req/json",
9+
"http://www.opengis.net/spec/ogcapi-common-3/1.0/conf/schemas",
810
"http://www.opengis.net/spec/ogcapi-common-3/1.0/conf/profile-parameter",
911
"http://www.opengis.net/spec/ogcapi-common-3/1.0/conf/profile-references",
1012
"http://www.opengis.net/spec/ogcapi-common-3/1.0/conf/profile-codelists",

0 commit comments

Comments
 (0)