-
-
Notifications
You must be signed in to change notification settings - Fork 3k
/
qgsmaprendererjob.cpp
390 lines (323 loc) · 13.4 KB
/
qgsmaprendererjob.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
/***************************************************************************
qgsmaprendererjob.cpp
--------------------------------------
Date : December 2013
Copyright : (C) 2013 by Martin Dobias
Email : wonder dot sk at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgsmaprendererjob.h"
#include <QPainter>
#include <QTime>
#include <QTimer>
#include <QtConcurrentMap>
#include <QSettings>
#include "qgscrscache.h"
#include "qgslogger.h"
#include "qgsrendercontext.h"
#include "qgsmaplayer.h"
#include "qgsmaplayerregistry.h"
#include "qgsmaplayerrenderer.h"
#include "qgsmaplayerstylemanager.h"
#include "qgsmaprenderercache.h"
#include "qgsmessagelog.h"
#include "qgspallabeling.h"
#include "qgsvectorlayerrenderer.h"
#include "qgsvectorlayer.h"
#include "qgscsexception.h"
QgsMapRendererJob::QgsMapRendererJob( const QgsMapSettings& settings )
: mSettings( settings )
, mCache( nullptr )
, mRenderingTime( 0 )
{
}
QgsMapRendererQImageJob::QgsMapRendererQImageJob( const QgsMapSettings& settings )
: QgsMapRendererJob( settings )
{
}
QgsMapRendererJob::Errors QgsMapRendererJob::errors() const
{
return mErrors;
}
void QgsMapRendererJob::setCache( QgsMapRendererCache* cache )
{
mCache = cache;
}
const QgsMapSettings& QgsMapRendererJob::mapSettings() const
{
return mSettings;
}
bool QgsMapRendererJob::reprojectToLayerExtent( const QgsMapLayer *ml, const QgsCoordinateTransform& ct, QgsRectangle &extent, QgsRectangle &r2 )
{
bool split = false;
try
{
#ifdef QGISDEBUG
// QgsLogger::debug<QgsRectangle>("Getting extent of canvas in layers CS. Canvas is ", extent, __FILE__, __FUNCTION__, __LINE__);
#endif
// Split the extent into two if the source CRS is
// geographic and the extent crosses the split in
// geographic coordinates (usually +/- 180 degrees,
// and is assumed to be so here), and draw each
// extent separately.
static const double splitCoord = 180.0;
if ( ml->crs().isGeographic() )
{
if ( ml->type() == QgsMapLayer::VectorLayer && !ct.destinationCrs().isGeographic() )
{
// if we transform from a projected coordinate system check
// check if transforming back roughly returns the input
// extend - otherwise render the world.
QgsRectangle extent1 = ct.transformBoundingBox( extent, QgsCoordinateTransform::ReverseTransform );
QgsRectangle extent2 = ct.transformBoundingBox( extent1, QgsCoordinateTransform::ForwardTransform );
QgsDebugMsgLevel( QString( "\n0:%1 %2x%3\n1:%4\n2:%5 %6x%7 (w:%8 h:%9)" )
.arg( extent.toString() ).arg( extent.width() ).arg( extent.height() )
.arg( extent1.toString(), extent2.toString() ).arg( extent2.width() ).arg( extent2.height() )
.arg( fabs( 1.0 - extent2.width() / extent.width() ) )
.arg( fabs( 1.0 - extent2.height() / extent.height() ) )
, 3 );
if ( fabs( 1.0 - extent2.width() / extent.width() ) < 0.5 &&
fabs( 1.0 - extent2.height() / extent.height() ) < 0.5 )
{
extent = extent1;
}
else
{
extent = QgsRectangle( -180.0, -90.0, 180.0, 90.0 );
}
}
else
{
// Note: ll = lower left point
QgsPoint ll = ct.transform( extent.xMinimum(), extent.yMinimum(),
QgsCoordinateTransform::ReverseTransform );
// and ur = upper right point
QgsPoint ur = ct.transform( extent.xMaximum(), extent.yMaximum(),
QgsCoordinateTransform::ReverseTransform );
QgsDebugMsg( QString( "in:%1 (ll:%2 ur:%3)" ).arg( extent.toString(), ll.toString(), ur.toString() ) );
extent = ct.transformBoundingBox( extent, QgsCoordinateTransform::ReverseTransform );
QgsDebugMsg( QString( "out:%1 (w:%2 h:%3)" ).arg( extent.toString() ).arg( extent.width() ).arg( extent.height() ) );
if ( ll.x() > ur.x() )
{
// the coordinates projected in reverse order than what one would expect.
// we are probably looking at an area that includes longitude of 180 degrees.
// we need to take into account coordinates from two intervals: (-180,x1) and (x2,180)
// so let's use (-180,180). This hopefully does not add too much overhead. It is
// more straightforward than rendering with two separate extents and more consistent
// for rendering, labeling and caching as everything is rendered just in one go
extent.setXMinimum( -splitCoord );
extent.setXMaximum( splitCoord );
}
}
// TODO: the above rule still does not help if using a projection that covers the whole
// world. E.g. with EPSG:3857 the longitude spectrum -180 to +180 is mapped to approx.
// -2e7 to +2e7. Converting extent from -5e7 to +5e7 is transformed as -90 to +90,
// but in fact the extent should cover the whole world.
}
else // can't cross 180
{
if ( ct.destinationCrs().isGeographic() &&
( extent.xMinimum() <= -180 || extent.xMaximum() >= 180 ||
extent.yMinimum() <= -90 || extent.yMaximum() >= 90 ) )
// Use unlimited rectangle because otherwise we may end up transforming wrong coordinates.
// E.g. longitude -200 to +160 would be understood as +40 to +160 due to periodicity.
// We could try to clamp coords to (-180,180) for lon resp. (-90,90) for lat,
// but this seems like a safer choice.
extent = QgsRectangle( -DBL_MAX, -DBL_MAX, DBL_MAX, DBL_MAX );
else
extent = ct.transformBoundingBox( extent, QgsCoordinateTransform::ReverseTransform );
}
}
catch ( QgsCsException &cse )
{
Q_UNUSED( cse );
QgsDebugMsg( "Transform error caught" );
extent = QgsRectangle( -DBL_MAX, -DBL_MAX, DBL_MAX, DBL_MAX );
r2 = QgsRectangle( -DBL_MAX, -DBL_MAX, DBL_MAX, DBL_MAX );
}
return split;
}
LayerRenderJobs QgsMapRendererJob::prepareJobs( QPainter* painter, QgsLabelingEngineV2* labelingEngine2 )
{
LayerRenderJobs layerJobs;
// render all layers in the stack, starting at the base
QListIterator<QString> li( mSettings.layers() );
li.toBack();
if ( mCache )
{
bool cacheValid = mCache->init( mSettings.visibleExtent(), mSettings.scale() );
QgsDebugMsg( QString( "CACHE VALID: %1" ).arg( cacheValid ) );
Q_UNUSED( cacheValid );
}
mGeometryCaches.clear();
while ( li.hasPrevious() )
{
QString layerId = li.previous();
QgsDebugMsgLevel( "Rendering at layer item " + layerId, 2 );
QgsMapLayer *ml = QgsMapLayerRegistry::instance()->mapLayer( layerId );
if ( !ml )
{
mErrors.append( Error( layerId, tr( "Layer not found in registry." ) ) );
continue;
}
QgsDebugMsgLevel( QString( "layer %1: minscale:%2 maxscale:%3 scaledepvis:%4 blendmode:%5" )
.arg( ml->name() )
.arg( ml->minimumScale() )
.arg( ml->maximumScale() )
.arg( ml->hasScaleBasedVisibility() )
.arg( ml->blendMode() )
, 3 );
if ( !ml->isInScaleRange( mSettings.scale() ) ) //|| mOverview )
{
QgsDebugMsgLevel( "Layer not rendered because it is not within the defined visibility scale range", 3 );
continue;
}
QgsRectangle r1 = mSettings.visibleExtent(), r2;
QgsCoordinateTransform ct;
if ( mSettings.hasCrsTransformEnabled() )
{
ct = mSettings.layerTransform( ml );
if ( ct.isValid() )
{
reprojectToLayerExtent( ml, ct, r1, r2 );
}
QgsDebugMsgLevel( "extent: " + r1.toString(), 3 );
if ( !r1.isFinite() || !r2.isFinite() )
{
mErrors.append( Error( layerId, tr( "There was a problem transforming the layer's extent. Layer skipped." ) ) );
continue;
}
}
// Force render of layers that are being edited
// or if there's a labeling engine that needs the layer to register features
if ( mCache && ml->type() == QgsMapLayer::VectorLayer )
{
QgsVectorLayer* vl = qobject_cast<QgsVectorLayer *>( ml );
if ( vl->isEditable() || ( labelingEngine2 && QgsPalLabeling::staticWillUseLayer( vl ) ) )
mCache->clearCacheImage( ml->id() );
}
layerJobs.append( LayerRenderJob() );
LayerRenderJob& job = layerJobs.last();
job.cached = false;
job.img = nullptr;
job.blendMode = ml->blendMode();
job.layerId = ml->id();
job.renderingTime = -1;
job.context = QgsRenderContext::fromMapSettings( mSettings );
job.context.setPainter( painter );
job.context.setLabelingEngineV2( labelingEngine2 );
job.context.setCoordinateTransform( ct );
job.context.setExtent( r1 );
// if we can use the cache, let's do it and avoid rendering!
if ( mCache && !mCache->cacheImage( ml->id() ).isNull() )
{
job.cached = true;
job.img = new QImage( mCache->cacheImage( ml->id() ) );
job.renderer = nullptr;
job.context.setPainter( nullptr );
continue;
}
// If we are drawing with an alternative blending mode then we need to render to a separate image
// before compositing this on the map. This effectively flattens the layer and prevents
// blending occurring between objects on the layer
if ( mCache || !painter || needTemporaryImage( ml ) )
{
// Flattened image for drawing when a blending mode is set
QImage * mypFlattenedImage = nullptr;
mypFlattenedImage = new QImage( mSettings.outputSize().width(),
mSettings.outputSize().height(),
mSettings.outputImageFormat() );
if ( mypFlattenedImage->isNull() )
{
mErrors.append( Error( layerId, tr( "Insufficient memory for image %1x%2" ).arg( mSettings.outputSize().width() ).arg( mSettings.outputSize().height() ) ) );
delete mypFlattenedImage;
layerJobs.removeLast();
continue;
}
mypFlattenedImage->fill( 0 );
job.img = mypFlattenedImage;
QPainter* mypPainter = new QPainter( job.img );
mypPainter->setRenderHint( QPainter::Antialiasing, mSettings.testFlag( QgsMapSettings::Antialiasing ) );
job.context.setPainter( mypPainter );
}
bool hasStyleOverride = mSettings.layerStyleOverrides().contains( ml->id() );
if ( hasStyleOverride )
ml->styleManager()->setOverrideStyle( mSettings.layerStyleOverrides().value( ml->id() ) );
job.renderer = ml->createMapRenderer( job.context );
if ( hasStyleOverride )
ml->styleManager()->restoreOverrideStyle();
if ( mRequestedGeomCacheForLayers.contains( ml->id() ) )
{
if ( QgsVectorLayerRenderer* vlr = dynamic_cast<QgsVectorLayerRenderer*>( job.renderer ) )
{
vlr->setGeometryCachePointer( &mGeometryCaches[ ml->id()] );
}
}
} // while (li.hasPrevious())
return layerJobs;
}
void QgsMapRendererJob::cleanupJobs( LayerRenderJobs& jobs )
{
for ( LayerRenderJobs::iterator it = jobs.begin(); it != jobs.end(); ++it )
{
LayerRenderJob& job = *it;
if ( job.img )
{
delete job.context.painter();
job.context.setPainter( nullptr );
if ( mCache && !job.cached && !job.context.renderingStopped() )
{
QgsDebugMsg( "caching image for " + job.layerId );
mCache->setCacheImage( job.layerId, *job.img );
}
delete job.img;
job.img = nullptr;
}
if ( job.renderer )
{
Q_FOREACH ( const QString& message, job.renderer->errors() )
mErrors.append( Error( job.renderer->layerId(), message ) );
delete job.renderer;
job.renderer = nullptr;
}
}
jobs.clear();
updateLayerGeometryCaches();
}
QImage QgsMapRendererJob::composeImage( const QgsMapSettings& settings, const LayerRenderJobs& jobs )
{
QImage image( settings.outputSize(), settings.outputImageFormat() );
image.fill( settings.backgroundColor().rgba() );
QPainter painter( &image );
for ( LayerRenderJobs::const_iterator it = jobs.constBegin(); it != jobs.constEnd(); ++it )
{
const LayerRenderJob& job = *it;
painter.setCompositionMode( job.blendMode );
Q_ASSERT( job.img );
painter.drawImage( 0, 0, *job.img );
}
painter.end();
return image;
}
void QgsMapRendererJob::logRenderingTime( const LayerRenderJobs& jobs )
{
QSettings settings;
if ( !settings.value( "/Map/logCanvasRefreshEvent", false ).toBool() )
return;
QMultiMap<int, QString> elapsed;
Q_FOREACH ( const LayerRenderJob& job, jobs )
elapsed.insert( job.renderingTime, job.layerId );
QList<int> tt( elapsed.uniqueKeys() );
qSort( tt.begin(), tt.end(), qGreater<int>() );
Q_FOREACH ( int t, tt )
{
QgsMessageLog::logMessage( tr( "%1 ms: %2" ).arg( t ).arg( QStringList( elapsed.values( t ) ).join( ", " ) ), tr( "Rendering" ) );
}
QgsMessageLog::logMessage( "---", tr( "Rendering" ) );
}