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