Skip to content

Commit

Permalink
[FEATURE] Preview of WMTS layers + add XYZ tile layers (PR #3473)
Browse files Browse the repository at this point in the history
This introduces live preview when rendering WMTS layers - as soon as individual tiles are loaded, they are shown in map canvas... no need to wait with a blank map until all tiles are fully downloaded. Additionally, if there are already locally cached tiles of other zoom levels, they may be used in the preview while the tiles with best matching zoom level are being downloaded. This greatly improves the user experience when working with WMTS layers.

Additionally, I have added native support for XYZ tile layers into WMS provider (based on existing implementation of WMTS tiling). This allows loading of various new raster tile sources (e.g. OpenStreetMap tiles) that were before available only with QuickMapServices or OpenLayers plugins. To use XYZ tile layers, open the browser dock in QGIS and look for "Tile Servers (XYZ)" root entry. Right-clicking will open a menu to add connections. For example for OpenStreetMap the URL would be http://c.tile.openstreetmap.org/{z}/{x}/{y}.png

The work on WMTS live preview has been funded by Land Information New Zealand.

The work on XYZ tile layers has been funded by Lutra Consulting.
  • Loading branch information
wonder-sk committed Sep 13, 2016
2 parents 1f715c1 + adc88f2 commit 717e716
Show file tree
Hide file tree
Showing 30 changed files with 1,526 additions and 696 deletions.
3 changes: 2 additions & 1 deletion python/core/qgsmapsettings.sip
Expand Up @@ -116,7 +116,8 @@ class QgsMapSettings
UseRenderingOptimization, //!< Enable vector simplification and other rendering optimizations
DrawSelection, //!< Whether vector selections should be shown in the rendered map
DrawSymbolBounds, //!< Draw bounds of symbols (for debugging/testing)
RenderMapTile //!< Draw map such that there are no problems between adjacent tiles
RenderMapTile, //!< Draw map such that there are no problems between adjacent tiles
RenderPartialOutput, //!< Whether to make extra effort to update map image with partially rendered layers (better for interactive map canvas). Added in QGIS 3.0
};
typedef QFlags<QgsMapSettings::Flag> Flags;

Expand Down
1 change: 1 addition & 0 deletions python/core/qgsrendercontext.sip
Expand Up @@ -23,6 +23,7 @@ class QgsRenderContext
DrawSymbolBounds, //!< Draw bounds of symbols (for debugging/testing)
RenderMapTile, //!< Draw map such that there are no problems between adjacent tiles
Antialiasing, //!< Use antialiasing while drawing
RenderPartialOutput, //!< Whether to make extra effort to update map image with partially rendered layers (better for interactive map canvas). Added in QGIS 3.0
};
typedef QFlags<QgsRenderContext::Flag> Flags;

Expand Down
30 changes: 29 additions & 1 deletion python/core/raster/qgsrasterinterface.sip
Expand Up @@ -9,7 +9,30 @@ class QgsRasterBlockFeedback : QgsFeedback
%TypeHeaderCode
#include <qgsrasterinterface.h>
%End
// TODO: extend with preview functionality??

public:
//! Construct a new raster block feedback object
QgsRasterBlockFeedback( QObject* parent = nullptr );

//! May be emitted by raster data provider to indicate that some partial data are available
//! and a new preview image may be produced
virtual void onNewData();

//! Whether the raster provider should return only data that are already available
//! without waiting for full result. By default this flag is not enabled.
//! @see setPreviewOnly()
bool isPreviewOnly() const;
//! set flag whether the block request is for preview purposes only
//! @see isPreviewOnly()
void setPreviewOnly( bool preview );

//! Whether our painter is drawing to a temporary image used just by this layer
//! @see setRenderPartialOutput()
bool renderPartialOutput() const;
//! Set whether our painter is drawing to a temporary image used just by this layer
//! @see renderPartialOutput()
void setRenderPartialOutput( bool enable );

};


Expand Down Expand Up @@ -256,5 +279,10 @@ class QgsRasterInterface
int theStats = QgsRasterBandStats::All,
const QgsRectangle & theExtent = QgsRectangle(),
int theBinCount = 0 );

