Skip to content

Commit 31879e5

Browse files
committed
[WFS provider] Fix handling of LatLongBoundingBox in WFS 1.0
According to the specification, the values of LatLongBoundingBox are supposed to be in the SRS, not necessarily in WGS84. But some servers do not follow the spec and return values in WGS84, so let's try to accomodate for this too. Fix #14876
1 parent bfc3577 commit 31879e5

File tree

4 files changed

+132
-30
lines changed

4 files changed

+132
-30
lines changed

src/providers/wfs/qgswfscapabilities.cpp

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
#include "qgslogger.h"
2121
#include "qgsmessagelog.h"
2222
#include "qgsogcutils.h"
23+
#include "qgscrscache.h"
24+
2325
#include <QDomDocument>
2426
#include <QSettings>
2527
#include <QStringList>
@@ -312,31 +314,72 @@ void QgsWFSCapabilities::capabilitiesReplyFinished()
312314
QDomElement latLongBB = featureTypeElem.firstChildElement( "LatLongBoundingBox" );
313315
if ( latLongBB.hasAttributes() )
314316
{
315-
featureType.bboxLongLat = QgsRectangle(
316-
latLongBB.attribute( "minx" ).toDouble(),
317-
latLongBB.attribute( "miny" ).toDouble(),
318-
latLongBB.attribute( "maxx" ).toDouble(),
319-
latLongBB.attribute( "maxy" ).toDouble() );
317+
// Despite the name LatLongBoundingBox, the coordinates are supposed to
318+
// be expressed in <SRS>. From the WFS schema;
319+
// <!-- The LatLongBoundingBox element is used to indicate the edges of
320+
// an enclosing rectangle in the SRS of the associated feature type.
321+
featureType.bbox = QgsRectangle(
322+
latLongBB.attribute( "minx" ).toDouble(),
323+
latLongBB.attribute( "miny" ).toDouble(),
324+
latLongBB.attribute( "maxx" ).toDouble(),
325+
latLongBB.attribute( "maxy" ).toDouble() );
326+
featureType.bboxSRSIsWGS84 = false;
327+
328+
// But some servers do not honour this and systematically reproject to WGS84
329+
// such as GeoServer. See http://osgeo-org.1560.x6.nabble.com/WFS-LatLongBoundingBox-td3813810.html
330+
// This is also true of TinyOWS
331+
if ( !featureType.crslist.isEmpty() &&
332+
featureType.bbox.xMinimum() >= -180 && featureType.bbox.yMinimum() >= -90 &&
333+
featureType.bbox.xMaximum() <= 180 && featureType.bbox.yMaximum() < 90 )
334+
{
335+
QgsCoordinateReferenceSystem crs = QgsCRSCache::instance()->crsByOgcWmsCrs( featureType.crslist[0] );
336+
if ( !crs.geographicFlag() )
337+
{
338+
// If the CRS is projected then check that projecting the corner of the bbox, assumed to be in WGS84,
339+
// into the CRS, and then back to WGS84, works (check that we are in the validity area)
340+
QgsCoordinateReferenceSystem crsWGS84 = QgsCRSCache::instance()->crsByOgcWmsCrs( "CRS:84" );
341+
QgsCoordinateTransform ct( crsWGS84, crs );
342+
343+
QgsPoint ptMin( featureType.bbox.xMinimum(), featureType.bbox.yMinimum() );
344+
QgsPoint ptMinBack( ct.transform( ct.transform( ptMin, QgsCoordinateTransform::ForwardTransform ), QgsCoordinateTransform::ReverseTransform ) );
345+
QgsPoint ptMax( featureType.bbox.xMaximum(), featureType.bbox.yMaximum() );
346+
QgsPoint ptMaxBack( ct.transform( ct.transform( ptMax, QgsCoordinateTransform::ForwardTransform ), QgsCoordinateTransform::ReverseTransform ) );
347+
348+
QgsDebugMsg( featureType.bbox.toString() );
349+
QgsDebugMsg( ptMinBack.toString() );
350+
QgsDebugMsg( ptMaxBack.toString() );
351+
352+
if ( fabs( featureType.bbox.xMinimum() - ptMinBack.x() ) < 1e-5 &&
353+
fabs( featureType.bbox.yMinimum() - ptMinBack.y() ) < 1e-5 &&
354+
fabs( featureType.bbox.xMaximum() - ptMaxBack.x() ) < 1e-5 &&
355+
fabs( featureType.bbox.yMaximum() - ptMaxBack.y() ) < 1e-5 )
356+
{
357+
QgsDebugMsg( "Values of LatLongBoundingBox are consistent with WGS84 long/lat bounds, so as the CRS is projected, assume they are indeed in WGS84 and not in the CRS units" );
358+
featureType.bboxSRSIsWGS84 = true;
359+
}
360+
}
361+
}
320362
}
321363
else
322364
{
323365
// WFS 1.1 way
324-
latLongBB = featureTypeElem.firstChildElement( "WGS84BoundingBox" );
325-
if ( !latLongBB.isNull() )
366+
QDomElement WGS84BoundingBox = featureTypeElem.firstChildElement( "WGS84BoundingBox" );
367+
if ( !WGS84BoundingBox.isNull() )
326368
{
327-
QDomElement lowerCorner = latLongBB.firstChildElement( "LowerCorner" );
328-
QDomElement upperCorner = latLongBB.firstChildElement( "UpperCorner" );
369+
QDomElement lowerCorner = WGS84BoundingBox.firstChildElement( "LowerCorner" );
370+
QDomElement upperCorner = WGS84BoundingBox.firstChildElement( "UpperCorner" );
329371
if ( !lowerCorner.isNull() && !upperCorner.isNull() )
330372
{
331373
QStringList lowerCornerList = lowerCorner.text().split( " ", QString::SkipEmptyParts );
332374
QStringList upperCornerList = upperCorner.text().split( " ", QString::SkipEmptyParts );
333375
if ( lowerCornerList.size() == 2 && upperCornerList.size() == 2 )
334376
{
335-
featureType.bboxLongLat = QgsRectangle(
336-
lowerCornerList[0].toDouble(),
337-
lowerCornerList[1].toDouble(),
338-
upperCornerList[0].toDouble(),
339-
upperCornerList[1].toDouble() );
377+
featureType.bbox = QgsRectangle(
378+
lowerCornerList[0].toDouble(),
379+
lowerCornerList[1].toDouble(),
380+
upperCornerList[0].toDouble(),
381+
upperCornerList[1].toDouble() );
382+
featureType.bboxSRSIsWGS84 = true;
340383
}
341384
}
342385
}

src/providers/wfs/qgswfscapabilities.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,14 @@ class QgsWFSCapabilities : public QgsWFSRequest
3636
struct FeatureType
3737
{
3838
//! Default constructor
39-
FeatureType() : insertCap( false ), updateCap( false ), deleteCap( false ) {}
39+
FeatureType() : bboxSRSIsWGS84( false ), insertCap( false ), updateCap( false ), deleteCap( false ) {}
4040

4141
QString name;
4242
QString title;
4343
QString abstract;
4444
QList<QString> crslist; // first is default
45-
QgsRectangle bboxLongLat;
45+
QgsRectangle bbox;
46+
bool bboxSRSIsWGS84; // if false, the bbox is expressed in crslist[0] CRS
4647
bool insertCap;
4748
bool updateCap;
4849
bool deleteCap;

src/providers/wfs/qgswfsprovider.cpp

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,21 +1457,28 @@ bool QgsWFSProvider::getCapabilities()
14571457
{
14581458
if ( thisLayerName == mShared->mCaps.featureTypes[i].name )
14591459
{
1460-
const QgsRectangle& r = mShared->mCaps.featureTypes[i].bboxLongLat;
1460+
const QgsRectangle& r = mShared->mCaps.featureTypes[i].bbox;
14611461
if ( mShared->mSourceCRS.authid().isEmpty() && mShared->mCaps.featureTypes[i].crslist.size() != 0 )
14621462
{
14631463
mShared->mSourceCRS = QgsCRSCache::instance()->crsByOgcWmsCrs( mShared->mCaps.featureTypes[i].crslist[0] );
14641464
}
14651465
if ( !r.isNull() )
14661466
{
1467-
QgsCoordinateReferenceSystem src = QgsCRSCache::instance()->crsByOgcWmsCrs( "CRS:84" );
1468-
QgsCoordinateTransform ct( src, mShared->mSourceCRS );
1467+
if ( mShared->mCaps.featureTypes[i].bboxSRSIsWGS84 )
1468+
{
1469+
QgsCoordinateReferenceSystem src = QgsCRSCache::instance()->crsByOgcWmsCrs( "CRS:84" );
1470+
QgsCoordinateTransform ct( src, mShared->mSourceCRS );
14691471

1470-
QgsDebugMsg( "latlon ext:" + r.toString() );
1471-
QgsDebugMsg( "src:" + src.authid() );
1472-
QgsDebugMsg( "dst:" + mShared->mSourceCRS.authid() );
1472+
QgsDebugMsg( "latlon ext:" + r.toString() );
1473+
QgsDebugMsg( "src:" + src.authid() );
1474+
QgsDebugMsg( "dst:" + mShared->mSourceCRS.authid() );
14731475

1474-
mShared->mCapabilityExtent = ct.transformBoundingBox( r, QgsCoordinateTransform::ForwardTransform );
1476+
mShared->mCapabilityExtent = ct.transformBoundingBox( r, QgsCoordinateTransform::ForwardTransform );
1477+
}
1478+
else
1479+
{
1480+
mShared->mCapabilityExtent = r;
1481+
}
14751482

14761483
QgsDebugMsg( "layer ext:" + mShared->mCapabilityExtent.toString() );
14771484
}

tests/src/python/test_provider_wfs.py

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -369,8 +369,9 @@ def testWFS10(self):
369369
<Name>my:typename</Name>
370370
<Title>Title</Title>
371371
<Abstract>Abstract</Abstract>
372-
<SRS>EPSG:4326</SRS>
373-
<LatLongBoundingBox minx="-71.123" miny="66.33" maxx="-65.32" maxy="78.3"/>
372+
<SRS>EPSG:32631</SRS>
373+
<!-- in WFS 1.0, LatLongBoundingBox is in SRS units, not necessarily lat/long... -->
374+
<LatLongBoundingBox minx="400000" miny="5400000" maxx="450000" maxy="5500000"/>
374375
</FeatureType>
375376
</FeatureTypeList>
376377
</WFS_Capabilities>""".encode('UTF-8'))
@@ -402,11 +403,11 @@ def testWFS10(self):
402403
self.assertEqual(vl.wkbType(), QgsWKBTypes.Point)
403404
self.assertEqual(len(vl.fields()), 5)
404405
self.assertEqual(vl.featureCount(), 0)
405-
reference = QgsGeometry.fromRect(QgsRectangle(-71.123, 66.33, -65.32, 78.3))
406+
reference = QgsGeometry.fromRect(QgsRectangle(400000.0, 5400000.0, 450000.0, 5500000.0))
406407
vl_extent = QgsGeometry.fromRect(vl.extent())
407408
assert QgsGeometry.compare(vl_extent.asPolygon()[0], reference.asPolygon()[0], 0.00001), 'Expected {}, got {}'.format(reference.exportToWkt(), vl_extent.exportToWkt())
408409

409-
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.0.0&TYPENAME=my:typename&SRSNAME=EPSG:4326'), 'wb') as f:
410+
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.0.0&TYPENAME=my:typename&SRSNAME=EPSG:32631'), 'wb') as f:
410411
f.write("""
411412
<wfs:FeatureCollection
412413
xmlns:wfs="http://www.opengis.net/wfs"
@@ -416,7 +417,7 @@ def testWFS10(self):
416417
<gml:featureMember>
417418
<my:typename fid="typename.0">
418419
<my:geometryProperty>
419-
<gml:Point srsName="http://www.opengis.net/gml/srs/epsg.xml#4326"><gml:coordinates decimal="." cs="," ts=" ">2,49</gml:coordinates></gml:Point></my:geometryProperty>
420+
<gml:Point srsName="http://www.opengis.net/gml/srs/epsg.xml#4326"><gml:coordinates decimal="." cs="," ts=" ">426858,5427937</gml:coordinates></gml:Point></my:geometryProperty>
420421
<my:INTFIELD>1</my:INTFIELD>
421422
<my:GEOMETRY>2</my:GEOMETRY>
422423
<my:longfield>1234567890123</my:longfield>
@@ -448,7 +449,7 @@ def testWFS10(self):
448449

449450
got_f = [f for f in vl.getFeatures()]
450451
got = got_f[0].geometry().geometry()
451-
self.assertEqual((got.x(), got.y()), (2.0, 49.0))
452+
self.assertEqual((got.x(), got.y()), (426858.0, 5427937.0))
452453

453454
self.assertEqual(vl.featureCount(), 1)
454455

@@ -459,6 +460,51 @@ def testWFS10(self):
459460

460461
assert not vl.dataProvider().deleteFeatures([0])
461462

463+
def testWFS10_latlongboundingbox_in_WGS84(self):
464+
"""Test WFS 1.0 with non conformatn LatLongBoundingBox"""
465+
466+
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_WFS1.0_latlongboundingbox_in_WGS84'
467+
468+
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=1.0.0'), 'wb') as f:
469+
f.write("""
470+
<WFS_Capabilities version="1.0.0" xmlns="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc">
471+
<FeatureTypeList>
472+
<FeatureType>
473+
<Name>my:typename</Name>
474+
<Title>Title</Title>
475+
<Abstract>Abstract</Abstract>
476+
<SRS>EPSG:32631</SRS>
477+
<!-- in WFS 1.0, LatLongBoundingBox are supposed to be in SRS units, not necessarily lat/long...
478+
But some servers do not honour this, so let's try to be robust -->
479+
<LatLongBoundingBox minx="1.63972075372399" miny="48.7449841112119" maxx="2.30733562794991" maxy="49.6504711179582"/>
480+
</FeatureType>
481+
</FeatureTypeList>
482+
</WFS_Capabilities>""".encode('UTF-8'))
483+
484+
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.0.0&TYPENAME=my:typename'), 'wb') as f:
485+
f.write("""
486+
<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">
487+
<xsd:import namespace="http://www.opengis.net/gml"/>
488+
<xsd:complexType name="typenameType">
489+
<xsd:complexContent>
490+
<xsd:extension base="gml:AbstractFeatureType">
491+
<xsd:sequence>
492+
<xsd:element maxOccurs="1" minOccurs="0" name="geometryProperty" nillable="true" type="gml:PointPropertyType"/>
493+
</xsd:sequence>
494+
</xsd:extension>
495+
</xsd:complexContent>
496+
</xsd:complexType>
497+
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
498+
</xsd:schema>
499+
""".encode('UTF-8'))
500+
501+
vl = QgsVectorLayer(u"url='http://" + endpoint + u"' typename='my:typename' version='1.0.0'", u'test', u'WFS')
502+
assert vl.isValid()
503+
504+
reference = QgsGeometry.fromRect(QgsRectangle(399999.9999999680439942,5399338.9090830031782389,449999.9999999987776391,5500658.0448500607162714))
505+
vl_extent = QgsGeometry.fromRect(vl.extent())
506+
assert QgsGeometry.compare(vl_extent.asPolygon()[0], reference.asPolygon()[0], 0.00001), 'Expected {}, got {}'.format(reference.exportToWkt(), vl_extent.exportToWkt())
507+
462508
def testWFST10(self):
463509
"""Test WFS-T 1.0 (read-write)"""
464510

@@ -1959,14 +2005,19 @@ def testGeomedia(self):
19592005
assert vl.isValid()
19602006
self.assertEqual(vl.wkbType(), QgsWKBTypes.MultiPolygon)
19612007

2008+
# Extent before downloading features
2009+
reference = QgsGeometry.fromRect(QgsRectangle(243900.3520259926444851,4427769.1559739429503679,1525592.3040170343592763,5607994.6020106188952923))
2010+
vl_extent = QgsGeometry.fromRect(vl.extent())
2011+
assert QgsGeometry.compare(vl_extent.asPolygon()[0], reference.asPolygon()[0], 0.00001), 'Expected {}, got {}'.format(reference.exportToWkt(), vl_extent.exportToWkt())
2012+
19622013
# Download all features
19632014
features = [f for f in vl.getFeatures()]
19642015
self.assertEqual(len(features), 2)
19652016

19662017
reference = QgsGeometry.fromRect(QgsRectangle(500000, 4500000, 510000, 4510000))
19672018
vl_extent = QgsGeometry.fromRect(vl.extent())
1968-
self.assertEqual(features[0]['intfield'], 1)
19692019
assert QgsGeometry.compare(vl_extent.asPolygon()[0], reference.asPolygon()[0], 0.00001), 'Expected {}, got {}'.format(reference.exportToWkt(), vl_extent.exportToWkt())
2020+
self.assertEqual(features[0]['intfield'], 1)
19702021
self.assertEqual(features[1]['intfield'], 2)
19712022

19722023

0 commit comments

Comments
 (0)