Skip to content

Commit 717e716

Browse files
authored
[FEATURE] Preview of WMTS layers + add XYZ tile layers (PR #3473)
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.
2 parents 1f715c1 + adc88f2 commit 717e716

30 files changed

+1526
-696
lines changed

python/core/qgsmapsettings.sip

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ class QgsMapSettings
116116
UseRenderingOptimization, //!< Enable vector simplification and other rendering optimizations
117117
DrawSelection, //!< Whether vector selections should be shown in the rendered map
118118
DrawSymbolBounds, //!< Draw bounds of symbols (for debugging/testing)
119-
RenderMapTile //!< Draw map such that there are no problems between adjacent tiles
119+
RenderMapTile, //!< Draw map such that there are no problems between adjacent tiles
120+
RenderPartialOutput, //!< Whether to make extra effort to update map image with partially rendered layers (better for interactive map canvas). Added in QGIS 3.0
120121
};
121122
typedef QFlags<QgsMapSettings::Flag> Flags;
122123

python/core/qgsrendercontext.sip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class QgsRenderContext
2323
DrawSymbolBounds, //!< Draw bounds of symbols (for debugging/testing)
2424
RenderMapTile, //!< Draw map such that there are no problems between adjacent tiles
2525
Antialiasing, //!< Use antialiasing while drawing
26+
RenderPartialOutput, //!< Whether to make extra effort to update map image with partially rendered layers (better for interactive map canvas). Added in QGIS 3.0
2627
};
2728
typedef QFlags<QgsRenderContext::Flag> Flags;
2829

python/core/raster/qgsrasterinterface.sip

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,30 @@ class QgsRasterBlockFeedback : QgsFeedback
99
%TypeHeaderCode
1010
#include <qgsrasterinterface.h>
1111
%End
12-
// TODO: extend with preview functionality??
12+
13+
public:
14+
//! Construct a new raster block feedback object
15+
QgsRasterBlockFeedback( QObject* parent = nullptr );
16+
17+
//! May be emitted by raster data provider to indicate that some partial data are available
18+
//! and a new preview image may be produced
19+
virtual void onNewData();
20+
21+
//! Whether the raster provider should return only data that are already available
22+
//! without waiting for full result. By default this flag is not enabled.
23+
//! @see setPreviewOnly()
24+
bool isPreviewOnly() const;
25+
//! set flag whether the block request is for preview purposes only
26+
//! @see isPreviewOnly()
27+
void setPreviewOnly( bool preview );
28+
29+
//! Whether our painter is drawing to a temporary image used just by this layer
30+
//! @see setRenderPartialOutput()
31+
bool renderPartialOutput() const;
32+
//! Set whether our painter is drawing to a temporary image used just by this layer
33+
//! @see renderPartialOutput()
34+
void setRenderPartialOutput( bool enable );
35+
1336
};
1437

1538

@@ -256,5 +279,10 @@ class QgsRasterInterface
256279
int theStats = QgsRasterBandStats::All,
257280
const QgsRectangle & theExtent = QgsRectangle(),
258281
int theBinCount = 0 );
282+
283+
private:
284+
QgsRasterInterface(const QgsRasterInterface &);
285+
QgsRasterInterface &operator=(const QgsRasterInterface &);
286+
259287
};
260288

python/core/raster/qgsrasterprojector.sip

Lines changed: 6 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11

2-
/** Raster projector */
3-
2+
/** \ingroup core
3+
* \brief QgsRasterProjector implements approximate projection support for
4+
* it calculates grid of points in source CRS for target CRS + extent
5+
* which are used to calculate affine transformation matrices.
6+
* \class QgsRasterProjector
7+
*/
48
class QgsRasterProjector : QgsRasterInterface
59
{
610
%TypeHeaderCode
@@ -18,33 +22,6 @@ class QgsRasterProjector : QgsRasterInterface
1822
Exact, //!< Exact, precise but slow
1923
};
2024

