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