QGIS API Documentation  3.20.0-Odense (decaadbb31)
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.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  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 ( LayerRenderJobs::iterator it = mLayerJobs.begin(); it != mLayerJobs.end(); ++it )
84  {
85  it->context.setRenderingStopped( true );
86  if ( it->renderer && it->renderer->feedback() )
87  it->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 ( LayerRenderJobs::iterator it = mLayerJobs.begin(); it != mLayerJobs.end(); ++it )
129  {
130  it->context.setRenderingStopped( true );
131  if ( it->renderer && it->renderer->feedback() )
132  it->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  LayerRenderJobs::const_iterator it = mLayerJobs.constBegin();
228  for ( ; it != mLayerJobs.constEnd(); ++it )
229  {
230  if ( !it->errors.isEmpty() )
231  {
232  mErrors.append( Error( it->layer->id(), it->errors.join( ',' ) ) );
233  }
234  }
235 
236  // compose final image for labeling
237  if ( mSecondPassLayerJobs.isEmpty() )
238  {
239  mFinalImage = composeImage( mSettings, mLayerJobs, mLabelJob, mCache );
240  }
241 
242  QgsDebugMsgLevel( QStringLiteral( "PARALLEL layers finished" ), 2 );
243 
244  if ( mSettings.testFlag( QgsMapSettings::DrawLabeling ) && !mLabelJob.context.renderingStopped() )
245  {
246  mStatus = RenderingLabels;
247 
248  connect( &mLabelingFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererParallelJob::renderingFinished );
249 
250  // now start rendering of labeling!
251  mLabelingFuture = QtConcurrent::run( renderLabelsStatic, this );
252  mLabelingFutureWatcher.setFuture( mLabelingFuture );
254  }
255  else
256  {
257  renderingFinished();
258  }
259 }
260 
261 #define DEBUG_RENDERING 0
262 
263 void QgsMapRendererParallelJob::renderingFinished()
264 {
265 #if DEBUG_RENDERING
266  int i = 0;
267  for ( LayerRenderJob &job : mLayerJobs )
268  {
269  if ( job.img )
270  {
271  job.img->save( QString( "/tmp/first_pass_%1.png" ).arg( i ) );
272  }
273  if ( job.maskPass.image )
274  {
275  job.maskPass.image->save( QString( "/tmp/first_pass_%1_mask.png" ).arg( i ) );
276  }
277  i++;
278  }
279  if ( mLabelJob.img )
280  {
281  mLabelJob.img->save( QString( "/tmp/labels.png" ) );
282  }
283  if ( mLabelJob.maskImage )
284  {
285  mLabelJob.maskImage->save( QString( "/tmp/labels_mask.png" ) );
286  }
287 #endif
288  if ( ! mSecondPassLayerJobs.isEmpty() )
289  {
290  mStatus = RenderingSecondPass;
291  // We have a second pass to do.
292  mSecondPassFuture = QtConcurrent::map( mSecondPassLayerJobs, renderLayerStatic );
293  mSecondPassFutureWatcher.setFuture( mSecondPassFuture );
294  connect( &mSecondPassFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererParallelJob::renderLayersSecondPassFinished );
295  }
296  else
297  {
298  QgsDebugMsgLevel( QStringLiteral( "PARALLEL finished" ), 2 );
299 
300  logRenderingTime( mLayerJobs, mSecondPassLayerJobs, mLabelJob );
301 
302  cleanupJobs( mLayerJobs );
303 
304  cleanupLabelJob( mLabelJob );
305 
306  mStatus = Idle;
307 
308  mRenderingTime = mRenderingStart.elapsed();
309 
310  emit finished();
311  }
312 }
313 
314 void QgsMapRendererParallelJob::renderLayersSecondPassFinished()
315 {
316  QgsDebugMsgLevel( QStringLiteral( "PARALLEL finished" ), 2 );
317 
318  // compose second pass images into first pass images
319  composeSecondPass( mSecondPassLayerJobs, mLabelJob );
320 
321  // compose final image
322  mFinalImage = composeImage( mSettings, mLayerJobs, mLabelJob );
323 
324  logRenderingTime( mLayerJobs, mSecondPassLayerJobs, mLabelJob );
325 
326  cleanupJobs( mLayerJobs );
327 
328  cleanupSecondPassJobs( mSecondPassLayerJobs );
329 
330  cleanupLabelJob( mLabelJob );
331 
332  mStatus = Idle;
333 
334  mRenderingTime = mRenderingStart.elapsed();
335 
336  emit finished();
337 }
338 
339 /*
340  * See section "Smarter Map Redraws"
341  * in https://github.com/qgis/QGIS-Enhancement-Proposals/issues/181
342  */
343 // #define SIMULATE_SLOW_RENDERER
344 
345 void QgsMapRendererParallelJob::renderLayerStatic( LayerRenderJob &job )
346 {
347  if ( job.context.renderingStopped() )
348  return;
349 
350  if ( job.cached )
351  return;
352 
353  if ( job.img )
354  {
355  job.img->fill( 0 );
356  job.imageInitialized = true;
357  }
358 
359  QElapsedTimer t;
360  t.start();
361  QgsDebugMsgLevel( QStringLiteral( "job %1 start (layer %2)" ).arg( reinterpret_cast< quint64 >( &job ), 0, 16 ).arg( job.layerId ), 2 );
362  try
363  {
364 #ifdef SIMULATE_SLOW_RENDERER
365  QThread::sleep( 1 );
366 #endif
367  job.completed = job.renderer->render();
368  }
369  catch ( QgsException &e )
370  {
371  Q_UNUSED( e )
372  QgsDebugMsg( "Caught unhandled QgsException: " + e.what() );
373  }
374  catch ( std::exception &e )
375  {
376  Q_UNUSED( e )
377  QgsDebugMsg( "Caught unhandled std::exception: " + QString::fromLatin1( e.what() ) );
378  }
379  catch ( ... )
380  {
381  QgsDebugMsg( QStringLiteral( "Caught unhandled unknown exception" ) );
382  }
383 
384  job.errors = job.renderer->errors();
385  job.renderingTime += t.elapsed();
386  QgsDebugMsgLevel( QStringLiteral( "job %1 end [%2 ms] (layer %3)" ).arg( reinterpret_cast< quint64 >( &job ), 0, 16 ).arg( job.renderingTime ).arg( job.layerId ), 2 );
387 }
388 
389 
390 void QgsMapRendererParallelJob::renderLabelsStatic( QgsMapRendererParallelJob *self )
391 {
392  LabelRenderJob &job = self->mLabelJob;
393 
394  if ( !job.cached )
395  {
396  QElapsedTimer labelTime;
397  labelTime.start();
398 
399  QPainter painter;
400  if ( job.img )
401  {
402  job.img->fill( 0 );
403  painter.begin( job.img );
404  }
405  else
406  {
407  painter.begin( &self->mFinalImage );
408  }
409 
410  // draw the labels!
411  try
412  {
413  drawLabeling( job.context, self->mLabelingEngineV2.get(), &painter );
414  }
415  catch ( QgsException &e )
416  {
417  Q_UNUSED( e )
418  QgsDebugMsg( "Caught unhandled QgsException: " + e.what() );
419  }
420  catch ( std::exception &e )
421  {
422  Q_UNUSED( e )
423  QgsDebugMsg( "Caught unhandled std::exception: " + QString::fromLatin1( e.what() ) );
424  }
425  catch ( ... )
426  {
427  QgsDebugMsg( QStringLiteral( "Caught unhandled unknown exception" ) );
428  }
429 
430  painter.end();
431 
432  job.renderingTime = labelTime.elapsed();
433  job.complete = true;
434  job.participatingLayers = _qgis_listRawToQPointer( self->mLabelingEngineV2->participatingLayers() );
435  if ( job.img )
436  {
437  self->mFinalImage = composeImage( self->mSettings, self->mLayerJobs, self->mLabelJob );
438  }
439  }
440 }
441 
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 cleanupSecondPassJobs(LayerRenderJobs &jobs)
LayerRenderJobs prepareSecondPassJobs(LayerRenderJobs &firstPassJobs, LabelRenderJob &labelJob)
Prepares jobs for a second pass, if selective masks exist (from labels or symbol layers).
void logRenderingTime(const LayerRenderJobs &jobs, const LayerRenderJobs &secondPassJobs, const LabelRenderJob &labelJob)
static Q_DECL_DEPRECATED void drawLabeling(const QgsMapSettings &settings, QgsRenderContext &renderContext, QgsLabelingEngine *labelingEngine2, QPainter *painter)
LayerRenderJobs 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.
static void composeSecondPass(LayerRenderJobs &secondPassJobs, LabelRenderJob &labelJob)
Compose second pass images into first pass images.
QElapsedTimer mRenderingStart
QgsMapRendererCache * mCache
void finished()
emitted when asynchronous rendering is finished (or canceled).
static QImage composeImage(const QgsMapSettings &settings, const LayerRenderJobs &jobs, const LabelRenderJob &labelJob, const QgsMapRendererCache *cache=nullptr)
QgsMapSettings mSettings
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...
void cleanupJobs(LayerRenderJobs &jobs)
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(Flag flag) const
Check whether a particular flag is enabled.
@ DrawLabeling
Enable drawing of labels on top of the map.
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugMsg(str)
Definition: qgslogger.h:38