QGIS API Documentation 4.1.0-Master (9af12b5a203)
Loading...
Searching...
No Matches
qgscategorizedchunkloader_p.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgscategorizedchunkloader_p.cpp
3 --------------------------------------
4 Date : November 2025
5 Copyright : (C) 2025 by Jean Felder
6 Email : jean dot felder at oslandia 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 "qgs3dsymbolregistry.h"
19#include "qgs3dutils.h"
21#include "qgsapplication.h"
24#include "qgschunknode.h"
25#include "qgseventtracing.h"
28#include "qgsfields.h"
29#include "qgsline3dsymbol.h"
30#include "qgspoint3dsymbol.h"
31#include "qgspolygon3dsymbol.h"
32#include "qgsvectorlayer.h"
35
36#include <QString>
37#include <Qt3DCore/QTransform>
38#include <QtConcurrentRun>
39
40#include "moc_qgscategorizedchunkloader_p.cpp"
41
42using namespace Qt::StringLiterals;
43
45
46
47QgsCategorizedChunkLoader::QgsCategorizedChunkLoader( const QgsCategorizedChunkLoaderFactory *factory, QgsChunkNode *node )
48 : QgsChunkLoader( node )
49 , mFactory( factory )
50 , mContext( factory->mRenderContext )
51 , mSource( new QgsVectorLayerFeatureSource( factory->mLayer ) )
52{}
53
54const QSet<QString> QgsCategorizedChunkLoader::prepareHandlers( const QgsBox3D &chunkExtent )
55{
56 const QgsVectorLayer *layer = mFactory->mLayer;
57 const QString attributeName = mFactory->mAttributeName;
58
59 QSet<QString> attributesNames;
60
61 // prepare the expression
62 mAttributeIdx = layer->fields().lookupField( attributeName );
63 if ( mAttributeIdx == -1 )
64 {
65 mExpression.reset( new QgsExpression( attributeName ) );
66 mExpression->prepare( &mContext.expressionContext() );
67 }
68
69 // build features hash
70 mHandlers.clear();
71 mFeaturesHandlerHash.clear();
72
73 for ( const Qgs3DRendererCategory &category : *mFactory->mCategories )
74 {
75 if ( !category.renderState() )
76 {
77 continue;
78 }
79
80 const QVariant value = category.value();
81 std::unique_ptr<QgsFeature3DHandler> handler( QgsApplication::symbol3DRegistry()->createHandlerForSymbol( layer, category.symbol() ) );
82 mHandlers.push_back( std::move( handler ) );
83 QgsFeature3DHandler *handlerPtr = mHandlers.back().get();
84 if ( value.userType() == QMetaType::Type::QVariantList )
85 {
86 const QVariantList variantList = value.toList();
87 for ( const QVariant &listElt : variantList )
88 {
89 mFeaturesHandlerHash.insert( listElt.toString(), handlerPtr );
90 }
91 }
92 else
93 {
94 mFeaturesHandlerHash.insert( value.toString(), handlerPtr );
95 }
96
97 handlerPtr->prepare( mContext, attributesNames, chunkExtent );
98 }
99
100 attributesNames.insert( attributeName );
101 return attributesNames;
102}
103
104void QgsCategorizedChunkLoader::processFeature( const QgsFeature &feature ) const
105{
106 // Get Value for feature
107 QgsAttributes attributes = feature.attributes();
108 QVariant value;
109 if ( mAttributeIdx == -1 )
110 {
111 Q_ASSERT( mExpression );
112 value = mExpression->evaluate( &mContext.expressionContext() );
113 }
114 else
115 {
116 value = attributes.value( mAttributeIdx );
117 }
118
119 auto handlerIt = mFeaturesHandlerHash.constFind( QgsVariantUtils::isNull( value ) ? QString() : value.toString() );
120 if ( handlerIt == mFeaturesHandlerHash.constEnd() )
121 {
122 if ( mFeaturesHandlerHash.isEmpty() )
123 {
124 QgsDebugError( u"There are no hashed symbols!"_s );
125 }
126 else
127 {
128 QgsDebugMsgLevel( u"Attribute value not found: %1"_s.arg( value.toString() ), 3 );
129 }
130 return;
131 }
132
133 QgsFeature3DHandler *handler = *handlerIt;
134 handler->processFeature( feature, mContext );
135}
136
137QString QgsCategorizedChunkLoader::filter() const
138{
139 const QgsVectorLayer *layer = mFactory->mLayer;
140 return QgsCategorizedSymbolUtils<QgsCategorized3DRenderer>::buildCategorizedFilter( mFactory->mAttributeName, layer->fields(), *mFactory->mCategories );
141}
142
143void QgsCategorizedChunkLoader::start()
144{
145 QgsChunkNode *node = chunk();
146
147 const QgsVectorLayer *layer = mFactory->mLayer;
148
149 QgsExpressionContext exprContext;
151 exprContext.setFields( layer->fields() );
152 mContext.setExpressionContext( exprContext );
153
154 // build the feature handlers
155 const QSet<QString> attributesNames = prepareHandlers( node->box3D() );
156
157 // build the feature request
158 // only a subset of data to be queried
159 const QgsRectangle rect = node->box3D().toRectangle();
160 QgsFeatureRequest request;
161 request.setDestinationCrs( mContext.crs(), mContext.transformContext() );
162 request.setSubsetOfAttributes( attributesNames, layer->fields() );
163 request.setFilterRect( rect );
164
165 const QString rendererFilter = filter();
166 if ( !rendererFilter.isEmpty() )
167 {
168 request.setFilterExpression( rendererFilter );
169 }
170
171 //
172 // this will be run in a background thread
173 //
174 mFutureWatcher = new QFutureWatcher<void>( this );
175
176 connect( mFutureWatcher, &QFutureWatcher<void>::finished, this, [this] {
177 if ( !mCanceled )
178 mFactory->mNodesAreLeafs[mNode->tileId().text()] = mNodeIsLeaf;
179 } );
180
181 connect( mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsChunkQueueJob::finished );
182
183 const QFuture<void> future = QtConcurrent::run( [request, this] {
184 const QgsEventTracing::ScopedEvent event( u"3D"_s, u"Categorized chunk load"_s );
185 QgsFeature feature;
186 QgsFeatureIterator featureIt = mSource->getFeatures( request );
187 int featureCount = 0;
188 bool featureLimitReached = false;
189 while ( featureIt.nextFeature( feature ) )
190 {
191 if ( mCanceled )
192 {
193 break;
194 }
195
196 if ( ++featureCount > mFactory->mMaxFeatures )
197 {
198 featureLimitReached = true;
199 break;
200 }
201
202 mContext.expressionContext().setFeature( feature );
203 processFeature( feature );
204 }
205 if ( !featureLimitReached )
206 {
207 QgsDebugMsgLevel( u"All features fetched for node: %1"_s.arg( mNode->tileId().text() ), 3 );
208
209 if ( featureCount == 0 || std::max<double>( mNode->box3D().width(), mNode->box3D().height() ) < QgsVectorLayer3DTilingSettings::maximumLeafExtent() )
210 mNodeIsLeaf = true;
211 }
212 } );
213
214 // emit finished() as soon as the handler is populated with features
215 mFutureWatcher->setFuture( future );
216}
217
218QgsCategorizedChunkLoader::~QgsCategorizedChunkLoader()
219{
220 if ( mFutureWatcher && !mFutureWatcher->isFinished() )
221 {
222 disconnect( mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsChunkQueueJob::finished );
223 mFutureWatcher->waitForFinished();
224 }
225}
226
227void QgsCategorizedChunkLoader::cancel()
228{
229 mCanceled = true;
230}
231
232Qt3DCore::QEntity *QgsCategorizedChunkLoader::createEntity( Qt3DCore::QEntity *parent )
233{
234 long long featureCount = 0;
235 for ( const auto &featureHandler : mHandlers )
236 {
237 featureCount += featureHandler->featureCount();
238 }
239 if ( featureCount == 0 )
240 {
241 // an empty node, so we return no entity. This tags the node as having no data and effectively removes it.
242 return nullptr;
243 }
244
245 Qt3DCore::QEntity *entity = new Qt3DCore::QEntity( parent );
246 float zMin = std::numeric_limits<float>::max();
247 float zMax = std::numeric_limits<float>::lowest();
248 for ( const auto &featureHandler : mHandlers )
249 {
250 featureHandler->finalize( entity, mContext );
251 if ( featureHandler->zMinimum() < zMin )
252 {
253 zMin = featureHandler->zMinimum();
254 }
255 if ( featureHandler->zMaximum() > zMax )
256 {
257 zMax = featureHandler->zMaximum();
258 }
259 }
260
261 // fix the vertical range of the node from the estimated vertical range to the true range
262 if ( zMin != std::numeric_limits<float>::max() && zMax != std::numeric_limits<float>::lowest() )
263 {
264 QgsBox3D box = mNode->box3D();
265 box.setZMinimum( zMin );
266 box.setZMaximum( zMax );
267 mNode->setExactBox3D( box );
268 mNode->updateParentBoundingBoxesRecursively();
269 }
270
271 return entity;
272}
273
274
276
277
278QgsCategorizedChunkLoaderFactory::QgsCategorizedChunkLoaderFactory(
279 const Qgs3DRenderContext &context, QgsVectorLayer *vectorLayer, const QgsCategorized3DRenderer *renderer, double zMin, double zMax, int maxFeatures
280)
281 : mRenderContext( context )
282 , mLayer( vectorLayer )
283 , mCategories( &renderer->categories() )
284 , mAttributeName( renderer->classAttribute() )
285 , mMaxFeatures( maxFeatures )
286{
287 if ( context.crs().type() == Qgis::CrsType::Geocentric )
288 {
289 // TODO: add support for handling of vector layers
290 // (we're using dummy quadtree here to make sure the empty extent does not break the scene completely)
291 QgsDebugError( u"Vector layers in globe scenes are not supported yet!"_s );
292 setupQuadtree( QgsBox3D( -7e6, -7e6, -7e6, 7e6, 7e6, 7e6 ), -1, 3 );
293 return;
294 }
295
296 // choose the smaller root extent between context and mLayer ones:
297 QgsRectangle extent = context.extent();
298 if ( context.extent().contains( mLayer->extent() ) )
299 {
300 extent = mLayer->extent();
301 }
302 QgsBox3D rootBox3D( extent, zMin, zMax );
303
304 // add small padding to avoid clipping of point features located at the edge of the bounding box
305 rootBox3D.grow( 1.0 );
306
307 const float rootError = static_cast<float>( std::max<double>( rootBox3D.width(), rootBox3D.height() ) * QgsVectorLayer3DTilingSettings::tileGeometryErrorRatio() );
308 setupQuadtree( rootBox3D, rootError );
309}
310
311QgsCategorizedChunkLoaderFactory::~QgsCategorizedChunkLoaderFactory() = default;
312
313QgsChunkLoader *QgsCategorizedChunkLoaderFactory::createChunkLoader( QgsChunkNode *node ) const
314{
315 return new QgsCategorizedChunkLoader( this, node );
316}
317
318bool QgsCategorizedChunkLoaderFactory::canCreateChildren( QgsChunkNode *node )
319{
320 return mNodesAreLeafs.contains( node->tileId().text() );
321}
322
323QVector<QgsChunkNode *> QgsCategorizedChunkLoaderFactory::createChildren( QgsChunkNode *node ) const
324{
325 if ( mNodesAreLeafs.value( node->tileId().text(), false ) )
326 return {};
327
328 return QgsQuadtreeChunkLoaderFactory::createChildren( node );
329}
330
331
333
334QgsCategorizedChunkedEntity::QgsCategorizedChunkedEntity(
335 Qgs3DMapSettings *mapSettings, QgsVectorLayer *vectorLayer, double zMin, double zMax, const QgsVectorLayer3DTilingSettings &tilingSettings, const QgsCategorized3DRenderer *renderer
336)
337 : QgsAbstractFeatureBasedChunkedEntity(
338 mapSettings,
339 -1, // max. allowed screen error (negative tau means that we need to go until leaves are reached)
340 new QgsCategorizedChunkLoaderFactory( Qgs3DRenderContext::fromMapSettings( mapSettings ), vectorLayer, renderer, zMin, zMax, tilingSettings.maximumChunkFeatures() ),
341 true
342 )
343{
344 onTerrainElevationOffsetChanged();
345 setShowBoundingBoxes( tilingSettings.showBoundingBoxes() );
346}
347
348QgsCategorizedChunkedEntity::~QgsCategorizedChunkedEntity()
349{
350 // cancel / wait for jobs
351 cancelActiveJobs();
352}
353
354// if the AltitudeClamping is `Absolute`, do not apply the offset
355bool QgsCategorizedChunkedEntity::applyTerrainOffset() const
356{
357 QgsCategorizedChunkLoaderFactory *loaderFactory = static_cast<QgsCategorizedChunkLoaderFactory *>( mChunkLoaderFactory );
358 if ( loaderFactory )
359 {
360 for ( const auto &category : *loaderFactory->mCategories )
361 {
362 const QgsAbstract3DSymbol *symbol = category.symbol();
363 if ( category.symbol() )
364 {
365 QString symbolType = symbol->type();
366 if ( symbolType == "line" )
367 {
368 const QgsLine3DSymbol *lineSymbol = static_cast<const QgsLine3DSymbol *>( symbol );
369 if ( lineSymbol && lineSymbol->altitudeClamping() == Qgis::AltitudeClamping::Absolute )
370 {
371 return false;
372 }
373 }
374 else if ( symbolType == "point" )
375 {
376 const QgsPoint3DSymbol *pointSymbol = static_cast<const QgsPoint3DSymbol *>( symbol );
377 if ( pointSymbol && pointSymbol->altitudeClamping() == Qgis::AltitudeClamping::Absolute )
378 {
379 return false;
380 }
381 }
382 else if ( symbolType == "polygon" )
383 {
384 const QgsPolygon3DSymbol *polygonSymbol = static_cast<const QgsPolygon3DSymbol *>( symbol );
385 if ( polygonSymbol && polygonSymbol->altitudeClamping() == Qgis::AltitudeClamping::Absolute )
386 {
387 return false;
388 }
389 }
390 else
391 {
392 QgsDebugMsgLevel( u"QgsRuleBasedChunkedEntityChunkedEntity::applyTerrainOffset, unhandled symbol type %1"_s.arg( symbolType ), 2 );
393 }
394 }
395 }
396 }
397
398 return true;
399}
400
@ Absolute
Elevation is taken directly from feature and is independent of terrain height (final elevation = feat...
Definition qgis.h:4167
@ Geocentric
Geocentric CRS.
Definition qgis.h:2465
Definition of the world.
Rendering context for preparation of 3D entities.
QgsCoordinateReferenceSystem crs() const
Returns the coordinate reference system used in the 3D scene.
QgsRectangle extent() const
Returns the 3D scene's 2D extent in the 3D scene's CRS.
Represents an individual category (class) from a QgsCategorized3DRenderer.
const QVariant value() const
Returns the value corresponding to this category.
bool renderState() const
Returns true if the category is currently enabled and should be rendered.
QgsAbstract3DSymbol * symbol() const
Returns the symbol which will be used to render this category.
Abstract base class for 3D symbols that are used by VectorLayer3DRenderer objects.
virtual QString type() const =0
Returns identifier of symbol type. Each 3D symbol implementation should return a different type.
static Qgs3DSymbolRegistry * symbol3DRegistry()
Returns registry of available 3D symbols.
A vector of attributes.
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
void grow(double delta)
Grows the box in place by the specified amount in all dimensions.
Definition qgsbox3d.cpp:302
static QString buildCategorizedFilter(const QString &attributeName, const QgsFields &fields, const Categories &categories)
Builds a filter expression from categories.
Qgis::CrsType type() const
Returns the type of the CRS.
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 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.
Handles parsing and evaluation of expressions (formerly called "search strings").
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 & setSubsetOfAttributes(const QgsAttributeList &attrs)
Set a subset of attributes that will be fetched.
QgsFeatureRequest & setDestinationCrs(const QgsCoordinateReferenceSystem &crs, const QgsCoordinateTransformContext &context)
Sets the destination crs for feature's geometries.
QgsFeatureRequest & setFilterExpression(const QString &expression)
Set the filter expression.
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
QgsAttributes attributes
Definition qgsfeature.h:69
Q_INVOKABLE int lookupField(const QString &fieldName) const
Looks up field's index from the field name.
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.
bool contains(const QgsRectangle &rect) const
Returns true when rectangle contains other rectangle.
static bool isNull(const QVariant &variant, bool silenceNullWarnings=false)
Returns true if the specified variant should be considered a NULL value.
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.
Partial snapshot of vector layer's state (only the members necessary for access to features).
Represents a vector layer which manages a vector based dataset.
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:63
#define QgsDebugError(str)
Definition qgslogger.h:59