21-
/** \brief QgsRasterProjector implements approximate projection support for
22-
* it calculates grid of points in source CRS for target CRS + extent
23-
* which are used to calculate affine transformation matrices.
24-
*/
25-
26-
QgsRasterProjector( const QgsCoordinateReferenceSystem& theSrcCRS,
27-
const QgsCoordinateReferenceSystem& theDestCRS,
28-
int theSrcDatumTransform,
29-
int theDestDatumTransform,
30-
const QgsRectangle& theDestExtent,
31-
int theDestRows, int theDestCols,
32-
double theMaxSrcXRes, double theMaxSrcYRes,
33-
const QgsRectangle& theExtent
34-
);
35-
36-
QgsRasterProjector( const QgsCoordinateReferenceSystem& theSrcCRS,
37-
const QgsCoordinateReferenceSystem& theDestCRS,
38-
const QgsRectangle& theDestExtent,
39-
int theDestRows, int theDestCols,
40-
double theMaxSrcXRes, double theMaxSrcYRes,
41-
const QgsRectangle& theExtent
42-
);
43-
QgsRasterProjector( const QgsCoordinateReferenceSystem& theSrcCRS,
44-
const QgsCoordinateReferenceSystem& theDestCRS,
45-
double theMaxSrcXRes, double theMaxSrcYRes,
46-
const QgsRectangle& theExtent
47-
);
4825
QgsRasterProjector();
4926

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

69-
/** \brief set maximum source resolution */
70-
void setMaxSrcRes( double theMaxSrcXRes, double theMaxSrcYRes );
71-
7246
Precision precision() const;
7347
void setPrecision( Precision precision );
7448
// Translated precision mode, for use in ComboBox etc.

src/app/qgslayerstylingwidget.cpp

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
#include "qgsrenderer.h"
3939
#include "qgsrendererregistry.h"
4040
#include "qgsmaplayerregistry.h"
41+
#include "qgsrasterdataprovider.h"
4142
#include "qgsrasterlayer.h"
4243
#include "qgsmaplayerconfigwidget.h"
4344
#include "qgsmaplayerstylemanagerwidget.h"
@@ -171,10 +172,14 @@ void QgsLayerStylingWidget::setLayer( QgsMapLayer *layer )
171172
transparencyItem->setToolTip( tr( "Transparency" ) );
172173
transparencyItem->setData( Qt::UserRole, RasterTransparency );
173174
mOptionsListWidget->addItem( transparencyItem );
174-
QListWidgetItem* histogramItem = new QListWidgetItem( QgsApplication::getThemeIcon( "propertyicons/histogram.png" ), QString() );
175-
histogramItem->setData( Qt::UserRole, RasterHistogram );
176-
mOptionsListWidget->addItem( histogramItem );
177-
histogramItem->setToolTip( tr( "Histogram" ) );
175+
176+
if ( static_cast<QgsRasterLayer*>( layer )->dataProvider()->capabilities() & QgsRasterDataProvider::Size )
177+
{
178+
QListWidgetItem* histogramItem = new QListWidgetItem( QgsApplication::getThemeIcon( "propertyicons/histogram.png" ), QString() );
179+
histogramItem->setData( Qt::UserRole, RasterHistogram );
180+
mOptionsListWidget->addItem( histogramItem );
181+
histogramItem->setToolTip( tr( "Histogram" ) );
182+
}
178183
}
179184

