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