private:
QgsRasterInterface(const QgsRasterInterface &);
QgsRasterInterface &operator=(const QgsRasterInterface &);

};

38 changes: 6 additions & 32 deletions python/core/raster/qgsrasterprojector.sip
@@ -1,6 +1,10 @@

/** Raster projector */

/** \ingroup core
* \brief QgsRasterProjector implements approximate projection support for
* it calculates grid of points in source CRS for target CRS + extent
* which are used to calculate affine transformation matrices.
* \class QgsRasterProjector
*/
class QgsRasterProjector : QgsRasterInterface
{
%TypeHeaderCode
Expand All @@ -18,33 +22,6 @@ class QgsRasterProjector : QgsRasterInterface
Exact, //!< Exact, precise but slow
};

/** \brief QgsRasterProjector implements approximate projection support for
* it calculates grid of points in source CRS for target CRS + extent
* which are used to calculate affine transformation matrices.
*/

QgsRasterProjector( const QgsCoordinateReferenceSystem& theSrcCRS,
const QgsCoordinateReferenceSystem& theDestCRS,
int theSrcDatumTransform,
int theDestDatumTransform,
const QgsRectangle& theDestExtent,
int theDestRows, int theDestCols,
double theMaxSrcXRes, double theMaxSrcYRes,
const QgsRectangle& theExtent
);

QgsRasterProjector( const QgsCoordinateReferenceSystem& theSrcCRS,
const QgsCoordinateReferenceSystem& theDestCRS,
const QgsRectangle& theDestExtent,
int theDestRows, int theDestCols,
double theMaxSrcXRes, double theMaxSrcYRes,
const QgsRectangle& theExtent
);
QgsRasterProjector( const QgsCoordinateReferenceSystem& theSrcCRS,
const QgsCoordinateReferenceSystem& theDestCRS,
double theMaxSrcXRes, double theMaxSrcYRes,
const QgsRectangle& theExtent
);
QgsRasterProjector();

/** \brief The destructor */
Expand All @@ -66,9 +43,6 @@ class QgsRasterProjector : QgsRasterInterface
/** \brief Get destination CRS */
QgsCoordinateReferenceSystem destinationCrs() const;

/** \brief set maximum source resolution */
void setMaxSrcRes( double theMaxSrcXRes, double theMaxSrcYRes );

Precision precision() const;
void setPrecision( Precision precision );
// Translated precision mode, for use in ComboBox etc.
Expand Down
42 changes: 25 additions & 17 deletions src/app/qgslayerstylingwidget.cpp
Expand Up @@ -38,6 +38,7 @@
#include "qgsrenderer.h"
#include "qgsrendererregistry.h"
#include "qgsmaplayerregistry.h"
#include "qgsrasterdataprovider.h"
#include "qgsrasterlayer.h"
#include "qgsmaplayerconfigwidget.h"
#include "qgsmaplayerstylemanagerwidget.h"
Expand Down Expand Up @@ -171,10 +172,14 @@ void QgsLayerStylingWidget::setLayer( QgsMapLayer *layer )
transparencyItem->setToolTip( tr( "Transparency" ) );
transparencyItem->setData( Qt::UserRole, RasterTransparency );
mOptionsListWidget->addItem( transparencyItem );
QListWidgetItem* histogramItem = new QListWidgetItem( QgsApplication::getThemeIcon( "propertyicons/histogram.png" ), QString() );
histogramItem->setData( Qt::UserRole, RasterHistogram );
mOptionsListWidget->addItem( histogramItem );
histogramItem->setToolTip( tr( "Histogram" ) );

if ( static_cast<QgsRasterLayer*>( layer )->dataProvider()->capabilities() & QgsRasterDataProvider::Size )
{
QListWidgetItem* histogramItem = new QListWidgetItem( QgsApplication::getThemeIcon( "propertyicons/histogram.png" ), QString() );
histogramItem->setData( Qt::UserRole, RasterHistogram );
mOptionsListWidget->addItem( histogramItem );
histogramItem->setToolTip( tr( "Histogram" ) );
}
}

