Skip to content

Commit

Permalink
[WFS provider] Add heuristics to detect MapServer WFS 1.1 behaviour (…
Browse files Browse the repository at this point in the history
…sometimes)

Fix #15061

MapServer honours EPSG axis order in WFS 1.1, but returns srsName in GetFeature
response with EPSG:XXXX syntax instead of urn EPSG srs. This confuses the GML
parser that thinks that no axis inversion should then happen.

The heuristics here consist in checking the envelope of the response with the
capabilities extent.

This should be safe and should work for layers with non global extent, but will
not detect all issues.

Proper fix is either to force WFS 1.0, or upgrade to MapServer 7.0 with WFS 2.0
  • Loading branch information
rouault committed Jun 18, 2016
1 parent eee599e commit f354998
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 9 deletions.
16 changes: 11 additions & 5 deletions src/core/qgsgml.cpp
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -881,10 +881,15 @@ void QgsGmlStreamingParser::endElement( const XML_Char* el )
//create bounding box from mStringCash //create bounding box from mStringCash
if ( mCurrentExtent.isNull() && if ( mCurrentExtent.isNull() &&
!mBoundedByNullFound && !mBoundedByNullFound &&
createBBoxFromCoordinateString( mCurrentExtent, mStringCash ) != 0 ) !createBBoxFromCoordinateString( mCurrentExtent, mStringCash ) )
{ {
QgsDebugMsg( "creation of bounding box failed" ); QgsDebugMsg( "creation of bounding box failed" );
} }
if ( !mCurrentExtent.isNull() && mLayerExtent.isNull() &&
mCurrentFeature == nullptr && mFeatureCount == 0 )
{
mLayerExtent = mCurrentExtent;
}


mParseModeStack.pop(); mParseModeStack.pop();
} }
Expand Down Expand Up @@ -1198,6 +1203,7 @@ int QgsGmlStreamingParser::readEpsgFromAttribute( int& epsgNr, const XML_Char**
return 1; return 1;
} }
epsgNr = eNr; epsgNr = eNr;
mSrsName = epsgString;


QgsCoordinateReferenceSystem crs = QgsCRSCache::instance()->crsByOgcWmsCrs( QString( "EPSG:%1" ).arg( epsgNr ) ); QgsCoordinateReferenceSystem crs = QgsCRSCache::instance()->crsByOgcWmsCrs( QString( "EPSG:%1" ).arg( epsgNr ) );
if ( crs.isValid() ) if ( crs.isValid() )
Expand Down Expand Up @@ -1230,22 +1236,22 @@ QString QgsGmlStreamingParser::readAttribute( const QString& attributeName, cons
return QString(); return QString();
} }


int QgsGmlStreamingParser::createBBoxFromCoordinateString( QgsRectangle &r, const QString& coordString ) const bool QgsGmlStreamingParser::createBBoxFromCoordinateString( QgsRectangle &r, const QString& coordString ) const
{ {
QList<QgsPoint> points; QList<QgsPoint> points;
if ( pointsFromCoordinateString( points, coordString ) != 0 ) if ( pointsFromCoordinateString( points, coordString ) != 0 )
{ {
return 2; return false;
} }


if ( points.size() < 2 ) if ( points.size() < 2 )
{ {
return 3; return false;
} }


r.set( points[0], points[1] ); r.set( points[0], points[1] );


return 0; return true;
} }


int QgsGmlStreamingParser::pointsFromCoordinateString( QList<QgsPoint>& points, const QString& coordString ) const int QgsGmlStreamingParser::pointsFromCoordinateString( QList<QgsPoint>& points, const QString& coordString ) const
Expand Down
15 changes: 12 additions & 3 deletions src/core/qgsgml.h
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ class CORE_EXPORT QgsGmlStreamingParser
/** Return the EPSG code, or 0 if unknown */ /** Return the EPSG code, or 0 if unknown */
int getEPSGCode() const { return mEpsg; } int getEPSGCode() const { return mEpsg; }


/** Return the value of the srsName attribute */
const QString& srsName() const { return mSrsName; }

/** Return layer bounding box */
const QgsRectangle& layerExtent() const { return mLayerExtent; }

/** Return the geometry type */ /** Return the geometry type */
QGis::WkbType wkbType() const { return mWkbType; } QGis::WkbType wkbType() const { return mWkbType; }


