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