QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
qgsmaprenderercustompainterjob.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsmaprenderercustompainterjob.cpp
3 --------------------------------------
4 Date : December 2013
5 Copyright : (C) 2013 by Martin Dobias
6 Email : wonder dot sk at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
17
18#include "qgsfeedback.h"
19#include "qgslabelingengine.h"
20#include "qgslogger.h"
21#include "qgsmaplayerrenderer.h"
23#include "qgselevationmap.h"
24
25#include <QtConcurrentRun>
26
27Q_GUI_EXPORT extern int qt_defaultDpiX();
28Q_GUI_EXPORT extern int qt_defaultDpiY();
29
30static void _fixQPictureDPI( QPainter *p )
31{
32 // QPicture makes an assumption that we drawing to it with system DPI.
33 // Then when being drawn, it scales the painter. The following call
34 // negates the effect. There is no way of setting QPicture's DPI.
35 // See QTBUG-20361
36 p->scale( static_cast< double >( qt_defaultDpiX() ) / p->device()->logicalDpiX(),
37 static_cast< double >( qt_defaultDpiY() ) / p->device()->logicalDpiY() );
38}
39
40//
41// QgsMapRendererAbstractCustomPainterJob
42//
43
45 : QgsMapRendererJob( settings )
46{
47
48}
49
50void QgsMapRendererAbstractCustomPainterJob::preparePainter( QPainter *painter, const QColor &backgroundColor )
51{
52 // clear the background
53 painter->fillRect( 0, 0, mSettings.deviceOutputSize().width(), mSettings.deviceOutputSize().height(), backgroundColor );
54
55 painter->setRenderHint( QPainter::Antialiasing, mSettings.testFlag( Qgis::MapSettingsFlag::Antialiasing ) );
56 painter->setRenderHint( QPainter::SmoothPixmapTransform, mSettings.testFlag( Qgis::MapSettingsFlag::HighQualityImageTransforms ) );
57 painter->setRenderHint( QPainter::LosslessImageRendering, mSettings.testFlag( Qgis::MapSettingsFlag::LosslessImageRendering ) );
58
59#ifndef QT_NO_DEBUG
60 QPaintDevice *paintDevice = painter->device();
61 const QString errMsg = QStringLiteral( "pre-set DPI not equal to painter's DPI (%1 vs %2)" )
62 .arg( paintDevice->logicalDpiX() )
63 .arg( mSettings.outputDpi() );
64 Q_ASSERT_X( qgsDoubleNear( paintDevice->logicalDpiX(), mSettings.outputDpi(), 1.0 ),
65 "Job::startRender()", errMsg.toLatin1().data() );
66#endif
67}
68
69
70//
71// QgsMapRendererCustomPainterJob
72//
73
76 , mPainter( painter )
77 , mActive( false )
78 , mRenderSynchronously( false )
79{
80 QgsDebugMsgLevel( QStringLiteral( "QPAINTER construct" ), 5 );
81}
82
84{
85 QgsDebugMsgLevel( QStringLiteral( "QPAINTER destruct" ), 5 );
86 Q_ASSERT( !mFutureWatcher.isRunning() );
87 //cancel();
88}
89
90void QgsMapRendererCustomPainterJob::startPrivate()
91{
92 if ( isActive() )
93 return;
94
95 if ( !mPrepareOnly )
96 mRenderingStart.start();
97
98 mActive = true;
99
100 mErrors.clear();
101
102 QgsDebugMsgLevel( QStringLiteral( "QPAINTER run!" ), 5 );
103
104 QgsDebugMsgLevel( QStringLiteral( "Preparing list of layer jobs for rendering" ), 5 );
105 QElapsedTimer prepareTime;
106 prepareTime.start();
107
109
110 mLabelingEngineV2.reset();
111
113 {
114 mLabelingEngineV2.reset( new QgsDefaultLabelingEngine() );
115 mLabelingEngineV2->setMapSettings( mSettings );
116 }
117
118 const bool canUseLabelCache = prepareLabelCache();
119 mLayerJobs = prepareJobs( mPainter, mLabelingEngineV2.get() );
120 mLabelJob = prepareLabelingJob( mPainter, mLabelingEngineV2.get(), canUseLabelCache );
121 mSecondPassLayerJobs = prepareSecondPassJobs( mLayerJobs, mLabelJob );
122
123 QgsDebugMsgLevel( QStringLiteral( "Rendering prepared in (seconds): %1" ).arg( prepareTime.elapsed() / 1000.0 ), 4 );
124
125 if ( mRenderSynchronously )
126 {
127 if ( !mPrepareOnly )
128 {
129 // do the rendering right now!
130 doRender();
131 }
132 return;
133 }
134
135 // now we are ready to start rendering!
136 connect( &mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererCustomPainterJob::futureFinished );
137
138 mFuture = QtConcurrent::run( staticRender, this );
139 mFutureWatcher.setFuture( mFuture );
140}
141
142
144{
145 if ( !isActive() )
146 {
147 QgsDebugMsgLevel( QStringLiteral( "QPAINTER not running!" ), 4 );
148 return;
149 }
150
151 QgsDebugMsgLevel( QStringLiteral( "QPAINTER canceling" ), 5 );
152 disconnect( &mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererCustomPainterJob::futureFinished );
154
155 QElapsedTimer t;
156 t.start();
157
158 mFutureWatcher.waitForFinished();
159
160 QgsDebugMsgLevel( QStringLiteral( "QPAINER cancel waited %1 ms" ).arg( t.elapsed() / 1000.0 ), 5 );
161
162 futureFinished();
163
164 QgsDebugMsgLevel( QStringLiteral( "QPAINTER canceled" ), 5 );
165}
166
168{
169 if ( !isActive() )
170 {
171 QgsDebugError( QStringLiteral( "QPAINTER not running!" ) );
172 return;
173 }
174
175 mLabelJob.context.setRenderingStopped( true );
176 for ( LayerRenderJob &job : mLayerJobs )
177 {
178 job.context()->setRenderingStopped( true );
179 if ( job.renderer && job.renderer->feedback() )
180 job.renderer->feedback()->cancel();
181 }
182}
183
185{
186 if ( !isActive() )
187 return;
188
189 disconnect( &mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererCustomPainterJob::futureFinished );
190
191 QElapsedTimer t;
192 t.start();
193
194 mFutureWatcher.waitForFinished();
195
196 QgsDebugMsgLevel( QStringLiteral( "waitForFinished: %1 ms" ).arg( t.elapsed() / 1000.0 ), 4 );
197
198 futureFinished();
199}
200
202{
203 return mActive;
204}
205
207{
208 return mLabelJob.cached;
209}
210
212{
213 if ( mLabelingEngineV2 )
214 return mLabelingEngineV2->takeResults();
215 else
216 return nullptr;
217}
218
219
220void QgsMapRendererCustomPainterJob::waitForFinishedWithEventLoop( QEventLoop::ProcessEventsFlags flags )
221{
222 QEventLoop loop;
223 connect( &mFutureWatcher, &QFutureWatcher<void>::finished, &loop, &QEventLoop::quit );
224 loop.exec( flags );
225}
226
227
229{
230 mRenderSynchronously = true;
231 start();
232 futureFinished();
233 mRenderSynchronously = false;
234}
235
237{
238 mRenderSynchronously = true;
239 mPrepareOnly = true;
240 start();
241 mPrepared = true;
242}
243
245{
246 if ( !mPrepared )
247 return;
248
249 doRender();
250 futureFinished();
251 mRenderSynchronously = false;
252 mPrepareOnly = false;
253 mPrepared = false;
254}
255
256void QgsMapRendererCustomPainterJob::futureFinished()
257{
258 mActive = false;
259 if ( !mPrepared ) // can't access from other thread
261 QgsDebugMsgLevel( QStringLiteral( "QPAINTER futureFinished" ), 5 );
262
263 if ( !mPrepared )
264 logRenderingTime( mLayerJobs, {}, mLabelJob );
265
266 // final cleanup
267 cleanupJobs( mLayerJobs );
268 cleanupSecondPassJobs( mSecondPassLayerJobs );
269 cleanupLabelJob( mLabelJob );
270
271 emit finished();
272}
273
274
275void QgsMapRendererCustomPainterJob::staticRender( QgsMapRendererCustomPainterJob *self )
276{
277 try
278 {
279 self->doRender();
280 }
281 catch ( QgsException &e )
282 {
283 Q_UNUSED( e )
284 QgsDebugError( "Caught unhandled QgsException: " + e.what() );
285 }
286 catch ( std::exception &e )
287 {
288 Q_UNUSED( e )
289 QgsDebugError( "Caught unhandled std::exception: " + QString::fromLatin1( e.what() ) );
290 }
291 catch ( ... )
292 {
293 QgsDebugError( QStringLiteral( "Caught unhandled unknown exception" ) );
294 }
295}
296
297void QgsMapRendererCustomPainterJob::doRender()
298{
299 const bool hasSecondPass = ! mSecondPassLayerJobs.empty();
300 QgsDebugMsgLevel( QStringLiteral( "Starting to render layer stack." ), 5 );
301 QElapsedTimer renderTime;
302 renderTime.start();
303
305 std::unique_ptr<QgsElevationMap> mainElevationMap;
306 if ( mapShadingRenderer.isActive() )
307 mainElevationMap.reset( new QgsElevationMap( mSettings.deviceOutputSize(), mSettings.devicePixelRatio() ) );
308
309 for ( LayerRenderJob &job : mLayerJobs )
310 {
311 if ( job.context()->renderingStopped() )
312 break;
313
314 emit layerRenderingStarted( job.layerId );
315
316 if ( ! hasSecondPass && job.context()->useAdvancedEffects() )
317 {
318 // Set the QPainter composition mode so that this layer is rendered using
319 // the desired blending mode
320 mPainter->setCompositionMode( job.blendMode );
321 }
322
323 if ( !job.cached )
324 {
325 QElapsedTimer layerTime;
326 layerTime.start();
327
328 if ( job.previewRenderImage && !job.previewRenderImageInitialized )
329 {
330 job.previewRenderImage->fill( 0 );
331 job.previewRenderImageInitialized = true;
332 }
333
334 if ( job.img )
335 {
336 job.img->fill( 0 );
337 job.imageInitialized = true;
338 }
339
340 job.completed = job.renderer->render();
341
342 if ( job.picture )
343 {
344 job.renderer->renderContext()->painter()->end();
345 }
346
347 job.renderingTime += layerTime.elapsed();
348 }
349
350 if ( ! hasSecondPass && job.img )
351 {
352 // If we flattened this layer for alternate blend modes, composite it now
353 mPainter->setOpacity( job.opacity );
354 mPainter->drawImage( 0, 0, *job.img );
355 mPainter->setOpacity( 1.0 );
356 }
357
358 if ( mainElevationMap && job.context()->elevationMap() )
359 {
360 const QgsElevationMap &layerElevationMap = *job.context()->elevationMap();
361 if ( layerElevationMap.isValid() )
362 mainElevationMap->combine( layerElevationMap, mapShadingRenderer.combinedElevationMethod() );
363 }
364
365 emit layerRendered( job.layerId );
366 }
367
369 QgsDebugMsgLevel( QStringLiteral( "Done rendering map layers" ), 5 );
370
371 if ( mapShadingRenderer.isActive() && mainElevationMap )
372 {
373 QImage image( mainElevationMap->rawElevationImage().size(), QImage::Format_RGB32 );
374 image.setDevicePixelRatio( mSettings.devicePixelRatio() );
375 image.fill( Qt::white );
376 mapShadingRenderer.renderShading( *mainElevationMap.get(), image, QgsRenderContext::fromMapSettings( mSettings ) );
377 mPainter->save();
378 mPainter->setCompositionMode( QPainter::CompositionMode_Multiply );
379 mPainter->drawImage( 0, 0, image );
380 mPainter->restore();
381 }
382
383 if ( mSettings.testFlag( Qgis::MapSettingsFlag::DrawLabeling ) && !mLabelJob.context.renderingStopped() )
384 {
385 if ( !mLabelJob.cached )
386 {
387 QElapsedTimer labelTime;
388 labelTime.start();
389
390 if ( mLabelJob.img )
391 {
392 QPainter painter;
393 mLabelJob.img->fill( 0 );
394 painter.begin( mLabelJob.img );
395 mLabelJob.context.setPainter( &painter );
396 drawLabeling( mLabelJob.context, mLabelingEngineV2.get(), &painter );
397 painter.end();
398 }
399 else if ( mLabelJob.picture )
400 {
401 QPainter painter;
402 painter.begin( mLabelJob.picture.get() );
403 mLabelJob.context.setPainter( &painter );
404 drawLabeling( mLabelJob.context, mLabelingEngineV2.get(), &painter );
405 painter.end();
406 }
407 else
408 {
409 drawLabeling( mLabelJob.context, mLabelingEngineV2.get(), mPainter );
410 }
411
412 mLabelJob.complete = true;
413 mLabelJob.renderingTime = labelTime.elapsed();
414 mLabelJob.participatingLayers = _qgis_listRawToQPointer( mLabelingEngineV2->participatingLayers() );
415 }
416 }
417
418 if ( ! hasSecondPass )
419 {
420 if ( mLabelJob.img && mLabelJob.complete )
421 {
422 mPainter->setCompositionMode( QPainter::CompositionMode_SourceOver );
423 mPainter->setOpacity( 1.0 );
424 mPainter->drawImage( 0, 0, *mLabelJob.img );
425 }
426 }
427 else
428 {
429 initSecondPassJobs( mSecondPassLayerJobs, mLabelJob );
430
431 for ( LayerRenderJob &job : mSecondPassLayerJobs )
432 {
433 if ( job.context()->renderingStopped() )
434 break;
435
436 if ( !job.cached )
437 {
438 QElapsedTimer layerTime;
439 layerTime.start();
440
441 if ( job.previewRenderImage && !job.previewRenderImageInitialized )
442 {
443 job.previewRenderImage->fill( 0 );
444 job.previewRenderImageInitialized = true;
445 }
446
447 if ( job.img )
448 {
449 job.img->fill( 0 );
450 job.imageInitialized = true;
451 }
452
453 job.completed = job.renderer->render();
454
455 if ( job.picture )
456 {
457 job.renderer->renderContext()->painter()->end();
458 }
459
460 job.renderingTime += layerTime.elapsed();
461 }
462 }
463
465 composeSecondPass( mSecondPassLayerJobs, mLabelJob, forceVector );
466
467 if ( !forceVector )
468 {
469 const QImage finalImage = composeImage( mSettings, mLayerJobs, mLabelJob );
470
471 mPainter->setCompositionMode( QPainter::CompositionMode_SourceOver );
472 mPainter->setOpacity( 1.0 );
473 mPainter->drawImage( 0, 0, finalImage );
474 }
475 else
476 {
477 //Vector composition is simply draw the saved picture on the painter
478 for ( LayerRenderJob &job : mLayerJobs )
479 {
480 // if there is vector rendering we use it, else we use the raster rendering
481 if ( job.picture )
482 {
483 mPainter->save();
484 _fixQPictureDPI( mPainter );
485 mPainter->drawPicture( 0, 0, *job.picture );
486 mPainter->restore();
487 }
488 else
489 mPainter->drawImage( 0, 0, *job.img );
490 }
491
492 if ( mLabelJob.picture )
493 {
494 mPainter->save();
495 _fixQPictureDPI( mPainter );
496 mPainter->drawPicture( 0, 0, *mLabelJob.picture );
497 mPainter->restore();
498 }
499 }
500 }
501
502 QgsDebugMsgLevel( QStringLiteral( "Rendering completed in (seconds): %1" ).arg( renderTime.elapsed() / 1000.0 ), 2 );
503}
@ ForceVectorOutput
Vector graphics should not be cached and drawn as raster images.
@ ForceRasterMasks
Force symbol masking to be applied using a raster method. This is considerably faster when compared t...
@ LosslessImageRendering
Render images losslessly whenever possible, instead of the default lossy jpeg rendering used for some...
@ Antialiasing
Enable anti-aliasing for map rendering.
@ DrawLabeling
Enable drawing of labels on top of the map.
@ HighQualityImageTransforms
Enable high quality image transformations, which results in better appearance of scaled or rotated ra...
Default QgsLabelingEngine implementation, which completes the whole labeling operation (including lab...
Stores digital elevation model in a raster image which may get updated as a part of map layer renderi...
void combine(const QgsElevationMap &otherElevationMap, Qgis::ElevationMapCombineMethod method)
Combines this elevation map with otherElevationMap.
bool isValid() const
Returns whether the elevation map is valid.
This class can render elevation shading on an image with different methods (eye dome lighting,...
Qgis::ElevationMapCombineMethod combinedElevationMethod() const
Returns the method used when conbining different elevation sources.
bool isActive() const
Returns whether this shading renderer is active.
void renderShading(const QgsElevationMap &elevation, QImage &image, const QgsRenderContext &context) const
Render shading on image condidering the elevation map elevation and the renderer context context If e...
Defines a QGIS exception class.
Definition: qgsexception.h:35
QString what() const
Definition: qgsexception.h:49
Class that stores computed placement from labeling engine.
Abstract base class for map renderer jobs which use custom painters.
void preparePainter(QPainter *painter, const QColor &backgroundColor=Qt::transparent)
Prepares the given painter ready for a map render.
QgsMapRendererAbstractCustomPainterJob(const QgsMapSettings &settings)
Constructor for QgsMapRendererAbstractCustomPainterJob, using the given map settings.
Job implementation that renders everything sequentially using a custom painter.
QgsMapRendererCustomPainterJob(const QgsMapSettings &settings, QPainter *painter)
void renderSynchronously()
Render the map synchronously in this thread.
void cancel() override
Stop the rendering job - does not return until the job has terminated.
void waitForFinishedWithEventLoop(QEventLoop::ProcessEventsFlags flags=QEventLoop::AllEvents)
Wait for the job to be finished - and keep the thread's event loop running while waiting.
void renderPrepared()
Render a pre-prepared job.
QgsLabelingResults * takeLabelingResults() override
Gets pointer to internal labeling engine (in order to get access to the results).
void cancelWithoutBlocking() override
Triggers cancellation of the rendering job without blocking.
void prepare()
Prepares the job for rendering synchronously in a background thread.
void waitForFinished() override
Block until the job has finished.
bool isActive() const override
Tell whether the rendering job is currently running in background.
bool usedCachedLabels() const override
Returns true if the render job was able to use a cached labeling solution.
Abstract base class for map rendering implementations.
void logRenderingTime(const std::vector< LayerRenderJob > &jobs, const std::vector< LayerRenderJob > &secondPassJobs, const LabelRenderJob &labelJob)
static QImage composeImage(const QgsMapSettings &settings, const std::vector< LayerRenderJob > &jobs, const LabelRenderJob &labelJob, const QgsMapRendererCache *cache=nullptr)
void cleanupSecondPassJobs(std::vector< LayerRenderJob > &jobs)
void initSecondPassJobs(std::vector< LayerRenderJob > &secondPassJobs, LabelRenderJob &labelJob) const
Initialize secondPassJobs according to what have been rendered (mask clipping path e....
static Q_DECL_DEPRECATED void drawLabeling(const QgsMapSettings &settings, QgsRenderContext &renderContext, QgsLabelingEngine *labelingEngine2, QPainter *painter)
void layerRendered(const QString &layerId)
Emitted when a layer has completed rendering.
std::vector< LayerRenderJob > prepareJobs(QPainter *painter, QgsLabelingEngine *labelingEngine2, bool deferredPainterSet=false)
Creates a list of layer rendering jobs and prepares them for later render.
void renderingLayersFinished()
Emitted when the layers are rendered.
void cleanupJobs(std::vector< LayerRenderJob > &jobs)
QElapsedTimer mRenderingStart
void finished()
emitted when asynchronous rendering is finished (or canceled).
QgsMapSettings mSettings
void start()
Start the rendering job and immediately return.
void layerRenderingStarted(const QString &layerId)
Emitted just before rendering starts for a particular layer.
static void composeSecondPass(std::vector< LayerRenderJob > &secondPassJobs, LabelRenderJob &labelJob, bool forceVector=false)
Compose second pass images into first pass images.
std::vector< LayerRenderJob > prepareSecondPassJobs(std::vector< LayerRenderJob > &firstPassJobs, LabelRenderJob &labelJob)
Prepares jobs for a second pass, if selective masks exist (from labels or symbol layers).
LabelRenderJob prepareLabelingJob(QPainter *painter, QgsLabelingEngine *labelingEngine2, bool canUseLabelCache=true)
Prepares a labeling job.
void cleanupLabelJob(LabelRenderJob &job)
Handles clean up tasks for a label job, including deletion of images and storing cached label results...
bool prepareLabelCache() const
Prepares the cache for storing the result of labeling.
The QgsMapSettings class contains configuration for rendering of the map.
QSize deviceOutputSize() const
Returns the device output size of the map render.
QColor backgroundColor() const
Returns the background color of the map.
float devicePixelRatio() const
Returns the device pixel ratio.
const QgsElevationShadingRenderer & elevationShadingRenderer() const
Returns the shading renderer used to render shading on the entire map.
double outputDpi() const
Returns the DPI (dots per inch) used for conversion between real world units (e.g.
bool testFlag(Qgis::MapSettingsFlag flag) const
Check whether a particular flag is enabled.
static QgsRenderContext fromMapSettings(const QgsMapSettings &mapSettings)
create initialized QgsRenderContext instance from given QgsMapSettings
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:5207
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugError(str)
Definition: qgslogger.h:38
Q_GUI_EXPORT int qt_defaultDpiX()
Q_GUI_EXPORT int qt_defaultDpiY()