QGIS API Documentation 3.99.0-Master (21b3aa880ba)
Loading...
Searching...
No Matches
qgspointcloudlayerrenderer.cpp
Go to the documentation of this file.
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
19
20#include <memory>
21
22#include "delaunator.hpp"
23#include "qgsapplication.h"
24#include "qgscolorramp.h"
25#include "qgselevationmap.h"
26#include "qgslogger.h"
27#include "qgsmapclippingutils.h"
28#include "qgsmeshlayerutils.h"
29#include "qgsmessagelog.h"
33#include "qgspointcloudindex.h"
34#include "qgspointcloudlayer.h"
38#include "qgsrendercontext.h"
39#include "qgsruntimeprofiler.h"
40#include "qgsthreadingutils.h"
41#include "qgsvirtualpointcloudprovider.h"
42
43#include <QElapsedTimer>
44#include <QPointer>
45
47 : QgsMapLayerRenderer( layer->id(), &context )
48 , mLayerName( layer->name() )
49 , mLayerAttributes( layer->attributes() )
50 , mSubIndexes( layer->dataProvider() ? layer->dataProvider()->subIndexes() : QVector<QgsPointCloudSubIndex>() )
51 , mFeedback( new QgsFeedback )
52 , mEnableProfile( context.flags() & Qgis::RenderContextFlag::RecordProfile )
53{
54 if ( !layer->dataProvider() || !layer->renderer() )
55 return;
56
57 mIndex = layer->index();
58
59 QElapsedTimer timer;
60 timer.start();
61
62 mRenderer.reset( layer->renderer()->clone() );
63 if ( !mSubIndexes.isEmpty() )
64 {
65 mSubIndexExtentRenderer = std::make_unique<QgsPointCloudExtentRenderer>( );
66 mSubIndexExtentRenderer->setShowLabels( mRenderer->showLabels() );
67 mSubIndexExtentRenderer->setLabelTextFormat( mRenderer->labelTextFormat() );
68 }
69
70 if ( mIndex )
71 {
72 mScale = mIndex.scale();
73 mOffset = mIndex.offset();
74 }
75
76 if ( const QgsPointCloudLayerElevationProperties *elevationProps = qobject_cast< const QgsPointCloudLayerElevationProperties * >( layer->elevationProperties() ) )
77 {
78 mZOffset = elevationProps->zOffset();
79 mZScale = elevationProps->zScale();
80 }
81
82 if ( const QgsVirtualPointCloudProvider *vpcProvider = dynamic_cast<QgsVirtualPointCloudProvider *>( layer->dataProvider() ) )
83 {
84 mIsVpc = true;
85 mAverageSubIndexWidth = vpcProvider->averageSubIndexWidth();
86 mAverageSubIndexHeight = vpcProvider->averageSubIndexHeight();
87 mOverviewIndex = vpcProvider->overview();
88 }
89
90 mCloudExtent = layer->dataProvider()->polygonBounds();
91
93
94 mReadyToCompose = false;
95
96 mPreparationTime = timer.elapsed();
97}
98
100{
101 QgsScopedThreadName threadName( QStringLiteral( "render:%1" ).arg( mLayerName ) );
102
103 std::unique_ptr< QgsScopedRuntimeProfile > profile;
104 if ( mEnableProfile )
105 {
106 profile = std::make_unique< QgsScopedRuntimeProfile >( mLayerName, QStringLiteral( "rendering" ), layerId() );
107 if ( mPreparationTime > 0 )
108 QgsApplication::profiler()->record( QObject::tr( "Create renderer" ), mPreparationTime / 1000.0, QStringLiteral( "rendering" ) );
109 }
110
111 std::unique_ptr< QgsScopedRuntimeProfile > preparingProfile;
112 if ( mEnableProfile )
113 {
114 preparingProfile = std::make_unique< QgsScopedRuntimeProfile >( QObject::tr( "Preparing render" ), QStringLiteral( "rendering" ) );
115 }
116
117 QgsPointCloudRenderContext context( *renderContext(), mScale, mOffset, mZScale, mZOffset, mFeedback.get() );
118
119 // Set up the render configuration options
120 QPainter *painter = context.renderContext().painter();
121
122 QgsScopedQPainterState painterState( painter );
123 context.renderContext().setPainterFlagsUsingContext( painter );
124
125 if ( !mClippingRegions.empty() )
126 {
127 bool needsPainterClipPath = false;
128 const QPainterPath path = QgsMapClippingUtils::calculatePainterClipRegion( mClippingRegions, *renderContext(), Qgis::LayerType::VectorTile, needsPainterClipPath );
129 if ( needsPainterClipPath )
130 renderContext()->painter()->setClipPath( path, Qt::IntersectClip );
131 }
132
133 if ( mRenderer->type() == QLatin1String( "extent" ) )
134 {
135 // special case for extent only renderer!
136 mRenderer->startRender( context );
137 static_cast< QgsPointCloudExtentRenderer * >( mRenderer.get() )->renderExtent( mCloudExtent, context );
138 mRenderer->stopRender( context );
139 mReadyToCompose = true;
140 return true;
141 }
142
143 if ( mSubIndexes.isEmpty() && ( !mIndex || !mIndex.isValid() ) )
144 {
145 mReadyToCompose = true;
146 return false;
147 }
148
149 // if the previous layer render was relatively quick (e.g. less than 3 seconds), the we show any previously
150 // cached version of the layer during rendering instead of the usual progressive updates
151 if ( mRenderTimeHint > 0 && mRenderTimeHint <= MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE )
152 {
153 mBlockRenderUpdates = true;
154 mElapsedTimer.start();
155 }
156
157 mRenderer->startRender( context );
158
159 mAttributes.push_back( QgsPointCloudAttribute( QStringLiteral( "X" ), QgsPointCloudAttribute::Int32 ) );
160 mAttributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Y" ), QgsPointCloudAttribute::Int32 ) );
161
162 if ( !context.renderContext().zRange().isInfinite() ||
163 mRenderer->drawOrder2d() == Qgis::PointCloudDrawOrder::BottomToTop ||
164 mRenderer->drawOrder2d() == Qgis::PointCloudDrawOrder::TopToBottom ||
165 renderContext()->elevationMap() )
166 mAttributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Z" ), QgsPointCloudAttribute::Int32 ) );
167
168 // collect attributes required by renderer
169 QSet< QString > rendererAttributes = mRenderer->usedAttributes( context );
170
171
172 for ( const QString &attribute : std::as_const( rendererAttributes ) )
173 {
174 if ( mAttributes.indexOf( attribute ) >= 0 )
175 continue; // don't re-add attributes we are already going to fetch
176
177 const int layerIndex = mLayerAttributes.indexOf( attribute );
178 if ( layerIndex < 0 )
179 {
180 QgsMessageLog::logMessage( QObject::tr( "Required attribute %1 not found in layer" ).arg( attribute ), QObject::tr( "Point Cloud" ) );
181 continue;
182 }
183
184 mAttributes.push_back( mLayerAttributes.at( layerIndex ) );
185 }
186
187 QgsRectangle renderExtent;
188 try
189 {
191 }
192 catch ( QgsCsException & )
193 {
194 QgsDebugError( QStringLiteral( "Transformation of extent failed!" ) );
195 }
196
197 preparingProfile.reset();
198 std::unique_ptr< QgsScopedRuntimeProfile > renderingProfile;
199 if ( mEnableProfile )
200 {
201 renderingProfile = std::make_unique< QgsScopedRuntimeProfile >( QObject::tr( "Rendering" ), QStringLiteral( "rendering" ) );
202 }
203
204 bool canceled = false;
205 if ( mSubIndexes.isEmpty() )
206 {
207 canceled = !renderIndex( mIndex );
208 }
209 else if ( mIsVpc )
210 {
211 QVector< QgsPointCloudSubIndex > visibleIndexes;
212 for ( const QgsPointCloudSubIndex &si : mSubIndexes )
213 {
214 if ( renderExtent.intersects( si.extent() ) )
215 {
216 visibleIndexes.append( si );
217 }
218 }
219 const bool zoomedOut = renderExtent.width() > mAverageSubIndexWidth ||
220 renderExtent.height() > mAverageSubIndexHeight;
221 // if the overview of virtual point cloud exists, and we are zoomed out, we render just overview
222 if ( mOverviewIndex && mOverviewIndex->isValid() && zoomedOut &&
223 mRenderer->zoomOutBehavior() == Qgis::PointCloudZoomOutRenderBehavior::RenderOverview )
224 {
225 renderIndex( *mOverviewIndex );
226 }
227 else
228 {
229 // if the overview of virtual point cloud exists, and we are zoomed out, but we want both overview and extents,
230 // we render overview
231 if ( mOverviewIndex && mOverviewIndex->isValid() && zoomedOut &&
233 {
234 renderIndex( *mOverviewIndex );
235 }
236 mSubIndexExtentRenderer->startRender( context );
237 for ( const QgsPointCloudSubIndex &si : visibleIndexes )
238 {
239 if ( canceled )
240 break;
241
242 QgsPointCloudIndex pc = si.index();
243 // if the index of point cloud is invalid, or we are zoomed out and want extents, we render the point cloud extent
244 if ( !pc || !pc.isValid() || ( ( mRenderer->zoomOutBehavior() == Qgis::PointCloudZoomOutRenderBehavior::RenderExtents || mRenderer->zoomOutBehavior() == Qgis::PointCloudZoomOutRenderBehavior::RenderOverviewAndExtents ) &&
245 zoomedOut ) )
246 {
247 mSubIndexExtentRenderer->renderExtent( si.polygonBounds(), context );
248 if ( mSubIndexExtentRenderer->showLabels() )
249 {
250 mSubIndexExtentRenderer->renderLabel(
251 context.renderContext().mapToPixel().transformBounds( si.extent().toRectF() ),
252 si.uri().section( "/", -1 ).section( ".", 0, 0 ),
253 context );
254 }
255 }
256 // else we just render the visible point cloud
257 else
258 {
259 canceled = !renderIndex( pc );
260 }
261 }
262 mSubIndexExtentRenderer->stopRender( context );
263 }
264 }
265
266 mRenderer->stopRender( context );
267 mReadyToCompose = true;
268 return !canceled;
269}
270
271bool QgsPointCloudLayerRenderer::renderIndex( QgsPointCloudIndex &pc )
272{
274 pc.scale(),
275 pc.offset(),
276 mZScale,
277 mZOffset,
278 mFeedback.get() );
279
280
281#ifdef QGISDEBUG
282 QElapsedTimer t;
283 t.start();
284#endif
285
286 const QgsPointCloudNodeId root = pc.root();
287
288 const double maximumError = context.renderContext().convertToPainterUnits( mRenderer->maximumScreenError(), mRenderer->maximumScreenErrorUnit() );// in pixels
289
290 const QgsPointCloudNode rootNode = pc.getNode( root );
291 const QgsRectangle rootNodeExtentLayerCoords = pc.extent();
292 QgsRectangle rootNodeExtentMapCoords;
293 if ( !context.renderContext().coordinateTransform().isShortCircuited() )
294 {
295 try
296 {
297 QgsCoordinateTransform extentTransform = context.renderContext().coordinateTransform();
298 extentTransform.setBallparkTransformsAreAppropriate( true );
299 rootNodeExtentMapCoords = extentTransform.transformBoundingBox( rootNodeExtentLayerCoords );
300 }
301 catch ( QgsCsException & )
302 {
303 QgsDebugError( QStringLiteral( "Could not transform node extent to map CRS" ) );
304 rootNodeExtentMapCoords = rootNodeExtentLayerCoords;
305 }
306 }
307 else
308 {
309 rootNodeExtentMapCoords = rootNodeExtentLayerCoords;
310 }
311
312 const double rootErrorInMapCoordinates = rootNodeExtentMapCoords.width() / pc.span(); // in map coords
313
314 double mapUnitsPerPixel = context.renderContext().mapToPixel().mapUnitsPerPixel();
315 if ( ( rootErrorInMapCoordinates < 0.0 ) || ( mapUnitsPerPixel < 0.0 ) || ( maximumError < 0.0 ) )
316 {
317 QgsDebugError( QStringLiteral( "invalid screen error" ) );
318 return false;
319 }
320 double rootErrorPixels = rootErrorInMapCoordinates / mapUnitsPerPixel; // in pixels
321 const QVector<QgsPointCloudNodeId> nodes = traverseTree( pc, context.renderContext(), pc.root(), maximumError, rootErrorPixels );
322
323 QgsPointCloudRequest request;
324 request.setAttributes( mAttributes );
325
326 // drawing
327 int nodesDrawn = 0;
328 bool canceled = false;
329
330 Qgis::PointCloudDrawOrder drawOrder = mRenderer->drawOrder2d();
331 if ( mRenderer->renderAsTriangles() )
332 {
333 // Ordered rendering is ignored when drawing as surface, because all points are used for triangulation.
334 // We would need to have a way to detect if a point is occluded by some other points, which may be costly.
336 }
337
338 switch ( drawOrder )
339 {
342 {
343 nodesDrawn += renderNodesSorted( nodes, pc, context, request, canceled, mRenderer->drawOrder2d() );
344 break;
345 }
347 {
348 switch ( pc.accessType() )
349 {
351 {
352 nodesDrawn += renderNodesSync( nodes, pc, context, request, canceled );
353 break;
354 }
356 {
357 nodesDrawn += renderNodesAsync( nodes, pc, context, request, canceled );
358 break;
359 }
360 }
361 }
362 }
363
364#ifdef QGISDEBUG
365 QgsDebugMsgLevel( QStringLiteral( "totals: %1 nodes | %2 points | %3ms" ).arg( nodesDrawn )
366 .arg( context.pointsRendered() )
367 .arg( t.elapsed() ), 2 );
368#else
369 ( void )nodesDrawn;
370#endif
371
372 return !canceled;
373}
374
375int QgsPointCloudLayerRenderer::renderNodesSync( const QVector<QgsPointCloudNodeId> &nodes, QgsPointCloudIndex &pc, QgsPointCloudRenderContext &context, QgsPointCloudRequest &request, bool &canceled )
376{
377 QPainter *finalPainter = context.renderContext().painter();
378 if ( mRenderer->renderAsTriangles() && context.renderContext().previewRenderPainter() )
379 {
380 // swap out the destination painter for the preview render painter to render points
381 // until the actual triangles are ready to be rendered
383 }
384
385 int nodesDrawn = 0;
386 for ( const QgsPointCloudNodeId &n : nodes )
387 {
388 if ( context.renderContext().renderingStopped() )
389 {
390 QgsDebugMsgLevel( QStringLiteral( "canceled" ), 2 );
391 canceled = true;
392 break;
393 }
394 std::unique_ptr<QgsPointCloudBlock> block( pc.nodeData( n, request ) );
395
396 if ( !block )
397 continue;
398
399 QgsVector3D contextScale = context.scale();
400 QgsVector3D contextOffset = context.offset();
401
402 context.setScale( block->scale() );
403 context.setOffset( block->offset() );
404
405 context.setAttributes( block->attributes() );
406
407 mRenderer->renderBlock( block.get(), context );
408
409 context.setScale( contextScale );
410 context.setOffset( contextOffset );
411
412 ++nodesDrawn;
413
414 // as soon as first block is rendered, we can start showing layer updates.
415 // but if we are blocking render updates (so that a previously cached image is being shown), we wait
416 // at most e.g. 3 seconds before we start forcing progressive updates.
417 if ( !mBlockRenderUpdates || mElapsedTimer.elapsed() > MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE )
418 {
419 mReadyToCompose = true;
420 }
421 }
422
423 if ( mRenderer->renderAsTriangles() )
424 {
425 // Switch back from the preview painter to the destination painter to render the triangles
426 context.renderContext().setPainter( finalPainter );
427 renderTriangulatedSurface( context );
428 }
429
430 return nodesDrawn;
431}
432
433int QgsPointCloudLayerRenderer::renderNodesAsync( const QVector<QgsPointCloudNodeId> &nodes, QgsPointCloudIndex &pc, QgsPointCloudRenderContext &context, QgsPointCloudRequest &request, bool &canceled )
434{
435 if ( nodes.isEmpty() )
436 return 0;
437
438 if ( context.feedback() && context.feedback()->isCanceled() )
439 return 0;
440
441 QPainter *finalPainter = context.renderContext().painter();
442 if ( mRenderer->renderAsTriangles() && context.renderContext().previewRenderPainter() )
443 {
444 // swap out the destination painter for the preview render painter to render points
445 // until the actual triangles are ready to be rendered
447 }
448
449 int nodesDrawn = 0;
450
451 // Async loading of nodes
452 QVector<QgsPointCloudBlockRequest *> blockRequests;
453 QEventLoop loop;
454 if ( context.feedback() )
455 QObject::connect( context.feedback(), &QgsFeedback::canceled, &loop, &QEventLoop::quit );
456
457 for ( int i = 0; i < nodes.size(); ++i )
458 {
459 const QgsPointCloudNodeId &n = nodes[i];
460 const QString nStr = n.toString();
461 QgsPointCloudBlockRequest *blockRequest = pc.asyncNodeData( n, request );
462 blockRequests.append( blockRequest );
463 QObject::connect( blockRequest, &QgsPointCloudBlockRequest::finished, &loop,
464 [ this, &canceled, &nodesDrawn, &loop, &blockRequests, &context, nStr, blockRequest ]()
465 {
466 blockRequests.removeOne( blockRequest );
467
468 // If all blocks are loaded, exit the event loop
469 if ( blockRequests.isEmpty() )
470 loop.exit();
471
472 std::unique_ptr<QgsPointCloudBlock> block( blockRequest->takeBlock() );
473
474 blockRequest->deleteLater();
475
476 if ( context.feedback() && context.feedback()->isCanceled() )
477 {
478 canceled = true;
479 return;
480 }
481
482 if ( !block )
483 {
484 QgsDebugError( QStringLiteral( "Unable to load node %1, error: %2" ).arg( nStr, blockRequest->errorStr() ) );
485 return;
486 }
487
488 QgsVector3D contextScale = context.scale();
489 QgsVector3D contextOffset = context.offset();
490
491 context.setScale( block->scale() );
492 context.setOffset( block->offset() );
493 context.setAttributes( block->attributes() );
494
495 mRenderer->renderBlock( block.get(), context );
496
497 context.setScale( contextScale );
498 context.setOffset( contextOffset );
499
500 ++nodesDrawn;
501
502 // as soon as first block is rendered, we can start showing layer updates.
503 // but if we are blocking render updates (so that a previously cached image is being shown), we wait
504 // at most e.g. 3 seconds before we start forcing progressive updates.
505 if ( !mBlockRenderUpdates || mElapsedTimer.elapsed() > MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE )
506 {
507 mReadyToCompose = true;
508 }
509
510 } );
511 }
512
513 // Wait for all point cloud nodes to finish loading
514 if ( !blockRequests.isEmpty() )
515 loop.exec();
516
517 // Rendering may have got canceled and the event loop exited before finished()
518 // was called for all blocks, so let's clean up anything that is left
519 for ( QgsPointCloudBlockRequest *blockRequest : std::as_const( blockRequests ) )
520 {
521 std::unique_ptr<QgsPointCloudBlock> block = blockRequest->takeBlock();
522 block.reset();
523
524 blockRequest->deleteLater();
525 }
526
527 if ( mRenderer->renderAsTriangles() )
528 {
529 // Switch back from the preview painter to the destination painter to render the triangles
530 context.renderContext().setPainter( finalPainter );
531 renderTriangulatedSurface( context );
532 }
533
534 return nodesDrawn;
535}
536
537int QgsPointCloudLayerRenderer::renderNodesSorted( const QVector<QgsPointCloudNodeId> &nodes, QgsPointCloudIndex &pc, QgsPointCloudRenderContext &context, QgsPointCloudRequest &request, bool &canceled, Qgis::PointCloudDrawOrder order )
538{
539 int blockCount = 0;
540 int pointCount = 0;
541
542 QgsVector3D blockScale;
543 QgsVector3D blockOffset;
544 QgsPointCloudAttributeCollection blockAttributes;
545 int recordSize = 0;
546
547 // We'll collect byte array data from all blocks
548 QByteArray allByteArrays;
549 // And pairs of byte array start positions paired with their Z values for sorting
550 QVector<QPair<int, double>> allPairs;
551
552 for ( const QgsPointCloudNodeId &n : nodes )
553 {
554 if ( context.renderContext().renderingStopped() )
555 {
556 QgsDebugMsgLevel( QStringLiteral( "canceled" ), 2 );
557 canceled = true;
558 break;
559 }
560 std::unique_ptr<QgsPointCloudBlock> block( pc.nodeData( n, request ) );
561
562 if ( !block )
563 continue;
564
565 // Individual nodes may have different offset values than the root node
566 // we'll calculate the differences and translate x,y,z values to use the root node's offset
567 QgsVector3D offsetDifference = QgsVector3D( 0, 0, 0 );
568 if ( blockCount == 0 )
569 {
570 blockScale = block->scale();
571 blockOffset = block->offset();
572 blockAttributes = block->attributes();
573 }
574 else
575 {
576 offsetDifference = blockOffset - block->offset();
577 }
578
579 const char *ptr = block->data();
580
581 context.setScale( block->scale() );
582 context.setOffset( block->offset() );
583 context.setAttributes( block->attributes() );
584
585 recordSize = context.pointRecordSize();
586
587 for ( int i = 0; i < block->pointCount(); ++i )
588 {
589 allByteArrays.append( ptr + i * recordSize, recordSize );
590
591 // Calculate the translated values only for axes that have a different offset
592 if ( offsetDifference.x() != 0 )
593 {
594 qint32 ix = *reinterpret_cast< const qint32 * >( ptr + i * recordSize + context.xOffset() );
595 ix -= std::lround( offsetDifference.x() / context.scale().x() );
596 const char *xPtr = reinterpret_cast< const char * >( &ix );
597 allByteArrays.replace( pointCount * recordSize + context.xOffset(), 4, QByteArray( xPtr, 4 ) );
598 }
599 if ( offsetDifference.y() != 0 )
600 {
601 qint32 iy = *reinterpret_cast< const qint32 * >( ptr + i * recordSize + context.yOffset() );
602 iy -= std::lround( offsetDifference.y() / context.scale().y() );
603 const char *yPtr = reinterpret_cast< const char * >( &iy );
604 allByteArrays.replace( pointCount * recordSize + context.yOffset(), 4, QByteArray( yPtr, 4 ) );
605 }
606 // We need the Z value regardless of the node's offset
607 qint32 iz = *reinterpret_cast< const qint32 * >( ptr + i * recordSize + context.zOffset() );
608 if ( offsetDifference.z() != 0 )
609 {
610 iz -= std::lround( offsetDifference.z() / context.scale().z() );
611 const char *zPtr = reinterpret_cast< const char * >( &iz );
612 allByteArrays.replace( pointCount * recordSize + context.zOffset(), 4, QByteArray( zPtr, 4 ) );
613 }
614 allPairs.append( qMakePair( pointCount, double( iz ) + block->offset().z() ) );
615
616 ++pointCount;
617 }
618 ++blockCount;
619 }
620
621 if ( pointCount == 0 )
622 return 0;
623
624 switch ( order )
625 {
627 std::sort( allPairs.begin(), allPairs.end(), []( QPair<int, double> a, QPair<int, double> b ) { return a.second < b.second; } );
628 break;
630 std::sort( allPairs.begin(), allPairs.end(), []( QPair<int, double> a, QPair<int, double> b ) { return a.second > b.second; } );
631 break;
633 break;
634 }
635
636 // Now we can reconstruct a byte array sorted by Z value
637 QByteArray sortedByteArray;
638 sortedByteArray.reserve( allPairs.size() );
639 for ( QPair<int, double> pair : allPairs )
640 sortedByteArray.append( allByteArrays.mid( pair.first * recordSize, recordSize ) );
641
642 std::unique_ptr<QgsPointCloudBlock> bigBlock { new QgsPointCloudBlock( pointCount,
643 blockAttributes,
644 sortedByteArray,
645 blockScale,
646 blockOffset ) };
647
648 QgsVector3D contextScale = context.scale();
649 QgsVector3D contextOffset = context.offset();
650
651 context.setScale( bigBlock->scale() );
652 context.setOffset( bigBlock->offset() );
653 context.setAttributes( bigBlock->attributes() );
654
655 mRenderer->renderBlock( bigBlock.get(), context );
656
657 context.setScale( contextScale );
658 context.setOffset( contextOffset );
659
660 return blockCount;
661}
662
663inline bool isEdgeTooLong( const QPointF &p1, const QPointF &p2, float length )
664{
665 QPointF p = p1 - p2;
666 return p.x() * p.x() + p.y() * p.y() > length;
667}
668
669static void renderTriangle( QImage &img, QPointF *pts, QRgb c0, QRgb c1, QRgb c2, float horizontalFilter, float *elev, QgsElevationMap *elevationMap )
670{
671 if ( horizontalFilter > 0 )
672 {
673 float filterThreshold2 = horizontalFilter * horizontalFilter;
674 if ( isEdgeTooLong( pts[0], pts[1], filterThreshold2 ) ||
675 isEdgeTooLong( pts[1], pts[2], filterThreshold2 ) ||
676 isEdgeTooLong( pts[2], pts[0], filterThreshold2 ) )
677 return;
678 }
679
680 QgsRectangle screenBBox = QgsMeshLayerUtils::triangleBoundingBox( pts[0], pts[1], pts[2] );
681
682 QSize outputSize = img.size();
683
684 int topLim = std::max( int( screenBBox.yMinimum() ), 0 );
685 int bottomLim = std::min( int( screenBBox.yMaximum() ), outputSize.height() - 1 );
686 int leftLim = std::max( int( screenBBox.xMinimum() ), 0 );
687 int rightLim = std::min( int( screenBBox.xMaximum() ), outputSize.width() - 1 );
688
689 int red0 = qRed( c0 ), green0 = qGreen( c0 ), blue0 = qBlue( c0 );
690 int red1 = qRed( c1 ), green1 = qGreen( c1 ), blue1 = qBlue( c1 );
691 int red2 = qRed( c2 ), green2 = qGreen( c2 ), blue2 = qBlue( c2 );
692
693 QRgb *elevData = elevationMap ? elevationMap->rawElevationImageData() : nullptr;
694
695 for ( int j = topLim; j <= bottomLim; j++ )
696 {
697 QRgb *scanLine = ( QRgb * ) img.scanLine( j );
698 QRgb *elevScanLine = elevData ? elevData + static_cast<size_t>( outputSize.width() * j ) : nullptr;
699 for ( int k = leftLim; k <= rightLim; k++ )
700 {
701 QPointF pt( k, j );
702 double lam1, lam2, lam3;
703 if ( !QgsMeshLayerUtils::calculateBarycentricCoordinates( pts[0], pts[1], pts[2], pt, lam3, lam2, lam1 ) )
704 continue;
705
706 // interpolate color
707 int r = static_cast<int>( red0 * lam1 + red1 * lam2 + red2 * lam3 );
708 int g = static_cast<int>( green0 * lam1 + green1 * lam2 + green2 * lam3 );
709 int b = static_cast<int>( blue0 * lam1 + blue1 * lam2 + blue2 * lam3 );
710 scanLine[k] = qRgb( r, g, b );
711
712 // interpolate elevation - in case we are doing global map shading
713 if ( elevScanLine )
714 {
715 float z = static_cast<float>( elev[0] * lam1 + elev[1] * lam2 + elev[2] * lam3 );
716 elevScanLine[k] = QgsElevationMap::encodeElevation( z );
717 }
718 }
719 }
720}
721
722void QgsPointCloudLayerRenderer::renderTriangulatedSurface( QgsPointCloudRenderContext &context )
723{
724 const QgsPointCloudRenderContext::TriangulationData &triangulation = context.triangulationData();
725 const std::vector<double> &points = triangulation.points;
726
727 // Delaunator would crash if it gets less than three points
728 if ( points.size() < 3 )
729 {
730 QgsDebugMsgLevel( QStringLiteral( "Need at least 3 points to triangulate" ), 4 );
731 return;
732 }
733
734 std::unique_ptr<delaunator::Delaunator> delaunator;
735 try
736 {
737 delaunator = std::make_unique<delaunator::Delaunator>( points );
738 }
739 catch ( std::exception & )
740 {
741 // something went wrong, better to retrieve initial state
742 QgsDebugMsgLevel( QStringLiteral( "Error with triangulation" ), 4 );
743 return;
744 }
745
746 float horizontalFilter = 0;
747 if ( mRenderer->horizontalTriangleFilter() )
748 {
749 horizontalFilter = static_cast<float>( renderContext()->convertToPainterUnits(
750 mRenderer->horizontalTriangleFilterThreshold(), mRenderer->horizontalTriangleFilterUnit() ) );
751 }
752
753 QImage img( context.renderContext().deviceOutputSize(), QImage::Format_ARGB32_Premultiplied );
754 img.setDevicePixelRatio( context.renderContext().devicePixelRatio() );
755 img.fill( 0 );
756
757 const std::vector<size_t> &triangleIndexes = delaunator->triangles;
758 QPainter *painter = context.renderContext().painter();
759 QgsElevationMap *elevationMap = context.renderContext().elevationMap();
760 QPointF triangle[3];
761 float elev[3] {0, 0, 0};
762 for ( size_t i = 0; i < triangleIndexes.size(); i += 3 )
763 {
764 size_t v0 = triangleIndexes[i], v1 = triangleIndexes[i + 1], v2 = triangleIndexes[i + 2];
765 triangle[0].rx() = points[v0 * 2];
766 triangle[0].ry() = points[v0 * 2 + 1];
767 triangle[1].rx() = points[v1 * 2];
768 triangle[1].ry() = points[v1 * 2 + 1];
769 triangle[2].rx() = points[v2 * 2];
770 triangle[2].ry() = points[v2 * 2 + 1];
771
772 if ( elevationMap )
773 {
774 elev[0] = triangulation.elevations[v0];
775 elev[1] = triangulation.elevations[v1];
776 elev[2] = triangulation.elevations[v2];
777 }
778
779 QRgb c0 = triangulation.colors[v0], c1 = triangulation.colors[v1], c2 = triangulation.colors[v2];
780 renderTriangle( img, triangle, c0, c1, c2, horizontalFilter, elev, elevationMap );
781 }
782
783 painter->drawImage( 0, 0, img );
784}
785
787{
788 // when rendering as triangles we still want to show temporary incremental renders as points until
789 // the final triangulated surface is ready, which may be slow
790 // So we request here a preview render image for the temporary incremental updates:
791 if ( mRenderer->renderAsTriangles() )
793
795}
796
798{
799 // unless we are using the extent only renderer, point cloud layers should always be rasterized -- we don't want to export points as vectors
800 // to formats like PDF!
801 return mRenderer ? mRenderer->type() != QLatin1String( "extent" ) : false;
802}
803
805{
806 mRenderTimeHint = time;
807}
808
809QVector<QgsPointCloudNodeId> QgsPointCloudLayerRenderer::traverseTree( const QgsPointCloudIndex &pc, const QgsRenderContext &context, QgsPointCloudNodeId n, double maxErrorPixels, double nodeErrorPixels )
810{
811 QVector<QgsPointCloudNodeId> nodes;
812
813 if ( context.renderingStopped() )
814 {
815 QgsDebugMsgLevel( QStringLiteral( "canceled" ), 2 );
816 return nodes;
817 }
818
819 QgsPointCloudNode node = pc.getNode( n );
820 QgsBox3D nodeExtent = node.bounds();
821
822 if ( !context.extent().intersects( nodeExtent.toRectangle() ) )
823 return nodes;
824
825 const QgsDoubleRange nodeZRange( nodeExtent.zMinimum(), nodeExtent.zMaximum() );
826 const QgsDoubleRange adjustedNodeZRange = QgsDoubleRange( nodeZRange.lower() + mZOffset, nodeZRange.upper() + mZOffset );
827 if ( !context.zRange().isInfinite() && !context.zRange().overlaps( adjustedNodeZRange ) )
828 return nodes;
829
830 if ( node.pointCount() > 0 )
831 nodes.append( n );
832
833 double childrenErrorPixels = nodeErrorPixels / 2.0;
834 if ( childrenErrorPixels < maxErrorPixels )
835 return nodes;
836
837 for ( const QgsPointCloudNodeId &nn : node.children() )
838 {
839 nodes += traverseTree( pc, context, nn, maxErrorPixels, childrenErrorPixels );
840 }
841
842 return nodes;
843}
844
Provides global constants and enumerations for use throughout the application.
Definition qgis.h:56
QFlags< MapLayerRendererFlag > MapLayerRendererFlags
Flags which control how map layer renderers behave.
Definition qgis.h:2796
PointCloudDrawOrder
Pointcloud rendering order for 2d views.
Definition qgis.h:4258
@ BottomToTop
Draw points with larger Z values last.
Definition qgis.h:4260
@ Default
Draw points in the order they are stored.
Definition qgis.h:4259
@ TopToBottom
Draw points with larger Z values first.
Definition qgis.h:4261
@ RenderOverviewAndExtents
Render point cloud extents over overview point cloud.
Definition qgis.h:6088
@ RenderExtents
Render only point cloud extents when zoomed out.
Definition qgis.h:6086
@ RenderOverview
Render overview point cloud when zoomed out.
Definition qgis.h:6087
@ VectorTile
Vector tile layer. Added in QGIS 3.14.
Definition qgis.h:195
@ RenderPartialOutputOverPreviousCachedImage
When rendering temporary in-progress preview renders, these preview renders can be drawn over any pre...
Definition qgis.h:2786
@ RenderPartialOutputs
The renderer benefits from rendering temporary in-progress preview renders. These are temporary resul...
Definition qgis.h:2785
@ Local
Local means the source is a local file on the machine.
Definition qgis.h:6075
@ Remote
Remote means it's loaded through a protocol like HTTP.
Definition qgis.h:6076
@ Reverse
Reverse/inverse transform (from destination to source).
Definition qgis.h:2673
static QgsRuntimeProfiler * profiler()
Returns the application runtime profiler.
double zMaximum() const
Returns the maximum z value.
Definition qgsbox3d.h:258
QgsRectangle toRectangle() const
Converts the box to a 2D rectangle.
Definition qgsbox3d.h:378
double zMinimum() const
Returns the minimum z value.
Definition qgsbox3d.h:251
Handles coordinate transforms between two coordinate systems.
void setBallparkTransformsAreAppropriate(bool appropriate)
Sets whether approximate "ballpark" results are appropriate for this coordinate transform.
QgsRectangle transformBoundingBox(const QgsRectangle &rectangle, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward, bool handle180Crossover=false) const
Transforms a rectangle from the source CRS to the destination CRS.
Custom exception class for Coordinate Reference System related exceptions.
bool isInfinite() const
Returns true if the range consists of all possible values.
Definition qgsrange.h:287
Stores a digital elevation model in a raster image which may get updated as a part of the map layer r...
static QRgb encodeElevation(float z)
Converts elevation value to an actual color.
QRgb * rawElevationImageData()
Returns pointer to the actual elevation image data.
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition qgsfeedback.h:44
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition qgsfeedback.h:53
void canceled()
Internal routines can connect to this signal if they use event loop.
static QPainterPath calculatePainterClipRegion(const QList< QgsMapClippingRegion > &regions, const QgsRenderContext &context, Qgis::LayerType layerType, bool &shouldClip)
Returns a QPainterPath representing the intersection of clipping regions from context which should be...
static QList< QgsMapClippingRegion > collectClippingRegionsForLayer(const QgsRenderContext &context, const QgsMapLayer *layer)
Collects the list of map clipping regions from a context which apply to a map layer.
bool mReadyToCompose
The flag must be set to false in renderer's constructor if wants to use the smarter map redraws funct...
static constexpr int MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE
Maximum time (in ms) to allow display of a previously cached preview image while rendering layers,...
QString layerId() const
Gets access to the ID of the layer rendered by this class.
QgsRenderContext * renderContext()
Returns the render context associated with the renderer.
QgsMapLayerRenderer(const QString &layerID, QgsRenderContext *context=nullptr)
Constructor for QgsMapLayerRenderer, with the associated layerID and render context.
QRectF transformBounds(const QRectF &bounds) const
Transforms a bounding box from map coordinates to device coordinates.
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true, const char *file=__builtin_FILE(), const char *function=__builtin_FUNCTION(), int line=__builtin_LINE())
Adds a message to the log instance (and creates it if necessary).
Attribute for point cloud data pair of name and size in bytes.
QString errorStr() const
Returns the error message string of the request.
void finished()
Emitted when the request processing has finished.
std::unique_ptr< QgsPointCloudBlock > takeBlock()
Returns the requested block.
virtual QgsGeometry polygonBounds() const
Returns the polygon bounds of the layer.
A renderer for 2d visualisation of point clouds which shows the dataset's extents using a fill symbol...
void renderExtent(const QgsGeometry &extent, QgsPointCloudRenderContext &context)
Renders a polygon extent geometry to the specified render context.
Smart pointer for QgsAbstractPointCloudIndex.
int span() const
Returns the number of points in one direction in a single node.
QgsVector3D offset() const
Returns offset of data from CRS.
QgsVector3D scale() const
Returns scale of data relative to CRS.
QgsPointCloudBlockRequest * asyncNodeData(const QgsPointCloudNodeId &n, const QgsPointCloudRequest &request)
Returns a handle responsible for loading a node data block.
bool isValid() const
Returns whether index is loaded and valid.
QgsRectangle extent() const
Returns extent of the data.
std::unique_ptr< QgsPointCloudBlock > nodeData(const QgsPointCloudNodeId &n, const QgsPointCloudRequest &request)
Returns node data block.
QgsPointCloudNodeId root() const
Returns root node of the index.
QgsPointCloudNode getNode(const QgsPointCloudNodeId &id) const
Returns object for a given node.
Qgis::PointCloudAccessType accessType() const
Returns the access type of the data If the access type is Remote, data will be fetched from an HTTP s...
Point cloud layer specific subclass of QgsMapLayerElevationProperties.
~QgsPointCloudLayerRenderer() override
bool forceRasterRender() const override
Returns true if the renderer must be rendered to a raster paint device (e.g.
QgsPointCloudLayerRenderer(QgsPointCloudLayer *layer, QgsRenderContext &context)
Ctor.
void setLayerRenderingTimeHint(int time) override
Sets approximate render time (in ms) for the layer to render.
bool render() override
Do the rendering (based on data stored in the class).
Qgis::MapLayerRendererFlags flags() const override
Returns flags which control how the map layer rendering behaves.
Represents a map layer supporting display of point clouds.
QgsMapLayerElevationProperties * elevationProperties() override
Returns the layer's elevation properties.
QgsPointCloudRenderer * renderer()
Returns the 2D renderer for the point cloud.
QgsPointCloudIndex index() const
Returns the point cloud index associated with the layer.
QgsPointCloudDataProvider * dataProvider() override
Returns the layer's data provider, it may be nullptr.
Represents an indexed point cloud node's position in octree.
QString toString() const
Encode node to string.
Keeps metadata for an indexed point cloud node.
QList< QgsPointCloudNodeId > children() const
Returns IDs of child nodes.
qint64 pointCount() const
Returns number of points contained in node data.
QgsBox3D bounds() const
Returns node's bounding cube in CRS coords.
Encapsulates the render context for a 2D point cloud rendering operation.
int yOffset() const
Returns the offset for the y value in a point record.
QgsVector3D offset() const
Returns the offset of the layer's int32 coordinates compared to CRS coords.
QgsRenderContext & renderContext()
Returns a reference to the context's render context.
void setOffset(const QgsVector3D &offset)
Sets the offset of the layer's int32 coordinates compared to CRS coords.
void setScale(const QgsVector3D &scale)
Sets the scale of the layer's int32 coordinates compared to CRS coords.
int pointRecordSize() const
Returns the size of a single point record.
int xOffset() const
Returns the offset for the x value in a point record.
QgsVector3D scale() const
Returns the scale of the layer's int32 coordinates compared to CRS coords.
TriangulationData & triangulationData()
Returns reference to the triangulation data structure (only used when rendering as triangles is enabl...
int zOffset() const
Returns the offset for the y value in a point record.
QgsFeedback * feedback() const
Returns the feedback object used to cancel rendering.
void setAttributes(const QgsPointCloudAttributeCollection &attributes)
Sets the attributes associated with the rendered block.
virtual QgsPointCloudRenderer * clone() const =0
Create a deep copy of this renderer.
Point cloud data request.
void setAttributes(const QgsPointCloudAttributeCollection &attributes)
Set attributes filter in the request.
bool overlaps(const QgsRange< T > &other) const
Returns true if this range overlaps another range.
Definition qgsrange.h:176
A rectangle specified with double values.
double xMinimum
double yMinimum
double xMaximum
bool intersects(const QgsRectangle &rect) const
Returns true when rectangle intersects with other rectangle.
double yMaximum
Contains information about the context of a rendering operation.
double convertToPainterUnits(double size, Qgis::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale(), Qgis::RenderSubcomponentProperty property=Qgis::RenderSubcomponentProperty::Generic) const
Converts a size from the specified units to painter units (pixels).
QPainter * painter()
Returns the destination QPainter for the render operation.
void setPainterFlagsUsingContext(QPainter *painter=nullptr) const
Sets relevant flags on a destination painter, using the flags and settings currently defined for the ...
QgsElevationMap * elevationMap() const
Returns the destination elevation map for the render operation.
const QgsRectangle & extent() const
When rendering a map layer, calling this method returns the "clipping" extent for the layer (in the l...
float devicePixelRatio() const
Returns the device pixel ratio.
const QgsMapToPixel & mapToPixel() const
Returns the context's map to pixel transform, which transforms between map coordinates and device coo...
void setPainter(QPainter *p)
Sets the destination QPainter for the render operation.
QgsDoubleRange zRange() const
Returns the range of z-values which should be rendered.
QSize deviceOutputSize() const
Returns the device output size of the render.
bool renderingStopped() const
Returns true if the rendering operation has been stopped and any ongoing rendering should be canceled...
QPainter * previewRenderPainter()
Returns the const destination QPainter for temporary in-progress preview renders.
QgsCoordinateTransform coordinateTransform() const
Returns the current coordinate transform for the context.
void record(const QString &name, double time, const QString &group="startup", const QString &id=QString())
Manually adds a profile event with the given name and total time (in seconds).
Scoped object for saving and restoring a QPainter object's state.
Scoped object for setting the current thread name.
double y() const
Returns Y coordinate.
Definition qgsvector3d.h:49
double z() const
Returns Z coordinate.
Definition qgsvector3d.h:51
double x() const
Returns X coordinate.
Definition qgsvector3d.h:47
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:61
#define QgsDebugError(str)
Definition qgslogger.h:57
bool isEdgeTooLong(const QPointF &p1, const QPointF &p2, float length)
std::vector< QRgb > colors
RGB color for each point.
std::vector< float > elevations
Z value for each point (only used when global map shading is enabled).
std::vector< double > points
X,Y for each point - kept in this structure so that we can use it without further conversions in Dela...