Skip to content
Permalink
Browse files
[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 737719e4877647260cdc1e2fbd85684a242d81b1
@@ -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() )
@@ -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;
@@ -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;
@@ -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" );
@@ -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;
@@ -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 )
{
@@ -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
@@ -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 );
@@ -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 )
@@ -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();
@@ -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 ) &&
@@ -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 */
@@ -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;
}

@@ -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"""

@@ -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.