Skip to content

Commit 737719e

Browse files
committed
[WFS provider] Select GML3 output format for WFS 1.0 when available
Some WFS servers like QGIS servers can expose GML3 output format for GetFeature requests, which enable to retrieve curve geometries, instead of linearized ones with the default GML2 output format. So use GML3 when advertized, and that no explicit outputFormat is passed in the URI. Cherry-picked from commit 852f01b
1 parent 5541ad0 commit 737719e

8 files changed

+251
-10
lines changed

src/providers/wfs/qgswfscapabilities.cpp

+35
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,32 @@ void QgsWFSCapabilities::capabilitiesReplyFinished()
142142
// Note: for conveniency, we do not use the elementsByTagNameNS() method as
143143
// the WFS and OWS namespaces URI are not the same in all versions
144144

145+
if ( mCaps.version.startsWith( QLatin1String( "1.0" ) ) )
146+
{
147+
QDomElement capabilityElem = doc.firstChildElement( QStringLiteral( "Capability" ) );
148+
if ( !capabilityElem.isNull() )
149+
{
150+
QDomElement requestElem = capabilityElem.firstChildElement( QStringLiteral( "Request" ) );
151+
if ( !requestElem.isNull() )
152+
{
153+
QDomElement getFeatureElem = requestElem.firstChildElement( QStringLiteral( "GetFeature" ) );
154+
if ( !getFeatureElem.isNull() )
155+
{
156+
QDomElement resultFormatElem = getFeatureElem.firstChildElement( QStringLiteral( "ResultFormat" ) );
157+
if ( !resultFormatElem.isNull() )
158+
{
159+
QDomElement child = resultFormatElem.firstChildElement();
160+
while ( !child.isNull() )
161+
{
162+
mCaps.outputFormats << child.tagName();
163+
child = child.nextSiblingElement();
164+
}
165+
}
166+
}
167+
}
168+
}
169+
}
170+
145171
// find <ows:OperationsMetadata>
146172
QDomElement operationsMetadataElem = doc.firstChildElement( "OperationsMetadata" );
147173
if ( !operationsMetadataElem.isNull() )
@@ -231,6 +257,15 @@ void QgsWFSCapabilities::capabilitiesReplyFinished()
231257
}
232258
}
233259
}
260+
else if ( parameter.attribute( QStringLiteral( "name" ) ) == QLatin1String( "outputFormat" ) )
261+
{
262+
QDomNodeList valueList = parameter.elementsByTagName( QStringLiteral( "Value" ) );
263+
for ( int k = 0; k < valueList.size(); ++k )
264+
{
265+
QDomElement value = valueList.at( k ).toElement();
266+
mCaps.outputFormats << value.text();
267+
}
268+
}
234269
}
235270

236271
break;

src/providers/wfs/qgswfscapabilities.h

+1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class QgsWFSCapabilities : public QgsWFSRequest
9797
QList<Function> spatialPredicatesList;
9898
QList<Function> functionList;
9999
bool useEPSGColumnFormat; // whether to use EPSG:XXXX srsname
100+
QList< QString > outputFormats;
100101

101102
QSet< QString > setAllTypenames;
102103
QMap< QString, QString> mapUnprefixedTypenameToPrefixedTypename;

src/providers/wfs/qgswfsconstants.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const QString QgsWFSConstants::URI_PARAM_TYPENAME( "typename" );
3131
const QString QgsWFSConstants::URI_PARAM_SRSNAME( "srsname" );
3232
const QString QgsWFSConstants::URI_PARAM_BBOX( "bbox" );
3333
const QString QgsWFSConstants::URI_PARAM_FILTER( "filter" );
34+
const QString QgsWFSConstants::URI_PARAM_OUTPUTFORMAT( "outputformat" );
3435
const QString QgsWFSConstants::URI_PARAM_RESTRICT_TO_REQUEST_BBOX( "restrictToRequestBBOX" );
3536
const QString QgsWFSConstants::URI_PARAM_MAXNUMFEATURES( "maxNumFeatures" );
3637
const QString QgsWFSConstants::URI_PARAM_IGNOREAXISORIENTATION( "IgnoreAxisOrientation" );

