QGIS API Documentation 3.41.0-Master (25ec5511245)
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 IndexedPointCloudNode root = pc->root();
244
245 const double maximumError = context.renderContext().convertToPainterUnits( mRenderer->maximumScreenError(), mRenderer->maximumScreenErrorUnit() );// in pixels
246
247 const QgsRectangle rootNodeExtentLayerCoords = pc->nodeMapExtent( root );
248 QgsRectangle rootNodeExtentMapCoords;
249 if ( !context.renderContext().coordinateTransform().isShortCircuited() )
250 {
251 try
252 {
253 QgsCoordinateTransform extentTransform = context.renderContext().coordinateTransform();
254 extentTransform.setBallparkTransformsAreAppropriate( true );
255 rootNodeExtentMapCoords = extentTransform.transformBoundingBox( rootNodeExtentLayerCoords );
256 }
257 catch ( QgsCsException & )
258 {
259 QgsDebugError( QStringLiteral( "Could not transform node extent to map CRS" ) );
260 rootNodeExtentMapCoords = rootNodeExtentLayerCoords;
261 }
262 }
263 else
264 {
265 rootNodeExtentMapCoords = rootNodeExtentLayerCoords;
266 }
267
268 const double rootErrorInMapCoordinates = rootNodeExtentMapCoords.width() / pc->span(); // in map coords
269
270 double mapUnitsPerPixel = context.renderContext().mapToPixel().mapUnitsPerPixel();
271 if ( ( rootErrorInMapCoordinates < 0.0 ) || ( mapUnitsPerPixel < 0.0 ) || ( maximumError < 0.0 ) )
272 {
273 QgsDebugError( QStringLiteral( "invalid screen error" ) );
274 return false;
275 }
276 double rootErrorPixels = rootErrorInMapCoordinates / mapUnitsPerPixel; // in pixels
277 const QVector<IndexedPointCloudNode> nodes = traverseTree( pc, context.renderContext(), pc->root(), maximumError, rootErrorPixels );
278
279 QgsPointCloudRequest request;
280 request.setAttributes( mAttributes );
281
282 // drawing
283 int nodesDrawn = 0;
284 bool canceled = false;
285
286 Qgis::PointCloudDrawOrder drawOrder = mRenderer->drawOrder2d();
287 if ( mRenderer->renderAsTriangles() )
288 {
289 // Ordered rendering is ignored when drawing as surface, because all points are used for triangulation.
290 // We would need to have a way to detect if a point is occluded by some other points, which may be costly.
292 }
293
294 switch ( drawOrder )
295 {
298 {
299 nodesDrawn += renderNodesSorted( nodes, pc, context, request, canceled, mRenderer->drawOrder2d() );
300 break;
301 }
303 {
304 switch ( pc->accessType() )
305 {
307 {
308 nodesDrawn += renderNodesSync( nodes, pc, context, request, canceled );
309 break;
310 }
312 {
313 nodesDrawn += renderNodesAsync( nodes, pc, context, request, canceled );
314 break;
315 }
316 }
317 }
318 }
319
320#ifdef QGISDEBUG
321 QgsDebugMsgLevel( QStringLiteral( "totals: %1 nodes | %2 points | %3ms" ).arg( nodesDrawn )
322 .arg( context.pointsRendered() )
323 .arg( t.elapsed() ), 2 );
324#else
325 ( void )nodesDrawn;
326#endif
327
328 return !canceled;
329}
330
331int QgsPointCloudLayerRenderer::renderNodesSync( const QVector<IndexedPointCloudNode> &nodes, QgsPointCloudIndex *pc, QgsPointCloudRenderContext &context, QgsPointCloudRequest &request, bool &canceled )
332{
333 QPainter *finalPainter = context.renderContext().painter();
334 if ( mRenderer->renderAsTriangles() && context.renderContext().previewRenderPainter() )
335 {
336 // swap out the destination painter for the preview render painter to render points
337 // until the actual triangles are ready to be rendered
339 }
340
341 int nodesDrawn = 0;
342 for ( const IndexedPointCloudNode &n : nodes )
343 {
344 if ( context.renderContext().renderingStopped() )
345 {
346 QgsDebugMsgLevel( QStringLiteral( "canceled" ), 2 );
347 canceled = true;
348 break;
349 }
350 std::unique_ptr<QgsPointCloudBlock> block( pc->nodeData( n, request ) );
351
352 if ( !block )
353 continue;
354
355 QgsVector3D contextScale = context.scale();
356 QgsVector3D contextOffset = context.offset();
357
358 context.setScale( block->scale() );
359 context.setOffset( block->offset() );
360
361 context.setAttributes( block->attributes() );
362
363 mRenderer->renderBlock( block.get(), context );
364
365 context.setScale( contextScale );
366 context.setOffset( contextOffset );
367
368 ++nodesDrawn;
369
370 // as soon as first block is rendered, we can start showing layer updates.
371 // but if we are blocking render updates (so that a previously cached image is being shown), we wait
372 // at most e.g. 3 seconds before we start forcing progressive updates.
373 if ( !mBlockRenderUpdates || mElapsedTimer.elapsed() > MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE )
374 {
375 mReadyToCompose = true;
376 }
377 }
378
379 if ( mRenderer->renderAsTriangles() )
380 {
381 // Switch back from the preview painter to the destination painter to render the triangles
382 context.renderContext().setPainter( finalPainter );
383 renderTriangulatedSurface( context );
384 }
385
386 return nodesDrawn;
387}
388
389int QgsPointCloudLayerRenderer::renderNodesAsync( const QVector<IndexedPointCloudNode> &nodes, QgsPointCloudIndex *pc, QgsPointCloudRenderContext &context, QgsPointCloudRequest &request, bool &canceled )
390{
391 if ( context.feedback() && context.feedback()->isCanceled() )
392 return 0;
393
394 QPainter *finalPainter = context.renderContext().painter();
395 if ( mRenderer->renderAsTriangles() && context.renderContext().previewRenderPainter() )
396 {
397 // swap out the destination painter for the preview render painter to render points
398 // until the actual triangles are ready to be rendered
400 }
401
402 int nodesDrawn = 0;
403
404 // Async loading of nodes
405 QVector<QgsPointCloudBlockRequest *> blockRequests;
406 QEventLoop loop;
407 if ( context.feedback() )
408 QObject::connect( context.feedback(), &QgsFeedback::canceled, &loop, &QEventLoop::quit );
409
410 for ( int i = 0; i < nodes.size(); ++i )
411 {
412 const IndexedPointCloudNode &n = nodes[i];
413 const QString nStr = n.toString();
414 QgsPointCloudBlockRequest *blockRequest = pc->asyncNodeData( n, request );
415 blockRequests.append( blockRequest );
416 QObject::connect( blockRequest, &QgsPointCloudBlockRequest::finished, &loop,
417 [ this, &canceled, &nodesDrawn, &loop, &blockRequests, &context, nStr, blockRequest ]()
418 {
419 blockRequests.removeOne( blockRequest );
420
421 // If all blocks are loaded, exit the event loop
422 if ( blockRequests.isEmpty() )
423 loop.exit();
424
425 std::unique_ptr<QgsPointCloudBlock> block( blockRequest->takeBlock() );
426
427 blockRequest->deleteLater();
428
429 if ( context.feedback() && context.feedback()->isCanceled() )
430 {
431 canceled = true;
432 return;
433 }
434
435 if ( !block )
436 {
437 QgsDebugError( QStringLiteral( "Unable to load node %1, error: %2" ).arg( nStr, blockRequest->errorStr() ) );
438 return;
439 }
440
441 QgsVector3D contextScale = context.scale();
442 QgsVector3D contextOffset = context.offset();
443
444 context.setScale( block->scale() );
445 context.setOffset( block->offset() );
446 context.setAttributes( block->attributes() );
447
448 mRenderer->renderBlock( block.get(), context );
449
450 context.setScale( contextScale );
451 context.setOffset( contextOffset );
452
453 ++nodesDrawn;
454
455 // as soon as first block is rendered, we can start showing layer updates.
456 // but if we are blocking render updates (so that a previously cached image is being shown), we wait
457 // at most e.g. 3 seconds before we start forcing progressive updates.
458 if ( !mBlockRenderUpdates || mElapsedTimer.elapsed() > MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE )
459 {
460 mReadyToCompose = true;
461 }
462
463 } );
464 }
465
466 // Wait for all point cloud nodes to finish loading
467 loop.exec();
468
469 // Rendering may have got canceled and the event loop exited before finished()
470 // was called for all blocks, so let's clean up anything that is left
471 for ( QgsPointCloudBlockRequest *blockRequest : std::as_const( blockRequests ) )
472 {
473 std::unique_ptr<QgsPointCloudBlock> block = blockRequest->takeBlock();
474 block.reset();
475
476 blockRequest->deleteLater();
477 }
478
479 if ( mRenderer->renderAsTriangles() )
480 {
481 // Switch back from the preview painter to the destination painter to render the triangles
482 context.renderContext().setPainter( finalPainter );
483 renderTriangulatedSurface( context );
484 }
485
486 return nodesDrawn;
487}
488
489int QgsPointCloudLayerRenderer::renderNodesSorted( const QVector<IndexedPointCloudNode> &nodes, QgsPointCloudIndex *pc, QgsPointCloudRenderContext &context, QgsPointCloudRequest &request, bool &canceled, Qgis::PointCloudDrawOrder order )
490{
491 int blockCount = 0;
492 int pointCount = 0;
493
494 QgsVector3D blockScale;
495 QgsVector3D blockOffset;
496 QgsPointCloudAttributeCollection blockAttributes;
497 int recordSize = 0;
498
499 // We'll collect byte array data from all blocks
500 QByteArray allByteArrays;
501 // And pairs of byte array start positions paired with their Z values for sorting
502 QVector<QPair<int, double>> allPairs;
503
504 for ( const IndexedPointCloudNode &n : nodes )
505 {
506 if ( context.renderContext().renderingStopped() )
507 {
508 QgsDebugMsgLevel( QStringLiteral( "canceled" ), 2 );
509 canceled = true;
510 break;
511 }
512 std::unique_ptr<QgsPointCloudBlock> block( pc->nodeData( n, request ) );
513
514 if ( !block )
515 continue;
516
517 // Individual nodes may have different offset values than the root node
518 // we'll calculate the differences and translate x,y,z values to use the root node's offset
519 QgsVector3D offsetDifference = QgsVector3D( 0, 0, 0 );
520 if ( blockCount == 0 )
521 {
522 blockScale = block->scale();
523 blockOffset = block->offset();
524 blockAttributes = block->attributes();
525 }
526 else
527 {
528 offsetDifference = blockOffset - block->offset();
529 }
530
531 const char *ptr = block->data();
532
533 context.setScale( block->scale() );
534 context.setOffset( block->offset() );
535 context.setAttributes( block->attributes() );
536
537 recordSize = context.pointRecordSize();
538
539 for ( int i = 0; i < block->pointCount(); ++i )
540 {
541 allByteArrays.append( ptr + i * recordSize, recordSize );
542
543 // Calculate the translated values only for axes that have a different offset
544 if ( offsetDifference.x() != 0 )
545 {
546 qint32 ix = *reinterpret_cast< const qint32 * >( ptr + i * recordSize + context.xOffset() );
547 ix -= std::lround( offsetDifference.x() / context.scale().x() );
548 const char *xPtr = reinterpret_cast< const char * >( &ix );
549 allByteArrays.replace( pointCount * recordSize + context.xOffset(), 4, QByteArray( xPtr, 4 ) );
550 }
551 if ( offsetDifference.y() != 0 )
552 {
553 qint32 iy = *reinterpret_cast< const qint32 * >( ptr + i * recordSize + context.yOffset() );
554 iy -= std::lround( offsetDifference.y() / context.scale().y() );
555 const char *yPtr = reinterpret_cast< const char * >( &iy );
556 allByteArrays.replace( pointCount * recordSize + context.yOffset(), 4, QByteArray( yPtr, 4 ) );
557 }
558 // We need the Z value regardless of the node's offset
559 qint32 iz = *reinterpret_cast< const qint32 * >( ptr + i * recordSize + context.zOffset() );
560 if ( offsetDifference.z() != 0 )
561 {
562 iz -= std::lround( offsetDifference.z() / context.scale().z() );
563 const char *zPtr = reinterpret_cast< const char * >( &iz );
564 allByteArrays.replace( pointCount * recordSize + context.zOffset(), 4, QByteArray( zPtr, 4 ) );
565 }
566 allPairs.append( qMakePair( pointCount, double( iz ) + block->offset().z() ) );
567
568 ++pointCount;
569 }
570 ++blockCount;
571 }
572
573 if ( pointCount == 0 )
574 return 0;
575
576 switch ( order )
577 {
579 std::sort( allPairs.begin(), allPairs.end(), []( QPair<int, double> a, QPair<int, double> b ) { return a.second < b.second; } );
580 break;
582 std::sort( allPairs.begin(), allPairs.end(), []( QPair<int, double> a, QPair<int, double> b ) { return a.second > b.second; } );
583 break;
585 break;
586 }
587
588 // Now we can reconstruct a byte array sorted by Z value
589 QByteArray sortedByteArray;
590 sortedByteArray.reserve( allPairs.size() );
591 for ( QPair<int, double> pair : allPairs )
592 sortedByteArray.append( allByteArrays.mid( pair.first * recordSize, recordSize ) );
593
594 std::unique_ptr<QgsPointCloudBlock> bigBlock { new QgsPointCloudBlock( pointCount,
595 blockAttributes,
596 sortedByteArray,
597 blockScale,
598 blockOffset ) };
599
600 QgsVector3D contextScale = context.scale();
601 QgsVector3D contextOffset = context.offset();
602
603 context.setScale( bigBlock->scale() );
604 context.setOffset( bigBlock->offset() );
605 context.setAttributes( bigBlock->attributes() );
606
607 mRenderer->renderBlock( bigBlock.get(), context );
608
609 context.setScale( contextScale );
610 context.setOffset( contextOffset );
611
612 return blockCount;
613}
614
615inline bool isEdgeTooLong( const QPointF &p1, const QPointF &p2, float length )
616{
617 QPointF p = p1 - p2;
618 return p.x() * p.x() + p.y() * p.y() > length;
619}
620
621static void renderTriangle( QImage &img, QPointF *pts, QRgb c0, QRgb c1, QRgb c2, float horizontalFilter, float *elev, QgsElevationMap *elevationMap )
622{
623 if ( horizontalFilter > 0 )
624 {
625 float filterThreshold2 = horizontalFilter * horizontalFilter;
626 if ( isEdgeTooLong( pts[0], pts[1], filterThreshold2 ) ||
627 isEdgeTooLong( pts[1], pts[2], filterThreshold2 ) ||
628 isEdgeTooLong( pts[2], pts[0], filterThreshold2 ) )
629 return;
630 }
631
632 QgsRectangle screenBBox = QgsMeshLayerUtils::triangleBoundingBox( pts[0], pts[1], pts[2] );
633
634 QSize outputSize = img.size();
635
636 int topLim = std::max( int( screenBBox.yMinimum() ), 0 );
637 int bottomLim = std::min( int( screenBBox.yMaximum() ), outputSize.height() - 1 );
638 int leftLim = std::max( int( screenBBox.xMinimum() ), 0 );
639 int rightLim = std::min( int( screenBBox.xMaximum() ), outputSize.width() - 1 );
640
641 int red0 = qRed( c0 ), green0 = qGreen( c0 ), blue0 = qBlue( c0 );
642 int red1 = qRed( c1 ), green1 = qGreen( c1 ), blue1 = qBlue( c1 );
643 int red2 = qRed( c2 ), green2 = qGreen( c2 ), blue2 = qBlue( c2 );
644
645 QRgb *elevData = elevationMap ? elevationMap->rawElevationImageData() : nullptr;
646
647 for ( int j = topLim; j <= bottomLim; j++ )
648 {
649 QRgb *scanLine = ( QRgb * ) img.scanLine( j );
650 QRgb *elevScanLine = elevData ? elevData + static_cast<size_t>( outputSize.width() * j ) : nullptr;
651 for ( int k = leftLim; k <= rightLim; k++ )
652 {
653 QPointF pt( k, j );
654 double lam1, lam2, lam3;
655 if ( !QgsMeshLayerUtils::calculateBarycentricCoordinates( pts[0], pts[1], pts[2], pt, lam3, lam2, lam1 ) )
656 continue;
657
658 // interpolate color
659 int r = static_cast<int>( red0 * lam1 + red1 * lam2 + red2 * lam3 );
660 int g = static_cast<int>( green0 * lam1 + green1 * lam2 + green2 * lam3 );
661 int b = static_cast<int>( blue0 * lam1 + blue1 * lam2 + blue2 * lam3 );
662 scanLine[k] = qRgb( r, g, b );
663
664 // interpolate elevation - in case we are doing global map shading
665 if ( elevScanLine )
666 {
667 float z = static_cast<float>( elev[0] * lam1 + elev[1] * lam2 + elev[2] * lam3 );
668 elevScanLine[k] = QgsElevationMap::encodeElevation( z );
669 }
670 }
671 }
672}
673
674void QgsPointCloudLayerRenderer::renderTriangulatedSurface( QgsPointCloudRenderContext &context )
675{
677 const std::vector<double> &points = triangulation.points;
678
679 // Delaunator would crash if it gets less than three points
680 if ( points.size() < 3 )
681 {
682 QgsDebugMsgLevel( QStringLiteral( "Need at least 3 points to triangulate" ), 4 );
683 return;
684 }
685
686 std::unique_ptr<delaunator::Delaunator> delaunator;
687 try
688 {
689 delaunator.reset( new delaunator::Delaunator( points ) );
690 }
691 catch ( std::exception & )
692 {
693 // something went wrong, better to retrieve initial state
694 QgsDebugMsgLevel( QStringLiteral( "Error with triangulation" ), 4 );
695 return;
696 }
697
698 float horizontalFilter = 0;
699 if ( mRenderer->horizontalTriangleFilter() )
700 {
701 horizontalFilter = static_cast<float>( renderContext()->convertToPainterUnits(
702 mRenderer->horizontalTriangleFilterThreshold(), mRenderer->horizontalTriangleFilterUnit() ) );
703 }
704
705 QImage img( context.renderContext().deviceOutputSize(), QImage::Format_ARGB32_Premultiplied );
706 img.setDevicePixelRatio( context.renderContext().devicePixelRatio() );
707 img.fill( 0 );
708
709 const std::vector<size_t> &triangleIndexes = delaunator->triangles;
710 QPainter *painter = context.renderContext().painter();
711 QgsElevationMap *elevationMap = context.renderContext().elevationMap();
712 QPointF triangle[3];
713 float elev[3] {0, 0, 0};
714 for ( size_t i = 0; i < triangleIndexes.size(); i += 3 )
715 {
716 size_t v0 = triangleIndexes[i], v1 = triangleIndexes[i + 1], v2 = triangleIndexes[i + 2];
717 triangle[0].rx() = points[v0 * 2];
718 triangle[0].ry() = points[v0 * 2 + 1];
719 triangle[1].rx() = points[v1 * 2];
720 triangle[1].ry() = points[v1 * 2 + 1];
721 triangle[2].rx() = points[v2 * 2];
722 triangle[2].ry() = points[v2 * 2 + 1];
723
724 if ( elevationMap )
725 {
726 elev[0] = triangulation.elevations[v0];
727 elev[1] = triangulation.elevations[v1];
728 elev[2] = triangulation.elevations[v2];
729 }
730
731 QRgb c0 = triangulation.colors[v0], c1 = triangulation.colors[v1], c2 = triangulation.colors[v2];
732 renderTriangle( img, triangle, c0, c1, c2, horizontalFilter, elev, elevationMap );
733 }
734
735 painter->drawImage( 0, 0, img );
736}
737
739{
740 // when rendering as triangles we still want to show temporary incremental renders as points until
741 // the final triangulated surface is ready, which may be slow
742 // So we request here a preview render image for the temporary incremental updates:
743 if ( mRenderer->renderAsTriangles() )
745
747}
748
750{
751 // unless we are using the extent only renderer, point cloud layers should always be rasterized -- we don't want to export points as vectors
752 // to formats like PDF!
753 return mRenderer ? mRenderer->type() != QLatin1String( "extent" ) : false;
754}
755
757{
758 mRenderTimeHint = time;
759}
760
761QVector<IndexedPointCloudNode> QgsPointCloudLayerRenderer::traverseTree( const QgsPointCloudIndex *pc,
762 const QgsRenderContext &context,
764 double maxErrorPixels,
765 double nodeErrorPixels )
766{
767 QVector<IndexedPointCloudNode> nodes;
768
769 if ( context.renderingStopped() )
770 {
771 QgsDebugMsgLevel( QStringLiteral( "canceled" ), 2 );
772 return nodes;
773 }
774
775 if ( !context.extent().intersects( pc->nodeMapExtent( n ) ) )
776 return nodes;
777
778 const QgsDoubleRange nodeZRange = pc->nodeZRange( n );
779 const QgsDoubleRange adjustedNodeZRange = QgsDoubleRange( nodeZRange.lower() + mZOffset, nodeZRange.upper() + mZOffset );
780 if ( !context.zRange().isInfinite() && !context.zRange().overlaps( adjustedNodeZRange ) )
781 return nodes;
782
783 if ( pc->nodePointCount( n ) > 0 )
784 nodes.append( n );
785
786 double childrenErrorPixels = nodeErrorPixels / 2.0;
787 if ( childrenErrorPixels < maxErrorPixels )
788 return nodes;
789
790 const QList<IndexedPointCloudNode> children = pc->nodeChildren( n );
791 for ( const IndexedPointCloudNode &nn : children )
792 {
793 nodes += traverseTree( pc, context, nn, maxErrorPixels, childrenErrorPixels );
794 }
795
796 return nodes;
797}
798
Represents a indexed point cloud node in octree.
QString toString() const
Encode node to string.
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:2613
PointCloudDrawOrder
Pointcloud rendering order for 2d views.
Definition qgis.h:3954
@ 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.
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.
virtual qint64 nodePointCount(const IndexedPointCloudNode &n) const
Returns the number of points of a given node n.
QgsRectangle nodeMapExtent(const IndexedPointCloudNode &node) const
Returns the extent of a node in map coordinates.
virtual QgsPointCloudBlockRequest * asyncNodeData(const IndexedPointCloudNode &n, const QgsPointCloudRequest &request)=0
Returns a handle responsible for loading a node data block.
virtual QList< IndexedPointCloudNode > nodeChildren(const IndexedPointCloudNode &n) const
Returns all children of 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.
QgsVector3D scale() const
Returns scale.
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.
IndexedPointCloudNode root()
Returns root node of the index.
QgsDoubleRange nodeZRange(const IndexedPointCloudNode &node) const
Returns the z range of a node.
virtual std::unique_ptr< QgsPointCloudBlock > nodeData(const IndexedPointCloudNode &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.
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
T lower() const
Returns the lower bound of the range.
Definition qgsrange.h:78
T upper() const
Returns the upper bound of the range.
Definition qgsrange.h:85
A rectangle specified with double values.
double xMinimum() const
Returns the x minimum value (left side of rectangle).
bool intersects(const QgsRectangle &rect) const
Returns true when rectangle intersects with other rectangle.
double yMinimum() const
Returns the y minimum value (bottom side of rectangle).
double width() const
Returns the width of the rectangle.
double xMaximum() const
Returns the x maximum value (right side of rectangle).
double yMaximum() const
Returns the y maximum value (top side of rectangle).
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...