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