1 /***************************************************************************
2   qgsmaprenderercustompainterjob.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 
16 #include "qgsmaprenderercustompainterjob.h"
17 
18 #include "qgsfeedback.h"
19 #include "qgslabelingengine.h"
20 #include "qgslogger.h"
21 #include "qgsmaplayerrenderer.h"
22 #include "qgsmaplayerlistutils.h"
23 #include "qgsvectorlayerlabeling.h"
24 
25 #include <QtConcurrentRun>
26 
27 //
28 // QgsMapRendererAbstractCustomPainterJob
29 //
30 
QgsMapRendererAbstractCustomPainterJob(const QgsMapSettings & settings)31 QgsMapRendererAbstractCustomPainterJob::QgsMapRendererAbstractCustomPainterJob( const QgsMapSettings &settings )
32   : QgsMapRendererJob( settings )
33 {
34 
35 }
36 
preparePainter(QPainter * painter,const QColor & backgroundColor)37 void QgsMapRendererAbstractCustomPainterJob::preparePainter( QPainter *painter, const QColor &backgroundColor )
38 {
39   // clear the background
40   painter->fillRect( 0, 0, mSettings.deviceOutputSize().width(), mSettings.deviceOutputSize().height(), backgroundColor );
41 
42   painter->setRenderHint( QPainter::Antialiasing, mSettings.testFlag( Qgis::MapSettingsFlag::Antialiasing ) );
43 #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
44   painter->setRenderHint( QPainter::LosslessImageRendering, mSettings.testFlag( Qgis::MapSettingsFlag::LosslessImageRendering ) );
45 #endif
46 
47 #ifndef QT_NO_DEBUG
48   QPaintDevice *paintDevice = painter->device();
49   const QString errMsg = QStringLiteral( "pre-set DPI not equal to painter's DPI (%1 vs %2)" )
50                          .arg( paintDevice->logicalDpiX() )
51                          .arg( mSettings.outputDpi() * mSettings.devicePixelRatio() );
52   Q_ASSERT_X( qgsDoubleNear( paintDevice->logicalDpiX(), mSettings.outputDpi() * mSettings.devicePixelRatio(), 1.0 ),
53               "Job::startRender()", errMsg.toLatin1().data() );
54 #endif
55 }
56 
57 
58 //
59 // QgsMapRendererCustomPainterJob
60 //
61 
QgsMapRendererCustomPainterJob(const QgsMapSettings & settings,QPainter * painter)62 QgsMapRendererCustomPainterJob::QgsMapRendererCustomPainterJob( const QgsMapSettings &settings, QPainter *painter )
63   : QgsMapRendererAbstractCustomPainterJob( settings )
64   , mPainter( painter )
65   , mActive( false )
66   , mRenderSynchronously( false )
67 {
68   QgsDebugMsgLevel( QStringLiteral( "QPAINTER construct" ), 5 );
69 }
70 
~QgsMapRendererCustomPainterJob()71 QgsMapRendererCustomPainterJob::~QgsMapRendererCustomPainterJob()
72 {
73   QgsDebugMsgLevel( QStringLiteral( "QPAINTER destruct" ), 5 );
74   Q_ASSERT( !mFutureWatcher.isRunning() );
75   //cancel();
76 }
77 
startPrivate()78 void QgsMapRendererCustomPainterJob::startPrivate()
79 {
80   if ( isActive() )
81     return;
82 
83   if ( !mPrepareOnly )
84     mRenderingStart.start();
85 
86   mActive = true;
87 
88   mErrors.clear();
89 
90   QgsDebugMsgLevel( QStringLiteral( "QPAINTER run!" ), 5 );
91 
92   QgsDebugMsgLevel( QStringLiteral( "Preparing list of layer jobs for rendering" ), 5 );
93   QElapsedTimer prepareTime;
94   prepareTime.start();
95 
96   preparePainter( mPainter, mSettings.backgroundColor() );
97 
98   mLabelingEngineV2.reset();
99 
100   if ( mSettings.testFlag( Qgis::MapSettingsFlag::DrawLabeling ) )
101   {
102     mLabelingEngineV2.reset( new QgsDefaultLabelingEngine() );
103     mLabelingEngineV2->setMapSettings( mSettings );
104   }
105 
106   const bool canUseLabelCache = prepareLabelCache();
107   mLayerJobs = prepareJobs( mPainter, mLabelingEngineV2.get() );
108   mLabelJob = prepareLabelingJob( mPainter, mLabelingEngineV2.get(), canUseLabelCache );
109   mSecondPassLayerJobs = prepareSecondPassJobs( mLayerJobs, mLabelJob );
110 
111   QgsDebugMsgLevel( QStringLiteral( "Rendering prepared in (seconds): %1" ).arg( prepareTime.elapsed() / 1000.0 ), 4 );
112 
113   if ( mRenderSynchronously )
114   {
115     if ( !mPrepareOnly )
116     {
117       // do the rendering right now!
118       doRender();
119     }
120     return;
121   }
122 
123   // now we are ready to start rendering!
124   connect( &mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererCustomPainterJob::futureFinished );
125 
126   mFuture = QtConcurrent::run( staticRender, this );
127   mFutureWatcher.setFuture( mFuture );
128 }
129 
130 
cancel()131 void QgsMapRendererCustomPainterJob::cancel()
132 {
133   if ( !isActive() )
134   {
135     QgsDebugMsgLevel( QStringLiteral( "QPAINTER not running!" ), 4 );
136     return;
137   }
138 
139   QgsDebugMsgLevel( QStringLiteral( "QPAINTER canceling" ), 5 );
140   disconnect( &mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererCustomPainterJob::futureFinished );
141   cancelWithoutBlocking();
142 
143   QElapsedTimer t;
144   t.start();
145 
146   mFutureWatcher.waitForFinished();
147 
148   QgsDebugMsgLevel( QStringLiteral( "QPAINER cancel waited %1 ms" ).arg( t.elapsed() / 1000.0 ), 5 );
149 
150   futureFinished();
151 
152   QgsDebugMsgLevel( QStringLiteral( "QPAINTER canceled" ), 5 );
153 }
154 
cancelWithoutBlocking()155 void QgsMapRendererCustomPainterJob::cancelWithoutBlocking()
156 {
157   if ( !isActive() )
158   {
159     QgsDebugMsg( QStringLiteral( "QPAINTER not running!" ) );
160     return;
161   }
162 
163   mLabelJob.context.setRenderingStopped( true );
164   for ( LayerRenderJob &job : mLayerJobs )
165   {
166     job.context()->setRenderingStopped( true );
167     if ( job.renderer && job.renderer->feedback() )
168       job.renderer->feedback()->cancel();
169   }
170 }
171 
waitForFinished()172 void QgsMapRendererCustomPainterJob::waitForFinished()
173 {
174   if ( !isActive() )
175     return;
176 
177   disconnect( &mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererCustomPainterJob::futureFinished );
178 
179   QElapsedTimer t;
180   t.start();
181 
182   mFutureWatcher.waitForFinished();
183 
184   QgsDebugMsgLevel( QStringLiteral( "waitForFinished: %1 ms" ).arg( t.elapsed() / 1000.0 ), 4 );
185 
186   futureFinished();
187 }
188 
isActive() const189 bool QgsMapRendererCustomPainterJob::isActive() const
190 {
191   return mActive;
192 }
193 
usedCachedLabels() const194 bool QgsMapRendererCustomPainterJob::usedCachedLabels() const
195 {
196   return mLabelJob.cached;
197 }
198 
takeLabelingResults()199 QgsLabelingResults *QgsMapRendererCustomPainterJob::takeLabelingResults()
200 {
201   if ( mLabelingEngineV2 )
202     return mLabelingEngineV2->takeResults();
203   else
204     return nullptr;
205 }
206 
207 
waitForFinishedWithEventLoop(QEventLoop::ProcessEventsFlags flags)208 void QgsMapRendererCustomPainterJob::waitForFinishedWithEventLoop( QEventLoop::ProcessEventsFlags flags )
209 {
210   QEventLoop loop;
211   connect( &mFutureWatcher, &QFutureWatcher<void>::finished, &loop, &QEventLoop::quit );
212   loop.exec( flags );
213 }
214 
215 
renderSynchronously()216 void QgsMapRendererCustomPainterJob::renderSynchronously()
217 {
218   mRenderSynchronously = true;
219   start();
220   futureFinished();
221   mRenderSynchronously = false;
222 }
223 
prepare()224 void QgsMapRendererCustomPainterJob::prepare()
225 {
226   mRenderSynchronously = true;
227   mPrepareOnly = true;
228   start();
229   mPrepared = true;
230 }
231 
renderPrepared()232 void QgsMapRendererCustomPainterJob::renderPrepared()
233 {
234   if ( !mPrepared )
235     return;
236 
237   doRender();
238   futureFinished();
239   mRenderSynchronously = false;
240   mPrepareOnly = false;
241   mPrepared = false;
242 }
243 
futureFinished()244 void QgsMapRendererCustomPainterJob::futureFinished()
245 {
246   mActive = false;
247   if ( !mPrepared ) // can't access from other thread
248     mRenderingTime = mRenderingStart.elapsed();
249   QgsDebugMsgLevel( QStringLiteral( "QPAINTER futureFinished" ), 5 );
250 
251   if ( !mPrepared )
252     logRenderingTime( mLayerJobs, {}, mLabelJob );
253 
254   // final cleanup
255   cleanupJobs( mLayerJobs );
256   cleanupSecondPassJobs( mSecondPassLayerJobs );
257   cleanupLabelJob( mLabelJob );
258 
259   emit finished();
260 }
261 
262 
staticRender(QgsMapRendererCustomPainterJob * self)263 void QgsMapRendererCustomPainterJob::staticRender( QgsMapRendererCustomPainterJob *self )
264 {
265   try
266   {
267     self->doRender();
268   }
269   catch ( QgsException &e )
270   {
271     Q_UNUSED( e )
272     QgsDebugMsg( "Caught unhandled QgsException: " + e.what() );
273   }
274   catch ( std::exception &e )
275   {
276     Q_UNUSED( e )
277     QgsDebugMsg( "Caught unhandled std::exception: " + QString::fromLatin1( e.what() ) );
278   }
279   catch ( ... )
280   {
281     QgsDebugMsg( QStringLiteral( "Caught unhandled unknown exception" ) );
282   }
283 }
284 
doRender()285 void QgsMapRendererCustomPainterJob::doRender()
286 {
287   const bool hasSecondPass = ! mSecondPassLayerJobs.empty();
288   QgsDebugMsgLevel( QStringLiteral( "Starting to render layer stack." ), 5 );
289   QElapsedTimer renderTime;
290   renderTime.start();
291 
292   for ( LayerRenderJob &job : mLayerJobs )
293   {
294     if ( job.context()->renderingStopped() )
295       break;
296 
297     if ( ! hasSecondPass && job.context()->useAdvancedEffects() )
298     {
299       // Set the QPainter composition mode so that this layer is rendered using
300       // the desired blending mode
301       mPainter->setCompositionMode( job.blendMode );
302     }
303 
304     if ( !job.cached )
305     {
306       QElapsedTimer layerTime;
307       layerTime.start();
308 
309       if ( job.img )
310       {
311         job.img->fill( 0 );
312         job.imageInitialized = true;
313       }
314 
315       job.completed = job.renderer->render();
316 
317       job.renderingTime += layerTime.elapsed();
318     }
319 
320     if ( ! hasSecondPass && job.img )
321     {
322       // If we flattened this layer for alternate blend modes, composite it now
323       mPainter->setOpacity( job.opacity );
324       mPainter->drawImage( 0, 0, *job.img );
325       mPainter->setOpacity( 1.0 );
326     }
327 
328   }
329 
330   QgsDebugMsgLevel( QStringLiteral( "Done rendering map layers" ), 5 );
331 
332   if ( mSettings.testFlag( Qgis::MapSettingsFlag::DrawLabeling ) && !mLabelJob.context.renderingStopped() )
333   {
334     if ( !mLabelJob.cached )
335     {
336       QElapsedTimer labelTime;
337       labelTime.start();
338 
339       if ( mLabelJob.img )
340       {
341         QPainter painter;
342         mLabelJob.img->fill( 0 );
343         painter.begin( mLabelJob.img );
344         mLabelJob.context.setPainter( &painter );
345         drawLabeling( mLabelJob.context, mLabelingEngineV2.get(), &painter );
346         painter.end();
347       }
348       else
349       {
350         drawLabeling( mLabelJob.context, mLabelingEngineV2.get(), mPainter );
351       }
352 
353       mLabelJob.complete = true;
354       mLabelJob.renderingTime = labelTime.elapsed();
355       mLabelJob.participatingLayers = _qgis_listRawToQPointer( mLabelingEngineV2->participatingLayers() );
356     }
357   }
358 
359   if ( ! hasSecondPass )
360   {
361     if ( mLabelJob.img && mLabelJob.complete )
362     {
363       mPainter->setCompositionMode( QPainter::CompositionMode_SourceOver );
364       mPainter->setOpacity( 1.0 );
365       mPainter->drawImage( 0, 0, *mLabelJob.img );
366     }
367   }
368   else
369   {
370     for ( LayerRenderJob &job : mSecondPassLayerJobs )
371     {
372       if ( job.context()->renderingStopped() )
373         break;
374 
375       if ( !job.cached )
376       {
377         QElapsedTimer layerTime;
378         layerTime.start();
379 
380         if ( job.img )
381         {
382           job.img->fill( 0 );
383           job.imageInitialized = true;
384         }
385 
386         job.completed = job.renderer->render();
387 
388         job.renderingTime += layerTime.elapsed();
389       }
390     }
391 
392     composeSecondPass( mSecondPassLayerJobs, mLabelJob );
393 
394     const QImage finalImage = composeImage( mSettings, mLayerJobs, mLabelJob );
395 
396     mPainter->setCompositionMode( QPainter::CompositionMode_SourceOver );
397     mPainter->setOpacity( 1.0 );
398     mPainter->drawImage( 0, 0, finalImage );
399   }
400 
401   QgsDebugMsgLevel( QStringLiteral( "Rendering completed in (seconds): %1" ).arg( renderTime.elapsed() / 1000.0 ), 2 );
402 }
403 
404 
405