180185
Q_FOREACH ( QgsMapLayerConfigWidgetFactory* factory, mPageFactories )
@@ -392,21 +397,24 @@ void QgsLayerStylingWidget::updateCurrentWidgetLayer()
392397
}
393398
case 2: // Histogram
394399
{
395-
if ( mRasterStyleWidget )
400+
if ( rlayer->dataProvider()->capabilities() & QgsRasterDataProvider::Size )
396401
{
397-
mRasterStyleWidget->deleteLater();
398-
delete mRasterStyleWidget;
402+
if ( mRasterStyleWidget )
403+
{
404+
mRasterStyleWidget->deleteLater();
405+
delete mRasterStyleWidget;
406+
}
407+
mRasterStyleWidget = new QgsRendererRasterPropertiesWidget( rlayer, mMapCanvas, mWidgetStack );
408+
mRasterStyleWidget->syncToLayer( rlayer );
409+
connect( mRasterStyleWidget, SIGNAL( widgetChanged() ), this, SLOT( autoApply() ) );
410+
411+
QgsRasterHistogramWidget* widget = new QgsRasterHistogramWidget( rlayer, mWidgetStack );
412+
connect( widget, SIGNAL( widgetChanged() ), this, SLOT( autoApply() ) );
413+
QString name = mRasterStyleWidget->currentRenderWidget()->renderer()->type();
414+
widget->setRendererWidget( name, mRasterStyleWidget->currentRenderWidget() );
415+
416+
mWidgetStack->addMainPanel( widget );
399417
}
400-
mRasterStyleWidget = new QgsRendererRasterPropertiesWidget( rlayer, mMapCanvas, mWidgetStack );
401-
mRasterStyleWidget->syncToLayer( rlayer );
402-
connect( mRasterStyleWidget, SIGNAL( widgetChanged() ), this, SLOT( autoApply() ) );
403-
404-
QgsRasterHistogramWidget* widget = new QgsRasterHistogramWidget( rlayer, mWidgetStack );
405-
connect( widget, SIGNAL( widgetChanged() ), this, SLOT( autoApply() ) );
406-
QString name = mRasterStyleWidget->currentRenderWidget()->renderer()->type();
407-
widget->setRendererWidget( name, mRasterStyleWidget->currentRenderWidget() );
408-
409-
mWidgetStack->addMainPanel( widget );
410418
break;
411419
}
412420
default:

src/core/qgsdataitemproviderregistry.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
#include "qgslogger.h"
2222
#include "qgsproviderregistry.h"
2323

24+
typedef QList<QgsDataItemProvider*> dataItemProviders_t();
25+
2426

