[wms] Fix unreported issue max tile size from capabilities ignored

Fix an unreported issue with WMS client ignoring the advertised tile
size from GetCapabilities (because the raster iterator always used
its own hardcoded default).

A new method was added to retrieve this information from data providers
only implemented for WMS at the moment.

The test has been reformatted for consistency with other core tests
by moving the implementation outside of the class. The actual change is
the addition of TestQgsWmsProvider::testMaxTileSize().

Funded by: M.O.S.S. Computer Grafik Systeme GmbH https://www.moss.de/
This commit is contained in:
Alessandro Pasotti 2024-10-16 09:29:36 +02:00
parent 1431d2c70e
commit 2078ef5c03
8 changed files with 649 additions and 487 deletions

View File

@ -144,6 +144,16 @@ Read band scale for raster value
Read band offset for raster value
%End
virtual QSize maximumTileSize() const;
%Docstring
Returns the maximum tile size in pixels for the data provider.
By default, the maximum tile size is set to :py:class:`QgsRasterIterator`.DEFAULT_MAXIMUM_TILE_WIDTH x
:py:class:`QgsRasterIterator`.DEFAULT_MAXIMUM_TILE_HEIGHT but can be overridden in subclasses (e.g. WMS
can retrieve that information from the GetCapabilities document).
.. versionadded:: 3.40
%End
virtual QgsRasterBlock *block( int bandNo, const QgsRectangle &boundingBox, int width, int height, QgsRasterBlockFeedback *feedback = 0 ) /Factory/;

View File

@ -144,6 +144,16 @@ Read band scale for raster value
Read band offset for raster value
%End
virtual QSize maximumTileSize() const;
%Docstring
Returns the maximum tile size in pixels for the data provider.
By default, the maximum tile size is set to :py:class:`QgsRasterIterator`.DEFAULT_MAXIMUM_TILE_WIDTH x
:py:class:`QgsRasterIterator`.DEFAULT_MAXIMUM_TILE_HEIGHT but can be overridden in subclasses (e.g. WMS
can retrieve that information from the GetCapabilities document).
.. versionadded:: 3.40
%End
virtual QgsRasterBlock *block( int bandNo, const QgsRectangle &boundingBox, int width, int height, QgsRasterBlockFeedback *feedback = 0 ) /Factory/;

View File

@ -157,6 +157,15 @@ class CORE_EXPORT QgsRasterDataProvider : public QgsDataProvider, public QgsRast
*/
virtual double bandOffset( int bandNo ) const { Q_UNUSED( bandNo ) return 0.0; }
/**
* Returns the maximum tile size in pixels for the data provider.
* By default, the maximum tile size is set to QgsRasterIterator::DEFAULT_MAXIMUM_TILE_WIDTH x
* QgsRasterIterator::DEFAULT_MAXIMUM_TILE_HEIGHT but can be overridden in subclasses (e.g. WMS
* can retrieve that information from the GetCapabilities document).
* \since QGIS 3.40
*/
virtual QSize maximumTileSize() const { return QSize( QgsRasterIterator::DEFAULT_MAXIMUM_TILE_WIDTH, QgsRasterIterator::DEFAULT_MAXIMUM_TILE_HEIGHT ); }
// TODO: remove or make protected all readBlock working with void*
//! Read block of data using given extent and size.

View File

@ -514,6 +514,16 @@ bool QgsRasterLayerRenderer::render()
// Drawer to pipe?
QgsRasterIterator iterator( mPipe->last() );
// Get the maximum tile size from the provider and set it as the maximum tile size for the iterator
QgsRasterDataProvider *provider { mPipe->provider() };
if ( provider )
{
const QSize maxTileSize {provider->maximumTileSize()};
iterator.setMaximumTileWidth( maxTileSize.width() );
iterator.setMaximumTileHeight( maxTileSize.height() );
}
QgsRasterDrawer drawer( &iterator );
drawer.draw( *( renderContext() ), mRasterViewPort, mFeedback );

View File

@ -1067,6 +1067,7 @@ class QgsWmsCapabilities
friend class QgsWmsProvider;
friend class TestQgsWmsCapabilities;
friend class TestQgsWmsProvider;
};

View File

