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