Branch data Line data Source code
1 : : /*************************************************************************** 2 : : qgspointcloudlayerrenderer.cpp 3 : : -------------------- 4 : : begin : October 2020 5 : : copyright : (C) 2020 by Peter Petrik 6 : : email : zilolv at gmail dot com 7 : : ***************************************************************************/ 8 : : 9 : : /*************************************************************************** 10 : : * * 11 : : * This program is free software; you can redistribute it and/or modify * 12 : : * it under the terms of the GNU General Public License as published by * 13 : : * the Free Software Foundation; either version 2 of the License, or * 14 : : * (at your option) any later version. * 15 : : * * 16 : : ***************************************************************************/ 17 : : 18 : : #include <QElapsedTimer> 19 : : #include <QPointer> 20 : : 21 : : #include "qgspointcloudlayerrenderer.h" 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" 30 : : #include "qgspointcloudextentrenderer.h" 31 : : #include "qgslogger.h" 32 : : #include "qgspointcloudlayerelevationproperties.h" 33 : : #include "qgsmessagelog.h" 34 : : #include "qgscircle.h" 35 : : #include "qgsmapclippingutils.h" 36 : : 37 : : 38 : 0 : QgsPointCloudLayerRenderer::QgsPointCloudLayerRenderer( QgsPointCloudLayer *layer, QgsRenderContext &context ) 39 : 0 : : QgsMapLayerRenderer( layer->id(), &context ) 40 : 0 : , mLayer( layer ) 41 : 0 : , mLayerAttributes( layer->attributes() ) 42 : 0 : { 43 : : // TODO: we must not keep pointer to mLayer (it's dangerous) - we must copy anything we need for rendering 44 : : // or use some locking to prevent read/write from multiple threads 45 : 0 : if ( !mLayer || !mLayer->dataProvider() || !mLayer->renderer() ) 46 : 0 : return; 47 : : 48 : 0 : mRenderer.reset( mLayer->renderer()->clone() ); 49 : : 50 : 0 : if ( mLayer->dataProvider()->index() ) 51 : : { 52 : 0 : mScale = mLayer->dataProvider()->index()->scale(); 53 : 0 : mOffset = mLayer->dataProvider()->index()->offset(); 54 : 0 : } 55 : : 56 : 0 : if ( const QgsPointCloudLayerElevationProperties *elevationProps = qobject_cast< const QgsPointCloudLayerElevationProperties * >( mLayer->elevationProperties() ) ) 57 : : { 58 : 0 : mZOffset = elevationProps->zOffset(); 59 : 0 : mZScale = elevationProps->zScale(); 60 : 0 : } 61 : : 62 : 0 : mCloudExtent = mLayer->dataProvider()->polygonBounds(); 63 : : 64 : 0 : mClippingRegions = QgsMapClippingUtils::collectClippingRegionsForLayer( *renderContext(), layer ); 65 : : 66 : 0 : mReadyToCompose = false; 67 : 0 : } 68 : : 69 : 0 : bool QgsPointCloudLayerRenderer::render() 70 : : { 71 : 0 : QgsPointCloudRenderContext context( *renderContext(), mScale, mOffset, mZScale, mZOffset ); 72 : : 73 : : // Set up the render configuration options 74 : 0 : QPainter *painter = context.renderContext().painter(); 75 : : 76 : 0 : QgsScopedQPainterState painterState( painter ); 77 : 0 : context.renderContext().setPainterFlagsUsingContext( painter ); 78 : 0 : 79 : 0 : if ( !mClippingRegions.empty() ) 80 : : { 81 : 0 : bool needsPainterClipPath = false; 82 : 0 : const QPainterPath path = QgsMapClippingUtils::calculatePainterClipRegion( mClippingRegions, *renderContext(), QgsMapLayerType::VectorTileLayer, needsPainterClipPath ); 83 : 0 : if ( needsPainterClipPath ) 84 : 0 : renderContext()->painter()->setClipPath( path, Qt::IntersectClip ); 85 : 0 : } 86 : 0 : 87 : 0 : if ( mRenderer->type() == QLatin1String( "extent" ) ) 88 : : { 89 : : // special case for extent only renderer! 90 : 0 : mRenderer->startRender( context ); 91 : 0 : static_cast< QgsPointCloudExtentRenderer * >( mRenderer.get() )->renderExtent( mCloudExtent, context ); 92 : 0 : mRenderer->stopRender( context ); 93 : 0 : mReadyToCompose = true; 94 : 0 : return true; 95 : : } 96 : : 97 : : // TODO cache!? 98 : 0 : QgsPointCloudIndex *pc = mLayer->dataProvider()->index(); 99 : 0 : if ( !pc || !pc->isValid() ) 100 : : { 101 : 0 : mReadyToCompose = true; 102 : 0 : return false; 103 : : } 104 : : 105 : : // if the previous layer render was relatively quick (e.g. less than 3 seconds), the we show any previously 106 : : // cached version of the layer during rendering instead of the usual progressive updates 107 : 0 : if ( mRenderTimeHint > 0 && mRenderTimeHint <= MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE ) 108 : : { 109 : 0 : mBlockRenderUpdates = true; 110 : 0 : mElapsedTimer.start(); 111 : 0 : } 112 : : 113 : 0 : mRenderer->startRender( context ); 114 : : 115 : 0 : mAttributes.push_back( QgsPointCloudAttribute( QStringLiteral( "X" ), QgsPointCloudAttribute::Int32 ) ); 116 : 0 : mAttributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Y" ), QgsPointCloudAttribute::Int32 ) ); 117 : : 118 : : // collect attributes required by renderer 119 : 0 : QSet< QString > rendererAttributes = mRenderer->usedAttributes( context ); 120 : : 121 : 0 : if ( !context.renderContext().zRange().isInfinite() ) 122 : 0 : rendererAttributes.insert( QStringLiteral( "Z" ) ); 123 : : 124 : 0 : for ( const QString &attribute : std::as_const( rendererAttributes ) ) 125 : : { 126 : 0 : if ( mAttributes.indexOf( attribute ) >= 0 ) 127 : 0 : continue; // don't re-add attributes we are already going to fetch 128 : : 129 : 0 : const int layerIndex = mLayerAttributes.indexOf( attribute ); 130 : 0 : if ( layerIndex < 0 ) 131 : : { 132 : 0 : QgsMessageLog::logMessage( QObject::tr( "Required attribute %1 not found in layer" ).arg( attribute ), QObject::tr( "Point Cloud" ) ); 133 : 0 : continue; 134 : : } 135 : : 136 : 0 : mAttributes.push_back( mLayerAttributes.at( layerIndex ) ); 137 : : } 138 : : 139 : 0 : QgsPointCloudDataBounds db; 140 : : 141 : : #ifdef QGISDEBUG 142 : : QElapsedTimer t; 143 : : t.start(); 144 : : #endif 145 : : 146 : 0 : const IndexedPointCloudNode root = pc->root(); 147 : : 148 : 0 : const double maximumError = context.renderContext().convertToPainterUnits( mRenderer->maximumScreenError(), mRenderer->maximumScreenErrorUnit() );// in pixels 149 : : 150 : 0 : const QgsRectangle rootNodeExtentLayerCoords = pc->nodeMapExtent( root ); 151 : 0 : QgsRectangle rootNodeExtentMapCoords; 152 : : try 153 : : { 154 : 0 : rootNodeExtentMapCoords = context.renderContext().coordinateTransform().transformBoundingBox( rootNodeExtentLayerCoords ); 155 : 0 : } 156 : : catch ( QgsCsException & ) 157 : : { 158 : 0 : QgsDebugMsg( QStringLiteral( "Could not transform node extent to map CRS" ) ); 159 : 0 : rootNodeExtentMapCoords = rootNodeExtentLayerCoords; 160 : 0 : } 161 : : 162 : 0 : const double rootErrorInMapCoordinates = rootNodeExtentMapCoords.width() / pc->span(); // in map coords 163 : : 164 : 0 : double mapUnitsPerPixel = context.renderContext().mapToPixel().mapUnitsPerPixel(); 165 : 0 : if ( ( rootErrorInMapCoordinates < 0.0 ) || ( mapUnitsPerPixel < 0.0 ) || ( maximumError < 0.0 ) ) 166 : : { 167 : 0 : QgsDebugMsg( QStringLiteral( "invalid screen error" ) ); 168 : 0 : mReadyToCompose = true; 169 : 0 : return false; 170 : : } 171 : 0 : double rootErrorPixels = rootErrorInMapCoordinates / mapUnitsPerPixel; // in pixels 172 : 0 : const QVector<IndexedPointCloudNode> nodes = traverseTree( pc, context.renderContext(), pc->root(), maximumError, rootErrorPixels ); 173 : : 174 : 0 : QgsPointCloudRequest request; 175 : 0 : request.setAttributes( mAttributes ); 176 : : 177 : : // drawing 178 : 0 : int nodesDrawn = 0; 179 : 0 : bool canceled = false; 180 : 0 : for ( const IndexedPointCloudNode &n : nodes ) 181 : : { 182 : 0 : if ( context.renderContext().renderingStopped() ) 183 : : { 184 : 0 : QgsDebugMsgLevel( "canceled", 2 ); 185 : 0 : canceled = true; 186 : 0 : break; 187 : : } 188 : 0 : std::unique_ptr<QgsPointCloudBlock> block( pc->nodeData( n, request ) ); 189 : : 190 : 0 : if ( !block ) 191 : 0 : continue; 192 : : 193 : 0 : context.setAttributes( block->attributes() ); 194 : : 195 : 0 : mRenderer->renderBlock( block.get(), context ); 196 : 0 : ++nodesDrawn; 197 : : 198 : : // as soon as first block is rendered, we can start showing layer updates. 199 : : // but if we are blocking render updates (so that a previously cached image is being shown), we wait 200 : : // at most e.g. 3 seconds before we start forcing progressive updates. 201 : 0 : if ( !mBlockRenderUpdates || mElapsedTimer.elapsed() > MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE ) 202 : : { 203 : 0 : mReadyToCompose = true; 204 : 0 : } 205 : 0 : } 206 : : 207 : 0 : QgsDebugMsgLevel( QStringLiteral( "totals: %1 nodes | %2 points | %3ms" ).arg( nodesDrawn ) 208 : : .arg( context.pointsRendered() ) 209 : : .arg( t.elapsed() ), 2 ); 210 : : 211 : 0 : mRenderer->stopRender( context ); 212 : : 213 : 0 : mReadyToCompose = true; 214 : 0 : return !canceled; 215 : 0 : } 216 : : 217 : 0 : bool QgsPointCloudLayerRenderer::forceRasterRender() const 218 : : { 219 : : // unless we are using the extent only renderer, point cloud layers should always be rasterized -- we don't want to export points as vectors 220 : : // to formats like PDF! 221 : 0 : return mRenderer ? mRenderer->type() != QLatin1String( "extent" ) : false; 222 : : } 223 : : 224 : 0 : void QgsPointCloudLayerRenderer::setLayerRenderingTimeHint( int time ) 225 : : { 226 : 0 : mRenderTimeHint = time; 227 : 0 : } 228 : : 229 : 0 : QVector<IndexedPointCloudNode> QgsPointCloudLayerRenderer::traverseTree( const QgsPointCloudIndex *pc, 230 : : const QgsRenderContext &context, 231 : : IndexedPointCloudNode n, 232 : : double maxErrorPixels, 233 : : double nodeErrorPixels ) 234 : : { 235 : 0 : QVector<IndexedPointCloudNode> nodes; 236 : : 237 : 0 : if ( context.renderingStopped() ) 238 : : { 239 : 0 : QgsDebugMsgLevel( QStringLiteral( "canceled" ), 2 ); 240 : 0 : return nodes; 241 : : } 242 : : 243 : 0 : if ( !context.extent().intersects( pc->nodeMapExtent( n ) ) ) 244 : 0 : return nodes; 245 : : 246 : 0 : const QgsDoubleRange nodeZRange = pc->nodeZRange( n ); 247 : 0 : const QgsDoubleRange adjustedNodeZRange = QgsDoubleRange( nodeZRange.lower() + mZOffset, nodeZRange.upper() + mZOffset ); 248 : 0 : if ( !context.zRange().isInfinite() && !context.zRange().overlaps( adjustedNodeZRange ) ) 249 : 0 : return nodes; 250 : : 251 : 0 : nodes.append( n ); 252 : : 253 : 0 : double childrenErrorPixels = nodeErrorPixels / 2.0; 254 : 0 : if ( childrenErrorPixels < maxErrorPixels ) 255 : 0 : return nodes; 256 : : 257 : 0 : const QList<IndexedPointCloudNode> children = pc->nodeChildren( n ); 258 : 0 : for ( const IndexedPointCloudNode &nn : children ) 259 : : { 260 : 0 : nodes += traverseTree( pc, context, nn, maxErrorPixels, childrenErrorPixels ); 261 : : } 262 : : 263 : 0 : return nodes; 264 : 0 : } 265 : : 266 : 0 : QgsPointCloudLayerRenderer::~QgsPointCloudLayerRenderer() = default; 267 : :