QGIS API Documentation 4.3.0-Master (52aa6503bed)
Loading...
Searching...
No Matches
qgsvectorlayerchunkloader_p.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsvectorlayerchunkloader_p.cpp
3 --------------------------------------
4 Date : July 2019
5 Copyright : (C) 2019 by Martin Dobias
6 Email : wonder dot sk at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
17
18#include <functional>
19
20#include "qgs3dsymbolregistry.h"
21#include "qgs3dutils.h"
22#include "qgsabstract3dsymbol.h"
25#include "qgsapplication.h"
26#include "qgschunknode.h"
27#include "qgseventtracing.h"
28#include "qgsexception.h"
31#include "qgsgeometry.h"
32#include "qgsglobeutils_p.h"
33#include "qgsline3dsymbol.h"
34#include "qgslogger.h"
35#include "qgspoint3dsymbol.h"
36#include "qgspolygon3dsymbol.h"
37#include "qgsthreadingutils.h"
38#include "qgsvectorlayer.h"
40#include "qgswkbtypes.h"
41
42#include <QFuture>
43#include <QString>
44#include <Qt3DCore/QTransform>
45#include <Qt3DRender/QGeometryRenderer>
46#include <QtConcurrentRun>
47
48#include "moc_qgsvectorlayerchunkloader_p.cpp"
49
50using namespace Qt::StringLiterals;
51
53
55
56
57QgsVectorLayerChunkLoader::QgsVectorLayerChunkLoader( const Qgs3DRenderContext &context, QgsVectorLayer *vl, QgsAbstract3DSymbol *symbol, double zMin, double zMax, int maxFeatures )
58 : mRenderContext( context )
59 , mLayer( vl )
60 , mSymbol( symbol->clone() )
61 , mNodesAreLeafs( std::make_shared<NodeIsLeafMap>() )
62 , mMaxFeatures( maxFeatures )
63{
64 if ( context.crs().type() == Qgis::CrsType::Geocentric )
65 {
66 // TODO: add support for handling of vector layers (other than points)
67 if ( QgsWkbTypes::geometryType( mLayer->wkbType() ) != Qgis::GeometryType::Point )
68 {
69 // (we're using dummy quadtree here to make sure the empty extent does not break the scene completely)
70 QgsDebugError( u"Non-point vector layers in globe scenes are not supported yet!"_s );
71 setupQuadtree( QgsBox3D( -7e6, -7e6, -7e6, 7e6, 7e6, 7e6 ), -1, 3 );
72 return;
73 }
74
75 mIsGeocentric = true;
76
77 const QgsCoordinateReferenceSystem geographicCrs = context.crs().toGeographicCrs();
78 mCrsToLatLon = QgsCoordinateTransform( context.crs(), geographicCrs, context.transformContext() );
79 mCrsToLatLon.setBallparkTransformsAreAppropriate( true );
80
81 try
82 {
83 mRadius = QgsGlobeUtils::ellipsoidRadius( mCrsToLatLon );
84 }
85 catch ( QgsCsException &e )
86 {
87 QgsDebugError( u"Error transforming globe ellipsoid extent: %1"_s.arg( e.what() ) );
88 setupQuadtree( QgsBox3D( -7e6, -7e6, -7e6, 7e6, 7e6, 7e6 ), -1, 3 );
89 return;
90 }
91
92 // choose the smaller root extent between context and mLayer ones
93 QgsRectangle layerExtentLonLat;
94 if ( mLayer->crs().type() == Qgis::CrsType::Geocentric )
95 {
96 QgsCoordinateTransform layerToLatLon( mLayer->crs(), geographicCrs, context.transformContext() );
97 layerToLatLon.setBallparkTransformsAreAppropriate( true );
98 try
99 {
100 layerExtentLonLat = layerToLatLon.transformBox3D( mLayer->extent3D() ).toRectangle();
101 }
102 catch ( const QgsCsException &e )
103 {
104 QgsDebugError( u"Error transforming layer extent to lat/lon: %1"_s.arg( e.what() ) );
105 }
106 }
107 else
108 {
109 layerExtentLonLat = Qgs3DUtils::tryReprojectExtent2D( mLayer->extent(), mLayer->crs(), geographicCrs, context.transformContext() );
110 }
111
112 if ( layerExtentLonLat.isValid() )
113 {
114 // add small padding to avoid clipping of point features located at the edge of the bounding box
115 layerExtentLonLat.grow( 0.01 );
116 layerExtentLonLat = layerExtentLonLat.intersect( QgsRectangle( -180, -90, 180, 90 ) );
117 }
118
119 mRootNodeId = QgsGlobeUtils::findSmallestIdContainingExtent( layerExtentLonLat );
120
121 QgsBox3D rootBox3D;
122 if ( mRootNodeId.d == 0 )
123 rootBox3D = QgsBox3D( -mRadius.x(), -mRadius.y(), -mRadius.z(), mRadius.x(), mRadius.y(), mRadius.z() );
124 else if ( mRootNodeId.d == 1 )
125 rootBox3D = mRootNodeId.x == 1 ? QgsBox3D( -mRadius.x(), 0, -mRadius.z(), mRadius.x(), mRadius.y(), mRadius.z() )
126 : QgsBox3D( -mRadius.x(), -mRadius.y(), -mRadius.z(), mRadius.x(), 0, mRadius.z() );
127 else
128 rootBox3D = QgsGlobeUtils::nodeIdToBox3D( mRootNodeId, mCrsToLatLon );
129
130 const float rootError = static_cast<float>( std::max<double>( rootBox3D.width(), rootBox3D.height() ) * QgsVectorLayer3DTilingSettings::tileGeometryErrorRatio() );
131 setupQuadtree( rootBox3D, rootError );
132 return;
133 }
134
135 QgsRectangle extent = context.extent();
136 const QgsRectangle layerExtentInMapCrs = Qgs3DUtils::tryReprojectExtent2D( mLayer->extent(), mLayer->crs(), context.crs(), context.transformContext() );
137 if ( layerExtentInMapCrs.isValid() )
138 {
139 extent = context.extent().intersect( layerExtentInMapCrs );
140 }
141 if ( extent.isValid() )
142 {
143 QgsBox3D rootBox3D( extent, zMin, zMax );
144 rootBox3D.grow( 1.0 );
145
146 const float rootError = static_cast<float>( std::max<double>( rootBox3D.width(), rootBox3D.height() ) * QgsVectorLayer3DTilingSettings::tileGeometryErrorRatio() );
147 setupQuadtree( rootBox3D, rootError );
148 }
149}
150
151QFuture<QgsChunkLoaderResult> QgsVectorLayerChunkLoader::loadChunk( QgsChunkNode *node )
152{
153 QgsFeature3DHandler *handlerPtr = QgsApplication::symbol3DRegistry()->createHandlerForSymbol( mLayer, mSymbol.get() );
154 if ( !handlerPtr )
155 {
156 QgsDebugError( u"Unknown 3D symbol type for vector layer: "_s + mSymbol->type() );
157 return QtFuture::makeReadyValueFuture( QgsChunkLoaderResult::sEmpty );
158 }
159 // Needs to be in shared_ptr instead of unique_ptr so it can be captured in copyable std::function
160 std::shared_ptr<QgsFeature3DHandler> handler( handlerPtr );
161
162 Qgs3DRenderContext renderCtx = mRenderContext; // Copy, since we mutate it locally per-chunk
163
164 QgsExpressionContext exprContext;
166 exprContext.setFields( mLayer->fields() );
167 renderCtx.setExpressionContext( exprContext );
168
169 QSet<QString> attributeNames;
170 if ( !handler->prepare( renderCtx, attributeNames, node->box3D() ) )
171 {
172 QgsDebugError( u"Failed to prepare 3D feature handler!"_s );
173 return QtFuture::makeReadyValueFuture( QgsChunkLoaderResult::sEmpty );
174 }
175
176 // build the feature request
177 // only a subset of data to be queried
179 req.setSubsetOfAttributes( attributeNames, mLayer->fields() );
180
181 QgsCoordinateTransform layerToRenderCrs;
182 if ( mIsGeocentric )
183 {
184 layerToRenderCrs = QgsCoordinateTransform( mLayer->crs3D(), renderCtx.crs(), renderCtx.transformContext() );
185 layerToRenderCrs.setBallparkTransformsAreAppropriate( true );
186
187 QgsRectangle filterRect;
188 if ( mLayer->crs().type() == Qgis::CrsType::Geocentric )
189 {
190 try
191 {
192 filterRect = layerToRenderCrs.transformBox3D( node->box3D(), Qgis::TransformDirection::Reverse ).toRectangle();
193 }
194 catch ( const QgsCsException & )
195 {
196 QgsDebugError( u"Error transforming node box3D to layer CRS"_s );
197 }
198 }
199 else
200 {
201 const QgsRectangle lonLatRect = QgsGlobeUtils::nodeIdToLonLatRect( node->tileId() );
202 filterRect = Qgs3DUtils::tryReprojectExtent2D( lonLatRect, mCrsToLatLon.destinationCrs(), mLayer->crs(), mRenderContext.transformContext() );
203 }
204 req.setFilterRect( filterRect );
205 }
206 else
207 {
208 req.setCoordinateTransform( QgsCoordinateTransform( mLayer->crs3D(), mRenderContext.crs(), mRenderContext.transformContext() ) );
209 req.setFilterRect( node->box3D().toRectangle() );
210 }
211
212 auto source = std::make_unique<QgsVectorLayerFeatureSource>( mLayer );
213
214 return QtConcurrent::
215 run( &QgsVectorLayerChunkLoader::loadChunkInWorker, std::move( handler ), std::move( renderCtx ), std::move( source ), std::move( req ), node, mMaxFeatures, mNodesAreLeafs, layerToRenderCrs, mIsGeocentric )
216 .then( this, [this, node]( ChunkData data ) { return QgsChunkLoaderResult { std::bind_front( &QgsVectorLayerChunkLoader::createEntity, this, node, data ) }; } );
217}
218
219void QgsVectorLayerChunkLoader::loadChunkInWorker(
220 QPromise<ChunkData> &promise,
221 std::shared_ptr<QgsFeature3DHandler> handler,
222 Qgs3DRenderContext renderCtx,
223 const std::unique_ptr<QgsVectorLayerFeatureSource> &source,
224 const QgsFeatureRequest &req,
225 QgsChunkNode *node,
226 int maxFeatures,
227 const std::shared_ptr<NodeIsLeafMap> &nodesAreLeafs,
228 QgsCoordinateTransform layerToRenderCrs,
229 bool isGeocentric
230)
231{
232 const QgsScopedEvent e( u"3D"_s, u"VL chunk load"_s );
233
234 QgsFeature f;
235 QgsFeatureIterator fi = source->getFeatures( req );
236 int featureCount = 0;
237 bool featureLimitReached = false;
238 while ( fi.nextFeature( f ) )
239 {
240 if ( promise.isCanceled() )
241 return;
242
243 if ( ++featureCount > maxFeatures )
244 {
245 featureLimitReached = true;
246 break;
247 }
248
249 if ( isGeocentric )
250 {
251 QgsGeometry g = f.geometry();
252 if ( !g.constGet()->is3D() )
253 g.get()->addZValue( 0 );
254
255 try
256 {
257 g.transform( layerToRenderCrs, Qgis::TransformDirection::Forward, true );
258 }
259 catch ( QgsCsException &e )
260 {
261 QgsDebugError( u"Error transforming feature %1 geometry to globe CRS: %2"_s.arg( f.id() ).arg( e.what() ) );
262 continue;
263 }
264 f.setGeometry( g );
265 }
266
267 renderCtx.expressionContext().setFeature( f );
268 handler->processFeature( f, renderCtx );
269 }
270
271 bool nodeIsLeaf = false;
272 if ( !featureLimitReached )
273 {
274 QgsDebugMsgLevel( u"All features fetched for node: %1"_s.arg( node->tileId().text() ), 3 );
275
276 if ( featureCount == 0 || std::max<double>( node->box3D().width(), node->box3D().height() ) < QgsVectorLayer3DTilingSettings::maximumLeafExtent() )
277 nodeIsLeaf = true;
278 }
279
280 {
281 QMutexLocker<QMutex> locker( &nodesAreLeafs->mutex );
282 nodesAreLeafs->map[node->tileId().text()] = nodeIsLeaf;
283 }
284
285 promise.addResult( ChunkData { .handler = std::move( handler ), .renderCtx = std::move( renderCtx ) } );
286}
287
288Qt3DCore::QEntity *QgsVectorLayerChunkLoader::createEntity( QgsChunkNode *node, ChunkData data, Qt3DCore::QEntity *parent )
289{
291 if ( data.handler->featureCount() == 0 )
292 {
293 // an empty node, so we return no entity. This tags the node as having no data and effectively removes it.
294 // we just make sure first that its initial estimated vertical range does not affect its parents' bboxes calculation
295 node->setExactBox3D( QgsBox3D() );
296 node->updateParentBoundingBoxesRecursively();
297 return nullptr;
298 }
299
300 Qt3DCore::QEntity *entity = new Qt3DCore::QEntity( parent );
301 entity->setObjectName( mLayer->name() + "_" + node->tileId().text() );
302 data.handler->finalize( entity, data.renderCtx );
303
304 // fix the vertical range of the node from the estimated vertical range to the true range
305 if ( data.handler->zMinimum() != std::numeric_limits<float>::max() && data.handler->zMaximum() != std::numeric_limits<float>::lowest() )
306 {
307 QgsBox3D box = node->box3D();
308 box.setZMinimum( data.handler->zMinimum() );
309 box.setZMaximum( data.handler->zMaximum() );
310 node->setExactBox3D( box );
311 node->updateParentBoundingBoxesRecursively();
312 }
313
314 return entity;
315}
316
317QgsChunkNode *QgsVectorLayerChunkLoader::createRootNode() const
318{
319 if ( mIsGeocentric )
320 return new QgsChunkNode( mRootNodeId, mRootBox3D, mRootError );
321
322 return QgsQuadtreeChunkLoader::createRootNode();
323}
324
325QFuture<QVector<QgsChunkNode *>> QgsVectorLayerChunkLoader::createChildren( QgsChunkNode *node )
326{
327 {
328 QMutexLocker locker( &mNodesAreLeafs->mutex );
329 if ( mNodesAreLeafs->map.value( node->tileId().text(), false ) )
330 return QtFuture::makeReadyValueFuture( QVector<QgsChunkNode *> {} );
331 }
332
333 if ( !mIsGeocentric )
334 return QgsQuadtreeChunkLoader::createChildren( node );
335
336 QVector<QgsChunkNode *> children;
337 if ( mMaxLevel != -1 && node->level() >= mMaxLevel )
338 return QtFuture::makeReadyValueFuture( children );
339
340 const QgsChunkNodeId nodeId = node->tileId();
341 const float childError = node->error() / 2;
342
343 if ( nodeId.d == 0 )
344 {
345 const QgsChunkNodeId westId( 1, 0, 0 );
346 const QgsChunkNodeId eastId( 1, 1, 0 );
347 children << new QgsChunkNode( westId, QgsBox3D( -mRadius.x(), -mRadius.y(), -mRadius.z(), mRadius.x(), 0, mRadius.z() ), childError, node );
348 children << new QgsChunkNode( eastId, QgsBox3D( -mRadius.x(), 0, -mRadius.z(), mRadius.x(), mRadius.y(), mRadius.z() ), childError, node );
349 return QtFuture::makeReadyValueFuture( children );
350 }
351
352 for ( int i = 0; i < 4; ++i )
353 {
354 const int dx = i & 1, dy = !!( i & 2 );
355 const QgsChunkNodeId childId( nodeId.d + 1, nodeId.x * 2 + dx, nodeId.y * 2 + dy );
356 children << new QgsChunkNode( childId, QgsGlobeUtils::nodeIdToBox3D( childId, mCrsToLatLon ), childError, node );
357 }
358 return QtFuture::makeReadyValueFuture( children );
359}
360
361
363
364
365QgsVectorLayerChunkedEntity::QgsVectorLayerChunkedEntity(
366 Qgs3DMapSettings *map, QgsVectorLayer *vl, double zMin, double zMax, const QgsVectorLayer3DTilingSettings &tilingSettings, QgsAbstract3DSymbol *symbol
367)
368 : QgsAbstractFeatureBasedChunkedEntity( map, 3, new QgsVectorLayerChunkLoader( Qgs3DRenderContext::fromMapSettings( map ), vl, symbol, zMin, zMax, tilingSettings.maximumChunkFeatures() ), true )
369{
370 onTerrainElevationOffsetChanged();
371 setShowBoundingBoxes( tilingSettings.showBoundingBoxes() );
372}
373
374QgsVectorLayerChunkedEntity::~QgsVectorLayerChunkedEntity()
375{
376 // cancel / wait for jobs
377 cancelActiveJobs();
378}
379
380// if the AltitudeClamping is `Absolute`, do not apply the offset
381bool QgsVectorLayerChunkedEntity::applyTerrainOffset() const
382{
383 QgsVectorLayerChunkLoader *loader = static_cast<QgsVectorLayerChunkLoader *>( mChunkLoader );
384 if ( loader )
385 {
386 QString symbolType = loader->mSymbol.get()->type();
387 if ( symbolType == "line" )
388 {
389 QgsLine3DSymbol *lineSymbol = static_cast<QgsLine3DSymbol *>( loader->mSymbol.get() );
390 if ( lineSymbol && lineSymbol->altitudeClamping() == Qgis::AltitudeClamping::Absolute )
391 {
392 return false;
393 }
394 }
395 else if ( symbolType == "point" )
396 {
397 QgsPoint3DSymbol *pointSymbol = static_cast<QgsPoint3DSymbol *>( loader->mSymbol.get() );
398 if ( pointSymbol && pointSymbol->altitudeClamping() == Qgis::AltitudeClamping::Absolute )
399 {
400 return false;
401 }
402 }
403 else if ( symbolType == "polygon" )
404 {
405 QgsPolygon3DSymbol *polygonSymbol = static_cast<QgsPolygon3DSymbol *>( loader->mSymbol.get() );
406 if ( polygonSymbol && polygonSymbol->altitudeClamping() == Qgis::AltitudeClamping::Absolute )
407 {
408 return false;
409 }
410 }
411 else
412 {
413 QgsDebugMsgLevel( u"QgsVectorLayerChunkedEntity::applyTerrainOffset, unhandled symbol type %1"_s.arg( symbolType ), 2 );
414 }
415 }
416
417 return true;
418}
419
@ Absolute
Elevation is taken directly from feature and is independent of terrain height (final elevation = feat...
Definition qgis.h:4246
@ Geocentric
Geocentric CRS.
Definition qgis.h:2516
@ Forward
Forward transform (from source to destination).
Definition qgis.h:2883
@ Reverse
Reverse/inverse transform (from destination to source).
Definition qgis.h:2884
Definition of the world.
Rendering context for preparation of 3D entities.
void setExpressionContext(const QgsExpressionContext &context)
Sets the expression context.
QgsCoordinateReferenceSystem crs() const
Returns the coordinate reference system used in the 3D scene.
QgsExpressionContext & expressionContext()
Gets the expression context.
QgsRectangle extent() const
Returns the 3D scene's 2D extent in the 3D scene's CRS.
QgsCoordinateTransformContext transformContext() const
Returns the coordinate transform context, which stores various information regarding which datum tran...
QgsFeature3DHandler * createHandlerForSymbol(const QgsVectorLayer *layer, const QgsAbstract3DSymbol *symbol)
Creates a feature handler for a symbol, for the specified vector layer.
static QgsRectangle tryReprojectExtent2D(const QgsRectangle &extent, const QgsCoordinateReferenceSystem &crs1, const QgsCoordinateReferenceSystem &crs2, const QgsCoordinateTransformContext &context)
Reprojects extent from crs1 to crs2 coordinate reference system with context context.
Abstract base class for 3D symbols that are used by VectorLayer3DRenderer objects.
virtual bool addZValue(double zValue=0)=0
Adds a z-dimension to the geometry, initialized to a preset value.
bool is3D() const
Returns true if the geometry is 3D and contains a z-value.
static Qgs3DSymbolRegistry * symbol3DRegistry()
Returns registry of available 3D symbols.
A 3-dimensional box composed of x, y, z coordinates.
Definition qgsbox3d.h:45
void setZMinimum(double z)
Sets the minimum z value.
Definition qgsbox3d.cpp:94
void setZMaximum(double z)
Sets the maximum z value.
Definition qgsbox3d.cpp:99
QgsRectangle toRectangle() const
Converts the box to a 2D rectangle.
Definition qgsbox3d.h:388
Represents a coordinate reference system (CRS).
QgsCoordinateReferenceSystem toGeographicCrs() const
Returns the geographic CRS associated with this CRS object.
Qgis::CrsType type() const
Returns the type of the CRS.
Handles coordinate transforms between two coordinate systems.
QgsBox3D transformBox3D(const QgsBox3D &box, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Transforms a 3D box from the source CRS to the destination CRS.
void setBallparkTransformsAreAppropriate(bool appropriate)
Sets whether approximate "ballpark" results are appropriate for this coordinate transform.
Custom exception class for Coordinate Reference System related exceptions.
QString what() const
static QList< QgsExpressionContextScope * > globalProjectLayerScopes(const QgsMapLayer *layer)
Creates a list of three scopes: global, layer's project and layer.
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
void setFeature(const QgsFeature &feature)
Convenience function for setting a feature for the context.
void setFields(const QgsFields &fields)
Convenience function for setting a fields for the context.
void appendScopes(const QList< QgsExpressionContextScope * > &scopes)
Appends a list of scopes to the end of the context.
Wrapper for iterator of features from vector data provider or vector layer.
bool nextFeature(QgsFeature &f)
Fetch next feature and stores in f, returns true on success.
Wraps a request for features to a vector layer (or directly its vector data provider).
QgsFeatureRequest & setCoordinateTransform(const QgsCoordinateTransform &transform)
Sets the coordinate transform which will be used to transform the feature's geometries.
QgsFeatureRequest & setSubsetOfAttributes(const QgsAttributeList &attrs)
Set a subset of attributes that will be fetched.
QgsFeatureRequest & setFilterRect(const QgsRectangle &rectangle)
Sets the rectangle from which features will be taken.
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
Definition qgsfeature.h:60
QgsFeatureId id
Definition qgsfeature.h:63
QgsGeometry geometry
Definition qgsfeature.h:66
void setGeometry(const QgsGeometry &geometry)
Set the feature's geometry.
A geometry is the spatial representation of a feature.
Qgis::GeometryOperationResult transform(const QgsCoordinateTransform &ct, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward, bool transformZ=false)
Transforms this geometry as described by the coordinate transform ct.
QgsAbstractGeometry * get()
Returns a modifiable (non-const) reference to the underlying abstract geometry primitive.
const QgsAbstractGeometry * constGet() const
Returns a non-modifiable (const) reference to the underlying abstract geometry primitive.
3D symbol that draws linestring geometries as planar polygons (created from lines using a buffer with...
Qgis::AltitudeClamping altitudeClamping() const
Returns method that determines altitude (whether to clamp to feature to terrain).
3D symbol that draws point geometries as 3D objects using one of the predefined shapes.
Qgis::AltitudeClamping altitudeClamping() const
Returns method that determines altitude (whether to clamp to feature to terrain).
3D symbol that draws polygon geometries as planar polygons, optionally extruded (with added walls).
Qgis::AltitudeClamping altitudeClamping() const
Returns method that determines altitude (whether to clamp to feature to terrain).
A rectangle specified with double values.
QgsRectangle intersect(const QgsRectangle &rect) const
Returns the intersection with the given rectangle.
Defines configuration of how a vector layer gets tiled for 3D rendering.
static double tileGeometryErrorRatio()
This is the ratio of tile's largest size to geometry error and is used when setting the root tile's e...
bool showBoundingBoxes() const
Returns whether to display bounding boxes of entity's tiles (for debugging).
static double maximumLeafExtent()
This is the maximum width or height a tile can have and still be considered a leaf node.
Represents a vector layer which manages a vector based dataset.
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:80
#define QgsDebugError(str)
Definition qgslogger.h:71
#define QGIS_CHECK_MAIN_THREAD_ACCESS