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