Skip to content

Commit ed1b0a2

Browse files
authored
Merge pull request #4270 from rouault/wfs_outputformat
[WFS provider] Select GML3 output format for WFS 1.0 when available
2 parents 1337ee3 + 852f01b commit ed1b0a2

8 files changed

+246
-8
lines changed

src/providers/wfs/qgswfscapabilities.cpp

+35
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

+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( 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

+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

+58-8
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

+6
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

+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( 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

+119
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)