Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[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
  • Loading branch information
rouault committed Mar 17, 2017
1 parent 5541ad0 commit 737719e
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 10 deletions.
35 changes: 35 additions & 0 deletions src/providers/wfs/qgswfscapabilities.cpp
Expand Up @@ -142,6 +142,32 @@ void QgsWFSCapabilities::capabilitiesReplyFinished()
// Note: for conveniency, we do not use the elementsByTagNameNS() method as
// the WFS and OWS namespaces URI are not the same in all versions

if ( mCaps.version.startsWith( QLatin1String( "1.0" ) ) )
{
QDomElement capabilityElem = doc.firstChildElement( QStringLiteral( "Capability" ) );
if ( !capabilityElem.isNull() )
{
QDomElement requestElem = capabilityElem.firstChildElement( QStringLiteral( "Request" ) );
if ( !requestElem.isNull() )
{
QDomElement getFeatureElem = requestElem.firstChildElement( QStringLiteral( "GetFeature" ) );
if ( !getFeatureElem.isNull() )
{
QDomElement resultFormatElem = getFeatureElem.firstChildElement( QStringLiteral( "ResultFormat" ) );
if ( !resultFormatElem.isNull() )
{
QDomElement child = resultFormatElem.firstChildElement();
while ( !child.isNull() )
{
mCaps.outputFormats << child.tagName();
child = child.nextSiblingElement();
}
}
}
}
}
}

// find <ows:OperationsMetadata>
QDomElement operationsMetadataElem = doc.firstChildElement( "OperationsMetadata" );
if ( !operationsMetadataElem.isNull() )
Expand Down Expand Up @@ -231,6 +257,15 @@ void QgsWFSCapabilities::capabilitiesReplyFinished()
}
}
}
else if ( parameter.attribute( QStringLiteral( "name" ) ) == QLatin1String( "outputFormat" ) )
{
QDomNodeList valueList = parameter.elementsByTagName( QStringLiteral( "Value" ) );
for ( int k = 0; k < valueList.size(); ++k )
{
QDomElement value = valueList.at( k ).toElement();
mCaps.outputFormats << value.text();
}
}
}

break;
Expand Down
1 change: 1 addition & 0 deletions src/providers/wfs/qgswfscapabilities.h
Expand Up @@ -97,6 +97,7 @@ class QgsWFSCapabilities : public QgsWFSRequest
QList<Function> spatialPredicatesList;
QList<Function> functionList;
bool useEPSGColumnFormat; // whether to use EPSG:XXXX srsname
QList< QString > outputFormats;

