Skip to content

Commit

Permalink
Merge pull request #4277 from rouault/outputformat_gml3_2.18
Browse files Browse the repository at this point in the history
[WFS provider] Select GML3 output format for WFS 1.0 when available
  • Loading branch information
rouault authored Mar 17, 2017
2 parents 5541ad0 + 737719e commit 707ff84
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 707ff84

Please sign in to comment.