Skip to content

Commit 852f01b

Browse files
committedMar 16, 2017
[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.
1 parent 2654c72 commit 852f01b

File tree

8 files changed

+246
-8
lines changed

8 files changed

+246
-8
lines changed
 

‎src/providers/wfs/qgswfscapabilities.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,32 @@ void QgsWfsCapabilities::capabilitiesReplyFinished()
141141
// Note: for conveniency, we do not use the elementsByTagNameNS() method as
142142
// the WFS and OWS namespaces URI are not the same in all versions
143143

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

235270
break;

‎src/providers/wfs/qgswfscapabilities.h

Lines changed: 1 addition & 0 deletions
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

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

‎src/providers/wfs/qgswfsconstants.h

Lines changed: 1 addition & 0 deletions
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

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,14 @@
2222
QgsWFSDataSourceURI::QgsWFSDataSourceURI( const QString &uri )
2323
: mURI( uri )
2424
{
25+
typedef QPair<QString, QString> queryItem;
26+
2527
// 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
Q_FOREACH ( const 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( QStringLiteral( "SERVICE" ) );
60-
url.removeQueryItem( QStringLiteral( "VERSION" ) );
61-
url.removeQueryItem( QStringLiteral( "TYPENAME" ) );
62-
url.removeQueryItem( QStringLiteral( "REQUEST" ) );
63-
url.removeQueryItem( QStringLiteral( "BBOX" ) );
64-
url.removeQueryItem( QStringLiteral( "SRSNAME" ) );
65-
url.removeQueryItem( QStringLiteral( "FILTER" ) );
61+
url.removeQueryItem( QStringLiteral( "service" ) );
62+
url.removeQueryItem( QgsWFSConstants::URI_PARAM_VERSION );
63+
url.removeQueryItem( QgsWFSConstants::URI_PARAM_TYPENAME );
64+
url.removeQueryItem( QStringLiteral( "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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ class QgsWFSDataSourceURI
117117
//! Set SQL query
118118
void setSql( const QString &sql );
119119

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

‎src/providers/wfs/qgswfsfeatureiterator.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,31 @@ QUrl QgsWFSFeatureDownloader::buildURL( int startIndex, int maxFeatures, bool fo
328328
getFeatureUrl.addQueryItem( QStringLiteral( "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

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,125 @@ def testWFS10(self):
489489
values = [f['INTFIELD'] for f in vl.getFeatures(request)]
490490
self.assertEqual(values, [100])
491491

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

0 commit comments

Comments
 (0)
Please sign in to comment.