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