Q_FOREACH ( QgsMapLayerConfigWidgetFactory* factory, mPageFactories )
Expand Down Expand Up @@ -392,21 +397,24 @@ void QgsLayerStylingWidget::updateCurrentWidgetLayer()
}
case 2: // Histogram
{
if ( mRasterStyleWidget )
if ( rlayer->dataProvider()->capabilities() & QgsRasterDataProvider::Size )
{
mRasterStyleWidget->deleteLater();
delete mRasterStyleWidget;
if ( mRasterStyleWidget )
{
mRasterStyleWidget->deleteLater();
delete mRasterStyleWidget;
}
mRasterStyleWidget = new QgsRendererRasterPropertiesWidget( rlayer, mMapCanvas, mWidgetStack );
mRasterStyleWidget->syncToLayer( rlayer );
connect( mRasterStyleWidget, SIGNAL( widgetChanged() ), this, SLOT( autoApply() ) );

QgsRasterHistogramWidget* widget = new QgsRasterHistogramWidget( rlayer, mWidgetStack );
connect( widget, SIGNAL( widgetChanged() ), this, SLOT( autoApply() ) );
QString name = mRasterStyleWidget->currentRenderWidget()->renderer()->type();
widget->setRendererWidget( name, mRasterStyleWidget->currentRenderWidget() );

mWidgetStack->addMainPanel( widget );
}
mRasterStyleWidget = new QgsRendererRasterPropertiesWidget( rlayer, mMapCanvas, mWidgetStack );
mRasterStyleWidget->syncToLayer( rlayer );
connect( mRasterStyleWidget, SIGNAL( widgetChanged() ), this, SLOT( autoApply() ) );

QgsRasterHistogramWidget* widget = new QgsRasterHistogramWidget( rlayer, mWidgetStack );
connect( widget, SIGNAL( widgetChanged() ), this, SLOT( autoApply() ) );
QString name = mRasterStyleWidget->currentRenderWidget()->renderer()->type();
widget->setRendererWidget( name, mRasterStyleWidget->currentRenderWidget() );

mWidgetStack->addMainPanel( widget );
break;
}
default:
Expand Down
13 changes: 13 additions & 0 deletions src/core/qgsdataitemproviderregistry.cpp
Expand Up @@ -21,6 +21,8 @@
#include "qgslogger.h"
#include "qgsproviderregistry.h"

typedef QList<QgsDataItemProvider*> dataItemProviders_t();


