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