1 /***************************************************************************
2 qgspointcloudlayerrenderer.cpp
3 --------------------
4 begin : October 2020
5 copyright : (C) 2020 by Peter Petrik
6 email : zilolv at gmail dot com
7 ***************************************************************************/
8
9 /***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
18 #include <QElapsedTimer>
19 #include <QPointer>
20
21 #include "qgspointcloudlayerrenderer.h"
22 #include "qgspointcloudlayer.h"
23 #include "qgsrendercontext.h"
24 #include "qgspointcloudindex.h"
25 #include "qgsstyle.h"
26 #include "qgscolorramp.h"
27 #include "qgspointcloudrequest.h"
28 #include "qgspointcloudattribute.h"
29 #include "qgspointcloudrenderer.h"
30 #include "qgspointcloudextentrenderer.h"
31 #include "qgslogger.h"
32 #include "qgspointcloudlayerelevationproperties.h"
33 #include "qgsmessagelog.h"
34 #include "qgscircle.h"
35 #include "qgsmapclippingutils.h"
36 #include "qgspointcloudblockrequest.h"
37
QgsPointCloudLayerRenderer(QgsPointCloudLayer * layer,QgsRenderContext & context)38 QgsPointCloudLayerRenderer::QgsPointCloudLayerRenderer( QgsPointCloudLayer *layer, QgsRenderContext &context )
39 : QgsMapLayerRenderer( layer->id(), &context )
40 , mLayer( layer )
41 , mLayerAttributes( layer->attributes() )
42 , mFeedback( new QgsFeedback )
43 {
44 // TODO: we must not keep pointer to mLayer (it's dangerous) - we must copy anything we need for rendering
45 // or use some locking to prevent read/write from multiple threads
46 if ( !mLayer || !mLayer->dataProvider() || !mLayer->renderer() )
47 return;
48
49 mRenderer.reset( mLayer->renderer()->clone() );
50
51 if ( mLayer->dataProvider()->index() )
52 {
53 mScale = mLayer->dataProvider()->index()->scale();
54 mOffset = mLayer->dataProvider()->index()->offset();
55 }
56
57 if ( const QgsPointCloudLayerElevationProperties *elevationProps = qobject_cast< const QgsPointCloudLayerElevationProperties * >( mLayer->elevationProperties() ) )
58 {
59 mZOffset = elevationProps->zOffset();
60 mZScale = elevationProps->zScale();
61 }
62
63 mCloudExtent = mLayer->dataProvider()->polygonBounds();
64
65 mClippingRegions = QgsMapClippingUtils::collectClippingRegionsForLayer( *renderContext(), layer );
66
67 mReadyToCompose = false;
68 }
69
render()70 bool QgsPointCloudLayerRenderer::render()
71 {
72 QgsPointCloudRenderContext context( *renderContext(), mScale, mOffset, mZScale, mZOffset, mFeedback.get() );
73
74 // Set up the render configuration options
75 QPainter *painter = context.renderContext().painter();
76
77 QgsScopedQPainterState painterState( painter );
78 context.renderContext().setPainterFlagsUsingContext( painter );
79
80 if ( !mClippingRegions.empty() )
81 {
82 bool needsPainterClipPath = false;
83 const QPainterPath path = QgsMapClippingUtils::calculatePainterClipRegion( mClippingRegions, *renderContext(), QgsMapLayerType::VectorTileLayer, needsPainterClipPath );
84 if ( needsPainterClipPath )
85 renderContext()->painter()->setClipPath( path, Qt::IntersectClip );
86 }
87
88 if ( mRenderer->type() == QLatin1String( "extent" ) )
89 {
90 // special case for extent only renderer!
91 mRenderer->startRender( context );
92 static_cast< QgsPointCloudExtentRenderer * >( mRenderer.get() )->renderExtent( mCloudExtent, context );
93 mRenderer->stopRender( context );
94 mReadyToCompose = true;
95 return true;
96 }
97
98 // TODO cache!?
99 QgsPointCloudIndex *pc = mLayer->dataProvider()->index();
100 if ( !pc || !pc->isValid() )
101 {
102 mReadyToCompose = true;
103 return false;
104 }
105
106 // if the previous layer render was relatively quick (e.g. less than 3 seconds), the we show any previously
107 // cached version of the layer during rendering instead of the usual progressive updates
108 if ( mRenderTimeHint > 0 && mRenderTimeHint <= MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE )
109 {
110 mBlockRenderUpdates = true;
111 mElapsedTimer.start();
112 }
113
114 mRenderer->startRender( context );
115
116 mAttributes.push_back( QgsPointCloudAttribute( QStringLiteral( "X" ), QgsPointCloudAttribute::Int32 ) );
117 mAttributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Y" ), QgsPointCloudAttribute::Int32 ) );
118
119 // collect attributes required by renderer
120 QSet< QString > rendererAttributes = mRenderer->usedAttributes( context );
121
122 if ( !context.renderContext().zRange().isInfinite() )
123 rendererAttributes.insert( QStringLiteral( "Z" ) );
124
125 for ( const QString &attribute : std::as_const( rendererAttributes ) )
126 {
127 if ( mAttributes.indexOf( attribute ) >= 0 )
128 continue; // don't re-add attributes we are already going to fetch
129
130 const int layerIndex = mLayerAttributes.indexOf( attribute );
131 if ( layerIndex < 0 )
132 {
133 QgsMessageLog::logMessage( QObject::tr( "Required attribute %1 not found in layer" ).arg( attribute ), QObject::tr( "Point Cloud" ) );
134 continue;
135 }
136
137 mAttributes.push_back( mLayerAttributes.at( layerIndex ) );
138 }
139
140 QgsPointCloudDataBounds db;
141
142 #ifdef QGISDEBUG
143 QElapsedTimer t;
144 t.start();
145 #endif
146
147 const IndexedPointCloudNode root = pc->root();
148
149 const double maximumError = context.renderContext().convertToPainterUnits( mRenderer->maximumScreenError(), mRenderer->maximumScreenErrorUnit() );// in pixels
150
151 const QgsRectangle rootNodeExtentLayerCoords = pc->nodeMapExtent( root );
152 QgsRectangle rootNodeExtentMapCoords;
153 try
154 {
155 rootNodeExtentMapCoords = context.renderContext().coordinateTransform().transformBoundingBox( rootNodeExtentLayerCoords );
156 }
157 catch ( QgsCsException & )
158 {
159 QgsDebugMsg( QStringLiteral( "Could not transform node extent to map CRS" ) );
160 rootNodeExtentMapCoords = rootNodeExtentLayerCoords;
161 }
162
163 const double rootErrorInMapCoordinates = rootNodeExtentMapCoords.width() / pc->span(); // in map coords
164
165 double mapUnitsPerPixel = context.renderContext().mapToPixel().mapUnitsPerPixel();
166 if ( ( rootErrorInMapCoordinates < 0.0 ) || ( mapUnitsPerPixel < 0.0 ) || ( maximumError < 0.0 ) )
167 {
168 QgsDebugMsg( QStringLiteral( "invalid screen error" ) );
169 mReadyToCompose = true;
170 return false;
171 }
172 double rootErrorPixels = rootErrorInMapCoordinates / mapUnitsPerPixel; // in pixels
173 const QVector<IndexedPointCloudNode> nodes = traverseTree( pc, context.renderContext(), pc->root(), maximumError, rootErrorPixels );
174
175 QgsPointCloudRequest request;
176 request.setAttributes( mAttributes );
177
178 // drawing
179 int nodesDrawn = 0;
180 bool canceled = false;
181
182 if ( pc->accessType() == QgsPointCloudIndex::AccessType::Local )
183 {
184 nodesDrawn += renderNodesSync( nodes, pc, context, request, canceled );
185 }
186 else if ( pc->accessType() == QgsPointCloudIndex::AccessType::Remote )
187 {
188 nodesDrawn += renderNodesAsync( nodes, pc, context, request, canceled );
189 }
190
191 #ifdef QGISDEBUG
192 QgsDebugMsgLevel( QStringLiteral( "totals: %1 nodes | %2 points | %3ms" ).arg( nodesDrawn )
193 .arg( context.pointsRendered() )
194 .arg( t.elapsed() ), 2 );
195 #endif
196
197 mRenderer->stopRender( context );
198
199 mReadyToCompose = true;
200 return !canceled;
201 }
202
renderNodesSync(const QVector<IndexedPointCloudNode> & nodes,QgsPointCloudIndex * pc,QgsPointCloudRenderContext & context,QgsPointCloudRequest & request,bool & canceled)203 int QgsPointCloudLayerRenderer::renderNodesSync( const QVector<IndexedPointCloudNode> &nodes, QgsPointCloudIndex *pc, QgsPointCloudRenderContext &context, QgsPointCloudRequest &request, bool &canceled )
204 {
205 int nodesDrawn = 0;
206 for ( const IndexedPointCloudNode &n : nodes )
207 {
208 if ( context.renderContext().renderingStopped() )
209 {
210 QgsDebugMsgLevel( "canceled", 2 );
211 canceled = true;
212 break;
213 }
214 std::unique_ptr<QgsPointCloudBlock> block( pc->nodeData( n, request ) );
215
216 if ( !block )
217 continue;
218
219 QgsVector3D contextScale = context.scale();
220 QgsVector3D contextOffset = context.offset();
221
222 context.setScale( block->scale() );
223 context.setOffset( block->offset() );
224
225 context.setAttributes( block->attributes() );
226
227 mRenderer->renderBlock( block.get(), context );
228
229 context.setScale( contextScale );
230 context.setOffset( contextOffset );
231
232 ++nodesDrawn;
233
234 // as soon as first block is rendered, we can start showing layer updates.
235 // but if we are blocking render updates (so that a previously cached image is being shown), we wait
236 // at most e.g. 3 seconds before we start forcing progressive updates.
237 if ( !mBlockRenderUpdates || mElapsedTimer.elapsed() > MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE )
238 {
239 mReadyToCompose = true;
240 }
241 }
242 return nodesDrawn;
243 }
244
renderNodesAsync(const QVector<IndexedPointCloudNode> & nodes,QgsPointCloudIndex * pc,QgsPointCloudRenderContext & context,QgsPointCloudRequest & request,bool & canceled)245 int QgsPointCloudLayerRenderer::renderNodesAsync( const QVector<IndexedPointCloudNode> &nodes, QgsPointCloudIndex *pc, QgsPointCloudRenderContext &context, QgsPointCloudRequest &request, bool &canceled )
246 {
247 int nodesDrawn = 0;
248
249 QElapsedTimer downloadTimer;
250 downloadTimer.start();
251
252 // Instead of loading all point blocks in parallel and then rendering the one by one,
253 // we split the processing into groups of size groupSize where we load the blocks of the group
254 // in parallel and then render the group's blocks sequentially.
255 // This way helps QGIS stay responsive if the nodes vector size is big
256 const int groupSize = 4;
257 for ( int groupIndex = 0; groupIndex < nodes.size(); groupIndex += groupSize )
258 {
259 if ( context.feedback() && context.feedback()->isCanceled() )
260 break;
261 // Async loading of nodes
262 const int currentGroupSize = std::min< size_t >( std::max< size_t >( nodes.size() - groupIndex, 0 ), groupSize );
263 QVector<QgsPointCloudBlockRequest *> blockRequests( currentGroupSize, nullptr );
264 QVector<bool> finishedLoadingBlock( currentGroupSize, false );
265 QEventLoop loop;
266 if ( context.feedback() )
267 QObject::connect( context.feedback(), &QgsFeedback::canceled, &loop, &QEventLoop::quit );
268 // Note: All capture by reference warnings here shouldn't be an issue since we have an event loop, so locals won't be deallocated
269 for ( int i = 0; i < blockRequests.size(); ++i )
270 {
271 int nodeIndex = groupIndex + i;
272 const IndexedPointCloudNode &n = nodes[nodeIndex];
273 const QString nStr = n.toString();
274 QgsPointCloudBlockRequest *blockRequest = pc->asyncNodeData( n, request );
275 blockRequests[ i ] = blockRequest;
276 QObject::connect( blockRequest, &QgsPointCloudBlockRequest::finished, &loop, [ &, i, nStr, blockRequest ]()
277 {
278 if ( !blockRequest->block() )
279 {
280 QgsDebugMsg( QStringLiteral( "Unable to load node %1, error: %2" ).arg( nStr, blockRequest->errorStr() ) );
281 }
282 finishedLoadingBlock[ i ] = true;
283 // If all blocks are loaded, exit the event loop
284 if ( !finishedLoadingBlock.contains( false ) ) loop.exit();
285 } );
286 }
287 // Wait for all point cloud nodes to finish loading
288 loop.exec();
289
290 QgsDebugMsg( QStringLiteral( "Downloaded in : %1ms" ).arg( downloadTimer.elapsed() ) );
291 if ( !context.feedback()->isCanceled() )
292 {
293 // Render all the point cloud blocks sequentially
294 for ( int i = 0; i < blockRequests.size(); ++i )
295 {
296 if ( context.renderContext().renderingStopped() )
297 {
298 QgsDebugMsgLevel( "canceled", 2 );
299 canceled = true;
300 break;
301 }
302
303 if ( !blockRequests[ i ]->block() )
304 continue;
305
306 QgsVector3D contextScale = context.scale();
307 QgsVector3D contextOffset = context.offset();
308
309 context.setScale( blockRequests[ i ]->block()->scale() );
310 context.setOffset( blockRequests[ i ]->block()->offset() );
311
312 context.setAttributes( blockRequests[ i ]->block()->attributes() );
313
314 mRenderer->renderBlock( blockRequests[ i ]->block(), context );
315
316 context.setScale( contextScale );
317 context.setOffset( contextOffset );
318
319 ++nodesDrawn;
320
321 // as soon as first block is rendered, we can start showing layer updates.
322 // but if we are blocking render updates (so that a previously cached image is being shown), we wait
323 // at most e.g. 3 seconds before we start forcing progressive updates.
324 if ( !mBlockRenderUpdates || mElapsedTimer.elapsed() > MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE )
325 {
326 mReadyToCompose = true;
327 }
328 }
329 }
330
331 for ( int i = 0; i < blockRequests.size(); ++i )
332 {
333 if ( blockRequests[ i ] )
334 {
335 if ( blockRequests[ i ]->block() )
336 delete blockRequests[ i ]->block();
337 blockRequests[ i ]->deleteLater();
338 }
339 }
340 }
341
342 return nodesDrawn;
343 }
344
forceRasterRender() const345 bool QgsPointCloudLayerRenderer::forceRasterRender() const
346 {
347 // unless we are using the extent only renderer, point cloud layers should always be rasterized -- we don't want to export points as vectors
348 // to formats like PDF!
349 return mRenderer ? mRenderer->type() != QLatin1String( "extent" ) : false;
350 }
351
setLayerRenderingTimeHint(int time)352 void QgsPointCloudLayerRenderer::setLayerRenderingTimeHint( int time )
353 {
354 mRenderTimeHint = time;
355 }
356
traverseTree(const QgsPointCloudIndex * pc,const QgsRenderContext & context,IndexedPointCloudNode n,double maxErrorPixels,double nodeErrorPixels)357 QVector<IndexedPointCloudNode> QgsPointCloudLayerRenderer::traverseTree( const QgsPointCloudIndex *pc,
358 const QgsRenderContext &context,
359 IndexedPointCloudNode n,
360 double maxErrorPixels,
361 double nodeErrorPixels )
362 {
363 QVector<IndexedPointCloudNode> nodes;
364
365 if ( context.renderingStopped() )
366 {
367 QgsDebugMsgLevel( QStringLiteral( "canceled" ), 2 );
368 return nodes;
369 }
370
371 if ( !context.extent().intersects( pc->nodeMapExtent( n ) ) )
372 return nodes;
373
374 const QgsDoubleRange nodeZRange = pc->nodeZRange( n );
375 const QgsDoubleRange adjustedNodeZRange = QgsDoubleRange( nodeZRange.lower() + mZOffset, nodeZRange.upper() + mZOffset );
376 if ( !context.zRange().isInfinite() && !context.zRange().overlaps( adjustedNodeZRange ) )
377 return nodes;
378
379 nodes.append( n );
380
381 double childrenErrorPixels = nodeErrorPixels / 2.0;
382 if ( childrenErrorPixels < maxErrorPixels )
383 return nodes;
384
385 const QList<IndexedPointCloudNode> children = pc->nodeChildren( n );
386 for ( const IndexedPointCloudNode &nn : children )
387 {
388 nodes += traverseTree( pc, context, nn, maxErrorPixels, childrenErrorPixels );
389 }
390
391 return nodes;
392 }
393
394 QgsPointCloudLayerRenderer::~QgsPointCloudLayerRenderer() = default;
395