QSet< QString > setAllTypenames;
QMap< QString, QString> mapUnprefixedTypenameToPrefixedTypename;
Expand Down
1 change: 1 addition & 0 deletions src/providers/wfs/qgswfsconstants.cpp
Expand Up @@ -31,6 +31,7 @@ const QString QgsWFSConstants::URI_PARAM_TYPENAME( "typename" );
const QString QgsWFSConstants::URI_PARAM_SRSNAME( "srsname" );
const QString QgsWFSConstants::URI_PARAM_BBOX( "bbox" );
const QString QgsWFSConstants::URI_PARAM_FILTER( "filter" );
const QString QgsWFSConstants::URI_PARAM_OUTPUTFORMAT( "outputformat" );
const QString QgsWFSConstants::URI_PARAM_RESTRICT_TO_REQUEST_BBOX( "restrictToRequestBBOX" );
const QString QgsWFSConstants::URI_PARAM_MAXNUMFEATURES( "maxNumFeatures" );
const QString QgsWFSConstants::URI_PARAM_IGNOREAXISORIENTATION( "IgnoreAxisOrientation" );
Expand Down
1 change: 1 addition & 0 deletions src/providers/wfs/qgswfsconstants.h
Expand Up @@ -38,6 +38,7 @@ struct QgsWFSConstants
static const QString URI_PARAM_TYPENAME;
static const QString URI_PARAM_SRSNAME;
static const QString URI_PARAM_FILTER;
static const QString URI_PARAM_OUTPUTFORMAT;
static const QString URI_PARAM_BBOX;
static const QString URI_PARAM_RESTRICT_TO_REQUEST_BBOX;
static const QString URI_PARAM_MAXNUMFEATURES;
Expand Down
68 changes: 59 additions & 9 deletions src/providers/wfs/qgswfsdatasourceuri.cpp
Expand Up @@ -22,13 +22,14 @@
QgsWFSDataSourceURI::QgsWFSDataSourceURI( const QString& uri )
: mURI( uri )
{
// Compatiblity with QGIS < 2.16 layer URI of the format
typedef QPair<QString, QString> queryItem;

// Compatibility with QGIS < 2.16 layer URI of the format
// http://example.com/?SERVICE=WFS&VERSION=1.0.0&REQUEST=GetFeature&TYPENAME=x&SRSNAME=y&username=foo&password=
if ( !mURI.hasParam( QgsWFSConstants::URI_PARAM_URL ) )
{
QUrl url( uri );
// Transform all param keys to lowercase
typedef QPair<QString, QString> queryItem;
QList<queryItem> items( url.queryItems() );
foreach ( queryItem item, items )
{
Expand All @@ -41,6 +42,7 @@ QgsWFSDataSourceURI::QgsWFSDataSourceURI( const QString& uri )
QString typeName = url.queryItemValue( QgsWFSConstants::URI_PARAM_TYPENAME );
QString version = url.queryItemValue( QgsWFSConstants::URI_PARAM_VERSION );
QString filter = url.queryItemValue( QgsWFSConstants::URI_PARAM_FILTER );
QString outputFormat = url.queryItemValue( QgsWFSConstants::URI_PARAM_OUTPUTFORMAT );
mAuth.mAuthCfg = url.queryItemValue( QgsWFSConstants::URI_PARAM_AUTHCFG );
// NOTE: A defined authcfg overrides any older username/password auth
// Only check for older auth if it is undefined
Expand All @@ -56,13 +58,14 @@ QgsWFSDataSourceURI::QgsWFSDataSourceURI( const QString& uri )
}

// Now remove all stuff that is not the core URL
url.removeQueryItem( "SERVICE" );
url.removeQueryItem( "VERSION" );
url.removeQueryItem( "TYPENAME" );
url.removeQueryItem( "REQUEST" );
url.removeQueryItem( "BBOX" );
url.removeQueryItem( "SRSNAME" );
url.removeQueryItem( "FILTER" );
url.removeQueryItem( "service" );
url.removeQueryItem( QgsWFSConstants::URI_PARAM_VERSION );
url.removeQueryItem( QgsWFSConstants::URI_PARAM_TYPENAME );
url.removeQueryItem( "request" );
url.removeQueryItem( QgsWFSConstants::URI_PARAM_BBOX );
url.removeQueryItem( QgsWFSConstants::URI_PARAM_SRSNAME );
url.removeQueryItem( QgsWFSConstants::URI_PARAM_FILTER );
url.removeQueryItem( QgsWFSConstants::URI_PARAM_OUTPUTFORMAT );
url.removeQueryItem( QgsWFSConstants::URI_PARAM_USERNAME );
url.removeQueryItem( QgsWFSConstants::URI_PARAM_PASSWORD );
url.removeQueryItem( QgsWFSConstants::URI_PARAM_AUTHCFG );
Expand All @@ -72,6 +75,7 @@ QgsWFSDataSourceURI::QgsWFSDataSourceURI( const QString& uri )
setTypeName( typeName );
setSRSName( srsname );
setVersion( version );
setOutputFormat( outputFormat );

//if the xml comes from the dialog, it needs to be a string to pass the validity test
if ( filter.startsWith( '\'' ) && filter.endsWith( '\'' ) && filter.size() > 1 )
Expand All @@ -86,6 +90,40 @@ QgsWFSDataSourceURI::QgsWFSDataSourceURI( const QString& uri )
}
else
{
QUrl url( mURI.param( QgsWFSConstants::URI_PARAM_URL ) );
bool URLModified = false;
bool somethingChanged = false;
do
{
somethingChanged = false;
QList<queryItem> items( url.queryItems() );
Q_FOREACH ( const queryItem &item, items )
{
const QString lowerName( item.first.toLower() );
if ( lowerName == QgsWFSConstants::URI_PARAM_OUTPUTFORMAT )
{
setOutputFormat( item.second );
url.removeQueryItem( item.first );
somethingChanged = true;
URLModified = true;
break;
}
else if ( lowerName == QLatin1String( "service" ) ||
lowerName == QLatin1String( "request" ) )
{
url.removeQueryItem( item.first );
somethingChanged = true;
URLModified = true;
break;
}
}
}
while ( somethingChanged );
if ( URLModified )
{
mURI.setParam( QgsWFSConstants::URI_PARAM_URL, url.toEncoded() );
}

mAuth.mUserName = mURI.username();
mAuth.mPassword = mURI.password();
mAuth.mAuthCfg = mURI.authConfigId();
Expand Down Expand Up @@ -201,6 +239,18 @@ void QgsWFSDataSourceURI::setSql( const QString& sql )
mURI.setSql( sql );
}

QString QgsWFSDataSourceURI::outputFormat() const
{
return mURI.param( QgsWFSConstants::URI_PARAM_OUTPUTFORMAT );
}

void QgsWFSDataSourceURI::setOutputFormat( const QString &outputFormat )
{
mURI.removeParam( QgsWFSConstants::URI_PARAM_OUTPUTFORMAT );
if ( !outputFormat.isEmpty() )
mURI.setParam( QgsWFSConstants::URI_PARAM_OUTPUTFORMAT, outputFormat );
}

bool QgsWFSDataSourceURI::isRestrictedToRequestBBOX() const
{
if ( mURI.hasParam( QgsWFSConstants::URI_PARAM_RESTRICT_TO_REQUEST_BBOX ) &&
Expand Down
8 changes: 7 additions & 1 deletion src/providers/wfs/qgswfsdatasourceuri.h
Expand Up @@ -117,7 +117,13 @@ class QgsWFSDataSourceURI
/** Set SQL query */
void setSql( const QString& sql );

/** Returns whether GetFeature request should include the request bounding box. Defaults to false */
//! Get GetFeature output format
QString outputFormat() const;

//! Set GetFeature output format
void setOutputFormat( const QString &outputFormat );

//! Returns whether GetFeature request should include the request bounding box. Defaults to false
bool isRestrictedToRequestBBOX() const;

/** Returns whether axis orientation should be ignored (for WFS >= 1.1). Defaults to false */
Expand Down
25 changes: 25 additions & 0 deletions src/providers/wfs/qgswfsfeatureiterator.cpp
Expand Up @@ -328,6 +328,31 @@ QUrl QgsWFSFeatureDownloader::buildURL( int startIndex, int maxFeatures, bool fo
getFeatureUrl.addQueryItem( "SORTBY", mShared->mSortBy );
}

if ( !forHits && !mShared->mURI.outputFormat().isEmpty() )
{
getFeatureUrl.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), mShared->mURI.outputFormat() );
}
else if ( !forHits && mShared->mWFSVersion.startsWith( QLatin1String( "1.0" ) ) )
{
QStringList list;
list << QLatin1String( "text/xml; subtype=gml/3.2.1" );
list << QLatin1String( "application/gml+xml; version=3.2" );
list << QLatin1String( "text/xml; subtype=gml/3.1.1" );
list << QLatin1String( "application/gml+xml; version=3.1" );
list << QLatin1String( "text/xml; subtype=gml/3.0.1" );
list << QLatin1String( "application/gml+xml; version=3.0" );
list << QLatin1String( "GML3" );
Q_FOREACH ( const QString &format, list )
{
if ( mShared->mCaps.outputFormats.contains( format ) )
{
getFeatureUrl.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ),
format );
break;
}
}
}