2527
/**
2628
* \ingroup core
@@ -64,6 +66,17 @@ QgsDataItemProviderRegistry::QgsDataItemProviderRegistry()
6466
if ( !library )
6567
continue;
6668

69+
// new / better way of returning data items from providers
70+
71+
dataItemProviders_t* dataItemProvidersFn = reinterpret_cast< dataItemProviders_t * >( cast_to_fptr( library->resolve( "dataItemProviders" ) ) );
72+
if ( dataItemProvidersFn )
73+
{
74+
// the function is a factory - we keep ownership of the returned providers
75+
mProviders << dataItemProvidersFn();
76+
}
77+
78+
// legacy support - using dataItem() and dataCapabilities() methods
79+
6780
dataCapabilities_t * dataCapabilities = reinterpret_cast< dataCapabilities_t * >( cast_to_fptr( library->resolve( "dataCapabilities" ) ) );
6881
if ( !dataCapabilities )
6982
{

src/core/qgsmaplayer.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,9 @@ bool QgsMapLayer::readLayerXml( const QDomElement& layerElement )
234234
// This is modified version of old QgsWmsProvider::parseUri
235235
// The new format has always params crs,format,layers,styles and that params
236236
// should not appear in old format url -> use them to identify version
237-
if ( !mDataSource.contains( "crs=" ) && !mDataSource.contains( "format=" ) )
237+
// XYZ tile layers do not need to contain crs,format params, but they have type=xyz
238+
if ( !mDataSource.contains( "type=" ) &&
239+
!mDataSource.contains( "crs=" ) && !mDataSource.contains( "format=" ) )
238240
{
239241
QgsDebugMsg( "Old WMS URI format detected -> converting to new format" );
240242
QgsDataSourceUri uri;

src/core/qgsmaprenderercustompainterjob.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,12 @@ bool QgsMapRendererJob::needTemporaryImage( QgsMapLayer* ml )
333333
return true;
334334
}
335335
}
336+
else if ( ml->type() == QgsMapLayer::RasterLayer )
337+
{
338+
// preview of intermediate raster rendering results requires a temporary output image
339+
if ( mSettings.testFlag( QgsMapSettings::RenderPartialOutput ) )
340+
return true;
341+
}
336342

337343
return false;
338344
}

src/core/qgsmapsettings.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ class CORE_EXPORT QgsMapSettings
164164
UseRenderingOptimization = 0x20, //!< Enable vector simplification and other rendering optimizations
165165
DrawSelection = 0x40, //!< Whether vector selections should be shown in the rendered map
166166
DrawSymbolBounds = 0x80, //!< Draw bounds of symbols (for debugging/testing)
167-
RenderMapTile = 0x100 //!< Draw map such that there are no problems between adjacent tiles
167+
RenderMapTile = 0x100, //!< Draw map such that there are no problems between adjacent tiles
168+
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
168169
// TODO: ignore scale-based visibility (overview)
169170
};
170171
Q_DECLARE_FLAGS( Flags, Flag )

src/core/qgsrendercontext.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ QgsRenderContext QgsRenderContext::fromMapSettings( const QgsMapSettings& mapSet
129129
ctx.setFlag( DrawSymbolBounds, mapSettings.testFlag( QgsMapSettings::DrawSymbolBounds ) );
130130
ctx.setFlag( RenderMapTile, mapSettings.testFlag( QgsMapSettings::RenderMapTile ) );
131131
ctx.setFlag( Antialiasing, mapSettings.testFlag( QgsMapSettings::Antialiasing ) );
132+
ctx.setFlag( RenderPartialOutput, mapSettings.testFlag( QgsMapSettings::RenderPartialOutput ) );
132133
ctx.setRasterScaleFactor( 1.0 );
133134
ctx.setScaleFactor( mapSettings.outputDpi() / 25.4 ); // = pixels per mm
134135
ctx.setRendererScale( mapSettings.scale() );

src/core/qgsrendercontext.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class CORE_EXPORT QgsRenderContext
6565
DrawSymbolBounds = 0x20, //!< Draw bounds of symbols (for debugging/testing)
6666
RenderMapTile = 0x40, //!< Draw map such that there are no problems between adjacent tiles
6767
Antialiasing = 0x80, //!< Use antialiasing while drawing
68+
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
6869
};
6970
Q_DECLARE_FLAGS( Flags, Flag )
7071

src/core/raster/qgsrasterdrawer.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,20 @@ void QgsRasterDrawer::draw( QPainter* p, QgsRasterViewPort* viewPort, const QgsM
8888
}
8989
}
9090

91+
if ( feedback && feedback->renderPartialOutput() )
92+
{
93+
// there could have been partial preview written before
94+
// so overwrite anything with the resulting image.
95+
// (we are guaranteed to have a temporary image for this layer, see QgsMapRendererJob::needTemporaryImage)
96+
p->setCompositionMode( QPainter::CompositionMode_Source );
97+
}
98+
9199
drawImage( p, viewPort, img, topLeftCol, topLeftRow, theQgsMapToPixel );
92100

93101
delete block;
94102

103+
p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // go back to the default composition mode
104+
95105
// ok this does not matter much anyway as the tile size quite big so most of the time
96106
// there would be just one tile for the whole display area, but it won't hurt...
97107
if ( feedback && feedback->isCancelled() )

src/core/raster/qgsrasterinterface.h

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,36 @@
3636
*/
3737
class CORE_EXPORT QgsRasterBlockFeedback : public QgsFeedback
3838
{
39-
// TODO: extend with preview functionality??
39+
public:
40+
//! Construct a new raster block feedback object
41+
QgsRasterBlockFeedback( QObject* parent = nullptr ) : QgsFeedback( parent ), mPreviewOnly( false ), mRenderPartialOutput( false ) {}
42+
43+
//! May be emitted by raster data provider to indicate that some partial data are available
44+
//! and a new preview image may be produced
45+
virtual void onNewData() {}
46+
47+
//! Whether the raster provider should return only data that are already available
48+
//! without waiting for full result. By default this flag is not enabled.
49+
//! @see setPreviewOnly()
50+
bool isPreviewOnly() const { return mPreviewOnly; }
51+
//! set flag whether the block request is for preview purposes only
52+
//! @see isPreviewOnly()
53+
void setPreviewOnly( bool preview ) { mPreviewOnly = preview; }
54+
55+
//! Whether our painter is drawing to a temporary image used just by this layer
56+
//! @see setRenderPartialOutput()
57+
bool renderPartialOutput() const { return mRenderPartialOutput; }
58+
//! Set whether our painter is drawing to a temporary image used just by this layer
59+
//! @see renderPartialOutput()
60+
void setRenderPartialOutput( bool enable ) { mRenderPartialOutput = enable; }
61+
62+
private:
63+
//! Whether the raster provider should return only data that are already available
64+
//! without waiting for full result
65+
bool mPreviewOnly;
66+
67+
//! Whether our painter is drawing to a temporary image used just by this layer
68+
bool mRenderPartialOutput;
4069
};
4170

