diff --git a/src/providers/postgres/raster/qgspostgresrasterprovider.cpp b/src/providers/postgres/raster/qgspostgresrasterprovider.cpp index ab3377e61379..c8724768604a 100644 --- a/src/providers/postgres/raster/qgspostgresrasterprovider.cpp +++ b/src/providers/postgres/raster/qgspostgresrasterprovider.cpp @@ -304,7 +304,7 @@ bool QgsPostgresRasterProvider::readBlock( int bandNo, const QgsRectangle &viewE } bool ok; - QString val { result.PQgetvalue( 0, 0 ) }; + QString val { result.PQntuples() > 0 ? result.PQgetvalue( 0, 0 ) : QString() }; if ( val.isNull() && mUseSrcNoDataValue[bandNo - 1] ) { @@ -402,6 +402,9 @@ bool QgsPostgresRasterProvider::readBlock( int bandNo, const QgsRectangle &viewE } else // Fetch block { + const Qgis::DataType dataType { mDataTypes[ bandNo - 1 ] }; + const GDALDataType gdalDataType = QgsGdalUtils::gdalDataTypeFromQgisDataType( dataType ); + const double noDataValue { mSrcNoDataValue[ bandNo - 1 ] }; const double xRes = viewExtent.width() / width; const double yRes = viewExtent.height() / height; @@ -456,10 +459,47 @@ bool QgsPostgresRasterProvider::readBlock( int bandNo, const QgsRectangle &viewE if ( tileResponse.tiles.isEmpty() ) { // rasters can be sparse by omitting some of the blocks/tiles + // so we should not log an error here but make sure + // the result buffer is filled with nodata + gdal::dataset_unique_ptr dstDS { QgsGdalUtils::createSingleBandMemoryDataset( + gdalDataType, viewExtent, width, height, mCrs ) }; + if ( ! dstDS ) + { + const QString lastError = QString::fromUtf8( CPLGetLastErrorMsg() ) ; + QgsMessageLog::logMessage( tr( "Unable to create destination raster for tiles from %1: %2" ) + .arg( tableToQuery, lastError ), tr( "PostGIS" ), Qgis::MessageLevel::Critical ); + return false; + } + + GDALSetRasterNoDataValue( GDALGetRasterBand( dstDS.get(), 1 ), noDataValue ); + // fill with nodata + GDALFillRaster( GDALGetRasterBand( dstDS.get(), 1 ), noDataValue, 0 ); + + // copy to the result buffer + CPLErrorReset(); + CPLErr err = GDALRasterIO( GDALGetRasterBand( dstDS.get(), 1 ), + GF_Read, + 0, + 0, + width, + height, + data, + width, + height, + gdalDataType, + 0, + 0 ); + if ( err != CE_None ) + { + const QString lastError = QString::fromUtf8( CPLGetLastErrorMsg() ) ; + QgsMessageLog::logMessage( tr( "Unable to write raster to block from %1: %2" ) + .arg( mQuery, lastError ), tr( "PostGIS" ), Qgis::MessageLevel::Critical ); + return false; + } + return true; } - // Finally merge the tiles // We must have at least one tile at this point (we checked for that before) @@ -469,8 +509,6 @@ bool QgsPostgresRasterProvider::readBlock( int bandNo, const QgsRectangle &viewE const int tmpWidth = static_cast( std::round( tilesExtent.width() / tileResponse.tiles.first().scaleX ) ); const int tmpHeight = static_cast( std::round( tilesExtent.height() / std::fabs( tileResponse.tiles.first().scaleY ) ) ); - GDALDataType gdalDataType { static_cast( sourceDataType( bandNo ) ) }; - //qDebug() << "Creating output raster: " << tilesExtent.toString() << tmpWidth << tmpHeight; gdal::dataset_unique_ptr tmpDS { QgsGdalUtils::createSingleBandMemoryDataset( @@ -484,8 +522,11 @@ bool QgsPostgresRasterProvider::readBlock( int bandNo, const QgsRectangle &viewE } } + GDALSetRasterNoDataValue( GDALGetRasterBand( tmpDS.get(), 1 ), noDataValue ); + // Write tiles to the temporary raster CPLErrorReset(); + for ( auto &tile : std::as_const( tileResponse.tiles ) ) { // Offset in px from the base raster @@ -515,6 +556,7 @@ bool QgsPostgresRasterProvider::readBlock( int bandNo, const QgsRectangle &viewE } } + #if 0 // Debug output raster content double pdfMin; @@ -536,6 +578,8 @@ bool QgsPostgresRasterProvider::readBlock( int bandNo, const QgsRectangle &viewE return false; } + GDALSetRasterNoDataValue( GDALGetRasterBand( dstDS.get(), 1 ), noDataValue ); + // Resample the raster to the final bounds and resolution if ( ! QgsGdalUtils::resampleSingleBandRaster( tmpDS.get(), dstDS.get(), GDALResampleAlg::GRA_NearestNeighbour, nullptr ) ) { diff --git a/tests/src/python/test_provider_postgresraster.py b/tests/src/python/test_provider_postgresraster.py index 7e583960b5bb..04a48ea333c9 100644 --- a/tests/src/python/test_provider_postgresraster.py +++ b/tests/src/python/test_provider_postgresraster.py @@ -630,6 +630,41 @@ def testSparseRaster(self): critical_postgis_logs = list(filter(lambda log: log[2] == Qgis.Critical and log[1] == "PostGIS", list(log_spy))) self.assertEqual(len(critical_postgis_logs), 0, list(log_spy)) + def testSparseTiles(self): + """Test issue GH #55784""" + + rl = QgsRasterLayer( + self.dbconn + " key=\'rid\' srid=3035 sslmode=disable table={table} schema={schema}".format( + table='raster_sparse_3035', schema='public'), 'pg_layer', 'postgresraster') + + self.assertTrue(rl.isValid()) + + dp = rl.dataProvider() + + r = dp.identify(QgsPointXY(4080317.72, 2430635.68), Qgis.RasterIdentifyFormat.Value).results() + self.assertEqual(r[1], -9999.0) + + # tile request returned no tiles, check nodata + ext = QgsRectangle.fromCenterAndSize(QgsPointXY(4080317.72, 2430635.68), 1, 1) + b = dp.block(1, ext, 1, 1) + self.assertTrue(b.isValid()) + self.assertEqual(b.value(0, 0), -9999.0) + self.assertTrue(b.isNoData(0, 0)) + + # tile request returned one tile with value and nodata in two adjacent cells + ext = QgsRectangle(4080186, 2430632, 4080216, 2430649) + b = dp.block(1, ext, 2, 1) + self.assertEqual(b.value(0, 1), -9999.0) + self.assertEqual(int(b.value(0, 0)), 223) + + # Multiple nodata tiles + ext = QgsRectangle(4080272, 2430686, 4080302, 2430703) + b = dp.block(1, ext, 2, 1) + self.assertEqual(b.value(0, 1), -9999.0) + self.assertEqual(b.value(0, 0), -9999.0) + self.assertTrue(b.isNoData(0, 0)) + self.assertTrue(b.isNoData(0, 1)) + if __name__ == '__main__': unittest.main()