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