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