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