Expand Down Expand Up @@ -182,9 +188,8 @@ class CORE_EXPORT QgsGmlStreamingParser
@return attribute value or an empty string if no such attribute @return attribute value or an empty string if no such attribute
*/ */
QString readAttribute( const QString& attributeName, const XML_Char** attr ) const; QString readAttribute( const QString& attributeName, const XML_Char** attr ) const;
/** Creates a rectangle from a coordinate string. /** Creates a rectangle from a coordinate string. */
@return 0 in case of success*/ bool createBBoxFromCoordinateString( QgsRectangle &bb, const QString& coordString ) const;
int createBBoxFromCoordinateString( QgsRectangle &bb, const QString& coordString ) const;
/** Creates a set of points from a coordinate string. /** Creates a set of points from a coordinate string.
@param points list that will contain the created points @param points list that will contain the created points
@param coordString the text containing the coordinates @param coordString the text containing the coordinates
Expand Down Expand Up @@ -285,6 +290,10 @@ class CORE_EXPORT QgsGmlStreamingParser
ParseMode mCoorMode; ParseMode mCoorMode;
/** EPSG of parsed features geometries */ /** EPSG of parsed features geometries */
int mEpsg; int mEpsg;
/** Literal srsName attribute */
QString mSrsName;
/** Layer bounding box */
QgsRectangle mLayerExtent;
/** GML namespace URI */ /** GML namespace URI */
QString mGMLNameSpaceURI; QString mGMLNameSpaceURI;
const char* mGMLNameSpaceURIPtr; const char* mGMLNameSpaceURIPtr;
Expand Down
32 changes: 31 additions & 1 deletion src/providers/wfs/qgswfsfeatureiterator.cpp
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "qgswfsprovider.h" #include "qgswfsprovider.h"
#include "qgswfsshareddata.h" #include "qgswfsshareddata.h"
#include "qgswfsutils.h" #include "qgswfsutils.h"
#include "qgscrscache.h"


#include <QDir> #include <QDir>
#include <QProgressDialog> #include <QProgressDialog>
Expand Down Expand Up @@ -540,11 +541,34 @@ void QgsWFSFeatureDownloader::run( bool serializeFeatures, int maxFeatures )


