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