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