/**
* \ingroup core
Expand Down Expand Up @@ -64,6 +66,17 @@ QgsDataItemProviderRegistry::QgsDataItemProviderRegistry()
if ( !library )
continue;

// new / better way of returning data items from providers

dataItemProviders_t* dataItemProvidersFn = reinterpret_cast< dataItemProviders_t * >( cast_to_fptr( library->resolve( "dataItemProviders" ) ) );
if ( dataItemProvidersFn )
{
// the function is a factory - we keep ownership of the returned providers
mProviders << dataItemProvidersFn();
}

// legacy support - using dataItem() and dataCapabilities() methods

dataCapabilities_t * dataCapabilities = reinterpret_cast< dataCapabilities_t * >( cast_to_fptr( library->resolve( "dataCapabilities" ) ) );
if ( !dataCapabilities )
{
Expand Down
4 changes: 3 additions & 1 deletion src/core/qgsmaplayer.cpp
Expand Up @@ -234,7 +234,9 @@ bool QgsMapLayer::readLayerXml( const QDomElement& layerElement )
// This is modified version of old QgsWmsProvider::parseUri
// The new format has always params crs,format,layers,styles and that params
// should not appear in old format url -> use them to identify version
if ( !mDataSource.contains( "crs=" ) && !mDataSource.contains( "format=" ) )
// XYZ tile layers do not need to contain crs,format params, but they have type=xyz
if ( !mDataSource.contains( "type=" ) &&
!mDataSource.contains( "crs=" ) && !mDataSource.contains( "format=" ) )
{
QgsDebugMsg( "Old WMS URI format detected -> converting to new format" );
QgsDataSourceUri uri;
Expand Down
6 changes: 6 additions & 0 deletions src/core/qgsmaprenderercustompainterjob.cpp
Expand Up @@ -333,6 +333,12 @@ bool QgsMapRendererJob::needTemporaryImage( QgsMapLayer* ml )
return true;
}
}
else if ( ml->type() == QgsMapLayer::RasterLayer )
{
// preview of intermediate raster rendering results requires a temporary output image
if ( mSettings.testFlag( QgsMapSettings::RenderPartialOutput ) )
return true;
}

return false;
}
Expand Down
3 changes: 2 additions & 1 deletion src/core/qgsmapsettings.h
Expand Up @@ -164,7 +164,8 @@ class CORE_EXPORT QgsMapSettings
UseRenderingOptimization = 0x20, //!< Enable vector simplification and other rendering optimizations
DrawSelection = 0x40, //!< Whether vector selections should be shown in the rendered map
DrawSymbolBounds = 0x80, //!< Draw bounds of symbols (for debugging/testing)
RenderMapTile = 0x100 //!< Draw map such that there are no problems between adjacent tiles
RenderMapTile = 0x100, //!< Draw map such that there are no problems between adjacent tiles
RenderPartialOutput = 0x200, //!< Whether to make extra effort to update map image with partially rendered layers (better for interactive map canvas). Added in QGIS 3.0
// TODO: ignore scale-based visibility (overview)
};
Q_DECLARE_FLAGS( Flags, Flag )
Expand Down
1 change: 1 addition & 0 deletions src/core/qgsrendercontext.cpp
Expand Up @@ -129,6 +129,7 @@ QgsRenderContext QgsRenderContext::fromMapSettings( const QgsMapSettings& mapSet
ctx.setFlag( DrawSymbolBounds, mapSettings.testFlag( QgsMapSettings::DrawSymbolBounds ) );
ctx.setFlag( RenderMapTile, mapSettings.testFlag( QgsMapSettings::RenderMapTile ) );
ctx.setFlag( Antialiasing, mapSettings.testFlag( QgsMapSettings::Antialiasing ) );
ctx.setFlag( RenderPartialOutput, mapSettings.testFlag( QgsMapSettings::RenderPartialOutput ) );
ctx.setRasterScaleFactor( 1.0 );
ctx.setScaleFactor( mapSettings.outputDpi() / 25.4 ); // = pixels per mm
ctx.setRendererScale( mapSettings.scale() );
Expand Down
1 change: 1 addition & 0 deletions src/core/qgsrendercontext.h
Expand Up @@ -65,6 +65,7 @@ class CORE_EXPORT QgsRenderContext
DrawSymbolBounds = 0x20, //!< Draw bounds of symbols (for debugging/testing)
RenderMapTile = 0x40, //!< Draw map such that there are no problems between adjacent tiles
Antialiasing = 0x80, //!< Use antialiasing while drawing
RenderPartialOutput = 0x100, //!< Whether to make extra effort to update map image with partially rendered layers (better for interactive map canvas). Added in QGIS 3.0
};
Q_DECLARE_FLAGS( Flags, Flag )

Expand Down
10 changes: 10 additions & 0 deletions src/core/raster/qgsrasterdrawer.cpp
Expand Up @@ -88,10 +88,20 @@ void QgsRasterDrawer::draw( QPainter* p, QgsRasterViewPort* viewPort, const QgsM
}
}

if ( feedback && feedback->renderPartialOutput() )
{
// there could have been partial preview written before
// so overwrite anything with the resulting image.
// (we are guaranteed to have a temporary image for this layer, see QgsMapRendererJob::needTemporaryImage)
p->setCompositionMode( QPainter::CompositionMode_Source );
}

drawImage( p, viewPort, img, topLeftCol, topLeftRow, theQgsMapToPixel );

delete block;

p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // go back to the default composition mode

// ok this does not matter much anyway as the tile size quite big so most of the time
// there would be just one tile for the whole display area, but it won't hurt...
if ( feedback && feedback->isCancelled() )
Expand Down
34 changes: 33 additions & 1 deletion src/core/raster/qgsrasterinterface.h
Expand Up @@ -36,7 +36,36 @@
*/
class CORE_EXPORT QgsRasterBlockFeedback : public QgsFeedback
{
// TODO: extend with preview functionality??
public:
//! Construct a new raster block feedback object
QgsRasterBlockFeedback( QObject* parent = nullptr ) : QgsFeedback( parent ), mPreviewOnly( false ), mRenderPartialOutput( false ) {}

//! May be emitted by raster data provider to indicate that some partial data are available
//! and a new preview image may be produced
virtual void onNewData() {}

//! Whether the raster provider should return only data that are already available
//! without waiting for full result. By default this flag is not enabled.
//! @see setPreviewOnly()
bool isPreviewOnly() const { return mPreviewOnly; }
//! set flag whether the block request is for preview purposes only
//! @see isPreviewOnly()
void setPreviewOnly( bool preview ) { mPreviewOnly = preview; }

//! Whether our painter is drawing to a temporary image used just by this layer
//! @see setRenderPartialOutput()
bool renderPartialOutput() const { return mRenderPartialOutput; }
//! Set whether our painter is drawing to a temporary image used just by this layer
//! @see renderPartialOutput()
void setRenderPartialOutput( bool enable ) { mRenderPartialOutput = enable; }

private:
//! Whether the raster provider should return only data that are already available
//! without waiting for full result
bool mPreviewOnly;

//! Whether our painter is drawing to a temporary image used just by this layer
bool mRenderPartialOutput;
};