return getFeatureUrl;
}

Expand Down
122 changes: 122 additions & 0 deletions tests/src/python/test_provider_wfs.py
Expand Up @@ -488,6 +488,127 @@ def testWFS10(self):
values = [f['INTFIELD'] for f in vl.getFeatures(request)]
self.assertEqual(values, [100])

def testWFS10_outputformat_GML3(self):
"""Test WFS 1.0 with OUTPUTFORMAT=GML3"""
# We also test attribute fields in upper-case, and a field named GEOMETRY

endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_WFS1.0_gml3'

with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=1.0.0'), 'wb') as f:
f.write("""
<WFS_Capabilities version="1.0.0" xmlns="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc">
<Capability>
<Request>
<GetFeature>
<ResultFormat>
<GML2/>
<GML3/>
</ResultFormat>
</GetFeature>
</Request>
</Capability>
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<SRS>EPSG:32631</SRS>
<!-- in WFS 1.0, LatLongBoundingBox is in SRS units, not necessarily lat/long... -->
<LatLongBoundingBox minx="400000" miny="5400000" maxx="450000" maxy="5500000"/>
</FeatureType>
</FeatureTypeList>
</WFS_Capabilities>""".encode('UTF-8'))

with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.0.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<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">
<xsd:import namespace="http://www.opengis.net/gml"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" name="geometry" nillable="true" type="gml:PointPropertyType"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
</xsd:schema>
""".encode('UTF-8'))

vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0'", 'test', 'WFS')
assert vl.isValid()

with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.0.0&TYPENAME=my:typename&SRSNAME=EPSG:32631&OUTPUTFORMAT=GML3'), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml"
xmlns:my="http://my">
<gml:boundedBy><gml:null>unknown</gml:null></gml:boundedBy>
<gml:featureMember>
<my:typename fid="typename.0">
<my:geometry>
<gml:Point srsName="urn:ogc:def:crs:EPSG::32631"><gml:coordinates decimal="." cs="," ts=" ">426858,5427937</gml:coordinates></gml:Point>
</my:geometry>
</my:typename>
</gml:featureMember>
</wfs:FeatureCollection>""".encode('UTF-8'))