src/providers/wfs/qgswfsconstants.h

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ struct QgsWFSConstants
3838
static const QString URI_PARAM_TYPENAME;
3939
static const QString URI_PARAM_SRSNAME;
4040
static const QString URI_PARAM_FILTER;
41+
static const QString URI_PARAM_OUTPUTFORMAT;
4142
static const QString URI_PARAM_BBOX;
4243
static const QString URI_PARAM_RESTRICT_TO_REQUEST_BBOX;
4344
static const QString URI_PARAM_MAXNUMFEATURES;

src/providers/wfs/qgswfsdatasourceuri.cpp

+59-9
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,14 @@
2222
QgsWFSDataSourceURI::QgsWFSDataSourceURI( const QString& uri )
2323
: mURI( uri )
2424
{
25-
// Compatiblity with QGIS < 2.16 layer URI of the format
25+
typedef QPair<QString, QString> queryItem;
26+
27+
// Compatibility with QGIS < 2.16 layer URI of the format
2628
// http://example.com/?SERVICE=WFS&VERSION=1.0.0&REQUEST=GetFeature&TYPENAME=x&SRSNAME=y&username=foo&password=
2729
if ( !mURI.hasParam( QgsWFSConstants::URI_PARAM_URL ) )
2830
{
2931
QUrl url( uri );
3032
// Transform all param keys to lowercase
31-
typedef QPair<QString, QString> queryItem;
3233
QList<queryItem> items( url.queryItems() );
3334
foreach ( queryItem item, items )
3435
{
@@ -41,6 +42,7 @@ QgsWFSDataSourceURI::QgsWFSDataSourceURI( const QString& uri )
4142
QString typeName = url.queryItemValue( QgsWFSConstants::URI_PARAM_TYPENAME );
4243
QString version = url.queryItemValue( QgsWFSConstants::URI_PARAM_VERSION );
4344
QString filter = url.queryItemValue( QgsWFSConstants::URI_PARAM_FILTER );
45+
QString outputFormat = url.queryItemValue( QgsWFSConstants::URI_PARAM_OUTPUTFORMAT );
4446
mAuth.mAuthCfg = url.queryItemValue( QgsWFSConstants::URI_PARAM_AUTHCFG );
4547
// NOTE: A defined authcfg overrides any older username/password auth
4648
// Only check for older auth if it is undefined
@@ -56,13 +58,14 @@ QgsWFSDataSourceURI::QgsWFSDataSourceURI( const QString& uri )
5658
}
5759

5860
// Now remove all stuff that is not the core URL
59-
url.removeQueryItem( "SERVICE" );
60-
url.removeQueryItem( "VERSION" );
61-
url.removeQueryItem( "TYPENAME" );
62-
url.removeQueryItem( "REQUEST" );
63-
url.removeQueryItem( "BBOX" );
64-
url.removeQueryItem( "SRSNAME" );
65-
url.removeQueryItem( "FILTER" );
61+
url.removeQueryItem( "service" );
62+
url.removeQueryItem( QgsWFSConstants::URI_PARAM_VERSION );
63+
url.removeQueryItem( QgsWFSConstants::URI_PARAM_TYPENAME );
64+
url.removeQueryItem( "request" );
65+
url.removeQueryItem( QgsWFSConstants::URI_PARAM_BBOX );
66+
url.removeQueryItem( QgsWFSConstants::URI_PARAM_SRSNAME );
67+
url.removeQueryItem( QgsWFSConstants::URI_PARAM_FILTER );
68+
url.removeQueryItem( QgsWFSConstants::URI_PARAM_OUTPUTFORMAT );
6669
url.removeQueryItem( QgsWFSConstants::URI_PARAM_USERNAME );
6770
url.removeQueryItem( QgsWFSConstants::URI_PARAM_PASSWORD );
6871
url.removeQueryItem( QgsWFSConstants::URI_PARAM_AUTHCFG );
@@ -72,6 +75,7 @@ QgsWFSDataSourceURI::QgsWFSDataSourceURI( const QString& uri )
7275
setTypeName( typeName );
7376
setSRSName( srsname );
7477
setVersion( version );
78+
setOutputFormat( outputFormat );
7579

7680
//if the xml comes from the dialog, it needs to be a string to pass the validity test
7781
if ( filter.startsWith( '\'' ) && filter.endsWith( '\'' ) && filter.size() > 1 )
@@ -86,6 +90,40 @@ QgsWFSDataSourceURI::QgsWFSDataSourceURI( const QString& uri )
8690
}
8791
else
8892
{
93+
QUrl url( mURI.param( QgsWFSConstants::URI_PARAM_URL ) );
94+
bool URLModified = false;
95+
bool somethingChanged = false;
96+
do
97+
{
98+
somethingChanged = false;
99+
QList<queryItem> items( url.queryItems() );
100+
Q_FOREACH ( const queryItem &item, items )
101+
{
102+
const QString lowerName( item.first.toLower() );
103+
if ( lowerName == QgsWFSConstants::URI_PARAM_OUTPUTFORMAT )
104+
{
105+
setOutputFormat( item.second );
106+
url.removeQueryItem( item.first );
107+
somethingChanged = true;
108+
URLModified = true;
109+
break;
110+
}
111+
else if ( lowerName == QLatin1String( "service" ) ||
112+
lowerName == QLatin1String( "request" ) )
113+
{
114+
url.removeQueryItem( item.first );
115+
somethingChanged = true;
116+
URLModified = true;
117+
break;
118+
}
119+
}
120+
}
121+
while ( somethingChanged );
122+
if ( URLModified )
123+
{
124+
mURI.setParam( QgsWFSConstants::URI_PARAM_URL, url.toEncoded() );
125+
}
126+
89127
mAuth.mUserName = mURI.username();
90128
mAuth.mPassword = mURI.password();
91129
mAuth.mAuthCfg = mURI.authConfigId();
@@ -201,6 +239,18 @@ void QgsWFSDataSourceURI::setSql( const QString& sql )
201239
mURI.setSql( sql );
202240
}
203241

242+
QString QgsWFSDataSourceURI::outputFormat() const
243+
{
244+
return mURI.param( QgsWFSConstants::URI_PARAM_OUTPUTFORMAT );
245+
}
246+
247+
void QgsWFSDataSourceURI::setOutputFormat( const QString &outputFormat )
248+
{
249+
mURI.removeParam( QgsWFSConstants::URI_PARAM_OUTPUTFORMAT );
250+
if ( !outputFormat.isEmpty() )
251+
mURI.setParam( QgsWFSConstants::URI_PARAM_OUTPUTFORMAT, outputFormat );
252+
}
253+
204254
bool QgsWFSDataSourceURI::isRestrictedToRequestBBOX() const
205255
{
206256
if ( mURI.hasParam( QgsWFSConstants::URI_PARAM_RESTRICT_TO_REQUEST_BBOX ) &&

src/providers/wfs/qgswfsdatasourceuri.h

+7-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,13 @@ class QgsWFSDataSourceURI
117117
/** Set SQL query */
118118
void setSql( const QString& sql );
119119

120-
/** Returns whether GetFeature request should include the request bounding box. Defaults to false */
120+
//! Get GetFeature output format
121+
QString outputFormat() const;
122+
123+
//! Set GetFeature output format
124+
void setOutputFormat( const QString &outputFormat );
125+
126+
//! Returns whether GetFeature request should include the request bounding box. Defaults to false
121127
bool isRestrictedToRequestBBOX() const;
122128

123129
/** Returns whether axis orientation should be ignored (for WFS >= 1.1). Defaults to false */

src/providers/wfs/qgswfsfeatureiterator.cpp

+25
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,31 @@ QUrl QgsWFSFeatureDownloader::buildURL( int startIndex, int maxFeatures, bool fo
328328
getFeatureUrl.addQueryItem( "SORTBY", mShared->mSortBy );
329329
}
330330

331+
if ( !forHits && !mShared->mURI.outputFormat().isEmpty() )
332+
{
333+
getFeatureUrl.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), mShared->mURI.outputFormat() );
334+
}
335+
else if ( !forHits && mShared->mWFSVersion.startsWith( QLatin1String( "1.0" ) ) )
336+
{
337+
QStringList list;
338+
list << QLatin1String( "text/xml; subtype=gml/3.2.1" );
339+
list << QLatin1String( "application/gml+xml; version=3.2" );
340+
list << QLatin1String( "text/xml; subtype=gml/3.1.1" );
341+
list << QLatin1String( "application/gml+xml; version=3.1" );
342+
list << QLatin1String( "text/xml; subtype=gml/3.0.1" );
343+
list << QLatin1String( "application/gml+xml; version=3.0" );
344+
list << QLatin1String( "GML3" );
345+
Q_FOREACH ( const QString &format, list )
346+
{
347+
if ( mShared->mCaps.outputFormats.contains( format ) )
348+
{
349+
getFeatureUrl.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ),
350+
format );
351+
break;
352+
}
353+
}
354+
}
355+
331356
return getFeatureUrl;
332357
}
333358

