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