got_f = [f for f in vl.getFeatures()]
got = got_f[0].geometry().geometry()
self.assertEqual((got.x(), got.y()), (426858.0, 5427937.0))

# Test with explicit OUTPUTFORMAT as parameter
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0' outputformat='GML2'", 'test', 'WFS')
assert vl.isValid()

with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.0.0&TYPENAME=my:typename&SRSNAME=EPSG:32631&OUTPUTFORMAT=GML2'), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml"
xmlns:my="http://my">
<gml:boundedBy><gml:null>unknown</gml:null></gml:boundedBy>
<gml:featureMember>
<my:typename fid="typename.0">
<my:geometry>
<gml:Point srsName="urn:ogc:def:crs:EPSG::32631"><gml:coordinates decimal="." cs="," ts=" ">1,2</gml:coordinates></gml:Point>
</my:geometry>
</my:typename>
</gml:featureMember>
</wfs:FeatureCollection>""".encode('UTF-8'))

got_f = [f for f in vl.getFeatures()]
got = got_f[0].geometry().geometry()
self.assertEqual((got.x(), got.y()), (1.0, 2.0))

# Test with explicit OUTPUTFORMAT in URL
# For some reason this fails on Travis (on assert vl.isValid()) whereas it works locally for me...
if False:
vl = QgsVectorLayer("url='http://" + endpoint + "?OUTPUTFORMAT=GML2' typename='my:typename' version='1.0.0'", 'test', 'WFS')
assert vl.isValid()

with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.0.0&TYPENAME=my:typename&SRSNAME=EPSG:32631&OUTPUTFORMAT=GML2'), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml"
xmlns:my="http://my">
<gml:boundedBy><gml:null>unknown</gml:null></gml:boundedBy>
<gml:featureMember>
<my:typename fid="typename.0">
<my:geometry>
<gml:Point srsName="urn:ogc:def:crs:EPSG::32631"><gml:coordinates decimal="." cs="," ts=" ">3,4</gml:coordinates></gml:Point>
</my:geometry>
</my:typename>
</gml:featureMember>
</wfs:FeatureCollection>""".encode('UTF-8'))

got_f = [f for f in vl.getFeatures()]
got = got_f[0].geometry().geometry()
self.assertEqual((got.x(), got.y()), (3.0, 4.0))

def testWFS10_latlongboundingbox_in_WGS84(self):
"""Test WFS 1.0 with non conformatn LatLongBoundingBox"""

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


if __name__ == '__main__':
unittest.main()

0 comments on commit 737719e

Please sign in to comment.