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