QGIS API Documentation  3.22.4-Białowieża (ce8e65e95e)
qgspointcloudlayerrenderer.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgspointcloudlayerrenderer.cpp
3  --------------------
4  begin : October 2020
5  copyright : (C) 2020 by Peter Petrik
6  email : zilolv at gmail dot com
7  ***************************************************************************/
8 
9 /***************************************************************************
10  * *
11  * This program is free software; you can redistribute it and/or modify *
12  * it under the terms of the GNU General Public License as published by *
13  * the Free Software Foundation; either version 2 of the License, or *
14  * (at your option) any later version. *
15  * *
16  ***************************************************************************/
17 
18 #include <QElapsedTimer>
19 #include <QPointer>
20 
22 #include "qgspointcloudlayer.h"
23 #include "qgsrendercontext.h"
24 #include "qgspointcloudindex.h"
25 #include "qgsstyle.h"
26 #include "qgscolorramp.h"
27 #include "qgspointcloudrequest.h"
28 #include "qgspointcloudattribute.h"
29 #include "qgspointcloudrenderer.h"
31 #include "qgslogger.h"
33 #include "qgsmessagelog.h"
34 #include "qgscircle.h"
35 #include "qgsmapclippingutils.h"
37 
39  : QgsMapLayerRenderer( layer->id(), &context )
40  , mLayer( layer )
41  , mLayerAttributes( layer->attributes() )
42  , mFeedback( new QgsFeedback )
43 {
44  // TODO: we must not keep pointer to mLayer (it's dangerous) - we must copy anything we need for rendering
45  // or use some locking to prevent read/write from multiple threads
46  if ( !mLayer || !mLayer->dataProvider() || !mLayer->renderer() )
47  return;
48 
49  mRenderer.reset( mLayer->renderer()->clone() );
50 
51  if ( mLayer->dataProvider()->index() )
52  {
53  mScale = mLayer->dataProvider()->index()->scale();
54  mOffset = mLayer->dataProvider()->index()->offset();
55  }
56 
57  if ( const QgsPointCloudLayerElevationProperties *elevationProps = qobject_cast< const QgsPointCloudLayerElevationProperties * >( mLayer->elevationProperties() ) )
58  {
59  mZOffset = elevationProps->zOffset();
60  mZScale = elevationProps->zScale();
61  }
62 
63  mCloudExtent = mLayer->dataProvider()->polygonBounds();
64 
66 
67  mReadyToCompose = false;
68 }
69 
71 {
72  QgsPointCloudRenderContext context( *renderContext(), mScale, mOffset, mZScale, mZOffset, mFeedback.get() );
73 
74  // Set up the render configuration options
75  QPainter *painter = context.renderContext().painter();
76 
77  QgsScopedQPainterState painterState( painter );
78  context.renderContext().setPainterFlagsUsingContext( painter );
79 
80  if ( !mClippingRegions.empty() )
81  {
82  bool needsPainterClipPath = false;
83  const QPainterPath path = QgsMapClippingUtils::calculatePainterClipRegion( mClippingRegions, *renderContext(), QgsMapLayerType::VectorTileLayer, needsPainterClipPath );
84  if ( needsPainterClipPath )
85  renderContext()->painter()->setClipPath( path, Qt::IntersectClip );
86  }
87 
88  if ( mRenderer->type() == QLatin1String( "extent" ) )
89  {
90  // special case for extent only renderer!
91  mRenderer->startRender( context );
92  static_cast< QgsPointCloudExtentRenderer * >( mRenderer.get() )->renderExtent( mCloudExtent, context );
93  mRenderer->stopRender( context );
94  mReadyToCompose = true;
95  return true;
96  }
97 
98  // TODO cache!?
99  QgsPointCloudIndex *pc = mLayer->dataProvider()->index();
100  if ( !pc || !pc->isValid() )
101  {
102  mReadyToCompose = true;
103  return false;
104  }
105 
106  // if the previous layer render was relatively quick (e.g. less than 3 seconds), the we show any previously
107  // cached version of the layer during rendering instead of the usual progressive updates
108  if ( mRenderTimeHint > 0 && mRenderTimeHint <= MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE )
109  {
110  mBlockRenderUpdates = true;
111  mElapsedTimer.start();
112  }
113 
114  mRenderer->startRender( context );
115 
116  mAttributes.push_back( QgsPointCloudAttribute( QStringLiteral( "X" ), QgsPointCloudAttribute::Int32 ) );
117  mAttributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Y" ), QgsPointCloudAttribute::Int32 ) );
118 
119  // collect attributes required by renderer
120  QSet< QString > rendererAttributes = mRenderer->usedAttributes( context );
121 
122  if ( !context.renderContext().zRange().isInfinite() )
123  rendererAttributes.insert( QStringLiteral( "Z" ) );
124 
125  for ( const QString &attribute : std::as_const( rendererAttributes ) )
126  {
127  if ( mAttributes.indexOf( attribute ) >= 0 )
128  continue; // don't re-add attributes we are already going to fetch
129 
130  const int layerIndex = mLayerAttributes.indexOf( attribute );
131  if ( layerIndex < 0 )
132  {
133  QgsMessageLog::logMessage( QObject::tr( "Required attribute %1 not found in layer" ).arg( attribute ), QObject::tr( "Point Cloud" ) );
134  continue;
135  }
136 
137  mAttributes.push_back( mLayerAttributes.at( layerIndex ) );
138  }
139 
141 
142 #ifdef QGISDEBUG
143  QElapsedTimer t;
144  t.start();
145 #endif
146 
147  const IndexedPointCloudNode root = pc->root();
148 
149  const double maximumError = context.renderContext().convertToPainterUnits( mRenderer->maximumScreenError(), mRenderer->maximumScreenErrorUnit() );// in pixels
150 
151  const QgsRectangle rootNodeExtentLayerCoords = pc->nodeMapExtent( root );
152  QgsRectangle rootNodeExtentMapCoords;
154  {
155  try
156  {
157  QgsCoordinateTransform extentTransform = context.renderContext().coordinateTransform();
158  extentTransform.setBallparkTransformsAreAppropriate( true );
159  rootNodeExtentMapCoords = extentTransform.transformBoundingBox( rootNodeExtentLayerCoords );
160  }
161  catch ( QgsCsException & )
162  {
163  QgsDebugMsg( QStringLiteral( "Could not transform node extent to map CRS" ) );
164  rootNodeExtentMapCoords = rootNodeExtentLayerCoords;
165  }
166  }
167  else
168  {
169  rootNodeExtentMapCoords = rootNodeExtentLayerCoords;
170  }
171 
172  const double rootErrorInMapCoordinates = rootNodeExtentMapCoords.width() / pc->span(); // in map coords
173 
174  double mapUnitsPerPixel = context.renderContext().mapToPixel().mapUnitsPerPixel();
175  if ( ( rootErrorInMapCoordinates < 0.0 ) || ( mapUnitsPerPixel < 0.0 ) || ( maximumError < 0.0 ) )
176  {
177  QgsDebugMsg( QStringLiteral( "invalid screen error" ) );
178  mReadyToCompose = true;
179  return false;
180  }
181  double rootErrorPixels = rootErrorInMapCoordinates / mapUnitsPerPixel; // in pixels
182  const QVector<IndexedPointCloudNode> nodes = traverseTree( pc, context.renderContext(), pc->root(), maximumError, rootErrorPixels );
183 
184  QgsPointCloudRequest request;
185  request.setAttributes( mAttributes );
186 
187  // drawing
188  int nodesDrawn = 0;
189  bool canceled = false;
190 
191  if ( pc->accessType() == QgsPointCloudIndex::AccessType::Local )
192  {
193  nodesDrawn += renderNodesSync( nodes, pc, context, request, canceled );
194  }
195  else if ( pc->accessType() == QgsPointCloudIndex::AccessType::Remote )
196  {
197  nodesDrawn += renderNodesAsync( nodes, pc, context, request, canceled );
198  }
199 
200 #ifdef QGISDEBUG
201  QgsDebugMsgLevel( QStringLiteral( "totals: %1 nodes | %2 points | %3ms" ).arg( nodesDrawn )
202  .arg( context.pointsRendered() )
203  .arg( t.elapsed() ), 2 );
204 #else
205  ( void )nodesDrawn;
206 #endif
207 
208  mRenderer->stopRender( context );
209 
210  mReadyToCompose = true;
211  return !canceled;
212 }
213 
214 int QgsPointCloudLayerRenderer::renderNodesSync( const QVector<IndexedPointCloudNode> &nodes, QgsPointCloudIndex *pc, QgsPointCloudRenderContext &context, QgsPointCloudRequest &request, bool &canceled )
215 {
216  int nodesDrawn = 0;
217  for ( const IndexedPointCloudNode &n : nodes )
218  {
219  if ( context.renderContext().renderingStopped() )
220  {
221  QgsDebugMsgLevel( "canceled", 2 );
222  canceled = true;
223  break;
224  }
225  std::unique_ptr<QgsPointCloudBlock> block( pc->nodeData( n, request ) );
226 
227  if ( !block )
228  continue;
229 
230  QgsVector3D contextScale = context.scale();
231  QgsVector3D contextOffset = context.offset();
232 
233  context.setScale( block->scale() );
234  context.setOffset( block->offset() );
235 
236  context.setAttributes( block->attributes() );
237 
238  mRenderer->renderBlock( block.get(), context );
239 
240  context.setScale( contextScale );
241  context.setOffset( contextOffset );
242 
243  ++nodesDrawn;
244 
245  // as soon as first block is rendered, we can start showing layer updates.
246  // but if we are blocking render updates (so that a previously cached image is being shown), we wait
247  // at most e.g. 3 seconds before we start forcing progressive updates.
248  if ( !mBlockRenderUpdates || mElapsedTimer.elapsed() > MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE )
249  {
250  mReadyToCompose = true;
251  }
252  }
253  return nodesDrawn;
254 }
255 
256 int QgsPointCloudLayerRenderer::renderNodesAsync( const QVector<IndexedPointCloudNode> &nodes, QgsPointCloudIndex *pc, QgsPointCloudRenderContext &context, QgsPointCloudRequest &request, bool &canceled )
257 {
258  int nodesDrawn = 0;
259 
260  QElapsedTimer downloadTimer;
261  downloadTimer.start();
262 
263  // Instead of loading all point blocks in parallel and then rendering the one by one,
264  // we split the processing into groups of size groupSize where we load the blocks of the group
265  // in parallel and then render the group's blocks sequentially.
266  // This way helps QGIS stay responsive if the nodes vector size is big
267  const int groupSize = 4;
268  for ( int groupIndex = 0; groupIndex < nodes.size(); groupIndex += groupSize )
269  {
270  if ( context.feedback() && context.feedback()->isCanceled() )
271  break;
272  // Async loading of nodes
273  const int currentGroupSize = std::min< size_t >( std::max< size_t >( nodes.size() - groupIndex, 0 ), groupSize );
274  QVector<QgsPointCloudBlockRequest *> blockRequests( currentGroupSize, nullptr );
275  QVector<bool> finishedLoadingBlock( currentGroupSize, false );
276  QEventLoop loop;
277  if ( context.feedback() )
278  QObject::connect( context.feedback(), &QgsFeedback::canceled, &loop, &QEventLoop::quit );
279  // Note: All capture by reference warnings here shouldn't be an issue since we have an event loop, so locals won't be deallocated
280  for ( int i = 0; i < blockRequests.size(); ++i )
281  {
282  int nodeIndex = groupIndex + i;
283  const IndexedPointCloudNode &n = nodes[nodeIndex];
284  const QString nStr = n.toString();
285  QgsPointCloudBlockRequest *blockRequest = pc->asyncNodeData( n, request );
286  blockRequests[ i ] = blockRequest;
287  QObject::connect( blockRequest, &QgsPointCloudBlockRequest::finished, &loop, [ &, i, nStr, blockRequest ]()
288  {
289  if ( !blockRequest->block() )
290  {
291  QgsDebugMsg( QStringLiteral( "Unable to load node %1, error: %2" ).arg( nStr, blockRequest->errorStr() ) );
292  }
293  finishedLoadingBlock[ i ] = true;
294  // If all blocks are loaded, exit the event loop
295  if ( !finishedLoadingBlock.contains( false ) ) loop.exit();
296  } );
297  }
298  // Wait for all point cloud nodes to finish loading
299  loop.exec();
300 
301  QgsDebugMsg( QStringLiteral( "Downloaded in : %1ms" ).arg( downloadTimer.elapsed() ) );
302  if ( !context.feedback()->isCanceled() )
303  {
304  // Render all the point cloud blocks sequentially
305  for ( int i = 0; i < blockRequests.size(); ++i )
306  {
307  if ( context.renderContext().renderingStopped() )
308  {
309  QgsDebugMsgLevel( "canceled", 2 );
310  canceled = true;
311  break;
312  }
313 
314  if ( !blockRequests[ i ]->block() )
315  continue;
316 
317  QgsVector3D contextScale = context.scale();
318  QgsVector3D contextOffset = context.offset();
319 
320  context.setScale( blockRequests[ i ]->block()->scale() );
321  context.setOffset( blockRequests[ i ]->block()->offset() );
322 
323  context.setAttributes( blockRequests[ i ]->block()->attributes() );
324 
325  mRenderer->renderBlock( blockRequests[ i ]->block(), context );
326 
327  context.setScale( contextScale );
328  context.setOffset( contextOffset );
329 
330  ++nodesDrawn;
331 
332  // as soon as first block is rendered, we can start showing layer updates.
333  // but if we are blocking render updates (so that a previously cached image is being shown), we wait
334  // at most e.g. 3 seconds before we start forcing progressive updates.
335  if ( !mBlockRenderUpdates || mElapsedTimer.elapsed() > MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE )
336  {
337  mReadyToCompose = true;
338  }
339  }
340  }
341 
342  for ( int i = 0; i < blockRequests.size(); ++i )
343  {
344  if ( blockRequests[ i ] )
345  {
346  if ( blockRequests[ i ]->block() )
347  delete blockRequests[ i ]->block();
348  blockRequests[ i ]->deleteLater();
349  }
350  }
351  }
352 
353  return nodesDrawn;
354 }
355 
357 {
358  // unless we are using the extent only renderer, point cloud layers should always be rasterized -- we don't want to export points as vectors
359  // to formats like PDF!
360  return mRenderer ? mRenderer->type() != QLatin1String( "extent" ) : false;
361 }
362 
364 {
365  mRenderTimeHint = time;
366 }
367 
368 QVector<IndexedPointCloudNode> QgsPointCloudLayerRenderer::traverseTree( const QgsPointCloudIndex *pc,
369  const QgsRenderContext &context,
371  double maxErrorPixels,
372  double nodeErrorPixels )
373 {
374  QVector<IndexedPointCloudNode> nodes;
375 
376  if ( context.renderingStopped() )
377  {
378  QgsDebugMsgLevel( QStringLiteral( "canceled" ), 2 );
379  return nodes;
380  }
381 
382  if ( !context.extent().intersects( pc->nodeMapExtent( n ) ) )
383  return nodes;
384 
385  const QgsDoubleRange nodeZRange = pc->nodeZRange( n );
386  const QgsDoubleRange adjustedNodeZRange = QgsDoubleRange( nodeZRange.lower() + mZOffset, nodeZRange.upper() + mZOffset );
387  if ( !context.zRange().isInfinite() && !context.zRange().overlaps( adjustedNodeZRange ) )
388  return nodes;
389 
390  nodes.append( n );
391 
392  double childrenErrorPixels = nodeErrorPixels / 2.0;
393  if ( childrenErrorPixels < maxErrorPixels )
394  return nodes;
395 
396  const QList<IndexedPointCloudNode> children = pc->nodeChildren( n );
397  for ( const IndexedPointCloudNode &nn : children )
398  {
399  nodes += traverseTree( pc, context, nn, maxErrorPixels, childrenErrorPixels );
400  }
401 
402  return nodes;
403 }
404 
Represents a indexed point cloud node in octree.
QString toString() const
Encode node to string.
Class for doing transforms between two map coordinate systems.
void setBallparkTransformsAreAppropriate(bool appropriate)
Sets whether approximate "ballpark" results are appropriate for this coordinate transform.
bool isShortCircuited() const
Returns true if the transform short circuits because the source and destination are equivalent.
QgsRectangle transformBoundingBox(const QgsRectangle &rectangle, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward, bool handle180Crossover=false) const SIP_THROW(QgsCsException)
Transforms a rectangle from the source CRS to the destination CRS.
Custom exception class for Coordinate Reference System related exceptions.
Definition: qgsexception.h:66
QgsRange which stores a range of double values.
Definition: qgsrange.h:203
bool isInfinite() const
Returns true if the range consists of all possible values.
Definition: qgsrange.h:247
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition: qgsfeedback.h:45
void canceled()
Internal routines can connect to this signal if they use event loop.
bool isCanceled() const SIP_HOLDGIL
Tells whether the operation has been canceled already.
Definition: qgsfeedback.h:54
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.
static QPainterPath calculatePainterClipRegion(const QList< QgsMapClippingRegion > &regions, const QgsRenderContext &context, QgsMapLayerType layerType, bool &shouldClip)
Returns a QPainterPath representing the intersection of clipping regions from context which should be...
Base class for utility classes that encapsulate information necessary for rendering of map layers.
bool mReadyToCompose
The flag must be set to false in renderer's constructor if wants to use the smarter map redraws funct...
static constexpr int MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE
Maximum time (in ms) to allow display of a previously cached preview image while rendering layers,...
QgsRenderContext * renderContext()
Returns the render context associated with the renderer.
double mapUnitsPerPixel() const
Returns the current map units per pixel.
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
void push_back(const QgsPointCloudAttribute &attribute)
Adds extra attribute.
const QgsPointCloudAttribute & at(int index) const
Returns the attribute at the specified index.
int indexOf(const QString &name) const
Returns the index of the attribute with the specified name.
Attribute for point cloud data pair of name and size in bytes.
Base class for handling loading QgsPointCloudBlock asynchronously.
QgsPointCloudBlock * block()
Returns the requested block.
void finished()
Emitted when the request processing has finished.
Represents packaged data bounds.
virtual QgsPointCloudIndex * index() const
Returns the point cloud index associated with the provider.
virtual QgsGeometry polygonBounds() const
Returns the polygon bounds of the layer.
A renderer for 2d visualisation of point clouds which shows the dataset's extents using a fill symbol...
Represents a indexed point clouds data in octree.
int span() const
Returns the number of points in one direction in a single node.
QgsRectangle nodeMapExtent(const IndexedPointCloudNode &node) const
Returns the extent of a node in map coordinates.
virtual QList< IndexedPointCloudNode > nodeChildren(const IndexedPointCloudNode &n) const
Returns all children of node.
QgsVector3D offset() const
Returns offset.
QgsVector3D scale() const
Returns scale.
virtual QgsPointCloudBlock * nodeData(const IndexedPointCloudNode &n, const QgsPointCloudRequest &request)=0
Returns node data block.
virtual AccessType accessType() const =0
Returns the access type of the data If the access type is Remote, data will be fetched from an HTTP s...
virtual bool isValid() const =0
Returns whether index is loaded and valid.
IndexedPointCloudNode root()
Returns root node of the index.
QgsDoubleRange nodeZRange(const IndexedPointCloudNode &node) const
Returns the z range of a node.
virtual QgsPointCloudBlockRequest * asyncNodeData(const IndexedPointCloudNode &n, const QgsPointCloudRequest &request)=0
Returns a handle responsible for loading a node data block.
Point cloud layer specific subclass of QgsMapLayerElevationProperties.
bool forceRasterRender() const override
Returns true if the renderer must be rendered to a raster paint device (e.g.
QgsPointCloudLayerRenderer(QgsPointCloudLayer *layer, QgsRenderContext &context)
Ctor.
void setLayerRenderingTimeHint(int time) override
Sets approximate render time (in ms) for the layer to render.
bool render() override
Do the rendering (based on data stored in the class).
Represents a map layer supporting display of point clouds.
QgsMapLayerElevationProperties * elevationProperties() override
Returns the layer's elevation properties.
QgsPointCloudRenderer * renderer()
Returns the 2D renderer for the point cloud.
QgsPointCloudDataProvider * dataProvider() override
Returns the layer's data provider, it may be nullptr.
Encapsulates the render context for a 2D point cloud rendering operation.
QgsVector3D offset() const
Returns the offset of the layer's int32 coordinates compared to CRS coords.
long pointsRendered() const
Returns the total number of points rendered.
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.
QgsFeedback * feedback() const
Returns the feedback object used to cancel rendering.
QgsVector3D scale() const
Returns the scale of the layer's int32 coordinates compared to CRS coords.
QgsRenderContext & renderContext()
Returns a reference to the context's render context.
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:147
T lower() const
Returns the lower bound of the range.
Definition: qgsrange.h:66
T upper() const
Returns the upper bound of the range.
Definition: qgsrange.h:73
A rectangle specified with double values.
Definition: qgsrectangle.h:42
bool intersects(const QgsRectangle &rect) const SIP_HOLDGIL
Returns true when rectangle intersects with other rectangle.
Definition: qgsrectangle.h:349
double width() const SIP_HOLDGIL
Returns the width of the rectangle.
Definition: qgsrectangle.h:223
Contains information about the context of a rendering operation.
QPainter * painter()
Returns the destination QPainter for the render operation.
const QgsMapToPixel & mapToPixel() const
Returns the context's map to pixel transform, which transforms between map coordinates and device coo...
void setPainterFlagsUsingContext(QPainter *painter=nullptr) const
Sets relevant flags on a destination painter, using the flags and settings currently defined for the ...
double convertToPainterUnits(double size, QgsUnitTypes::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale(), Qgis::RenderSubcomponentProperty property=Qgis::RenderSubcomponentProperty::Generic) const
Converts a size from the specified units to painter units (pixels).
QgsDoubleRange zRange() const
Returns the range of z-values which should be rendered.
bool renderingStopped() const
Returns true if the rendering operation has been stopped and any ongoing rendering should be canceled...
QgsCoordinateTransform coordinateTransform() const
Returns the current coordinate transform for the context.
const QgsRectangle & extent() const
When rendering a map layer, calling this method returns the "clipping" extent for the layer (in the l...
Scoped object for saving and restoring a QPainter object's state.
@ VectorTileLayer
Added in 3.14.
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugMsg(str)
Definition: qgslogger.h:38