QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
qgsmaprendererparalleljob.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsmaprendererparalleljob.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"
22#include "qgsproject.h"
23#include "qgsmaplayer.h"
25
26#include <QtConcurrentMap>
27#include <QtConcurrentRun>
28
30 : QgsMapRendererQImageJob( settings )
31 , mStatus( Idle )
32{
34 {
35 QgsLogger::warning( QStringLiteral( "Vector rendering in parallel job is not supported, so Qgis::MapSettingsFlag::ForceVectorOutput option will be ignored!" ) );
37 }
38}
39
41{
42 if ( isActive() )
43 {
44 cancel();
45 }
46}
47
48void QgsMapRendererParallelJob::startPrivate()
49{
50 if ( isActive() )
51 return;
52
53 mRenderingStart.start();
54
55 mStatus = RenderingLayers;
56
57 mLabelingEngineV2.reset();
58
60 {
61 mLabelingEngineV2.reset( new QgsDefaultLabelingEngine() );
62 mLabelingEngineV2->setMapSettings( mSettings );
63 }
64
65 const bool canUseLabelCache = prepareLabelCache();
66 mLayerJobs = prepareJobs( nullptr, mLabelingEngineV2.get() );
67 mLabelJob = prepareLabelingJob( nullptr, mLabelingEngineV2.get(), canUseLabelCache );
68 mSecondPassLayerJobs = prepareSecondPassJobs( mLayerJobs, mLabelJob );
69
70 QgsDebugMsgLevel( QStringLiteral( "QThreadPool max thread count is %1" ).arg( QThreadPool::globalInstance()->maxThreadCount() ), 2 );
71
72 // start async job
73
74 connect( &mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererParallelJob::renderLayersFinished );
75
76 mFuture = QtConcurrent::map( mLayerJobs, renderLayerStatic );
77 mFutureWatcher.setFuture( mFuture );
78}
79
81{
82 if ( !isActive() )
83 return;
84
85 QgsDebugMsgLevel( QStringLiteral( "PARALLEL cancel at status %1" ).arg( mStatus ), 2 );
86
87 mLabelJob.context.setRenderingStopped( true );
88 for ( LayerRenderJob &job : mLayerJobs )
89 {
90 job.context()->setRenderingStopped( true );
91 if ( job.renderer && job.renderer->feedback() )
92 job.renderer->feedback()->cancel();
93 }
94
95 if ( mStatus == RenderingLayers )
96 {
97 disconnect( &mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererParallelJob::renderLayersFinished );
98
99 mFutureWatcher.waitForFinished();
100
101 renderLayersFinished();
102 }
103
104 if ( mStatus == RenderingLabels )
105 {
106 disconnect( &mLabelingFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererParallelJob::renderingFinished );
107
108 mLabelingFutureWatcher.waitForFinished();
109
110 renderingFinished();
111 }
112
113 if ( mStatus == RenderingSecondPass )
114 {
115 disconnect( &mSecondPassFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererParallelJob::renderLayersSecondPassFinished );
116
117 mSecondPassFutureWatcher.waitForFinished();
118
119 renderLayersSecondPassFinished();
120 }
121
122 Q_ASSERT( mStatus == Idle );
123}
124
126{
127 if ( !isActive() )
128 return;
129
130 QgsDebugMsgLevel( QStringLiteral( "PARALLEL cancel at status %1" ).arg( mStatus ), 2 );
131
132 mLabelJob.context.setRenderingStopped( true );
133 for ( LayerRenderJob &job : mLayerJobs )
134 {
135 job.context()->setRenderingStopped( true );
136 if ( job.renderer && job.renderer->feedback() )
137 job.renderer->feedback()->cancel();
138 }
139
140 if ( mStatus == RenderingLayers )
141 {
142 disconnect( &mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererParallelJob::renderLayersFinished );
143 connect( &mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererParallelJob::renderingFinished );
144 }
145}
146
148{
149 if ( !isActive() )
150 return;
151
152 if ( mStatus == RenderingLayers )
153 {
154 disconnect( &mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererParallelJob::renderLayersFinished );
155
156 QElapsedTimer t;
157 t.start();
158
159 mFutureWatcher.waitForFinished();
160
161 QgsDebugMsgLevel( QStringLiteral( "waitForFinished (1): %1 ms" ).arg( t.elapsed() / 1000.0 ), 2 );
162
163 renderLayersFinished();
164 }
165
166 if ( mStatus == RenderingLabels )
167 {
168 disconnect( &mLabelingFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererParallelJob::renderingFinished );
169
170 QElapsedTimer t;
171 t.start();
172
173 mLabelingFutureWatcher.waitForFinished();
174
175 QgsDebugMsgLevel( QStringLiteral( "waitForFinished (2): %1 ms" ).arg( t.elapsed() / 1000.0 ), 2 );
176
177 renderingFinished();
178 }
179
180 if ( mStatus == RenderingSecondPass )
181 {
182 disconnect( &mSecondPassFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererParallelJob::renderLayersSecondPassFinished );
183
184 QElapsedTimer t;
185 t.start();
186
187 mSecondPassFutureWatcher.waitForFinished();
188
189 QgsDebugMsgLevel( QStringLiteral( "waitForFinished (1): %1 ms" ).arg( t.elapsed() / 1000.0 ), 2 );
190
191 renderLayersSecondPassFinished();
192 }
193
194 Q_ASSERT( mStatus == Idle );
195}
196
198{
199 return mStatus != Idle;
200}
201
203{
204 return mLabelJob.cached;
205}
206
208{
209 if ( mLabelingEngineV2 )
210 return mLabelingEngineV2->takeResults();
211 else
212 return nullptr;
213}
214
216{
217 // if status == Idle we are either waiting for the render to start, OR have finished the render completely.
218 // We can differentiate between those states by checking whether mFinalImage is null -- at the "waiting for
219 // render to start" state mFinalImage has not yet been created.
220 const bool jobIsComplete = mStatus == Idle && !mFinalImage.isNull();
221
222 if ( !jobIsComplete )
223 return composeImage( mSettings, mLayerJobs, mLabelJob, mCache );
224 else
225 return mFinalImage; // when rendering labels or idle
226}
227
228void QgsMapRendererParallelJob::renderLayersFinished()
229{
230 Q_ASSERT( mStatus == RenderingLayers );
231
232 for ( const LayerRenderJob &job : mLayerJobs )
233 {
234 if ( !job.errors.isEmpty() )
235 {
236 mErrors.append( Error( job.layerId, job.errors.join( ',' ) ) );
237 }
238 }
239
240 // compose final image for labeling
241 if ( mSecondPassLayerJobs.empty() )
242 {
243 mFinalImage = composeImage( mSettings, mLayerJobs, mLabelJob, mCache );
244 }
245
246 QgsDebugMsgLevel( QStringLiteral( "PARALLEL layers finished" ), 2 );
247
248 if ( mSettings.testFlag( Qgis::MapSettingsFlag::DrawLabeling ) && !mLabelJob.context.renderingStopped() )
249 {
250 mStatus = RenderingLabels;
251
252 connect( &mLabelingFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererParallelJob::renderingFinished );
253
254 // now start rendering of labeling!
255 mLabelingFuture = QtConcurrent::run( renderLabelsStatic, this );
256 mLabelingFutureWatcher.setFuture( mLabelingFuture );
258 }
259 else
260 {
261 renderingFinished();
262 }
263}
264
265#define DEBUG_RENDERING 0
266
267void QgsMapRendererParallelJob::renderingFinished()
268{
269#if DEBUG_RENDERING
270 int i = 0;
271 for ( LayerRenderJob &job : mLayerJobs )
272 {
273 if ( job.img )
274 {
275 job.img->save( QString( "/tmp/first_pass_%1.png" ).arg( i ) );
276 }
277 if ( job.maskPass.image )
278 {
279 job.maskPass.image->save( QString( "/tmp/first_pass_%1_mask.png" ).arg( i ) );
280 }
281 i++;
282 }
283 if ( mLabelJob.img )
284 {
285 mLabelJob.img->save( QString( "/tmp/labels.png" ) );
286 }
287 if ( mLabelJob.maskImage )
288 {
289 mLabelJob.maskImage->save( QString( "/tmp/labels_mask.png" ) );
290 }
291#endif
292 if ( ! mSecondPassLayerJobs.empty() )
293 {
294 initSecondPassJobs( mSecondPassLayerJobs, mLabelJob );
295
296 mStatus = RenderingSecondPass;
297 // We have a second pass to do.
298 connect( &mSecondPassFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererParallelJob::renderLayersSecondPassFinished );
299 mSecondPassFuture = QtConcurrent::map( mSecondPassLayerJobs, renderLayerStatic );
300 mSecondPassFutureWatcher.setFuture( mSecondPassFuture );
301 }
302 else
303 {
304 QgsDebugMsgLevel( QStringLiteral( "PARALLEL finished" ), 2 );
305
306 logRenderingTime( mLayerJobs, mSecondPassLayerJobs, mLabelJob );
307
308 cleanupJobs( mLayerJobs );
309
310 cleanupLabelJob( mLabelJob );
311
312 mStatus = Idle;
313
315
316 emit finished();
317 }
318}
319
320void QgsMapRendererParallelJob::renderLayersSecondPassFinished()
321{
322 QgsDebugMsgLevel( QStringLiteral( "PARALLEL finished" ), 2 );
323
324 // compose second pass images into first pass images
325 composeSecondPass( mSecondPassLayerJobs, mLabelJob );
326
327 // compose final image
328 mFinalImage = composeImage( mSettings, mLayerJobs, mLabelJob );
329
330 logRenderingTime( mLayerJobs, mSecondPassLayerJobs, mLabelJob );
331
332 cleanupJobs( mLayerJobs );
333
334 cleanupSecondPassJobs( mSecondPassLayerJobs );
335
336 cleanupLabelJob( mLabelJob );
337
338 mStatus = Idle;
339
341
342 emit finished();
343}
344
345/*
346 * See section "Smarter Map Redraws"
347 * in https://github.com/qgis/QGIS-Enhancement-Proposals/issues/181
348 */
349// #define SIMULATE_SLOW_RENDERER
350
351void QgsMapRendererParallelJob::renderLayerStatic( LayerRenderJob &job )
352{
353 if ( job.context()->renderingStopped() )
354 return;
355
356 if ( job.cached )
357 return;
358
359 if ( job.previewRenderImage && !job.previewRenderImageInitialized )
360 {
361 job.previewRenderImage->fill( 0 );
362 job.previewRenderImageInitialized = true;
363 }
364
365 if ( job.img )
366 {
367 job.img->fill( 0 );
368 job.imageInitialized = true;
369 }
370
371 QElapsedTimer t;
372 t.start();
373 QgsDebugMsgLevel( QStringLiteral( "job %1 start (layer %2)" ).arg( reinterpret_cast< quint64 >( &job ), 0, 16 ).arg( job.layerId ), 2 );
374 try
375 {
376#ifdef SIMULATE_SLOW_RENDERER
377 QThread::sleep( 1 );
378#endif
379 job.completed = job.renderer->render();
380 }
381 catch ( QgsException &e )
382 {
383 Q_UNUSED( e )
384 QgsDebugError( "Caught unhandled QgsException: " + e.what() );
385 }
386 catch ( std::exception &e )
387 {
388 Q_UNUSED( e )
389 QgsDebugError( "Caught unhandled std::exception: " + QString::fromLatin1( e.what() ) );
390 }
391 catch ( ... )
392 {
393 QgsDebugError( QStringLiteral( "Caught unhandled unknown exception" ) );
394 }
395
396 job.errors = job.renderer->errors();
397 job.renderingTime += t.elapsed();
398 QgsDebugMsgLevel( QStringLiteral( "job %1 end [%2 ms] (layer %3)" ).arg( reinterpret_cast< quint64 >( &job ), 0, 16 ).arg( job.renderingTime ).arg( job.layerId ), 2 );
399}
400
401
402void QgsMapRendererParallelJob::renderLabelsStatic( QgsMapRendererParallelJob *self )
403{
404 LabelRenderJob &job = self->mLabelJob;
405
406 if ( !job.cached )
407 {
408 QElapsedTimer labelTime;
409 labelTime.start();
410
411 QPainter painter;
412 if ( job.img )
413 {
414 job.img->fill( 0 );
415 painter.begin( job.img );
416 }
417 else
418 {
419 painter.begin( &self->mFinalImage );
420 }
421
422 // draw the labels!
423 try
424 {
425 drawLabeling( job.context, self->mLabelingEngineV2.get(), &painter );
426 }
427 catch ( QgsException &e )
428 {
429 Q_UNUSED( e )
430 QgsDebugError( "Caught unhandled QgsException: " + e.what() );
431 }
432 catch ( std::exception &e )
433 {
434 Q_UNUSED( e )
435 QgsDebugError( "Caught unhandled std::exception: " + QString::fromLatin1( e.what() ) );
436 }
437 catch ( ... )
438 {
439 QgsDebugError( QStringLiteral( "Caught unhandled unknown exception" ) );
440 }
441
442 painter.end();
443
444 job.renderingTime = labelTime.elapsed();
445 job.complete = true;
446 job.participatingLayers = _qgis_listRawToQPointer( self->mLabelingEngineV2->participatingLayers() );
447 if ( job.img )
448 {
449 self->mFinalImage = composeImage( self->mSettings, self->mLayerJobs, self->mLabelJob );
450 }
451 }
452}
@ ForceVectorOutput
Vector graphics should not be cached and drawn as raster images.
@ DrawLabeling
Enable drawing of labels on top of the map.
Default QgsLabelingEngine implementation, which completes the whole labeling operation (including lab...
Defines a QGIS exception class.
Definition: qgsexception.h:35
QString what() const
Definition: qgsexception.h:49
Class that stores computed placement from labeling engine.
static void warning(const QString &msg)
Goes to qWarning.
Definition: qgslogger.cpp:131
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)
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
QgsMapRendererCache * mCache
void finished()
emitted when asynchronous rendering is finished (or canceled).
QgsMapSettings mSettings
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.
Job implementation that renders all layers in parallel.
QgsLabelingResults * takeLabelingResults() override
Gets pointer to internal labeling engine (in order to get access to the results).
bool usedCachedLabels() const override
Returns true if the render job was able to use a cached labeling solution.
bool isActive() const override
Tell whether the rendering job is currently running in background.
QgsMapRendererParallelJob(const QgsMapSettings &settings)
void cancelWithoutBlocking() override
Triggers cancellation of the rendering job without blocking.
QImage renderedImage() override
Gets a preview/resulting image.
void cancel() override
Stop the rendering job - does not return until the job has terminated.
void waitForFinished() override
Block until the job has finished.
Intermediate base class adding functionality that allows client to query the rendered image.
The QgsMapSettings class contains configuration for rendering of the map.
bool testFlag(Qgis::MapSettingsFlag flag) const
Check whether a particular flag is enabled.
void setFlag(Qgis::MapSettingsFlag flag, bool on=true)
Enable or disable a particular flag (other flags are not affected)
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugError(str)
Definition: qgslogger.h:38