Expand Down Expand Up @@ -258,6 +287,9 @@ class CORE_EXPORT QgsRasterInterface
int theStats = QgsRasterBandStats::All,
const QgsRectangle & theExtent = QgsRectangle(),
int theBinCount = 0 );

private:
Q_DISABLE_COPY( QgsRasterInterface ) // there is clone() for copying
};

#endif
Expand Down
34 changes: 33 additions & 1 deletion src/core/raster/qgsrasterlayerrenderer.cpp
Expand Up @@ -29,7 +29,7 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer* layer, QgsRender
, mRasterViewPort( nullptr )
, mPipe( nullptr )
, mContext( rendererContext )
, mFeedback( new QgsRasterBlockFeedback() )
, mFeedback( new Feedback( this ) )
{
mPainter = rendererContext.painter();
const QgsMapToPixel& theQgsMapToPixel = rendererContext.mapToPixel();
Expand Down Expand Up @@ -226,3 +226,35 @@ QgsFeedback* QgsRasterLayerRenderer::feedback() const
{
return mFeedback;
}

QgsRasterLayerRenderer::Feedback::Feedback( QgsRasterLayerRenderer *r )
: mR( r )
, mMinimalPreviewInterval( 250 )
{
setRenderPartialOutput( r->mContext.testFlag( QgsRenderContext::RenderPartialOutput ) );
}

void QgsRasterLayerRenderer::Feedback::onNewData()
{
if ( !renderPartialOutput() )
return; // we were not asked for partial renders and we may not have a temporary image for overwriting...

// update only once upon a time
// (preview itself takes some time)
if ( mLastPreview.isValid() && mLastPreview.msecsTo( QTime::currentTime() ) < mMinimalPreviewInterval )
return;

// TODO: update only the area that got new data

QgsDebugMsg( QString( "new raster preview! %1" ).arg( mLastPreview.msecsTo( QTime::currentTime() ) ) );
QTime t;
t.start();
QgsRasterBlockFeedback feedback;
feedback.setPreviewOnly( true );
feedback.setRenderPartialOutput( true );
QgsRasterIterator iterator( mR->mPipe->last() );
QgsRasterDrawer drawer( &iterator );
drawer.draw( mR->mPainter, mR->mRasterViewPort, mR->mMapToPixel, &feedback );
QgsDebugMsg( QString( "total raster preview time: %1 ms" ).arg( t.elapsed() ) );
mLastPreview = QTime::currentTime();
}

0 comments on commit 717e716

Please sign in to comment.