4271

@@ -258,6 +287,9 @@ class CORE_EXPORT QgsRasterInterface
258287
int theStats = QgsRasterBandStats::All,
259288
const QgsRectangle & theExtent = QgsRectangle(),
260289
int theBinCount = 0 );
290+
291+
private:
292+
Q_DISABLE_COPY( QgsRasterInterface ) // there is clone() for copying
261293
};
262294

263295
#endif

src/core/raster/qgsrasterlayerrenderer.cpp

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer* layer, QgsRender
2929
, mRasterViewPort( nullptr )
3030
, mPipe( nullptr )
3131
, mContext( rendererContext )
32-
, mFeedback( new QgsRasterBlockFeedback() )
32+
, mFeedback( new Feedback( this ) )
3333
{
3434
mPainter = rendererContext.painter();
3535
const QgsMapToPixel& theQgsMapToPixel = rendererContext.mapToPixel();
@@ -226,3 +226,35 @@ QgsFeedback* QgsRasterLayerRenderer::feedback() const
226226
{
227227
return mFeedback;
228228
}
229+
230+
QgsRasterLayerRenderer::Feedback::Feedback( QgsRasterLayerRenderer *r )
231+
: mR( r )
232+
, mMinimalPreviewInterval( 250 )
233+
{
234+
setRenderPartialOutput( r->mContext.testFlag( QgsRenderContext::RenderPartialOutput ) );
235+
}
236+
237+
void QgsRasterLayerRenderer::Feedback::onNewData()
238+
{
239+
if ( !renderPartialOutput() )
240+
return; // we were not asked for partial renders and we may not have a temporary image for overwriting...
241+
242+
// update only once upon a time
243+
// (preview itself takes some time)
244+
if ( mLastPreview.isValid() && mLastPreview.msecsTo( QTime::currentTime() ) < mMinimalPreviewInterval )
245+
return;
246+
247+
// TODO: update only the area that got new data
248+
249+
QgsDebugMsg( QString( "new raster preview! %1" ).arg( mLastPreview.msecsTo( QTime::currentTime() ) ) );
250+
QTime t;
251+
t.start();
252+
QgsRasterBlockFeedback feedback;
253+
feedback.setPreviewOnly( true );
254+
feedback.setRenderPartialOutput( true );
255+
QgsRasterIterator iterator( mR->mPipe->last() );
256+
QgsRasterDrawer drawer( &iterator );
257+
drawer.draw( mR->mPainter, mR->mRasterViewPort, mR->mMapToPixel, &feedback );
258+
QgsDebugMsg( QString( "total raster preview time: %1 ms" ).arg( t.elapsed() ) );
259+
mLastPreview = QTime::currentTime();
260+
}

0 commit comments

Comments
 (0)