Skip to content

Commit f354998

Browse files
committed
[WFS provider] Add heuristics to detect MapServer WFS 1.1 behaviour (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
1 parent eee599e commit f354998

File tree

6 files changed

+147
-9
lines changed

6 files changed

+147
-9
lines changed

src/core/qgsgml.cpp

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -881,10 +881,15 @@ void QgsGmlStreamingParser::endElement( const XML_Char* el )
881881
//create bounding box from mStringCash
882882
if ( mCurrentExtent.isNull() &&
883883
!mBoundedByNullFound &&
884-
createBBoxFromCoordinateString( mCurrentExtent, mStringCash ) != 0 )
884+
!createBBoxFromCoordinateString( mCurrentExtent, mStringCash ) )
885885
{
886886
QgsDebugMsg( "creation of bounding box failed" );
887887
}
888+
if ( !mCurrentExtent.isNull() && mLayerExtent.isNull() &&
889+
mCurrentFeature == nullptr && mFeatureCount == 0 )
890+
{
891+
mLayerExtent = mCurrentExtent;
892+
}
888893

889894
mParseModeStack.pop();
890895
}
@@ -1198,6 +1203,7 @@ int QgsGmlStreamingParser::readEpsgFromAttribute( int& epsgNr, const XML_Char**
11981203
return 1;
11991204
}
12001205
epsgNr = eNr;
1206+
mSrsName = epsgString;
12011207

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

1233-
int QgsGmlStreamingParser::createBBoxFromCoordinateString( QgsRectangle &r, const QString& coordString ) const
1239+
bool QgsGmlStreamingParser::createBBoxFromCoordinateString( QgsRectangle &r, const QString& coordString ) const
12341240
{
12351241
QList<QgsPoint> points;
12361242
if ( pointsFromCoordinateString( points, coordString ) != 0 )
12371243
{
1238-
return 2;
1244+
return false;
12391245
}
12401246

12411247
if ( points.size() < 2 )
12421248
{
1243-
return 3;
1249+
return false;
12441250
}
12451251

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

1248-
return 0;
1254+
return true;
12491255
}
12501256

12511257
int QgsGmlStreamingParser::pointsFromCoordinateString( QList<QgsPoint>& points, const QString& coordString ) const

src/core/qgsgml.h

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ class CORE_EXPORT QgsGmlStreamingParser
105105
/** Return the EPSG code, or 0 if unknown */
106106
int getEPSGCode() const { return mEpsg; }
107107

108+
/** Return the value of the srsName attribute */
109+
const QString& srsName() const { return mSrsName; }
110+
111+
/** Return layer bounding box */
112+
const QgsRectangle& layerExtent() const { return mLayerExtent; }
113+
108114
/** Return the geometry type */
109115
QGis::WkbType wkbType() const { return mWkbType; }
110116

@@ -182,9 +188,8 @@ class CORE_EXPORT QgsGmlStreamingParser
182188
@return attribute value or an empty string if no such attribute
183189
*/
184190
QString readAttribute( const QString& attributeName, const XML_Char** attr ) const;
185-
/** Creates a rectangle from a coordinate string.
186-
@return 0 in case of success*/
187-
int createBBoxFromCoordinateString( QgsRectangle &bb, const QString& coordString ) const;
191+
/** Creates a rectangle from a coordinate string. */
192+
bool createBBoxFromCoordinateString( QgsRectangle &bb, const QString& coordString ) const;
188193
/** Creates a set of points from a coordinate string.
189194
@param points list that will contain the created points
190195
@param coordString the text containing the coordinates
@@ -285,6 +290,10 @@ class CORE_EXPORT QgsGmlStreamingParser
285290
ParseMode mCoorMode;
286291
/** EPSG of parsed features geometries */
287292
int mEpsg;
293+
/** Literal srsName attribute */
294+
QString mSrsName;
295+
/** Layer bounding box */
296+
QgsRectangle mLayerExtent;
288297
/** GML namespace URI */
289298
QString mGMLNameSpaceURI;
290299
const char* mGMLNameSpaceURIPtr;

