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.
  • Loading branch information
rouault committed Mar 16, 2017
1 parent 2654c72 commit 852f01b0f92d79b5786ef2f63c6c329067128e88
@@ -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() )
@@ -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;
@@ -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( 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" ) );
@@ -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 )
{
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 )
{
@@ -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( 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 );
@@ -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,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;

@@ -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;
}

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

0 comments on commit 852f01b

Please sign in to comment.