diff --git a/src/providers/wms/qgswmscapabilities.cpp b/src/providers/wms/qgswmscapabilities.cpp index f1d1e0ade64..fc3e1a90381 100644 --- a/src/providers/wms/qgswmscapabilities.cpp +++ b/src/providers/wms/qgswmscapabilities.cpp @@ -2151,3 +2151,15 @@ const QgsWmtsTileMatrix* QgsWmtsTileMatrixSet::findNearestResolution( double vre return &it.value(); } + +const QgsWmtsTileMatrix *QgsWmtsTileMatrixSet::findOtherResolution( double tres, int offset ) const +{ + QMap::const_iterator it = tileMatrices.constFind( tres ); + if ( it == tileMatrices.constEnd() ) + return nullptr; + it += offset; + if ( it == tileMatrices.constEnd() ) + return nullptr; + + return &it.value(); +} diff --git a/src/providers/wms/qgswmscapabilities.h b/src/providers/wms/qgswmscapabilities.h index 4e4d078d58a..f760e5c35f2 100644 --- a/src/providers/wms/qgswmscapabilities.h +++ b/src/providers/wms/qgswmscapabilities.h @@ -350,6 +350,9 @@ struct QgsWmtsTileMatrixSet //! Returns closest tile resolution to the requested one. (resolution = width [map units] / with [pixels]) const QgsWmtsTileMatrix* findNearestResolution( double vres ) const; + + //! Return tile matrix for other near resolution from given tres (positive offset = lower resolution tiles) + const QgsWmtsTileMatrix* findOtherResolution( double tres, int offset ) const; }; enum QgsTileMode { WMTS, WMSC, XYZ }; diff --git a/src/providers/wms/qgswmsprovider.cpp b/src/providers/wms/qgswmsprovider.cpp index 4e16e8b643b..f6e1bb1fa9a 100644 --- a/src/providers/wms/qgswmsprovider.cpp +++ b/src/providers/wms/qgswmsprovider.cpp @@ -502,9 +502,143 @@ QImage *QgsWmsProvider::draw( QgsRectangle const &viewExtent, int pixelWidth, in } #include -static QCache sTileCache; +static QCache sTileCache( 256 ); static QMutex sTileCacheMutex; +static bool _fetchCachedTileImage( const QUrl& url, QImage& localImage ) +{ + QMutexLocker locker( &sTileCacheMutex ); + if ( QImage* i = sTileCache.object( url ) ) + { + localImage = *i; + return true; + } + else if ( QgsNetworkAccessManager::instance()->cache()->metaData( url ).isValid() ) + { + if ( QIODevice* data = QgsNetworkAccessManager::instance()->cache()->data( url ) ) + { + QByteArray imageData = data->readAll(); + delete data; + + localImage = QImage::fromData( imageData ); + + // cache it as well (mutex is already locked) + sTileCache.insert( url, new QImage( localImage ) ); + + return true; + } + } + return false; +} + +static bool _fuzzyContainsRect( const QRectF& r1, const QRectF& r2 ) +{ + double significantDigits = log10( qMax( r1.width(), r1.height() ) ); + double epsilon = exp10( significantDigits - 5 ); // floats have 6-9 significant digits + return r1.contains( r2.adjusted( epsilon, epsilon, -epsilon, -epsilon ) ); +} + +void QgsWmsProvider::fetchOtherResTiles( QgsTileMode tileMode, const QgsRectangle& viewExtent, int imageWidth, QList& missingRects, double tres, int resOffset, QList& otherResTiles ) +{ + const QgsWmtsTileMatrix* tmOther = mTileMatrixSet->findOtherResolution( tres, resOffset ); + if ( !tmOther ) + return; + + QSet tilesSet; + Q_FOREACH ( const QRectF& missingTileRect, missingRects ) + { + int c0, r0, c1, r1; + tmOther->viewExtentIntersection( QgsRectangle( missingTileRect ), nullptr, c0, r0, c1, r1 ); + + for ( int row = r0; row <= r1; row++ ) + { + for ( int col = c0; col <= c1; col++ ) + { + tilesSet << TilePosition( row, col ); + } + } + } + + // get URLs of tiles because their URLs are used as keys in the tile cache + TilePositions tiles = tilesSet.toList(); + TileRequests requests; + switch ( tileMode ) + { + case WMSC: + createTileRequestsWMSC( tmOther, tiles, requests ); + break; + + case WMTS: + createTileRequestsWMTS( tmOther, tiles, requests ); + break; + + case XYZ: + createTileRequestsXYZ( tmOther, tiles, requests ); + break; + } + + QList missingRectsToDelete; + Q_FOREACH ( const TileRequest& r, requests ) + { + QImage localImage; + if ( !_fetchCachedTileImage( r.url, localImage ) ) + continue; + + double cr = viewExtent.width() / imageWidth; + QRectF dst(( r.rect.left() - viewExtent.xMinimum() ) / cr, + ( viewExtent.yMaximum() - r.rect.bottom() ) / cr, + r.rect.width() / cr, + r.rect.height() / cr ); + otherResTiles << TileImage( dst, localImage ); + + // see if there are any missing rects that are completely covered by this tile + Q_FOREACH ( const QRectF& missingRect, missingRects ) + { + // we need to do a fuzzy "contains" check because the coordinates may not align perfectly + // due to numerical errors and/or transform of coords from double to floats + if ( _fuzzyContainsRect( r.rect, missingRect ) ) + { + missingRectsToDelete << missingRect; + } + } + } + + // remove all the rectangles we have completely covered by tiles from this resolution + // so we will not use tiles from multiple resolutions for one missing tile (to save time) + Q_FOREACH ( const QRectF& rectToDelete, missingRectsToDelete ) + { + missingRects.removeOne( rectToDelete ); + } + + QgsDebugMsg( QString( "Other resolution tiles: offset %1, res %2, missing rects %3, remaining rects %4, added tiles %5" ) + .arg( resOffset ) + .arg( tmOther->tres ) + .arg( missingRects.count() + missingRectsToDelete.count() ) + .arg( missingRects.count() ) + .arg( otherResTiles.count() ) ); +} + +uint qHash( const QgsWmsProvider::TilePosition& tp ) +{ + return ( uint ) tp.col + (( uint ) tp.row << 16 ); +} + +static void _drawDebugRect( QPainter& p, const QRectF& rect, const QColor& color ) +{ +#if 0 // good for debugging how tiles from various resolutions are used + QPainter::CompositionMode oldMode = p.compositionMode(); + p.setCompositionMode( QPainter::CompositionMode_SourceOver ); + QColor c = color; + c.setAlpha( 100 ); + p.fillRect( rect, QBrush( c, Qt::DiagCrossPattern ) ); + p.setCompositionMode( oldMode ); +#else + Q_UNUSED( p ); + Q_UNUSED( rect ); + Q_UNUSED( color ); +#endif +} + QImage *QgsWmsProvider::draw( QgsRectangle const & viewExtent, int pixelWidth, int pixelHeight, QgsRasterBlockFeedback* feedback ) { QgsDebugMsg( "Entering." ); @@ -642,39 +776,16 @@ QImage *QgsWmsProvider::draw( QgsRectangle const & viewExtent, int pixelWidth, i emit statusChanged( tr( "Getting tiles." ) ); + QList tileImages; // in the correct resolution + QList missing; // rectangles (in map coords) of missing tiles for this view + QTime t; t.start(); - int memCached = 0, diskCached = 0; TileRequests requestsFinal; Q_FOREACH ( const TileRequest& r, requests ) { QImage localImage; - - sTileCacheMutex.lock(); - if ( QImage* i = sTileCache.object( r.url ) ) - { - localImage = *i; - memCached++; - } - else if ( QgsNetworkAccessManager::instance()->cache()->metaData( r.url ).isValid() ) - { - if ( QIODevice* data = QgsNetworkAccessManager::instance()->cache()->data( r.url ) ) - { - QByteArray imageData = data->readAll(); - delete data; - - localImage = QImage::fromData( imageData ); - - // cache it as well (mutex is already locked) - sTileCache.insert( r.url, new QImage( localImage ) ); - - diskCached++; - } - } - sTileCacheMutex.unlock(); - - // draw the tile directly if possible - if ( !localImage.isNull() ) + if ( _fetchCachedTileImage( r.url, localImage ) ) { double cr = viewExtent.width() / image->width(); @@ -682,28 +793,82 @@ QImage *QgsWmsProvider::draw( QgsRectangle const & viewExtent, int pixelWidth, i ( viewExtent.yMaximum() - r.rect.bottom() ) / cr, r.rect.width() / cr, r.rect.height() / cr ); - - QPainter p( image ); - if ( mSettings.mSmoothPixmapTransform ) - p.setRenderHint( QPainter::SmoothPixmapTransform, true ); - p.drawImage( dst, localImage ); + tileImages << TileImage( dst, localImage ); } else { + missing << r.rect; + // need to make a request requestsFinal << r; } } + int t0 = t.elapsed(); + + + // draw other res tiles if preview + QPainter p( image ); + if ( feedback && feedback->preview_only && missing.count() > 0 ) + { + // some tiles are still missing, so let's see if we have any cached tiles + // from lower or higher resolution available to give the user a bit of context + // while loading the right resolution + + p.setCompositionMode( QPainter::CompositionMode_Source ); + p.fillRect( image->rect(), QBrush( Qt::lightGray, Qt::CrossPattern ) ); + p.setRenderHint( QPainter::SmoothPixmapTransform, false ); // let's not waste time with bilinear filtering + + QList lowerResTiles, lowerResTiles2, higherResTiles; + // first we check lower resolution tiles: one level back, then two levels back (if there is still some are not covered), + // finally (in the worst case we use one level higher resolution tiles). This heuristic should give + // good overviews while not spending too much time drawing cached tiles from resolutions far away. + fetchOtherResTiles( tileMode, viewExtent, image->width(), missing, tm->tres, 1, lowerResTiles ); + fetchOtherResTiles( tileMode, viewExtent, image->width(), missing, tm->tres, 2, lowerResTiles2 ); + fetchOtherResTiles( tileMode, viewExtent, image->width(), missing, tm->tres, -1, higherResTiles ); + + // draw the cached tiles lowest to highest resolution + Q_FOREACH ( const TileImage& ti, lowerResTiles2 ) + { + p.drawImage( ti.rect, ti.img ); + _drawDebugRect( p, ti.rect, Qt::blue ); + } + Q_FOREACH ( const TileImage& ti, lowerResTiles ) + { + p.drawImage( ti.rect, ti.img ); + _drawDebugRect( p, ti.rect, Qt::yellow ); + } + Q_FOREACH ( const TileImage& ti, higherResTiles ) + { + p.drawImage( ti.rect, ti.img ); + _drawDebugRect( p, ti.rect, Qt::red ); + } + } + + int t1 = t.elapsed() - t0; + + // draw composite in this resolution + Q_FOREACH ( const TileImage& ti, tileImages ) + { + if ( mSettings.mSmoothPixmapTransform ) + p.setRenderHint( QPainter::SmoothPixmapTransform, true ); + p.drawImage( ti.rect, ti.img ); + + if ( feedback && feedback->preview_only ) + _drawDebugRect( p, ti.rect, Qt::green ); + } + p.end(); + + int t2 = t.elapsed() - t1; if ( feedback && feedback->preview_only ) { - qDebug( "PREVIEW - MEM CACHED: %d / DISK CACHED: %d / MISSING: %d", memCached, diskCached, requests.count() - memCached - diskCached ); - qDebug( "PREVIEW - SPENT IN WMTS PROVIDER: %d ms", t.elapsed() ); + qDebug( "PREVIEW - CACHED: %d / MISSING: %d", tileImages.count(), requests.count() - tileImages.count() ); + qDebug( "PREVIEW - TIME: this res %d ms | other res %d ms | TOTAL %d ms", t0 + t2, t1, t0 + t1 + t2 ); } else if ( !requestsFinal.isEmpty() ) { // let the feedback object know about the tiles we have already - if ( feedback && memCached + diskCached > 0 ) + if ( feedback && feedback->render_partial_output ) feedback->onNewData(); // order tile requests according to the distance from view center @@ -715,6 +880,7 @@ QImage *QgsWmsProvider::draw( QgsRectangle const & viewExtent, int pixelWidth, i handler.downloadBlocking(); } + qDebug( "TILE CACHE total: %d / %d ", sTileCache.totalCost(), sTileCache.maxCost() ); #if 0 const QgsWmsStatistics::Stat& stat = QgsWmsStatistics::statForUri( dataSourceUri() ); diff --git a/src/providers/wms/qgswmsprovider.h b/src/providers/wms/qgswmsprovider.h index 798e14662d5..954277e3caa 100644 --- a/src/providers/wms/qgswmsprovider.h +++ b/src/providers/wms/qgswmsprovider.h @@ -362,6 +362,16 @@ class QgsWmsProvider : public QgsRasterDataProvider }; typedef QList TileRequests; + //! Tile identifier within a tile source + typedef struct TilePosition + { + TilePosition( int r, int c ): row( r ), col( c ) {} + bool operator==( const TilePosition& other ) const { return row == other.row && col == other.col; } + int row; + int col; + } TilePosition; + typedef QList TilePositions; + signals: /** \brief emit a signal to notify of a progress event */ @@ -441,20 +451,21 @@ class QgsWmsProvider : public QgsRasterDataProvider private: - //! Tile identifier within a tile source - typedef struct TilePosition - { - TilePosition( int r, int c ): row( r ), col( c ) {} - int row; - int col; - } TilePosition; - typedef QList TilePositions; - QUrl createRequestUrlWMS( const QgsRectangle& viewExtent, int pixelWidth, int pixelHeight ); void createTileRequestsWMSC( const QgsWmtsTileMatrix* tm, const QgsWmsProvider::TilePositions& tiles, QgsWmsProvider::TileRequests& requests ); void createTileRequestsWMTS( const QgsWmtsTileMatrix* tm, const QgsWmsProvider::TilePositions& tiles, QgsWmsProvider::TileRequests& requests ); void createTileRequestsXYZ( const QgsWmtsTileMatrix* tm, const QgsWmsProvider::TilePositions& tiles, QgsWmsProvider::TileRequests& requests ); + //! Helper structure to store a cached tile image with its rectangle + typedef struct TileImage + { + TileImage( QRectF r, QImage i ): rect( r ), img( i ) {} + QRectF rect; //!< destination rectangle for a tile (in screen coordinates) + QImage img; //!< cached tile to be drawn + } TileImage; + //! Get tiles from a different resolution to cover the missing areas + void fetchOtherResTiles( QgsTileMode tileMode, const QgsRectangle& viewExtent, int imageWidth, QList& missing, double tres, int resOffset, QList &otherResTiles ); + /** Return the full url to request legend graphic * The visibleExtent isi only used if provider supports contextual * legends according to the QgsWmsSettings