@ -835,10 +835,13 @@ QImage *QgsWmsProvider::draw( const QgsRectangle &viewExtent, int pixelWidth, in
QImage *image = new QImage( pixelWidth, pixelHeight, QImage::Format_ARGB32 );
image->fill( 0 );
int maxWidth = mCaps.mCapabilities.service.maxWidth == 0 ? std::numeric_limits<int>::max() : mCaps.mCapabilities.service.maxWidth;
int maxHeight = mCaps.mCapabilities.service.maxHeight == 0 ? std::numeric_limits<int>::max() : mCaps.mCapabilities.service.maxHeight;
if ( !mSettings.mTiled && mSettings.mMaxWidth == 0 && mSettings.mMaxHeight == 0 && pixelWidth <= maxWidth && pixelHeight <= maxHeight )
const QSize maxTileSize { maximumTileSize() };
const int maxWidth { maxTileSize.width() };
const int maxHeight { maxTileSize.height() };
if ( !mSettings.mTiled && pixelWidth <= maxWidth && pixelHeight <= maxHeight )
{
QUrl url = createRequestUrlWMS( viewExtent, pixelWidth, pixelHeight );
@ -4104,6 +4107,32 @@ QgsLayerMetadata QgsWmsProvider::layerMetadata() const
return mLayerMetadata;
}
QSize QgsWmsProvider::maximumTileSize() const
{
const int capsMaxHeight { static_cast<int>( mCaps.mCapabilities.service.maxHeight ) };
const int capsMaxWidth { static_cast<int>( mCaps.mCapabilities.service.maxWidth ) };
if ( mSettings.mMaxHeight > 0 && mSettings.mMaxWidth > 0 )
{
if ( capsMaxHeight > 0 && capsMaxWidth > 0 )
{
return QSize( std::min( mSettings.mMaxWidth, capsMaxWidth ), std::min( mSettings.mMaxHeight, capsMaxHeight ) );
}
else
{
return QSize( mSettings.mMaxWidth, mSettings.mMaxHeight );
}
}
else if ( capsMaxHeight > 0 && capsMaxWidth > 0 )
{
return QSize( capsMaxWidth, capsMaxHeight );
}
else // default fallback
{
return QgsRasterDataProvider::maximumTileSize();
}
}
QgsRasterBandStats QgsWmsProvider::bandStatistics(
int bandNo,
Qgis::RasterBandStatistics stats,

View File

@ -313,6 +313,13 @@ class QgsWmsProvider final: public QgsRasterDataProvider
bool setZoomedInResamplingMethod( ResamplingMethod method ) override { mZoomedInResamplingMethod = method; return true; }
bool setZoomedOutResamplingMethod( ResamplingMethod method ) override { mZoomedOutResamplingMethod = method; return true; }
/*
* Overridden because WMS provider can retrieve this information from the capabilities document.
* The size defined by the user in the provider settings takes precedence but cannot exceed the
* maximum tile size advertised by the capabilities document (if not null).
*/
QSize maximumTileSize() const override;
// Statistics could be available if the provider has a converter from colors to other value type, the returned statistics depend on the converter
QgsRasterBandStats bandStatistics( int bandNo,
Qgis::RasterBandStatistics stats = Qgis::RasterBandStatistic::All,

View File

@ -46,7 +46,66 @@ class TestQgsWmsProvider: public QgsTest
private slots:
void initTestCase()
void initTestCase();
//runs after all tests
void cleanupTestCase();
void legendGraphicsWithStyle();
void legendGraphicsWithSecondStyle();
void legendGraphicsWithoutStyleWithDefault();
void legendGraphicsWithoutStyleWithoutDefault();
// regression #20271 - WMS is not displayed in QGIS 3.4.0
void queryItemsWithNullValue();
// regression #41116
void queryItemsWithPlusSign();
void noCrsSpecified();
void testWmtsConstruction();
void testMBTiles();
void testMBTilesSample();
void testMbtilesProviderMetadata();
void testDpiDependentData();
void providerUriUpdates();
void providerUriLocalFile();
void absoluteRelativeUri();
void testXyzIsBasemap();
void testOsmMetadata();
void testConvertToValue();
void testTerrariumInterpretation();
void testResampling();
bool imageCheck( const QString &testType, QgsMapSettings &mapSettings );
void testParseWmstUriWithoutTemporalExtent();
void testMaxTileSize();
private:
QgsWmsCapabilities *mCapabilities = nullptr;
};
void TestQgsWmsProvider::initTestCase()
{
// init QGIS's paths - true means that all path will be inited from prefix
QgsApplication::init();
@ -62,41 +121,39 @@ class TestQgsWmsProvider: public QgsTest
QVERIFY( mCapabilities->parseResponse( content, config ) );
}
//runs after all tests
void cleanupTestCase()
void TestQgsWmsProvider::cleanupTestCase()
{
delete mCapabilities;
QgsApplication::exitQgis();
}
void legendGraphicsWithStyle()
void TestQgsWmsProvider::legendGraphicsWithStyle()
{
QgsWmsProvider provider( QStringLiteral( "http://localhost:8380/mapserv?xxx&layers=agri_zones&styles=fb_style&format=image/jpg" ), QgsDataProvider::ProviderOptions(), mCapabilities );
QCOMPARE( provider.getLegendGraphicUrl(), QString( "http://www.example.com/fb.png?" ) );
}
void legendGraphicsWithSecondStyle()
void TestQgsWmsProvider::legendGraphicsWithSecondStyle()
{
QgsWmsProvider provider( QStringLiteral( "http://localhost:8380/mapserv?xxx&layers=agri_zones&styles=yt_style&format=image/jpg" ), QgsDataProvider::ProviderOptions(), mCapabilities );
QCOMPARE( provider.getLegendGraphicUrl(), QString( "http://www.example.com/yt.png?" ) );
}
void legendGraphicsWithoutStyleWithDefault()
void TestQgsWmsProvider::legendGraphicsWithoutStyleWithDefault()
{
QgsWmsProvider provider( QStringLiteral( "http://localhost:8380/mapserv?xxx&layers=buildings&styles=&format=image/jpg" ), QgsDataProvider::ProviderOptions(), mCapabilities );
//only one style, can guess default => use it
QCOMPARE( provider.getLegendGraphicUrl(), QString( "http://www.example.com/buildings.png?" ) );
}
void legendGraphicsWithoutStyleWithoutDefault()
void TestQgsWmsProvider::legendGraphicsWithoutStyleWithoutDefault()
{
QgsWmsProvider provider( QStringLiteral( "http://localhost:8380/mapserv?xxx&layers=agri_zones&styles=&format=image/jpg" ), QgsDataProvider::ProviderOptions(), mCapabilities );
//two style, cannot guess default => use the WMS GetLegendGraphics
QCOMPARE( provider.getLegendGraphicUrl(), QString( "http://localhost:8380/mapserv?" ) );
}
// regression #20271 - WMS is not displayed in QGIS 3.4.0
void queryItemsWithNullValue()
void TestQgsWmsProvider::queryItemsWithNullValue()
{
QString failingAddress( "http://localhost:8380/mapserv" );
QgsWmsProvider provider( failingAddress, QgsDataProvider::ProviderOptions(), mCapabilities );
@ -106,8 +163,7 @@ class TestQgsWmsProvider: public QgsTest
"STYLES=&FORMAT=&TRANSPARENT=TRUE" ) );
}
// regression #41116
void queryItemsWithPlusSign()
void TestQgsWmsProvider::queryItemsWithPlusSign()
{
const QString failingAddress( "layers=plus+sign&styles=&url=http://localhost:8380/mapserv" );
const QgsWmsParserSettings config;
@ -123,8 +179,7 @@ class TestQgsWmsProvider: public QgsTest
"LAYERS=plus%2Bsign&STYLES=&FORMAT=&TRANSPARENT=TRUE" ) );
}
void noCrsSpecified()
void TestQgsWmsProvider::noCrsSpecified()
{
QgsWmsProvider provider( QStringLiteral( "http://localhost:8380/mapserv?xxx&layers=agri_zones&styles=&format=image/jpg" ), QgsDataProvider::ProviderOptions(), mCapabilities );
QCOMPARE( provider.crs().authid(), QStringLiteral( "EPSG:2056" ) );
@ -145,7 +200,7 @@ class TestQgsWmsProvider: public QgsTest
QCOMPARE( provider4.crs().authid(), QStringLiteral( "EPSG:3857" ) );
}
void testWmtsConstruction()
void TestQgsWmsProvider::testWmtsConstruction()
{
const QgsWmsParserSettings config;
QgsWmsCapabilities cap;
@ -188,7 +243,7 @@ class TestQgsWmsProvider: public QgsTest
}
}
void testMBTiles()
void TestQgsWmsProvider::testMBTiles()
{
QString dataDir( TEST_DATA_DIR );
QUrlQuery uq;
@ -215,7 +270,7 @@ class TestQgsWmsProvider: public QgsTest
QVERIFY( !layer.dataProvider()->elevationProperties()->containsElevationData() );
}
void testMBTilesSample()
void TestQgsWmsProvider::testMBTilesSample()
{
QString dataDir( TEST_DATA_DIR );
QUrlQuery uq;
@ -235,7 +290,7 @@ class TestQgsWmsProvider: public QgsTest
QVERIFY( layer.dataProvider()->elevationProperties()->containsElevationData() );
}
void testMbtilesProviderMetadata()
void TestQgsWmsProvider::testMbtilesProviderMetadata()
{
QgsProviderMetadata *wmsMetadata = QgsProviderRegistry::instance()->providerMetadata( "wms" );
QVERIFY( wmsMetadata );
@ -340,7 +395,7 @@ class TestQgsWmsProvider: public QgsTest
QCOMPARE( candidates.at( candidateIndex ).layerTypes(), QList< Qgis::LayerType >() << Qgis::LayerType::Raster );
}
void testDpiDependentData()
void TestQgsWmsProvider::testDpiDependentData()
{
QString dataDir( TEST_DATA_DIR );
QUrlQuery uq;
@ -365,7 +420,7 @@ class TestQgsWmsProvider: public QgsTest
QVERIFY( imageCheck( "mbtiles_dpidependentdata", mapSettings ) );
}
void providerUriUpdates()
void TestQgsWmsProvider::providerUriUpdates()
{
QgsProviderMetadata *metadata = QgsProviderRegistry::instance()->providerMetadata( "wms" );
QString uriString = QStringLiteral( "crs=EPSG:4326&dpiMode=7&"
@ -391,7 +446,7 @@ class TestQgsWmsProvider: public QgsTest
}
void providerUriLocalFile()
void TestQgsWmsProvider::providerUriLocalFile()
{
QString uriString = QStringLiteral( "url=file:///my/local/tiles.mbtiles&type=mbtiles" );
QVariantMap parts = QgsProviderRegistry::instance()->decodeUri( QStringLiteral( "wms" ), uriString );
@ -418,7 +473,7 @@ class TestQgsWmsProvider: public QgsTest
QVERIFY( !QgsProviderUtils::sublayerDetailsAreIncomplete( sublayers ) );
}
void absoluteRelativeUri()
void TestQgsWmsProvider::absoluteRelativeUri()
{
QgsReadWriteContext context;
context.setPathResolver( QgsPathResolver( QStringLiteral( TEST_DATA_DIR ) + QStringLiteral( "/project.qgs" ) ) );
@ -432,14 +487,14 @@ class TestQgsWmsProvider: public QgsTest
QCOMPARE( wmsMetadata->relativeToAbsoluteUri( relativeUri, context ), absoluteUri );
}
void testXyzIsBasemap()
void TestQgsWmsProvider::testXyzIsBasemap()
{
// test that xyz tile layers are considered basemap layers
QgsRasterLayer layer( QStringLiteral( "type=xyz&url=file://tile.openstreetmap.org/%7Bz%7D/%7Bx%7D/%7By%7D.png&zmax=19&zmin=0" ), QString(), QStringLiteral( "wms" ) );
QCOMPARE( layer.properties(), Qgis::MapLayerProperties( Qgis::MapLayerProperty::IsBasemapLayer ) );
}
void testOsmMetadata()
void TestQgsWmsProvider::testOsmMetadata()
{
// test that we auto-populate openstreetmap tile metadata
@ -453,7 +508,7 @@ class TestQgsWmsProvider: public QgsTest
QVERIFY( provider.layerMetadata().rights().at( 0 ).startsWith( "Base map and data from OpenStreetMap and OpenStreetMap Foundation" ) );
}
void testConvertToValue()
void TestQgsWmsProvider::testConvertToValue()
{
QString dataDir( TEST_DATA_DIR );
@ -477,7 +532,7 @@ class TestQgsWmsProvider: public QgsTest
QVERIFY( imageCheck( "convert_value", mapSettings ) );
}
void testTerrariumInterpretation()
void TestQgsWmsProvider::testTerrariumInterpretation()
{
QString dataDir( TEST_DATA_DIR );
@ -505,7 +560,7 @@ class TestQgsWmsProvider: public QgsTest
QVERIFY( imageCheck( "terrarium_terrain", mapSettings ) );
}
void testResampling()
void TestQgsWmsProvider::testResampling()
{
QString dataDir( TEST_DATA_DIR );
@ -537,7 +592,7 @@ class TestQgsWmsProvider: public QgsTest
QVERIFY( imageCheck( "cubic_resampling", mapSettings ) );
}
bool imageCheck( const QString &testType, QgsMapSettings &mapSettings )
bool TestQgsWmsProvider::imageCheck( const QString &testType, QgsMapSettings &mapSettings )
{
//use the QgsRenderChecker test utility class to
//ensure the rendered output matches our control image
@ -551,17 +606,48 @@ class TestQgsWmsProvider: public QgsTest
return myResultFlag;
}
void testParseWmstUriWithoutTemporalExtent()
void TestQgsWmsProvider::testParseWmstUriWithoutTemporalExtent()
{
// test fix for https://github.com/qgis/QGIS/issues/43158
// we just check we don't crash
QgsWmsProvider provider( QStringLiteral( "allowTemporalUpdates=true&temporalSource=provider&type=wmst&layers=foostyles=bar&crs=EPSG:3857&format=image/png&url=file:///dummy" ), QgsDataProvider::ProviderOptions(), mCapabilities );
}
private:
QgsWmsCapabilities *mCapabilities = nullptr;
void TestQgsWmsProvider::testMaxTileSize()
{
QgsWmsProvider provider( QStringLiteral( "http://localhost:8380/mapserv?xxx&layers=buildings&styles=&format=image/jpg" ), QgsDataProvider::ProviderOptions(), mCapabilities );
const QSize maxTileSize = provider.maximumTileSize();
QCOMPARE( maxTileSize.width(), 5000 );
QCOMPARE( maxTileSize.height(), 5000 );
};
// test that we can override the max tile size if the server advertises a larger size
QgsWmsProvider provider2( QStringLiteral( "http://localhost:8380/mapserv?xxx&layers=buildings&styles=&format=image/jpg&maxHeight=3000&maxWidth=3000" ), QgsDataProvider::ProviderOptions(), mCapabilities );
const QSize maxTileSize2 = provider2.maximumTileSize();
QCOMPARE( maxTileSize2.width(), 3000 );
QCOMPARE( maxTileSize2.height(), 3000 );
// test that we cannot override the maximum advertised size
QgsWmsProvider provider3( QStringLiteral( "http://localhost:8380/mapserv?xxx&layers=buildings&styles=&format=image/jpg&maxHeight=6000&maxWidth=6000" ), QgsDataProvider::ProviderOptions(), mCapabilities );
const QSize maxTileSize3 = provider3.maximumTileSize();
QCOMPARE( maxTileSize3.width(), 5000 );
QCOMPARE( maxTileSize3.height(), 5000 );
// Remove the max tile size from the capabilities to check that the default value is used
QgsWmsCapabilities capabilities { *mCapabilities };
capabilities.mCapabilities.service.maxHeight = 0;
capabilities.mCapabilities.service.maxWidth = 0;
QgsWmsProvider provider4( QStringLiteral( "http://localhost:8380/mapserv?xxx&layers=buildings&styles=&format=image/jpg" ), QgsDataProvider::ProviderOptions(), &capabilities );
const QSize maxTileSize4 = provider4.maximumTileSize();
QCOMPARE( maxTileSize4.width(), QgsRasterIterator::DEFAULT_MAXIMUM_TILE_WIDTH );
QCOMPARE( maxTileSize4.height(), QgsRasterIterator::DEFAULT_MAXIMUM_TILE_HEIGHT );
// test that we can override the default maximum tile size
QgsWmsProvider provider5( QStringLiteral( "http://localhost:8380/mapserv?xxx&layers=buildings&styles=&format=image/jpg&maxHeight=3000&maxWidth=3000" ), QgsDataProvider::ProviderOptions(), &capabilities );
const QSize maxTileSize5 = provider5.maximumTileSize();
QCOMPARE( maxTileSize5.width(), 3000 );
QCOMPARE( maxTileSize5.height(), 3000 );
}
QGSTEST_MAIN( TestQgsWmsProvider )
#include "testqgswmsprovider.moc"