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