QGIS API Documentation 3.41.0-Master (d2aaa9c6e02)
Loading...
Searching...
No Matches
qgspointcloudlayerrenderer.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgspointcloudlayerrenderer.cpp
3 --------------------
4 begin : October 2020
5 copyright : (C) 2020 by Peter Petrik
6 email : zilolv at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
18#include <QElapsedTimer>
19#include <QPointer>
20
21#include "qgsapplication.h"
22#include "qgscolorramp.h"
23#include "qgselevationmap.h"
24#include "qgslogger.h"
25#include "qgsmapclippingutils.h"
26#include "qgsmeshlayerutils.h"
27#include "qgsmessagelog.h"
31#include "qgspointcloudindex.h"
32#include "qgspointcloudlayer.h"
37#include "qgsrendercontext.h"
38#include "qgsruntimeprofiler.h"
39#include "qgsvirtualpointcloudprovider.h"
40
41#include <delaunator.hpp>
42
43
45 : QgsMapLayerRenderer( layer->id(), &context )
46 , mLayer( layer )
47 , mLayerName( layer->name() )
48 , mLayerAttributes( layer->attributes() )
49 , mSubIndexes( layer && layer->dataProvider() ? layer->dataProvider()->subIndexes() : QVector<QgsPointCloudSubIndex>() )
50 , mFeedback( new QgsFeedback )
51 , mEnableProfile( context.flags() & Qgis::RenderContextFlag::RecordProfile )
52{
53 // TODO: we must not keep pointer to mLayer (it's dangerous) - we must copy anything we need for rendering
54 // or use some locking to prevent read/write from multiple threads
55 if ( !mLayer || !mLayer->dataProvider() || !mLayer->renderer() )
56 return;
57
58 QElapsedTimer timer;
59 timer.start();
60
61 mRenderer.reset( mLayer->renderer()->clone() );
62 if ( !mSubIndexes.isEmpty() )
63 {
64 mSubIndexExtentRenderer.reset( new QgsPointCloudExtentRenderer() );
65 mSubIndexExtentRenderer->setShowLabels( mRenderer->showLabels() );
66 mSubIndexExtentRenderer->setLabelTextFormat( mRenderer->labelTextFormat() );
67 }
68
69 if ( mLayer->index() )
70 {
71 mScale = mLayer->index().scale();
72 mOffset = mLayer->index().offset();
73 }
74
75 if ( const QgsPointCloudLayerElevationProperties *elevationProps = qobject_cast< const QgsPointCloudLayerElevationProperties * >( mLayer->elevationProperties() ) )
76 {
77 mZOffset = elevationProps->zOffset();
78 mZScale = elevationProps->zScale();
79 }
80
81 mCloudExtent = mLayer->dataProvider()->polygonBounds();
82
84
85 mReadyToCompose = false;
86
87 mPreparationTime = timer.elapsed();
88}
89
91{
92 std::unique_ptr< QgsScopedRuntimeProfile > profile;
93 if ( mEnableProfile )
94 {
95 profile = std::make_unique< QgsScopedRuntimeProfile >( mLayerName, QStringLiteral( "rendering" ), layerId() );
96 if ( mPreparationTime > 0 )
97 QgsApplication::profiler()->record( QObject::tr( "Create renderer" ), mPreparationTime / 1000.0, QStringLiteral( "rendering" ) );
98 }
99
100 std::unique_ptr< QgsScopedRuntimeProfile > preparingProfile;
101 if ( mEnableProfile )
102 {
103 preparingProfile = std::make_unique< QgsScopedRuntimeProfile >( QObject::tr( "Preparing render" ), QStringLiteral( "rendering" ) );
104 }
105
106 QgsPointCloudRenderContext context( *renderContext(), mScale, mOffset, mZScale, mZOffset, mFeedback.get() );
107
108 // Set up the render configuration options
109 QPainter *painter = context.renderContext().painter();
110
111 QgsScopedQPainterState painterState( painter );
112 context.renderContext().setPainterFlagsUsingContext( painter );
113
114 if ( !mClippingRegions.empty() )
115 {
116 bool needsPainterClipPath = false;
117 const QPainterPath path = QgsMapClippingUtils::calculatePainterClipRegion( mClippingRegions, *renderContext(), Qgis::LayerType::VectorTile, needsPainterClipPath );
118 if ( needsPainterClipPath )
119 renderContext()->painter()->setClipPath( path, Qt::IntersectClip );
120 }
121
122 if ( mRenderer->type() == QLatin1String( "extent" ) )
123 {
124 // special case for extent only renderer!
125 mRenderer->startRender( context );
126 static_cast< QgsPointCloudExtentRenderer * >( mRenderer.get() )->renderExtent( mCloudExtent, context );
127 mRenderer->stopRender( context );
128 mReadyToCompose = true;
129 return true;
130 }
131
132 // TODO cache!?
133 QgsPointCloudIndex pc = mLayer->index();
134 if ( mSubIndexes.isEmpty() && ( !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 if ( const QgsVirtualPointCloudProvider *vpcProvider = dynamic_cast<QgsVirtualPointCloudProvider *>( mLayer->dataProvider() ) )
201 {
202 QVector< QgsPointCloudSubIndex > visibleIndexes;
203 for ( const QgsPointCloudSubIndex &si : mSubIndexes )
204 {
205 if ( renderExtent.intersects( si.extent() ) )
206 {
207 visibleIndexes.append( si );
208 }
209 }
210 const bool zoomedOut = renderExtent.width() > vpcProvider->averageSubIndexWidth() ||
211 renderExtent.height() > vpcProvider->averageSubIndexHeight();
212 QgsPointCloudIndex overviewIndex = vpcProvider->overview();
213 // if the overview of virtual point cloud exists, and we are zoomed out, we render just overview
214 if ( vpcProvider->overview() && zoomedOut &&
215 mRenderer->zoomOutBehavior() == Qgis::PointCloudZoomOutRenderBehavior::RenderOverview )
216 {
217 renderIndex( overviewIndex );
218 }
219 else
220 {
221 // if the overview of virtual point cloud exists, and we are zoomed out, but we want both overview and extents,
222 // we render overview
223 if ( vpcProvider->overview() && zoomedOut &&
225 {
226 renderIndex( overviewIndex );
227 }
228 mSubIndexExtentRenderer->startRender( context );
229 for ( const QgsPointCloudSubIndex &si : visibleIndexes )
230 {
231 if ( canceled )
232 break;
233
234 QgsPointCloudIndex pc = si.index();
235 // if the index of point cloud is invalid, or we are zoomed out and want extents, we render the point cloud extent
236 if ( !pc || !pc.isValid() || ( ( mRenderer->zoomOutBehavior() == Qgis::PointCloudZoomOutRenderBehavior::RenderExtents || mRenderer->zoomOutBehavior() == Qgis::PointCloudZoomOutRenderBehavior::RenderOverviewAndExtents ) &&
237 zoomedOut ) )
238 {
239 mSubIndexExtentRenderer->renderExtent( si.polygonBounds(), context );
240 if ( mSubIndexExtentRenderer->showLabels() )
241 {
242 mSubIndexExtentRenderer->renderLabel(
243 context.renderContext().mapToPixel().transformBounds( si.extent().toRectF() ),
244 si.uri().section( "/", -1 ).section( ".", 0, 0 ),
245 context );
246 }
247 }
248 // else we just render the visible point cloud
249 else
250 {
251 canceled = !renderIndex( pc );
252 }
253 }
254 mSubIndexExtentRenderer->stopRender( context );
255 }
256 }
257
258 mRenderer->stopRender( context );
259 mReadyToCompose = true;
260 return !canceled;
261}
262
263bool QgsPointCloudLayerRenderer::renderIndex( QgsPointCloudIndex &pc )
264{
266 pc.scale(),
267 pc.offset(),
268 mZScale,
269 mZOffset,
270 mFeedback.get() );
271
272
273#ifdef QGISDEBUG
274 QElapsedTimer t;
275 t.start();
276#endif
277
278 const QgsPointCloudNodeId root = pc.root();
279
280 const double maximumError = context.renderContext().convertToPainterUnits( mRenderer->maximumScreenError(), mRenderer->maximumScreenErrorUnit() );// in pixels
281
282 const QgsPointCloudNode rootNode = pc.getNode( root );
283 const QgsRectangle rootNodeExtentLayerCoords = pc.extent();
284 QgsRectangle rootNodeExtentMapCoords;
285 if ( !context.renderContext().coordinateTransform().isShortCircuited() )
286 {
287 try
288 {
289 QgsCoordinateTransform extentTransform = context.renderContext().coordinateTransform();
290 extentTransform.setBallparkTransformsAreAppropriate( true );
291 rootNodeExtentMapCoords = extentTransform.transformBoundingBox( rootNodeExtentLayerCoords );
292 }
293 catch ( QgsCsException & )
294 {
295 QgsDebugError( QStringLiteral( "Could not transform node extent to map CRS" ) );
296 rootNodeExtentMapCoords = rootNodeExtentLayerCoords;
297 }
298 }
299 else
300 {
301 rootNodeExtentMapCoords = rootNodeExtentLayerCoords;
302 }
303
304 const double rootErrorInMapCoordinates = rootNodeExtentMapCoords.width() / pc.span(); // in map coords
305
306 double mapUnitsPerPixel = context.renderContext().mapToPixel().mapUnitsPerPixel();
307 if ( ( rootErrorInMapCoordinates < 0.0 ) || ( mapUnitsPerPixel < 0.0 ) || ( maximumError < 0.0 ) )
308 {
309 QgsDebugError( QStringLiteral( "invalid screen error" ) );
310 return false;
311 }
312 double rootErrorPixels = rootErrorInMapCoordinates / mapUnitsPerPixel; // in pixels
313 const QVector<QgsPointCloudNodeId> nodes = traverseTree( pc, context.renderContext(), pc.root(), maximumError, rootErrorPixels );
314
315 QgsPointCloudRequest request;
316 request.setAttributes( mAttributes );
317
318 // drawing
319 int nodesDrawn = 0;
320 bool canceled = false;
321
322 Qgis::PointCloudDrawOrder drawOrder = mRenderer->drawOrder2d();
323 if ( mRenderer->renderAsTriangles() )
324 {
325 // Ordered rendering is ignored when drawing as surface, because all points are used for triangulation.
326 // We would need to have a way to detect if a point is occluded by some other points, which may be costly.
328 }
329
330 switch ( drawOrder )
331 {
334 {
335 nodesDrawn += renderNodesSorted( nodes, pc, context, request, canceled, mRenderer->drawOrder2d() );
336 break;
337 }
339 {
340 switch ( pc.accessType() )
341 {
343 {
344 nodesDrawn += renderNodesSync( nodes, pc, context, request, canceled );
345 break;
346 }
348 {
349 nodesDrawn += renderNodesAsync( nodes, pc, context, request, canceled );
350 break;
351 }
352 }
353 }
354 }
355
356#ifdef QGISDEBUG
357 QgsDebugMsgLevel( QStringLiteral( "totals: %1 nodes | %2 points | %3ms" ).arg( nodesDrawn )
358 .arg( context.pointsRendered() )
359 .arg( t.elapsed() ), 2 );
360#else
361 ( void )nodesDrawn;
362#endif
363
364 return !canceled;
365}
366
367int QgsPointCloudLayerRenderer::renderNodesSync( const QVector<QgsPointCloudNodeId> &nodes, QgsPointCloudIndex &pc, QgsPointCloudRenderContext &context, QgsPointCloudRequest &request, bool &canceled )
368{
369 QPainter *finalPainter = context.renderContext().painter();
370 if ( mRenderer->renderAsTriangles() && context.renderContext().previewRenderPainter() )
371 {
372 // swap out the destination painter for the preview render painter to render points
373 // until the actual triangles are ready to be rendered
375 }
376
377 int nodesDrawn = 0;
378 for ( const QgsPointCloudNodeId &n : nodes )
379 {
380 if ( context.renderContext().renderingStopped() )
381 {
382 QgsDebugMsgLevel( QStringLiteral( "canceled" ), 2 );
383 canceled = true;
384 break;
385 }
386 std::unique_ptr<QgsPointCloudBlock> block( pc.nodeData( n, request ) );
387
388 if ( !block )
389 continue;
390
391 QgsVector3D contextScale = context.scale();
392 QgsVector3D contextOffset = context.offset();
393
394 context.setScale( block->scale() );
395 context.setOffset( block->offset() );
396
397 context.setAttributes( block->attributes() );
398
399 mRenderer->renderBlock( block.get(), context );
400
401 context.setScale( contextScale );
402 context.setOffset( contextOffset );
403
404 ++nodesDrawn;
405
406 // as soon as first block is rendered, we can start showing layer updates.
407 // but if we are blocking render updates (so that a previously cached image is being shown), we wait
408 // at most e.g. 3 seconds before we start forcing progressive updates.
409 if ( !mBlockRenderUpdates || mElapsedTimer.elapsed() > MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE )
410 {
411 mReadyToCompose = true;
412 }
413 }
414
415 if ( mRenderer->renderAsTriangles() )
416 {
417 // Switch back from the preview painter to the destination painter to render the triangles
418 context.renderContext().setPainter( finalPainter );
419 renderTriangulatedSurface( context );
420 }
421
422 return nodesDrawn;
423}
424
425int QgsPointCloudLayerRenderer::renderNodesAsync( const QVector<QgsPointCloudNodeId> &nodes, QgsPointCloudIndex &pc, QgsPointCloudRenderContext &context, QgsPointCloudRequest &request, bool &canceled )
426{
427 if ( context.feedback() && context.feedback()->isCanceled() )
428 return 0;
429
430 QPainter *finalPainter = context.renderContext().painter();
431 if ( mRenderer->renderAsTriangles() && context.renderContext().previewRenderPainter() )
432 {
433 // swap out the destination painter for the preview render painter to render points
434 // until the actual triangles are ready to be rendered
436 }
437
438 int nodesDrawn = 0;
439
440 // Async loading of nodes
441 QVector<QgsPointCloudBlockRequest *> blockRequests;
442 QEventLoop loop;
443 if ( context.feedback() )
444 QObject::connect( context.feedback(), &QgsFeedback::canceled, &loop, &QEventLoop::quit );
445
446 for ( int i = 0; i < nodes.size(); ++i )
447 {
448 const QgsPointCloudNodeId &n = nodes[i];
449 const QString nStr = n.toString();
450 QgsPointCloudBlockRequest *blockRequest = pc.asyncNodeData( n, request );
451 blockRequests.append( blockRequest );
452 QObject::connect( blockRequest, &QgsPointCloudBlockRequest::finished, &loop,
453 [ this, &canceled, &nodesDrawn, &loop, &blockRequests, &context, nStr, blockRequest ]()
454 {
455 blockRequests.removeOne( blockRequest );
456
457 // If all blocks are loaded, exit the event loop
458 if ( blockRequests.isEmpty() )
459 loop.exit();
460
461 std::unique_ptr<QgsPointCloudBlock> block( blockRequest->takeBlock() );
462
463 blockRequest->deleteLater();
464
465 if ( context.feedback() && context.feedback()->isCanceled() )
466 {
467 canceled = true;
468 return;
469 }
470
471 if ( !block )
472 {
473 QgsDebugError( QStringLiteral( "Unable to load node %1, error: %2" ).arg( nStr, blockRequest->errorStr() ) );
474 return;
475 }
476
477 QgsVector3D contextScale = context.scale();
478 QgsVector3D contextOffset = context.offset();
479
480 context.setScale( block->scale() );
481 context.setOffset( block->offset() );
482 context.setAttributes( block->attributes() );
483
484 mRenderer->renderBlock( block.get(), context );
485
486 context.setScale( contextScale );
487 context.setOffset( contextOffset );
488
489 ++nodesDrawn;
490
491 // as soon as first block is rendered, we can start showing layer updates.
492 // but if we are blocking render updates (so that a previously cached image is being shown), we wait
493 // at most e.g. 3 seconds before we start forcing progressive updates.
494 if ( !mBlockRenderUpdates || mElapsedTimer.elapsed() > MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE )
495 {
496 mReadyToCompose = true;
497 }
498
499 } );
500 }
501
502 // Wait for all point cloud nodes to finish loading
503 loop.exec();
504
505 // Rendering may have got canceled and the event loop exited before finished()
506 // was called for all blocks, so let's clean up anything that is left
507 for ( QgsPointCloudBlockRequest *blockRequest : std::as_const( blockRequests ) )
508 {
509 std::unique_ptr<QgsPointCloudBlock> block = blockRequest->takeBlock();
510 block.reset();
511
512 blockRequest->deleteLater();
513 }
514
515 if ( mRenderer->renderAsTriangles() )
516 {
517 // Switch back from the preview painter to the destination painter to render the triangles
518 context.renderContext().setPainter( finalPainter );
519 renderTriangulatedSurface( context );
520 }
521
522 return nodesDrawn;
523}
524
525int QgsPointCloudLayerRenderer::renderNodesSorted( const QVector<QgsPointCloudNodeId> &nodes, QgsPointCloudIndex &pc, QgsPointCloudRenderContext &context, QgsPointCloudRequest &request, bool &canceled, Qgis::PointCloudDrawOrder order )
526{
527 int blockCount = 0;
528 int pointCount = 0;
529
530 QgsVector3D blockScale;
531 QgsVector3D blockOffset;
532 QgsPointCloudAttributeCollection blockAttributes;
533 int recordSize = 0;
534
535 // We'll collect byte array data from all blocks
536 QByteArray allByteArrays;
537 // And pairs of byte array start positions paired with their Z values for sorting
538 QVector<QPair<int, double>> allPairs;
539
540 for ( const QgsPointCloudNodeId &n : nodes )
541 {
542 if ( context.renderContext().renderingStopped() )
543 {
544 QgsDebugMsgLevel( QStringLiteral( "canceled" ), 2 );
545 canceled = true;
546 break;
547 }
548 std::unique_ptr<QgsPointCloudBlock> block( pc.nodeData( n, request ) );
549
550 if ( !block )
551 continue;
552
553 // Individual nodes may have different offset values than the root node
554 // we'll calculate the differences and translate x,y,z values to use the root node's offset
555 QgsVector3D offsetDifference = QgsVector3D( 0, 0, 0 );
556 if ( blockCount == 0 )
557 {
558 blockScale = block->scale();
559 blockOffset = block->offset();
560 blockAttributes = block->attributes();
561 }
562 else
563 {
564 offsetDifference = blockOffset - block->offset();
565 }
566
567 const char *ptr = block->data();
568
569 context.setScale( block->scale() );
570 context.setOffset( block->offset() );
571 context.setAttributes( block->attributes() );
572
573 recordSize = context.pointRecordSize();
574
575 for ( int i = 0; i < block->pointCount(); ++i )
576 {
577 allByteArrays.append( ptr + i * recordSize, recordSize );
578
579 // Calculate the translated values only for axes that have a different offset
580 if ( offsetDifference.x() != 0 )
581 {
582 qint32 ix = *reinterpret_cast< const qint32 * >( ptr + i * recordSize + context.xOffset() );
583 ix -= std::lround( offsetDifference.x() / context.scale().x() );
584 const char *xPtr = reinterpret_cast< const char * >( &ix );
585 allByteArrays.replace( pointCount * recordSize + context.xOffset(), 4, QByteArray( xPtr, 4 ) );
586 }
587 if ( offsetDifference.y() != 0 )
588 {
589 qint32 iy = *reinterpret_cast< const qint32 * >( ptr + i * recordSize + context.yOffset() );
590 iy -= std::lround( offsetDifference.y() / context.scale().y() );
591 const char *yPtr = reinterpret_cast< const char * >( &iy );
592 allByteArrays.replace( pointCount * recordSize + context.yOffset(), 4, QByteArray( yPtr, 4 ) );
593 }
594 // We need the Z value regardless of the node's offset
595 qint32 iz = *reinterpret_cast< const qint32 * >( ptr + i * recordSize + context.zOffset() );
596 if ( offsetDifference.z() != 0 )
597 {
598 iz -= std::lround( offsetDifference.z() / context.scale().z() );
599 const char *zPtr = reinterpret_cast< const char * >( &iz );
600 allByteArrays.replace( pointCount * recordSize + context.zOffset(), 4, QByteArray( zPtr, 4 ) );
601 }
602 allPairs.append( qMakePair( pointCount, double( iz ) + block->offset().z() ) );
603
604 ++pointCount;
605 }
606 ++blockCount;
607 }
608
609 if ( pointCount == 0 )
610 return 0;
611
612 switch ( order )
613 {
615 std::sort( allPairs.begin(), allPairs.end(), []( QPair<int, double> a, QPair<int, double> b ) { return a.second < b.second; } );
616 break;
618 std::sort( allPairs.begin(), allPairs.end(), []( QPair<int, double> a, QPair<int, double> b ) { return a.second > b.second; } );
619 break;
621 break;
622 }
623
624 // Now we can reconstruct a byte array sorted by Z value
625 QByteArray sortedByteArray;
626 sortedByteArray.reserve( allPairs.size() );
627 for ( QPair<int, double> pair : allPairs )
628 sortedByteArray.append( allByteArrays.mid( pair.first * recordSize, recordSize ) );
629
630 std::unique_ptr<QgsPointCloudBlock> bigBlock { new QgsPointCloudBlock( pointCount,
631 blockAttributes,
632 sortedByteArray,
633 blockScale,
634 blockOffset ) };
635
636 QgsVector3D contextScale = context.scale();
637 QgsVector3D contextOffset = context.offset();
638
639 context.setScale( bigBlock->scale() );
640 context.setOffset( bigBlock->offset() );
641 context.setAttributes( bigBlock->attributes() );
642
643 mRenderer->renderBlock( bigBlock.get(), context );
644
645 context.setScale( contextScale );
646 context.setOffset( contextOffset );
647
648 return blockCount;
649}
650
651inline bool isEdgeTooLong( const QPointF &p1, const QPointF &p2, float length )
652{
653 QPointF p = p1 - p2;
654 return p.x() * p.x() + p.y() * p.y() > length;
655}
656
657static void renderTriangle( QImage &img, QPointF *pts, QRgb c0, QRgb c1, QRgb c2, float horizontalFilter, float *elev, QgsElevationMap *elevationMap )
658{
659 if ( horizontalFilter > 0 )
660 {
661 float filterThreshold2 = horizontalFilter * horizontalFilter;
662 if ( isEdgeTooLong( pts[0], pts[1], filterThreshold2 ) ||
663 isEdgeTooLong( pts[1], pts[2], filterThreshold2 ) ||
664 isEdgeTooLong( pts[2], pts[0], filterThreshold2 ) )
665 return;
666 }
667
668 QgsRectangle screenBBox = QgsMeshLayerUtils::triangleBoundingBox( pts[0], pts[1], pts[2] );
669
670 QSize outputSize = img.size();
671
672 int topLim = std::max( int( screenBBox.yMinimum() ), 0 );
673 int bottomLim = std::min( int( screenBBox.yMaximum() ), outputSize.height() - 1 );
674 int leftLim = std::max( int( screenBBox.xMinimum() ), 0 );
675 int rightLim = std::min( int( screenBBox.xMaximum() ), outputSize.width() - 1 );
676
677 int red0 = qRed( c0 ), green0 = qGreen( c0 ), blue0 = qBlue( c0 );
678 int red1 = qRed( c1 ), green1 = qGreen( c1 ), blue1 = qBlue( c1 );
679 int red2 = qRed( c2 ), green2 = qGreen( c2 ), blue2 = qBlue( c2 );
680
681 QRgb *elevData = elevationMap ? elevationMap->rawElevationImageData() : nullptr;
682
683 for ( int j = topLim; j <= bottomLim; j++ )
684 {
685 QRgb *scanLine = ( QRgb * ) img.scanLine( j );
686 QRgb *elevScanLine = elevData ? elevData + static_cast<size_t>( outputSize.width() * j ) : nullptr;
687 for ( int k = leftLim; k <= rightLim; k++ )
688 {
689 QPointF pt( k, j );
690 double lam1, lam2, lam3;
691 if ( !QgsMeshLayerUtils::calculateBarycentricCoordinates( pts[0], pts[1], pts[2], pt, lam3, lam2, lam1 ) )
692 continue;
693
694 // interpolate color
695 int r = static_cast<int>( red0 * lam1 + red1 * lam2 + red2 * lam3 );
696 int g = static_cast<int>( green0 * lam1 + green1 * lam2 + green2 * lam3 );
697 int b = static_cast<int>( blue0 * lam1 + blue1 * lam2 + blue2 * lam3 );
698 scanLine[k] = qRgb( r, g, b );
699
700 // interpolate elevation - in case we are doing global map shading
701 if ( elevScanLine )
702 {
703 float z = static_cast<float>( elev[0] * lam1 + elev[1] * lam2 + elev[2] * lam3 );
704 elevScanLine[k] = QgsElevationMap::encodeElevation( z );
705 }
706 }
707 }
708}
709
710void QgsPointCloudLayerRenderer::renderTriangulatedSurface( QgsPointCloudRenderContext &context )
711{
713 const std::vector<double> &points = triangulation.points;
714
715 // Delaunator would crash if it gets less than three points
716 if ( points.size() < 3 )
717 {
718 QgsDebugMsgLevel( QStringLiteral( "Need at least 3 points to triangulate" ), 4 );
719 return;
720 }
721
722 std::unique_ptr<delaunator::Delaunator> delaunator;
723 try
724 {
725 delaunator.reset( new delaunator::Delaunator( points ) );
726 }
727 catch ( std::exception & )
728 {
729 // something went wrong, better to retrieve initial state
730 QgsDebugMsgLevel( QStringLiteral( "Error with triangulation" ), 4 );
731 return;
732 }
733
734 float horizontalFilter = 0;
735 if ( mRenderer->horizontalTriangleFilter() )
736 {
737 horizontalFilter = static_cast<float>( renderContext()->convertToPainterUnits(
738 mRenderer->horizontalTriangleFilterThreshold(), mRenderer->horizontalTriangleFilterUnit() ) );
739 }
740
741 QImage img( context.renderContext().deviceOutputSize(), QImage::Format_ARGB32_Premultiplied );
742 img.setDevicePixelRatio( context.renderContext().devicePixelRatio() );
743 img.fill( 0 );
744
745 const std::vector<size_t> &triangleIndexes = delaunator->triangles;
746 QPainter *painter = context.renderContext().painter();
747 QgsElevationMap *elevationMap = context.renderContext().elevationMap();
748 QPointF triangle[3];
749 float elev[3] {0, 0, 0};
750 for ( size_t i = 0; i < triangleIndexes.size(); i += 3 )
751 {
752 size_t v0 = triangleIndexes[i], v1 = triangleIndexes[i + 1], v2 = triangleIndexes[i + 2];
753 triangle[0].rx() = points[v0 * 2];
754 triangle[0].ry() = points[v0 * 2 + 1];
755 triangle[1].rx() = points[v1 * 2];
756 triangle[1].ry() = points[v1 * 2 + 1];
757 triangle[2].rx() = points[v2 * 2];
758 triangle[2].ry() = points[v2 * 2 + 1];
759
760 if ( elevationMap )
761 {
762 elev[0] = triangulation.elevations[v0];
763 elev[1] = triangulation.elevations[v1];
764 elev[2] = triangulation.elevations[v2];
765 }
766
767 QRgb c0 = triangulation.colors[v0], c1 = triangulation.colors[v1], c2 = triangulation.colors[v2];
768 renderTriangle( img, triangle, c0, c1, c2, horizontalFilter, elev, elevationMap );
769 }
770
771 painter->drawImage( 0, 0, img );
772}
773
775{
776 // when rendering as triangles we still want to show temporary incremental renders as points until
777 // the final triangulated surface is ready, which may be slow
778 // So we request here a preview render image for the temporary incremental updates:
779 if ( mRenderer->renderAsTriangles() )
781
783}
784
786{
787 // unless we are using the extent only renderer, point cloud layers should always be rasterized -- we don't want to export points as vectors
788 // to formats like PDF!
789 return mRenderer ? mRenderer->type() != QLatin1String( "extent" ) : false;
790}
791
793{
794 mRenderTimeHint = time;
795}
796
797QVector<QgsPointCloudNodeId> QgsPointCloudLayerRenderer::traverseTree( const QgsPointCloudIndex &pc, const QgsRenderContext &context, QgsPointCloudNodeId n, double maxErrorPixels, double nodeErrorPixels )
798{
799 QVector<QgsPointCloudNodeId> nodes;
800
801 if ( context.renderingStopped() )
802 {
803 QgsDebugMsgLevel( QStringLiteral( "canceled" ), 2 );
804 return nodes;
805 }
806
807 QgsPointCloudNode node = pc.getNode( n );
808 QgsBox3D nodeExtent = node.bounds();
809
810 if ( !context.extent().intersects( nodeExtent.toRectangle() ) )
811 return nodes;
812
813 const QgsDoubleRange nodeZRange( nodeExtent.zMinimum(), nodeExtent.zMaximum() );
814 const QgsDoubleRange adjustedNodeZRange = QgsDoubleRange( nodeZRange.lower() + mZOffset, nodeZRange.upper() + mZOffset );
815 if ( !context.zRange().isInfinite() && !context.zRange().overlaps( adjustedNodeZRange ) )
816 return nodes;
817
818 if ( node.pointCount() > 0 )
819 nodes.append( n );
820
821 double childrenErrorPixels = nodeErrorPixels / 2.0;
822 if ( childrenErrorPixels < maxErrorPixels )
823 return nodes;
824
825 for ( const QgsPointCloudNodeId &nn : node.children() )
826 {
827 nodes += traverseTree( pc, context, nn, maxErrorPixels, childrenErrorPixels );
828 }
829
830 return nodes;
831}
832
The Qgis class provides global constants for use throughout the application.
Definition qgis.h:54
QFlags< MapLayerRendererFlag > MapLayerRendererFlags
Flags which control how map layer renderers behave.
Definition qgis.h:2676
PointCloudDrawOrder
Pointcloud rendering order for 2d views.
Definition qgis.h:4025
@ BottomToTop
Draw points with larger Z values last.
@ Default
Draw points in the order they are stored.
@ TopToBottom
Draw points with larger Z values first.
@ RenderOverviewAndExtents
Render point cloud extents over overview point cloud.
@ RenderExtents
Render only point cloud extents when zoomed out.
@ RenderOverview
Render overview point cloud when zoomed out.
@ VectorTile
Vector tile layer. Added in QGIS 3.14.
@ RenderPartialOutputOverPreviousCachedImage
When rendering temporary in-progress preview renders, these preview renders can be drawn over any pre...
@ RenderPartialOutputs
The renderer benefits from rendering temporary in-progress preview renders. These are temporary resul...
@ Local
Local means the source is a local file on the machine.
@ Remote
Remote means it's loaded through a protocol like HTTP.
@ Reverse
Reverse/inverse transform (from destination to source)
static QgsRuntimeProfiler * profiler()
Returns the application runtime profiler.
A 3-dimensional box composed of x, y, z coordinates.
Definition qgsbox3d.h:43
double zMaximum() const
Returns the maximum z value.
Definition qgsbox3d.h:274
QgsRectangle toRectangle() const
Converts the box to a 2D rectangle.
Definition qgsbox3d.h:394
double zMinimum() const
Returns the minimum z value.
Definition qgsbox3d.h:267
Class for doing transforms between two map coordinate systems.
void setBallparkTransformsAreAppropriate(bool appropriate)
Sets whether approximate "ballpark" results are appropriate for this coordinate transform.
QgsRectangle transformBoundingBox(const QgsRectangle &rectangle, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward, bool handle180Crossover=false) const
Transforms a rectangle from the source CRS to the destination CRS.
Custom exception class for Coordinate Reference System related exceptions.
QgsRange which stores a range of double values.
Definition qgsrange.h:237
bool isInfinite() const
Returns true if the range consists of all possible values.
Definition qgsrange.h:291
Stores digital elevation model in a raster image which may get updated as a part of map layer renderi...
static QRgb encodeElevation(float z)
Converts elevation value to an actual color.
QRgb * rawElevationImageData()
Returns pointer to the actual elevation image data.
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition qgsfeedback.h:44
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition qgsfeedback.h:53
void canceled()
Internal routines can connect to this signal if they use event loop.
static QPainterPath calculatePainterClipRegion(const QList< QgsMapClippingRegion > &regions, const QgsRenderContext &context, Qgis::LayerType layerType, bool &shouldClip)
Returns a QPainterPath representing the intersection of clipping regions from context which should be...
static QList< QgsMapClippingRegion > collectClippingRegionsForLayer(const QgsRenderContext &context, const QgsMapLayer *layer)
Collects the list of map clipping regions from a context which apply to a map layer.
Base class for utility classes that encapsulate information necessary for rendering of map layers.
bool mReadyToCompose
The flag must be set to false in renderer's constructor if wants to use the smarter map redraws funct...
static constexpr int MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE
Maximum time (in ms) to allow display of a previously cached preview image while rendering layers,...
QString layerId() const
Gets access to the ID of the layer rendered by this class.
QgsRenderContext * renderContext()
Returns the render context associated with the renderer.
QRectF transformBounds(const QRectF &bounds) const
Transforms a bounding box from map coordinates to device coordinates.
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
Collection of point cloud attributes.
void push_back(const QgsPointCloudAttribute &attribute)
Adds extra attribute.
const QgsPointCloudAttribute & at(int index) const
Returns the attribute at the specified index.
QVector< QgsPointCloudAttribute > attributes() const
Returns all attributes.
int indexOf(const QString &name) const
Returns the index of the attribute with the specified name.
Attribute for point cloud data pair of name and size in bytes.
Base class for handling loading QgsPointCloudBlock asynchronously.
QString errorStr()
Returns the error message string of the request.
void finished()
Emitted when the request processing has finished.
std::unique_ptr< QgsPointCloudBlock > takeBlock()
Returns the requested block.
Base class for storing raw data from point cloud nodes.
virtual QgsGeometry polygonBounds() const
Returns the polygon bounds of the layer.
A renderer for 2d visualisation of point clouds which shows the dataset's extents using a fill symbol...
Smart pointer for QgsAbstractPointCloudIndex.
int span() const
Returns the number of points in one direction in a single node.
QgsVector3D offset() const
Returns offset of data from CRS.
QgsVector3D scale() const
Returns scale of data relative to CRS.
QgsPointCloudBlockRequest * asyncNodeData(const QgsPointCloudNodeId &n, const QgsPointCloudRequest &request)
Returns a handle responsible for loading a node data block.
bool isValid() const
Returns whether index is loaded and valid.
QgsRectangle extent() const
Returns extent of the data.
std::unique_ptr< QgsPointCloudBlock > nodeData(const QgsPointCloudNodeId &n, const QgsPointCloudRequest &request)
Returns node data block.
QgsPointCloudNodeId root() const
Returns root node of the index.
QgsPointCloudNode getNode(const QgsPointCloudNodeId &id) const
Returns object for a given node.
Qgis::PointCloudAccessType accessType() const
Returns the access type of the data If the access type is Remote, data will be fetched from an HTTP s...
Point cloud layer specific subclass of QgsMapLayerElevationProperties.
bool forceRasterRender() const override
Returns true if the renderer must be rendered to a raster paint device (e.g.
QgsPointCloudLayerRenderer(QgsPointCloudLayer *layer, QgsRenderContext &context)
Ctor.
void setLayerRenderingTimeHint(int time) override
Sets approximate render time (in ms) for the layer to render.
bool render() override
Do the rendering (based on data stored in the class).
Qgis::MapLayerRendererFlags flags() const override
Returns flags which control how the map layer rendering behaves.
Represents a map layer supporting display of point clouds.
QgsMapLayerElevationProperties * elevationProperties() override
Returns the layer's elevation properties.
QgsPointCloudRenderer * renderer()
Returns the 2D renderer for the point cloud.
QgsPointCloudIndex index() const
Returns the point cloud index associated with the layer.
QgsPointCloudDataProvider * dataProvider() override
Returns the layer's data provider, it may be nullptr.
Represents a indexed point cloud node's position in octree.
QString toString() const
Encode node to string.
Keeps metadata for indexed point cloud node.
qint64 pointCount() const
Returns number of points contained in node data.
QgsBox3D bounds() const
Returns node's bounding cube in CRS coords.
Encapsulates the render context for a 2D point cloud rendering operation.
int yOffset() const
Returns the offset for the y value in a point record.
QgsVector3D offset() const
Returns the offset of the layer's int32 coordinates compared to CRS coords.
QgsRenderContext & renderContext()
Returns a reference to the context's render context.
void setOffset(const QgsVector3D &offset)
Sets the offset of the layer's int32 coordinates compared to CRS coords.
void setScale(const QgsVector3D &scale)
Sets the scale of the layer's int32 coordinates compared to CRS coords.
int pointRecordSize() const
Returns the size of a single point record.
int xOffset() const
Returns the offset for the x value in a point record.
QgsVector3D scale() const
Returns the scale of the layer's int32 coordinates compared to CRS coords.
TriangulationData & triangulationData()
Returns reference to the triangulation data structure (only used when rendering as triangles is enabl...
int zOffset() const
Returns the offset for the y value in a point record.
QgsFeedback * feedback() const
Returns the feedback object used to cancel rendering.
void setAttributes(const QgsPointCloudAttributeCollection &attributes)
Sets the attributes associated with the rendered block.
virtual QgsPointCloudRenderer * clone() const =0
Create a deep copy of this renderer.
Point cloud data request.
void setAttributes(const QgsPointCloudAttributeCollection &attributes)
Set attributes filter in the request.
bool overlaps(const QgsRange< T > &other) const
Returns true if this range overlaps another range.
Definition qgsrange.h:176
A rectangle specified with double values.
double xMinimum
double yMinimum
double xMaximum
bool intersects(const QgsRectangle &rect) const
Returns true when rectangle intersects with other rectangle.
double yMaximum
Contains information about the context of a rendering operation.
double convertToPainterUnits(double size, Qgis::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale(), Qgis::RenderSubcomponentProperty property=Qgis::RenderSubcomponentProperty::Generic) const
Converts a size from the specified units to painter units (pixels).
QPainter * painter()
Returns the destination QPainter for the render operation.
void setPainterFlagsUsingContext(QPainter *painter=nullptr) const
Sets relevant flags on a destination painter, using the flags and settings currently defined for the ...
QgsElevationMap * elevationMap() const
Returns the destination elevation map for the render operation.
const QgsRectangle & extent() const
When rendering a map layer, calling this method returns the "clipping" extent for the layer (in the l...
float devicePixelRatio() const
Returns the device pixel ratio.
const QgsMapToPixel & mapToPixel() const
Returns the context's map to pixel transform, which transforms between map coordinates and device coo...
void setPainter(QPainter *p)
Sets the destination QPainter for the render operation.
QgsDoubleRange zRange() const
Returns the range of z-values which should be rendered.
QSize deviceOutputSize() const
Returns the device output size of the render.
bool renderingStopped() const
Returns true if the rendering operation has been stopped and any ongoing rendering should be canceled...
QPainter * previewRenderPainter()
Returns the const destination QPainter for temporary in-progress preview renders.
QgsCoordinateTransform coordinateTransform() const
Returns the current coordinate transform for the context.
void record(const QString &name, double time, const QString &group="startup", const QString &id=QString())
Manually adds a profile event with the given name and total time (in seconds).
Scoped object for saving and restoring a QPainter object's state.
Class for storage of 3D vectors similar to QVector3D, with the difference that it uses double precisi...
Definition qgsvector3d.h:31
double y() const
Returns Y coordinate.
Definition qgsvector3d.h:50
double z() const
Returns Z coordinate.
Definition qgsvector3d.h:52
double x() const
Returns X coordinate.
Definition qgsvector3d.h:48
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h: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...