tests/src/python/test_provider_wfs.py

+122
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,127 @@ def testWFS10(self):
488488
values = [f['INTFIELD'] for f in vl.getFeatures(request)]
489489
self.assertEqual(values, [100])
490490

491+
def testWFS10_outputformat_GML3(self):
492+
"""Test WFS 1.0 with OUTPUTFORMAT=GML3"""
493+
# We also test attribute fields in upper-case, and a field named GEOMETRY
494+
495+
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_WFS1.0_gml3'
496+
497+
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=1.0.0'), 'wb') as f:
498+
f.write("""
499+
<WFS_Capabilities version="1.0.0" xmlns="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc">
500+
<Capability>
501+
<Request>
502+
<GetFeature>
503+
<ResultFormat>
504+
<GML2/>
505+
<GML3/>
506+
</ResultFormat>
507+
</GetFeature>
508+
</Request>
509+
</Capability>
510+
<FeatureTypeList>
511+
<FeatureType>
512+
<Name>my:typename</Name>
513+
<Title>Title</Title>
514+
<Abstract>Abstract</Abstract>
515+
<SRS>EPSG:32631</SRS>
516+
<!-- in WFS 1.0, LatLongBoundingBox is in SRS units, not necessarily lat/long... -->
517+
<LatLongBoundingBox minx="400000" miny="5400000" maxx="450000" maxy="5500000"/>
518+
</FeatureType>
519+
</FeatureTypeList>
520+
</WFS_Capabilities>""".encode('UTF-8'))
521+
522+
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.0.0&TYPENAME=my:typename'), 'wb') as f:
523+
f.write("""
524+
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
525+
<xsd:import namespace="http://www.opengis.net/gml"/>
526+
<xsd:complexType name="typenameType">
527+
<xsd:complexContent>
528+
<xsd:extension base="gml:AbstractFeatureType">
529+
<xsd:sequence>
530+
<xsd:element maxOccurs="1" minOccurs="0" name="geometry" nillable="true" type="gml:PointPropertyType"/>
531+
</xsd:sequence>
532+
</xsd:extension>
533+
</xsd:complexContent>
534+
</xsd:complexType>
535+
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
536+
</xsd:schema>
537+
""".encode('UTF-8'))
538+
539+
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0'", 'test', 'WFS')
540+
assert vl.isValid()
541+
542+
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.0.0&TYPENAME=my:typename&SRSNAME=EPSG:32631&OUTPUTFORMAT=GML3'), 'wb') as f:
543+
f.write("""
544+
<wfs:FeatureCollection
545+
xmlns:wfs="http://www.opengis.net/wfs"
546+
xmlns:gml="http://www.opengis.net/gml"
547+
xmlns:my="http://my">
548+
<gml:boundedBy><gml:null>unknown</gml:null></gml:boundedBy>
549+
<gml:featureMember>
550+
<my:typename fid="typename.0">
551+
<my:geometry>
552+
<gml:Point srsName="urn:ogc:def:crs:EPSG::32631"><gml:coordinates decimal="." cs="," ts=" ">426858,5427937</gml:coordinates></gml:Point>
553+
</my:geometry>
554+
</my:typename>
555+
</gml:featureMember>
556+
</wfs:FeatureCollection>""".encode('UTF-8'))
557+
558+
got_f = [f for f in vl.getFeatures()]
559+
got = got_f[0].geometry().geometry()
560+
self.assertEqual((got.x(), got.y()), (426858.0, 5427937.0))
561+
562+
# Test with explicit OUTPUTFORMAT as parameter
563+
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0' outputformat='GML2'", 'test', 'WFS')
564+
assert vl.isValid()
565+
566+
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.0.0&TYPENAME=my:typename&SRSNAME=EPSG:32631&OUTPUTFORMAT=GML2'), 'wb') as f:
567+
f.write("""
568+
<wfs:FeatureCollection
569+
xmlns:wfs="http://www.opengis.net/wfs"
570+
xmlns:gml="http://www.opengis.net/gml"
571+
xmlns:my="http://my">
572+
<gml:boundedBy><gml:null>unknown</gml:null></gml:boundedBy>
573+
<gml:featureMember>
574+
<my:typename fid="typename.0">
575+
<my:geometry>
576+
<gml:Point srsName="urn:ogc:def:crs:EPSG::32631"><gml:coordinates decimal="." cs="," ts=" ">1,2</gml:coordinates></gml:Point>
577+
</my:geometry>
578+
</my:typename>
579+
</gml:featureMember>
580+
</wfs:FeatureCollection>""".encode('UTF-8'))
581+
582+
got_f = [f for f in vl.getFeatures()]
583+
got = got_f[0].geometry().geometry()
584+
self.assertEqual((got.x(), got.y()), (1.0, 2.0))
585+
586+
# Test with explicit OUTPUTFORMAT in URL
587+
# For some reason this fails on Travis (on assert vl.isValid()) whereas it works locally for me...
588+
if False:
589+
vl = QgsVectorLayer("url='http://" + endpoint + "?OUTPUTFORMAT=GML2' typename='my:typename' version='1.0.0'", 'test', 'WFS')
590+
assert vl.isValid()
591+
592+
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.0.0&TYPENAME=my:typename&SRSNAME=EPSG:32631&OUTPUTFORMAT=GML2'), 'wb') as f:
593+
f.write("""
594+
<wfs:FeatureCollection
595+
xmlns:wfs="http://www.opengis.net/wfs"
596+
xmlns:gml="http://www.opengis.net/gml"
597+
xmlns:my="http://my">
598+
<gml:boundedBy><gml:null>unknown</gml:null></gml:boundedBy>
599+
<gml:featureMember>
600+
<my:typename fid="typename.0">
601+
<my:geometry>
602+
<gml:Point srsName="urn:ogc:def:crs:EPSG::32631"><gml:coordinates decimal="." cs="," ts=" ">3,4</gml:coordinates></gml:Point>
603+
</my:geometry>
604+
</my:typename>
605+
</gml:featureMember>
606+
</wfs:FeatureCollection>""".encode('UTF-8'))
607+
608+
got_f = [f for f in vl.getFeatures()]
609+
got = got_f[0].geometry().geometry()
610+
self.assertEqual((got.x(), got.y()), (3.0, 4.0))
611+
491612
def testWFS10_latlongboundingbox_in_WGS84(self):
492613
"""Test WFS 1.0 with non conformatn LatLongBoundingBox"""
493614

@@ -2362,5 +2483,6 @@ def testWFS20TransactionsEnabled(self):
23622483
self.assertNotEqual(vl.dataProvider().capabilities() & vl.dataProvider().EditingCapabilities, 0)
23632484
self.assertEqual(vl.wkbType(), QgsWKBTypes.Point)
23642485

2486+
23652487
if __name__ == '__main__':
23662488
unittest.main()

0 commit comments

Comments
 (0)