Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #4270 from rouault/wfs_outputformat
[WFS provider] Select GML3 output format for WFS 1.0 when available
  • Loading branch information
rouault committed Mar 17, 2017
2 parents 1337ee3 + 852f01b commit ed1b0a2
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 8 deletions.
35 changes: 35 additions & 0 deletions src/providers/wfs/qgswfscapabilities.cpp
Expand Up @@ -141,6 +141,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( QStringLiteral( "OperationsMetadata" ) );
if ( !operationsMetadataElem.isNull() )
Expand Down Expand Up @@ -230,6 +256,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( QStringLiteral( "typename" )
const QString QgsWFSConstants::URI_PARAM_SRSNAME( QStringLiteral( "srsname" ) );
const QString QgsWFSConstants::URI_PARAM_BBOX( QStringLiteral( "bbox" ) );
const QString QgsWFSConstants::URI_PARAM_FILTER( QStringLiteral( "filter" ) );
const QString QgsWFSConstants::URI_PARAM_OUTPUTFORMAT( QStringLiteral( "outputformat" ) );
const QString QgsWFSConstants::URI_PARAM_RESTRICT_TO_REQUEST_BBOX( QStringLiteral( "restrictToRequestBBOX" ) );
const QString QgsWFSConstants::URI_PARAM_MAXNUMFEATURES( QStringLiteral( "maxNumFeatures" ) );
const QString QgsWFSConstants::URI_PARAM_IGNOREAXISORIENTATION( QStringLiteral( "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
66 changes: 58 additions & 8 deletions src/providers/wfs/qgswfsdatasourceuri.cpp
Expand Up @@ -22,13 +22,14 @@
QgsWFSDataSourceURI::QgsWFSDataSourceURI( const QString &uri )
: mURI( uri )
{
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() );
Q_FOREACH ( const 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( QStringLiteral( "SERVICE" ) );
url.removeQueryItem( QStringLiteral( "VERSION" ) );
url.removeQueryItem( QStringLiteral( "TYPENAME" ) );
url.removeQueryItem( QStringLiteral( "REQUEST" ) );
url.removeQueryItem( QStringLiteral( "BBOX" ) );
url.removeQueryItem( QStringLiteral( "SRSNAME" ) );
url.removeQueryItem( QStringLiteral( "FILTER" ) );
url.removeQueryItem( QStringLiteral( "service" ) );
url.removeQueryItem( QgsWFSConstants::URI_PARAM_VERSION );
url.removeQueryItem( QgsWFSConstants::URI_PARAM_TYPENAME );
url.removeQueryItem( QStringLiteral( "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
6 changes: 6 additions & 0 deletions src/providers/wfs/qgswfsdatasourceuri.h
Expand Up @@ -117,6 +117,12 @@ class QgsWFSDataSourceURI
//! Set SQL query
void setSql( const QString &sql );

//! 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;

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( QStringLiteral( "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
119 changes: 119 additions & 0 deletions tests/src/python/test_provider_wfs.py
Expand Up @@ -489,6 +489,125 @@ 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
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

0 comments on commit ed1b0a2

Please sign in to comment.