if ( featurePtrList.size() != 0 ) if ( featurePtrList.size() != 0 )
{ {
// Heuristics to try to detect MapServer WFS 1.1 that honours EPSG axis order, but returns
// EPSG:XXXX srsName and not EPSG urns
if ( pagingIter == 1 && featureCountForThisResponse == 0 &&
mShared->mWFSVersion.startsWith( "1.1" ) &&
parser->srsName().startsWith( "EPSG:" ) &&
!parser->layerExtent().isNull() &&
!mShared->mURI.ignoreAxisOrientation() &&
!mShared->mURI.invertAxisOrientation() )
{
QgsCoordinateReferenceSystem crs = QgsCRSCache::instance()->crsByOgcWmsCrs( parser->srsName() );
if ( crs.isValid() && crs.axisInverted() &&
!mShared->mCapabilityExtent.contains( parser->layerExtent() ) )
{
QgsRectangle invertedRectangle( parser->layerExtent() );
invertedRectangle.invert();
if ( mShared->mCapabilityExtent.contains( invertedRectangle ) )
{
mShared->mGetFeatureEPSGDotHonoursEPSGOrder = true;
QgsDebugMsg( "Server is likely MapServer. Using mGetFeatureEPSGDotHonoursEPSGOrder mode" );
}
}
}

QVector<QgsWFSFeatureGmlIdPair> featureList; QVector<QgsWFSFeatureGmlIdPair> featureList;
for ( int i = 0;i < featurePtrList.size();i++ ) for ( int i = 0;i < featurePtrList.size();i++ )
{ {
QgsGmlStreamingParser::QgsGmlFeaturePtrGmlIdPair& featPair = featurePtrList[i]; QgsGmlStreamingParser::QgsGmlFeaturePtrGmlIdPair& featPair = featurePtrList[i];
const QgsFeature& f = *( featPair.first ); QgsFeature& f = *( featPair.first );
QString gmlId( featPair.second ); QString gmlId( featPair.second );
if ( gmlId.isEmpty() ) if ( gmlId.isEmpty() )
{ {
Expand All @@ -566,6 +590,12 @@ void QgsWFSFeatureDownloader::run( bool serializeFeatures, int maxFeatures )
disablePaging = true; disablePaging = true;
QgsDebugMsg( "Server does not seem to properly support paging since it returned the same first feature for 2 different page requests. Disabling paging" ); QgsDebugMsg( "Server does not seem to properly support paging since it returned the same first feature for 2 different page requests. Disabling paging" );
} }

if ( mShared->mGetFeatureEPSGDotHonoursEPSGOrder && f.geometry() )
{
f.geometry()->transform( QTransform( 0, 1, 1, 0, 0, 0 ) );
}

featureList.push_back( QgsWFSFeatureGmlIdPair( f, gmlId ) ); featureList.push_back( QgsWFSFeatureGmlIdPair( f, gmlId ) );
delete featPair.first; delete featPair.first;
if (( i > 0 && ( i % 1000 ) == 0 ) || i + 1 == featurePtrList.size() ) if (( i > 0 && ( i % 1000 ) == 0 ) || i + 1 == featurePtrList.size() )
Expand Down
1 change: 1 addition & 0 deletions src/providers/wfs/qgswfsshareddata.cpp
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ QgsWFSSharedData::QgsWFSSharedData( const QString& uri )
, mHideProgressDialog( mURI.hideDownloadProgressDialog() ) , mHideProgressDialog( mURI.hideDownloadProgressDialog() )
, mDistinctSelect( false ) , mDistinctSelect( false )
, mHasWarnedAboutMissingFeatureId( false ) , mHasWarnedAboutMissingFeatureId( false )
, mGetFeatureEPSGDotHonoursEPSGOrder( false )
, mDownloader( nullptr ) , mDownloader( nullptr )
, mDownloadFinished( false ) , mDownloadFinished( false )
, mGenCounter( 0 ) , mGenCounter( 0 )
Expand Down
4 changes: 4 additions & 0 deletions src/providers/wfs/qgswfsshareddata.h
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ class QgsWFSSharedData : public QObject
/** Create GML parser */ /** Create GML parser */
QgsGmlStreamingParser* createParser(); QgsGmlStreamingParser* createParser();


/** If the server (typically MapServer WFS 1.1) honours EPSG axis order, but returns
EPSG:XXXX srsName and not EPSG urns */
bool mGetFeatureEPSGDotHonoursEPSGOrder;

private: private:


/** Main mutex to protect most data members that can be modified concurrently */ /** Main mutex to protect most data members that can be modified concurrently */
Expand Down
88 changes: 88 additions & 0 deletions tests/src/python/test_provider_wfs.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -2020,6 +2020,94 @@ def testGeomedia(self):
self.assertEqual(features[0]['intfield'], 1) self.assertEqual(features[0]['intfield'], 1)
self.assertEqual(features[1]['intfield'], 2) self.assertEqual(features[1]['intfield'], 2)


def testMapServerWFS1_1_EPSG_4326(self):
"""Test interoperability with MapServer WFS 1.1."""

endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_mapserver_wfs_1_1'

with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=1.1.0'), 'wb') as f:
f.write("""
<wfs:WFS_Capabilities version="1.1.0" xmlns="http://www.opengis.net/wfs" xmlns:wfs="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc" xmlns:ows="http://www.opengis.net/ows" xmlns:gml="http://schemas.opengis.net/gml">
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
<ows:WGS84BoundingBox>
<ows:LowerCorner>2 49</ows:LowerCorner>
<ows:UpperCorner>2 49</ows:UpperCorner>
</ows:WGS84BoundingBox>
</FeatureType>
</FeatureTypeList>
</wfs:WFS_Capabilities>""".encode('UTF-8'))

with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.1.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<schema
targetNamespace="http://my"
xmlns:my="http://my"
xmlns:ogc="http://www.opengis.net/ogc"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://www.w3.org/2001/XMLSchema"
xmlns:gml="http://www.opengis.net/gml"
elementFormDefault="qualified" version="0.1" >
<import namespace="http://www.opengis.net/gml"
schemaLocation="http://schemas.opengis.net/gml/3.1.1/base/gml.xsd" />
<element name="typename"
type="my:typenameType"
substitutionGroup="gml:_Feature" />
<complexType name="typenameType">
<complexContent>
<extension base="gml:AbstractFeatureType">
<sequence>
<element name="geometryProperty" type="gml:GeometryPropertyType" minOccurs="0" maxOccurs="1"/>
</sequence>
</extension>
</complexContent>
</complexType>
</schema>
""".encode('UTF-8'))

with open(sanitize(endpoint, """?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.1.0&TYPENAME=my:typename&SRSNAME=urn:ogc:def:crs:EPSG::4326"""), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:my="http://my"
xmlns:gml="http://www.opengis.net/gml"
xmlns:wfs="http://www.opengis.net/wfs"
xmlns:ogc="http://www.opengis.net/ogc">
<gml:boundedBy>
<gml:Envelope srsName="EPSG:4326">
<gml:lowerCorner>49.000000 2.000000</gml:lowerCorner>
<gml:upperCorner>49.000000 2.000000</gml:upperCorner>
</gml:Envelope>
</gml:boundedBy>
<gml:featureMember>
<my:typename gml:id="typename.1">
<gml:boundedBy>
<gml:Envelope srsName="EPSG:4326">
<gml:lowerCorner>49.000000 2.000000</gml:lowerCorner>
<gml:upperCorner>49.000000 2.000000</gml:upperCorner>
</gml:Envelope>
</gml:boundedBy>
<my:geometryProperty>
<gml:Point srsName="EPSG:4326">
<gml:pos>49.000000 2.000000</gml:pos>
</gml:Point>
</my:geometryProperty>
</my:typename>
</gml:featureMember>
</wfs:FeatureCollection>
""".encode('UTF-8'))

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

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



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

0 comments on commit f354998

Please sign in to comment.