src/providers/wfs/qgswfsfeatureiterator.cpp

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "qgswfsprovider.h"
2525
#include "qgswfsshareddata.h"
2626
#include "qgswfsutils.h"
27+
#include "qgscrscache.h"
2728

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

541542
if ( featurePtrList.size() != 0 )
542543
{
544+
// Heuristics to try to detect MapServer WFS 1.1 that honours EPSG axis order, but returns
545+
// EPSG:XXXX srsName and not EPSG urns
546+
if ( pagingIter == 1 && featureCountForThisResponse == 0 &&
547+
mShared->mWFSVersion.startsWith( "1.1" ) &&
548+
parser->srsName().startsWith( "EPSG:" ) &&
549+
!parser->layerExtent().isNull() &&
550+
!mShared->mURI.ignoreAxisOrientation() &&
551+
!mShared->mURI.invertAxisOrientation() )
552+
{
553+
QgsCoordinateReferenceSystem crs = QgsCRSCache::instance()->crsByOgcWmsCrs( parser->srsName() );
554+
if ( crs.isValid() && crs.axisInverted() &&
555+
!mShared->mCapabilityExtent.contains( parser->layerExtent() ) )
556+
{
557+
QgsRectangle invertedRectangle( parser->layerExtent() );
558+
invertedRectangle.invert();
559+
if ( mShared->mCapabilityExtent.contains( invertedRectangle ) )
560+
{
561+
mShared->mGetFeatureEPSGDotHonoursEPSGOrder = true;
562+
QgsDebugMsg( "Server is likely MapServer. Using mGetFeatureEPSGDotHonoursEPSGOrder mode" );
563+
}
564+
}
565+
}
566+
543567
QVector<QgsWFSFeatureGmlIdPair> featureList;
544568
for ( int i = 0;i < featurePtrList.size();i++ )
545569
{
546570
QgsGmlStreamingParser::QgsGmlFeaturePtrGmlIdPair& featPair = featurePtrList[i];
547-
const QgsFeature& f = *( featPair.first );
571+
QgsFeature& f = *( featPair.first );
548572
QString gmlId( featPair.second );
549573
if ( gmlId.isEmpty() )
550574
{
@@ -566,6 +590,12 @@ void QgsWFSFeatureDownloader::run( bool serializeFeatures, int maxFeatures )
566590
disablePaging = true;
567591
QgsDebugMsg( "Server does not seem to properly support paging since it returned the same first feature for 2 different page requests. Disabling paging" );
568592
}
593+
594+
if ( mShared->mGetFeatureEPSGDotHonoursEPSGOrder && f.geometry() )
595+
{
596+
f.geometry()->transform( QTransform( 0, 1, 1, 0, 0, 0 ) );
597+
}
598+
569599
featureList.push_back( QgsWFSFeatureGmlIdPair( f, gmlId ) );
570600
delete featPair.first;
571601
if (( i > 0 && ( i % 1000 ) == 0 ) || i + 1 == featurePtrList.size() )

src/providers/wfs/qgswfsshareddata.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ QgsWFSSharedData::QgsWFSSharedData( const QString& uri )
4343
, mHideProgressDialog( mURI.hideDownloadProgressDialog() )
4444
, mDistinctSelect( false )
4545
, mHasWarnedAboutMissingFeatureId( false )
46+
, mGetFeatureEPSGDotHonoursEPSGOrder( false )
4647
, mDownloader( nullptr )
4748
, mDownloadFinished( false )
4849
, mGenCounter( 0 )

src/providers/wfs/qgswfsshareddata.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ class QgsWFSSharedData : public QObject
171171
/** Create GML parser */
172172
QgsGmlStreamingParser* createParser();
173173

174+
/** If the server (typically MapServer WFS 1.1) honours EPSG axis order, but returns
175+
EPSG:XXXX srsName and not EPSG urns */
176+
bool mGetFeatureEPSGDotHonoursEPSGOrder;
177+
174178
private:
175179

176180
/** Main mutex to protect most data members that can be modified concurrently */

tests/src/python/test_provider_wfs.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2020,6 +2020,94 @@ def testGeomedia(self):
20202020
self.assertEqual(features[0]['intfield'], 1)
20212021
self.assertEqual(features[1]['intfield'], 2)
20222022

2023+
def testMapServerWFS1_1_EPSG_4326(self):
2024+
"""Test interoperability with MapServer WFS 1.1."""
2025+
2026+
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_mapserver_wfs_1_1'
2027+
2028+
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=1.1.0'), 'wb') as f:
2029+
f.write("""
2030+
<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">
2031+
<FeatureTypeList>
2032+
<FeatureType>
2033+
<Name>my:typename</Name>
2034+
<Title>Title</Title>
2035+
<Abstract>Abstract</Abstract>
2036+
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
2037+
<ows:WGS84BoundingBox>
2038+
<ows:LowerCorner>2 49</ows:LowerCorner>
2039+
<ows:UpperCorner>2 49</ows:UpperCorner>
2040+
</ows:WGS84BoundingBox>
2041+
</FeatureType>
2042+
</FeatureTypeList>
2043+
</wfs:WFS_Capabilities>""".encode('UTF-8'))
2044+
2045+
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.1.0&TYPENAME=my:typename'), 'wb') as f:
2046+
f.write("""
2047+
<schema
2048+
targetNamespace="http://my"
2049+
xmlns:my="http://my"
2050+
xmlns:ogc="http://www.opengis.net/ogc"
2051+
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
2052+
xmlns="http://www.w3.org/2001/XMLSchema"
2053+
xmlns:gml="http://www.opengis.net/gml"
2054+
elementFormDefault="qualified" version="0.1" >
2055+
<import namespace="http://www.opengis.net/gml"
2056+
schemaLocation="http://schemas.opengis.net/gml/3.1.1/base/gml.xsd" />
2057+
<element name="typename"
2058+
type="my:typenameType"
2059+
substitutionGroup="gml:_Feature" />
2060+
<complexType name="typenameType">
2061+
<complexContent>
2062+
<extension base="gml:AbstractFeatureType">
2063+
<sequence>
2064+
<element name="geometryProperty" type="gml:GeometryPropertyType" minOccurs="0" maxOccurs="1"/>
2065+
</sequence>
2066+
</extension>
2067+
</complexContent>
2068+
</complexType>
2069+
</schema>
2070+
""".encode('UTF-8'))
2071+
2072+
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:
2073+
f.write("""
2074+
<wfs:FeatureCollection
2075+
xmlns:my="http://my"
2076+
xmlns:gml="http://www.opengis.net/gml"
2077+
xmlns:wfs="http://www.opengis.net/wfs"
2078+
xmlns:ogc="http://www.opengis.net/ogc">
2079+
<gml:boundedBy>
2080+
<gml:Envelope srsName="EPSG:4326">
2081+
<gml:lowerCorner>49.000000 2.000000</gml:lowerCorner>
2082+
<gml:upperCorner>49.000000 2.000000</gml:upperCorner>
2083+
</gml:Envelope>
2084+
</gml:boundedBy>
2085+
<gml:featureMember>
2086+
<my:typename gml:id="typename.1">
2087+
<gml:boundedBy>
2088+
<gml:Envelope srsName="EPSG:4326">
2089+
<gml:lowerCorner>49.000000 2.000000</gml:lowerCorner>
2090+
<gml:upperCorner>49.000000 2.000000</gml:upperCorner>
2091+
</gml:Envelope>
2092+
</gml:boundedBy>
2093+
<my:geometryProperty>
2094+
<gml:Point srsName="EPSG:4326">
2095+
<gml:pos>49.000000 2.000000</gml:pos>
2096+
</gml:Point>
2097+
</my:geometryProperty>
2098+
</my:typename>
2099+
</gml:featureMember>
2100+
</wfs:FeatureCollection>
2101+
2102+
""".encode('UTF-8'))
2103+
2104+
vl = QgsVectorLayer(u"url='http://" + endpoint + u"' typename='my:typename' version='1.1.0'", u'test', u'WFS')
2105+
assert vl.isValid()
2106+
2107+
got_f = [f for f in vl.getFeatures()]
2108+
got = got_f[0].geometry().geometry()
2109+
self.assertEqual((got.x(), got.y()), (2.0, 49.0))
2110+
20232111

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

0 commit comments

Comments
 (0)